From 6a2c9e386f4326c75ef141f3f543d7fa26dcfd23 Mon Sep 17 00:00:00 2001 From: Arslan Ablikim Date: Mon, 29 Jan 2024 18:28:35 +0800 Subject: [PATCH 1/5] feat: folder drag drop --- __init__.py | 41 ++++++------ ui/src/Api.ts | 12 +++- ui/src/App.tsx | 8 +-- .../RecentFilesDrawer/FilesListFolderItem.tsx | 62 ++++++++++++------- .../RecentFilesDrawer/RecentFilesDrawer.tsx | 4 +- ui/src/RecentFilesDrawer/WorkflowListItem.tsx | 42 ++++++++----- ui/src/db-tables/FoldersTable.ts | 27 ++++++-- ui/src/db-tables/WorkflowsTable.ts | 5 +- ui/src/utils.tsx | 19 +++--- 9 files changed, 135 insertions(+), 85 deletions(-) diff --git a/__init__.py b/__init__.py index 6638cef..18eed92 100644 --- a/__init__.py +++ b/__init__.py @@ -261,6 +261,22 @@ def file_handle(name, file, existFlowIds, fileList): else: fileList.append(fileInfo) +def folder_handle(path, existFlowIds): + fileList = [] + for item in os.listdir(path): + item_path = os.path.join(path, item) + if os.path.isfile(item_path) and item_path.endswith('.json'): + with open(item_path, 'r') as f: + file_handle(item, f, existFlowIds, fileList) + + elif os.path.isdir(item_path): + fileList.append({ + 'name': item, + 'list': folder_handle(item_path, existFlowIds) + }) + return fileList + + # Scan all files and subfolders in the local save directory. # For files, compare the extra.workspace_info.id in the json format file with the flow of the current DB to determine whether it is a flow that needs to be added; # For subfolders, scan the json files in the subfolder and use the same processing method as the file to determine whether it is a flow that needs to be added; @@ -270,29 +286,8 @@ async def scan_local_new_files(request): path = reqJson['path'] existFlowIds = reqJson['existFlowIds'] - fileList = [] - folderList = [] - - for item in os.listdir(path): - item_path = os.path.join(path, item) - if os.path.isfile(item_path) and item_path.endswith('.json'): - with open(item_path, 'r') as f: - file_handle(item, f, existFlowIds, fileList) - - elif os.path.isdir(item_path): - folder = { - 'name': item, - 'list': [] - } - for sub_item in os.listdir(item_path): - sub_item_path = os.path.join(item_path, sub_item) - if os.path.isfile(sub_item_path) and sub_item_path.endswith('.json'): - with open(sub_item_path, 'r') as f: - file_handle(sub_item, f, existFlowIds, folder['list']) - - if len(folder['list']) > 0: - folderList.append(folder) - return web.Response(text=json.dumps({'fileList': fileList, 'folderList': folderList}), content_type='application/json') + fileList = folder_handle(path, existFlowIds) + return web.Response(text=json.dumps(fileList), content_type='application/json') @server.PromptServer.instance.routes.post("/workspace/delete_folder") diff --git a/ui/src/Api.ts b/ui/src/Api.ts index 4ae1547..cd36b3d 100644 --- a/ui/src/Api.ts +++ b/ui/src/Api.ts @@ -122,6 +122,16 @@ export async function openWorkflowsFolder() { } } +export type ScanLocalFile = { + name: string; + json: string; +}; +export type ScanLocalFolder = { + name: string; + list: ScanLocalResult[]; +}; +export type ScanLocalResult = ScanLocalFile | ScanLocalFolder; + export async function scanLocalNewFiles(path: string, existFlowIds: string[]) { try { const response = await fetch("/workspace/scan_local_new_files", { @@ -134,7 +144,7 @@ export async function scanLocalNewFiles(path: string, existFlowIds: string[]) { existFlowIds, }), }); - const result = await response.json(); + const result: ScanLocalResult[] = await response.json(); return result; } catch (error) { console.error("Error scan local new files:", error); diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 12d7afb..daa4bcf 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -154,11 +154,9 @@ export default function App() { await userSettingsTable?.getSetting("myWorkflowsDir"); const allFlows = await workflowsTable?.listAll(); const existFlowIds = (allFlows && allFlows.map((flow) => flow.id)) || []; - const { fileList, folderList } = await scanLocalNewFiles( - myWorkflowsDir!, - existFlowIds, - ); - await syncNewFlowOfLocalDisk(fileList, folderList); + const fileList = await scanLocalNewFiles(myWorkflowsDir!, existFlowIds); + if (!fileList || fileList.length === 0) return; + await syncNewFlowOfLocalDisk(fileList); } }; diff --git a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx index e7d0f6e..4f1078d 100644 --- a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx +++ b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx @@ -4,7 +4,14 @@ import { isFolder, workflowsTable, } from "../db-tables/WorkspaceDB"; -import { useState, memo, useContext, useEffect, MouseEvent } from "react"; +import { + useState, + memo, + useContext, + useEffect, + MouseEvent, + DragEvent, +} from "react"; import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"; import { RecentFilesContext } from "../WorkspaceContext"; import WorkflowListItem from "./WorkflowListItem"; @@ -22,8 +29,12 @@ export default memo(function FilesListFolderItem({ folder }: Props) { const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); const [isMenuOpen, setIsMenuOpen] = useState(false); const { colorMode } = useColorMode(); - const { draggingFile, refreshFolderStamp, onRefreshFilesList } = - useContext(RecentFilesContext); + const { + draggingFile, + setDraggingFile, + refreshFolderStamp, + onRefreshFilesList, + } = useContext(RecentFilesContext); const activeStyle = colorMode === "light" ? { backgroundColor: "#E2E8F0" } @@ -45,6 +56,22 @@ export default memo(function FilesListFolderItem({ folder }: Props) { setMenuPosition({ x: event.clientX, y: event.clientY }); setIsMenuOpen(true); }; + const handleDrop = async () => { + if (!draggingFile) return setIsActive(false); + if (isFolder(draggingFile)) { + if (draggingFile.id === folder.id) return setIsActive(false); + await foldersTable?.update({ + id: draggingFile.id, + parentFolderID: folder.id, + }); + } else if (!isFolder(draggingFile)) { + await workflowsTable?.updateFlow(draggingFile.id, { + parentFolderID: folder.id, + }); + } + await onRefreshFilesList?.(); + setIsActive(false); + }; useEffect(() => { if (!!folder.isCollapse === isCollapsed) return; foldersTable?.update({ @@ -62,22 +89,14 @@ export default memo(function FilesListFolderItem({ folder }: Props) { setIsCollapsed(!isCollapsed); }} onContextMenu={handleContextMenu} + draggable="true" onDragOver={(e) => { e.preventDefault(); setIsActive(true); }} - onDragLeave={() => { - setIsActive(false); - }} - onDrop={async () => { - if (draggingFile && !isFolder(draggingFile)) { - await workflowsTable?.updateFlow(draggingFile.id, { - parentFolderID: folder.id, - }); - await onRefreshFilesList?.(); - } - setIsActive(false); - }} + onDragLeave={() => setIsActive(false)} + onDragStart={() => setDraggingFile?.(folder)} + onDrop={handleDrop} _hover={activeStyle} style={isActive || isMenuOpen ? activeStyle : undefined} > @@ -97,14 +116,13 @@ export default memo(function FilesListFolderItem({ folder }: Props) { {folder.name} - - setIsMenuOpen(false)} - folder={folder} - /> + setIsMenuOpen(false)} + folder={folder} + /> {!isCollapsed && ( {children.map((file) => { diff --git a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx index d03157d..e25f467 100644 --- a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx +++ b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx @@ -31,7 +31,7 @@ import { sortFileItem } from "../utils"; import WorkflowListItem from "./WorkflowListItem"; import MultipleSelectionOperation from "./MultipleSelectionOperation"; import { ESortTypes, sortTypeLocalStorageKey } from "./types"; -// @ts-ignore +// @ts-expect-error ComfyUI import import { app } from "/scripts/app.js"; import { insertWorkflowToCanvas3 } from "./InsertWorkflowToCanvas"; import FilesListFolderItem from "./FilesListFolderItem"; @@ -129,7 +129,7 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { useEffect(() => { loadLatestWorkflows(); - const handleDrop = async (e: any) => { + const handleDrop = async (e: { canvasX: number; canvasY: number }) => { if (draggingWorkflowID.current) { const flow = await workflowsTable?.get(draggingWorkflowID.current); flow && diff --git a/ui/src/RecentFilesDrawer/WorkflowListItem.tsx b/ui/src/RecentFilesDrawer/WorkflowListItem.tsx index 9b9e133..013b43f 100644 --- a/ui/src/RecentFilesDrawer/WorkflowListItem.tsx +++ b/ui/src/RecentFilesDrawer/WorkflowListItem.tsx @@ -11,10 +11,14 @@ import { Tooltip, } from "@chakra-ui/react"; import { IconExternalLink } from "@tabler/icons-react"; -import { isFolder, workflowsTable } from "../db-tables/WorkspaceDB"; +import { + foldersTable, + isFolder, + workflowsTable, +} from "../db-tables/WorkspaceDB"; import { formatTimestamp, openWorkflowInNewTab, isImageFormat } from "../utils"; import AddTagToWorkflowPopover from "./AddTagToWorkflowPopover"; -import { useState, memo, ChangeEvent, useContext } from "react"; +import { MouseEvent, useState, memo, ChangeEvent, useContext } from "react"; import WorkflowListItemRightClickMenu from "./WorkflowListItemRightClickMenu"; import DeleteConfirm from "../components/DeleteConfirm"; import { RecentFilesContext, WorkspaceContext } from "../WorkspaceContext"; @@ -43,7 +47,7 @@ export default memo(function WorkflowListItem({ workflow }: Props) { multiSelectedFlowsID.includes(workflow.id); const { curFlowID, loadWorkflowID } = useContext(WorkspaceContext); const isSelected = curFlowID === workflow.id; - const handleContextMenu = (event: any) => { + const handleContextMenu = (event: MouseEvent) => { event.preventDefault(); setMenuPosition({ x: event.clientX, y: event.clientY }); setIsMenuOpen(true); @@ -54,6 +58,24 @@ export default memo(function WorkflowListItem({ workflow }: Props) { }; const hoverBgColor = colorMode === "light" ? "gray.200" : "#4A5568"; + const handleDrop = async () => { + if (!draggingFile) return setIsDraggingOver(false); + if (isFolder(draggingFile)) { + if (draggingFile.id === workflow.parentFolderID) + return setIsDraggingOver(false); + await foldersTable?.update({ + id: draggingFile.id, + parentFolderID: workflow.parentFolderID, + }); + } else { + await workflowsTable?.updateFlow(draggingFile.id, { + parentFolderID: workflow.parentFolderID, + }); + } + await onRefreshFilesList?.(); + setIsDraggingOver(false); + }; + const basicInfoComp = ( { setIsDraggingOver(false); }} - onDrop={async () => { - if (draggingFile && !isFolder(draggingFile)) { - await workflowsTable?.updateFlow(draggingFile.id, { - parentFolderID: workflow.parentFolderID, - }); - await onRefreshFilesList?.(); - } - setIsDraggingOver(false); - }} + onDrop={handleDrop} backgroundColor={ isSelected ? "teal.200" : isMenuOpen ? hoverBgColor : undefined } color={isSelected && !isMultiSelecting ? "#333" : undefined} draggable={!isMultiSelecting} - onDragStart={(e) => { - setDraggingFile && setDraggingFile(workflow); - }} + onDragStart={() => setDraggingFile?.(workflow)} borderRadius={6} px={1} py={1} diff --git a/ui/src/db-tables/FoldersTable.ts b/ui/src/db-tables/FoldersTable.ts index 62a87ec..ab40491 100644 --- a/ui/src/db-tables/FoldersTable.ts +++ b/ui/src/db-tables/FoldersTable.ts @@ -23,7 +23,10 @@ export class FoldersTable extends TableBase { name: string; parentFolderID?: string; }): Promise { - const uniqueName = await this.generateUniqueName(input.name); + const uniqueName = await this.generateUniqueName( + input.name, + input.parentFolderID, + ); const folder: Folder = { id: uuidv4(), name: uniqueName, @@ -53,11 +56,27 @@ export class FoldersTable extends TableBase { if (input.name != null) { newRecord.updateTime = Date.now(); } + const allFolders = await this.listAll(); + const folderWithSameName = allFolders.find( + (f) => + f.parentFolderID == newRecord.parentFolderID && // use == instead of ===, because parentFolderID can be null or undefined + f.name === newRecord.name && + f.id !== newRecord.id, + ); + if (folderWithSameName != null) { + input.name = await this.generateUniqueName( + newRecord.name, + newRecord.parentFolderID ?? undefined, + ); + } await indexdb.folders.update(input.id, input); this.saveDiskDB(); // folder moved or renamed - move all workflows to the right directory(not required when folded state changes) - if (input.name != null || input.parentFolderID != null) { + if ( + input.name != folder.name || + input.parentFolderID != folder.parentFolderID + ) { validateOrSaveAllJsonFileMyWorkflows(true); } } @@ -109,10 +128,10 @@ export class FoldersTable extends TableBase { this.saveDiskDB(); } - public async generateUniqueName(name?: string) { + public async generateUniqueName(name?: string, parentFolderID?: string) { let newFlowName = name ?? "New folder"; const folderNameList = await this.listAll().then((list) => - list.map((f) => f.name), + list.filter((f) => f.parentFolderID == parentFolderID).map((f) => f.name), ); if (folderNameList.includes(newFlowName)) { let num = 2; diff --git a/ui/src/db-tables/WorkflowsTable.ts b/ui/src/db-tables/WorkflowsTable.ts index c455e26..53d2ac6 100644 --- a/ui/src/db-tables/WorkflowsTable.ts +++ b/ui/src/db-tables/WorkflowsTable.ts @@ -113,10 +113,7 @@ export class WorkflowsTable extends TableBase { } await this.saveDiskDB(); // save to my_workflows/ - if ( - Object.prototype.hasOwnProperty.call(input, "name") || - Object.prototype.hasOwnProperty.call(input, "parentFolderID") - ) { + if ("name" in input || "parentFolderID" in input) { // renamed file or moved file folder await deleteJsonFileMyWorkflows(before); await saveJsonFileMyWorkflows(after); diff --git a/ui/src/utils.tsx b/ui/src/utils.tsx index 71e734c..7f61a94 100644 --- a/ui/src/utils.tsx +++ b/ui/src/utils.tsx @@ -1,5 +1,5 @@ // @ts-ignore -import { deleteFile, updateFile } from "./Api"; +import { ScanLocalFile, ScanLocalFolder, ScanLocalResult, deleteFile, updateFile } from "./Api"; import { ESortTypes } from "./RecentFilesDrawer/types"; import { workflowsTable, @@ -157,6 +157,7 @@ export async function validateOrSaveAllJsonFileMyWorkflows( for (const workflow of flowList) { const fullPath = await generateFilePathAbsolute(workflow); if (workflow.filePath != fullPath) { + console.log(workflow.filePath, fullPath); // file path changed workflow.filePath != null && (await deleteFile(workflow.filePath, deleteEmptyFolder)); @@ -546,16 +547,15 @@ export function getFileUrl(relativePath: string) { } export async function syncNewFlowOfLocalDisk( - singleFlowList: Workflow[], - folderList: { - name: string; - list: Workflow[]; - }[], + scanList: ScanLocalResult[], + parentFolderID?: string, ) { - if (singleFlowList.length) { - await workflowsTable?.batchCreateFlows(singleFlowList, true); + const fileList = scanList.filter((s): s is ScanLocalFile => "json" in s); + if (fileList.length) { + await workflowsTable?.batchCreateFlows(fileList, true, parentFolderID); } + const folderList = scanList.filter((s): s is ScanLocalFolder => "list" in s); if (folderList.length) { const currentFolderList = await foldersTable?.listAll(); @@ -571,11 +571,12 @@ export async function syncNewFlowOfLocalDisk( } else { const newFolder = await foldersTable?.create({ name: folder.name, + parentFolderID, }); folderId = newFolder?.id; } - await workflowsTable?.batchCreateFlows(folder.list, true, folderId); + await syncNewFlowOfLocalDisk(folder.list, folderId); } } } From 941aa7cb13f568649e5fffb61413838fb389a55f Mon Sep 17 00:00:00 2001 From: Arslan Ablikim Date: Mon, 29 Jan 2024 18:58:07 +0800 Subject: [PATCH 2/5] fix: drag style --- .../RecentFilesDrawer/FilesListFolderItem.tsx | 9 +-- ui/src/RecentFilesDrawer/ItemsList.tsx | 68 +++++++++++++++++++ .../RecentFilesDrawer/RecentFilesDrawer.tsx | 8 +-- ui/src/RecentFilesDrawer/WorkflowListItem.tsx | 37 ---------- 4 files changed, 72 insertions(+), 50 deletions(-) create mode 100644 ui/src/RecentFilesDrawer/ItemsList.tsx diff --git a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx index 4f1078d..a8b31a0 100644 --- a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx +++ b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx @@ -18,6 +18,7 @@ import WorkflowListItem from "./WorkflowListItem"; import FilesListFolderItemRightClickMenu from "./FilesListFolderItemRightClickMenu"; import { ESortTypes, sortTypeLocalStorageKey } from "./types"; import { Folder, Workflow } from "../types/dbTypes"; +import ItemsList from "./ItemsList"; type Props = { folder: Folder; @@ -125,13 +126,7 @@ export default memo(function FilesListFolderItem({ folder }: Props) { /> {!isCollapsed && ( - {children.map((file) => { - if (isFolder(file)) { - return ; - } else { - return ; - } - })} + )} diff --git a/ui/src/RecentFilesDrawer/ItemsList.tsx b/ui/src/RecentFilesDrawer/ItemsList.tsx new file mode 100644 index 0000000..01b142c --- /dev/null +++ b/ui/src/RecentFilesDrawer/ItemsList.tsx @@ -0,0 +1,68 @@ +import { useContext, useState } from "react"; +import { RecentFilesContext } from "../WorkspaceContext"; +import { + foldersTable, + isFolder, + workflowsTable, +} from "../db-tables/WorkspaceDB"; +import { Folder, Workflow } from "../types/dbTypes"; +import FilesListFolderItem from "./FilesListFolderItem"; +import WorkflowListItem from "./WorkflowListItem"; +import { Box } from "@chakra-ui/react"; + +export default function ItemsList({ + items, +}: { + items: Array; +}) { + const folders = items + .filter(isFolder) + .sort((a, b) => a.name.localeCompare(b.name)); + const workflows = items + .filter((item): item is Workflow => !isFolder(item)) + .sort((a, b) => a.name.localeCompare(b.name)); + const parentFolderID = workflows[0]?.parentFolderID; + + const [isDraggingOver, setIsDraggingOver] = useState(false); + const { onRefreshFilesList, draggingFile } = useContext(RecentFilesContext); + + const handleDrop = async () => { + if (!draggingFile) return setIsDraggingOver(false); + if (isFolder(draggingFile)) { + if (draggingFile.id === parentFolderID) return setIsDraggingOver(false); + await foldersTable?.update({ + id: draggingFile.id, + parentFolderID: parentFolderID, + }); + } else { + await workflowsTable?.updateFlow(draggingFile.id, { + parentFolderID: parentFolderID, + }); + } + await onRefreshFilesList?.(); + setIsDraggingOver(false); + }; + + return ( + <> + {folders.map((folder) => ( + + ))} + { + e.preventDefault(); + setIsDraggingOver(true); + }} + onDragLeave={() => { + setIsDraggingOver(false); + }} + onDrop={handleDrop} + > + {workflows.map((workflow) => ( + + ))} + + + ); +} diff --git a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx index e25f467..9eb3fee 100644 --- a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx +++ b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx @@ -41,6 +41,7 @@ import { openWorkflowsFolder } from "../Api"; import { Folder, Workflow } from "../types/dbTypes"; import ImportFileButton from "./ImportFileButton"; import MyTagsRow from "./MyTagsRow"; +import ItemsList from "./ItemsList"; type Props = { onClose: () => void; @@ -325,12 +326,7 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { onUpdateSearchValue={onUpdateSearchValue} /> - {currentRenderingData.map((n) => { - if (isFolder(n)) { - return ; - } - return ; - })} + diff --git a/ui/src/RecentFilesDrawer/WorkflowListItem.tsx b/ui/src/RecentFilesDrawer/WorkflowListItem.tsx index 013b43f..57e6708 100644 --- a/ui/src/RecentFilesDrawer/WorkflowListItem.tsx +++ b/ui/src/RecentFilesDrawer/WorkflowListItem.tsx @@ -11,11 +11,6 @@ import { Tooltip, } from "@chakra-ui/react"; import { IconExternalLink } from "@tabler/icons-react"; -import { - foldersTable, - isFolder, - workflowsTable, -} from "../db-tables/WorkspaceDB"; import { formatTimestamp, openWorkflowInNewTab, isImageFormat } from "../utils"; import AddTagToWorkflowPopover from "./AddTagToWorkflowPopover"; import { MouseEvent, useState, memo, ChangeEvent, useContext } from "react"; @@ -29,7 +24,6 @@ type Props = { }; export default memo(function WorkflowListItem({ workflow }: Props) { const { colorMode } = useColorMode(); - const [isDraggingOver, setIsDraggingOver] = useState(false); const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); const [isMenuOpen, setIsMenuOpen] = useState(false); const { @@ -38,8 +32,6 @@ export default memo(function WorkflowListItem({ workflow }: Props) { onMultiSelectFlow, onDeleteFlow, multiSelectedFlowsID, - onRefreshFilesList, - draggingFile, } = useContext(RecentFilesContext); const isChecked = multiSelectedFlowsID && @@ -58,36 +50,10 @@ export default memo(function WorkflowListItem({ workflow }: Props) { }; const hoverBgColor = colorMode === "light" ? "gray.200" : "#4A5568"; - const handleDrop = async () => { - if (!draggingFile) return setIsDraggingOver(false); - if (isFolder(draggingFile)) { - if (draggingFile.id === workflow.parentFolderID) - return setIsDraggingOver(false); - await foldersTable?.update({ - id: draggingFile.id, - parentFolderID: workflow.parentFolderID, - }); - } else { - await workflowsTable?.updateFlow(draggingFile.id, { - parentFolderID: workflow.parentFolderID, - }); - } - await onRefreshFilesList?.(); - setIsDraggingOver(false); - }; - const basicInfoComp = ( { - e.preventDefault(); - setIsDraggingOver(true); - }} - onDragLeave={() => { - setIsDraggingOver(false); - }} - onDrop={handleDrop} backgroundColor={ isSelected ? "teal.200" : isMenuOpen ? hoverBgColor : undefined } @@ -132,9 +98,6 @@ export default memo(function WorkflowListItem({ workflow }: Props) { - {isDraggingOver && ( - - )} ); From 0a56d12eff5cc681903cc3dcbd603b039cb0283c Mon Sep 17 00:00:00 2001 From: Arslan Ablikim Date: Tue, 30 Jan 2024 12:43:06 +0800 Subject: [PATCH 3/5] feat: add folders on top setting --- .../RecentFilesDrawer/FilesListFolderItem.tsx | 12 +++-- ui/src/RecentFilesDrawer/ItemsList.tsx | 53 +++++++++++++------ .../RecentFilesDrawer/RecentFilesDrawer.tsx | 35 ++++++++++-- ui/src/RecentFilesDrawer/types.ts | 1 + ui/src/db-tables/FoldersTable.ts | 18 +++---- 5 files changed, 84 insertions(+), 35 deletions(-) diff --git a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx index a8b31a0..078bc69 100644 --- a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx +++ b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx @@ -14,7 +14,6 @@ import { } from "react"; import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"; import { RecentFilesContext } from "../WorkspaceContext"; -import WorkflowListItem from "./WorkflowListItem"; import FilesListFolderItemRightClickMenu from "./FilesListFolderItemRightClickMenu"; import { ESortTypes, sortTypeLocalStorageKey } from "./types"; import { Folder, Workflow } from "../types/dbTypes"; @@ -57,7 +56,9 @@ export default memo(function FilesListFolderItem({ folder }: Props) { setMenuPosition({ x: event.clientX, y: event.clientY }); setIsMenuOpen(true); }; - const handleDrop = async () => { + const handleDrop = async (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); if (!draggingFile) return setIsActive(false); if (isFolder(draggingFile)) { if (draggingFile.id === folder.id) return setIsActive(false); @@ -93,9 +94,14 @@ export default memo(function FilesListFolderItem({ folder }: Props) { draggable="true" onDragOver={(e) => { e.preventDefault(); + e.stopPropagation(); setIsActive(true); }} - onDragLeave={() => setIsActive(false)} + onDragLeave={(e) => { + e.preventDefault(); + e.stopPropagation(); + setIsActive(false); + }} onDragStart={() => setDraggingFile?.(folder)} onDrop={handleDrop} _hover={activeStyle} diff --git a/ui/src/RecentFilesDrawer/ItemsList.tsx b/ui/src/RecentFilesDrawer/ItemsList.tsx index 01b142c..63cdbd3 100644 --- a/ui/src/RecentFilesDrawer/ItemsList.tsx +++ b/ui/src/RecentFilesDrawer/ItemsList.tsx @@ -1,4 +1,4 @@ -import { useContext, useState } from "react"; +import { DragEvent, useContext, useEffect, useState } from "react"; import { RecentFilesContext } from "../WorkspaceContext"; import { foldersTable, @@ -9,24 +9,27 @@ import { Folder, Workflow } from "../types/dbTypes"; import FilesListFolderItem from "./FilesListFolderItem"; import WorkflowListItem from "./WorkflowListItem"; import { Box } from "@chakra-ui/react"; +import { folderOnTopLocalStorageKey } from "./types"; export default function ItemsList({ items, }: { items: Array; }) { - const folders = items - .filter(isFolder) - .sort((a, b) => a.name.localeCompare(b.name)); - const workflows = items - .filter((item): item is Workflow => !isFolder(item)) - .sort((a, b) => a.name.localeCompare(b.name)); + const [folderOnTop, setFolderOnTop] = useState( + window.localStorage.getItem(folderOnTopLocalStorageKey) === "true", + ); + const folders = items.filter(isFolder); + const workflows = items.filter((item): item is Workflow => !isFolder(item)); const parentFolderID = workflows[0]?.parentFolderID; const [isDraggingOver, setIsDraggingOver] = useState(false); - const { onRefreshFilesList, draggingFile } = useContext(RecentFilesContext); + const { onRefreshFilesList, draggingFile, refreshFolderStamp } = + useContext(RecentFilesContext); - const handleDrop = async () => { + const handleDrop = async (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); if (!draggingFile) return setIsDraggingOver(false); if (isFolder(draggingFile)) { if (draggingFile.id === parentFolderID) return setIsDraggingOver(false); @@ -43,25 +46,43 @@ export default function ItemsList({ setIsDraggingOver(false); }; + useEffect(() => { + setFolderOnTop( + window.localStorage.getItem(folderOnTopLocalStorageKey) === "true", + ); + }, [refreshFolderStamp]); + return ( <> - {folders.map((folder) => ( - - ))} + {folderOnTop && + folders.map((folder) => ( + + ))} { e.preventDefault(); + e.stopPropagation(); setIsDraggingOver(true); }} - onDragLeave={() => { + onDragLeave={(e) => { + console.log(e.target); + e.preventDefault(); + e.stopPropagation(); setIsDraggingOver(false); }} onDrop={handleDrop} > - {workflows.map((workflow) => ( - - ))} + {folderOnTop + ? workflows.map((workflow) => ( + + )) + : items.map((n) => { + if (isFolder(n)) { + return ; + } + return ; + })} ); diff --git a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx index 9eb3fee..7cf7852 100644 --- a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx +++ b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx @@ -12,8 +12,16 @@ import { Box, Flex, Tooltip, + Switch, } from "@chakra-ui/react"; -import { useEffect, useState, useRef, useCallback, useContext } from "react"; +import { + useEffect, + useState, + useRef, + useCallback, + useContext, + ChangeEvent, +} from "react"; import { workflowsTable, isFolder, @@ -28,13 +36,15 @@ import { import { RecentFilesContext, WorkspaceContext } from "../WorkspaceContext"; import RecentFilesDrawerMenu from "./RecentFilesDrawerMenu"; import { sortFileItem } from "../utils"; -import WorkflowListItem from "./WorkflowListItem"; import MultipleSelectionOperation from "./MultipleSelectionOperation"; -import { ESortTypes, sortTypeLocalStorageKey } from "./types"; +import { + ESortTypes, + folderOnTopLocalStorageKey, + sortTypeLocalStorageKey, +} from "./types"; // @ts-expect-error ComfyUI import import { app } from "/scripts/app.js"; import { insertWorkflowToCanvas3 } from "./InsertWorkflowToCanvas"; -import FilesListFolderItem from "./FilesListFolderItem"; import { useDebounce } from "../customHooks/useDebounce"; import SearchInput from "../components/SearchInput"; import { openWorkflowsFolder } from "../Api"; @@ -69,6 +79,9 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { (window.localStorage.getItem(sortTypeLocalStorageKey) as ESortTypes) ?? ESortTypes.RECENTLY_MODIFIED, ); + const folderOnTopRef = useRef( + window.localStorage.getItem(folderOnTopLocalStorageKey) === "true", + ); const loadLatestWorkflows = async () => { const all = (await workflowsTable?.listFolderContent()) ?? []; @@ -113,6 +126,13 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { !isFilter && setRefreshFolderStamp(Date.now()); }; + const onFolderOnTopChange = (e: ChangeEvent) => { + const state = e.target.checked; + folderOnTopRef.current = state; + window.localStorage.setItem(folderOnTopLocalStorageKey, state.toString()); + !isFilter && setRefreshFolderStamp(Date.now()); + }; + useEffect(() => { filterFlows(); }, [debounceSearchValue, selectedTag]); @@ -320,6 +340,13 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { + + Folders on Top + + { if (folder == null) { return; } + const nameChanged = input.name != undefined && input.name != folder.name; + const parentFolderChanged = + input.parentFolderID != undefined && + input.parentFolderID != folder.parentFolderID; const newRecord = { ...folder, ...input, @@ -56,14 +60,7 @@ export class FoldersTable extends TableBase { if (input.name != null) { newRecord.updateTime = Date.now(); } - const allFolders = await this.listAll(); - const folderWithSameName = allFolders.find( - (f) => - f.parentFolderID == newRecord.parentFolderID && // use == instead of ===, because parentFolderID can be null or undefined - f.name === newRecord.name && - f.id !== newRecord.id, - ); - if (folderWithSameName != null) { + if (parentFolderChanged) { input.name = await this.generateUniqueName( newRecord.name, newRecord.parentFolderID ?? undefined, @@ -73,10 +70,7 @@ export class FoldersTable extends TableBase { this.saveDiskDB(); // folder moved or renamed - move all workflows to the right directory(not required when folded state changes) - if ( - input.name != folder.name || - input.parentFolderID != folder.parentFolderID - ) { + if (nameChanged || parentFolderChanged) { validateOrSaveAllJsonFileMyWorkflows(true); } } From 1399adfce394976914799d4b5184093906416d2e Mon Sep 17 00:00:00 2001 From: Arslan Ablikim Date: Tue, 30 Jan 2024 12:57:30 +0800 Subject: [PATCH 4/5] fix: drag flicker --- .../RecentFilesDrawer/FilesListFolderItem.tsx | 1 + ui/src/RecentFilesDrawer/ItemsList.tsx | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx index 078bc69..1820e93 100644 --- a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx +++ b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx @@ -106,6 +106,7 @@ export default memo(function FilesListFolderItem({ folder }: Props) { onDrop={handleDrop} _hover={activeStyle} style={isActive || isMenuOpen ? activeStyle : undefined} + className="droppable" > {isCollapsed ? ( diff --git a/ui/src/RecentFilesDrawer/ItemsList.tsx b/ui/src/RecentFilesDrawer/ItemsList.tsx index 63cdbd3..d87dce2 100644 --- a/ui/src/RecentFilesDrawer/ItemsList.tsx +++ b/ui/src/RecentFilesDrawer/ItemsList.tsx @@ -27,6 +27,20 @@ export default function ItemsList({ const { onRefreshFilesList, draggingFile, refreshFolderStamp } = useContext(RecentFilesContext); + const handleDragLeave = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + const targetIsDroppable = + e.relatedTarget instanceof Element && + e.relatedTarget.classList.contains("droppable") && + e.relatedTarget !== e.currentTarget; + const targetIsChild = e.currentTarget.contains(e.relatedTarget as Node); + // fixes a bug where the dragleave event is fired when dragging over a child element, causing elements to flicker + if (targetIsDroppable || !targetIsChild) { + setIsDraggingOver(false); + } + }; + const handleDrop = async (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); @@ -65,13 +79,9 @@ export default function ItemsList({ e.stopPropagation(); setIsDraggingOver(true); }} - onDragLeave={(e) => { - console.log(e.target); - e.preventDefault(); - e.stopPropagation(); - setIsDraggingOver(false); - }} + onDragLeave={handleDragLeave} onDrop={handleDrop} + className="droppable" > {folderOnTop ? workflows.map((workflow) => ( From 75447025d32a834f69125636c0717f1cf37af5d1 Mon Sep 17 00:00:00 2001 From: Arslan Ablikim Date: Tue, 30 Jan 2024 17:54:40 +0800 Subject: [PATCH 5/5] fix: put folder on top into userSettings table --- ui/src/RecentFilesDrawer/ItemsList.tsx | 11 ++++--- .../RecentFilesDrawer/RecentFilesDrawer.tsx | 29 +++++++++++-------- ui/src/RecentFilesDrawer/types.ts | 1 - ui/src/types/dbTypes.ts | 1 + 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/ui/src/RecentFilesDrawer/ItemsList.tsx b/ui/src/RecentFilesDrawer/ItemsList.tsx index d87dce2..0f00bdd 100644 --- a/ui/src/RecentFilesDrawer/ItemsList.tsx +++ b/ui/src/RecentFilesDrawer/ItemsList.tsx @@ -10,15 +10,14 @@ import FilesListFolderItem from "./FilesListFolderItem"; import WorkflowListItem from "./WorkflowListItem"; import { Box } from "@chakra-ui/react"; import { folderOnTopLocalStorageKey } from "./types"; +import { userSettingsTable } from "../db-tables/WorkspaceDB"; export default function ItemsList({ items, }: { items: Array; }) { - const [folderOnTop, setFolderOnTop] = useState( - window.localStorage.getItem(folderOnTopLocalStorageKey) === "true", - ); + const [folderOnTop, setFolderOnTop] = useState(false); const folders = items.filter(isFolder); const workflows = items.filter((item): item is Workflow => !isFolder(item)); const parentFolderID = workflows[0]?.parentFolderID; @@ -61,9 +60,9 @@ export default function ItemsList({ }; useEffect(() => { - setFolderOnTop( - window.localStorage.getItem(folderOnTopLocalStorageKey) === "true", - ); + userSettingsTable?.getSetting("foldersOnTop").then((res) => { + setFolderOnTop(res ?? false); + }); }, [refreshFolderStamp]); return ( diff --git a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx index 7cf7852..dd364a3 100644 --- a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx +++ b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx @@ -26,6 +26,7 @@ import { workflowsTable, isFolder, foldersTable, + userSettingsTable, } from "../db-tables/WorkspaceDB"; import { IconChevronDown, @@ -37,11 +38,7 @@ import { RecentFilesContext, WorkspaceContext } from "../WorkspaceContext"; import RecentFilesDrawerMenu from "./RecentFilesDrawerMenu"; import { sortFileItem } from "../utils"; import MultipleSelectionOperation from "./MultipleSelectionOperation"; -import { - ESortTypes, - folderOnTopLocalStorageKey, - sortTypeLocalStorageKey, -} from "./types"; +import { ESortTypes, sortTypeLocalStorageKey } from "./types"; // @ts-expect-error ComfyUI import import { app } from "/scripts/app.js"; import { insertWorkflowToCanvas3 } from "./InsertWorkflowToCanvas"; @@ -79,9 +76,12 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { (window.localStorage.getItem(sortTypeLocalStorageKey) as ESortTypes) ?? ESortTypes.RECENTLY_MODIFIED, ); - const folderOnTopRef = useRef( - window.localStorage.getItem(folderOnTopLocalStorageKey) === "true", - ); + const [folderOnTop, setFolderOnTop] = useState(false); + useEffect(() => { + userSettingsTable?.getSetting("foldersOnTop").then((res) => { + setFolderOnTop(res ?? false); + }); + }, []); const loadLatestWorkflows = async () => { const all = (await workflowsTable?.listFolderContent()) ?? []; @@ -128,9 +128,14 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { const onFolderOnTopChange = (e: ChangeEvent) => { const state = e.target.checked; - folderOnTopRef.current = state; - window.localStorage.setItem(folderOnTopLocalStorageKey, state.toString()); - !isFilter && setRefreshFolderStamp(Date.now()); + userSettingsTable + ?.upsert({ + foldersOnTop: state, + }) + .then(() => { + setFolderOnTop(state); + !isFilter && setRefreshFolderStamp(Date.now()); + }); }; useEffect(() => { @@ -343,7 +348,7 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) { Folders on Top diff --git a/ui/src/RecentFilesDrawer/types.ts b/ui/src/RecentFilesDrawer/types.ts index e8fbe2c..53ecff8 100644 --- a/ui/src/RecentFilesDrawer/types.ts +++ b/ui/src/RecentFilesDrawer/types.ts @@ -2,7 +2,6 @@ * The key value of the sort type in the local cache */ export const sortTypeLocalStorageKey = "CWM_WORKFLOWS_SORT_TYPE"; -export const folderOnTopLocalStorageKey = "CWM_WORKFLOWS_FOLDER_ON_TOP"; export enum ESortTypes { /** diff --git a/ui/src/types/dbTypes.ts b/ui/src/types/dbTypes.ts index 554902c..16b1e8f 100644 --- a/ui/src/types/dbTypes.ts +++ b/ui/src/types/dbTypes.ts @@ -54,6 +54,7 @@ export type UserSettings = { }; autoSave?: boolean; twoWaySync?: boolean; + foldersOnTop?: boolean; }; export interface PanelPosition {