From 66900a6c3c8e27be1aa4d329c57ca706fdb3dfd4 Mon Sep 17 00:00:00 2001 From: yanle <421361608@qq.com> Date: Sun, 21 Jan 2024 16:49:44 +0800 Subject: [PATCH] feat: nested folder deletion logic reconstruction --- __init__.py | 16 ++- ui/src/Api.ts | 25 ++++- .../FilesListFolderItemRightClickMenu.tsx | 100 ++++++++---------- ui/src/db-tables/DiskFileUtils.ts | 58 +++++++++- ui/src/db-tables/FoldersTable.ts | 38 ++++--- ui/src/db-tables/WorkspaceDB.ts | 15 +-- ui/src/types/dbTypes.ts | 14 +++ 7 files changed, 186 insertions(+), 80 deletions(-) diff --git a/__init__.py b/__init__.py index 6e61e88..1161cf3 100644 --- a/__init__.py +++ b/__init__.py @@ -3,6 +3,7 @@ from aiohttp import web import aiohttp import requests import folder_paths +import shutil import os import sys import threading @@ -303,4 +304,17 @@ async def scan_local_new_files(request): if len(folder['list']) > 0: folderList.append(folder) - return web.Response(text=json.dumps({'fileList': fileList, 'folderList': folderList}), content_type='application/json') \ No newline at end of file + return web.Response(text=json.dumps({'fileList': fileList, 'folderList': folderList}), content_type='application/json') + + +@server.PromptServer.instance.routes.post("/workspace/delete_folder") +async def delete_folder(request): + data = await request.json() + folder_path = data['folder_path'] + + if os.path.exists(folder_path): + shutil.rmtree(folder_path) + return web.Response(text="Successfully deleted folder: {folder_path}") + else: + return web.Response(text="folder not found: {folder_path}", status=404) + \ No newline at end of file diff --git a/ui/src/Api.ts b/ui/src/Api.ts index 5743875..f37339e 100644 --- a/ui/src/Api.ts +++ b/ui/src/Api.ts @@ -171,8 +171,29 @@ export async function getAllModelsList() { }, }); const result = await response.json(); - return result as {file_list: ModelsListRespItem[], populate_done: boolean}; + return result as { + file_list: ModelsListRespItem[]; + populate_done: boolean; + }; } catch (error) { console.error("Error get all models list:", error); } -} \ No newline at end of file +} + +export async function deleteLocalDiskFolder(folderPath: string) { + try { + const response = await fetch("/workspace/delete_folder", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + folder_path: folderPath, + }), + }); + const result = await response.text(); + return result; + } catch (error) { + console.error("Error move file:", error); + } +} diff --git a/ui/src/RecentFilesDrawer/FilesListFolderItemRightClickMenu.tsx b/ui/src/RecentFilesDrawer/FilesListFolderItemRightClickMenu.tsx index a72c214..01453d3 100644 --- a/ui/src/RecentFilesDrawer/FilesListFolderItemRightClickMenu.tsx +++ b/ui/src/RecentFilesDrawer/FilesListFolderItemRightClickMenu.tsx @@ -1,17 +1,4 @@ -import { - Box, - Menu, - MenuList, - MenuItem, - Popover, - PopoverTrigger, - PopoverContent, - PopoverCloseButton, - PopoverBody, - Button, - PopoverArrow, - Text, -} from "@chakra-ui/react"; +import { Box, Menu, MenuList, MenuItem } from "@chakra-ui/react"; import { MouseEvent, useContext, useRef, useState } from "react"; import { RecentFilesContext } from "../WorkspaceContext"; import { createFlow, foldersTable } from "../db-tables/WorkspaceDB"; @@ -23,9 +10,11 @@ import { IconPlus, IconTrash, } from "@tabler/icons-react"; -import { Folder } from "../types/dbTypes"; +import { EFlowOperationType, Folder } from "../types/dbTypes"; import ImportFlowsFileInput from "./ImportFlowsFileInput"; import { defaultGraph } from "../defaultGraph"; +import { useDialog } from "../components/AlertDialogProvider"; +import { getFileCountInFolder } from "../db-tables/DiskFileUtils"; type Props = { menuPosition: { x: number; y: number }; @@ -40,8 +29,9 @@ export default function FilesListFolderItemRightClickMenu({ onClose, }: Props) { const [isRenameOpen, setIsRenameOpen] = useState(false); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); const { onRefreshFilesList } = useContext(RecentFilesContext); + const { showDialog } = useDialog(); + const onClickNewFolder = async (e: MouseEvent) => { e.stopPropagation(); await foldersTable?.create({ @@ -60,6 +50,43 @@ export default function FilesListFolderItemRightClickMenu({ }; const fileInputRef = useRef(null); + const openDeleteConfirm = async (e: MouseEvent) => { + e.stopPropagation(); + const fileCount = await getFileCountInFolder(folder.id); + if (fileCount > 0) { + showDialog( + `Do you want to delete the ${fileCount} files inside the folder too?`, + [ + { + label: "Keep and move to root directory", + colorScheme: "teal", + onClick: () => { + onDelete(fileCount, EFlowOperationType.MOVE_TO_ROOT_FOLDER); + }, + }, + { + label: "Delete all files", + colorScheme: "red", + onClick: () => { + onDelete(fileCount, EFlowOperationType.DELETE); + }, + }, + ], + ); + } else { + onDelete(fileCount); + } + }; + + const onDelete = async ( + fileCount: number, + operationType?: EFlowOperationType, + ) => { + await foldersTable?.deleteFolder(folder.id, fileCount, operationType); + onClose(); + onRefreshFilesList && onRefreshFilesList(); + }; + return ( <> @@ -77,7 +104,7 @@ export default function FilesListFolderItemRightClickMenu({ } - onClick={() => setIsDeleteOpen(true)} + onClick={openDeleteConfirm} > Delete @@ -112,45 +139,6 @@ export default function FilesListFolderItemRightClickMenu({ onclose={() => setIsRenameOpen(false)} /> )} - {isDeleteOpen && ( - setIsDeleteOpen(false)} - // placement="right" - closeOnBlur={false} - > - -
-
- - - - - - Are you sure you want to delete this folder, - {folder.name}? - - - This will NOT delete any files in the folder. The files will be - moved to the root folder. - - - - -
- )} ); } diff --git a/ui/src/db-tables/DiskFileUtils.ts b/ui/src/db-tables/DiskFileUtils.ts index e5862b3..9577371 100644 --- a/ui/src/db-tables/DiskFileUtils.ts +++ b/ui/src/db-tables/DiskFileUtils.ts @@ -1,9 +1,10 @@ -import { deleteFile, saveDB, updateFile } from "../Api"; +import { deleteFile, updateFile } from "../Api"; import { Workflow, foldersTable, updateFlow, userSettingsTable, + listWorkflows, } from "./WorkspaceDB"; import { COMFYSPACE_TRACKING_FIELD_NAME, @@ -71,3 +72,58 @@ export async function generateFilePathAbsolute( } return myWorkflowsDir + subPath; } + +export async function generateFolderPath(id: string): Promise { + const folder = await foldersTable?.get(id); + let parentFolderID = folder?.parentFolderID; + let folderPath = folder?.name; + while (parentFolderID) { + const folder = await foldersTable?.get(parentFolderID); + if (folder == null) { + break; + } + folderPath = `${folder.name}/${folderPath}`; + parentFolderID = folder.parentFolderID ?? undefined; + } + + let myWorkflowsDir = await userSettingsTable?.getSetting("myWorkflowsDir"); + + if (myWorkflowsDir == null) { + console.error("myWorkflowsDir is not set"); + return null; + } + + if (!myWorkflowsDir.endsWith("/")) { + myWorkflowsDir = myWorkflowsDir + "/"; + } + return myWorkflowsDir + folderPath; +} + +export async function getFileCountInFolder(folderId: string): Promise { + const allFlows = await listWorkflows(); + const allFolders = (await foldersTable?.listAll()) ?? []; + const nestedFolderIdStack = [folderId]; + let count = 0; + + while (nestedFolderIdStack.length > 0) { + const curFolderId = nestedFolderIdStack.shift(); + + if (curFolderId) { + for (const flow of allFlows) { + if (flow.parentFolderID === curFolderId) { + count++; + } + } + + const curNestedFolderIds = allFolders + .filter((f) => f.parentFolderID === curFolderId) + .map((f) => f.id); + + if (curNestedFolderIds.length) { + nestedFolderIdStack.push(...curNestedFolderIds); + } + } + } + + return count; +} diff --git a/ui/src/db-tables/FoldersTable.ts b/ui/src/db-tables/FoldersTable.ts index fc5aba3..2095146 100644 --- a/ui/src/db-tables/FoldersTable.ts +++ b/ui/src/db-tables/FoldersTable.ts @@ -1,10 +1,11 @@ -import { saveDB } from "../Api"; -import { listWorkflows, updateFlow } from "./WorkspaceDB"; -import { Folder } from "../types/dbTypes"; +import { saveDB, deleteLocalDiskFolder } from "../Api"; +import { deleteFlow, listWorkflows, updateFlow } from "./WorkspaceDB"; +import { EFlowOperationType, Folder } from "../types/dbTypes"; import { validateOrSaveAllJsonFileMyWorkflows } from "../utils"; import { v4 as uuidv4 } from "uuid"; import { TableBase } from "./TableBase"; import { indexdb } from "./indexdb"; +import { generateFolderPath } from "./DiskFileUtils"; export class FoldersTable extends TableBase { static readonly TABLE_NAME = "folders"; @@ -61,11 +62,16 @@ export class FoldersTable extends TableBase { validateOrSaveAllJsonFileMyWorkflows(true); } } - public async deleteFolder(id: string) { + public async deleteFolder( + id: string, + fileCount: number = 0, + flowOperationType: EFlowOperationType = EFlowOperationType.DELETE, + ) { + const folderPath = await generateFolderPath(id); + /** - * When deleting a folder, - * breadth traverses all nested folders and deletes them from the DB one by one. - * And modify the related flow synchronously, setting parentFolderId to undefined + * When deleting a folder, if there are files in the folder + * Breadth traverse all nested folders, find all files, move to root directory or delete as needed. */ const allFlows = await listWorkflows(); const allFolders = await this.listAll(); @@ -77,12 +83,19 @@ export class FoldersTable extends TableBase { if (curFolderId) { for (const flow of allFlows) { if (flow.parentFolderID === curFolderId) { - await updateFlow(flow.id, { parentFolderID: undefined }); + switch (flowOperationType) { + case EFlowOperationType.DELETE: + await deleteFlow(flow.id); + break; + case EFlowOperationType.MOVE_TO_ROOT_FOLDER: + await updateFlow(flow.id, { parentFolderID: undefined }); + console.log("updateFlow"); + break; + } } } await indexdb.folders.delete(curFolderId); - const curNestedFolderIds = allFolders .filter((f) => f.parentFolderID === curFolderId) .map((f) => f.id); @@ -92,11 +105,10 @@ export class FoldersTable extends TableBase { } } } - + fileCount > 0 && + (await saveDB("workflows", JSON.stringify(await listWorkflows()))); + folderPath && (await deleteLocalDiskFolder(folderPath)); await saveDB("folders", JSON.stringify(await this.listAll())); - - // TODO 在嵌套文件夹场景下,重建逻辑无法有效判断子文件夹是否为空,导致子文件夹为空时无法准确删除。 - await validateOrSaveAllJsonFileMyWorkflows(true); } public async generateUniqueName(name?: string) { diff --git a/ui/src/db-tables/WorkspaceDB.ts b/ui/src/db-tables/WorkspaceDB.ts index bd768a6..e200b26 100644 --- a/ui/src/db-tables/WorkspaceDB.ts +++ b/ui/src/db-tables/WorkspaceDB.ts @@ -1,5 +1,5 @@ import { v4 as uuidv4 } from "uuid"; -import { getDB, saveDB, updateFile } from "../Api"; +import { getDB, saveDB } from "../Api"; import { generateUniqueName, sortFileItem, sortFlows } from "../utils"; import { ESortTypes, ImportWorkflow } from "../RecentFilesDrawer/types"; import { ChangelogsTable } from "./ChangelogsTable"; @@ -166,20 +166,21 @@ export async function updateFlow(id: string, input: Partial) { // update memory workspace[id] = newWorkflow; //update indexdb - indexdb.workflows.update(id, newWorkflow); + await indexdb.workflows.update(id, newWorkflow); //update legacy indexdb backup updateWorkspaceIndexDB(); // update disk file db - saveDB("workflows", JSON.stringify(workspace)); + await saveDB("workflows", JSON.stringify(workspace)); // save to my_workflows/ - if (input.name != null || input.parentFolderID != null) { + input.name != null || input.parentFolderID != null; + if (input.name !== null || input.parentFolderID !== null) { // renamed file or moved file folder - deleteJsonFileMyWorkflows(before); - saveJsonFileMyWorkflows(after); + await deleteJsonFileMyWorkflows(before); + await saveJsonFileMyWorkflows(after); return; } if (input.json != null) { - saveJsonFileMyWorkflows(after); + await saveJsonFileMyWorkflows(after); } } diff --git a/ui/src/types/dbTypes.ts b/ui/src/types/dbTypes.ts index 83b112a..70847d9 100644 --- a/ui/src/types/dbTypes.ts +++ b/ui/src/types/dbTypes.ts @@ -51,3 +51,17 @@ export type Media = { localPath: string; format: string; }; + +/** + * When deleting a folder, how files in the folder and files in nested folders are handled + */ +export enum EFlowOperationType { + /** + * Move to root directory + */ + MOVE_TO_ROOT_FOLDER = "moveToRootFolder", + /** + * Delete + */ + DELETE = "delete", +}