Merge pull request #160 from 11cafe/feat/folder-drag-drop

feat: folder drag drop
This commit is contained in:
Arslan Ablikim
2024-01-31 11:15:11 +08:00
committed by GitHub
11 changed files with 249 additions and 113 deletions
+18 -23
View File
@@ -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")
+11 -1
View File
@@ -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);
+3 -5
View File
@@ -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);
}
};
@@ -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"
>
<HStack gap={1}>
{isCollapsed ? (
@@ -97,23 +124,16 @@ export default memo(function FilesListFolderItem({ folder }: Props) {
</svg>
</HStack>
<Text>{folder.name}</Text>
<FilesListFolderItemRightClickMenu
isopen={isMenuOpen}
menuPosition={menuPosition}
onClose={() => setIsMenuOpen(false)}
folder={folder}
/>
</HStack>
<FilesListFolderItemRightClickMenu
isopen={isMenuOpen}
menuPosition={menuPosition}
onClose={() => setIsMenuOpen(false)}
folder={folder}
/>
{!isCollapsed && (
<Stack ml={4} gap={0}>
{children.map((file) => {
if (isFolder(file)) {
return <FilesListFolderItem key={file.id} folder={file} />;
} else {
return <WorkflowListItem key={file.id} workflow={file} />;
}
})}
<ItemsList items={children} />
</Stack>
)}
</Stack>
+98
View File
@@ -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<Folder | Workflow>;
}) {
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) => (
<FilesListFolderItem key={folder.id} folder={folder} />
))}
<Box
border={isDraggingOver ? "2px dashed #718096" : undefined}
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
setIsDraggingOver(true);
}}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className="droppable"
>
{folderOnTop
? workflows.map((workflow) => (
<WorkflowListItem key={workflow.id} workflow={workflow} />
))
: items.map((n) => {
if (isFolder(n)) {
return <FilesListFolderItem folder={n} key={n.id} />;
}
return <WorkflowListItem key={n.id} workflow={n} />;
})}
</Box>
</>
);
}
+39 -11
View File
@@ -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<HTMLInputElement>) => {
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) {
</MenuOptionGroup>
</MenuList>
</Menu>
<Flex gap={2} align="center">
<Text>Folders on Top</Text>
<Switch
isChecked={folderOnTop}
onChange={onFolderOnTopChange}
/>
</Flex>
</HStack>
<SearchInput
searchValue={searchValue}
onUpdateSearchValue={onUpdateSearchValue}
/>
<Flex overflowY={"auto"} overflowX={"hidden"} direction="column">
{currentRenderingData.map((n) => {
if (isFolder(n)) {
return <FilesListFolderItem folder={n} key={n.id} />;
}
return <WorkflowListItem key={n.id} workflow={n} />;
})}
<ItemsList items={currentRenderingData} />
</Flex>
</Flex>
</Card>
+3 -28
View File
@@ -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) {
<Box
flexShrink={1}
flexGrow={1}
onDragOver={(e) => {
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) {
</Text>
</Stack>
</HStack>
{isDraggingOver && (
<Box width={"100%"} mt={2} height={"2px"} backgroundColor={"#4299E1"} />
)}
</Box>
);
+17 -4
View File
@@ -23,7 +23,10 @@ export class FoldersTable extends TableBase<Folder> {
name: string;
parentFolderID?: string;
}): Promise<Folder> {
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<Folder> {
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<Folder> {
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<Folder> {
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;
+1 -4
View File
@@ -113,10 +113,7 @@ export class WorkflowsTable extends TableBase<Workflow> {
}
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);
+1
View File
@@ -54,6 +54,7 @@ export type UserSettings = {
};
autoSave?: boolean;
twoWaySync?: boolean;
foldersOnTop?: boolean;
};
export interface PanelPosition {
+10 -9
View File
@@ -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);
}
}
}