Files
11cafe-comfyui-workspace-ma…/ui/src/RecentFilesDrawer/RecentFilesDrawer.tsx
T

344 lines
11 KiB
TypeScript

import {
Text,
Button,
HStack,
IconButton,
Menu,
MenuButton,
MenuList,
MenuItemOption,
MenuOptionGroup,
Card,
Box,
Flex,
Tooltip,
Input,
} from "@chakra-ui/react";
import { useContext, useEffect, useState, useRef, useCallback } from "react";
import {
Workflow,
deleteFlow,
isFolder,
listWorkflows,
listFolderContent,
tagsTable,
workspace,
foldersTable,
Folder,
} from "../WorkspaceDB";
import {
IconChevronDown,
IconChevronUp,
IconFolderPlus,
IconPlus,
IconX,
} from "@tabler/icons-react";
import { RecentFilesContext, WorkspaceContext } from "../WorkspaceContext";
import RecentFilesDrawerMenu from "./RecentFilesDrawerMenu";
import { sortFlows, sortFileItem } from "../utils";
import WorkflowListItem from "./WorkflowListItem";
import ImportJsonFlows from "./ImportJsonFlows";
import MultipleSelectionOperation from "./MultipleSelectionOperation";
import { ESortTypes, sortTypeLocalStorageKey } from "./types";
// @ts-ignore
import { app } from "/scripts/app.js";
import { insertWorkflowToCanvas3 } from "./InsertWorkflowToCanvas";
import FilesListFolderItem from "./FilesListFolderItem";
import { useDebounce } from "../customHooks/useDebaunce";
import SeacrhInput from "../components/SearchInput";
const MAX_TAGS_TO_SHOW = 6;
type Props = {
onclose: () => void;
onClickNewFlow: () => void;
};
export default function RecentFilesDrawer({ onclose, onClickNewFlow }: Props) {
const [files, setFiles] = useState<Array<Folder | Workflow>>([]);
const recentFlows = files.filter((n) => !isFolder(n)) as Workflow[];
const { curFlowID } = useContext(WorkspaceContext);
const [selectedTag, setSelectedTag] = useState<string>();
const [showAllTags, setShowAllTags] = useState(false);
const [multipleState, setMultipleState] = useState(false);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [refreshFolderStamp, setRefreshFolderStamp] = useState(0);
const [searchValue, setSearchValue] = useState("");
const debaunceSearchValue = useDebounce(searchValue, 400);
const draggingWorkflowID = useRef<string | null>(null);
const [draggingFile, setDraggingFile] = useState<Workflow | Folder | null>(
null
);
const sortTypeRef = useRef<ESortTypes>(
(window.localStorage.getItem(sortTypeLocalStorageKey) as ESortTypes) ??
ESortTypes.RECENTLY_MODIFIED
);
const onClickTag = (name: string) => {
setSelectedTag(name);
setFiles(listWorkflows().filter((n) => n.tags?.includes(name)));
};
const loadLatestWorkflows = () => {
const all = listFolderContent();
setFiles(sortFileItem(all, sortTypeRef.current));
setRefreshFolderStamp(Date.now());
};
const onSort = (type: ESortTypes) => {
setFiles(sortFileItem(files, type));
sortTypeRef.current = type;
window.localStorage.setItem(sortTypeLocalStorageKey, type);
};
const onDelete = useCallback((id: string) => {
deleteFlow(id);
loadLatestWorkflows();
}, []);
const handleDrop = (e: any) => {
workspace &&
draggingWorkflowID.current &&
insertWorkflowToCanvas3(workspace[draggingWorkflowID.current].json, [
e.canvasX,
e.canvasY,
]);
};
const handleClick = (e: any) => {
onclose();
};
useEffect(() => {
app.canvasEl.addEventListener("drop", handleDrop);
app.canvasEl.addEventListener("click", handleClick);
// Cleanup function to remove event listeners
return () => {
app.canvasEl.removeEventListener("drop", handleDrop);
app.canvasEl.removeEventListener("click", handleClick);
};
}, []);
useEffect(() => {
loadLatestWorkflows();
}, [curFlowID]);
useEffect(() => {
if (debaunceSearchValue === "") {
loadLatestWorkflows();
} else {
setFiles(
listWorkflows().filter((flow) =>
flow.name
.toLocaleLowerCase()
.includes(debaunceSearchValue.toLocaleLowerCase())
)
);
}
}, [debaunceSearchValue]);
const onSelect = useCallback((flowId: string, selected: boolean) => {
setSelectedKeys((preState) => {
const copyKeys = [...preState];
if (selected) {
copyKeys.push(flowId);
} else {
copyKeys.splice(copyKeys.indexOf(flowId), 1);
}
return copyKeys;
});
}, []);
const batchOperationCallback = (type: string, value: unknown) => {
switch (type) {
case "batchDelete":
loadLatestWorkflows();
setMultipleState(false);
setSelectedKeys([]);
return;
case "selectAll":
setSelectedKeys(value ? recentFlows.map((flow) => flow.id) : []);
return;
}
};
const onDraggingFile = (file: Workflow | Folder) => {
setDraggingFile(file);
if (!isFolder(file)) {
draggingWorkflowID.current = file.id;
}
};
const onUpdateSearchValue = (newValue: string) => {
setSearchValue(newValue);
};
const DRAWER_WIDTH = 440;
return (
<RecentFilesContext.Provider
value={{
onRefreshFilesList: loadLatestWorkflows,
draggingFile: draggingFile ?? undefined,
setDraggingFile: onDraggingFile,
isMultiSelecting: multipleState,
onMultiSelectFlow: onSelect,
multiSelectedFlowsID: selectedKeys,
onDeleteFlow: onDelete,
refreshFolderStamp: refreshFolderStamp,
setRefreshFolderStamp: setRefreshFolderStamp,
}}
>
<Box style={{ width: DRAWER_WIDTH }}>
<Card
width={DRAWER_WIDTH}
height={"100vh"}
pl={4}
pr={5}
pt={4}
gap={4}
position={"absolute"}
top={0}
left={0}
shadow={"xl"}
zIndex={1000}
// boxShadow={"rgba(200, 200, 200, 0.4) 1px 4px 8px 1px"}
>
<Flex alignItems={"center"} justifyContent={"space-between"}>
<HStack gap={4}>
<Text fontSize={20} fontWeight={600} mr={4}>
Workflows
</Text>
<ImportJsonFlows />
<Tooltip label="New workflow">
<IconButton
aria-label="New workflow"
size={"sm"}
icon={<IconPlus />}
variant={"outline"}
colorScheme="teal"
onClick={onClickNewFlow}
/>
</Tooltip>
</HStack>
<HStack alignItems={"center"}>
<RecentFilesDrawerMenu />
{/* <Button onClick={onclose}>CLOSE</Button> */}
</HStack>
</Flex>
<Flex direction="column" h="calc(100% - 64px)">
<HStack spacing={2} wrap={"wrap"} mb={0}>
{selectedTag != null && (
<IconButton
aria-label="Close"
size={"sm"}
icon={<IconX />}
onClick={() => {
setSelectedTag(undefined);
loadLatestWorkflows();
}}
/>
)}
{tagsTable
?.listAll()
.slice(0, showAllTags ? undefined : MAX_TAGS_TO_SHOW)
.map((tag) => (
<Button
variant="solid"
width={"auto"}
flexShrink={0}
size={"sm"}
borderRadius={15}
py={4}
onClick={() => onClickTag(tag.name)}
isActive={selectedTag === tag.name}
>
{tag.name}
</Button>
))}
{(tagsTable?.listAll().length ?? 0) > MAX_TAGS_TO_SHOW && (
<IconButton
aria-label="Show-all-tags"
size={"sm"}
icon={showAllTags ? <IconChevronUp /> : <IconChevronDown />}
onClick={() => setShowAllTags(!showAllTags)}
/>
)}
</HStack>
<HStack mb={2} mt={2} p={0} justifyContent="space-between">
<HStack>
{files.length > 0 ? (
<MultipleSelectionOperation
multipleState={multipleState}
changeMultipleState={(state) => {
setMultipleState(state);
!state && setSelectedKeys([]);
}}
selectedKeys={selectedKeys}
isSelectedAll={selectedKeys.length === recentFlows.length}
batchOperationCallback={batchOperationCallback}
/>
) : (
<Box />
)}
{!multipleState && (
<Tooltip hasArrow label="New folder" placement="bottom-start">
<IconButton
size={"sm"}
variant={"outline"}
icon={<IconFolderPlus size={21} />}
onClick={() => {
foldersTable?.create({
name: "New Folder",
});
loadLatestWorkflows();
}}
aria-label="new folder button"
/>
</Tooltip>
)}
</HStack>
<Menu closeOnSelect={true}>
<MenuButton
as={Button}
variant={"ghost"}
size="xs"
pr={0}
rightIcon={<IconChevronDown size="16" />}
>
<HStack>
<Text>Sort by:</Text>
<Text display="inline-block">{sortTypeRef.current}</Text>
</HStack>
</MenuButton>
<MenuList>
<MenuOptionGroup
value={sortTypeRef.current}
type="radio"
onChange={(type) => onSort(type as ESortTypes)}
>
{Object.values(ESortTypes).map((sortType, index) => (
<MenuItemOption key={index} value={sortType}>
{sortType}
</MenuItemOption>
))}
</MenuOptionGroup>
</MenuList>
</Menu>
</HStack>
<SeacrhInput
searchValue={searchValue}
onUpdateSearchValue={onUpdateSearchValue}
/>
<Flex overflowY={"auto"} overflowX={"hidden"} direction="column">
{files.map((n) => {
if (isFolder(n)) {
return <FilesListFolderItem folder={n} key={n.id} />;
}
return <WorkflowListItem key={n.id} workflow={n} />;
})}
</Flex>
</Flex>
</Card>
</Box>
</RecentFilesContext.Provider>
);
}