diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 8371dece..f665e23f 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -44,13 +44,15 @@ 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, + 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); - const curFlowID = useRef(null); const [isDirty, setIsDirty] = useState(false); const workspaceContainerRef = useRef(null); @@ -59,9 +61,10 @@ export default function App() { const developmentEnvLoadFirst = useRef(false); const toast = useToast(); const [curVersion, setCurVersion] = useStateRef(null); - const saveCurWorkflow = useCallback(async (force = false) => { - if (curFlowID.current) { - if (!force && workflowsTable?.curWorkflow?.saveLock) { + const saveCurWorkflow = useCallback(async (newGraphJson?: string) => { + const flowId = workflowsTable?.curWorkflow?.id; + if (flowId) { + if (workflowsTable?.curWorkflow?.saveLock) { toast({ title: "The workflow is locked and cannot be saved", status: "warning", @@ -69,13 +72,14 @@ export default function App() { }); return; } - const graphJson = JSON.stringify(app.graph.serialize()); + !newGraphJson && setIsDirty(false); + const graphJson = newGraphJson || 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, }), @@ -87,7 +91,6 @@ export default function App() { duration: 1000, isClosable: true, }); - setIsDirty(false); } }, []); @@ -122,9 +125,7 @@ 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); - setCurFlowName(workflow?.name ?? ""); if (id == null) { document.title = "ComfyUI"; window.location.hash = ""; @@ -160,7 +161,7 @@ export default function App() { latestWfID = localStorage.getItem("curFlowID"); } if (latestWfID) { - loadWorkflowIDImpl(latestWfID); + loadWorkflowIDImpl(latestWfID!, null, true); } fetch("/workspace/deduplicate_workflow_ids"); const twoway = await userSettingsTable?.getSetting("twoWaySync"); @@ -208,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; @@ -223,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; } @@ -238,27 +243,55 @@ export default function App() { ? await workflowVersionsTable?.get(versionID) : null; + 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(); - if (version) { - setCurVersion(version); + setRoute("root"); + isDirty && setIsDirty(false); + + 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: version.json, + json: newTabInfo.json, }); - app.loadGraphData(JSON.parse(version.json)); } else { - setCurFlowIDAndName(flow); - setCurVersion(null); - app.loadGraphData(JSON.parse(flow.json)); + document.dispatchEvent( + new CustomEvent(OPEN_TAB_EVENT, { + detail: { + tabInfo: newTabInfo, + }, + }), + ); } - setRoute("root"); - isDirty && setIsDirty(false); }; 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) { @@ -266,18 +299,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 @@ -358,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, }); }, []); @@ -403,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); @@ -458,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); @@ -491,6 +526,7 @@ export default function App() { route: route, curVersion: curVersion, setCurVersion: setCurVersion, + setCurFlowIDAndName: setCurFlowIDAndName, }} >
@@ -505,7 +541,7 @@ export default function App() { zIndex={DRAWER_Z_INDEX} draggable={false} > - + {loadChild && route === "recentFlows" && ( >([]); const allFlowsRef = useRef>([]); - const { loadWorkflowID, curFlowID } = useContext(WorkspaceContext); const [selectedTag, setSelectedTag] = useState(); const [multipleState, setMultipleState] = useState(false); const [selectedKeys, setSelectedKeys] = useState([]); @@ -113,9 +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); - } await loadLatestWorkflows(); }, [selectedTag, debounceSearchValue], @@ -155,12 +151,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 feb95f34..248301d6 100644 --- a/ui/src/WorkspaceContext.ts +++ b/ui/src/WorkspaceContext.ts @@ -9,13 +9,9 @@ export type JsonDiff = { export const WorkspaceContext = createContext<{ curFlowID: string | null; onDuplicateWorkflow?: (flowID: string, newFlowName?: string) => void; - loadWorkflowID: ( - id: string | null, - versionID?: string | null, - forceLoad?: boolean, - ) => void; + loadWorkflowID: (id: string | null, versionID?: string | null) => void; setIsDirty: (dirty: boolean) => void; - saveCurWorkflow: (force?: boolean) => Promise; + saveCurWorkflow: (saveCurWorkflow?: string) => Promise; 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 3cd84fa9..d645fa4b 100644 --- a/ui/src/components/DropdownTitle.tsx +++ b/ui/src/components/DropdownTitle.tsx @@ -98,7 +98,7 @@ export default function DropdownTitle() { parentFolderID: workflow?.parentFolderID, }); - flow && (await loadWorkflowID(flow.id, null, true)); + flow && (await loadWorkflowID(flow.id, null)); handleOnCloseModal(); }; diff --git a/ui/src/components/EditFlowName.tsx b/ui/src/components/EditFlowName.tsx index 52a003ef..c5b1cb15 100644 --- a/ui/src/components/EditFlowName.tsx +++ b/ui/src/components/EditFlowName.tsx @@ -22,15 +22,10 @@ import { WorkspaceContext } from "../WorkspaceContext"; 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 +58,6 @@ export default function EditFlowName({ await workflowsTable?.updateName(curFlowID, { name: trimEditName, }); - updateFlowName(trimEditName); onCloseModal(); } } @@ -79,17 +73,15 @@ export default function EditFlowName({ -
- {isDirty && "* "} -
{displayName}
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/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/Topbar.tsx b/ui/src/topbar/Topbar.tsx index 5419d9b9..8405a379 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,25 +17,21 @@ 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"), ); 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, @@ -131,19 +126,6 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) { - { - setCurFlowName(newName); - requestAnimationFrame(() => { - updatePanelPosition(); - }); - }} - /> - {workflowsTable?.curWorkflow?.saveLock && ( - - )} {curFlowID && ( @@ -172,6 +154,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..0e6fb3e5 --- /dev/null +++ b/ui/src/topbar/multipleTabs/TabDataManager.ts @@ -0,0 +1,131 @@ +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; + json: string; + isDirty: boolean; +}; +type TabUpdateInput = Partial; + +class TabDataManager { + tabs: TabData[] = []; + activeIndex: number = 0; + activeTab: TabData | null = null; + + changeActiveTab(index: number, needLoadNewFlow: boolean = true) { + this.saveCurTabJson(); + this.activeIndex = index; + 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.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); + + if (this.tabs.length === 0) { + this.tabs.push(newTab); + this.activeTab = newTab; + } else { + 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("loadNewFlow"); + } + + deleteTabData(index: number) { + if (index < 0 || index >= this.tabs.length) return; + + if (this.tabs.length === 1) { + this.tabs = []; + this.activeIndex = 0; + this.activeTab = null; + this.notifyChanges("clearCanvas"); + return; + } + + this.tabs.splice(index, 1); + + 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.notifyChanges(''); + } + + 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 { + 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(); + } + } + + private notifyChanges(otherAction?: string) { + const event = new CustomEvent(RE_RENDER_MULTIPLE_TABS_EVENT, { + detail: { + tabs: this.tabs, + activeIndex: this.activeIndex, + activeTab: this.activeTab, + otherAction, + }, + }); + document.dispatchEvent(event); + } +} + +export const tabDataManager = new TabDataManager(); diff --git a/ui/src/topbar/multipleTabs/index.css b/ui/src/topbar/multipleTabs/index.css new file mode 100644 index 00000000..663e5eea --- /dev/null +++ b/ui/src/topbar/multipleTabs/index.css @@ -0,0 +1,25 @@ +.workspace-multiple-tabs::-webkit-scrollbar { + background-color: transparent; + width: 2px; + height: 2px; +} + +.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; +} \ No newline at end of file diff --git a/ui/src/topbar/multipleTabs/index.tsx b/ui/src/topbar/multipleTabs/index.tsx new file mode 100644 index 00000000..74c1bb68 --- /dev/null +++ b/ui/src/topbar/multipleTabs/index.tsx @@ -0,0 +1,209 @@ +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, + RE_RENDER_MULTIPLE_TABS_EVENT, +} 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 { showDialog } = useDialog(); + const { + setCurFlowIDAndName, + isDirty, + setIsDirty, + 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 () => { + const deleteFlow = await workflowsTable?.get(deleteTabInfo.id); + if (deleteFlow) { + /** + * 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); + } + }, + }, + { + label: "Close", + colorScheme: "red", + onClick: async () => { + tabDataManager.deleteTabData(index); + }, + }, + ], + ); + } else { + tabDataManager.deleteTabData(index); + } + }; + + useEffect(() => { + tabDataManager.updateTabData(tabDataManager.activeIndex, { isDirty }); + }, [isDirty]); + + useEffect(() => { + const reRender = (e: CustomEvent) => { + const detail = e.detail; + 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, + }); + }); + setIsDirty(detail.activeTab.isDirty); + curVersion && setCurVersion(null); + // 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; + } + }; + + const openTab = (e: CustomEvent) => { + const detail = e.detail; + if (detail.tabInfo.id !== tabDataManager.activeTab?.id) { + 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) => { + const isActive = tabDataManager.activeIndex === index; + const { id, name, isDirty } = tab; + return ( + { + tabDataManager.activeIndex !== index && + tabDataManager.changeActiveTab(index); + }} + > + + {isDirty ? ( + + } + size={"xs"} + aria-label="un save flag" + variant={"ghost"} + display="flex" + _hover={{ bg: "whiteAlpha.200" }} + className="workspace-multiple-tabs-item-dirty" + /> + { + e.stopPropagation(); + onClose(index); + }} + icon={} + size={"xs"} + aria-label="close tab" + variant={"ghost"} + _hover={{ bg: "whiteAlpha.200" }} + display="none" + className="workspace-multiple-tabs-item-dirty-close" + /> + + ) : ( + { + e.stopPropagation(); + onClose(index); + }} + icon={} + size={"xs"} + aria-label="close tab" + variant={"ghost"} + _hover={{ bg: "whiteAlpha.200" }} + display={isActive ? "flex" : "none"} + position="absolute" + right="4px" + className={isActive ? "" : "workspace-multiple-tabs-item-close"} + /> + )} + {isActive && 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; } }