Review fixes
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user