feat: Added batch export and batch deletion

This commit is contained in:
yanle
2023-12-17 00:03:11 +08:00
parent 363fa12183
commit 08b8220de8
9 changed files with 497 additions and 92 deletions
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body style="background: #575353;height: 300px;">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+165 -4
View File
@@ -14,6 +14,7 @@
"@tabler/icons-react": "^2.42.0",
"chakra-react-select": "^4.7.6",
"framer-motion": "^10.16.12",
"jszip": "^3.10.1",
"lodash.throttle": "^4.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -3094,6 +3095,11 @@
"toggle-selection": "^1.0.6"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -3861,6 +3867,11 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -3898,8 +3909,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/invariant": {
"version": "2.2.4",
@@ -3964,6 +3974,11 @@
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4034,6 +4049,17 @@
"node": ">=6"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -4056,6 +4082,14 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -4251,6 +4285,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4374,6 +4413,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -4590,6 +4634,20 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
@@ -4710,6 +4768,11 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
@@ -4751,6 +4814,11 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4798,6 +4866,14 @@
"node": ">=0.10.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -5032,6 +5108,11 @@
}
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
@@ -7289,6 +7370,11 @@
"toggle-selection": "^1.0.6"
}
},
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -7871,6 +7957,11 @@
"integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
"dev": true
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -7899,8 +7990,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"invariant": {
"version": "2.2.4",
@@ -7950,6 +8040,11 @@
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
"dev": true
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -8005,6 +8100,17 @@
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -8024,6 +8130,14 @@
"type-check": "~0.4.0"
}
},
"lie": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"requires": {
"immediate": "~3.0.5"
}
},
"lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -8171,6 +8285,11 @@
"p-limit": "^3.0.2"
}
},
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -8247,6 +8366,11 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -8381,6 +8505,20 @@
"prop-types": "^15.6.2"
}
},
"readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
@@ -8455,6 +8593,11 @@
"queue-microtask": "^1.2.2"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
@@ -8489,6 +8632,11 @@
}
}
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -8521,6 +8669,14 @@
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -8659,6 +8815,11 @@
"tslib": "^2.0.0"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+2 -1
View File
@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite && vite build --watch",
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
@@ -16,6 +16,7 @@
"@tabler/icons-react": "^2.42.0",
"chakra-react-select": "^4.7.6",
"framer-motion": "^10.16.12",
"jszip": "^3.10.1",
"lodash.throttle": "^4.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -9,6 +9,7 @@ import {
PopoverHeader,
Input,
HStack,
IconButton,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { Tag, Workflow, tagsTable, updateFlow } from "../WorkspaceDB";
@@ -52,9 +53,12 @@ export default function AddTagToWorkflowPopover({ workflow }: Props) {
return (
<Popover isLazy={true}>
<PopoverTrigger>
<Button variant={"ghost"} size={"sm"} colorScheme="teal">
<IconTag color={"#718096"} />
</Button>
<IconButton
aria-label="Delete confirm"
size={"sm"}
variant="ghost"
icon={<IconTag color={"#718096"} />}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
@@ -0,0 +1,102 @@
import { Checkbox, HStack, IconButton, Text, Tooltip } from "@chakra-ui/react";
import { IconListCheck, IconX, IconFileExport } from "@tabler/icons-react";
import DeleteConfirm from "../components/DeleteConfirm";
import { ChangeEvent } from "react";
import { batchDeleteFlow, listWorkflows } from "../WorkspaceDB";
import JSZip from "JSZip";
import { formatTimestamp } from "../utils";
type Props = {
multipleState: boolean;
selectedKeys: string[];
changeMultipleState: (isMultiple: boolean) => void;
batchOperationCallback: (type: string, value?: unknown) => void;
};
export default function MultipleSelectionOperation(props: Props) {
const {
multipleState,
changeMultipleState,
selectedKeys,
batchOperationCallback,
} = props;
const batchExport = () => {
const selectedList = listWorkflows().filter((flow) =>
selectedKeys.includes(flow.id)
);
const exportName = `ComfyUI workflow ${formatTimestamp(Date.now())}`;
const zip = new JSZip();
const folder = zip.folder(exportName);
selectedList.forEach((flow) => {
folder && folder.file(`${flow.name}.json`, flow.json);
});
zip.generateAsync({ type: "blob" }).then(function (content) {
const a = document.createElement("a");
a.href = window.URL.createObjectURL(content);
a.download = `${exportName}.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
};
return (
<HStack spacing={2} wrap={"wrap"} mb={0}>
{multipleState ? (
<>
<Checkbox
onChange={(e: ChangeEvent<HTMLInputElement>) => {
batchOperationCallback("selectAll", e.target.checked);
}}
/>
<Tooltip hasArrow label="Batch export" placement="bottom">
<IconButton
isDisabled={!selectedKeys.length}
aria-label="Batch export"
size={"sm"}
icon={<IconFileExport />}
onClick={batchExport}
/>
</Tooltip>
<Tooltip hasArrow label="Batch deletion">
<DeleteConfirm
isDisabled={!selectedKeys.length}
variant="solid"
promptMessage={`Are you sure you want to delete these ${selectedKeys.length} checked workflows?`}
onDelete={() => {
batchDeleteFlow(selectedKeys);
batchOperationCallback("batchDelete");
}}
/>
</Tooltip>
{selectedKeys.length > 0 && (
<Text fontWeight={600}>{`Selected ${selectedKeys.length}`}</Text>
)}
<Tooltip hasArrow label="Exit batch operation">
<IconButton
aria-label="Close multi-select operation"
size={"sm"}
icon={<IconX />}
onClick={() => {
changeMultipleState(false);
}}
/>
</Tooltip>
</>
) : (
<Tooltip hasArrow label="Start batch operation" placement="right">
<IconButton
aria-label="Start multi-select operation"
size={"sm"}
icon={<IconListCheck />}
onClick={() => {
changeMultipleState(true);
}}
/>
</Tooltip>
)}
</HStack>
);
}
+57 -8
View File
@@ -11,9 +11,8 @@ import {
Card,
Box,
Flex,
Stack,
} from "@chakra-ui/react";
import { useContext, useEffect, useState, useRef } from "react";
import { useContext, useEffect, useState, useRef, useCallback } from "react";
import {
Workflow,
deleteFlow,
@@ -27,6 +26,7 @@ import RecentFilesDrawerMenu from "./RecentFilesDrawerMenu";
import { insertWorkflowToCanvas, sortFlows } from "../utils";
import WorkflowListItem from "./WorkflowListItem";
import ImportJsonFlows from "./ImportJsonFlows";
import MultipleSelectionOperation from "./MultipleSelectionOperation";
import { ESortTypes, sortTypeLocalStorageKey } from "./types";
// @ts-ignore
import { app } from "/scripts/app.js";
@@ -42,6 +42,8 @@ export default function RecentFilesDrawer({ onclose, loadWorkflowID }: Props) {
const { curFlowID } = useContext(WorkspaceContext);
const [selectedTag, setSelectedTag] = useState<string>();
const [showAllTags, setShowAllTags] = useState(false);
const [multipleState, setMultipleState] = useState(false);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const draggingWorkflowID = useRef<string | null>(null);
const sortTypeRef = useRef<ESortTypes>(
(window.localStorage.getItem(sortTypeLocalStorageKey) as ESortTypes) ??
@@ -64,10 +66,10 @@ export default function RecentFilesDrawer({ onclose, loadWorkflowID }: Props) {
window.localStorage.setItem(sortTypeLocalStorageKey, type);
};
const onDelete = (id: string) => {
const onDelete = useCallback((id: string) => {
deleteFlow(id);
loadLatestWorkflows();
};
}, []);
// const handleDragOver = (e) => {};
@@ -93,6 +95,36 @@ export default function RecentFilesDrawer({ onclose, loadWorkflowID }: Props) {
app.canvasEl.removeEventListener("click", handleClick);
};
}, [curFlowID]);
const updateDraggingWorkflowID = useCallback((id: string) => {
draggingWorkflowID.current = id;
}, []);
const onSelect = useCallback((flowId: string, selected: boolean) => {
setSelectedKeys((preState) => {
const copyKeys = [...preState];
if (selected) {
copyKeys.push(flowId);
} else {
copyKeys.splice(copyKeys.indexOf(flowId), 1);
}
return copyKeys;
});
}, []);
const batchOperationCallback = (type: string, value: unknown) => {
switch (type) {
case "batchDelete":
loadLatestWorkflows();
setMultipleState(false);
setSelectedKeys([]);
return;
case "selectAll":
setSelectedKeys(value ? recentFlows.map((flow) => flow.id) : []);
return;
}
};
const DRAWER_WIDTH = 440;
return (
<RecentFilesContext.Provider value={{ setRecentFiles: setRecentFlow }}>
@@ -161,7 +193,20 @@ export default function RecentFilesDrawer({ onclose, loadWorkflowID }: Props) {
/>
)}
</HStack>
<HStack mb={2} p={0} justifyContent="end">
<HStack mb={2} p={0} justifyContent="space-between">
{recentFlows.length > 0 ? (
<MultipleSelectionOperation
multipleState={multipleState}
changeMultipleState={(state) => {
setMultipleState(state);
!state && setSelectedKeys([]);
}}
selectedKeys={selectedKeys}
batchOperationCallback={batchOperationCallback}
/>
) : (
<Box />
)}
<Menu closeOnSelect={true}>
<MenuButton
as={Button}
@@ -192,13 +237,17 @@ export default function RecentFilesDrawer({ onclose, loadWorkflowID }: Props) {
</HStack>
{recentFlows.map((n) => (
<WorkflowListItem
key={n.id}
isSelected={n.id === curFlowID}
workflow={n}
loadWorkflowID={loadWorkflowID}
onDelete={onDelete}
onDraggingWorkflowID={(id: string) => {
draggingWorkflowID.current = id;
}}
onDraggingWorkflowID={updateDraggingWorkflowID}
multipleState={multipleState}
isChecked={
selectedKeys.length > 0 && selectedKeys.includes(n.id)
}
onSelect={onSelect}
/>
))}
</Flex>
+66 -76
View File
@@ -1,36 +1,30 @@
import {
Box,
HStack,
useColorMode,
Text,
Popover,
PopoverTrigger,
PopoverContent,
PopoverCloseButton,
PopoverArrow,
Button,
PopoverBody,
} from "@chakra-ui/react";
import { Box, HStack, useColorMode, Text, Checkbox } from "@chakra-ui/react";
import { Workflow } from "../WorkspaceDB";
import { formatTimestamp } from "../utils";
import AddTagToWorkflowPopover from "./AddTagToWorkflowPopover";
import { IconTrash } from "@tabler/icons-react";
import { useState } from "react";
import { useState, memo, ChangeEvent } from "react";
import WorkflowListItemRightClickMenu from "./WorkflowListItemRightClickMenu";
import DeleteConfirm from "../components/DeleteConfirm";
type Props = {
isSelected: boolean;
workflow: Workflow;
multipleState: boolean;
isChecked: boolean;
loadWorkflowID: (id: string) => void;
onDelete: (id: string) => void;
onDraggingWorkflowID: (id: string) => void;
onSelect: (id: string, selected: boolean) => void;
};
export default function WorkflowListItem({
export default memo(function WorkflowListItem({
isSelected,
workflow,
multipleState,
isChecked,
loadWorkflowID,
onDelete,
onDraggingWorkflowID,
onSelect,
}: Props) {
const { colorMode } = useColorMode();
@@ -42,45 +36,71 @@ export default function WorkflowListItem({
setMenuPosition({ x: event.clientX, y: event.clientY });
setIsMenuOpen(true);
};
const handleClose = () => {
setIsMenuOpen(false);
};
const basicInfoComp = (
<Box
textAlign={"left"}
backgroundColor={isSelected ? "teal.200" : undefined}
color={isSelected ? "#333" : undefined}
w={"90%"}
draggable
onDragStart={(e) => {
!multipleState && workflow.id && onDraggingWorkflowID(workflow.id);
}}
borderRadius={6}
p={2}
minW={320}
onClick={() => {
!multipleState && loadWorkflowID(workflow.id);
}}
_hover={{
bg: colorMode === "light" ? "gray.200" : "#4A5568",
}}
_active={{
transform: "scale(0.98)",
borderColor: "#bec3c9",
}}
>
<Text fontWeight={"500"}>{workflow.name ?? "untitled"}</Text>
<Text color={"GrayText"} ml={2} fontSize={"sm"}>
Updated: {formatTimestamp(workflow.updateTime)}
</Text>
</Box>
);
return (
<HStack
w={"100%"}
mb={2}
justify={"space-between"}
onContextMenu={handleContextMenu}
>
<Box
as="button"
textAlign={"left"}
backgroundColor={isSelected ? "teal.200" : undefined}
color={isSelected ? "#333" : undefined}
w={"90%"}
draggable
onDragStart={(e) => {
workflow.id && onDraggingWorkflowID(workflow.id);
}}
borderRadius={6}
p={2}
mb={2}
onClick={() => {
loadWorkflowID(workflow.id);
}}
_hover={{
bg: colorMode === "light" ? "gray.200" : "#4A5568",
}}
_active={{
transform: "scale(0.98)",
borderColor: "#bec3c9",
}}
>
<Text fontWeight={"500"}>{workflow.name ?? "untitled"}</Text>
<Text color={"GrayText"} ml={2} fontSize={"sm"}>
Updated: {formatTimestamp(workflow.updateTime)}
</Text>
</Box>
{multipleState ? (
<Checkbox
isChecked={isChecked}
spacing={4}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
onSelect(workflow.id, e.target.checked);
}}
>
{basicInfoComp}
</Checkbox>
) : (
<>
{basicInfoComp}
<AddTagToWorkflowPopover workflow={workflow} />
<DeleteConfirm
promptMessage="Are you sure you want to delete this workflow?"
onDelete={() => {
onDelete(workflow.id);
}}
/>
</>
)}
{isMenuOpen && (
<WorkflowListItemRightClickMenu
menuPosition={menuPosition}
@@ -88,36 +108,6 @@ export default function WorkflowListItem({
workflowID={workflow.id}
/>
)}
<AddTagToWorkflowPopover workflow={workflow} />
<Popover isLazy={true}>
{({ isOpen, onClose }) => (
<>
<PopoverTrigger>
<IconTrash color="#F56565" cursor={"pointer"} />
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
{/* <PopoverHeader>Confirmation!</PopoverHeader> */}
<PopoverBody>
<Text mb={4} fontWeight={600}>
Are you sure you want to delete this workflow?
</Text>
<Button
colorScheme="red"
size={"sm"}
onClick={() => {
onDelete(workflow.id);
onClose();
}}
>
Yes, delete
</Button>
</PopoverBody>
</PopoverContent>
</>
)}
</Popover>
</HStack>
);
}
});
+22
View File
@@ -196,6 +196,28 @@ export function deleteFlow(id: string) {
}
}
export function batchDeleteFlow(ids: string[]) {
if (workspace == null) {
throw new Error("workspace is not loaded");
}
const filePathList = ids.map(id => {
const filePath = workspace?.[id]?.filePath;
workspace && delete workspace[id];
return filePath;
});
const stringifyWorkspace = JSON.stringify(workspace);
localStorage.setItem("workspace", stringifyWorkspace);
saveDB("workflows", stringifyWorkspace);
if (filePathList.length) {
filePathList.forEach(filePath => {
filePath && deleteFile(filePath);
})
}
}
async function loadTagsTable(): Promise<TagsTable> {
let tagsStr = await getDB("tags");
let tags: Tags = JSON.parse(tagsStr ?? "{}") ?? {};
+64
View File
@@ -0,0 +1,64 @@
import {
Button,
Popover,
PopoverTrigger,
PopoverContent,
PopoverCloseButton,
PopoverArrow,
PopoverBody,
Text,
IconButton,
} from "@chakra-ui/react";
import { IconTrash } from "@tabler/icons-react";
type Props = {
promptMessage: string;
isDisabled?: boolean;
variant?: string;
onDelete: () => void;
};
export default function DeleteConfirm(props: Props) {
const {
promptMessage = "Are you sure you want to delete this?",
variant = "ghost",
onDelete,
isDisabled = false,
} = props;
return (
<Popover isLazy={true} placement="auto">
{({ onClose }) => (
<>
<PopoverTrigger>
<IconButton
aria-label="Delete confirm"
size={"sm"}
icon={<IconTrash color="#F56565" />}
isDisabled={isDisabled}
variant={variant}
/>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverBody>
<Text mb={4} pr={4} fontWeight={600}>
{promptMessage}
</Text>
<Button
colorScheme="red"
size={"sm"}
onClick={() => {
onDelete();
onClose();
}}
>
Yes, delete
</Button>
</PopoverBody>
</PopoverContent>
</>
)}
</Popover>
);
}