Skip to content

Commit dfe8a6a

Browse files
committed
feat: add saved output panel to show output directory
1 parent 4590fea commit dfe8a6a

15 files changed

Lines changed: 506 additions & 68 deletions

File tree

backend/migrations/20260127203948-create-projects.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ module.exports = {
2424
type: Sequelize.STRING(200),
2525
allowNull: false,
2626
},
27+
public_id: {
28+
type: Sequelize.STRING(12),
29+
allowNull: false,
30+
unique: true,
31+
defaultValue: "",
32+
},
2733
description: {
2834
type: Sequelize.TEXT,
2935
allowNull: true,

backend/package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"dotenv": "^17.2.3",
3030
"express": "^5.1.0",
3131
"jsonwebtoken": "^9.0.2",
32+
"nanoid": "^3.3.11",
3233
"nodemailer": "^7.0.11",
3334
"passport": "^0.7.0",
3435
"passport-google-oauth20": "^2.0.0",

backend/src/controllers/projectController.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { Project, User } = require("../models");
2+
const { nanoid } = require("nanoid");
23

34
// Create a new organizer project
45
const createProject = async (req, res) => {
@@ -8,6 +9,7 @@ const createProject = async (req, res) => {
89

910
const project = await Project.create({
1011
user_id: user.id,
12+
public_id: nanoid(12),
1113
name: name || `Dataset Project ${new Date().toLocaleDateString()}`,
1214
description: description || "don't have description yet",
1315
extractor_state: {
@@ -40,6 +42,7 @@ const getUserProjects = async (req, res) => {
4042
order: [["updated_at", "DESC"]],
4143
attributes: [
4244
"id",
45+
"public_id", // ← ADD
4346
"name",
4447
"description",
4548
"created_at",
@@ -53,6 +56,7 @@ const getUserProjects = async (req, res) => {
5356
const state = project.extractor_state || { files: [] };
5457
return {
5558
id: project.id,
59+
public_id: project.public_id, // ← ADD
5660
name: project.name,
5761
description: project.description,
5862
created_at: project.created_at,
@@ -82,7 +86,8 @@ const getProject = async (req, res) => {
8286

8387
const project = await Project.findOne({
8488
where: {
85-
id: projectId,
89+
// id: projectId,
90+
public_id: projectId,
8691
user_id: user.id,
8792
},
8893
});
@@ -112,7 +117,8 @@ const updateProject = async (req, res) => {
112117

113118
const project = await Project.findOne({
114119
where: {
115-
id: projectId,
120+
// id: projectId,
121+
public_id: projectId,
116122
user_id: user.id,
117123
},
118124
});
@@ -154,7 +160,8 @@ const deleteProject = async (req, res) => {
154160

155161
const project = await Project.findOne({
156162
where: {
157-
id: projectId,
163+
// id: projectId,
164+
public_id: projectId,
158165
user_id: user.id,
159166
},
160167
});

backend/src/models/Project.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const { DataTypes, Model } = require("sequelize");
22
const { sequelize } = require("../config/database");
3+
const { nanoid } = require("nanoid");
4+
console.log("nanoid test:", nanoid(12));
35

46
class Project extends Model {}
57

@@ -22,6 +24,11 @@ Project.init(
2224
type: DataTypes.STRING(200),
2325
allowNull: false,
2426
},
27+
public_id: {
28+
type: DataTypes.STRING(12),
29+
allowNull: false,
30+
unique: true,
31+
},
2532
description: {
2633
type: DataTypes.TEXT,
2734
allowNull: true,

backend/src/server.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ app.use(
3333
})
3434
);
3535

36-
app.use(express.json());
37-
app.use(express.urlencoded({ extended: true }));
36+
app.use(express.json({ limit: "50mb" }));
37+
app.use(express.urlencoded({ limit: "50mb", extended: true }));
3838
app.use(cookieParser()); // parse cookies
3939
app.use(passport.initialize());
4040

src/components/User/Dashboard/DatasetOrganizer/DropZone.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ interface DropZoneProps {
1717
files: FileItem[];
1818
setFiles: React.Dispatch<React.SetStateAction<FileItem[]>>;
1919
baseDirectoryPath: string; // ✅ ADD this line
20-
setBaseDirectoryPath: React.Dispatch<React.SetStateAction<string>>; // ✅ ADD this line
20+
// setBaseDirectoryPath: React.Dispatch<React.SetStateAction<string>>;
21+
setBaseDirectoryPath: (path: string) => void;
2122
selectedIds: Set<string>;
2223
setSelectedIds: React.Dispatch<React.SetStateAction<Set<string>>>;
2324
expandedIds: Set<string>;

src/components/User/Dashboard/DatasetOrganizer/FileTree.tsx

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Add,
1212
AutoAwesome,
1313
FolderSpecial,
14+
Download,
1415
} from "@mui/icons-material";
1516
import {
1617
Box,
@@ -25,6 +26,7 @@ import {
2526
DialogActions,
2627
} from "@mui/material";
2728
import { Colors } from "design/theme";
29+
import JSZip from "jszip";
2830
import React, { useState } from "react";
2931
import { FileItem } from "redux/projects/types/projects.interface";
3032

@@ -55,6 +57,10 @@ const FileTree: React.FC<FileTreeProps> = ({
5557
const [metaFileName, setMetaFileName] = useState("");
5658
const [metaContent, setMetaContent] = useState("");
5759

60+
// split files into two groups
61+
const userFiles = files.filter((f) => f.source !== "output");
62+
const outputFiles = files.filter((f) => f.source === "output");
63+
5864
// In FileTree.tsx
5965
const metaConfigs = {
6066
readme: {
@@ -167,6 +173,42 @@ const FileTree: React.FC<FileTreeProps> = ({
167173
setSelectedIds(new Set());
168174
};
169175

176+
const handleDownloadOutputFolder = async (
177+
folderId: string,
178+
folderName: string
179+
) => {
180+
const zip = new JSZip();
181+
182+
// Recursive function to add files to zip
183+
const addToZip = (
184+
parentId: string,
185+
zipFolder: any,
186+
currentPath: string
187+
) => {
188+
const children = files.filter((f) => f.parentId === parentId);
189+
children.forEach((child) => {
190+
if (child.type === "folder") {
191+
const subFolder = zipFolder.folder(child.name);
192+
addToZip(child.id, subFolder, `${currentPath}/${child.name}`);
193+
} else {
194+
if (child.content) {
195+
zipFolder.file(child.name, child.content);
196+
}
197+
}
198+
});
199+
};
200+
201+
addToZip(folderId, zip, folderName);
202+
203+
const blob = await zip.generateAsync({ type: "blob" });
204+
const url = URL.createObjectURL(blob);
205+
const a = document.createElement("a");
206+
a.href = url;
207+
a.download = `${folderName}.zip`;
208+
a.click();
209+
URL.revokeObjectURL(url);
210+
};
211+
170212
const handleAddNote = (id: string) => {
171213
const file = files.find((f) => f.id === id);
172214
setEditingNoteId(id);
@@ -187,6 +229,12 @@ const FileTree: React.FC<FileTreeProps> = ({
187229
};
188230

189231
const renderFileIcon = (file: FileItem) => {
232+
if (file.source === "output") {
233+
if (file.type === "folder") {
234+
return <FolderSpecial sx={{ color: Colors.darkGreen, fontSize: 20 }} />;
235+
}
236+
return <InsertDriveFile sx={{ color: Colors.darkGreen, fontSize: 20 }} />;
237+
}
190238
// AI generated files — use AutoAwesome icon with purple color
191239
if (file.source === "ai") {
192240
return (
@@ -245,8 +293,13 @@ const FileTree: React.FC<FileTreeProps> = ({
245293
};
246294

247295
// one item in the tree
248-
const renderTreeItem = (file: FileItem, depth: number = 0) => {
249-
const children = files.filter((f) => f.parentId === file.id);
296+
const renderTreeItem = (
297+
file: FileItem,
298+
depth: number = 0,
299+
filePool: FileItem[] = files
300+
) => {
301+
// const children = files.filter((f) => f.parentId === file.id); // origin
302+
const children = filePool.filter((f) => f.parentId === file.id);
250303
const hasChildren = children.length > 0;
251304

252305
// Check if file has content or children to show expand button
@@ -315,6 +368,23 @@ const FileTree: React.FC<FileTreeProps> = ({
315368
{file.name}
316369
</Typography>
317370

371+
{/* Download button for output root folders */}
372+
{file.source === "output" &&
373+
file.parentId === null &&
374+
file.type === "folder" && (
375+
<IconButton
376+
size="small"
377+
onClick={(e) => {
378+
e.stopPropagation();
379+
handleDownloadOutputFolder(file.id, file.name);
380+
}}
381+
sx={{ p: 0.25, color: Colors.purple }}
382+
title="Download as ZIP"
383+
>
384+
<Download sx={{ fontSize: 16 }} />
385+
</IconButton>
386+
)}
387+
318388
{/* Add timestamp for AI files */}
319389
{file.source === "ai" && file.generatedAt && (
320390
<Typography
@@ -396,7 +466,11 @@ const FileTree: React.FC<FileTreeProps> = ({
396466

397467
{/* Children */}
398468
{hasChildren && isExpanded && (
399-
<Box>{children.map((child) => renderTreeItem(child, depth + 1))}</Box>
469+
<Box>
470+
{children.map((child) =>
471+
renderTreeItem(child, depth + 1, filePool)
472+
)}
473+
</Box> // add filePool
400474
)}
401475
</Box>
402476
);
@@ -534,8 +608,42 @@ const FileTree: React.FC<FileTreeProps> = ({
534608
</Box>
535609

536610
{/* File Tree */}
537-
<Box sx={{ flex: 1, overflow: "auto", p: 1 }}>
611+
{/* <Box sx={{ flex: 1, overflow: "auto", p: 1 }}>
538612
{rootFiles.map((file) => renderTreeItem(file))}
613+
</Box> */}
614+
615+
<Box sx={{ flex: 1, overflow: "auto", p: 1 }}>
616+
{userFiles
617+
.filter((f) => f.parentId === null)
618+
.map((f) => renderTreeItem(f, 0, userFiles))}
619+
620+
{outputFiles.length > 0 && (
621+
<>
622+
<Box
623+
sx={{
624+
display: "flex",
625+
alignItems: "center",
626+
gap: 1,
627+
px: 1,
628+
py: 0.5,
629+
mt: 1,
630+
borderTop: 1,
631+
borderColor: "divider",
632+
}}
633+
>
634+
<FolderSpecial sx={{ color: Colors.darkGreen, fontSize: 16 }} />
635+
<Typography
636+
variant="caption"
637+
sx={{ color: Colors.darkGreen, fontWeight: 600 }}
638+
>
639+
Saved Outputs
640+
</Typography>
641+
</Box>
642+
{outputFiles
643+
.filter((f) => f.parentId === null)
644+
.map((f) => renderTreeItem(f, 0, outputFiles))}
645+
</>
646+
)}
539647
</Box>
540648

541649
{/* Footer Legend */}

0 commit comments

Comments
 (0)