Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added discard/save dialog & clear current graph when loading new graph #632

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/metal-jokes-jam.md
Original file line number Diff line number Diff line change
@@ -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
21 changes: 10 additions & 11 deletions packages/graph-editor/src/components/toolbar/buttons/download.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
102 changes: 78 additions & 24 deletions packages/graph-editor/src/components/toolbar/buttons/upload.tsx
Original file line number Diff line number Diff line change
@@ -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<FileReader>) => {
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 (
<Tooltip label="Upload" side="bottom">
<IconButton emphasis="low" onClick={onUpload} icon={<Upload />} />
</Tooltip>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<Dialog.Trigger asChild>
<Tooltip label="Upload" side="bottom">
<IconButton
emphasis="low"
icon={<Upload />}
onClick={handleUploadClick}
/>
</Tooltip>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Stack direction="column" gap={4}>
<Dialog.Title>Download Current Graph?</Dialog.Title>
<Text>
Do you want to download the current graph before uploading a new
one?
</Text>
<Stack direction="row" gap={2} justify="end">
<Dialog.Close asChild>
<Button emphasis="low" onClick={onDiscard}>
Discard
</Button>
</Dialog.Close>
<Button emphasis="high" onClick={onDownload}>
Download
</Button>
</Stack>
</Stack>
<Dialog.CloseButton />
</Dialog.Content>
</Dialog.Portal>
</Dialog>
);
};
34 changes: 34 additions & 0 deletions packages/graph-editor/src/utils/loadGraph.ts
Original file line number Diff line number Diff line change
@@ -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<FileReader>) => {
const text = e.target?.result as string;
const data = JSON.parse(text);
graphRef.loadRaw(data);
};
reader.readAsText(file);
};
input.click();
}
30 changes: 30 additions & 0 deletions packages/graph-editor/src/utils/saveGraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ImperativeEditorRef } from '@/editor/editorTypes.js';

export async function saveGraph(
graphRef: ImperativeEditorRef,
filename: string,
): Promise<boolean> {
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;
}
}
Loading