Files
11cafe-comfyui-workspace-ma…/ui/src/RecentFilesDrawer/ImportFlowsFileInput.tsx
T
2024-02-05 21:18:52 +08:00

248 lines
7.1 KiB
TypeScript

import { ChangeEvent, useContext, useRef, useState } from "react";
import { Button, Menu, MenuButton, MenuList, MenuItem } from "@chakra-ui/react";
import { IconFile, IconFileImport, IconFolder } from "@tabler/icons-react";
import { workflowsTable, foldersTable } from "../db-tables/WorkspaceDB";
import { RecentFilesContext } from "../WorkspaceContext";
import { getPngMetadata } from "../utils/mediaMetadataUtils";
import { ImportWorkflow, importMenuItemList } from "./types";
interface WorkflowFile {
name: string;
json: string;
}
interface TreeNode {
name: string;
isFile: boolean;
file?: WorkflowFile;
children?: TreeNode[];
parentFolderID?: string;
}
interface HTMLInputDirectoryElement extends HTMLInputElement {
webkitdirectory: boolean;
}
interface Props {
onlyImportFile?: boolean;
parentFolderID?: string;
}
export default function ImportFlowsFileInput({
parentFolderID,
onlyImportFile = false,
}: Props) {
const { onRefreshFilesList } = useContext(RecentFilesContext);
const [importType, setImportType] = useState("file"); // file/folder
const fileInputRef = useRef<HTMLInputDirectoryElement>(null);
const onClick = (type: string) => {
setImportType(type);
if (type === "folder") {
fileInputRef.current!.setAttribute("webkitdirectory", "");
} else {
fileInputRef.current!.removeAttribute("webkitdirectory");
}
fileInputRef.current?.click();
};
const parseFile = async (file: File): Promise<WorkflowFile | null> => {
if (file.type === "image/png") {
const pngInfo = await getPngMetadata(file);
// @ts-expect-error
const flowJson = pngInfo?.workflow;
if (flowJson) {
return {
json: flowJson,
name: file.name.replace(".png", ""),
};
}
} else if (
file.type === "application/json" ||
file.name?.endsWith(".json")
) {
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const result = e.target?.result;
if (typeof result === "string") {
resolve({
json: result,
name: file.name.replace(".json", ""),
});
} else {
reject(new Error("File content is not a string"));
}
} catch (error) {
reject(error);
}
};
reader.onerror = reject;
reader.readAsText(file);
});
}
return null;
};
const createFileAndFolderFormTree = async (treeNode: TreeNode) => {
// Traverse the folders in the tree through queue breadth first, creating folders and files under the current folder in sequence;
const queue = [treeNode];
while (queue.length > 0) {
const currentNode = queue.shift();
if (currentNode && !currentNode.isFile) {
const folder = await foldersTable?.create({
name: currentNode.name,
parentFolderID: currentNode.parentFolderID,
});
if (currentNode.children && currentNode.children.length) {
const fileList: WorkflowFile[] = [];
currentNode.children.forEach((child) => {
if (child.isFile && child.file) {
fileList.push(child.file);
} else {
child.parentFolderID = folder?.id;
queue.push(child);
}
});
if (fileList.length) {
await workflowsTable?.batchCreateFlows(
fileList,
undefined,
folder?.id,
);
}
}
}
}
onRefreshFilesList?.();
};
const handleFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (!files) return;
if (importType === "folder") {
// Create a root node based on the selected directory
const rootNode: TreeNode = {
name: files[0].webkitRelativePath.split("/")[0],
isFile: false,
children: [],
};
// Create nested tree file structure from file path
for (const file of files) {
if (file.name !== ".DS_Store") {
const splitPath = file.webkitRelativePath.split("/");
let currentLevel = rootNode.children ?? [];
// Start traversing the second part of the path
for (let index = 1; index < splitPath.length; index++) {
const part = splitPath[index];
let existingNode = currentLevel.find(
(child) => child.name === part,
);
if (!existingNode) {
existingNode = {
name: part,
isFile: index === splitPath.length - 1,
children: index === splitPath.length - 1 ? undefined : [],
};
if (existingNode.isFile) {
const parseFileResult = await parseFile(file);
if (parseFileResult) {
existingNode.file = parseFileResult;
}
}
currentLevel.push(existingNode);
}
if (existingNode && !existingNode.isFile) {
currentLevel = existingNode.children!;
}
}
}
}
createFileAndFolderFormTree(rootNode);
} else {
const parsedFileList: ImportWorkflow[] = [];
for (const file of files) {
const parseFileResult = await parseFile(file);
if (parseFileResult) {
parsedFileList.push(parseFileResult);
}
}
if (parsedFileList.length) {
await workflowsTable?.batchCreateFlows(
parsedFileList,
undefined,
parentFolderID,
);
onRefreshFilesList && onRefreshFilesList();
}
}
};
return (
<>
{onlyImportFile ? (
<MenuItem
onClick={(e) => {
e.stopPropagation();
onClick("file");
}}
icon={<IconFileImport size={19} />}
>
Import workflows
</MenuItem>
) : (
<Menu closeOnSelect={true}>
<MenuButton
as={Button}
leftIcon={<IconFileImport size={18} />}
variant="outline"
iconSpacing={1}
size={"sm"}
colorScheme="teal"
>
Import
</MenuButton>
<MenuList>
{importMenuItemList.map((item, index) => (
<MenuItem
key={index}
onClick={() => {
onClick(item.type);
}}
iconSpacing={2}
icon={
item.type === "folder" ? (
<IconFolder size={18} />
) : (
<IconFile size={18} />
)
}
>
{item.title}
</MenuItem>
))}
</MenuList>
</Menu>
)}
<input
style={{ display: "none" }}
ref={fileInputRef}
type="file"
accept=".json,image/png"
multiple
onChange={handleFileChange}
onClick={(e) => {
e.stopPropagation();
}}
/>
</>
);
}