diff --git a/src/GraphArea.jsx b/src/GraphArea.jsx index efe0166..373eb82 100644 --- a/src/GraphArea.jsx +++ b/src/GraphArea.jsx @@ -44,7 +44,8 @@ function Graph({ useEffect(() => active && instance && instance.setCurStatus(), [active && instance]); useEffect(() => { if (active && instance) dispatcher({ type: T.SET_CUR_INSTANCE, payload: instance }); - }, [active && instance]); + if (instance) dispatcher({ type: T.SET_GRAPH_INSTANCE, payload: { graphID, instance } }); + }, [active, instance, graphID, dispatcher]); useEffect(() => { if (ref.current) { diff --git a/src/component/TabBar.jsx b/src/component/TabBar.jsx index 6ce73f1..9a3ba89 100644 --- a/src/component/TabBar.jsx +++ b/src/component/TabBar.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { MdEdit, MdClose, MdAdd, } from 'react-icons/md'; @@ -8,12 +8,13 @@ import localStorageManager from '../graph-builder/local-storage-manager'; import { actionType as T } from '../reducer'; import { newProject, editDetails } from '../toolbarActions/toolbarFunctions'; import './tabBar.css'; +import ConfirmModal from './modals/ConfirmModal'; const TabBar = ({ superState, dispatcher }) => { - const closeTab = (i, e) => { - e.stopPropagation(); - // eslint-disable-next-line no-alert - if (!window.confirm('Do you confirm to close the tab? This action is irreversable.')) return; + const [confirmOpen, setConfirmOpen] = useState(false); + const [tabToClose, setTabToClose] = useState(null); + + const closeTab = (i) => { localStorageManager.remove(superState.graphs[i] ? superState.graphs[i].graphID : null); dispatcher({ type: T.REMOVE_GRAPH, payload: i }); if (!superState.curGraphIndex && superState.graphs.length === 1) { @@ -21,6 +22,28 @@ const TabBar = ({ superState, dispatcher }) => { dispatcher({ type: T.SET_CUR_INDEX, payload: -1 }); } }; + + const handleRequestCloseTab = (i, e) => { + e.stopPropagation(); + const graph = superState.graphs[i]; + if (graph && graph.instance && !graph.instance.isSaved) { + setTabToClose(i); + setConfirmOpen(true); + } else { + closeTab(i); + } + }; + + const handleConfirmClose = () => { + closeTab(tabToClose); + setConfirmOpen(false); + setTabToClose(null); + }; + + const handleCancelClose = () => { + setConfirmOpen(false); + setTabToClose(null); + }; const editCur = (e) => { e.stopPropagation(); editDetails(superState, dispatcher); @@ -80,7 +103,7 @@ const TabBar = ({ superState, dispatcher }) => { ) : <>} { e.target.value = null; }} onChange={(e) => { - const filesArray = Array.from(e.target.files).map((file) => ({ - key: file.webkitRelativePath, - modified: file.lastModified, - size: file.size, - fileObj: file, - })); - - setFileState(filesArray); - window.localStorage.setItem('fileList', JSON.stringify(filesArray)); + const { files } = e.target; + if (files && files.length > 0) { + tempFilesRef.current = files; + const folderName = files[0].webkitRelativePath.split('/')[0]; + setPendingFolderName(folderName); + setConfirmOpen(true); + } }} - webkitdirectory + webkitdirectory="" /> - + )} {dirButton && ( + + + + +); + +export default ConfirmModal; diff --git a/src/component/modals/confirmModal.css b/src/component/modals/confirmModal.css new file mode 100644 index 0000000..5402b8b --- /dev/null +++ b/src/component/modals/confirmModal.css @@ -0,0 +1,34 @@ +.confirm-modal-content { + padding: 20px; + text-align: center; +} +.confirm-modal-message { + margin-bottom: 20px; + font-size: 1.1em; +} +.confirm-modal-actions { + display: flex; + justify-content: center; + gap: 20px; +} +.confirm-btn, .cancel-btn { + padding: 8px 20px; + font-size: 1em; + border: none; + border-radius: 4px; + cursor: pointer; +} +.confirm-btn { + background: #d9534f; + color: #fff; +} +.cancel-btn { + background: #f0f0f0; + color: #333; +} +.confirm-btn:hover { + background: #c9302c; +} +.cancel-btn:hover { + background: #e0e0e0; +} diff --git a/src/graph-builder/graph-core/5-load-save.js b/src/graph-builder/graph-core/5-load-save.js index 4a78774..e285098 100644 --- a/src/graph-builder/graph-core/5-load-save.js +++ b/src/graph-builder/graph-core/5-load-save.js @@ -13,6 +13,11 @@ class GraphLoadSave extends GraphUndoRedo { constructor(...args) { super(...args); this.autoSaveIntervalId = null; + this.lastSavedActionIndex = 0; + } + + get isSaved() { + return this.curActionIndex === this.lastSavedActionIndex; } registerEvents() { @@ -132,6 +137,7 @@ class GraphLoadSave extends GraphUndoRedo { fileHandle: handle, }]); this.dispatcher({ type: T.SET_FILE_STATE, payload: fS }); + this.lastSavedActionIndex = this.curActionIndex; toast.success('File saved Successfully'); } catch (error) { // AbortError is silently ignored (user cancelled) @@ -140,6 +146,7 @@ class GraphLoadSave extends GraphUndoRedo { // eslint-disable-next-line no-alert const fileName = prompt('Filename:'); saveAs(blob, `${fileName || `${this.getName()}-concore`}.graphml`); + this.lastSavedActionIndex = this.curActionIndex; toast.success('File saved Successfully'); } } @@ -172,6 +179,7 @@ class GraphLoadSave extends GraphUndoRedo { const stream = await handle.createWritable(); await stream.write(blob); await stream.close(); + this.lastSavedActionIndex = this.curActionIndex; toast.success('File saved Successfully'); } catch (error) { // AbortError is silently ignored (user cancelled) @@ -213,6 +221,7 @@ class GraphLoadSave extends GraphUndoRedo { graphMLParser(graphML).then((graphObject) => { localStorageManager.save(this.id, graphObject); this.loadGraphFromLocalStorage(); + this.lastSavedActionIndex = this.curActionIndex; }); } @@ -225,6 +234,10 @@ class GraphLoadSave extends GraphUndoRedo { const graphContent = localStorageManager.get(this.id); if (!graphContent) return false; this.loadJson(graphContent); + // If loaded from localStorage, assume UNSAVED unless actions are 0. + // Actually, loadJson sets curActionIndex based on history. + // If we want to support recovering unsaved work, we should leave lastSavedActionIndex as 0 (unless graph is empty and curActionIndex is 0). + if (this.curActionIndex === 0) this.lastSavedActionIndex = 0; return true; } diff --git a/src/reducer/actionType.js b/src/reducer/actionType.js index f211767..09f7981 100644 --- a/src/reducer/actionType.js +++ b/src/reducer/actionType.js @@ -44,6 +44,7 @@ const actionType = { SET_FUNCTIONS: 'SET_FUNCTIONS', SET_LOGS: 'SET_LOGS', SET_LOGS_MESSAGE: 'SET_LOGS_MESSAGE', + SET_GRAPH_INSTANCE: 'SET_GRAPH_INSTANCE', }; export default zealit(actionType); diff --git a/src/reducer/reducer.js b/src/reducer/reducer.js index 7654b1f..dd3139f 100644 --- a/src/reducer/reducer.js +++ b/src/reducer/reducer.js @@ -243,6 +243,14 @@ const reducer = (state, action) => { return { ...newState }; } + case T.SET_GRAPH_INSTANCE: { + const newState = { ...state }; + newState.graphs = newState.graphs.map((g) => ( + g.graphID === action.payload.graphID ? { ...g, instance: action.payload.instance } : g + )); + return { ...newState }; + } + default: return state; }