Refactor: 사이드바 리팩토링
This commit is contained in:
parent
f628a84477
commit
6b4946b273
575
frontend/package-lock.json
generated
575
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,12 +36,14 @@
|
|||||||
"konva": "^9.3.14",
|
"konva": "^9.3.14",
|
||||||
"lucide-react": "^0.436.0",
|
"lucide-react": "^0.436.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
"react-konva": "^18.2.10",
|
"react-konva": "^18.2.10",
|
||||||
"react-resizable-panels": "^2.1.1",
|
"react-resizable-panels": "^2.1.1",
|
||||||
"react-router-dom": "^6.26.1",
|
"react-router-dom": "^6.26.1",
|
||||||
"react-slick": "^0.30.2",
|
"react-slick": "^0.30.2",
|
||||||
|
"react-treebeard": "^3.2.4",
|
||||||
"react-virtualized-auto-sizer": "^1.0.24",
|
"react-virtualized-auto-sizer": "^1.0.24",
|
||||||
"react-window": "^1.8.10",
|
"react-window": "^1.8.10",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
@ -66,6 +68,7 @@
|
|||||||
"@storybook/test": "^8.2.9",
|
"@storybook/test": "^8.2.9",
|
||||||
"@types/node": "^22.5.0",
|
"@types/node": "^22.5.0",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||||
"@typescript-eslint/parser": "^7.13.1",
|
"@typescript-eslint/parser": "^7.13.1",
|
||||||
|
@ -8,10 +8,8 @@ export async function getImage(imageId: number, memberId: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function moveImage(imageId: number, memberId: number, moveRequest: ImageMoveRequest) {
|
export async function moveImage(projectId: number, folderId: number, imageId: number, moveRequest: ImageMoveRequest) {
|
||||||
return api.put(`/images/${imageId}`, moveRequest, {
|
return api.put(`/projects/${projectId}/folders/${folderId}/images/${imageId}`, moveRequest);
|
||||||
params: { memberId },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteImage(imageId: number, memberId: number) {
|
export async function deleteImage(imageId: number, memberId: number) {
|
||||||
@ -23,7 +21,7 @@ export async function deleteImage(imageId: number, memberId: number) {
|
|||||||
export async function changeImageStatus(
|
export async function changeImageStatus(
|
||||||
imageId: number,
|
imageId: number,
|
||||||
memberId: number,
|
memberId: number,
|
||||||
statusChangeRequest: ImageStatusChangeRequest,
|
statusChangeRequest: ImageStatusChangeRequest
|
||||||
) {
|
) {
|
||||||
return api
|
return api
|
||||||
.put(`/images/${imageId}/status`, statusChangeRequest, {
|
.put(`/images/${imageId}/status`, statusChangeRequest, {
|
||||||
@ -37,7 +35,7 @@ export async function uploadImageFile(
|
|||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
files: File[],
|
files: File[],
|
||||||
processCallback: (progress: number) => void,
|
processCallback: (progress: number) => void
|
||||||
) {
|
) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
@ -62,7 +60,7 @@ export async function uploadImageFolderFile(
|
|||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
files: File[],
|
files: File[],
|
||||||
processCallback: (progress: number) => void,
|
processCallback: (progress: number) => void
|
||||||
) {
|
) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
@ -87,7 +85,7 @@ export async function uploadImageFolder(
|
|||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
files: File[],
|
files: File[],
|
||||||
processCallback: (progress: number) => void,
|
processCallback: (progress: number) => void
|
||||||
) {
|
) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
@ -112,7 +110,7 @@ export async function uploadImageZip(
|
|||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
file: File,
|
file: File,
|
||||||
processCallback: (progress: number) => void,
|
processCallback: (progress: number) => void
|
||||||
) {
|
) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('folderZip', file);
|
formData.append('folderZip', file);
|
||||||
@ -135,7 +133,7 @@ export async function uploadImagePresigned(
|
|||||||
projectId: number,
|
projectId: number,
|
||||||
folderId: number,
|
folderId: number,
|
||||||
files: File[],
|
files: File[],
|
||||||
processCallback: (index: number) => void,
|
processCallback: (index: number) => void
|
||||||
) {
|
) {
|
||||||
// 업로드 시작 시간 기록
|
// 업로드 시작 시간 기록
|
||||||
const startTime = new Date().getTime();
|
const startTime = new Date().getTime();
|
||||||
@ -152,11 +150,10 @@ export async function uploadImagePresigned(
|
|||||||
imageMetaList,
|
imageMetaList,
|
||||||
{
|
{
|
||||||
params: { memberId },
|
params: { memberId },
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 각 파일을 presigned URL에 맞춰서 업로드 (axios 직접 사용)
|
||||||
// 각 파일을 presigned URL에 맞춰서 업로드 (axios 직접 사용)
|
|
||||||
for (const presignedUrlInfo of presignedUrlList) {
|
for (const presignedUrlInfo of presignedUrlList) {
|
||||||
const file = files[presignedUrlInfo.id];
|
const file = files[presignedUrlInfo.id];
|
||||||
|
|
||||||
|
@ -1,66 +1,263 @@
|
|||||||
import { Project } from '@/types';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import ProjectFileItem from './ProjectFileItem';
|
import { TreeNode } from 'react-treebeard';
|
||||||
import ProjectDirectoryItem from './ProjectDirectoryItem';
|
|
||||||
import useFolderQuery from '@/queries/folders/useFolderQuery';
|
|
||||||
import useCanvasStore from '@/stores/useCanvasStore';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import WorkspaceDropdownMenu from '../WorkspaceDropdownMenu';
|
|
||||||
import useProjectStore from '@/stores/useProjectStore';
|
import useProjectStore from '@/stores/useProjectStore';
|
||||||
import useProjectCategoriesQuery from '@/queries/category/useProjectCategoriesQuery';
|
import useCanvasStore from '@/stores/useCanvasStore';
|
||||||
|
import useTreeData from '@/hooks/useTreeData';
|
||||||
|
import { Project, ImageResponse, ImageStatus } from '@/types';
|
||||||
|
import WorkspaceDropdownMenu from '../WorkspaceDropdownMenu';
|
||||||
import AutoLabelButton from './AutoLabelButton';
|
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 { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
export default function ProjectStructure({ project }: { project: Project }) {
|
export default function ProjectStructure({ project }: { project: Project }) {
|
||||||
const { setProject, setCategories } = useProjectStore();
|
const { setProject } = useProjectStore();
|
||||||
const { data: categories } = useProjectCategoriesQuery(project.id);
|
const { setImage } = useCanvasStore();
|
||||||
const image = useCanvasStore((state) => state.image);
|
const { treeData, fetchNodeData, initializeTree, setTreeData, isLoading } = useTreeData(project.id.toString(), 0);
|
||||||
const { data: folderData, refetch } = useFolderQuery(project.id.toString(), 0);
|
const [cursor, setCursor] = useState<TreeNode | null>(null);
|
||||||
|
const moveImageMutation = useMoveImageQuery();
|
||||||
useEffect(() => {
|
|
||||||
setCategories(categories);
|
|
||||||
}, [categories, setCategories]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProject(project);
|
setProject(project);
|
||||||
}, [project, setProject]);
|
initializeTree();
|
||||||
|
}, [project, setProject, initializeTree]);
|
||||||
|
|
||||||
|
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 (toggled && (!node.children || node.children.length === 0)) {
|
||||||
|
await fetchNodeData(node);
|
||||||
|
}
|
||||||
|
node.toggled = toggled;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTreeData((prevData) => ({ ...prevData! }));
|
||||||
|
},
|
||||||
|
[cursor, fetchNodeData, setImage, setTreeData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderStatusIcon = (status: ImageStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'PENDING':
|
||||||
|
return (
|
||||||
|
<Minus
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-gray-400"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'IN_PROGRESS':
|
||||||
|
return (
|
||||||
|
<Loader
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 animate-spin stroke-yellow-400"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'SAVE':
|
||||||
|
return (
|
||||||
|
<ArrowDownToLine
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-gray-400"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'REVIEW_REQUEST':
|
||||||
|
return (
|
||||||
|
<Send
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-blue-400"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'REVIEW_REJECT':
|
||||||
|
return (
|
||||||
|
<CircleSlash
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-red-400"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'COMPLETED':
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<Check
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 stroke-green-400"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTree = useCallback(
|
||||||
|
(nodes: TreeNode[], _parentId: string, level: number = 0) => {
|
||||||
|
return nodes.map((node, index) => (
|
||||||
|
<Draggable
|
||||||
|
draggableId={node.id!}
|
||||||
|
index={index}
|
||||||
|
key={node.id}
|
||||||
|
>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
style={{
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: `${level * 20}px`,
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() => onToggle(node, !node.toggled)}
|
||||||
|
>
|
||||||
|
<div style={{ marginRight: '5px' }}>
|
||||||
|
{!node.imageData ? (
|
||||||
|
<Folder
|
||||||
|
size={16}
|
||||||
|
className="stroke-gray-500"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ImageIcon
|
||||||
|
size={16}
|
||||||
|
className="stroke-gray-500"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span style={{ color: '#4a4a4a', flexGrow: 1 }}>{node.name}</span>
|
||||||
|
{node.imageData && <div style={{ marginLeft: '10px' }}>{renderStatusIcon(node.imageData.status)}</div>}
|
||||||
|
</div>
|
||||||
|
{node.toggled && node.children && node.children.length > 0 && (
|
||||||
|
<Droppable
|
||||||
|
droppableId={node.id!}
|
||||||
|
type="TREE"
|
||||||
|
>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.droppableProps}
|
||||||
|
style={{ paddingLeft: `${level * 20}px` }}
|
||||||
|
>
|
||||||
|
{renderTree(node.children!, node.id!, level + 1)}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
));
|
||||||
|
},
|
||||||
|
[onToggle]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onDragEnd = useCallback(
|
||||||
|
(result: DropResult) => {
|
||||||
|
if (!result.destination || !treeData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceDroppableId = result.source.droppableId;
|
||||||
|
const destinationDroppableId = result.destination.droppableId;
|
||||||
|
const sourceIndex = result.source.index;
|
||||||
|
const destinationIndex = result.destination.index;
|
||||||
|
|
||||||
|
const findNodeById = (nodes: TreeNode[], id: string): TreeNode | null => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.id === id) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
if (node.children) {
|
||||||
|
const found = findNodeById(node.children, id);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceParentNode = sourceDroppableId === 'root' ? treeData : findNodeById([treeData], sourceDroppableId);
|
||||||
|
const destinationParentNode =
|
||||||
|
destinationDroppableId === 'root' ? treeData : findNodeById([treeData], destinationDroppableId);
|
||||||
|
|
||||||
|
if (!sourceParentNode || !destinationParentNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [movedItem] = sourceParentNode.children!.splice(sourceIndex, 1);
|
||||||
|
|
||||||
|
destinationParentNode.children!.splice(destinationIndex, 0, movedItem);
|
||||||
|
|
||||||
|
setTreeData({ ...treeData });
|
||||||
|
|
||||||
|
if (movedItem && movedItem.imageData) {
|
||||||
|
const moveFolderId = Number(destinationParentNode.id) || 0;
|
||||||
|
const folderId = Number(sourceParentNode.id) || 0;
|
||||||
|
const projectId = Number(project.id);
|
||||||
|
|
||||||
|
moveImageMutation.mutate({
|
||||||
|
projectId,
|
||||||
|
folderId,
|
||||||
|
imageId: movedItem.imageData.id,
|
||||||
|
moveRequest: {
|
||||||
|
moveFolderId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[treeData, setTreeData, moveImageMutation, project.id]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 grow-0 flex-col">
|
<div className="box-border flex h-full min-h-0 flex-col bg-gray-50 p-2">
|
||||||
<div className="flex h-full flex-col overflow-hidden px-1 pb-2">
|
<div className="flex h-full flex-col gap-2 overflow-hidden px-1 pb-2">
|
||||||
<header className="flex w-full items-center gap-2 rounded p-1">
|
<header className="flex w-full items-center gap-2 rounded-md bg-white p-2 shadow-sm">
|
||||||
<div className="flex w-full min-w-0 items-center gap-1 pr-1">
|
<div className="flex w-full min-w-0 items-center gap-1 pr-1">
|
||||||
<h2 className="caption overflow-hidden text-ellipsis whitespace-nowrap text-gray-500">{project.type}</h2>
|
<h2 className="caption overflow-hidden text-ellipsis whitespace-nowrap text-gray-600">{project.type}</h2>
|
||||||
</div>
|
</div>
|
||||||
<WorkspaceDropdownMenu
|
<WorkspaceDropdownMenu
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
folderId={0}
|
folderId={0}
|
||||||
onRefetch={refetch}
|
onRefetch={() => {}}
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
{folderData.children.length === 0 && folderData.images.length === 0 ? (
|
{isLoading ? (
|
||||||
<div className="body-small flex h-full select-none items-center justify-center text-gray-400">
|
<div className="flex h-full items-center justify-center">
|
||||||
빈 프로젝트입니다.
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
|
) : !treeData ? (
|
||||||
|
<div className="body-small flex h-full select-none items-center justify-center text-gray-400">Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="caption flex flex-col overflow-y-auto">
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
{folderData.children.map((item) => (
|
<Droppable
|
||||||
<ProjectDirectoryItem
|
droppableId="root"
|
||||||
key={`${project.id}-${item.title}`}
|
type="TREE"
|
||||||
projectId={project.id}
|
>
|
||||||
item={item}
|
{(provided) => (
|
||||||
initialExpanded={true}
|
<div
|
||||||
/>
|
ref={provided.innerRef}
|
||||||
))}
|
{...provided.droppableProps}
|
||||||
{folderData.images.map((item) => (
|
className="flex-1 overflow-auto"
|
||||||
<ProjectFileItem
|
style={{ overflowX: 'hidden' }}
|
||||||
key={`${project.id}-${item.imageTitle}`}
|
>
|
||||||
item={item}
|
{renderTree(treeData.children!, 'root')}
|
||||||
selected={image?.id === item.id}
|
{provided.placeholder}
|
||||||
/>
|
</div>
|
||||||
))}
|
)}
|
||||||
</div>
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<AutoLabelButton projectId={project.id} />
|
<AutoLabelButton projectId={project.id} />
|
||||||
</div>
|
</div>
|
||||||
|
84
frontend/src/hooks/useTreeData.ts
Normal file
84
frontend/src/hooks/useTreeData.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { TreeNode } from 'react-treebeard';
|
||||||
|
import { getFolder } from '@/api/folderApi';
|
||||||
|
import { ImageResponse, ChildFolder } from '@/types';
|
||||||
|
|
||||||
|
export default function useTreeData(projectId: string, initialFolderId: number) {
|
||||||
|
const [treeData, setTreeData] = useState<TreeNode | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const fetchNodeData = useCallback(
|
||||||
|
async (node: TreeNode) => {
|
||||||
|
node.loading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const folder = await getFolder(projectId, Number(node.id));
|
||||||
|
const childFolders: TreeNode[] =
|
||||||
|
folder.children?.map((child: ChildFolder) => ({
|
||||||
|
id: child.id.toString(),
|
||||||
|
name: child.title,
|
||||||
|
children: [],
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
const images: TreeNode[] =
|
||||||
|
folder.images?.map((image: ImageResponse) => ({
|
||||||
|
id: image.id.toString(),
|
||||||
|
name: image.imageTitle,
|
||||||
|
imageData: image,
|
||||||
|
children: [],
|
||||||
|
})) || [];
|
||||||
|
|
||||||
|
node.children = [...childFolders, ...images];
|
||||||
|
node.loading = false;
|
||||||
|
node.toggled = true;
|
||||||
|
setTreeData((prevData) => ({ ...prevData! }));
|
||||||
|
} 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,
|
||||||
|
};
|
||||||
|
}
|
@ -3,8 +3,9 @@ import { moveImage } from '@/api/imageApi';
|
|||||||
import { ImageMoveRequest } from '@/types';
|
import { ImageMoveRequest } from '@/types';
|
||||||
|
|
||||||
interface MoveImageMutationVariables {
|
interface MoveImageMutationVariables {
|
||||||
|
projectId: number;
|
||||||
|
folderId: number;
|
||||||
imageId: number;
|
imageId: number;
|
||||||
memberId: number;
|
|
||||||
moveRequest: ImageMoveRequest;
|
moveRequest: ImageMoveRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,10 +13,12 @@ export default function useMoveImageQuery() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ imageId, memberId, moveRequest }: MoveImageMutationVariables) =>
|
mutationFn: ({ projectId, folderId, imageId, moveRequest }: MoveImageMutationVariables) =>
|
||||||
moveImage(imageId, memberId, moveRequest),
|
moveImage(projectId, folderId, imageId, moveRequest),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['image', variables.imageId] });
|
queryClient.invalidateQueries({ queryKey: ['image', variables.imageId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['project', variables.projectId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['folder', variables.folderId] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
64
frontend/src/types/react-treebeard.d.ts
vendored
Normal file
64
frontend/src/types/react-treebeard.d.ts
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
declare module 'react-treebeard' {
|
||||||
|
import React from 'react';
|
||||||
|
type TreeDecoratorTypes = 'Loading' | 'Toggle' | 'Header' | 'Container';
|
||||||
|
type CSS = React.CSSProperties;
|
||||||
|
|
||||||
|
export type TreeAnimations = object;
|
||||||
|
export interface TreeTheme {
|
||||||
|
tree: {
|
||||||
|
base: CSS;
|
||||||
|
node: {
|
||||||
|
base: CSS;
|
||||||
|
link: CSS;
|
||||||
|
activeLink: CSS;
|
||||||
|
toggle: {
|
||||||
|
base: CSS;
|
||||||
|
wrapper: CSS;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
arrow: CSS;
|
||||||
|
};
|
||||||
|
header: {
|
||||||
|
base: CSS;
|
||||||
|
connector: CSS;
|
||||||
|
title: CSS;
|
||||||
|
};
|
||||||
|
subtree: CSS;
|
||||||
|
loading: CSS;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export type TreeDecorators = { [T in TreeDecoratorTypes]: React.ElementType };
|
||||||
|
export interface TreeNode {
|
||||||
|
imageData?: ImageResponse;
|
||||||
|
/** The component key. If not defined, an auto - generated index is used. */
|
||||||
|
id?: string;
|
||||||
|
/** The name prop passed into the Header component. */
|
||||||
|
name: string;
|
||||||
|
/** The children attached to the node. This value populates the subtree at the specific node.Each child is built from the same basic data structure.
|
||||||
|
*
|
||||||
|
* Tip: Make this an empty array, if you want to asynchronously load a potential parent. */
|
||||||
|
children?: Array<TreeNode>;
|
||||||
|
/** Toggled flag. Sets the visibility of a node's children. It also sets the state for the toggle decorator. */
|
||||||
|
toggled?: boolean;
|
||||||
|
/** Active flag. If active, the node will be highlighted.The highlight is derived from the node.activeLink style object in the theme. */
|
||||||
|
active?: boolean;
|
||||||
|
/** Loading flag. It will populate the treeview with the loading component.Useful when asynchronously pulling the data into the treeview. */
|
||||||
|
loading?: boolean;
|
||||||
|
/** Attach specific decorators to a node. Provides the low level functionality to create visuals on a node-by-node basis. */
|
||||||
|
decorators?: TreeDecorators;
|
||||||
|
/** Attach specific animations to a node. Provides the low level functionality to create visuals on a node-by-node basis. */
|
||||||
|
animations?: TreeAnimations;
|
||||||
|
}
|
||||||
|
type TreebeardProps = {
|
||||||
|
data: TreeNode | Array<TreeNode>;
|
||||||
|
onToggle?: (node: TreeNode, toggled: boolean) => void;
|
||||||
|
style?: TreeTheme;
|
||||||
|
animations?: TreeAnimations | boolean;
|
||||||
|
decorators?: TreeDecorators;
|
||||||
|
};
|
||||||
|
export const Treebeard: React.ElementType<TreebeardProps>;
|
||||||
|
export const decorators: TreeDecorators;
|
||||||
|
export const animations: TreeAnimations;
|
||||||
|
export const theme: TreeTheme;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user