Переглянути джерело

fix: restore button, folder tags, download name, folder tree

- Fix canRestore condition: was checking !isDeleted, always false.
  Restore button now appears for project owner/ADMIN on deleted comments.
- Fix folder tag display: video now shows only deepest (leaf)
  folder, not all ancestor folders.
- Fix download filename: save originalFilename on upload and
  use it for download attribute (falls back to UID filename).
- Fix FolderTree: merge 2 collapse/expand buttons into 1 toggle.
  Default state is now fully collapsed on page load.
- Fix comment count in project grid: only count non-deleted
  comments via _count filter in Prisma.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude Dev 1 місяць тому
батько
коміт
6daccd3f9e

+ 1 - 0
packages/api/prisma/schema.prisma

@@ -75,6 +75,7 @@ model Asset {
   uploaderId      String?         // null for legacy assets before this feature
   title           String
   filename        String
+  originalFilename String?
   filePath        String
   thumbnail       String?
   hlsPath         String?

+ 2 - 1
packages/api/src/routes/assets.ts

@@ -104,7 +104,7 @@ router.get('/', async (req: Request, res: Response) => {
       where: { projectId },
       include: {
         uploader: { select: { id: true, name: true, email: true, avatarUrl: true } },
-        _count: { select: { comments: true } },
+        _count: { select: { comments: { where: { deleted: false } } } },
         shareLinks: { select: { id: true }, take: 1 },
       },
       orderBy: { createdAt: 'desc' },
@@ -253,6 +253,7 @@ router.post('/upload', upload.single('video'), async (req: Request, res: Respons
         uploaderId: req.user!.userId,
         title: assetTitle,
         filename: req.file.filename,
+        originalFilename: req.file.originalname,
         filePath: req.file.filename,
         mimeType: req.file.mimetype,
         fileSize,

+ 17 - 11
src/app/(dashboard)/projects/[projectId]/page.tsx

@@ -99,23 +99,29 @@ function getSubfolders(folders: FolderNode[], targetId: string | null): FolderNo
   return [];
 }
 
-/** Build a map of assetId -> folder names it belongs to */
-function buildAssetFolders(allFolders: FolderNode[]): Map<string, string[]> {
-  const map = new Map<string, string[]>();
-  function addFolders(f: FolderNode, trail: string[]): void {
+/** Build a map of assetId -> single deepest folder name */
+function buildAssetFolders(allFolders: FolderNode[]): Map<string, string> {
+  const map = new Map<string, string>();
+  const depthMap = new Map<string, number>();
+
+  function search(f: FolderNode, depth: number): void {
     for (const id of f.assetIds) {
-      if (!map.has(id)) map.set(id, []);
-      map.get(id)!.push(f.name);
+      const existingDepth = depthMap.get(id) ?? -1;
+      if (depth > existingDepth) {
+        map.set(id, f.name);
+        depthMap.set(id, depth);
+      }
     }
-    for (const child of f.children) addFolders(child, [...trail, f.name]);
+    for (const child of f.children) search(child, depth + 1);
   }
-  for (const f of allFolders) addFolders(f, []);
+  for (const f of allFolders) search(f, 0);
   return map;
 }
 
-/** Get the folder names an asset belongs to */
-function getAssetFolderNames(assetFolders: Map<string, string[]>, assetId: string): string[] {
-  return assetFolders.get(assetId) ?? [];
+/** Get the folder name an asset belongs to (deepest only) */
+function getAssetFolderNames(assetFolders: Map<string, string>, assetId: string): string[] {
+  const name = assetFolders.get(assetId);
+  return name ? [name] : [];
 }
 
 /** Returns a breadcrumb path of folder names for the selected folder */

+ 3 - 2
src/app/review/[assetId]/page.tsx

@@ -49,6 +49,7 @@ export default function ReviewPage() {
   const [replyTo, setReplyTo] = useState<Comment | null>(null);
   const [showResolved, setShowResolved] = useState(false);
   const [showDeleted, setShowDeleted] = useState(false);
+  const [deletedLoaded, setDeletedLoaded] = useState(false); // true once we've fetched comments with deleted included
   const [showShareModal, setShowShareModal] = useState(false);
 
   // Drawing state — lifted to page level
@@ -462,7 +463,7 @@ export default function ReviewPage() {
         {/* Download */}
         <a
           href={`${API_BASE}/uploads/${asset.filePath}`}
-          download={asset.filename}
+          download={asset.originalFilename ?? asset.filename}
           className="flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-md transition-all shrink-0"
           style={{ color: '#60A5FA', background: 'rgba(96,165,250,0.08)' }}
           title="Download original video"
@@ -1260,7 +1261,7 @@ function CommentItem({
   const annotations = comment.annotations ?? [];
   const canAddMore = annotations.length < MAX_ANNOTATIONS;
   const isDeleted = !!comment.deleted;
-  const canRestore = !isDeleted && (isProjectOwner || isProjectAdmin);
+  const canRestore = isDeleted && (isProjectOwner || isProjectAdmin);
 
   // Resolve state machine
   const isResolved = comment.resolveStatus === 'RESOLVED';

+ 8 - 29
src/components/folders/FolderTree.tsx

@@ -39,22 +39,8 @@ export function FolderTree({
   onRefresh,
   totalAssetCount,
 }: Props) {
-  // Auto-expand folders that contain the selected folder
-  const initialExpanded = (() => {
-    const s = new Set<string>();
-    if (selectedFolderId !== null) {
-      function markAncestors(f: FolderNode): boolean {
-        if (f.id === selectedFolderId) return true;
-        for (const child of f.children) {
-          if (markAncestors(child)) { s.add(f.id); return true; }
-        }
-        return false;
-      }
-      for (const f of folders) markAncestors(f);
-    }
-    return s;
-  })();
-  const [expanded, setExpanded] = useState<Set<string>>(initialExpanded);
+  // Default: all folders collapsed on first load
+  const [expanded, setExpanded] = useState<Set<string>>(new Set());
   const [creatingIn, setCreatingIn] = useState<string | null>(null); // null = not creating, '__root__' = creating at root, string = creating in folder
   const [newFolderName, setNewFolderName] = useState('');
   const [renamingId, setRenamingId] = useState<string | null>(null);
@@ -327,22 +313,15 @@ export function FolderTree({
       {hasFolders && (
         <div className="flex items-center gap-0.5 px-1 mb-1">
           <button
-            onClick={() => setExpanded(new Set())}
-            title="Collapse all"
-            className="w-5 h-5 flex items-center justify-center rounded hover:bg-white/10 transition-colors shrink-0"
-            style={{ color: 'var(--text-subtle)' }}
-          >
-            <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
-              <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
-            </svg>
-          </button>
-          <button
-            onClick={() => setExpanded(allFolderIds)}
-            title="Expand all"
+            onClick={() => setExpanded(expanded.size > 0 ? new Set() : allFolderIds)}
+            title={expanded.size > 0 ? 'Collapse all' : 'Expand all'}
             className="w-5 h-5 flex items-center justify-center rounded hover:bg-white/10 transition-colors shrink-0"
             style={{ color: 'var(--text-subtle)' }}
           >
-            <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
+            <svg
+              className={`w-3 h-3 transition-transform ${expanded.size > 0 ? 'rotate-0' : 'rotate-180'}`}
+              fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
+            >
               <path strokeLinecap="round" strokeLinejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
             </svg>
           </button>

+ 1 - 1
src/components/ui/AssetCard.tsx

@@ -264,7 +264,7 @@ export function AssetCard({ asset, canManage, showHour, onPlay, onDelete, onCanc
           {/* Download */}
           <a
             href={`/uploads/${asset.filePath}`}
-            download={asset.filename}
+            download={asset.originalFilename ?? asset.filename}
             onClick={e => e.stopPropagation()}
             className="p-1 rounded transition-colors hover:bg-blue-500/20 flex-shrink-0"
             title="Download original"

+ 1 - 0
src/lib/api.ts

@@ -415,6 +415,7 @@ export interface Asset {
   projectId: string;
   title: string;
   filename: string;
+  originalFilename?: string | null;
   filePath: string;
   thumbnail?: string | null;
   hlsPath?: string | null;