feat: nested folder deletion logic reconstruction
This commit is contained in:
+15
-1
@@ -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')
|
||||
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)
|
||||
|
||||
+23
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
await foldersTable?.create({
|
||||
@@ -60,6 +50,43 @@ export default function FilesListFolderItemRightClickMenu({
|
||||
};
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const openDeleteConfirm = async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
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 (
|
||||
<>
|
||||
<Box position="absolute" top={menuPosition.y} left={menuPosition.x}>
|
||||
@@ -77,7 +104,7 @@ export default function FilesListFolderItemRightClickMenu({
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<IconTrash size={19} />}
|
||||
onClick={() => setIsDeleteOpen(true)}
|
||||
onClick={openDeleteConfirm}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
@@ -112,45 +139,6 @@ export default function FilesListFolderItemRightClickMenu({
|
||||
onclose={() => setIsRenameOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isDeleteOpen && (
|
||||
<Popover
|
||||
returnFocusOnClose={false}
|
||||
isOpen={true}
|
||||
onClose={() => setIsDeleteOpen(false)}
|
||||
// placement="right"
|
||||
closeOnBlur={false}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<div></div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverBody textAlign={"left"}>
|
||||
<Text mb={2}>
|
||||
Are you sure you want to delete this folder,
|
||||
<b> {folder.name}</b>?
|
||||
</Text>
|
||||
<Text color={"GrayText"} mb={5}>
|
||||
This will NOT delete any files in the folder. The files will be
|
||||
moved to the root folder.
|
||||
</Text>
|
||||
<Button
|
||||
colorScheme="red"
|
||||
size={"sm"}
|
||||
onClick={async () => {
|
||||
setIsDeleteOpen(false);
|
||||
onClose();
|
||||
await foldersTable?.deleteFolder(folder.id);
|
||||
onRefreshFilesList && onRefreshFilesList();
|
||||
}}
|
||||
>
|
||||
Yes, delete
|
||||
</Button>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string | null> {
|
||||
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<number> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<Folder> {
|
||||
static readonly TABLE_NAME = "folders";
|
||||
@@ -61,11 +62,16 @@ export class FoldersTable extends TableBase<Folder> {
|
||||
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<Folder> {
|
||||
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<Folder> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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<Workflow>) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user