12 changed files with 489 additions and 99 deletions
+79 -43
View File
@@ -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([]);
+4 -6
View File
@@ -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<{
+1 -1
View File
@@ -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();
};
+7 -15
View File
@@ -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>
+2 -2
View File
@@ -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);
};
}, []);
+9
View File
@@ -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();
}
+4 -21
View File
@@ -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();
+25
View File
@@ -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;
}
+209
View File
@@ -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>
);
}
+18 -1
View File
@@ -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;
}
}