Commit fa92df02 authored by Michael Vrána's avatar Michael Vrána

Reworked file upload,

algorithm import/export keybinds
parent fd6c00f8
Pipeline #80143 passed with stages
in 4 minutes and 5 seconds
......@@ -3,7 +3,7 @@ import ImportIcon from '@material-ui/icons/CloudUpload'
import ElementButton from '../../Toolbar/ElementButton'
export interface ImportButtonProps {
onImport?: (filename: string, data: string) => void
onImport?: (e: React.MouseEvent) => void
}
/**
......@@ -13,53 +13,13 @@ export interface ImportButtonProps {
*/
const ImportButton = (props: ImportButtonProps) => {
const { onImport } = props
// Reference to hidden file input element
const fileInput = React.createRef<HTMLInputElement>()
// Click on hidden file input
const handleImport = React.useCallback(() => {
if (fileInput && fileInput.current) {
fileInput.current.click()
}
}, [fileInput])
// Loads selected file and imports
const handleLoadFile = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
// Allow anly loading 1 file at once
if (!files || files.length !== 1) return
const file = files[0]
const reader = new FileReader()
reader.addEventListener('loadend', () =>
onImport?.(file.name, reader.result as string)
)
reader.readAsText(file, 'utf-8')
e.target.value = ''
},
[onImport]
)
return (
<React.Fragment>
<input
ref={fileInput}
type="file"
accept="application/json, text/plain, .json, .txt"
onChange={handleLoadFile}
style={{
position: 'absolute',
display: 'none'
}}
/>
<ElementButton
Icon={ImportIcon}
label="Import from file"
onAction={handleImport}
/>
</React.Fragment>
<ElementButton
Icon={ImportIcon}
label="Import from file"
onAction={onImport}
/>
)
}
......
......@@ -18,14 +18,16 @@ import StateButton from './StateButton'
import TransitionButton from './TransitionButton'
import PositionButton from './PositionButton'
import SetAlgorithmButton from './SetAlgorithmButton'
import ImportButton from './ImportButton'
import ExportButton from './ExportButton'
import SaveButton from './SaveButton'
import { stateCanvasActions } from '../../../reducers/stateCanvas'
import { CanvasToolbar } from '../../Toolbar/CanvasToolbar'
import { loadData } from '../../../utils/import'
import positionForce from '../../../utils/positioning/force'
import positionLayer from '../../../utils/positioning/layered'
import { upload } from '../../../utils/upload'
import ImportButton from './ImportButton'
import ExportButton from './ExportButton'
const { deselect, setCursorMode, zoom, toggleGrid } = stateCanvasActions
const { setErrors } = Actions
......@@ -157,10 +159,11 @@ function StatemakerToolbar() {
dispatch(redoStatemaker())
}, [dispatch])
const handleImport = React.useCallback((filename: string, data: string) => {
const handleImport = React.useCallback(async () => {
const { filename, content } = await upload()
const filenameArr = filename.split('.')
// Loads and converts data to JSON
const result = loadData(data, filenameArr[filenameArr.length - 1])
const result = loadData(content, filenameArr[filenameArr.length - 1])
let chosenPosition = positionLayer
if (algorithm === 'force') chosenPosition = positionForce
......
......@@ -6,23 +6,20 @@ import {
Undo as UndoIcon,
Redo as RedoIcon
} from '@material-ui/icons'
import { saveAs } from 'file-saver'
import { CanvasToolbar } from '../../Toolbar/CanvasToolbar'
import { ReduxState } from '../../../reducers'
import { algorithmCanvasActions } from '../../../reducers/algorithmCanvas'
import { CursorMode } from '../../../interfaces/Canvas'
import { AlgorithmImportExportToolbar } from './AlgorithmImportExportToolbar'
import {
AlgorithmGraph,
algorithmGraphFromJSON
} from '../../../interfaces/AlgorithmGraph'
import { algorithmDataActions } from '../../../reducers/algorithmData'
import { useSnackbar } from 'notistack'
import ElementButton from '../../Toolbar/ElementButton'
import { Actions } from '../../../reducers'
import { useAlgorithmExport } from '../../../hooks/useAlgorithmExport'
import { useAlgorithmImport } from '../../../hooks/useAlgorithmImport'
const { toggleGrid, setCursorMode, zoom } = algorithmCanvasActions
const { importGraph, clear } = algorithmDataActions
const { clear } = algorithmDataActions
const { undoAlgorithm, redoAlgorithm } = Actions
const useStyles = makeStyles((theme) => ({
......@@ -54,17 +51,13 @@ const useStyles = makeStyles((theme) => ({
export const AlgorithmCanvasToolbar = () => {
const dispatch = useDispatch()
const classes = useStyles()
const { cursorMode, gridOn, nodes, edges } = useSelector(
const { cursorMode, gridOn } = useSelector(
(state: ReduxState) => ({
cursorMode: state.algorithmCanvas.cursorMode,
gridOn: state.algorithmCanvas.gridOn,
nodes: state.algorithmData.present.nodes,
edges: state.algorithmData.present.edges
gridOn: state.algorithmCanvas.gridOn
})
)
const { enqueueSnackbar } = useSnackbar()
const handleSelectCursorModeFactory = useCallback(
(cursorMode: CursorMode) => () => dispatch(setCursorMode(cursorMode)),
[dispatch]
......@@ -88,39 +81,8 @@ export const AlgorithmCanvasToolbar = () => {
dispatch
])
const handleExport = useCallback(() => {
const exportData: Omit<AlgorithmGraph, 'outputValues'> = {
nodes,
edges
}
const blob = new Blob([JSON.stringify(exportData)], {
type: 'application/json;charset=utf-8'
})
saveAs(blob, 'webui_export.json')
}, [nodes, edges])
const handleImport = useCallback(
(filename: string, data: string) => {
let algorithmData: AlgorithmGraph
try {
algorithmData = algorithmGraphFromJSON(data)
} catch (e) {
enqueueSnackbar(e.message ?? "Couldn't parse JSON", {
variant: 'error'
})
return
}
enqueueSnackbar(`${filename} imported`, {
variant: 'info'
})
dispatch(importGraph(algorithmData))
},
[dispatch, enqueueSnackbar]
)
const handleExport = useAlgorithmExport()
const handleImport = useAlgorithmImport()
const handleUndo = useCallback(() => dispatch(undoAlgorithm()), [dispatch])
const handleRedo = useCallback(() => dispatch(redoAlgorithm()), [dispatch])
......
import React from 'react'
import { Paper } from '@material-ui/core'
import ElementButton from '../../Toolbar/ElementButton'
import {
CloudDownload as DownloadIcon,
CloudDownload as DownloadIcon
} from '@material-ui/icons'
import ImportButton, { ImportButtonProps } from '../../Statemaker/Toolbar/ImportButton'
import ElementButton from '../../Toolbar/ElementButton'
interface AlgorithmImportExportToolbarProps {
onExport?: (e: React.MouseEvent) => void
......@@ -20,6 +20,6 @@ export const AlgorithmImportExportToolbar = (
label="Export to file"
onAction={props.onExport}
/>
<ImportButton onImport={props.onImport} />
<ImportButton onImport={props.onImport}/>
</Paper>
)
import { useCallback } from 'react'
import { useSelector } from 'react-redux'
import { saveAs } from 'file-saver'
import { AlgorithmGraph } from '../interfaces/AlgorithmGraph'
import { ReduxState } from '../reducers'
export const useAlgorithmExport = () => {
const { nodes, edges } = useSelector((state: ReduxState) => ({
nodes: state.algorithmData.present.nodes,
edges: state.algorithmData.present.edges
}))
return useCallback(() => {
const exportData: Omit<AlgorithmGraph, 'outputValues'> = {
nodes,
edges
}
const blob = new Blob([JSON.stringify(exportData)], {
type: 'application/json;charset=utf-8'
})
saveAs(blob, 'webui_export.json')
}, [nodes, edges])
}
\ No newline at end of file
import { useCallback } from 'react'
import { useDispatch } from 'react-redux'
import { useSnackbar } from 'notistack'
import { algorithmDataActions } from '../reducers/algorithmData'
import {
AlgorithmGraph,
algorithmGraphFromJSON
} from '../interfaces/AlgorithmGraph'
import { upload } from '../utils/upload'
const { importGraph } = algorithmDataActions
export const useAlgorithmImport = () => {
const dispatch = useDispatch()
const { enqueueSnackbar } = useSnackbar()
return useCallback(async () => {
const { filename, content } = await upload()
let algorithmData: AlgorithmGraph
try {
algorithmData = algorithmGraphFromJSON(content)
} catch (e) {
enqueueSnackbar(e.message ?? "Couldn't parse JSON", {
variant: 'error'
})
return
}
enqueueSnackbar(`${filename} imported`, {
variant: 'info'
})
dispatch(importGraph(algorithmData))
}, [dispatch, enqueueSnackbar])
}
......@@ -5,6 +5,8 @@ import { ReduxState } from '../reducers'
import { Actions } from '../reducers'
import { useEvaluate } from './useEvaluate'
import { algorithmCanvasActions } from '../reducers/algorithmCanvas'
import { useAlgorithmExport } from './useAlgorithmExport'
import { useAlgorithmImport } from './useAlgorithmImport'
const { toggleGrid, setCursorMode } = algorithmCanvasActions
const { deleteNode, deleteEdge } = algorithmDataActions
......@@ -34,10 +36,14 @@ export const useAlgorithmKeybinds = () => {
)
const evaluate = useEvaluate()
const handleExport = useAlgorithmExport()
const handleImport = useAlgorithmImport()
const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
e.stopPropagation()
e.preventDefault()
switch (e.key) {
case 'Delete':
handleDelete()
......@@ -63,9 +69,15 @@ export const useAlgorithmKeybinds = () => {
case 'l':
dispatch(setCursorMode('select'))
break
case 's':
if (e.ctrlKey) handleExport()
break
case 'o':
if (e.ctrlKey) handleImport()
break
}
},
[dispatch, handleDelete, algorithmToolbarOpen, evaluate]
[dispatch, handleDelete, algorithmToolbarOpen, evaluate, handleImport, handleExport]
)
return {
......
interface UploadResult {
filename: string,
content: string
}
export const upload = () => new Promise<UploadResult>((resolve) => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'application/json, text/plain, .json, .txt'
input.addEventListener('change', (e: Event) => {
const files = (e as any)?.target?.files
// Allow anly loading 1 file at once
if (!files || files.length !== 1) return
const reader = new FileReader()
reader.addEventListener('loadend', () =>
resolve({ filename: file.name, content: reader.result as string })
)
const [file] = files
reader.readAsText(file, 'utf-8')
})
input.click()
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment