1
0
mirror of https://github.com/sasjs/server.git synced 2026-01-08 23:10:05 +00:00

chore(web): refactor react code

This commit is contained in:
2022-09-08 21:49:35 +05:00
parent d6651bbdbe
commit 4560ef942f
26 changed files with 1534 additions and 1127 deletions

View File

@@ -0,0 +1,112 @@
import React, { useState } from 'react'
import { Button, IconButton, Menu, MenuItem, Tooltip } from '@mui/material'
import { MoreVert, Save, SaveAs, Difference, Edit } from '@mui/icons-material'
type FileMenuProps = {
showDiff: boolean
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>
prevFileContent: string
currentFileContent: string
selectedFilePath: string
setOpenFilePathInputModal: React.Dispatch<React.SetStateAction<boolean>>
saveFile: () => void
}
const FileMenu = ({
showDiff,
setShowDiff,
prevFileContent,
currentFileContent,
selectedFilePath,
setOpenFilePathInputModal,
saveFile
}: FileMenuProps) => {
const [anchorEl, setAnchorEl] = useState<
(EventTarget & HTMLButtonElement) | null
>(null)
const handleMenu = (
event?: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
if (event) setAnchorEl(event.currentTarget)
else setAnchorEl(null)
}
const handleDiffBtnClick = () => {
setAnchorEl(null)
setShowDiff(!showDiff)
}
const handleSaveAsBtnClick = () => {
setAnchorEl(null)
setOpenFilePathInputModal(true)
}
const handleSaveBtnClick = () => {
setAnchorEl(null)
saveFile()
}
return (
<>
<Tooltip title="Save File Menu">
<IconButton onClick={handleMenu}>
<MoreVert />
</IconButton>
</Tooltip>
<Menu
id="save-file-menu"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
open={!!anchorEl}
onClose={() => handleMenu()}
>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
onClick={handleDiffBtnClick}
variant="contained"
color="primary"
startIcon={showDiff ? <Edit /> : <Difference />}
>
{showDiff ? 'Edit' : 'Diff'}
</Button>
</MenuItem>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
onClick={handleSaveBtnClick}
variant="contained"
color="primary"
startIcon={<Save />}
disabled={
!selectedFilePath || prevFileContent === currentFileContent
}
>
Save
</Button>
</MenuItem>
<MenuItem sx={{ justifyContent: 'center' }}>
<Button
onClick={handleSaveAsBtnClick}
variant="contained"
color="primary"
startIcon={<SaveAs />}
>
Save As
</Button>
</MenuItem>
</Menu>
</>
)
}
export default FileMenu

View File

