diff --git a/backend/migrations/20250804175803-add-cascade-delete-parent-folder.js b/backend/migrations/20250804175803-add-cascade-delete-parent-folder.js
new file mode 100644
index 0000000..608372d
--- /dev/null
+++ b/backend/migrations/20250804175803-add-cascade-delete-parent-folder.js
@@ -0,0 +1,22 @@
+'use strict';
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+ async up(queryInterface) {
+ await queryInterface.addConstraint('folders', {
+ fields: ['parentFolderId'],
+ type: 'foreign key',
+ name: 'fk_folders_parentFolderId',
+ references: {
+ table: 'folders',
+ field: 'id',
+ },
+ onDelete: 'CASCADE',
+ onUpdate: 'CASCADE',
+ });
+ },
+
+ async down(queryInterface) {
+ await queryInterface.removeConstraint('folders', 'fk_folders_parentFolderId');
+ },
+};
diff --git a/backend/models/folders.js b/backend/models/folders.js
index a0b3880..9cdd238 100644
--- a/backend/models/folders.js
+++ b/backend/models/folders.js
@@ -11,6 +11,11 @@ function defineFolder(sequelize, DataTypes) {
parentFolderId: {
type: DataTypes.INTEGER,
allowNull: true,
+ references: {
+ model: 'folder',
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
},
projectId: {
type: DataTypes.INTEGER,
@@ -25,6 +30,7 @@ function defineFolder(sequelize, DataTypes) {
Folder.associate = (models) => {
Folder.belongsTo(models.Project, { foreignKey: 'projectId', onDelete: 'CASCADE' });
+ Folder.belongsTo(models.folder, { foreignKey: 'parentFolderId', onDelete: 'CASCADE' });
Folder.hasMany(models.Case, { foreignKey: 'folderId' });
};
diff --git a/frontend/components/TreeItem.tsx b/frontend/components/TreeItem.tsx
new file mode 100644
index 0000000..2adc46c
--- /dev/null
+++ b/frontend/components/TreeItem.tsx
@@ -0,0 +1,46 @@
+import { cn } from '@heroui/react';
+import { ReactNode } from 'react';
+
+interface TreeItemProps {
+ style: React.CSSProperties;
+ isSelected: boolean | null;
+ onClick: () => void;
+ toggleButton?: ReactNode;
+ icon: ReactNode;
+ label: string;
+ actions?: ReactNode;
+ className?: string;
+}
+
+export default function TreeItem({
+ style,
+ isSelected,
+ onClick,
+ toggleButton,
+ icon,
+ label,
+ actions,
+ className,
+}: TreeItemProps) {
+ const baseClass = '';
+ const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700 dark:hover:bg-neutral-600 hover:bg-neutral-300`;
+
+ return (
+
+
+ {toggleButton ||
}
+ {icon}
+
{label}
+ {actions &&
{actions}
}
+
+
+ );
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 384eb58..1f64ca3 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -30,6 +30,7 @@
"postcss": "8.5.3",
"react": "^18.3.1",
"react-apexcharts": "^1.7.0",
+ "react-arborist": "^3.4.3",
"react-dom": "18.3.1",
"sharp": "^0.33.5",
"tailwind-variants": "^0.3.1",
@@ -9310,6 +9311,24 @@
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
+ "node_modules/@react-dnd/asap": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz",
+ "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==",
+ "license": "MIT"
+ },
+ "node_modules/@react-dnd/invariant": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz",
+ "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-dnd/shallowequal": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
+ "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==",
+ "license": "MIT"
+ },
"node_modules/@react-stately/flags": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.0.5.tgz",
@@ -10135,6 +10154,26 @@
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
},
+ "node_modules/dnd-core": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz",
+ "integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-dnd/asap": "^4.0.0",
+ "@react-dnd/invariant": "^2.0.0",
+ "redux": "^4.1.1"
+ }
+ },
+ "node_modules/dnd-core/node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -10158,6 +10197,12 @@
"node": ">=6"
}
},
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -10312,6 +10357,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
"node_modules/input-otp": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz",
@@ -10455,6 +10509,12 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/memoize-one": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
+ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
+ "license": "MIT"
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -10934,6 +10994,62 @@
"react": ">=0.13"
}
},
+ "node_modules/react-arborist": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.4.3.tgz",
+ "integrity": "sha512-yFnq1nIQhT2uJY4TZVz2tgAiBb9lxSyvF4vC3S8POCK8xLzjGIxVv3/4dmYquQJ7AHxaZZArRGHiHKsEewKdTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-dnd": "^14.0.3",
+ "react-dnd-html5-backend": "^14.0.3",
+ "react-window": "^1.8.11",
+ "redux": "^5.0.0",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.14",
+ "react-dom": ">= 16.14"
+ }
+ },
+ "node_modules/react-dnd": {
+ "version": "14.0.5",
+ "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz",
+ "integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-dnd/invariant": "^2.0.0",
+ "@react-dnd/shallowequal": "^2.0.0",
+ "dnd-core": "14.0.1",
+ "fast-deep-equal": "^3.1.3",
+ "hoist-non-react-statics": "^3.3.2"
+ },
+ "peerDependencies": {
+ "@types/hoist-non-react-statics": ">= 3.3.1",
+ "@types/node": ">= 12",
+ "@types/react": ">= 16",
+ "react": ">= 16.14"
+ },
+ "peerDependenciesMeta": {
+ "@types/hoist-non-react-statics": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-dnd-html5-backend": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz",
+ "integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==",
+ "license": "MIT",
+ "dependencies": {
+ "dnd-core": "14.0.1"
+ }
+ },
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -10967,6 +11083,23 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/react-window": {
+ "version": "1.8.11",
+ "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
+ "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0",
+ "memoize-one": ">=3.1.1 <6"
+ },
+ "engines": {
+ "node": ">8.0.0"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -10986,6 +11119,12 @@
"node": ">=8.10.0"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 9fa9d8c..f87a34e 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -31,6 +31,7 @@
"postcss": "8.5.3",
"react": "^18.3.1",
"react-apexcharts": "^1.7.0",
+ "react-arborist": "^3.4.3",
"react-dom": "18.3.1",
"sharp": "^0.33.5",
"tailwind-variants": "^0.3.1",
diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/FolderTree.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/FolderTree.tsx
new file mode 100644
index 0000000..2a3c995
--- /dev/null
+++ b/frontend/src/app/[locale]/projects/[projectId]/folders/FolderTree.tsx
@@ -0,0 +1,83 @@
+import { Button } from '@heroui/react';
+import { ChevronDown, ChevronRight, Folder, Plus } from 'lucide-react';
+import { NodeApi } from 'react-arborist';
+import { useContext } from 'react';
+import FolderEditMenu from './FolderEditMenu';
+
+import { FolderType, FoldersMessages, TreeNodeData } from '@/types/folder';
+import { useRouter } from '@/src/i18n/routing';
+import { TokenContext } from '@/utils/TokenProvider';
+import TreeItem from '@/components/TreeItem';
+
+interface FolderItemProps {
+ node: NodeApi;
+ style: React.CSSProperties;
+ projectId: string;
+ selectedFolder: FolderType | null;
+ locale: string;
+ messages: FoldersMessages;
+ openDialogForCreate: (folderId: number | null) => void;
+ onEditClick: (folder: FolderType) => void;
+ onDeleteClick: (folderId: number) => void;
+}
+
+export default function FolderItem({
+ node,
+ style,
+ projectId,
+ selectedFolder,
+ locale,
+ messages,
+ openDialogForCreate,
+ onEditClick,
+ onDeleteClick,
+}: FolderItemProps) {
+ const router = useRouter();
+ const context = useContext(TokenContext);
+ const isSelected = selectedFolder && node.data.folderData.id === selectedFolder.id;
+
+ const toggleButton =
+ node.data.children && node.data.children.length > 0 ? (
+
+ ) : null;
+
+ const actions = (
+ <>
+
+
+ >
+ );
+
+ return (
+ router.push(`/projects/${projectId}/folders/${node.data.folderData.id}/cases`, { locale })}
+ toggleButton={toggleButton}
+ icon={}
+ label={node.data.name}
+ actions={actions}
+ />
+ );
+}
diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx
index c0ba0b9..de9e3a3 100644
--- a/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx
+++ b/frontend/src/app/[locale]/projects/[projectId]/folders/FoldersPane.tsx
@@ -1,16 +1,18 @@
'use client';
import { useState, useEffect, useContext } from 'react';
-import { Button, Listbox, ListboxItem } from '@heroui/react';
-import { Folder, Plus } from 'lucide-react';
+import { Button } from '@heroui/react';
+import { Plus } from 'lucide-react';
+import { Tree } from 'react-arborist';
import FolderDialog from './FolderDialog';
-import FolderEditMenu from './FolderEditMenu';
import { fetchFolders, createFolder, updateFolder, deleteFolder } from './foldersControl';
+import FolderItem from './FolderTree';
import { usePathname, useRouter } from '@/src/i18n/routing';
import { TokenContext } from '@/utils/TokenProvider';
import useGetCurrentIds from '@/utils/useGetCurrentIds';
import DeleteConfirmDialog from '@/components/DeleteConfirmDialog';
-import { FolderType, FoldersMessages } from '@/types/folder';
+import { FolderType, FoldersMessages, TreeNodeData } from '@/types/folder';
import { logError } from '@/utils/errorHandler';
+import { buildFolderTree } from '@/utils/buildFolderTree';
type Props = {
projectId: string;
@@ -22,11 +24,12 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
const router = useRouter();
const pathname = usePathname();
const context = useContext(TokenContext);
- const [folders, setFolders] = useState([]);
+ const [treeData, setTreeData] = useState([]);
const [selectedFolder, setSelectedFolder] = useState(null);
const { folderId } = useGetCurrentIds();
const [isFolderDialogOpen, setIsFolderDialogOpen] = useState(false);
const [editingFolder, setEditingFolder] = useState(null);
+ const [parentFolderId, setParentFolderId] = useState(null);
useEffect(() => {
async function fetchDataEffect() {
@@ -34,21 +37,20 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
return;
}
try {
- const folders: FolderType[] = await fetchFolders(context.token.access_token, Number(projectId));
- setFolders(folders);
+ const fetchedFolders: FolderType[] = await fetchFolders(context.token.access_token, Number(projectId));
+ const tree = buildFolderTree(fetchedFolders);
+ setTreeData(tree);
- // no folder on project
- if (folders.length === 0) {
+ if (tree.length === 0) {
return;
}
- const selectedFolderFromUrl = folders.find((folder) => folder.id === folderId);
+ const selectedFolderFromUrl = fetchedFolders.find((folder) => folder.id === folderId);
setSelectedFolder(selectedFolderFromUrl ? selectedFolderFromUrl : null);
- // Redirect to the smallest folder ID page if the path is "projects/[projectId]/folders
if (pathname === `/projects/${projectId}/folders`) {
- const smallestFolderId = Math.min(...folders.map((folder) => folder.id));
- router.push(`/projects/${projectId}/folders/${smallestFolderId}/cases`, { locale: locale });
+ const smallestFolderId = Math.min(...fetchedFolders.map((folder) => folder.id));
+ router.push(`/projects/${projectId}/folders/${smallestFolderId}/cases`, { locale });
}
} catch (error: unknown) {
logError('Error fetching folders:', error);
@@ -58,7 +60,8 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
fetchDataEffect();
}, [context, folderId, locale, pathname, projectId, router]);
- const openDialogForCreate = () => {
+ const openDialogForCreate = (folderId: number | null = null) => {
+ setParentFolderId(folderId);
setIsFolderDialogOpen(true);
setEditingFolder(null);
};
@@ -66,35 +69,30 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
const closeDialog = () => {
setIsFolderDialogOpen(false);
setEditingFolder(null);
+ setParentFolderId(null);
};
const onSubmit = async (name: string, detail: string) => {
if (editingFolder) {
- const updatedProject = await updateFolder(
- context.token.access_token,
- editingFolder.id,
- name,
- detail,
- projectId,
- null
- );
- const updatedProjects = folders.map((project) => (project.id === updatedProject.id ? updatedProject : project));
- setFolders(updatedProjects);
+ await updateFolder(context.token.access_token, editingFolder.id, name, detail, projectId, parentFolderId);
} else {
- const newProject = await createFolder(context.token.access_token, name, detail, projectId, null);
- setFolders([...folders, newProject]);
+ await createFolder(context.token.access_token, name, detail, projectId, parentFolderId);
}
+ const fetchedFolders: FolderType[] = await fetchFolders(context.token.access_token, Number(projectId));
+ const tree = buildFolderTree(fetchedFolders);
+ setTreeData(tree);
closeDialog();
};
const onEditClick = (folder: FolderType) => {
setEditingFolder(folder);
+ setParentFolderId(folder.parentFolderId);
setIsFolderDialogOpen(true);
};
- // Delete confirm dialog
const [isDeleteConfirmDialogOpen, setIsDeleteConfirmDialogOpen] = useState(false);
const [deleteFolderId, setDeleteFolderId] = useState(null);
+
const closeDeleteConfirmDialog = () => {
setIsDeleteConfirmDialogOpen(false);
setDeleteFolderId(null);
@@ -108,48 +106,57 @@ export default function FoldersPane({ projectId, messages, locale }: Props) {
const onConfirm = async () => {
if (deleteFolderId) {
await deleteFolder(context.token.access_token, deleteFolderId);
- router.push(`/projects/${projectId}/folders`, { locale: locale });
+ const fetchedFolders: FolderType[] = await fetchFolders(context.token.access_token, Number(projectId));
+ const tree = buildFolderTree(fetchedFolders);
+ setTreeData(tree);
+ router.push(`/projects/${projectId}/folders`, { locale });
closeDeleteConfirmDialog();
}
};
- const baseClass = '';
- const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
-
return (
<>
-
+
}
size="sm"
variant="bordered"
className="m-2"
isDisabled={!context.isProjectDeveloper(Number(projectId))}
- onPress={openDialogForCreate}
+ onPress={() => openDialogForCreate()}
>
{messages.newFolder}
-
- {folders.map((folder, index) => (
- router.push(`/projects/${projectId}/folders/${folder.id}/cases`, { locale: locale })}
- startContent={}
- className={selectedFolder && folder.id === selectedFolder.id ? selectedClass : baseClass}
- endContent={
-
- }
- >
- {folder.name}
-
- ))}
-
+
+ {treeData.length > 0 && (
+
+ {(props) => (
+
+ )}
+
+ )}
(defaultTestRun);
- const [folders, setFolders] = useState([]);
+ const [treeData, setTreeData] = useState([]);
const [runStatusCounts, setRunStatusCounts] = useState([]);
const [selectedKeys, setSelectedKeys] = useState(new Set([]));
- const [selectedFolder, setSelectedFolder] = useState(null);
+ const [selectedFolder, setSelectedFolder] = useState(null);
const [testCases, setTestCases] = useState([]);
const [filteredTestCases, setFilteredTestCases] = useState([]);
const [isNameInvalid] = useState(false);
@@ -130,7 +132,8 @@ export default function RunEditor({
try {
await fetchRunAndStatusCount();
const foldersData = await fetchFolders(tokenContext.token.access_token, Number(projectId));
- setFolders(foldersData);
+ const tree = buildFolderTree(foldersData);
+ setTreeData(tree);
setSelectedFolder(foldersData[0]);
initTestCases();
} catch (error: unknown) {
@@ -146,7 +149,7 @@ export default function RunEditor({
function onFilter() {
if (selectedFolder && selectedFolder.id) {
try {
- const filteredData = testCases.filter((testCase) => testCase.folderId === selectedFolder.id);
+ const filteredData = testCases.filter((testCase) => testCase.folderId.toString() === selectedFolder.id);
setFilteredTestCases(filteredData);
} catch (error: unknown) {
logError('Error filtering test cases', error);
@@ -198,9 +201,6 @@ export default function RunEditor({
setIsDirty(false);
};
- const baseClass = '';
- const selectedClass = `${baseClass} bg-neutral-200 dark:bg-neutral-700`;
-
const handleExportTypeChange = (keys: Selection) => {
setExportType(new Set(Array.from(keys as Set)));
};
@@ -386,21 +386,49 @@ export default function RunEditor({
-
- {folders.map((folder, index) => (
- {
- setSelectedKeys(new Set([])); // reset selection
- setSelectedFolder(folder);
+
+ {({ node, style }: { node: NodeApi; style: React.CSSProperties }) => (
+ {
+ setSelectedKeys(new Set([]));
+ setSelectedFolder(node.data);
}}
- startContent={}
- className={selectedFolder && folder.id === selectedFolder.id ? selectedClass : baseClass}
- >
- {folder.name}
-
- ))}
-
+ toggleButton={
+ node.data.children && node.data.children.length > 0 ? (
+
+ ) : null
+ }
+ icon={}
+ label={node.data.name}
+ />
+ )}
+
();
+
+ folders.forEach((folder) => {
+ folderMap.set(folder.id, {
+ id: folder.id.toString(),
+ name: folder.name,
+ detail: folder.detail,
+ parentFolderId: folder.parentFolderId,
+ projectId: folder.projectId,
+ folderData: folder,
+ children: [],
+ });
+ });
+
+ const tree: TreeNodeData[] = [];
+
+ folders.forEach((folder) => {
+ const currentNode = folderMap.get(folder.id);
+
+ if (!currentNode) return;
+
+ if (folder.parentFolderId === null) {
+ tree.push(currentNode);
+ } else {
+ const parent = folderMap.get(folder.parentFolderId);
+ if (parent && parent.children) {
+ parent.children.push(currentNode);
+ }
+ }
+ });
+
+ return tree;
+}