mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34: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 DeleteConfirmationModal from './deleteConfirmationModal'
|
||||
import NameInputModal from './nameInputModal'
|
||||
|
||||
import { TreeNode } from '../utils/types'
|
||||
|
||||
@@ -12,6 +13,8 @@ type Props = {
|
||||
selectedFilePath: string
|
||||
handleSelect: (filePath: string) => void
|
||||
deleteNode: (path: string, isFolder: boolean) => void
|
||||
addFile: (path: string) => void
|
||||
addFolder: (path: string) => void
|
||||
defaultExpanded?: string[]
|
||||
}
|
||||
|
||||
@@ -20,6 +23,8 @@ const TreeView = ({
|
||||
selectedFilePath,
|
||||
handleSelect,
|
||||
deleteNode,
|
||||
addFile,
|
||||
addFolder,
|
||||
defaultExpanded
|
||||
}: Props) => {
|
||||
return (
|
||||
@@ -35,6 +40,8 @@ const TreeView = ({
|
||||
selectedFilePath={selectedFilePath}
|
||||
handleSelect={handleSelect}
|
||||
deleteNode={deleteNode}
|
||||
addFile={addFile}
|
||||
addFolder={addFolder}
|
||||
defaultExpanded={defaultExpanded}
|
||||
/>
|
||||
</ul>
|
||||
@@ -48,12 +55,16 @@ const TreeViewNode = ({
|
||||
selectedFilePath,
|
||||
handleSelect,
|
||||
deleteNode,
|
||||
addFile,
|
||||
addFolder,
|
||||
defaultExpanded
|
||||
}: Props) => {
|
||||
const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] =
|
||||
useState(false)
|
||||
const [deleteConfirmationModalMessage, setDeleteConfirmationModalMessage] =
|
||||
useState('')
|
||||
const [nameInputModalOpen, setNameInputModalOpen] = useState(false)
|
||||
const [nameInputModalForFolder, setNameInputModalForFolder] = useState(false)
|
||||
const [childVisible, setChildVisibility] = useState(false)
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
mouseX: number
|
||||
@@ -108,6 +119,25 @@ const TreeViewNode = ({
|
||||
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 (
|
||||
<div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
|
||||
<li style={{ display: 'list-item' }}>
|
||||
@@ -131,6 +161,8 @@ const TreeViewNode = ({
|
||||
selectedFilePath={selectedFilePath}
|
||||
handleSelect={handleSelect}
|
||||
deleteNode={deleteNode}
|
||||
addFile={addFile}
|
||||
addFolder={addFolder}
|
||||
defaultExpanded={defaultExpanded}
|
||||
/>
|
||||
))}
|
||||
@@ -141,6 +173,12 @@ const TreeViewNode = ({
|
||||
message={deleteConfirmationModalMessage}
|
||||
_delete={deleteConfirm}
|
||||
/>
|
||||
<NameInputModal
|
||||
open={nameInputModalOpen}
|
||||
setOpen={setNameInputModalOpen}
|
||||
isFolder={nameInputModalForFolder}
|
||||
add={addFileFolder}
|
||||
/>
|
||||
<Menu
|
||||
open={contextMenu !== null}
|
||||
onClose={() => setContextMenu(null)}
|
||||
@@ -153,7 +191,16 @@ const TreeViewNode = ({
|
||||
>
|
||||
{node.isFolder &&
|
||||
['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 disabled={!node.relativePath} onClick={handleDeleteItemClick}>
|
||||
|
||||
@@ -88,6 +88,7 @@ const Studio = () => {
|
||||
directoryData={directoryData}
|
||||
handleSelect={handleSelect}
|
||||
removeFileFromTree={removeFileFromTree}
|
||||
refreshSideBar={fetchDirectoryData}
|
||||
/>
|
||||
<SASjsEditor
|
||||
selectedFilePath={selectedFilePath}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useState, useMemo } from 'react'
|
||||
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 BootstrapSnackbar, { AlertSeverityType } from '../../components/snackbar'
|
||||
import Modal from '../../components/modal'
|
||||
import { TreeNode } from '../../utils/types'
|
||||
|
||||
const drawerWidth = 240
|
||||
@@ -12,14 +14,25 @@ type Props = {
|
||||
directoryData: TreeNode | null
|
||||
handleSelect: (filePath: string) => void
|
||||
removeFileFromTree: (filePath: string) => void
|
||||
refreshSideBar: () => void
|
||||
}
|
||||
|
||||
const SideBar = ({
|
||||
selectedFilePath,
|
||||
directoryData,
|
||||
handleSelect,
|
||||
removeFileFromTree
|
||||
removeFileFromTree,
|
||||
refreshSideBar
|
||||
}: 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 splittedPath = selectedFilePath.split('/')
|
||||
const arr = ['']
|
||||
@@ -34,6 +47,7 @@ const SideBar = ({
|
||||
}, [selectedFilePath])
|
||||
|
||||
const deleteNode = (path: string, isFolder: boolean) => {
|
||||
setIsLoading(true)
|
||||
const axiosPromise = axios.delete(
|
||||
`/SASjsApi/drive/${
|
||||
isFolder ? `folder?_folderPath=${path}` : `file?_filePath=${path}`
|
||||
@@ -41,10 +55,71 @@ const SideBar = ({
|
||||
)
|
||||
|
||||
axiosPromise
|
||||
.then(() => removeFileFromTree(path))
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
.then(() => {
|
||||
removeFileFromTree(path)
|
||||
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 (
|
||||
@@ -56,6 +131,12 @@ const SideBar = ({
|
||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' }
|
||||
}}
|
||||
>
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={isLoading}
|
||||
>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
<Toolbar />
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
{directoryData && (
|
||||
@@ -64,10 +145,24 @@ const SideBar = ({
|
||||
selectedFilePath={selectedFilePath}
|
||||
handleSelect={handleSelect}
|
||||
deleteNode={deleteNode}
|
||||
addFile={addFile}
|
||||
addFolder={addFolder}
|
||||
defaultExpanded={defaultExpanded}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<BootstrapSnackbar
|
||||
open={openSnackbar}
|
||||
setOpen={setOpenSnackbar}
|
||||
message={snackbarMessage}
|
||||
severity={snackbarSeverity}
|
||||
/>
|
||||
<Modal
|
||||
open={openModal}
|
||||
setOpen={setOpenModal}
|
||||
title={modalTitle}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user