Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dc3aa557c | ||
|
|
316f5c1def | ||
|
|
d80e791547 | ||
|
|
b37e9b6449 | ||
|
|
87b82d6f2d | ||
|
|
ce56822a69 | ||
|
|
1cf711e4fa | ||
|
|
b08c4e0079 | ||
|
|
b2052ff205 | ||
|
|
fb5fac0621 | ||
|
|
7b64353aba |
+79
-43
@@ -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<string | null>(null);
|
||||
const [route, setRoute] = useState<WorkspaceRoute>("root");
|
||||
const [loadingDB, setLoadingDB] = useState(true);
|
||||
const [flowID, setFlowID] = useState<string | null>(null);
|
||||
const curFlowID = useRef<string | null>(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<WorkflowVersion | null>(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;
|
||||
|
||||
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);
|
||||
|
||||
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 (
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<div ref={workspaceContainerRef} className="workspace_manager">
|
||||
@@ -505,7 +541,7 @@ export default function App() {
|
||||
zIndex={DRAWER_Z_INDEX}
|
||||
draggable={false}
|
||||
>
|
||||
<Topbar curFlowName={curFlowName} setCurFlowName={setCurFlowName} />
|
||||
<Topbar />
|
||||
{loadChild && route === "recentFlows" && (
|
||||
<Suspense>
|
||||
<RecentFilesDrawer
|
||||
|
||||
@@ -51,7 +51,6 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
|
||||
Array<Folder | Workflow>
|
||||
>([]);
|
||||
const allFlowsRef = useRef<Array<Workflow>>([]);
|
||||
const { loadWorkflowID, curFlowID } = useContext(WorkspaceContext);
|
||||
const [selectedTag, setSelectedTag] = useState<string>();
|
||||
const [multipleState, setMultipleState] = useState(false);
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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<void>;
|
||||
saveCurWorkflow: (saveCurWorkflow?: string) => Promise<void>;
|
||||
discardUnsavedChanges: () => Promise<void>;
|
||||
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<{
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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({
|
||||
<Tooltip label={displayName} placement="bottom">
|
||||
<Text
|
||||
as="div"
|
||||
color="white"
|
||||
onClick={startEdit}
|
||||
maxW={200}
|
||||
fontWeight={500}
|
||||
color={isActive ? "white" : "#9d9d9d"}
|
||||
onDoubleClick={startEdit}
|
||||
maxW="114px"
|
||||
marginRight="4px"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
userSelect="none"
|
||||
>
|
||||
<div style={{ width: 8, display: "inline-block" }}>
|
||||
<Text as="span">{isDirty && "* "}</Text>
|
||||
</div>
|
||||
{displayName}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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<Workflow> {
|
||||
static readonly TABLE_NAME = "workflows";
|
||||
@@ -219,6 +220,7 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
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<Workflow, "json">) {
|
||||
@@ -318,6 +320,12 @@ export class WorkflowsTable extends TableBase<Workflow> {
|
||||
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<Workflow> {
|
||||
}
|
||||
}
|
||||
await indexdb.workflows.bulkDelete(ids);
|
||||
tabDataManager.batchDeleteTabData(ids);
|
||||
this.saveDiskDB();
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
</HStack>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<EditFlowName
|
||||
isDirty={isDirty}
|
||||
displayName={curFlowName ?? ""}
|
||||
updateFlowName={(newName) => {
|
||||
setCurFlowName(newName);
|
||||
requestAnimationFrame(() => {
|
||||
updatePanelPosition();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{workflowsTable?.curWorkflow?.saveLock && (
|
||||
<IconLock color="#FFF" size={20} />
|
||||
)}
|
||||
{curFlowID && (
|
||||
<HStack gap={"4px"}>
|
||||
<Tooltip label="Open gallery">
|
||||
@@ -172,6 +154,7 @@ export function Topbar({ curFlowName, setCurFlowName }: Props) {
|
||||
) : (
|
||||
<div style={{ width: 1 }} />
|
||||
)}
|
||||
<MultipleTabs />
|
||||
<SharedTopbarButton />
|
||||
<VersionNameTopbar />
|
||||
<AppIsDirtyEventListener />
|
||||
|
||||
@@ -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<TabData>;
|
||||
|
||||
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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<HStack
|
||||
spacing={0}
|
||||
className="workspace-multiple-tabs"
|
||||
overflowX="auto"
|
||||
maxW="calc(100vw - 380px)"
|
||||
>
|
||||
{tabDataManager.tabs.map((tab, index) => {
|
||||
const isActive = tabDataManager.activeIndex === index;
|
||||
const { id, name, isDirty } = tab;
|
||||
return (
|
||||
<Box
|
||||
key={`${id}-${name}`}
|
||||
className="workspace-multiple-tabs-item"
|
||||
id={`workspace-multiple-tabs-item-${id}`}
|
||||
cursor="pointer"
|
||||
border="1px"
|
||||
borderColor="#64646459"
|
||||
borderTopColor={isActive ? "#0078d4" : "#64646459"}
|
||||
borderRightWidth={0}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
h="32px"
|
||||
lineHeight="32px"
|
||||
padding="0 30px 0 10px"
|
||||
position="relative"
|
||||
bg={isActive ? "#1f1f1f" : "#181818"}
|
||||
_hover={{ bg: "#1f1f1f" }}
|
||||
_last={{ borderRightWidth: 1 }}
|
||||
onClick={() => {
|
||||
tabDataManager.activeIndex !== index &&
|
||||
tabDataManager.changeActiveTab(index);
|
||||
}}
|
||||
>
|
||||
<EditFlowName isActive={isActive} displayName={name ?? ""} />
|
||||
{isDirty ? (
|
||||
<Box
|
||||
className="workspace-multiple-tabs-item-action"
|
||||
position="absolute"
|
||||
right="4px"
|
||||
>
|
||||
<IconButton
|
||||
icon={<IconAsterisk color="white" width={14} height={14} />}
|
||||
size={"xs"}
|
||||
aria-label="un save flag"
|
||||
variant={"ghost"}
|
||||
display="flex"
|
||||
_hover={{ bg: "whiteAlpha.200" }}
|
||||
className="workspace-multiple-tabs-item-dirty"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose(index);
|
||||
}}
|
||||
icon={<IconX color="white" width={14} height={14} />}
|
||||
size={"xs"}
|
||||
aria-label="close tab"
|
||||
variant={"ghost"}
|
||||
_hover={{ bg: "whiteAlpha.200" }}
|
||||
display="none"
|
||||
className="workspace-multiple-tabs-item-dirty-close"
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose(index);
|
||||
}}
|
||||
icon={<IconX color="white" width={14} height={14} />}
|
||||
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 && (
|
||||
<IconLock color="#FFF" size={20} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
Vendored
+18
-1
@@ -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<ShortcutTriggerDetail>;
|
||||
[RE_RENDER_MULTIPLE_TABS_EVENT]: CustomEvent<reRenderTabsDetail>;
|
||||
[OPEN_TAB_EVENT]: CustomEvent;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user