From 993abdae453076abdea05b5c9be17fb3f5426301 Mon Sep 17 00:00:00 2001 From: Reece Date: Fri, 13 Mar 2026 15:21:03 +0000 Subject: [PATCH] Review fixes --- .../components/fileManager/FileListItem.tsx | 8 +++-- .../src/core/components/layout/Workbench.tsx | 3 +- .../smartFolders/SmartFolderHomePage.tsx | 32 ++++++++++++----- .../smartFolders/SmartFolderWorkbenchView.tsx | 36 +++++++++++-------- .../src/core/hooks/useFolderMembership.ts | 5 +-- frontend/src/core/hooks/useSmartFolders.ts | 16 ++++++--- frontend/src/core/types/smartFolders.ts | 7 +++- 7 files changed, 74 insertions(+), 33 deletions(-) diff --git a/frontend/src/core/components/fileManager/FileListItem.tsx b/frontend/src/core/components/fileManager/FileListItem.tsx index 0e0e6f1d09..10f6b9cd03 100644 --- a/frontend/src/core/components/fileManager/FileListItem.tsx +++ b/frontend/src/core/components/fileManager/FileListItem.tsx @@ -20,6 +20,10 @@ import { useAllSmartFolders } from '@app/hooks/useAllSmartFolders'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { useNavigationActions } from '@app/contexts/NavigationContext'; import { iconMap } from '@app/components/tools/automate/iconMap'; +import { + SMART_FOLDER_VIEW_ID, + SMART_FOLDER_WORKBENCH_ID, +} from '@app/components/smartFolders/SmartFoldersRegistration'; interface FileListItemProps { file: StirlingFileStub; @@ -282,8 +286,8 @@ const FileListItem: React.FC = ({ leftSection={} onClick={(e) => { e.stopPropagation(); - setCustomWorkbenchViewData('smartFolder', { folderId: folder.id, pendingFileId: file.id }); - actions.setWorkbench('custom:smartFolder'); + setCustomWorkbenchViewData(SMART_FOLDER_VIEW_ID, { folderId: folder.id, pendingFileId: file.id }); + actions.setWorkbench(SMART_FOLDER_WORKBENCH_ID); }} > {folder.name} diff --git a/frontend/src/core/components/layout/Workbench.tsx b/frontend/src/core/components/layout/Workbench.tsx index 64fe052dc3..39eb6dc6cd 100644 --- a/frontend/src/core/components/layout/Workbench.tsx +++ b/frontend/src/core/components/layout/Workbench.tsx @@ -6,6 +6,7 @@ import { useFileHandler } from '@app/hooks/useFileHandler'; import { useFileState, useFileActions } from '@app/contexts/FileContext'; import { useNavigationState, useNavigationActions, useNavigationGuard } from '@app/contexts/NavigationContext'; import { isBaseWorkbench } from '@app/types/workbench'; +import { SMART_FOLDER_WORKBENCH_ID } from '@app/components/smartFolders/SmartFoldersRegistration'; import { useViewer } from '@app/contexts/ViewerContext'; import { useAppConfig } from '@app/contexts/AppConfigContext'; import { FileId } from '@app/types/file'; @@ -98,7 +99,7 @@ export default function Workbench() { const customView = customWorkbenchViews.find((view) => view.workbenchId === currentView && view.data != null); if (customView) { // These custom views handle their own empty state (show dropzone when no document) - const handlesOwnEmptyState = currentView === 'custom:pdfTextEditor' || currentView === 'custom:smartFolder'; + const handlesOwnEmptyState = currentView === 'custom:pdfTextEditor' || currentView === SMART_FOLDER_WORKBENCH_ID; if (handlesOwnEmptyState || activeFiles.length > 0) { const CustomComponent = customView.component; return ; diff --git a/frontend/src/core/components/smartFolders/SmartFolderHomePage.tsx b/frontend/src/core/components/smartFolders/SmartFolderHomePage.tsx index 367ed624dc..068b9310e2 100644 --- a/frontend/src/core/components/smartFolders/SmartFolderHomePage.tsx +++ b/frontend/src/core/components/smartFolders/SmartFolderHomePage.tsx @@ -539,6 +539,8 @@ export function SmartFolderHomePage() { const processFiles = useCallback( async (folder: SmartFolder, files: File[]) => { + if (folder.isPaused) return; + const pdfs = files.filter((f) => f.name.toLowerCase().endsWith('.pdf')); if (pdfs.length === 0) return; @@ -550,11 +552,14 @@ export function SmartFolderHomePage() { if (!automation) return; for (const file of pdfs) { - // Resolve input file ID — reuse existing if already in the main store + // Resolve input file ID — reuse existing if already in the main store. + // Track whether we created a fresh stub so we know whether to mark it as processed. let inputFileId: string; + let ownedByFolder = false; if (isStirlingFile(file)) { inputFileId = file.fileId; } else { + ownedByFolder = true; const newFileId = createFileId(); const stub: StirlingFileStub = { id: newFileId, @@ -576,16 +581,17 @@ export function SmartFolderHomePage() { await folderStorage.addFileToFolder(folder.id, inputFileId, { status: 'processing', name: file.name, + ownedByFolder, }); try { const resultFiles = await executeAutomationSequence(automation, [file], toolRegistry as any); const existingRuns = await folderRunStateStorage.getFolderRunState(folder.id); const newRuns: SmartFolderRunEntry[] = [...existingRuns]; - let firstOutputId: string | undefined; + const allOutputIds: string[] = []; for (const resultFile of resultFiles) { const outputId = createFileId(); - if (!firstOutputId) firstOutputId = outputId; + allOutputIds.push(outputId); const outputStub: StirlingFileStub = { id: outputId, name: resultFile.name, @@ -603,16 +609,26 @@ export function SmartFolderHomePage() { await fileStorage.storeStirlingFile(createStirlingFile(resultFile, outputId), outputStub); newRuns.push({ inputFileId, displayFileId: outputId, status: 'processed' }); } - // Mark input as no longer a leaf (processed into an output) - await fileStorage.markFileAsProcessed(inputFileId as FileId); + // Only hide the input from "My Files" if the folder owns it (fresh drop from disk). + // Sidebar files belong to the user and must remain visible after processing. + if (ownedByFolder) { + await fileStorage.markFileAsProcessed(inputFileId as FileId); + } await folderStorage.updateFileMetadata(folder.id, inputFileId, { status: 'processed', processedAt: new Date(), - displayFileId: firstOutputId, + displayFileId: allOutputIds[0], + displayFileIds: allOutputIds, }); await folderRunStateStorage.setFolderRunState(folder.id, newRuns); - } catch { - await folderStorage.updateFileMetadata(folder.id, inputFileId, { status: 'error' }); + } catch (err: any) { + const existing = await folderStorage.getFolderData(folder.id); + const prev = existing?.files[inputFileId]; + await folderStorage.updateFileMetadata(folder.id, inputFileId, { + status: 'error', + errorMessage: err?.message, + failedAttempts: (prev?.failedAttempts ?? 0) + 1, + }); } } } finally { diff --git a/frontend/src/core/components/smartFolders/SmartFolderWorkbenchView.tsx b/frontend/src/core/components/smartFolders/SmartFolderWorkbenchView.tsx index 0e5a6c53cc..0bed4b509a 100644 --- a/frontend/src/core/components/smartFolders/SmartFolderWorkbenchView.tsx +++ b/frontend/src/core/components/smartFolders/SmartFolderWorkbenchView.tsx @@ -31,6 +31,7 @@ import { useFolderRunState } from '@app/hooks/useFolderRunState'; import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; import { SMART_FOLDER_VIEW_ID, SMART_FOLDER_WORKBENCH_ID } from '@app/components/smartFolders/SmartFoldersRegistration'; import { automationStorage } from '@app/services/automationStorage'; +import { folderRunStateStorage } from '@app/services/folderRunStateStorage'; import { executeAutomationSequence } from '@app/utils/automationExecutor'; import { SmartFolderRunEntry } from '@app/types/smartFolders'; import { AutomationConfig } from '@app/types/automation'; @@ -112,8 +113,7 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps } const inputIds = Object.keys(folderRecord.files); const outputIds = Object.values(folderRecord.files) - .map(m => m.displayFileId) - .filter((id): id is string => Boolean(id)); + .flatMap(m => m.displayFileIds ?? (m.displayFileId ? [m.displayFileId] : [])); Promise.all(inputIds.map(id => fileStorage.getStirlingFile(id as FileId))) .then(files => setInputFiles(files.filter(Boolean) as StirlingFile[])); @@ -130,7 +130,7 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps const runAutomation = useCallback( - async (inputFile: File, inputFileId: string) => { + async (inputFile: File, inputFileId: string, ownedByFolder = false) => { if (processingRef.current.has(inputFileId)) return; processingRef.current.add(inputFileId); @@ -153,11 +153,13 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps () => {} ); - const newRuns: SmartFolderRunEntry[] = [...recentRuns]; - let firstOutputId: string | undefined; + // Read fresh run state to avoid stale-closure overwrites under concurrent processing. + const currentRuns = await folderRunStateStorage.getFolderRunState(folderId!); + const newRuns: SmartFolderRunEntry[] = [...currentRuns]; + const allOutputIds: string[] = []; for (const resultFile of resultFiles) { const outputId = createFileId(); - if (!firstOutputId) firstOutputId = outputId; + allOutputIds.push(outputId); const outputStub: StirlingFileStub = { id: outputId, name: resultFile.name, @@ -175,13 +177,17 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps await fileStorage.storeStirlingFile(createStirlingFile(resultFile, outputId), outputStub); newRuns.push({ inputFileId, displayFileId: outputId, status: 'processed' }); } - // Mark input as no longer a leaf (it has been processed into an output) - await fileStorage.markFileAsProcessed(inputFileId as FileId); + // Only hide the input from "My Files" when the folder owns the file (fresh drop from disk). + // Sidebar files belong to the user and must remain visible after processing. + if (ownedByFolder) { + await fileStorage.markFileAsProcessed(inputFileId as FileId); + } await updateFileMetadata(inputFileId, { status: 'processed', processedAt: new Date(), - displayFileId: firstOutputId, + displayFileId: allOutputIds[0], + displayFileIds: allOutputIds, }); await setRecentRuns(newRuns); } catch (error: any) { @@ -195,7 +201,7 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps processingRef.current.delete(inputFileId); } }, - [folder, recentRuns, setRecentRuns, toolRegistry, updateFileMetadata, getFileMetadata] + [folder, folderId, setRecentRuns, toolRegistry, updateFileMetadata, getFileMetadata] ); const handleFiles = useCallback( @@ -205,13 +211,15 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps if (!file.name.toLowerCase().endsWith('.pdf')) continue; let inputFileId: string; + let ownedByFolder = false; if (sourceFileId) { - // File from sidebar — already in stirling-pdf-files + // File from sidebar — already in stirling-pdf-files; user still owns it inputFileId = sourceFileId; } else if (isStirlingFile(file)) { inputFileId = file.fileId; } else { - // Fresh drop from disk — store in main file DB + // Fresh drop from disk — folder owns this file + ownedByFolder = true; const newFileId = createFileId(); const stub: StirlingFileStub = { id: newFileId, @@ -230,8 +238,8 @@ export function SmartFolderWorkbenchView({ data }: SmartFolderWorkbenchViewProps inputFileId = newFileId; } - await addFile(inputFileId, { status: 'pending', name: file.name }); - runAutomation(file, inputFileId); + await addFile(inputFileId, { status: 'pending', name: file.name, ownedByFolder: ownedByFolder || undefined }); + runAutomation(file, inputFileId, ownedByFolder); } }, [addFile, runAutomation] diff --git a/frontend/src/core/hooks/useFolderMembership.ts b/frontend/src/core/hooks/useFolderMembership.ts index a2898bdad3..7ab6c3b54c 100644 --- a/frontend/src/core/hooks/useFolderMembership.ts +++ b/frontend/src/core/hooks/useFolderMembership.ts @@ -31,8 +31,9 @@ export function useFolderMembership(): Map { if (record) { Object.entries(record.files).forEach(([fileId, meta]) => { add(fileId, folder.id); - // Also tag the output file with this folder - if (meta?.displayFileId) add(meta.displayFileId, folder.id); + // Tag all output files with this folder + const outputIds = meta?.displayFileIds ?? (meta?.displayFileId ? [meta.displayFileId] : []); + outputIds.forEach(oid => add(oid, folder.id)); }); } } catch { diff --git a/frontend/src/core/hooks/useSmartFolders.ts b/frontend/src/core/hooks/useSmartFolders.ts index 2d17e7c99c..7dba66cc91 100644 --- a/frontend/src/core/hooks/useSmartFolders.ts +++ b/frontend/src/core/hooks/useSmartFolders.ts @@ -59,17 +59,23 @@ export function useSmartFolders(): UseSmartFoldersReturn { ); const deleteFolder = useCallback(async (id: string): Promise => { - // Clean up file blobs from the unified file store before clearing the record const record = await folderStorage.getFolderData(id); if (record) { - const allFileIds = Object.entries(record.files).flatMap(([inputId, meta]) => - meta.displayFileId ? [inputId, meta.displayFileId] : [inputId] - ); - await Promise.all(allFileIds.map(fid => fileStorage.deleteStirlingFile(fid as FileId).catch(() => {}))); + // Only delete input files the folder created from disk — never touch sidebar-sourced files. + const ownedInputIds = Object.entries(record.files) + .filter(([, meta]) => meta.ownedByFolder === true) + .map(([fid]) => fid); + // Always delete every output the folder produced (folder always owns those). + const outputIds = Object.values(record.files) + .flatMap(meta => meta.displayFileIds ?? (meta.displayFileId ? [meta.displayFileId] : [])); + const toDelete = [...new Set([...ownedInputIds, ...outputIds])]; + await Promise.all(toDelete.map(fid => fileStorage.deleteStirlingFile(fid as FileId).catch(() => {}))); } await folderStorage.clearFolder(id); await folderRunStateStorage.clearFolderRunState(id); await smartFolderStorage.deleteFolder(id); + // Notify the sidebar file list that files have been removed. + window.dispatchEvent(new CustomEvent('stirling:files-changed')); }, []); return { folders, loading, createFolder, updateFolder, deleteFolder, refreshFolders }; diff --git a/frontend/src/core/types/smartFolders.ts b/frontend/src/core/types/smartFolders.ts index ab15506a59..c78da2005c 100644 --- a/frontend/src/core/types/smartFolders.ts +++ b/frontend/src/core/types/smartFolders.ts @@ -20,8 +20,13 @@ export interface FolderFileMetadata { addedAt: Date; status: 'pending' | 'processing' | 'processed' | 'error'; processedAt?: Date; - /** Output file id — references the main stirling-pdf-files store */ + /** All output file ids produced by this run — references stirling-pdf-files */ + displayFileIds?: string[]; + /** First output file id — kept for backwards compat with existing records */ displayFileId?: string; + /** True when the folder created this file from a disk drop and therefore owns it. + * False / absent when the file came from the shared sidebar store — do NOT delete on folder removal. */ + ownedByFolder?: boolean; errorMessage?: string; failedAttempts?: number; name?: string; // original filename