diff --git a/.changeset/metal-jokes-jam.md b/.changeset/metal-jokes-jam.md new file mode 100644 index 000000000..6742841c2 --- /dev/null +++ b/.changeset/metal-jokes-jam.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/graph-editor": patch +--- + +Current graph is now cleared when loading a new graph, with a dialog option to save diff --git a/packages/graph-editor/src/components/toolbar/buttons/download.tsx b/packages/graph-editor/src/components/toolbar/buttons/download.tsx index 5a8757fbd..cf7351c29 100644 --- a/packages/graph-editor/src/components/toolbar/buttons/download.tsx +++ b/packages/graph-editor/src/components/toolbar/buttons/download.tsx @@ -1,24 +1,23 @@ +import { Graph } from '@tokens-studio/graph-engine'; import { IconButton, Tooltip } from '@tokens-studio/ui'; import { ImperativeEditorRef } from '@/editor/editorTypes.js'; import { mainGraphSelector } from '@/redux/selectors/graph.js'; +import { saveGraph } from '@/utils/saveGraph.js'; +import { title } from '@/annotations/index.js'; import { useSelector } from 'react-redux'; import Download from '@tokens-studio/icons/Download.js'; import React from 'react'; + export const DownloadToolbarButton = () => { const mainGraph = useSelector(mainGraphSelector); const graphRef = mainGraph?.ref as ImperativeEditorRef | undefined; + const graph = mainGraph?.graph as Graph; - const onDownload = () => { - const saved = graphRef!.save(); - const blob = new Blob([JSON.stringify(saved)], { - type: 'application/json', - }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = 'graph.json'; - document.body.appendChild(link); - link.click(); + const onDownload = async () => { + await saveGraph( + graphRef!, + (graph.annotations[title] || 'graph').toLowerCase().replace(/\s+/g, '-'), + ); }; return ( diff --git a/packages/graph-editor/src/components/toolbar/buttons/upload.tsx b/packages/graph-editor/src/components/toolbar/buttons/upload.tsx index cabd83da4..d4506c6b3 100644 --- a/packages/graph-editor/src/components/toolbar/buttons/upload.tsx +++ b/packages/graph-editor/src/components/toolbar/buttons/upload.tsx @@ -1,40 +1,94 @@ -import { IconButton, Tooltip } from '@tokens-studio/ui'; +import { + Button, + Dialog, + IconButton, + Stack, + Text, + Tooltip, +} from '@tokens-studio/ui'; +import { Graph } from '@tokens-studio/graph-engine'; import { ImperativeEditorRef } from '@/editor/editorTypes.js'; import { mainGraphSelector } from '@/redux/selectors/graph.js'; +import { saveGraph } from '@/utils/saveGraph.js'; +import { title } from '@/annotations/index.js'; +import { useReactFlow } from 'reactflow'; import { useSelector } from 'react-redux'; -import React from 'react'; +import React, { useState } from 'react'; import Upload from '@tokens-studio/icons/Upload.js'; +import loadGraph from '@/utils/loadGraph.js'; export const UploadToolbarButton = () => { + const reactFlowInstance = useReactFlow(); const mainGraph = useSelector(mainGraphSelector); const graphRef = mainGraph?.ref as ImperativeEditorRef | undefined; + const graph = mainGraph?.graph as Graph; + const [isDialogOpen, setIsDialogOpen] = useState(false); - const onUpload = () => { + const handleUploadClick = () => { + const isCurrentGraphEmpty = + !graph?.nodes || Object.keys(graph.nodes).length === 0; + + if (!isCurrentGraphEmpty) { + setIsDialogOpen(true); + } else { + loadGraph(graphRef, graph, reactFlowInstance); + } + }; + + const onDownload = async () => { if (!graphRef) return; - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json'; - //@ts-expect-error - input.onchange = (e: HTMLInputElement) => { - //@ts-expect-error - const file = e.target.files[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = (e: ProgressEvent) => { - const text = e.target?.result as string; - const data = JSON.parse(text); - - graphRef.loadRaw(data); - }; - reader.readAsText(file); - }; - input.click(); + const saved = await saveGraph( + graphRef, + (graph.annotations[title] || 'graph').toLowerCase().replace(/\s+/g, '-'), + ); + + setIsDialogOpen(false); + + if (saved) { + loadGraph(graphRef, graph, reactFlowInstance); + } + }; + + const onDiscard = () => { + setIsDialogOpen(false); + loadGraph(graphRef, graph, reactFlowInstance); }; return ( - - } /> - + + + + } + onClick={handleUploadClick} + /> + + + + + + + Download Current Graph? + + Do you want to download the current graph before uploading a new + one? + + + + + + + + + + + + ); }; diff --git a/packages/graph-editor/src/utils/loadGraph.ts b/packages/graph-editor/src/utils/loadGraph.ts new file mode 100644 index 000000000..fe4de6705 --- /dev/null +++ b/packages/graph-editor/src/utils/loadGraph.ts @@ -0,0 +1,34 @@ +import { Graph } from '@tokens-studio/graph-engine'; +import { ImperativeEditorRef } from '@/editor/editorTypes.js'; +import { ReactFlowInstance } from 'reactflow'; +import { clear } from '@/editor/actions/clear.js'; + +export default function loadGraph( + graphRef: ImperativeEditorRef | undefined, + graph: Graph, + reactFlowInstance: ReactFlowInstance, +) { + if (!graphRef) return; + + // always clear the graph before loading a new one, + // the user already had the choice to save it + clear(reactFlowInstance, graph); + + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + //@ts-expect-error + input.onchange = (e: HTMLInputElement) => { + //@ts-expect-error + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e: ProgressEvent) => { + const text = e.target?.result as string; + const data = JSON.parse(text); + graphRef.loadRaw(data); + }; + reader.readAsText(file); + }; + input.click(); +} diff --git a/packages/graph-editor/src/utils/saveGraph.ts b/packages/graph-editor/src/utils/saveGraph.ts new file mode 100644 index 000000000..da837fd53 --- /dev/null +++ b/packages/graph-editor/src/utils/saveGraph.ts @@ -0,0 +1,30 @@ +import { ImperativeEditorRef } from '@/editor/editorTypes.js'; + +export async function saveGraph( + graphRef: ImperativeEditorRef, + filename: string, +): Promise { + const saved = graphRef.save(); + const blob = new Blob([JSON.stringify(saved)], { type: 'application/json' }); + + try { + const handle = await globalThis.showSaveFilePicker({ + suggestedName: filename + '.json', + types: [ + { + description: 'JSON Files', + accept: { 'application/json': ['.json'] }, + }, + ], + }); + + const writable = await handle.createWritable(); + await writable.write(blob); + await writable.close(); + // we need to return a result so the caller can make decisions based on it + return true; + } catch (err) { + // do nothing if picker fails or is cancelled + return false; + } +}