feat: Added batch export and batch deletion
This commit is contained in:
@@ -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>
|
||||
Generated
+165
-4
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 ?? "{}") ?? {};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user