@@ -0,0 +1,100 @@
import {
Box,
Button,
FormControl,
IconButton,
MenuItem,
Select,
SelectChangeEvent,
Tooltip
} from '@mui/material'
import { RocketLaunch } from '@mui/icons-material'
type RunMenuProps = {
selectedFilePath: string
fileContent: string
prevFileContent: string
selectedRunTime: string
runTimes: string[]
handleChangeRunTime: (event: SelectChangeEvent) => void
handleRunBtnClick: () => void
}
const RunMenu = ({
selectedFilePath,
fileContent,
prevFileContent,
selectedRunTime,
runTimes,
handleChangeRunTime,
handleRunBtnClick
}: RunMenuProps) => {
const launchProgram = () => {
const baseUrl = window.location.origin
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${selectedFilePath}`)
}
return (
<>
<Tooltip title="CTRL+ENTER will also run code">
<Button
onClick={handleRunBtnClick}
sx={{
display: 'flex',
alignItems: 'center',
padding: '5px 5px',
minWidth: 'unset'
}}
>
<img
alt=""
draggable="false"
style={{ width: '25px' }}
src="/running-sas.png"
></img>
<span style={{ fontSize: '12px' }}>RUN</span>
</Button>
</Tooltip>
{selectedFilePath ? (
<Box sx={{ marginLeft: '10px' }}>
<Tooltip
title={
fileContent !== prevFileContent
? 'Save file before launching program'
: 'Launch program in new window'
}
>
<span>
<IconButton
disabled={fileContent !== prevFileContent}
onClick={launchProgram}
>
<RocketLaunch />
</IconButton>
</span>
</Tooltip>
</Box>
) : (
<Box sx={{ minWidth: '75px', marginLeft: '10px' }}>
<FormControl variant="standard">
<Select
labelId="run-time-select-label"
id="run-time-select"
value={selectedRunTime}
onChange={handleChangeRunTime}
>
{runTimes.map((runTime) => (
<MenuItem key={runTime} value={runTime}>
{runTime}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
)}
</>
)
}
export default RunMenu

View File

@@ -0,0 +1,14 @@
export const getLanguageFromExtension = (extension: string) => {
if (extension === 'js') return 'javascript'
if (extension === 'ts') return 'typescript'
if (extension === 'md' || extension === 'mdx') return 'markdown'
return extension
}
export const getSelection = (editor: any) => {
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
return selection ?? ''
}

View File

@@ -0,0 +1,299 @@
import axios from 'axios'
import {
Dispatch,
SetStateAction,
useCallback,
useContext,
useEffect,
useRef,
useState
} from 'react'
import { DiffEditorDidMount, EditorDidMount, monaco } from 'react-monaco-editor'
import { SelectChangeEvent } from '@mui/material'
import { getSelection } from '../helper'
import { AppContext, RunTimeType } from '../../../../context/appContext'
import { AlertSeverityType } from '../../../../components/snackbar'
import {
useModal,
useSnackbar,
useStateWithCallback
} from '../../../../utils/hooks'
const SASJS_LOGS_SEPARATOR =
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
type UseEditorParams = {
selectedFilePath: string
setSelectedFilePath: (filePath: string, refreshSideBar?: boolean) => void
setTab: Dispatch<SetStateAction<string>>
}
const useEditor = ({
selectedFilePath,
setSelectedFilePath,
setTab
}: UseEditorParams) => {
const appContext = useContext(AppContext)
const { Dialog, setOpenModal, setModalTitle, setModalPayload } = useModal()
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
useSnackbar()
const [isLoading, setIsLoading] = useState(false)
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('')
const [ctrlPressed, setCtrlPressed] = useState(false)
const [webout, setWebout] = useState('')
const [runTimes, setRunTimes] = useState<string[]>([])
const [selectedRunTime, setSelectedRunTime] = useState('')
const [selectedFileExtension, setSelectedFileExtension] = useState('')
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
const [showDiff, setShowDiff] = useState(false)
const editorRef = useRef(null as any)
const handleEditorDidMount: EditorDidMount = (editor) => {
editorRef.current = editor
editor.focus()
editor.addAction({
// An unique identifier of the contributed action.
id: 'show-difference',
// A label of the action that will be presented to the user.
label: 'Show Differences',
// An optional array of keybindings for the action.
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD],
contextMenuGroupId: 'navigation',
contextMenuOrder: 1,
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: function (ed) {
setShowDiff(true)
}
})
}
const handleDiffEditorDidMount: DiffEditorDidMount = (diffEditor) => {
diffEditor.focus()
diffEditor.addCommand(monaco.KeyCode.Escape, function () {
setShowDiff(false)
})
}
const saveFile = useCallback(
(filePath?: string) => {
setIsLoading(true)
if (filePath) {
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
}
const formData = new FormData()
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
formData.append('file', stringBlob)
formData.append('filePath', filePath ?? selectedFilePath)
const axiosPromise = filePath
? axios.post('/SASjsApi/drive/file', formData)
: axios.patch('/SASjsApi/drive/file', formData)
axiosPromise
.then(() => {
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)
}
})
}
setSnackbarMessage('File saved!')
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)
})
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[fileContent, prevFileContent, selectedFilePath]
)
const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue)
}
const handleRunBtnClick = () =>
runCode(getSelection(editorRef.current as any) || fileContent)
const runCode = (code: string) => {
setIsLoading(true)
axios
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
.then((res: any) => {
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
setTab('log')
// Scroll to bottom of log
const logElement = document.getElementById('log')
if (logElement) logElement.scrollTop = logElement.scrollHeight
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => setIsLoading(false))
}
const handleKeyDown = (event: any) => {
if (event.ctrlKey) {
if (event.key === 'v') {
setCtrlPressed(false)
}
if (event.key === 'Enter')
runCode(getSelection(editorRef.current as any) || fileContent)
if (!ctrlPressed) setCtrlPressed(true)
}
}
const handleKeyUp = (event: any) => {
if (!event.ctrlKey && ctrlPressed) setCtrlPressed(false)
}
const handleChangeRunTime = (event: SelectChangeEvent) => {
setSelectedRunTime(event.target.value as RunTimeType)
}
const handleFilePathInput = (filePath: string) => {
setOpenFilePathInputModal(false)
saveFile(filePath)
}
useEffect(() => {
editorRef.current.addAction({
// An unique identifier of the contributed action.
id: 'save-file',
// A label of the action that will be presented to the user.
label: 'Save',
// An optional array of keybindings for the action.
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: () => {
if (!selectedFilePath) return setOpenFilePathInputModal(true)
if (prevFileContent !== fileContent) return saveFile()
}
})
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
useEffect(() => {
setRunTimes(Object.values(appContext.runTimes))
}, [appContext.runTimes])
useEffect(() => {
if (runTimes.length) setSelectedRunTime(runTimes[0])
}, [runTimes])
useEffect(() => {
if (selectedFilePath) {
setIsLoading(true)
setSelectedFileExtension(selectedFilePath.split('.').pop() ?? '')
axios
.get(`/SASjsApi/drive/file?_filePath=${selectedFilePath}`)
.then((res: any) => {
setPrevFileContent(res.data)
setFileContent(res.data)
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => setIsLoading(false))
} else {
const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content)
}
setLog('')
setWebout('')
setTab('code')
// 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)
}, [selectedFileExtension, runTimes])
return {
ctrlPressed,
fileContent,
isLoading,
log,
openFilePathInputModal,
prevFileContent,
runTimes,
selectedFileExtension,
selectedRunTime,
showDiff,
webout,
Dialog,
handleChangeRunTime,
handleDiffEditorDidMount,
handleEditorDidMount,
handleFilePathInput,
handleKeyDown,
handleKeyUp,
handleRunBtnClick,
handleTabChange,
saveFile,
setShowDiff,
setOpenFilePathInputModal,
setFileContent,
Snackbar
}
}
export default useEditor