mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 19:44:35 +00:00
fix(web): use mui treeView instead of custom implementation
This commit is contained in:
@@ -31,14 +31,24 @@ const DeleteConfirmationModal = ({
|
|||||||
message,
|
message,
|
||||||
_delete
|
_delete
|
||||||
}: DeleteConfirmationModalProps) => {
|
}: DeleteConfirmationModalProps) => {
|
||||||
|
const handleDeleteClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
_delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog onClose={() => setOpen(false)} open={open}>
|
<BootstrapDialog onClose={handleClose} open={open}>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography gutterBottom>{message}</Typography>
|
<Typography gutterBottom>{message}</Typography>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
<Button color="error" onClick={() => _delete()}>
|
<Button color="error" onClick={handleDeleteClick}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -69,8 +69,18 @@ const NameInputModal = ({
|
|||||||
action(name)
|
action(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleActionClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
action(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
<BootstrapDialog fullWidth onClose={handleClose} open={open}>
|
||||||
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||||
{title}
|
{title}
|
||||||
</BootstrapDialogTitle>
|
</BootstrapDialogTitle>
|
||||||
@@ -91,12 +101,12 @@ const NameInputModal = ({
|
|||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="contained" onClick={() => setOpen(false)}>
|
<Button variant="contained" onClick={handleClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => action(name)}
|
onClick={handleActionClick}
|
||||||
disabled={hasError || !name}
|
disabled={hasError || !name}
|
||||||
>
|
>
|
||||||
{actionLabel}
|
{actionLabel}
|
||||||
|
|||||||
@@ -1,67 +1,79 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Menu, MenuItem } from '@mui/material'
|
import { Menu, MenuItem, Typography } from '@mui/material'
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||||
|
import MuiTreeView from '@mui/lab/TreeView'
|
||||||
|
import MuiTreeItem from '@mui/lab/TreeItem'
|
||||||
|
|
||||||
import DeleteConfirmationModal from './deleteConfirmationModal'
|
import DeleteConfirmationModal from './deleteConfirmationModal'
|
||||||
import NameInputModal from './nameInputModal'
|
import NameInputModal from './nameInputModal'
|
||||||
|
|
||||||
import { TreeNode } from '../utils/types'
|
import { TreeNode } from '../utils/types'
|
||||||
|
|
||||||
type Props = {
|
interface Props {
|
||||||
node: TreeNode
|
node: TreeNode
|
||||||
selectedFilePath: string
|
|
||||||
handleSelect: (filePath: string) => void
|
handleSelect: (filePath: string) => void
|
||||||
deleteNode: (path: string, isFolder: boolean) => void
|
deleteNode: (path: string, isFolder: boolean) => void
|
||||||
addFile: (path: string) => void
|
addFile: (path: string) => void
|
||||||
addFolder: (path: string) => void
|
addFolder: (path: string) => void
|
||||||
rename: (oldPath: string, newPath: string) => void
|
rename: (oldPath: string, newPath: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TreeViewProps extends Props {
|
||||||
defaultExpanded?: string[]
|
defaultExpanded?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const TreeView = ({
|
const TreeView = ({
|
||||||
node,
|
node,
|
||||||
selectedFilePath,
|
|
||||||
handleSelect,
|
handleSelect,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
addFile,
|
addFile,
|
||||||
addFolder,
|
addFolder,
|
||||||
rename,
|
rename,
|
||||||
defaultExpanded
|
defaultExpanded
|
||||||
}: Props) => {
|
}: TreeViewProps) => {
|
||||||
return (
|
const renderTree = (nodes: TreeNode) => (
|
||||||
<ul
|
<MuiTreeItem
|
||||||
style={{
|
key={nodes.relativePath}
|
||||||
listStyle: 'none',
|
nodeId={nodes.relativePath}
|
||||||
padding: '0.25rem 0.85rem',
|
label={
|
||||||
width: 'max-content'
|
<TreeItemWithContextMenu
|
||||||
}}
|
node={nodes}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
deleteNode={deleteNode}
|
||||||
|
addFile={addFile}
|
||||||
|
addFolder={addFolder}
|
||||||
|
rename={rename}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<TreeViewNode
|
{Array.isArray(nodes.children)
|
||||||
node={node}
|
? nodes.children.map((node) => renderTree(node))
|
||||||
selectedFilePath={selectedFilePath}
|
: null}
|
||||||
handleSelect={handleSelect}
|
</MuiTreeItem>
|
||||||
deleteNode={deleteNode}
|
)
|
||||||
addFile={addFile}
|
|
||||||
addFolder={addFolder}
|
return (
|
||||||
rename={rename}
|
<MuiTreeView
|
||||||
defaultExpanded={defaultExpanded}
|
defaultCollapseIcon={<ExpandMoreIcon />}
|
||||||
/>
|
defaultExpandIcon={<ChevronRightIcon />}
|
||||||
</ul>
|
defaultExpanded={defaultExpanded}
|
||||||
|
sx={{ flexGrow: 1, maxWidth: 400, overflowY: 'auto' }}
|
||||||
|
>
|
||||||
|
{renderTree(node)}
|
||||||
|
</MuiTreeView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TreeView
|
export default TreeView
|
||||||
|
|
||||||
const TreeViewNode = ({
|
const TreeItemWithContextMenu = ({
|
||||||
node,
|
node,
|
||||||
selectedFilePath,
|
|
||||||
handleSelect,
|
handleSelect,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
addFile,
|
addFile,
|
||||||
addFolder,
|
addFolder,
|
||||||
rename,
|
rename
|
||||||
defaultExpanded
|
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||||
useState(false)
|
useState(false)
|
||||||
@@ -72,18 +84,19 @@ const TreeViewNode = ({
|
|||||||
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
const [nameInputModalTitle, setNameInputModalTitle] = useState('')
|
||||||
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
const [nameInputModalActionLabel, setNameInputModalActionLabel] = useState('')
|
||||||
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
||||||
const [childVisible, setChildVisibility] = useState(false)
|
|
||||||
const [contextMenu, setContextMenu] = useState<{
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
mouseX: number
|
mouseX: number
|
||||||
mouseY: number
|
mouseY: number
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const launchProgram = () => {
|
const launchProgram = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchProgramWithDebug = () => {
|
const launchProgramWithDebug = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
window.open(
|
window.open(
|
||||||
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
|
||||||
@@ -103,25 +116,18 @@ const TreeViewNode = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasChild = node.children.length ? true : false
|
const handleClose = (event: any) => {
|
||||||
|
event.stopPropagation()
|
||||||
const handleItemClick = () => {
|
setContextMenu(null)
|
||||||
if (node.children.length) {
|
}
|
||||||
setChildVisibility((v) => !v)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const handleItemClick = (event: React.MouseEvent) => {
|
||||||
|
if (node.children.length) return
|
||||||
handleSelect(node.relativePath)
|
handleSelect(node.relativePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const handleDeleteItemClick = (event: React.MouseEvent) => {
|
||||||
if (defaultExpanded && defaultExpanded[0] === node.relativePath) {
|
event.stopPropagation()
|
||||||
setChildVisibility(true)
|
|
||||||
defaultExpanded.shift()
|
|
||||||
}
|
|
||||||
}, [defaultExpanded, node.relativePath])
|
|
||||||
|
|
||||||
const handleDeleteItemClick = () => {
|
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setDeleteConfirmationModalOpen(true)
|
setDeleteConfirmationModalOpen(true)
|
||||||
setDeleteConfirmationModalMessage(
|
setDeleteConfirmationModalMessage(
|
||||||
@@ -136,7 +142,8 @@ const TreeViewNode = ({
|
|||||||
deleteNode(node.relativePath, node.isFolder)
|
deleteNode(node.relativePath, node.isFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewFolderItemClick = () => {
|
const handleNewFolderItemClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setNameInputModalOpen(true)
|
setNameInputModalOpen(true)
|
||||||
setNameInputModalTitle('Add Folder')
|
setNameInputModalTitle('Add Folder')
|
||||||
@@ -145,7 +152,8 @@ const TreeViewNode = ({
|
|||||||
setDefaultInputModalName('')
|
setDefaultInputModalName('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewFileItemClick = () => {
|
const handleNewFileItemClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setNameInputModalOpen(true)
|
setNameInputModalOpen(true)
|
||||||
setNameInputModalTitle('Add File')
|
setNameInputModalTitle('Add File')
|
||||||
@@ -161,7 +169,8 @@ const TreeViewNode = ({
|
|||||||
else addFile(path)
|
else addFile(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRenameItemClick = () => {
|
const handleRenameItemClick = (event: React.MouseEvent) => {
|
||||||
|
event.stopPropagation()
|
||||||
setContextMenu(null)
|
setContextMenu(null)
|
||||||
setNameInputModalOpen(true)
|
setNameInputModalOpen(true)
|
||||||
setNameInputModalTitle('Rename')
|
setNameInputModalTitle('Rename')
|
||||||
@@ -181,34 +190,7 @@ const TreeViewNode = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
||||||
<li style={{ display: 'list-item' }}>
|
<Typography onClick={handleItemClick}>{node.name}</Typography>
|
||||||
<div
|
|
||||||
className={`tree-item-label ${
|
|
||||||
selectedFilePath === node.relativePath ? 'selected' : ''
|
|
||||||
}`}
|
|
||||||
onClick={() => handleItemClick()}
|
|
||||||
>
|
|
||||||
{hasChild &&
|
|
||||||
(childVisible ? <ExpandMoreIcon /> : <ChevronRightIcon />)}
|
|
||||||
<div>{node.name}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasChild &&
|
|
||||||
childVisible &&
|
|
||||||
node.children.map((child, index) => (
|
|
||||||
<TreeView
|
|
||||||
key={node.relativePath + '-' + index}
|
|
||||||
node={child}
|
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
handleSelect={handleSelect}
|
|
||||||
deleteNode={deleteNode}
|
|
||||||
addFile={addFile}
|
|
||||||
addFolder={addFolder}
|
|
||||||
rename={rename}
|
|
||||||
defaultExpanded={defaultExpanded}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</li>
|
|
||||||
<DeleteConfirmationModal
|
<DeleteConfirmationModal
|
||||||
open={deleteConfirmationModalOpen}
|
open={deleteConfirmationModalOpen}
|
||||||
setOpen={setDeleteConfirmationModalOpen}
|
setOpen={setDeleteConfirmationModalOpen}
|
||||||
@@ -228,7 +210,7 @@ const TreeViewNode = ({
|
|||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
open={contextMenu !== null}
|
open={contextMenu !== null}
|
||||||
onClose={() => setContextMenu(null)}
|
onClose={handleClose}
|
||||||
anchorReference="anchorPosition"
|
anchorReference="anchorPosition"
|
||||||
anchorPosition={
|
anchorPosition={
|
||||||
contextMenu !== null
|
contextMenu !== null
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ const SideBar = ({
|
|||||||
{directoryData && (
|
{directoryData && (
|
||||||
<TreeView
|
<TreeView
|
||||||
node={directoryData}
|
node={directoryData}
|
||||||
selectedFilePath={selectedFilePath}
|
|
||||||
handleSelect={handleFileSelect}
|
handleSelect={handleFileSelect}
|
||||||
deleteNode={deleteNode}
|
deleteNode={deleteNode}
|
||||||
addFile={addFile}
|
addFile={addFile}
|
||||||
|
|||||||
Reference in New Issue
Block a user