diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 14d8d28..cec634f 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -20,17 +20,16 @@ import { workflowVersionsTable, } from "./db-tables/WorkspaceDB"; import { defaultGraph } from "./defaultGraph"; -import { JsonDiff, WorkspaceContext } from "./WorkspaceContext"; +import { WorkspaceContext } from "./WorkspaceContext"; import { getFileUrl, - matchShortcut, getWorkflowIdInUrlHash, generateUrlHashWithFlowId, openWorkflowInNewTab, validateOrSaveAllJsonFileMyWorkflows, } from "./utils"; import { Topbar } from "./topbar/Topbar"; -import { EShortcutKeys, Workflow, WorkflowVersion } from "./types/dbTypes"; +import { Workflow, WorkflowVersion } from "./types/dbTypes"; import { useDialog } from "./components/AlertDialogProvider"; import React from "react"; const RecentFilesDrawer = React.lazy( @@ -46,7 +45,7 @@ import { useStateRef } from "./customHooks/useStateRef"; import { indexdb } from "./db-tables/indexdb"; import EnableTwowaySyncConfirm from "./settings/EnableTwowaySyncConfirm"; import { deepJsonDiffCheck } from "./utils/deepJsonDiffCheck"; - +const AppEventListener = React.lazy(() => import("./topbar/AppEventListener")); const ModelManagerTopbar = React.lazy( () => import("./model-manager/topbar/ModelManagerTopbar"), ); @@ -70,12 +69,8 @@ export default function App() { const { showDialog } = useDialog(); const [loadChild, setLoadChild] = useState(false); const developmentEnvLoadFirst = useRef(false); - const autoSaveTimer = useRef(0); const toast = useToast(); - const workflowOverwriteNoticeStateRef = useRef("hide"); // disabled/hide/show; - const [jsonDiff, setJsonDiff] = useState(null); - const [curVersion, setCurVersion, curVersionRef] = - useStateRef(null); + const [curVersion, setCurVersion] = useStateRef(null); const saveCurWorkflow = useCallback(async () => { if (curFlowID.current) { if (workflowsTable?.curWorkflow?.saveLock) { @@ -120,6 +115,10 @@ export default function App() { }; const discardUnsavedChanges = async () => { + if (userSettingsTable?.autoSave) { + alert("You cannot discard unsaved changes when auto save is enabled"); + return; + } const userInput = confirm( "Are you sure you want to discard unsaved changes? This will revert current workflow to your last saved version. You will lose all changes made since your last save.", ); @@ -128,19 +127,14 @@ export default function App() { // User clicked OK const curID = workflowsTable?.curWorkflow?.id; if (curID == null) return; - let lastSavedJson; - if (userSettingsTable?.autoSave) { - lastSavedJson = (await changelogsTable?.getLastestByWorkflowID(curID)) - ?.json; - } else { - lastSavedJson = (await workflowsTable?.get(curID))?.json; - } + const lastSavedJson = (await workflowsTable?.get(curID))?.json; if (lastSavedJson) { workflowsTable?.updateCurWorkflow({ ...workflowsTable.curWorkflow!, json: lastSavedJson, }); await app.loadGraphData(JSON.parse(lastSavedJson)); + setIsDirty(false); } else { alert("Error: No last saved version found"); } @@ -189,24 +183,6 @@ export default function App() { if (latestWfID) { loadWorkflowIDImpl(latestWfID); } - // console.log("jjjj๐Ÿ˜‚j", app.graph); - document.addEventListener("click", (e) => { - console.log("app.canvas.", app.canvas); - if ( - app.canvas.node_over != null || - app.canvas.node_capturing_input != null || - app.canvas.node_widget != null - ) { - console.log("๐Ÿ™€on change node click!!"); - } - }); - // console.log("jjjj๐Ÿ˜‚j LGrpaoh", LGraph.prototype); - document.addEventListener("keyup", function (event) { - console.log("keyyyyyy app.canvas", app.canvas); - if (event.target?.matches("input, textarea")) { - console.log("๐ŸŽน๐ŸŽนon change keyboard click!!"); - } - }); await validateOrSaveAllJsonFileMyWorkflows(); indexdb.cache.get(UPGRADE_TO_2WAY_SYNC_KEY).then(async (value) => { @@ -275,10 +251,8 @@ export default function App() { const loadWorkflowIDImpl = async (id: string, versionID?: string | null) => { if (app.graph == null) { - console.error("app.graph is null cannot load workflow"); return; } - const flow = await workflowsTable?.get(id); // If the currently loaded flow does not exist, you need to clear the URL hash and localStorage to avoid popping up another prompt that the flow does not exist when refreshing the page. if (flow == null) { @@ -304,20 +278,9 @@ export default function App() { app.loadGraphData(JSON.parse(flow.json)); } setRoute("root"); - /** - * By an unlocked flow with isDirty true and unsaved, - * When switching to a locked flow, isDirty is still true, and checkIsDirty() is not executed in autoSaveTimer. - * causes * to be displayed in front of the topbar flow name, - * So add this logic and reset isDirty every time you open a new process - * In fact, it is reasonable to reset isDirty every time you open a new flow. - */ isDirty && setIsDirty(false); }; - const compareJsonDiff = (diff: { old: Object; new: Object } | null) => { - setJsonDiff(diff); - }; - const loadWorkflowID = async ( id: string | null, versionID?: string | null, @@ -464,72 +427,6 @@ export default function App() { } graphAppSetup(); setLoadChild(true); - autoSaveTimer.current = setInterval(async () => { - const autoSaveEnabled = userSettingsTable?.autoSave; - if (workflowsTable?.curWorkflow?.saveLock) return; - const isDirty = checkIsDirty(); - setIsDirty(!!isDirty); - if (!isDirty) { - return; - } - if ( - curVersionRef.current != null && - curVersionRef.current.json !== JSON.stringify(app.graph.serialize()) - ) { - setCurVersion(null); - } - if (curFlowID.current != null && autoSaveEnabled) { - const isLatest = await workflowsTable?.latestVersionCheck(); - if ( - workflowOverwriteNoticeStateRef.current !== "disabled" && - !isLatest - ) { - workflowOverwriteNoticeStateRef.current = "show"; - showDialog( - `This notification is to inform you that it appears you've opened the same workflow in multiple tabs. If all tabs are set to auto-save, this may lead to conflicts and potential data loss.\n\nYou might want to consider disabling auto-save to prevent this. We will remind you to save changes if you leave a page with unsaved work.`, - [ - { - label: "Do Not Remind Again", - onClick: () => { - workflowOverwriteNoticeStateRef.current = "disabled"; - }, - }, - { - label: "Disable Auto-Save", - colorScheme: "teal", - onClick: () => { - userSettingsTable?.upsert({ autoSave: false }); - }, - }, - ], - ); - } else { - // autosave workflow if enabled - const graphJson = JSON.stringify(app.graph.serialize()); - graphJson != null && - (await workflowsTable?.updateFlow(curFlowID.current!, { - json: graphJson, - })); - } - } - }, 1000); - - const shortcutListener = async (event: KeyboardEvent) => { - if (document.visibilityState === "hidden") return; - - const matchResult = await matchShortcut(event); - - switch (matchResult) { - case EShortcutKeys.SAVE: - saveCurWorkflow(); - break; - case EShortcutKeys.SAVE_AS: - setRoute("saveAsModal"); - break; - } - }; - - window.addEventListener("keydown", shortcutListener); const fileInput = document.getElementById( "comfy-file-input", @@ -605,13 +502,10 @@ export default function App() { app.canvasEl.addEventListener("drop", handleDrop); return () => { - window.removeEventListener("keydown", shortcutListener); window.removeEventListener("change", fileInputListener); window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener("executed", handleExecuted); app.canvasEl.removeEventListener("drop", handleDrop); - - clearInterval(autoSaveTimer.current); }; }, []); @@ -628,13 +522,13 @@ export default function App() { discardUnsavedChanges: discardUnsavedChanges, saveCurWorkflow: saveCurWorkflow, isDirty: isDirty, + setIsDirty: setIsDirty, loadNewWorkflow: loadNewWorkflow, loadFilePath: loadFilePath, setRoute: setRoute, route: route, - jsonDiff: jsonDiff, - compareJson: compareJsonDiff, curVersion: curVersion, + setCurVersion: setCurVersion, }} >
@@ -673,6 +567,7 @@ export default function App() { )} +
diff --git a/ui/src/WorkspaceContext.ts b/ui/src/WorkspaceContext.ts index 1ccdb31..59770d4 100644 --- a/ui/src/WorkspaceContext.ts +++ b/ui/src/WorkspaceContext.ts @@ -14,6 +14,7 @@ export const WorkspaceContext = createContext<{ versionID?: string | null, forceLoad?: boolean, ) => void; + setIsDirty: (dirty: boolean) => void; saveCurWorkflow: () => void; discardUnsavedChanges: () => Promise; isDirty: boolean; @@ -21,9 +22,8 @@ export const WorkspaceContext = createContext<{ loadFilePath: (path: string, overwriteCurrent?: boolean) => void; setRoute: (route: WorkspaceRoute) => void; route: WorkspaceRoute; - jsonDiff: JsonDiff; - compareJson: (jsonDiff: JsonDiff) => void; curVersion: WorkflowVersion | null; + setCurVersion: (version: WorkflowVersion | null) => void; }>({ curFlowID: null, loadWorkflowID: () => {}, @@ -34,9 +34,9 @@ export const WorkspaceContext = createContext<{ loadFilePath: () => {}, setRoute: () => {}, route: "root", - jsonDiff: null, - compareJson: () => {}, curVersion: null, + setIsDirty: () => {}, + setCurVersion: () => {}, }); export const RecentFilesContext = createContext<{ diff --git a/ui/src/components/DropdownTitle.tsx b/ui/src/components/DropdownTitle.tsx index 2701581..a2d623c 100644 --- a/ui/src/components/DropdownTitle.tsx +++ b/ui/src/components/DropdownTitle.tsx @@ -45,7 +45,6 @@ import HoverMenu from "./HoverMenu"; const ShareDialog = lazy(() => import("../share/ShareDialog")); // @ts-ignore import { app } from "/scripts/app.js"; -import JsonDiffCompareModal from "./JsonDiffCompareModal"; export default function DropdownTitle() { const { @@ -211,9 +210,6 @@ export default function DropdownTitle() { {route == "versionHistory" && ( setRoute("root")} /> )} - - - {route === "saveAsModal" && ( diff --git a/ui/src/components/JsonDiffCompareModal.tsx b/ui/src/components/JsonDiffCompareModal.tsx deleted file mode 100644 index 2b1af38..0000000 --- a/ui/src/components/JsonDiffCompareModal.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Box, Modal, ModalContent } from "@chakra-ui/react"; -import { useContext } from "react"; -import { WorkspaceContext } from "../WorkspaceContext"; - -export default function JsonDiffCompareModal() { - const { jsonDiff, compareJson } = useContext(WorkspaceContext); - - if (jsonDiff == null) { - return null; - } - return ( - { - compareJson(null); - }} - > - - - {/* TODO: add json comparison ui view */} - - - - ); -} diff --git a/ui/src/share/SharedTopbarButton.tsx b/ui/src/share/SharedTopbarButton.tsx index edd56a2..edc2aba 100644 --- a/ui/src/share/SharedTopbarButton.tsx +++ b/ui/src/share/SharedTopbarButton.tsx @@ -31,7 +31,7 @@ export function SharedTopbarButton({}) { - {privacy ? : "Shared"} + {privacy === "PUBLIC" ? "๐ŸŒ" : privacy === "UNLISTED" ? "๐Ÿ”—" : "๐Ÿ”’"} diff --git a/ui/src/topbar/AppEventListener.tsx b/ui/src/topbar/AppEventListener.tsx new file mode 100644 index 0000000..1fd9ce4 --- /dev/null +++ b/ui/src/topbar/AppEventListener.tsx @@ -0,0 +1,75 @@ +import { useContext, useEffect } from "react"; +// @ts-expect-error +import { app } from "/scripts/app.js"; +import { WorkspaceContext } from "../WorkspaceContext"; +import { userSettingsTable, workflowsTable } from "../db-tables/WorkspaceDB"; +import { useToast } from "@chakra-ui/react"; +import { matchShortcut } from "../utils"; +import { EShortcutKeys } from "../types/dbTypes"; + +export default function AppEventListener() { + const { setIsDirty, setRoute, saveCurWorkflow } = + useContext(WorkspaceContext); + const toast = useToast(); + useEffect(() => { + const shortcutListener = async (matchResult: string) => { + switch (matchResult) { + case EShortcutKeys.SAVE: + saveCurWorkflow(); + break; + case EShortcutKeys.SAVE_AS: + setRoute("saveAsModal"); + break; + } + }; + document.addEventListener("click", (e) => { + if ( + app.canvas.node_over != null || + app.canvas.node_capturing_input != null || + app.canvas.node_widget != null + ) { + onIsDirty(); + } + }); + document.addEventListener("keydown", async function (event) { + if (document.visibilityState === "hidden") return; + + const matchResult = await matchShortcut(event); + if (matchResult) { + shortcutListener(matchResult); + // @ts-expect-error + } else if (event.target?.matches("input, textarea")) { + onIsDirty(); + } + }); + }, []); + const onIsDirty = async () => { + if (workflowsTable?.curWorkflow?.saveLock) return; + const autoSaveEnabled = await userSettingsTable?.getSetting("autoSave"); + if (!autoSaveEnabled) { + setIsDirty(true); + } + if (workflowsTable?.curWorkflow?.id && autoSaveEnabled) { + const isLatest = await workflowsTable?.latestVersionCheck(); + if (!isLatest) { + toast({ + title: "You are working on an outdated version", + description: + "This workflow is changed by another tab. Please close the page and reopen to get the latest version.", + status: "warning", + duration: 5000, + isClosable: true, + }); + } else { + // autosave workflow if enabled + const graphJson = JSON.stringify(app.graph.serialize()); + graphJson != null && + (await workflowsTable?.updateFlow(workflowsTable.curWorkflow.id, { + json: graphJson, + })); + setIsDirty(false); + } + } + }; + return null; +} diff --git a/ui/src/topbar/VersionNameTopbar.tsx b/ui/src/topbar/VersionNameTopbar.tsx index 7c7fa8c..447abe1 100644 --- a/ui/src/topbar/VersionNameTopbar.tsx +++ b/ui/src/topbar/VersionNameTopbar.tsx @@ -1,16 +1,17 @@ import { Button, ButtonGroup, Flex, IconButton } from "@chakra-ui/react"; import { IconPlus, IconTriangleInvertedFilled } from "@tabler/icons-react"; -import { useContext } from "react"; +import { useContext, useEffect } from "react"; import { WorkspaceContext } from "../WorkspaceContext"; -export default function VersionNameTopbar( - { - // onClick, - }: { - // onClick: () => void; - }, -) { - const { route, setRoute, curVersion } = useContext(WorkspaceContext); +export default function VersionNameTopbar({}: {}) { + const { setRoute, curVersion, isDirty, setCurVersion } = + useContext(WorkspaceContext); + useEffect(() => { + if (isDirty) { + curVersion && + setCurVersion?.({ ...curVersion, name: curVersion.name + "*" }); + } + }, [isDirty]); if (!curVersion) { return null; } @@ -22,7 +23,7 @@ export default function VersionNameTopbar( rightIcon={} onClick={() => setRoute("versionHistory")} > - {curVersion?.name ?? "latest*"} + {curVersion.name}