diff --git a/web/src/components/filePathInputModal.tsx b/web/src/components/filePathInputModal.tsx index a75dedc..d1eeaa5 100644 --- a/web/src/components/filePathInputModal.tsx +++ b/web/src/components/filePathInputModal.tsx @@ -22,8 +22,14 @@ const FilePathInputModal = ({ const handleChange = (event: React.ChangeEvent) => { const value = event.target.value - const regex = /\.(exe|sh|htaccess)$/i - if (regex.test(value)) { + + const specialChars = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>?~]/ + const fileExtension = /\.(exe|sh|htaccess)$/i + + if (specialChars.test(value)) { + setHasError(true) + setErrorText('can not have special characters') + } else if (fileExtension.test(value)) { setHasError(true) setErrorText('can not save file with extensions [exe, sh, htaccess]') } else { diff --git a/web/src/components/nameInputModal.tsx b/web/src/components/nameInputModal.tsx index 32e6f52..ff652bd 100644 --- a/web/src/components/nameInputModal.tsx +++ b/web/src/components/nameInputModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { Button, DialogActions, DialogContent, TextField } from '@mui/material' @@ -12,6 +12,7 @@ type NameInputModalProps = { isFolder: boolean actionLabel: string action: (name: string) => void + defaultName?: string } const NameInputModal = ({ @@ -20,12 +21,17 @@ const NameInputModal = ({ title, isFolder, actionLabel, - action + action, + defaultName }: NameInputModalProps) => { const [name, setName] = useState('') const [hasError, setHasError] = useState(false) const [errorText, setErrorText] = useState('') + useEffect(() => { + if (defaultName) setName(defaultName) + }, [defaultName]) + const handleChange = (event: React.ChangeEvent) => { const value = event.target.value diff --git a/web/src/components/tree.tsx b/web/src/components/tree.tsx index e4d7279..df4af5b 100644 --- a/web/src/components/tree.tsx +++ b/web/src/components/tree.tsx @@ -208,6 +208,7 @@ const TreeViewNode = ({ action={ nameInputModalActionLabel === 'Add' ? addFileFolder : renameFileFolder } + defaultName={node.relativePath.split('/').pop()} /> ({ padding: '10px' @@ -74,7 +75,7 @@ const SASjsEditor = ({ const [snackbarSeverity, setSnackbarSeverity] = useState( AlertSeverityType.Success ) - const [prevFileContent, setPrevFileContent] = useState('') + const [prevFileContent, setPrevFileContent] = useStateWithCallback('') const [fileContent, setFileContent] = useState('') const [log, setLog] = useState('') const [ctrlPressed, setCtrlPressed] = useState(false) @@ -102,7 +103,7 @@ const SASjsEditor = ({ usePrompt( 'Changes you made may not be saved.', - prevFileContent !== fileContent + prevFileContent !== fileContent && !!selectedFilePath ) useEffect(() => { @@ -134,10 +135,21 @@ const SASjsEditor = ({ }) .finally(() => setIsLoading(false)) } else { - setFileContent('') + const content = localStorage.getItem('fileContent') ?? '' + setFileContent(content) } + setLog('') + setWebout('') + setTab('1') + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedFilePath]) + useEffect(() => { + if (fileContent.length && !selectedFilePath) { + localStorage.setItem('fileContent', fileContent) + } + }, [fileContent, selectedFilePath]) + useEffect(() => { if (runTimes.includes(selectedFileExtension)) setSelectedRunTime(selectedFileExtension) @@ -211,6 +223,10 @@ const SASjsEditor = ({ const saveFile = (filePath?: string) => { setIsLoading(true) + if (filePath) { + filePath = filePath.startsWith('/') ? filePath : `/${filePath}` + } + const formData = new FormData() const stringBlob = new Blob([fileContent], { type: 'text/plain' }) @@ -223,10 +239,22 @@ const SASjsEditor = ({ axiosPromise .then(() => { - if (filePath) { + if (filePath && fileContent === prevFileContent) { + // when fileContent and prevFileContent is same, + // callback function in setPrevFileContent method is not called + // because behind the scene useEffect hook is being used + // for calling callback function, and it's only fired when the + // new value is not equal to old value. + // So, we'll have to explicitly update the selected file path + setSelectedFilePath(filePath, true) + } else { + setPrevFileContent(fileContent, () => { + if (filePath) { + setSelectedFilePath(filePath, true) + } + }) } - setPrevFileContent(fileContent) setSnackbarMessage('File saved!') setSnackbarSeverity(AlertSeverityType.Success) setOpenSnackbar(true) @@ -312,9 +340,14 @@ const SASjsEditor = ({ - - - + + Webout + + } + value="3" + /> @@ -324,6 +357,8 @@ const SASjsEditor = ({ > void @@ -431,6 +468,8 @@ type RunMenuProps = { const RunMenu = ({ selectedFilePath, + fileContent, + prevFileContent, selectedRunTime, runTimes, handleChangeRunTime, @@ -463,10 +502,21 @@ const RunMenu = ({ {selectedFilePath ? ( - - - - + + + + + + ) : ( diff --git a/web/src/utils/hooks/index.ts b/web/src/utils/hooks/index.ts new file mode 100644 index 0000000..bb694d1 --- /dev/null +++ b/web/src/utils/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './usePrompt' +export * from './useStateWithCallback' diff --git a/web/src/utils/usePrompt.ts b/web/src/utils/hooks/usePrompt.ts similarity index 86% rename from web/src/utils/usePrompt.ts rename to web/src/utils/hooks/usePrompt.ts index 8c2676c..8c3cece 100644 --- a/web/src/utils/usePrompt.ts +++ b/web/src/utils/hooks/usePrompt.ts @@ -2,7 +2,7 @@ import { useEffect, useCallback, useContext } from 'react' import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom' import { History, Blocker, Transition } from 'history' -function useBlocker(blocker: Blocker, when = true) { +const useBlocker = (blocker: Blocker, when = true) => { const navigator = useContext(NavigationContext).navigator as History useEffect(() => { @@ -24,7 +24,7 @@ function useBlocker(blocker: Blocker, when = true) { }, [navigator, blocker, when]) } -export default function usePrompt(message: string, when = true) { +export const usePrompt = (message: string, when = true) => { const blocker = useCallback( (tx) => { if (window.confirm(message)) tx.retry() diff --git a/web/src/utils/hooks/useStateWithCallback.ts b/web/src/utils/hooks/useStateWithCallback.ts new file mode 100644 index 0000000..f6acb92 --- /dev/null +++ b/web/src/utils/hooks/useStateWithCallback.ts @@ -0,0 +1,27 @@ +import { useState, useEffect, useRef } from 'react' + +export const useStateWithCallback = ( + initialValue: T +): [T, (newValue: T, callback?: () => void) => void] => { + const callbackRef = useRef(null) + + const [value, setValue] = useState(initialValue) + + useEffect(() => { + if (typeof callbackRef.current === 'function') { + callbackRef.current() + + callbackRef.current = null + } + }, [value]) + + const setValueWithCallback = (newValue: T, callback?: () => void) => { + callbackRef.current = callback + + setValue(newValue) + } + + return [value, setValueWithCallback] +} + +export default useStateWithCallback