mirror of
https://github.com/sasjs/server.git
synced 2025-12-12 03:54:34 +00:00
feat: implemented functionality for adding file/folder from sidebar context menu
This commit is contained in:
82
web/src/components/nameInputModal.tsx
Normal file
82
web/src/components/nameInputModal.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import { Button, DialogActions, DialogContent, TextField } from '@mui/material'
|
||||||
|
|
||||||
|
import { BootstrapDialogTitle } from './dialogTitle'
|
||||||
|
import { BootstrapDialog } from './modal'
|
||||||
|
|
||||||
|
type NameInputModalProps = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
isFolder: boolean
|
||||||
|
add: (name: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const NameInputModal = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
isFolder,
|
||||||
|
add
|
||||||
|
}: NameInputModalProps) => {
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [hasError, setHasError] = useState(false)
|
||||||
|
const [errorText, setErrorText] = useState('')
|
||||||
|
|
||||||
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value
|
||||||
|
|
||||||
|
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
|
||||||
|
const fileNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>/?~]/
|
||||||
|
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
|
const specialChars = isFolder ? folderNameRegex : fileNameRegex
|
||||||
|
|
||||||
|
if (specialChars.test(value)) {
|
||||||
|
setHasError(true)
|
||||||
|
setErrorText('can not have special characters')
|
||||||
|
} else if (!isFolder && fileNameExtensionRegex.test(value)) {
|
||||||
|
setHasError(true)
|
||||||
|
setErrorText('can not add file with extensions [exe, sh, htaccess]')
|
||||||
|
} else {
|
||||||
|
setHasError(false)
|
||||||
|
setErrorText('')
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BootstrapDialog fullWidth onClose={() => setOpen(false)} open={open}>
|
||||||
|
<BootstrapDialogTitle id="abort-modal" handleOpen={setOpen}>
|
||||||
|
{isFolder ? 'Add Folder' : 'Add File'}
|
||||||
|
</BootstrapDialogTitle>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
label={isFolder ? 'Folder Name' : 'File Name'}
|
||||||
|
value={name}
|
||||||
|
onChange={handleChange}
|
||||||
|
error={hasError}
|
||||||
|
helperText={errorText}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="contained" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
add(name)
|
||||||
|
}}
|
||||||
|
disabled={hasError || !name}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</BootstrapDialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NameInputModal
|
||||||
@@ -4,6 +4,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
|||||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
|
||||||
|
|
||||||
import DeleteConfirmationModal from './deleteConfirmationModal'
|
import DeleteConfirmationModal from './deleteConfirmationModal'
|
||||||
|
import NameInputModal from './nameInputModal'
|
||||||
|
|
||||||
import { TreeNode } from '../utils/types'
|
import { TreeNode } from '../utils/types'
|
||||||
|
|
||||||
@@ -12,6 +13,8 @@ type Props = {
|
|||||||
selectedFilePath: string
|
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
|
||||||
|
addFolder: (path: string) => void
|
||||||
defaultExpanded?: string[]
|
defaultExpanded?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,6 +23,8 @@ const TreeView = ({
|
|||||||
selectedFilePath,
|
selectedFilePath,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
|
addFile,
|
||||||
|
addFolder,
|
||||||
defaultExpanded
|
defaultExpanded
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
return (
|
return (
|
||||||
@@ -35,6 +40,8 @@ const TreeView = ({
|
|||||||
selectedFilePath={selectedFilePath}
|
selectedFilePath={selectedFilePath}
|
||||||
handleSelect={handleSelect}
|
handleSelect={handleSelect}
|
||||||
deleteNode={deleteNode}
|
deleteNode={deleteNode}
|
||||||
|
addFile={addFile}
|
||||||
|
addFolder={addFolder}
|
||||||
defaultExpanded={defaultExpanded}
|
defaultExpanded={defaultExpanded}
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -48,12 +55,16 @@ const TreeViewNode = ({
|
|||||||
selectedFilePath,
|
selectedFilePath,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
|
addFile,
|
||||||
|
addFolder,
|
||||||
defaultExpanded
|
defaultExpanded
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||||
useState(false)
|
useState(false)
|
||||||
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
||||||
useState('')
|
useState('')
|
||||||
|
const [nameInputModalOpen, setNameInputModalOpen] = useState(false)
|
||||||
|
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
||||||
const [childVisible, setChildVisibility] = useState(false)
|
const [childVisible, setChildVisibility] = useState(false)
|
||||||
const [contextMenu, setContextMenu] = useState<{
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
mouseX: number
|
mouseX: number
|
||||||
@@ -108,6 +119,25 @@ const TreeViewNode = ({
|
|||||||
deleteNode(node.relativePath, node.isFolder)
|
deleteNode(node.relativePath, node.isFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleNewFolderItemClick = () => {
|
||||||
|
setContextMenu(null)
|
||||||
|
setNameInputModalOpen(true)
|
||||||
|
setNameInputModalForFolder(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewFileItemClick = () => {
|
||||||
|
setContextMenu(null)
|
||||||
|
setNameInputModalOpen(true)
|
||||||
|
setNameInputModalForFolder(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFileFolder = (name: string) => {
|
||||||
|
setNameInputModalOpen(false)
|
||||||
|
const path = node.relativePath + '/' + name
|
||||||
|
if (nameInputModalForFolder) addFolder(path)
|
||||||
|
else addFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
||||||
<li style={{ display: 'list-item' }}>
|
<li style={{ display: 'list-item' }}>
|
||||||
@@ -131,6 +161,8 @@ const TreeViewNode = ({
|
|||||||
selectedFilePath={selectedFilePath}
|
selectedFilePath={selectedFilePath}
|
||||||
handleSelect={handleSelect}
|
handleSelect={handleSelect}
|
||||||
deleteNode={deleteNode}
|
deleteNode={deleteNode}
|
||||||
|
addFile={addFile}
|
||||||
|
addFolder={addFolder}
|
||||||
defaultExpanded={defaultExpanded}
|
defaultExpanded={defaultExpanded}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -141,6 +173,12 @@ const TreeViewNode = ({
|
|||||||
message={deleteConfirmationModalMessage}
|
message={deleteConfirmationModalMessage}
|
||||||
_delete={deleteConfirm}
|
_delete={deleteConfirm}
|
||||||
/>
|
/>
|
||||||
|
<NameInputModal
|
||||||
|
open={nameInputModalOpen}
|
||||||
|
setOpen={setNameInputModalOpen}
|
||||||
|
isFolder={nameInputModalForFolder}
|
||||||
|
add={addFileFolder}
|
||||||
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
open={contextMenu !== null}
|
open={contextMenu !== null}
|
||||||
onClose={() => setContextMenu(null)}
|
onClose={() => setContextMenu(null)}
|
||||||
@@ -153,7 +191,16 @@ const TreeViewNode = ({
|
|||||||
>
|
>
|
||||||
{node.isFolder &&
|
{node.isFolder &&
|
||||||
['Add Folder', 'Add File'].map((item) => (
|
['Add Folder', 'Add File'].map((item) => (
|
||||||
<MenuItem key={item}>{item}</MenuItem>
|
<MenuItem
|
||||||
|
key={item}
|
||||||
|
onClick={() =>
|
||||||
|
item === 'Add Folder'
|
||||||
|
? handleNewFolderItemClick()
|
||||||
|
: handleNewFileItemClick()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
<MenuItem>Rename</MenuItem>
|
<MenuItem>Rename</MenuItem>
|
||||||
<MenuItem disabled={!node.relativePath} onClick={handleDeleteItemClick}>
|
<MenuItem disabled={!node.relativePath} onClick={handleDeleteItemClick}>
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ const Studio = () => {
|
|||||||
directoryData={directoryData}
|
directoryData={directoryData}
|
||||||
handleSelect={handleSelect}
|
handleSelect={handleSelect}
|
||||||
removeFileFromTree={removeFileFromTree}
|
removeFileFromTree={removeFileFromTree}
|
||||||
|
refreshSideBar={fetchDirectoryData}
|
||||||
/>
|
/>
|
||||||
<SASjsEditor
|
<SASjsEditor
|
||||||
selectedFilePath={selectedFilePath}
|
selectedFilePath={selectedFilePath}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useMemo } from 'react'
|
import React, { useState, useMemo } from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Box, Drawer, Toolbar } from '@mui/material'
|
import { Backdrop, Box, CircularProgress, Drawer, Toolbar } from '@mui/material'
|
||||||
|
|
||||||
import TreeView from '../../components/tree'
|
import TreeView from '../../components/tree'
|
||||||
|
import BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||||
|
import Modal from '../../components/modal'
|
||||||
import { TreeNode } from '../../utils/types'
|
import { TreeNode } from '../../utils/types'
|
||||||
|
|
||||||
const drawerWidth = 240
|
const drawerWidth = 240
|
||||||
@@ -12,14 +14,25 @@ type Props = {
|
|||||||
directoryData: TreeNode | null
|
directoryData: TreeNode | null
|
||||||
handleSelect: (filePath: string) => void
|
handleSelect: (filePath: string) => void
|
||||||
removeFileFromTree: (filePath: string) => void
|
removeFileFromTree: (filePath: string) => void
|
||||||
|
refreshSideBar: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SideBar = ({
|
const SideBar = ({
|
||||||
selectedFilePath,
|
selectedFilePath,
|
||||||
directoryData,
|
directoryData,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
removeFileFromTree
|
removeFileFromTree,
|
||||||
|
refreshSideBar
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [openModal, setOpenModal] = useState(false)
|
||||||
|
const [modalTitle, setModalTitle] = useState('')
|
||||||
|
const [modalPayload, setModalPayload] = useState('')
|
||||||
|
const [openSnackbar, setOpenSnackbar] = useState(false)
|
||||||
|
const [snackbarMessage, setSnackbarMessage] = useState('')
|
||||||
|
const [snackbarSeverity, setSnackbarSeverity] = useState<AlertSeverityType>(
|
||||||
|
AlertSeverityType.Success
|
||||||
|
)
|
||||||
const defaultExpanded = useMemo(() => {
|
const defaultExpanded = useMemo(() => {
|
||||||
const splittedPath = selectedFilePath.split('/')
|
const splittedPath = selectedFilePath.split('/')
|
||||||
const arr = ['']
|
const arr = ['']
|
||||||
@@ -34,6 +47,7 @@ const SideBar = ({
|
|||||||
}, [selectedFilePath])
|
}, [selectedFilePath])
|
||||||
|
|
||||||
const deleteNode = (path: string, isFolder: boolean) => {
|
const deleteNode = (path: string, isFolder: boolean) => {
|
||||||
|
setIsLoading(true)
|
||||||
const axiosPromise = axios.delete(
|
const axiosPromise = axios.delete(
|
||||||
`/SASjsApi/drive/${
|
`/SASjsApi/drive/${
|
||||||
isFolder ? `folder?_folderPath=${path}` : `file?_filePath=${path}`
|
isFolder ? `folder?_folderPath=${path}` : `file?_filePath=${path}`
|
||||||
@@ -41,10 +55,71 @@ const SideBar = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
axiosPromise
|
axiosPromise
|
||||||
.then(() => removeFileFromTree(path))
|
.then(() => {
|
||||||
.catch((err) => {
|
removeFileFromTree(path)
|
||||||
console.log(err)
|
setSnackbarMessage('Deleted!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
})
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFile = (filePath: string) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
const stringBlob = new Blob([''], { type: 'text/plain' })
|
||||||
|
formData.append('file', stringBlob)
|
||||||
|
formData.append('filePath', filePath)
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.post('/SASjsApi/drive/file', formData)
|
||||||
|
.then(() => {
|
||||||
|
setSnackbarMessage('File added!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
refreshSideBar()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFolder = (folderPath: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
axios
|
||||||
|
.post('/SASjsApi/drive/folder', { folderPath })
|
||||||
|
.then(() => {
|
||||||
|
setSnackbarMessage('Folder added!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
refreshSideBar()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,6 +131,12 @@ const SideBar = ({
|
|||||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
|
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Backdrop
|
||||||
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
|
open={isLoading}
|
||||||
|
>
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
</Backdrop>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Box sx={{ overflow: 'auto' }}>
|
<Box sx={{ overflow: 'auto' }}>
|
||||||
{directoryData && (
|
{directoryData && (
|
||||||
@@ -64,10 +145,24 @@ const SideBar = ({
|
|||||||
selectedFilePath={selectedFilePath}
|
selectedFilePath={selectedFilePath}
|
||||||
handleSelect={handleSelect}
|
handleSelect={handleSelect}
|
||||||
deleteNode={deleteNode}
|
deleteNode={deleteNode}
|
||||||
|
addFile={addFile}
|
||||||
|
addFolder={addFolder}
|
||||||
defaultExpanded={defaultExpanded}
|
defaultExpanded={defaultExpanded}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
<BootstrapSnackbar
|
||||||
|
open={openSnackbar}
|
||||||
|
setOpen={setOpenSnackbar}
|
||||||
|
message={snackbarMessage}
|
||||||
|
severity={snackbarSeverity}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
open={openModal}
|
||||||
|
setOpen={setOpenModal}
|
||||||
|
title={modalTitle}
|
||||||
|
payload={modalPayload}
|
||||||
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user