From fbb7d9c746ffc363f351674e056831b58005ea0b Mon Sep 17 00:00:00 2001 From: AlexBxl Date: Tue, 14 Jan 2025 09:11:03 +0100 Subject: [PATCH 1/2] added discard/save dialog & clear current graph when loading new graph (#618) --- .changeset/metal-jokes-jam.md | 5 + .../src/components/toolbar/buttons/upload.tsx | 97 ++++++++++++++----- packages/graph-editor/src/utils/loadGraph.ts | 34 +++++++ packages/graph-editor/src/utils/saveGraph.ts | 24 +++++ 4 files changed, 136 insertions(+), 24 deletions(-) create mode 100644 .changeset/metal-jokes-jam.md create mode 100644 packages/graph-editor/src/utils/loadGraph.ts create mode 100644 packages/graph-editor/src/utils/saveGraph.ts 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/upload.tsx b/packages/graph-editor/src/components/toolbar/buttons/upload.tsx index cabd83da4..b18088d03 100644 --- a/packages/graph-editor/src/components/toolbar/buttons/upload.tsx +++ b/packages/graph-editor/src/components/toolbar/buttons/upload.tsx @@ -1,40 +1,89 @@ -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 = () => { - if (!graphRef) return; + const handleUploadClick = () => { + const isCurrentGraphEmpty = + !graph?.nodes || Object.keys(graph.nodes).length === 0; + + if (!isCurrentGraphEmpty) { + setIsDialogOpen(true); + } else { + loadGraph(graphRef, graph, reactFlowInstance); + } + }; - 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); + const onSave = async () => { + if (!graphRef) return; + await saveGraph( + graphRef, + graph, + (graph.annotations[title] || 'graph').toLowerCase().replace(/\s+/g, '-'), + ); + setIsDialogOpen(false); + loadGraph(graphRef, graph, reactFlowInstance); + }; - graphRef.loadRaw(data); - }; - reader.readAsText(file); - }; - input.click(); + const onDiscard = () => { + setIsDialogOpen(false); + loadGraph(graphRef, graph, reactFlowInstance); }; return ( - - } /> - + + + + } + onClick={handleUploadClick} + /> + + + + + + + Save Current Graph? + + Do you want to save 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..d86dbb732 --- /dev/null +++ b/packages/graph-editor/src/utils/saveGraph.ts @@ -0,0 +1,24 @@ +import { Graph } from '@tokens-studio/graph-engine'; +import { ImperativeEditorRef } from '@/editor/editorTypes.js'; + +export function saveGraph( + graphRef: ImperativeEditorRef, + graph: Graph, + filename: string, +) { + 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 = filename + '.json'; + document.body.appendChild(link); + + link.click(); + + document.body.removeChild(link); + URL.revokeObjectURL(url); +} From 4671edb1eef655f8d329e2af69827daa19925690 Mon Sep 17 00:00:00 2001 From: AlexBxl Date: Tue, 14 Jan 2025 12:04:44 +0100 Subject: [PATCH 2/2] added save file dialog when downloading graph --- .../components/toolbar/buttons/download.tsx | 21 ++++++----- .../src/components/toolbar/buttons/upload.tsx | 21 ++++++----- packages/graph-editor/src/utils/saveGraph.ts | 36 +++++++++++-------- 3 files changed, 44 insertions(+), 34 deletions(-) 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 b18088d03..d4506c6b3 100644 --- a/packages/graph-editor/src/components/toolbar/buttons/upload.tsx +++ b/packages/graph-editor/src/components/toolbar/buttons/upload.tsx @@ -35,15 +35,19 @@ export const UploadToolbarButton = () => { } }; - const onSave = async () => { + const onDownload = async () => { if (!graphRef) return; - await saveGraph( + + const saved = await saveGraph( graphRef, - graph, (graph.annotations[title] || 'graph').toLowerCase().replace(/\s+/g, '-'), ); + setIsDialogOpen(false); - loadGraph(graphRef, graph, reactFlowInstance); + + if (saved) { + loadGraph(graphRef, graph, reactFlowInstance); + } }; const onDiscard = () => { @@ -66,9 +70,10 @@ export const UploadToolbarButton = () => { - Save Current Graph? + Download Current Graph? - Do you want to save the current graph before uploading a new one? + Do you want to download the current graph before uploading a new + one? @@ -76,8 +81,8 @@ export const UploadToolbarButton = () => { Discard - diff --git a/packages/graph-editor/src/utils/saveGraph.ts b/packages/graph-editor/src/utils/saveGraph.ts index d86dbb732..da837fd53 100644 --- a/packages/graph-editor/src/utils/saveGraph.ts +++ b/packages/graph-editor/src/utils/saveGraph.ts @@ -1,24 +1,30 @@ -import { Graph } from '@tokens-studio/graph-engine'; import { ImperativeEditorRef } from '@/editor/editorTypes.js'; -export function saveGraph( +export async function saveGraph( graphRef: ImperativeEditorRef, - graph: Graph, filename: string, -) { +): Promise { 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 = filename + '.json'; - document.body.appendChild(link); - link.click(); + try { + const handle = await globalThis.showSaveFilePicker({ + suggestedName: filename + '.json', + types: [ + { + description: 'JSON Files', + accept: { 'application/json': ['.json'] }, + }, + ], + }); - document.body.removeChild(link); - URL.revokeObjectURL(url); + 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; + } }