diff --git a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx index 8341791..dd65430 100644 --- a/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx +++ b/frontend/src/components/WorkspaceSidebar/ProjectStructure.tsx @@ -3,13 +3,15 @@ import { TreeNode } from 'react-treebeard'; import useProjectStore from '@/stores/useProjectStore'; import useCanvasStore from '@/stores/useCanvasStore'; import useTreeData from '@/hooks/useTreeData'; -import { Project, ImageResponse, ImageStatus } from '@/types'; +import useFolderQuery from '@/queries/folders/useFolderQuery'; +import useProjectCategoriesQuery from '@/queries/category/useProjectCategoriesQuery'; +import useImage from '@/hooks/useImage'; +import { Project, ImageResponse } from '@/types'; import WorkspaceDropdownMenu from '../WorkspaceDropdownMenu'; import AutoLabelButton from './AutoLabelButton'; -import useMoveImageQuery from '@/queries/images/useMoveImageQuery'; import { Folder, Image as ImageIcon, Minus, Loader, ArrowDownToLine, Send, CircleSlash, Check } from 'lucide-react'; import { Spinner } from '../ui/spinner'; - +import { ImageStatus } from '@/types'; import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; import { DndProvider, useDrag, useDrop } from 'react-dnd'; @@ -27,19 +29,52 @@ const ItemTypes = { }; export default function ProjectStructure({ project }: { project: Project }) { - const { setProject } = useProjectStore(); - const { setImage } = useCanvasStore(); - const { treeData, fetchNodeData, initializeTree, setTreeData, isLoading } = useTreeData(project.id.toString(), 0); - const [cursor, setCursor] = useState(null); - const moveImageMutation = useMoveImageQuery(); + const { setProject, setCategories } = useProjectStore(); + const { image: selectedImage, setImage } = useCanvasStore(); + const { treeData, fetchNodeData, setTreeData } = useTreeData(project.id.toString()); + const { data: categories } = useProjectCategoriesQuery(project.id); + const { data: rootFolder, isLoading, refetch } = useFolderQuery(project.id.toString(), 0); + + const { data: updatedImage } = useImage(project.id, rootFolder?.id || 0, selectedImage?.id || 0); const containerRef = useRef(null); const [containerHeight, setContainerHeight] = useState(400); + useEffect(() => { + setCategories(categories); + }, [categories, setCategories]); + useEffect(() => { setProject(project); - initializeTree(); - }, [project, setProject, initializeTree]); + }, [project, setProject]); + + useEffect(() => { + if (rootFolder) { + const childFolders: TreeNode[] = + rootFolder.children?.map((child) => ({ + id: child.id.toString(), + name: child.title, + children: [], + })) || []; + + const images: TreeNode[] = + rootFolder.images?.map((image: ImageResponse) => ({ + id: image.id.toString(), + name: image.imageTitle, + imageData: image, + children: [], + })) || []; + + const rootNode: TreeNode = { + id: rootFolder.id.toString(), + name: rootFolder.title, + children: [...childFolders, ...images], + toggled: true, + }; + + setTreeData(rootNode); + } + }, [rootFolder, setTreeData]); useEffect(() => { if (containerRef.current) { @@ -47,17 +82,28 @@ export default function ProjectStructure({ project }: { project: Project }) { } }, [containerRef, treeData, isLoading]); + useEffect(() => { + if (updatedImage?.data) { + const updateNodeStatus = (currentNode: TreeNode): TreeNode => { + if (currentNode.imageData?.id === updatedImage.data.id) { + return { ...currentNode, imageData: { ...currentNode.imageData, status: updatedImage.data.status } }; + } + if (currentNode.children) { + return { + ...currentNode, + children: currentNode.children.map(updateNodeStatus), + }; + } + return currentNode; + }; + + setTreeData((prevData) => prevData && updateNodeStatus(prevData)); + } + }, [updatedImage, setTreeData]); + const onToggle = useCallback( async (node: TreeNode, toggled: boolean) => { - if (cursor) { - cursor.active = false; - } - node.active = true; - setCursor(node); - - if (node.imageData) { - setImage(node.imageData as ImageResponse); - } else { + if (!node.imageData) { if (toggled && (!node.children || node.children.length === 0)) { await fetchNodeData(node); } @@ -78,56 +124,68 @@ export default function ProjectStructure({ project }: { project: Project }) { setTreeData((prevData) => prevData && updateNode(prevData)); } }, - [cursor, fetchNodeData, setImage, setTreeData] + [fetchNodeData, setTreeData] ); - const renderStatusIcon = useCallback((status: ImageStatus) => { - switch (status) { - case 'PENDING': - return ( - - ); - case 'IN_PROGRESS': - return ( - - ); - case 'SAVE': - return ( - - ); - case 'REVIEW_REQUEST': - return ( - - ); - case 'REVIEW_REJECT': - return ( - - ); - case 'COMPLETED': - default: - return ( - - ); - } - }, []); + const handleImageClick = useCallback( + (image: ImageResponse) => { + setImage(image); + }, + [setImage] + ); + + const renderStatusIcon = (status: ImageStatus) => { + const iconProps = { size: 12, className: 'shrink-0' }; + const iconColor = { + PENDING: 'stroke-gray-400', + IN_PROGRESS: 'animate-spin stroke-yellow-400', + SAVE: 'stroke-gray-400', + REVIEW_REQUEST: 'stroke-blue-400', + REVIEW_REJECT: 'stroke-red-400', + COMPLETED: 'stroke-green-400', + }; + + const iconMapping = { + PENDING: ( + + ), + IN_PROGRESS: ( + + ), + SAVE: ( + + ), + REVIEW_REQUEST: ( + + ), + REVIEW_REJECT: ( + + ), + COMPLETED: ( + + ), + }; + + return iconMapping[status] || null; + }; const flattenTree = useCallback((nodes: TreeNode[], depth: number = 0, parent?: FlatNode): FlatNode[] => { let flatList: FlatNode[] = []; @@ -183,23 +241,8 @@ export default function ProjectStructure({ project }: { project: Project }) { })(treeData!); setTreeData(updatedTreeData); - - if (dragItem.imageData) { - const moveFolderId = Number(hoverItem.parent?.id) || 0; - const folderId = Number(dragItem.parent?.id) || 0; - const projectId = Number(project.id); - - moveImageMutation.mutate({ - projectId, - folderId, - imageId: dragItem.imageData.id, - moveRequest: { - moveFolderId, - }, - }); - } }, - [treeData, setTreeData, moveImageMutation, project.id] + [treeData, setTreeData] ); const Row = ({ index, style, data }: ListChildComponentProps) => { @@ -240,8 +283,15 @@ export default function ProjectStructure({ project }: { project: Project }) { alignItems: 'center', paddingLeft: `${node.depth * 20}px`, cursor: 'pointer', + backgroundColor: node.imageData && selectedImage?.id === node.imageData.id ? '#e5e7eb' : 'transparent', + }} + onClick={() => { + if (node.imageData) { + handleImageClick(node.imageData as ImageResponse); + } else { + onToggle(node, !node.toggled); + } }} - onClick={() => onToggle(node, !node.toggled)} >
{!node.imageData ? ( @@ -277,7 +327,7 @@ export default function ProjectStructure({ project }: { project: Project }) { {}} + onRefetch={refetch} /> {isLoading ? ( diff --git a/frontend/src/hooks/useTreeData.ts b/frontend/src/hooks/useTreeData.ts index 84778fb..43fa2f5 100644 --- a/frontend/src/hooks/useTreeData.ts +++ b/frontend/src/hooks/useTreeData.ts @@ -3,9 +3,8 @@ import { TreeNode } from 'react-treebeard'; import { getFolder } from '@/api/folderApi'; import { ImageResponse, ChildFolder } from '@/types'; -export default function useTreeData(projectId: string, initialFolderId: number) { +export default function useTreeData(projectId: string) { const [treeData, setTreeData] = useState(null); - const [isLoading, setIsLoading] = useState(false); const fetchNodeData = useCallback( async (node: TreeNode) => { @@ -52,51 +51,14 @@ export default function useTreeData(projectId: string, initialFolderId: number) }); } catch (error) { node.loading = false; - console.error(`Error fetching data for node ${node.id}:`, error); } }, [projectId] ); - const initializeTree = useCallback(async () => { - setIsLoading(true); - try { - const rootFolder = await getFolder(projectId, initialFolderId); - const childFolders: TreeNode[] = - rootFolder.children?.map((child: ChildFolder) => ({ - id: child.id.toString(), - name: child.title, - children: [], - })) || []; - - const images: TreeNode[] = - rootFolder.images?.map((image: ImageResponse) => ({ - id: image.id.toString(), - name: image.imageTitle, - imageData: image, - children: [], - })) || []; - - const rootNode: TreeNode = { - id: rootFolder.id.toString(), - name: rootFolder.title, - children: [...childFolders, ...images], - toggled: true, - }; - - setTreeData(rootNode); - } catch (error) { - console.error('Error initializing tree data:', error); - } finally { - setIsLoading(false); - } - }, [projectId, initialFolderId]); - return { treeData, fetchNodeData, - initializeTree, setTreeData, - isLoading, }; }