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..1820e93 100644
--- a/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx
+++ b/ui/src/RecentFilesDrawer/FilesListFolderItem.tsx
@@ -4,13 +4,20 @@ 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";
import FilesListFolderItemRightClickMenu from "./FilesListFolderItemRightClickMenu";
import { ESortTypes, sortTypeLocalStorageKey } from "./types";
import { Folder, Workflow } from "../types/dbTypes";
+import ItemsList from "./ItemsList";
type Props = {
folder: Folder;
@@ -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,24 @@ export default memo(function FilesListFolderItem({ folder }: Props) {
setMenuPosition({ x: event.clientX, y: event.clientY });
setIsMenuOpen(true);
};
+ 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);
+ 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,24 +91,22 @@ export default memo(function FilesListFolderItem({ folder }: Props) {
setIsCollapsed(!isCollapsed);
}}
onContextMenu={handleContextMenu}
+ draggable="true"
onDragOver={(e) => {
e.preventDefault();
+ e.stopPropagation();
setIsActive(true);
}}
- onDragLeave={() => {
- setIsActive(false);
- }}
- onDrop={async () => {
- if (draggingFile && !isFolder(draggingFile)) {
- await workflowsTable?.updateFlow(draggingFile.id, {
- parentFolderID: folder.id,
- });
- await onRefreshFilesList?.();
- }
+ onDragLeave={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
setIsActive(false);
}}
+ onDragStart={() => setDraggingFile?.(folder)}
+ onDrop={handleDrop}
_hover={activeStyle}
style={isActive || isMenuOpen ? activeStyle : undefined}
+ className="droppable"
>
{isCollapsed ? (
@@ -97,23 +124,16 @@ export default memo(function FilesListFolderItem({ folder }: Props) {
{folder.name}
-
- setIsMenuOpen(false)}
- folder={folder}
- />
+ setIsMenuOpen(false)}
+ folder={folder}
+ />
{!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..0f00bdd
--- /dev/null
+++ b/ui/src/RecentFilesDrawer/ItemsList.tsx
@@ -0,0 +1,98 @@
+import { DragEvent, useContext, useEffect, 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";
+import { folderOnTopLocalStorageKey } from "./types";
+import { userSettingsTable } from "../db-tables/WorkspaceDB";
+
+export default function ItemsList({
+ items,
+}: {
+ items: Array;
+}) {
+ 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;
+
+ const [isDraggingOver, setIsDraggingOver] = useState(false);
+ 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();
+ 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);
+ };
+
+ useEffect(() => {
+ userSettingsTable?.getSetting("foldersOnTop").then((res) => {
+ setFolderOnTop(res ?? false);
+ });
+ }, [refreshFolderStamp]);
+
+ return (
+ <>
+ {folderOnTop &&
+ folders.map((folder) => (
+
+ ))}
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsDraggingOver(true);
+ }}
+ onDragLeave={handleDragLeave}
+ onDrop={handleDrop}
+ className="droppable"
+ >
+ {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 d03157d..dd364a3 100644
--- a/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx
+++ b/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx
@@ -12,12 +12,21 @@ 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,
foldersTable,
+ userSettingsTable,
} from "../db-tables/WorkspaceDB";
import {
IconChevronDown,
@@ -28,19 +37,18 @@ 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";
-// @ts-ignore
+// @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";
import { Folder, Workflow } from "../types/dbTypes";
import ImportFileButton from "./ImportFileButton";
import MyTagsRow from "./MyTagsRow";
+import ItemsList from "./ItemsList";
type Props = {
onClose: () => void;
@@ -68,6 +76,12 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
(window.localStorage.getItem(sortTypeLocalStorageKey) as ESortTypes) ??
ESortTypes.RECENTLY_MODIFIED,
);
+ const [folderOnTop, setFolderOnTop] = useState(false);
+ useEffect(() => {
+ userSettingsTable?.getSetting("foldersOnTop").then((res) => {
+ setFolderOnTop(res ?? false);
+ });
+ }, []);
const loadLatestWorkflows = async () => {
const all = (await workflowsTable?.listFolderContent()) ?? [];
@@ -112,6 +126,18 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
!isFilter && setRefreshFolderStamp(Date.now());
};
+ const onFolderOnTopChange = (e: ChangeEvent) => {
+ const state = e.target.checked;
+ userSettingsTable
+ ?.upsert({
+ foldersOnTop: state,
+ })
+ .then(() => {
+ setFolderOnTop(state);
+ !isFilter && setRefreshFolderStamp(Date.now());
+ });
+ };
+
useEffect(() => {
filterFlows();
}, [debounceSearchValue, selectedTag]);
@@ -129,7 +155,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 &&
@@ -319,18 +345,20 @@ export default function RecentFilesDrawer({ onClose, onClickNewFlow }: Props) {
+
+ Folders on Top
+
+
- {currentRenderingData.map((n) => {
- if (isFolder(n)) {
- return ;
- }
- return ;
- })}
+
diff --git a/ui/src/RecentFilesDrawer/WorkflowListItem.tsx b/ui/src/RecentFilesDrawer/WorkflowListItem.tsx
index 9b9e133..57e6708 100644
--- a/ui/src/RecentFilesDrawer/WorkflowListItem.tsx
+++ b/ui/src/RecentFilesDrawer/WorkflowListItem.tsx
@@ -11,10 +11,9 @@ import {
Tooltip,
} from "@chakra-ui/react";
import { IconExternalLink } from "@tabler/icons-react";
-import { 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";
@@ -25,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 {
@@ -34,8 +32,6 @@ export default memo(function WorkflowListItem({ workflow }: Props) {
onMultiSelectFlow,
onDeleteFlow,
multiSelectedFlowsID,
- onRefreshFilesList,
- draggingFile,
} = useContext(RecentFilesContext);
const isChecked =
multiSelectedFlowsID &&
@@ -43,7 +39,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);
@@ -58,30 +54,12 @@ export default memo(function WorkflowListItem({ workflow }: Props) {
{
- e.preventDefault();
- setIsDraggingOver(true);
- }}
- onDragLeave={() => {
- setIsDraggingOver(false);
- }}
- onDrop={async () => {
- if (draggingFile && !isFolder(draggingFile)) {
- await workflowsTable?.updateFlow(draggingFile.id, {
- parentFolderID: workflow.parentFolderID,
- });
- await onRefreshFilesList?.();
- }
- setIsDraggingOver(false);
- }}
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}
@@ -120,9 +98,6 @@ export default memo(function WorkflowListItem({ workflow }: Props) {
- {isDraggingOver && (
-
- )}
);
diff --git a/ui/src/db-tables/FoldersTable.ts b/ui/src/db-tables/FoldersTable.ts
index 62a87ec..7dd9595 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,
@@ -46,6 +49,10 @@ export class FoldersTable extends TableBase {
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,
@@ -53,11 +60,17 @@ export class FoldersTable extends TableBase {
if (input.name != null) {
newRecord.updateTime = Date.now();
}
+ if (parentFolderChanged) {
+ 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 (nameChanged || parentFolderChanged) {
validateOrSaveAllJsonFileMyWorkflows(true);
}
}
@@ -109,10 +122,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/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 {
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);
}
}
}