From 7b64353aba99100f76ac994a06e3dbe88f529fca Mon Sep 17 00:00:00 2001 From: yanle <421361608@qq.com> Date: Fri, 29 Mar 2024 22:17:33 +0800 Subject: [PATCH 1/8] feat: multi tabs 30% complete --- ui/src/App.tsx | 17 +++- ui/src/topbar/Topbar.tsx | 19 +---- ui/src/topbar/multipleTabs/TabDataManager.ts | 74 ++++++++++++++++++ ui/src/topbar/multipleTabs/index.tsx | 81 ++++++++++++++++++++ ui/src/types/workspace.d.ts | 19 ++++- 5 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 ui/src/topbar/multipleTabs/TabDataManager.ts create mode 100644 ui/src/topbar/multipleTabs/index.tsx diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 7dafbbba..c2bfef1d 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -44,6 +44,7 @@ import { useStateRef } from "./customHooks/useStateRef"; import { indexdb } from "./db-tables/indexdb"; import EnableTwowaySyncConfirm from "./settings/EnableTwowaySyncConfirm"; import { deepJsonDiffCheck } from "./utils/deepJsonDiffCheck"; +import { OPEN_TAB_EVENT } from "./topbar/multipleTabs/TabDataManager"; const usedWsEvents = [ // InstallProgress.tsx @@ -181,7 +182,9 @@ export default function App() { latestWfID = localStorage.getItem("curFlowID"); } if (latestWfID) { - loadWorkflowIDImpl(latestWfID); + setTimeout(() => { + loadWorkflowIDImpl(latestWfID!); + }, 200); } // await validateOrSaveAllJsonFileMyWorkflows(); const twoway = await userSettingsTable?.getSetting("twoWaySync"); @@ -280,6 +283,18 @@ export default function App() { } setRoute("root"); isDirty && setIsDirty(false); + + document.dispatchEvent( + new CustomEvent(OPEN_TAB_EVENT, { + detail: { + tabInfo: { + id: flow.id, + name: flow.name, + json: version ? version.json : flow.json, + }, + }, + }), + ); }; const loadWorkflowID = async ( diff --git a/ui/src/topbar/Topbar.tsx b/ui/src/topbar/Topbar.tsx index 8943a1d7..1f34c032 100644 --- a/ui/src/topbar/Topbar.tsx +++ b/ui/src/topbar/Topbar.tsx @@ -7,7 +7,6 @@ import { IconPhoto, IconPlus, IconTriangleInvertedFilled, - IconLock, } from "@tabler/icons-react"; import DropdownTitle from "../components/DropdownTitle"; import { @@ -18,15 +17,15 @@ import { useEffect, useState, } from "react"; -import EditFlowName from "../components/EditFlowName"; import { WorkspaceContext } from "../WorkspaceContext"; import { PanelPosition } from "../types/dbTypes"; import "./Topbar.css"; import { SharedTopbarButton } from "../share/SharedTopbarButton"; import VersionNameTopbar from "./VersionNameTopbar"; -import { userSettingsTable, workflowsTable } from "../db-tables/WorkspaceDB"; +import { userSettingsTable } from "../db-tables/WorkspaceDB"; import { TOPBAR_BUTTON_HEIGHT } from "../const"; const AppIsDirtyEventListener = lazy(() => import("./AppIsDirtyEventListener")); +import MultipleTabs from "./multipleTabs"; const ModelManagerTopbar = lazy( () => import("../model-manager/topbar/ModelManagerTopbar"), ); @@ -131,19 +130,6 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) { - { - setCurFlowName(newName); - requestAnimationFrame(() => { - updatePanelPosition(); - }); - }} - /> - {workflowsTable?.curWorkflow?.saveLock && ( - - )} {curFlowID && ( @@ -173,6 +159,7 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) { ) : (
)} + diff --git a/ui/src/topbar/multipleTabs/TabDataManager.ts b/ui/src/topbar/multipleTabs/TabDataManager.ts new file mode 100644 index 00000000..71b7378c --- /dev/null +++ b/ui/src/topbar/multipleTabs/TabDataManager.ts @@ -0,0 +1,74 @@ +export const RE_RENDER_MULTIPLE_TABS_EVENT = "RE_RENDER_MULTIPLE_TABS_EVENT"; +export const OPEN_TAB_EVENT = "OPEN_TAB_EVENT"; + +export type TabData = { + id: string; + name: string; + json: string; + isDirty: boolean; +}; +type TabUpdateInput = Partial>; + +class TabDataManager { + tabs: TabData[] = []; + activeIndex: number = 0; + + setActiveIndex(index: number) { + this.activeIndex = index; + this.notifyChanges(); + } + + updateTabData(id: string, updateInput: TabUpdateInput) { + const tabIndex = this.tabs.findIndex((tab) => tab.id === id); + if (tabIndex !== -1) { + this.tabs[tabIndex] = { ...this.tabs[tabIndex], ...updateInput }; + this.notifyChanges(); + } + } + + addTabData(newTab: TabData) { + const existingIndex = this.tabs.findIndex((tab) => tab.id === newTab.id); + if (existingIndex !== -1) { + this.activeIndex = existingIndex; + } else { + const insertIndex = this.activeIndex + 1; + this.tabs.splice(insertIndex, 0, newTab); + this.activeIndex = insertIndex; + } + this.notifyChanges(); + } + + deleteTabData(index: number) { + if (index < 0 || index >= this.tabs.length) return; + + if (this.tabs.length === 1) { + this.tabs = []; + this.notifyChanges(); + return; + } + + this.tabs.splice(index, 1); + + if (index === this.activeIndex) { + this.activeIndex = Math.min(this.tabs.length - 1, this.activeIndex); + } else if (index < this.activeIndex) { + this.activeIndex--; + } + + this.notifyChanges(); + } + + private notifyChanges() { + console.log("Tab data or active index changed"); + const event = new CustomEvent(RE_RENDER_MULTIPLE_TABS_EVENT, { + detail: { + tabs: this.tabs, + activeIndex: this.activeIndex, + }, + }); + document.dispatchEvent(event); + } +} + +// Exporting a single instance +export const tabDataManager = new TabDataManager(); diff --git a/ui/src/topbar/multipleTabs/index.tsx b/ui/src/topbar/multipleTabs/index.tsx new file mode 100644 index 00000000..30437e95 --- /dev/null +++ b/ui/src/topbar/multipleTabs/index.tsx @@ -0,0 +1,81 @@ +import { + IconButton, + Button, + HStack, + Text, + useColorMode, + Box, +} from "@chakra-ui/react"; +import { IconLock, IconX } from "@tabler/icons-react"; +import { useState, useEffect, useContext, useReducer } from "react"; +import { WorkspaceContext } from "../../WorkspaceContext"; +import { + OPEN_TAB_EVENT, + RE_RENDER_MULTIPLE_TABS_EVENT, +} from "./TabDataManager"; +import { tabDataManager } from "./TabDataManager"; + +export default function MultipleTabs() { + const { colorMode } = useColorMode(); + const { curFlowID, isDirty } = useContext(WorkspaceContext); + const [, reRenderDispatch] = useReducer((state) => state + 1, 0); + + useEffect(() => { + const reRender = (e: CustomEvent) => { + const detail = e.detail; + console.log("Tabs changed:", detail.tabs); + console.log("Active index:", detail.activeIndex); + reRenderDispatch(); + }; + + const openTab = (e: CustomEvent) => { + const detail = e.detail; + tabDataManager.addTabData(detail.tabInfo); + }; + + document.addEventListener(RE_RENDER_MULTIPLE_TABS_EVENT, reRender); + document.addEventListener(OPEN_TAB_EVENT, openTab); + return () => { + document.removeEventListener(RE_RENDER_MULTIPLE_TABS_EVENT, reRender); + document.removeEventListener(OPEN_TAB_EVENT, reRender); + }; + }, []); + + return ( + + {tabDataManager.tabs.map((tab, index) => ( + + {tab.name} + { + tabDataManager.deleteTabData(index); + }} + icon={} + size={"xs"} + aria-label="close tab" + variant={"ghost"} + /> + + ))} + {/* { + setCurFlowName(newName); + requestAnimationFrame(() => { + updatePanelPosition(); + }); + }} + /> + {workflowsTable?.curWorkflow?.saveLock && ( + + )} */} + + ); +} diff --git a/ui/src/types/workspace.d.ts b/ui/src/types/workspace.d.ts index 1b836a73..78da11fb 100644 --- a/ui/src/types/workspace.d.ts +++ b/ui/src/types/workspace.d.ts @@ -1,12 +1,29 @@ import { SHORTCUT_TRIGGER_EVENT } from "../const"; +import { + OPEN_TAB_EVENT, + RE_RENDER_MULTIPLE_TABS_EVENT, + TabData, +} from "../topbar/multipleTabs/TabDataManager"; import { EOtherKeys, EShortcutKeys } from "./dbTypes"; interface ShortcutTriggerDetail { shortcutType: EShortcutKeys | EOtherKeys; } +interface reRenderTabsDetail { + tabs: TabData; + activeIndex: number; +} + +interface reRenderTabsDetail { + tabs: TabData; + activeIndex: number; +} + declare global { - interface WindowEventMap { + interface DocumentEventMap { [SHORTCUT_TRIGGER_EVENT]: CustomEvent; + [RE_RENDER_MULTIPLE_TABS_EVENT]: CustomEvent; + [OPEN_TAB_EVENT]: CustomEvent; } } From b2052ff2058deaf09cfff3df90b467ce463b3c77 Mon Sep 17 00:00:00 2001 From: Aller05 <421361608@qq.com> Date: Fri, 12 Apr 2024 08:50:51 +0000 Subject: [PATCH 2/8] feta: multi tab style --- ui/src/topbar/multipleTabs/TabDataManager.ts | 17 +-- ui/src/topbar/multipleTabs/index.css | 11 ++ ui/src/topbar/multipleTabs/index.tsx | 109 +++++++++++++++---- 3 files changed, 107 insertions(+), 30 deletions(-) create mode 100644 ui/src/topbar/multipleTabs/index.css diff --git a/ui/src/topbar/multipleTabs/TabDataManager.ts b/ui/src/topbar/multipleTabs/TabDataManager.ts index 71b7378c..1cda801a 100644 --- a/ui/src/topbar/multipleTabs/TabDataManager.ts +++ b/ui/src/topbar/multipleTabs/TabDataManager.ts @@ -18,22 +18,24 @@ class TabDataManager { this.notifyChanges(); } - updateTabData(id: string, updateInput: TabUpdateInput) { - const tabIndex = this.tabs.findIndex((tab) => tab.id === id); - if (tabIndex !== -1) { - this.tabs[tabIndex] = { ...this.tabs[tabIndex], ...updateInput }; + updateTabData(index: number, updateInput: TabUpdateInput) { + if (this.tabs[index]) { + this.tabs[index] = { ...this.tabs[index], ...updateInput }; this.notifyChanges(); } } addTabData(newTab: TabData) { const existingIndex = this.tabs.findIndex((tab) => tab.id === newTab.id); + // TODO: 新增和替换前,都需要在this.tabs中更新一下当前flow的json和isDirty if (existingIndex !== -1) { this.activeIndex = existingIndex; + // TODO: 根据activeIndex更新画布数据 } else { const insertIndex = this.activeIndex + 1; this.tabs.splice(insertIndex, 0, newTab); this.activeIndex = insertIndex; + // TODO: 根据activeIndex更新画布数据 } this.notifyChanges(); } @@ -43,7 +45,7 @@ class TabDataManager { if (this.tabs.length === 1) { this.tabs = []; - this.notifyChanges(); + this.notifyChanges('clearCanvas'); return; } @@ -51,6 +53,7 @@ class TabDataManager { if (index === this.activeIndex) { this.activeIndex = Math.min(this.tabs.length - 1, this.activeIndex); + // TODO: 根据activeIndex更新画布数据 } else if (index < this.activeIndex) { this.activeIndex--; } @@ -58,12 +61,12 @@ class TabDataManager { this.notifyChanges(); } - private notifyChanges() { - console.log("Tab data or active index changed"); + private notifyChanges(otherAction?: string) { const event = new CustomEvent(RE_RENDER_MULTIPLE_TABS_EVENT, { detail: { tabs: this.tabs, activeIndex: this.activeIndex, + otherAction }, }); document.dispatchEvent(event); diff --git a/ui/src/topbar/multipleTabs/index.css b/ui/src/topbar/multipleTabs/index.css new file mode 100644 index 00000000..54b964ab --- /dev/null +++ b/ui/src/topbar/multipleTabs/index.css @@ -0,0 +1,11 @@ +.workspace-multiple-tabs-item-action:hover .workspace-multiple-tabs-item-dirty { + display: none !important; +} + +.workspace-multiple-tabs-item-action:hover .workspace-multiple-tabs-item-dirty-close { + display: flex !important; +} + +.workspace-multiple-tabs-item:hover .workspace-multiple-tabs-item-close { + display: flex !important; +} \ No newline at end of file diff --git a/ui/src/topbar/multipleTabs/index.tsx b/ui/src/topbar/multipleTabs/index.tsx index 30437e95..76767814 100644 --- a/ui/src/topbar/multipleTabs/index.tsx +++ b/ui/src/topbar/multipleTabs/index.tsx @@ -6,7 +6,7 @@ import { useColorMode, Box, } from "@chakra-ui/react"; -import { IconLock, IconX } from "@tabler/icons-react"; +import { IconLock, IconX, IconAsterisk } from "@tabler/icons-react"; import { useState, useEffect, useContext, useReducer } from "react"; import { WorkspaceContext } from "../../WorkspaceContext"; import { @@ -14,18 +14,28 @@ import { RE_RENDER_MULTIPLE_TABS_EVENT, } from "./TabDataManager"; import { tabDataManager } from "./TabDataManager"; +import "./index.css"; export default function MultipleTabs() { const { colorMode } = useColorMode(); - const { curFlowID, isDirty } = useContext(WorkspaceContext); + const { curFlowID, isDirty, loadWorkflowID } = useContext(WorkspaceContext); const [, reRenderDispatch] = useReducer((state) => state + 1, 0); + useEffect(() => { + tabDataManager.updateTabData(tabDataManager.activeIndex, { isDirty }); + }, [isDirty]); + useEffect(() => { const reRender = (e: CustomEvent) => { const detail = e.detail; console.log("Tabs changed:", detail.tabs); console.log("Active index:", detail.activeIndex); reRenderDispatch(); + switch (detail.otherAction) { + case "clearCanvas": + loadWorkflowID(null); + break; + } }; const openTab = (e: CustomEvent) => { @@ -42,27 +52,80 @@ export default function MultipleTabs() { }, []); return ( - - {tabDataManager.tabs.map((tab, index) => ( - - {tab.name} - { - tabDataManager.deleteTabData(index); - }} - icon={} - size={"xs"} - aria-label="close tab" - variant={"ghost"} - /> - - ))} + + {tabDataManager.tabs.map((tab, index) => { + const isActive = tabDataManager.activeIndex === index; + const { id, name, isDirty } = tab; + return ( + + + {name} + + {isDirty ? ( + + } + size={"xs"} + aria-label="un save flag" + variant={"ghost"} + display="flex" + _hover={{ bg: "whiteAlpha.200" }} + className="workspace-multiple-tabs-item-dirty" + /> + { + tabDataManager.deleteTabData(index); + }} + icon={} + size={"xs"} + aria-label="close tab" + variant={"ghost"} + display="none" + _hover={{ bg: "whiteAlpha.200" }} + className="workspace-multiple-tabs-item-dirty-close" + /> + + ) : ( + { + tabDataManager.deleteTabData(index); + }} + icon={} + size={"xs"} + aria-label="close tab" + variant={"ghost"} + display="none" + position="absolute" + right="4px" + _hover={{ bg: "whiteAlpha.200" }} + className={isActive ? "" : "workspace-multiple-tabs-item-close"} + /> + )} + + ); + })} {/* Date: Mon, 15 Apr 2024 01:03:09 +0800 Subject: [PATCH 3/8] feat: multi-tab completed --- ui/src/App.tsx | 60 ++++----- ui/src/WorkspaceContext.ts | 10 +- ui/src/components/DropdownTitle.tsx | 6 +- ui/src/components/EditFlowName.tsx | 26 ++-- ui/src/topbar/Topbar.tsx | 10 +- ui/src/topbar/multipleTabs/TabDataManager.ts | 63 +++++++--- ui/src/topbar/multipleTabs/index.css | 24 +++- ui/src/topbar/multipleTabs/index.tsx | 125 +++++++++++++------ 8 files changed, 200 insertions(+), 124 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index d1b5be9e..aac4d932 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -44,10 +44,12 @@ import { useStateRef } from "./customHooks/useStateRef"; import { indexdb } from "./db-tables/indexdb"; import EnableTwowaySyncConfirm from "./settings/EnableTwowaySyncConfirm"; import { deepJsonDiffCheck } from "./utils/deepJsonDiffCheck"; -import { OPEN_TAB_EVENT } from "./topbar/multipleTabs/TabDataManager"; +import { + OPEN_TAB_EVENT, + tabDataManager, +} from "./topbar/multipleTabs/TabDataManager"; export default function App() { - const [curFlowName, setCurFlowName] = useState(null); const [route, setRoute] = useState("root"); const [loadingDB, setLoadingDB] = useState(true); const [flowID, setFlowID] = useState(null); @@ -60,8 +62,9 @@ export default function App() { const developmentEnvLoadFirst = useRef(false); const toast = useToast(); const [curVersion, setCurVersion] = useStateRef(null); - const saveCurWorkflow = useCallback(async () => { - if (curFlowID.current) { + const saveCurWorkflow = useCallback(async (id?: string) => { + const flowId = id || curFlowID.current; + if (flowId) { if (workflowsTable?.curWorkflow?.saveLock) { toast({ title: "The workflow is locked and cannot be saved", @@ -70,13 +73,14 @@ export default function App() { }); return; } + setIsDirty(false); const graphJson = JSON.stringify(app.graph.serialize()); await Promise.all([ - workflowsTable?.updateFlow(curFlowID.current, { + workflowsTable?.updateFlow(flowId, { json: graphJson, }), changelogsTable?.create({ - workflowID: curFlowID.current, + workflowID: flowId, json: graphJson, isAutoSave: false, }), @@ -88,9 +92,9 @@ export default function App() { duration: 1000, isClosable: true, }); - setIsDirty(false); } }, []); + const deleteCurWorkflow = async () => { if (curFlowID.current) { const userInput = confirm( @@ -137,7 +141,6 @@ export default function App() { workflowsTable?.updateCurWorkflow(workflow); curFlowID.current = id; setFlowID(id); - setCurFlowName(workflow?.name ?? ""); if (id == null) { document.title = "ComfyUI"; window.location.hash = ""; @@ -253,19 +256,18 @@ export default function App() { ? await workflowVersionsTable?.get(versionID) : null; - app.ui.dialog.close(); - if (version) { - setCurVersion(version); - setCurFlowIDAndName({ - ...flow, - json: version.json, - }); - app.loadGraphData(JSON.parse(version.json)); - } else { - setCurFlowIDAndName(flow); - setCurVersion(null); - app.loadGraphData(JSON.parse(flow.json)); + setCurVersion(version ?? null); + + const tabIndex = tabDataManager.tabs.findIndex((tab) => tab.id === id); + if (tabIndex !== -1) { + tabDataManager.changeActiveTab(tabIndex); + if (version) { + tabDataManager.updateTabData(tabIndex, { json: version.json }); + } + return; } + + app.ui.dialog.close(); setRoute("root"); isDirty && setIsDirty(false); @@ -285,7 +287,6 @@ export default function App() { const loadWorkflowID = async ( id: string | null, versionID?: string | null, - forceLoad: boolean = false, ) => { // No current workflow, id is null when you deleted current workflow if (id === null) { @@ -293,18 +294,10 @@ export default function App() { app.graph.clear(); return; } - if ( - !isDirty || - forceLoad || - userSettingsTable?.settings?.autoSave || - workflowsTable?.curWorkflow == null - ) { - loadWorkflowIDImpl(id, versionID); - return; - } - // prompt when auto save disabled and dirty - showSaveOrDiscardCurWorkflowDialog(id); + + loadWorkflowIDImpl(id, versionID); }; + const showSaveOrDiscardCurWorkflowDialog = async (newIDToLoad?: string) => { const buttons = [ newIDToLoad @@ -527,6 +520,7 @@ export default function App() { route: route, curVersion: curVersion, setCurVersion: setCurVersion, + setCurFlowIDAndName: setCurFlowIDAndName, }} >
@@ -541,7 +535,7 @@ export default function App() { zIndex={DRAWER_Z_INDEX} draggable={false} > - + {loadChild && route === "recentFlows" && ( void; - loadWorkflowID: ( - id: string | null, - versionID?: string | null, - forceLoad?: boolean, - ) => void; + loadWorkflowID: (id: string | null, versionID?: string | null) => void; setIsDirty: (dirty: boolean) => void; - saveCurWorkflow: () => void; + saveCurWorkflow: (id?: string) => void; discardUnsavedChanges: () => Promise; isDirty: boolean; loadNewWorkflow: (input?: { json: string; name?: string }) => void; @@ -24,6 +20,7 @@ export const WorkspaceContext = createContext<{ route: WorkspaceRoute; curVersion: WorkflowVersion | null; setCurVersion: (version: WorkflowVersion | null) => void; + setCurFlowIDAndName: (workflow: Workflow) => void; }>({ curFlowID: null, loadWorkflowID: () => {}, @@ -37,6 +34,7 @@ export const WorkspaceContext = createContext<{ curVersion: null, setIsDirty: () => {}, setCurVersion: () => {}, + setCurFlowIDAndName: () => {}, }); export const RecentFilesContext = createContext<{ diff --git a/ui/src/components/DropdownTitle.tsx b/ui/src/components/DropdownTitle.tsx index b691e561..50454832 100644 --- a/ui/src/components/DropdownTitle.tsx +++ b/ui/src/components/DropdownTitle.tsx @@ -99,7 +99,7 @@ export default function DropdownTitle() { parentFolderID: workflow?.parentFolderID, }); - flow && (await loadWorkflowID(flow.id, null, true)); + flow && (await loadWorkflowID(flow.id, null)); handleOnCloseModal(); }; @@ -140,7 +140,9 @@ export default function DropdownTitle() { { + saveCurWorkflow(); + }} icon={} iconSpacing={1} command={saveShortcut.save} diff --git a/ui/src/components/EditFlowName.tsx b/ui/src/components/EditFlowName.tsx index 52a003ef..71ebacd9 100644 --- a/ui/src/components/EditFlowName.tsx +++ b/ui/src/components/EditFlowName.tsx @@ -19,18 +19,14 @@ import { import { useContext, ChangeEvent, useState } from "react"; import { workflowsTable } from "../db-tables/WorkspaceDB"; import { WorkspaceContext } from "../WorkspaceContext"; +import { tabDataManager } from "../topbar/multipleTabs/TabDataManager"; type Props = { displayName: string; - updateFlowName: (newName: string) => void; - isDirty: boolean; + isActive: boolean; }; -export default function EditFlowName({ - displayName, - updateFlowName, - isDirty, -}: Props) { +export default function EditFlowName({ displayName, isActive }: Props) { const { isOpen, onOpen, onClose } = useDisclosure(); const { curFlowID } = useContext(WorkspaceContext); const [editName, setEditName] = useState(displayName); @@ -63,7 +59,10 @@ export default function EditFlowName({ await workflowsTable?.updateName(curFlowID, { name: trimEditName, }); - updateFlowName(trimEditName); + tabDataManager.updateTabData(tabDataManager.activeIndex, { + name: trimEditName, + }); + onCloseModal(); } } @@ -79,17 +78,14 @@ export default function EditFlowName({ -
- {isDirty && "* "} -
{displayName}
diff --git a/ui/src/topbar/Topbar.tsx b/ui/src/topbar/Topbar.tsx index 1f34c032..ab53ea10 100644 --- a/ui/src/topbar/Topbar.tsx +++ b/ui/src/topbar/Topbar.tsx @@ -31,11 +31,7 @@ const ModelManagerTopbar = lazy( ); const SpotlightSearch = lazy(() => import("../components/SpotlightSearch")); -interface Props { - curFlowName: string | null; - setCurFlowName: (newName: string) => void; -} -export function Topbar({ curFlowName, setCurFlowName }: Props) { +export function Topbar() { const { isDirty, loadNewWorkflow, @@ -148,7 +144,9 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) { { + saveCurWorkflow(); + }} icon={} size={"xs"} paddingY={4} diff --git a/ui/src/topbar/multipleTabs/TabDataManager.ts b/ui/src/topbar/multipleTabs/TabDataManager.ts index 1cda801a..563ca216 100644 --- a/ui/src/topbar/multipleTabs/TabDataManager.ts +++ b/ui/src/topbar/multipleTabs/TabDataManager.ts @@ -1,6 +1,7 @@ export const RE_RENDER_MULTIPLE_TABS_EVENT = "RE_RENDER_MULTIPLE_TABS_EVENT"; export const OPEN_TAB_EVENT = "OPEN_TAB_EVENT"; - +// @ts-ignore +import { app } from "/scripts/app.js"; export type TabData = { id: string; name: string; @@ -12,32 +13,56 @@ type TabUpdateInput = Partial>; class TabDataManager { tabs: TabData[] = []; activeIndex: number = 0; + activeTab: TabData | null = null; - setActiveIndex(index: number) { + changeActiveTab(index: number, needLoadNewFlow: boolean = true) { + this.saveCurTabJson(); this.activeIndex = index; - this.notifyChanges(); + this.activeTab = this.tabs[index]; + this.notifyChanges(needLoadNewFlow ? "loadNewFlow" : ""); } updateTabData(index: number, updateInput: TabUpdateInput) { if (this.tabs[index]) { this.tabs[index] = { ...this.tabs[index], ...updateInput }; - this.notifyChanges(); + this.activeTab = this.tabs[index]; + this.notifyChanges(updateInput.json ? "loadNewFlow" : ""); } } + private saveCurTabJson() { + const graphJson = JSON.stringify(app.graph.serialize()); + this.tabs[this.activeIndex].json = graphJson; + } + addTabData(newTab: TabData) { + const currentFlowIsDirty = this.activeTab?.isDirty; const existingIndex = this.tabs.findIndex((tab) => tab.id === newTab.id); - // TODO: 新增和替换前,都需要在this.tabs中更新一下当前flow的json和isDirty - if (existingIndex !== -1) { - this.activeIndex = existingIndex; - // TODO: 根据activeIndex更新画布数据 + + if (this.tabs.length === 0) { + this.tabs.push(newTab); + this.activeTab = newTab; } else { - const insertIndex = this.activeIndex + 1; - this.tabs.splice(insertIndex, 0, newTab); - this.activeIndex = insertIndex; - // TODO: 根据activeIndex更新画布数据 + let nextActiveIndex = -1; + + if (existingIndex !== -1) { + nextActiveIndex = existingIndex; + } else if (currentFlowIsDirty) { + nextActiveIndex = this.activeIndex + 1; + this.tabs.splice(nextActiveIndex, 0, newTab); + } + + if (nextActiveIndex >= 0) { + this.saveCurTabJson(); + this.activeIndex = nextActiveIndex; + this.activeTab = this.tabs[nextActiveIndex]; + } else { + this.updateTabData(this.activeIndex, newTab); + return; + } } - this.notifyChanges(); + + this.notifyChanges("loadNewFlow"); } deleteTabData(index: number) { @@ -45,7 +70,9 @@ class TabDataManager { if (this.tabs.length === 1) { this.tabs = []; - this.notifyChanges('clearCanvas'); + this.activeIndex = 0; + this.activeTab = null; + this.notifyChanges("clearCanvas"); return; } @@ -53,12 +80,12 @@ class TabDataManager { if (index === this.activeIndex) { this.activeIndex = Math.min(this.tabs.length - 1, this.activeIndex); - // TODO: 根据activeIndex更新画布数据 } else if (index < this.activeIndex) { this.activeIndex--; } + this.activeTab = this.tabs[this.activeIndex]; - this.notifyChanges(); + this.notifyChanges("loadNewFlow"); } private notifyChanges(otherAction?: string) { @@ -66,12 +93,12 @@ class TabDataManager { detail: { tabs: this.tabs, activeIndex: this.activeIndex, - otherAction + activeTab: this.activeTab, + otherAction, }, }); document.dispatchEvent(event); } } -// Exporting a single instance export const tabDataManager = new TabDataManager(); diff --git a/ui/src/topbar/multipleTabs/index.css b/ui/src/topbar/multipleTabs/index.css index 54b964ab..663e5eea 100644 --- a/ui/src/topbar/multipleTabs/index.css +++ b/ui/src/topbar/multipleTabs/index.css @@ -1,11 +1,25 @@ -.workspace-multiple-tabs-item-action:hover .workspace-multiple-tabs-item-dirty { - display: none !important; +.workspace-multiple-tabs::-webkit-scrollbar { + background-color: transparent; + width: 2px; + height: 2px; } -.workspace-multiple-tabs-item-action:hover .workspace-multiple-tabs-item-dirty-close { - display: flex !important; +.workspace-multiple-tabs::-webkit-scrollbar-thumb { + border-radius: 1em; + background-color: rgba(130, 129, 129, 0.3); +} + +.workspace-multiple-tabs + .workspace-multiple-tabs-item-action:hover + .workspace-multiple-tabs-item-dirty { + display: none !important; +} + +.workspace-multiple-tabs-item-action:hover + .workspace-multiple-tabs-item-dirty-close { + display: flex !important; } .workspace-multiple-tabs-item:hover .workspace-multiple-tabs-item-close { - display: flex !important; + display: flex !important; } \ No newline at end of file diff --git a/ui/src/topbar/multipleTabs/index.tsx b/ui/src/topbar/multipleTabs/index.tsx index 76767814..8032500d 100644 --- a/ui/src/topbar/multipleTabs/index.tsx +++ b/ui/src/topbar/multipleTabs/index.tsx @@ -1,13 +1,8 @@ -import { - IconButton, - Button, - HStack, - Text, - useColorMode, - Box, -} from "@chakra-ui/react"; -import { IconLock, IconX, IconAsterisk } from "@tabler/icons-react"; -import { useState, useEffect, useContext, useReducer } from "react"; +import { IconButton, HStack, Box } from "@chakra-ui/react"; +import { IconX, IconAsterisk, IconLock } from "@tabler/icons-react"; +import { useEffect, useContext, useReducer } from "react"; +// @ts-ignore +import { app } from "/scripts/app.js"; import { WorkspaceContext } from "../../WorkspaceContext"; import { OPEN_TAB_EVENT, @@ -15,12 +10,54 @@ import { } from "./TabDataManager"; import { tabDataManager } from "./TabDataManager"; import "./index.css"; +import { workflowsTable } from "../../db-tables/WorkspaceDB"; +import EditFlowName from "../../components/EditFlowName"; +import { useDialog } from "../../components/AlertDialogProvider"; export default function MultipleTabs() { - const { colorMode } = useColorMode(); - const { curFlowID, isDirty, loadWorkflowID } = useContext(WorkspaceContext); + const { showDialog } = useDialog(); + const { + setCurFlowIDAndName, + isDirty, + setCurVersion, + curVersion, + loadWorkflowID, + saveCurWorkflow, + } = useContext(WorkspaceContext); const [, reRenderDispatch] = useReducer((state) => state + 1, 0); + const onClose = (index: number) => { + const deleteTabInfo = tabDataManager.tabs[index]; + if (deleteTabInfo.isDirty) { + showDialog( + `Please save or discard your changes to "${deleteTabInfo.name}" before leaving, or your changes will be lost.`, + [ + { + label: "Save", + colorScheme: "teal", + onClick: async () => { + tabDataManager.changeActiveTab(index, false); + app.loadGraphData(JSON.parse(deleteTabInfo.json)); + setTimeout(async () => { + await saveCurWorkflow(deleteTabInfo.id); + tabDataManager.deleteTabData(index); + }, 10); + }, + }, + { + label: "Close", + colorScheme: "red", + onClick: async () => { + tabDataManager.deleteTabData(index); + }, + }, + ], + ); + } else { + tabDataManager.deleteTabData(index); + } + }; + useEffect(() => { tabDataManager.updateTabData(tabDataManager.activeIndex, { isDirty }); }, [isDirty]); @@ -28,19 +65,30 @@ export default function MultipleTabs() { useEffect(() => { const reRender = (e: CustomEvent) => { const detail = e.detail; - console.log("Tabs changed:", detail.tabs); - console.log("Active index:", detail.activeIndex); reRenderDispatch(); switch (detail.otherAction) { case "clearCanvas": loadWorkflowID(null); break; + case "loadNewFlow": + app.loadGraphData(JSON.parse(detail.activeTab.json)); + workflowsTable?.get(detail.activeTab.id).then((flow) => { + flow && + setCurFlowIDAndName({ + ...flow, + json: detail.activeTab.json, + }); + }); + curVersion && setCurVersion(null); + break; } }; const openTab = (e: CustomEvent) => { const detail = e.detail; - tabDataManager.addTabData(detail.tabInfo); + if (detail.tabInfo.id !== tabDataManager.activeTab?.id) { + tabDataManager.addTabData(detail.tabInfo); + } }; document.addEventListener(RE_RENDER_MULTIPLE_TABS_EVENT, reRender); @@ -52,7 +100,12 @@ export default function MultipleTabs() { }, []); return ( - + {tabDataManager.tabs.map((tab, index) => { const isActive = tabDataManager.activeIndex === index; const { id, name, isDirty } = tab; @@ -75,10 +128,12 @@ export default function MultipleTabs() { bg={isActive ? "#1f1f1f" : "#181818"} _hover={{ bg: "#1f1f1f" }} _last={{ borderRightWidth: 1 }} + onClick={() => { + tabDataManager.activeIndex !== index && + tabDataManager.changeActiveTab(index); + }} > - - {name} - + {isDirty ? ( { - tabDataManager.deleteTabData(index); + onClick={(e) => { + e.stopPropagation(); + onClose(index); }} icon={} size={"xs"} aria-label="close tab" variant={"ghost"} - display="none" _hover={{ bg: "whiteAlpha.200" }} + display="none" className="workspace-multiple-tabs-item-dirty-close" /> ) : ( { - tabDataManager.deleteTabData(index); + onClick={(e) => { + e.stopPropagation(); + onClose(index); }} icon={} size={"xs"} aria-label="close tab" variant={"ghost"} - display="none" + _hover={{ bg: "whiteAlpha.200" }} + display={isActive ? "flex" : "none"} position="absolute" right="4px" - _hover={{ bg: "whiteAlpha.200" }} className={isActive ? "" : "workspace-multiple-tabs-item-close"} /> )} + {isActive && workflowsTable?.curWorkflow?.saveLock && ( + + )} ); })} - {/* { - setCurFlowName(newName); - requestAnimationFrame(() => { - updatePanelPosition(); - }); - }} - /> - {workflowsTable?.curWorkflow?.saveLock && ( - - )} */} ); } From 87b82d6f2d8e29d64da31b5bcb52c672921ed8e9 Mon Sep 17 00:00:00 2001 From: Aller05 <421361608@qq.com> Date: Wed, 17 Apr 2024 10:18:59 +0000 Subject: [PATCH 4/8] feat: pr problem fix --- ui/src/App.tsx | 62 ++++++++++++------- .../MultipleSelectionOperation.tsx | 2 + .../RecentFilesDrawer/RecentFilesDrawer.tsx | 10 +-- ui/src/WorkspaceContext.ts | 2 +- ui/src/components/SpotlightSearch.tsx | 4 +- ui/src/topbar/multipleTabs/TabDataManager.ts | 18 ++++++ ui/src/topbar/multipleTabs/index.tsx | 18 ++++-- 7 files changed, 79 insertions(+), 37 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 8c386c64..a26d9820 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -53,7 +53,6 @@ export default function App() { const [route, setRoute] = useState("root"); const [loadingDB, setLoadingDB] = useState(true); const [flowID, setFlowID] = useState(null); - const curFlowID = useRef(null); const [isDirty, setIsDirty] = useState(false); const workspaceContainerRef = useRef(null); @@ -62,8 +61,8 @@ export default function App() { const developmentEnvLoadFirst = useRef(false); const toast = useToast(); const [curVersion, setCurVersion] = useStateRef(null); - const saveCurWorkflow = useCallback(async (id?: string) => { - const flowId = id || curFlowID.current; + const saveCurWorkflow = useCallback(async (newGraphJson?: string) => { + const flowId = workflowsTable?.curWorkflow?.id; if (flowId) { if (workflowsTable?.curWorkflow?.saveLock) { toast({ @@ -74,7 +73,7 @@ export default function App() { return; } setIsDirty(false); - const graphJson = JSON.stringify(app.graph.serialize()); + const graphJson = newGraphJson || JSON.stringify(app.graph.serialize()); await Promise.all([ workflowsTable?.updateFlow(flowId, { json: graphJson, @@ -126,7 +125,6 @@ export default function App() { // curID null is when you deleted current workflow const id = workflow?.id ?? null; workflowsTable?.updateCurWorkflow(workflow); - curFlowID.current = id; setFlowID(id); if (id == null) { document.title = "ComfyUI"; @@ -163,9 +161,7 @@ export default function App() { latestWfID = localStorage.getItem("curFlowID"); } if (latestWfID) { - setTimeout(() => { - loadWorkflowIDImpl(latestWfID!); - }, 200); + loadWorkflowIDImpl(latestWfID!, null, true); } fetch("/workspace/deduplicate_workflow_ids"); const twoway = await userSettingsTable?.getSetting("twoWaySync"); @@ -213,7 +209,7 @@ export default function App() { }; const checkIsDirty = () => { - if (curFlowID.current == null) return false; + if (workflowsTable?.curWorkflow?.id == null) return false; const curflow = workflowsTable?.curWorkflow; if (curflow == null) return false; if (curflow.json == null) return true; @@ -228,7 +224,11 @@ export default function App() { return !equal; }; - const loadWorkflowIDImpl = async (id: string, versionID?: string | null) => { + const loadWorkflowIDImpl = async ( + id: string, + versionID?: string | null, + isInit?: boolean, + ) => { if (app.graph == null) { return; } @@ -258,17 +258,35 @@ export default function App() { setRoute("root"); isDirty && setIsDirty(false); - document.dispatchEvent( - new CustomEvent(OPEN_TAB_EVENT, { - detail: { - tabInfo: { - id: flow.id, - name: flow.name, - json: version ? version.json : flow.json, + const newTabInfo = { + id: flow.id, + name: flow.name, + json: version ? version.json : flow.json, + isDirty: false, + }; + + /** + * During initialization, + * because the event listener of OPEN_TAB_EVENT has not been registered yet, + * the newly added tab will be invalid, + * so the tab is added manually during initialization. + */ + if (isInit) { + tabDataManager.addTabData(newTabInfo); + app.loadGraphData(JSON.parse(newTabInfo.json)); + setCurFlowIDAndName({ + ...flow, + json: newTabInfo.json, + }); + } else { + document.dispatchEvent( + new CustomEvent(OPEN_TAB_EVENT, { + detail: { + tabInfo: newTabInfo, }, - }, - }), - ); + }), + ); + } }; const loadWorkflowID = async ( @@ -365,13 +383,13 @@ export default function App() { }; const onExecutedCreateMedia = useCallback((image: any) => { - if (curFlowID.current == null) return; + if (workflowsTable?.curWorkflow?.id == null) return; let path = image.filename; if (image.subfolder != null && image.subfolder !== "") { path = image.subfolder + "/" + path; } mediaTable?.create({ - workflowID: curFlowID.current, + workflowID: workflowsTable?.curWorkflow?.id, localPath: path, }); }, []); diff --git a/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx b/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx index 76c27334..d67d73ca 100644 --- a/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx +++ b/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx @@ -4,6 +4,7 @@ import DeleteConfirm from "../components/DeleteConfirm"; import { ChangeEvent } from "react"; import { workflowsTable } from "../db-tables/WorkspaceDB"; import { downloadWorkflowsZip } from "../utils/downloadWorkflowsZip"; +import { tabDataManager } from "../topbar/multipleTabs/TabDataManager"; type Props = { multipleState: boolean; @@ -57,6 +58,7 @@ export default function MultipleSelectionOperation(props: Props) { onDelete={async () => { await workflowsTable?.batchDeleteFlow(selectedKeys); batchOperationCallback("batchDelete"); + tabDataManager.batchDeleteTabData(selectedKeys); }} /> diff --git a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx index c1eeeddc..74a7e388 100644 --- a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx +++ b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx @@ -40,6 +40,7 @@ import MyTagsRow from "./MyTagsRow"; import ImportFlowsFileInput from "./ImportFlowsFileInput"; import ItemsList from "./ItemsList"; import { DRAWER_Z_INDEX } from "../const"; +import { tabDataManager } from "../topbar/multipleTabs/TabDataManager"; type Props = { onClose: () => void; @@ -51,7 +52,7 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { Array >([]); const allFlowsRef = useRef>([]); - const { loadWorkflowID, curFlowID } = useContext(WorkspaceContext); + const { loadWorkflowID } = useContext(WorkspaceContext); const [selectedTag, setSelectedTag] = useState(); const [multipleState, setMultipleState] = useState(false); const [selectedKeys, setSelectedKeys] = useState([]); @@ -115,6 +116,7 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { await workflowsTable?.deleteFlow(id); if (workflowsTable?.curWorkflow?.id === id) { await loadWorkflowID?.(null); + tabDataManager?.deleteTabData(tabDataManager.activeIndex); } await loadLatestWorkflows(); }, @@ -155,12 +157,6 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { const batchOperationCallback = (type: string, value: unknown) => { switch (type) { case "batchDelete": - curFlowID && - workflowsTable?.get(curFlowID).then((res) => { - if (!res) { - loadWorkflowID?.(null); - } - }); loadLatestWorkflows(); setMultipleState(false); setSelectedKeys([]); diff --git a/ui/src/WorkspaceContext.ts b/ui/src/WorkspaceContext.ts index 112840c0..248301d6 100644 --- a/ui/src/WorkspaceContext.ts +++ b/ui/src/WorkspaceContext.ts @@ -11,7 +11,7 @@ export const WorkspaceContext = createContext<{ onDuplicateWorkflow?: (flowID: string, newFlowName?: string) => void; loadWorkflowID: (id: string | null, versionID?: string | null) => void; setIsDirty: (dirty: boolean) => void; - saveCurWorkflow: (id?: string) => Promise; + saveCurWorkflow: (saveCurWorkflow?: string) => Promise; discardUnsavedChanges: () => Promise; isDirty: boolean; loadNewWorkflow: (input?: { json: string; name?: string }) => void; diff --git a/ui/src/components/SpotlightSearch.tsx b/ui/src/components/SpotlightSearch.tsx index 576723a4..e87e9f40 100644 --- a/ui/src/components/SpotlightSearch.tsx +++ b/ui/src/components/SpotlightSearch.tsx @@ -219,12 +219,12 @@ export default function SpotlightSearch() { document.addEventListener("keyup", handleKeyUp); document.addEventListener("keydown", keydownListener); - window.addEventListener(SHORTCUT_TRIGGER_EVENT, shortcutTrigger); + document.addEventListener(SHORTCUT_TRIGGER_EVENT, shortcutTrigger); return () => { document.removeEventListener("keyup", handleKeyUp); document.removeEventListener("keydown", keydownListener); - window.removeEventListener(SHORTCUT_TRIGGER_EVENT, shortcutTrigger); + document.removeEventListener(SHORTCUT_TRIGGER_EVENT, shortcutTrigger); }; }, []); diff --git a/ui/src/topbar/multipleTabs/TabDataManager.ts b/ui/src/topbar/multipleTabs/TabDataManager.ts index 563ca216..dcea900a 100644 --- a/ui/src/topbar/multipleTabs/TabDataManager.ts +++ b/ui/src/topbar/multipleTabs/TabDataManager.ts @@ -88,6 +88,24 @@ class TabDataManager { this.notifyChanges("loadNewFlow"); } + batchDeleteTabData(ids: string[]) { + if (this.tabs.length > 0) { + this.tabs = this.tabs.filter(tab => !ids.includes(tab.id)); + if (this.tabs.length === 0) { + this.activeIndex = 0; + this.activeTab = null; + this.notifyChanges("clearCanvas"); + return; + } else if (this.activeTab && ids.includes(this.activeTab.id)) { + this.activeIndex = this.tabs.length - 1; + this.activeTab = this.tabs[this.activeIndex]; + this.notifyChanges("loadNewFlow"); + return; + } + this.notifyChanges(); + } + } + private notifyChanges(otherAction?: string) { const event = new CustomEvent(RE_RENDER_MULTIPLE_TABS_EVENT, { detail: { diff --git a/ui/src/topbar/multipleTabs/index.tsx b/ui/src/topbar/multipleTabs/index.tsx index 8032500d..bdfa447e 100644 --- a/ui/src/topbar/multipleTabs/index.tsx +++ b/ui/src/topbar/multipleTabs/index.tsx @@ -36,12 +36,20 @@ export default function MultipleTabs() { label: "Save", colorScheme: "teal", onClick: async () => { - tabDataManager.changeActiveTab(index, false); - app.loadGraphData(JSON.parse(deleteTabInfo.json)); - setTimeout(async () => { - await saveCurWorkflow(deleteTabInfo.id); + const deleteFlow = await workflowsTable?.get(deleteTabInfo.id); + if (deleteFlow) { + tabDataManager.changeActiveTab(index, false); + /** + * Get the deleted flow information and update updateCurWorkflow, + * Because workflowsTable.curWorkflow.id needs to be used when saving + */ + workflowsTable?.updateCurWorkflow({ + ...deleteFlow, + json: deleteTabInfo.json, + }); + await saveCurWorkflow(deleteTabInfo.json); tabDataManager.deleteTabData(index); - }, 10); + } }, }, { From b37e9b64492ef83bef2e6792921a1df807f892d3 Mon Sep 17 00:00:00 2001 From: Aller05 <421361608@qq.com> Date: Thu, 18 Apr 2024 01:50:10 +0000 Subject: [PATCH 5/8] feat: when dragging and loading the flow, update the tab --- ui/src/App.tsx | 14 ++++++++++++-- ui/src/topbar/multipleTabs/TabDataManager.ts | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index a26d9820..436f3d53 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -428,7 +428,12 @@ export default function App() { createTime: Date.now(), }; setCurFlowIDAndName(newFlow); - await workflowsTable?.createFlow(newFlow); + const res = await workflowsTable?.createFlow(newFlow); + res && + tabDataManager.updateTabData(tabDataManager.activeIndex, { + id: res.id, + name: res.name, + }); } }; fileInput?.addEventListener("change", fileInputListener); @@ -483,7 +488,12 @@ export default function App() { createTime: Date.now(), }; setCurFlowIDAndName(newFlow); - await workflowsTable?.createFlow(newFlow); + const res = await workflowsTable?.createFlow(newFlow); + res && + tabDataManager.updateTabData(tabDataManager.activeIndex, { + id: res.id, + name: res.name, + }); } }; app.canvasEl.addEventListener("drop", handleDrop); diff --git a/ui/src/topbar/multipleTabs/TabDataManager.ts b/ui/src/topbar/multipleTabs/TabDataManager.ts index dcea900a..85071d4d 100644 --- a/ui/src/topbar/multipleTabs/TabDataManager.ts +++ b/ui/src/topbar/multipleTabs/TabDataManager.ts @@ -8,7 +8,7 @@ export type TabData = { json: string; isDirty: boolean; }; -type TabUpdateInput = Partial>; +type TabUpdateInput = Partial; class TabDataManager { tabs: TabData[] = []; From d80e791547360087b07744dfa0e46c66fc8880ad Mon Sep 17 00:00:00 2001 From: Aller05 <421361608@qq.com> Date: Thu, 18 Apr 2024 02:42:09 +0000 Subject: [PATCH 6/8] feat: delete workflow and update mulit tab at the same time --- .../MultipleSelectionOperation.tsx | 2 -- .../RecentFilesDrawer/RecentFilesDrawer.tsx | 6 ------ ui/src/components/EditFlowName.tsx | 5 ----- ui/src/db-tables/WorkflowsTable.ts | 9 +++++++++ ui/src/topbar/multipleTabs/TabDataManager.ts | 19 ++++++++++++++----- ui/src/topbar/multipleTabs/index.tsx | 2 ++ 6 files changed, 25 insertions(+), 18 deletions(-) diff --git a/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx b/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx index d67d73ca..76c27334 100644 --- a/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx +++ b/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx @@ -4,7 +4,6 @@ import DeleteConfirm from "../components/DeleteConfirm"; import { ChangeEvent } from "react"; import { workflowsTable } from "../db-tables/WorkspaceDB"; import { downloadWorkflowsZip } from "../utils/downloadWorkflowsZip"; -import { tabDataManager } from "../topbar/multipleTabs/TabDataManager"; type Props = { multipleState: boolean; @@ -58,7 +57,6 @@ export default function MultipleSelectionOperation(props: Props) { onDelete={async () => { await workflowsTable?.batchDeleteFlow(selectedKeys); batchOperationCallback("batchDelete"); - tabDataManager.batchDeleteTabData(selectedKeys); }} /> diff --git a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx index 74a7e388..2030ce0e 100644 --- a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx +++ b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx @@ -40,7 +40,6 @@ import MyTagsRow from "./MyTagsRow"; import ImportFlowsFileInput from "./ImportFlowsFileInput"; import ItemsList from "./ItemsList"; import { DRAWER_Z_INDEX } from "../const"; -import { tabDataManager } from "../topbar/multipleTabs/TabDataManager"; type Props = { onClose: () => void; @@ -52,7 +51,6 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { Array >([]); const allFlowsRef = useRef>([]); - const { loadWorkflowID } = useContext(WorkspaceContext); const [selectedTag, setSelectedTag] = useState(); const [multipleState, setMultipleState] = useState(false); const [selectedKeys, setSelectedKeys] = useState([]); @@ -114,10 +112,6 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { const onDelete = useCallback( async (id: string) => { await workflowsTable?.deleteFlow(id); - if (workflowsTable?.curWorkflow?.id === id) { - await loadWorkflowID?.(null); - tabDataManager?.deleteTabData(tabDataManager.activeIndex); - } await loadLatestWorkflows(); }, [selectedTag, debounceSearchValue], diff --git a/ui/src/components/EditFlowName.tsx b/ui/src/components/EditFlowName.tsx index 71ebacd9..54763886 100644 --- a/ui/src/components/EditFlowName.tsx +++ b/ui/src/components/EditFlowName.tsx @@ -19,7 +19,6 @@ import { import { useContext, ChangeEvent, useState } from "react"; import { workflowsTable } from "../db-tables/WorkspaceDB"; import { WorkspaceContext } from "../WorkspaceContext"; -import { tabDataManager } from "../topbar/multipleTabs/TabDataManager"; type Props = { displayName: string; @@ -59,10 +58,6 @@ export default function EditFlowName({ displayName, isActive }: Props) { await workflowsTable?.updateName(curFlowID, { name: trimEditName, }); - tabDataManager.updateTabData(tabDataManager.activeIndex, { - name: trimEditName, - }); - onCloseModal(); } } diff --git a/ui/src/db-tables/WorkflowsTable.ts b/ui/src/db-tables/WorkflowsTable.ts index f2a71be1..0b8e7ddb 100644 --- a/ui/src/db-tables/WorkflowsTable.ts +++ b/ui/src/db-tables/WorkflowsTable.ts @@ -21,6 +21,7 @@ import { scanLocalFiles, } from "../apis/TwowaySyncApi"; import { COMFYSPACE_TRACKING_FIELD_NAME } from "../const"; +import { tabDataManager } from "../topbar/multipleTabs/TabDataManager"; export class WorkflowsTable extends TableBase { static readonly TABLE_NAME = "workflows"; @@ -219,6 +220,7 @@ export class WorkflowsTable extends TableBase { before && (await TwowaySyncAPI.renameWorkflow(before, change.name + ".json")); } + tabDataManager.updateTabData(tabDataManager.activeIndex, change); return await this.updateMetaInfo(id, change as any); } public async updateFlow(id: string, input: Pick) { @@ -318,6 +320,12 @@ export class WorkflowsTable extends TableBase { deleteJsonFileMyWorkflows({ ...workflow }); } } + + const tabIndex = tabDataManager.tabs.findIndex((tab) => tab.id === id); + if (tabIndex !== -1) { + tabDataManager?.deleteTabData(tabIndex); + } + //add to IndexDB await indexdb.workflows.delete(id); this.saveDiskDB(); @@ -336,6 +344,7 @@ export class WorkflowsTable extends TableBase { } } await indexdb.workflows.bulkDelete(ids); + tabDataManager.batchDeleteTabData(ids); this.saveDiskDB(); } diff --git a/ui/src/topbar/multipleTabs/TabDataManager.ts b/ui/src/topbar/multipleTabs/TabDataManager.ts index 85071d4d..f49ac947 100644 --- a/ui/src/topbar/multipleTabs/TabDataManager.ts +++ b/ui/src/topbar/multipleTabs/TabDataManager.ts @@ -96,12 +96,21 @@ class TabDataManager { this.activeTab = null; this.notifyChanges("clearCanvas"); return; - } else if (this.activeTab && ids.includes(this.activeTab.id)) { - this.activeIndex = this.tabs.length - 1; - this.activeTab = this.tabs[this.activeIndex]; - this.notifyChanges("loadNewFlow"); - return; + } else { + const activeIndex = this.tabs.findIndex(tab => tab.id === this.activeTab?.id); + if (activeIndex === -1) { + this.activeIndex = this.tabs.length - 1; + this.activeTab = this.tabs[this.activeIndex]; + this.notifyChanges("loadNewFlow"); + return; + } else if (activeIndex !== this.activeIndex) { + this.activeIndex = activeIndex; + this.activeTab = this.tabs[activeIndex]; + this.notifyChanges(); + return; + } } + this.notifyChanges(); } } diff --git a/ui/src/topbar/multipleTabs/index.tsx b/ui/src/topbar/multipleTabs/index.tsx index bdfa447e..ee8eb323 100644 --- a/ui/src/topbar/multipleTabs/index.tsx +++ b/ui/src/topbar/multipleTabs/index.tsx @@ -19,6 +19,7 @@ export default function MultipleTabs() { const { setCurFlowIDAndName, isDirty, + setIsDirty, setCurVersion, curVersion, loadWorkflowID, @@ -87,6 +88,7 @@ export default function MultipleTabs() { json: detail.activeTab.json, }); }); + setIsDirty(detail.activeTab.isDirty); curVersion && setCurVersion(null); break; } From 316f5c1def82dcca30af19d136886a6c57e3534d Mon Sep 17 00:00:00 2001 From: Aller05 <421361608@qq.com> Date: Thu, 18 Apr 2024 05:10:19 +0000 Subject: [PATCH 7/8] feat: multi tab supports automatic scrolling, tab deletion logic optimization --- ui/src/App.tsx | 2 +- ui/src/topbar/multipleTabs/TabDataManager.ts | 14 +++++++------- ui/src/topbar/multipleTabs/index.tsx | 7 ++++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 436f3d53..f665e23f 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -72,7 +72,7 @@ export default function App() { }); return; } - setIsDirty(false); + !newGraphJson && setIsDirty(false); const graphJson = newGraphJson || JSON.stringify(app.graph.serialize()); await Promise.all([ workflowsTable?.updateFlow(flowId, { diff --git a/ui/src/topbar/multipleTabs/TabDataManager.ts b/ui/src/topbar/multipleTabs/TabDataManager.ts index f49ac947..0e6fb3e5 100644 --- a/ui/src/topbar/multipleTabs/TabDataManager.ts +++ b/ui/src/topbar/multipleTabs/TabDataManager.ts @@ -78,14 +78,14 @@ class TabDataManager { this.tabs.splice(index, 1); - if (index === this.activeIndex) { - this.activeIndex = Math.min(this.tabs.length - 1, this.activeIndex); - } else if (index < this.activeIndex) { - this.activeIndex--; + if (index <= this.activeIndex) { + const isDeleteActiveTab = index === this.activeIndex; + this.activeIndex = isDeleteActiveTab ? Math.min(this.tabs.length - 1, this.activeIndex) : this.activeIndex - 1; + this.activeTab = this.tabs[this.activeIndex]; + this.notifyChanges(isDeleteActiveTab ? "loadNewFlow" : ''); + return; } - this.activeTab = this.tabs[this.activeIndex]; - - this.notifyChanges("loadNewFlow"); + this.notifyChanges(''); } batchDeleteTabData(ids: string[]) { diff --git a/ui/src/topbar/multipleTabs/index.tsx b/ui/src/topbar/multipleTabs/index.tsx index ee8eb323..a92d0a63 100644 --- a/ui/src/topbar/multipleTabs/index.tsx +++ b/ui/src/topbar/multipleTabs/index.tsx @@ -39,7 +39,6 @@ export default function MultipleTabs() { onClick: async () => { const deleteFlow = await workflowsTable?.get(deleteTabInfo.id); if (deleteFlow) { - tabDataManager.changeActiveTab(index, false); /** * Get the deleted flow information and update updateCurWorkflow, * Because workflowsTable.curWorkflow.id needs to be used when saving @@ -90,6 +89,11 @@ export default function MultipleTabs() { }); setIsDirty(detail.activeTab.isDirty); curVersion && setCurVersion(null); + document + .getElementById( + `workspace-multiple-tabs-item-${detail.activeTab.id}`, + ) + ?.scrollIntoView({ behavior: "smooth" }); break; } }; @@ -123,6 +127,7 @@ export default function MultipleTabs() { Date: Thu, 18 Apr 2024 05:25:04 +0000 Subject: [PATCH 8/8] feat: multi tab scrolling optimization --- ui/src/components/EditFlowName.tsx | 1 + ui/src/topbar/multipleTabs/index.tsx | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ui/src/components/EditFlowName.tsx b/ui/src/components/EditFlowName.tsx index 54763886..c5b1cb15 100644 --- a/ui/src/components/EditFlowName.tsx +++ b/ui/src/components/EditFlowName.tsx @@ -80,6 +80,7 @@ export default function EditFlowName({ displayName, isActive }: Props) { whiteSpace="nowrap" overflow="hidden" textOverflow="ellipsis" + userSelect="none" > {displayName} diff --git a/ui/src/topbar/multipleTabs/index.tsx b/ui/src/topbar/multipleTabs/index.tsx index a92d0a63..74c1bb68 100644 --- a/ui/src/topbar/multipleTabs/index.tsx +++ b/ui/src/topbar/multipleTabs/index.tsx @@ -89,11 +89,14 @@ export default function MultipleTabs() { }); setIsDirty(detail.activeTab.isDirty); curVersion && setCurVersion(null); - document - .getElementById( - `workspace-multiple-tabs-item-${detail.activeTab.id}`, - ) - ?.scrollIntoView({ behavior: "smooth" }); + // Avoid scrolling when the corresponding element has not yet been rendered. + setTimeout(() => { + document + .getElementById( + `workspace-multiple-tabs-item-${detail.activeTab.id}`, + ) + ?.scrollIntoView({ behavior: "smooth" }); + }, 0); break; } };