Review fixes

This commit is contained in:
Reece
2026-03-13 15:21:03 +00:00
parent 8e29790ff0
commit 993abdae45
7 changed files with 74 additions and 33 deletions
@@ -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<FileListItemProps> = ({
leftSection={<FolderItemIcon style={{ fontSize: 16, color: folder.accentColor }} />}
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}
@@ -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 <CustomComponent data={customView.data} />;
@@ -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 {
@@ -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]
@@ -31,8 +31,9 @@ export function useFolderMembership(): Map<string, string[]> {
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 {
+11 -5
View File
@@ -59,17 +59,23 @@ export function useSmartFolders(): UseSmartFoldersReturn {
);
const deleteFolder = useCallback(async (id: string): Promise<void> => {
// 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 };
+6 -1
View File
@@ -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