diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..ae51f47 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,12 @@ + + + + + + + + +
+ + + diff --git a/ui/package-lock.json b/ui/package-lock.json index 031a0ed..483aad2 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -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", diff --git a/ui/package.json b/ui/package.json index 2a90be1..f11eef3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/RecentFilesDrawer/AddTagToWorkflowPopover.tsx b/ui/src/RecentFilesDrawer/AddTagToWorkflowPopover.tsx index db17215..b208c79 100644 --- a/ui/src/RecentFilesDrawer/AddTagToWorkflowPopover.tsx +++ b/ui/src/RecentFilesDrawer/AddTagToWorkflowPopover.tsx @@ -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 ( - + } + /> diff --git a/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx b/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx new file mode 100644 index 0000000..d77f9b1 --- /dev/null +++ b/ui/src/RecentFilesDrawer/MultipleSelectionOperation.tsx @@ -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 ( + + {multipleState ? ( + <> + ) => { + batchOperationCallback("selectAll", e.target.checked); + }} + /> + + } + onClick={batchExport} + /> + + + { + batchDeleteFlow(selectedKeys); + batchOperationCallback("batchDelete"); + }} + /> + + + {selectedKeys.length > 0 && ( + {`Selected ${selectedKeys.length}`} + )} + + } + onClick={() => { + changeMultipleState(false); + }} + /> + + + ) : ( + + } + onClick={() => { + changeMultipleState(true); + }} + /> + + )} + + ); +} diff --git a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx index 3383d16..d2c37ef 100644 --- a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx +++ b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx @@ -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(); const [showAllTags, setShowAllTags] = useState(false); + const [multipleState, setMultipleState] = useState(false); + const [selectedKeys, setSelectedKeys] = useState([]); const draggingWorkflowID = useRef(null); const sortTypeRef = useRef( (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 ( @@ -161,7 +193,20 @@ export default function RecentFilesDrawer({ onclose, loadWorkflowID }: Props) { /> )} - + + {recentFlows.length > 0 ? ( + { + setMultipleState(state); + !state && setSelectedKeys([]); + }} + selectedKeys={selectedKeys} + batchOperationCallback={batchOperationCallback} + /> + ) : ( + + )} {recentFlows.map((n) => ( { - draggingWorkflowID.current = id; - }} + onDraggingWorkflowID={updateDraggingWorkflowID} + multipleState={multipleState} + isChecked={ + selectedKeys.length > 0 && selectedKeys.includes(n.id) + } + onSelect={onSelect} /> ))} diff --git a/ui/src/RecentFilesDrawer/WorkflowListItem.tsx b/ui/src/RecentFilesDrawer/WorkflowListItem.tsx index e94ac53..ba89326 100644 --- a/ui/src/RecentFilesDrawer/WorkflowListItem.tsx +++ b/ui/src/RecentFilesDrawer/WorkflowListItem.tsx @@ -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 = ( + { + !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", + }} + > + {workflow.name ?? "untitled"} + + Updated: {formatTimestamp(workflow.updateTime)} + + + ); + return ( - { - 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", - }} - > - {workflow.name ?? "untitled"} - - Updated: {formatTimestamp(workflow.updateTime)} - - + {multipleState ? ( + ) => { + onSelect(workflow.id, e.target.checked); + }} + > + {basicInfoComp} + + ) : ( + <> + {basicInfoComp} + + { + onDelete(workflow.id); + }} + /> + + )} {isMenuOpen && ( )} - - - {({ isOpen, onClose }) => ( - <> - - - - - - - {/* Confirmation! */} - - - Are you sure you want to delete this workflow? - - - - - - )} - ); -} +}); diff --git a/ui/src/WorkspaceDB.ts b/ui/src/WorkspaceDB.ts index 1083f4a..cecba2e 100644 --- a/ui/src/WorkspaceDB.ts +++ b/ui/src/WorkspaceDB.ts @@ -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 { let tagsStr = await getDB("tags"); let tags: Tags = JSON.parse(tagsStr ?? "{}") ?? {}; diff --git a/ui/src/components/DeleteConfirm.tsx b/ui/src/components/DeleteConfirm.tsx new file mode 100644 index 0000000..5c8d8de --- /dev/null +++ b/ui/src/components/DeleteConfirm.tsx @@ -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 ( + + {({ onClose }) => ( + <> + + } + isDisabled={isDisabled} + variant={variant} + /> + + + + + + + {promptMessage} + + + + + + )} + + ); +}