add a dismiss all errors button and change the wording of invalid file errors

This commit is contained in:
EthanHealy01 2025-09-25 17:53:01 +01:00
parent 428f9eadbe
commit 245d1cb551
11 changed files with 138 additions and 33 deletions

View File

@ -75,6 +75,7 @@
"error": { "error": {
"pdfPassword": "The PDF Document is passworded and either the password was not provided or was incorrect", "pdfPassword": "The PDF Document is passworded and either the password was not provided or was incorrect",
"_value": "Error", "_value": "Error",
"dismissAllErrors": "Dismiss All Errors",
"sorry": "Sorry for the issue!", "sorry": "Sorry for the issue!",
"needHelp": "Need help / Found an issue?", "needHelp": "Need help / Found an issue?",
"contactTip": "If you're still having trouble, don't hesitate to reach out to us for help. You can submit a ticket on our GitHub page or contact us through Discord:", "contactTip": "If you're still having trouble, don't hesitate to reach out to us for help. You can submit a ticket on our GitHub page or contact us through Discord:",

View File

@ -1,6 +1,6 @@
import React, { useState, useCallback, useRef, useMemo } from 'react'; import React, { useState, useCallback, useRef, useMemo } from 'react';
import { import {
Text, Center, Box, Notification, LoadingOverlay, Stack, Group, Portal Text, Center, Box, LoadingOverlay, Stack, Group
} from '@mantine/core'; } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone'; import { Dropzone } from '@mantine/dropzone';
import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext'; import { useFileSelection, useFileState, useFileManagement } from '../../contexts/FileContext';
@ -47,8 +47,8 @@ const FileEditor = ({
// Get file selection context // Get file selection context
const { setSelectedFiles } = useFileSelection(); const { setSelectedFiles } = useFileSelection();
const [status, setStatus] = useState<string | null>(null); const [_status, _setStatus] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [_error, _setError] = useState<string | null>(null);
// Toast helpers // Toast helpers
const showStatus = useCallback((message: string, type: 'neutral' | 'success' | 'warning' | 'error' = 'neutral') => { const showStatus = useCallback((message: string, type: 'neutral' | 'success' | 'warning' | 'error' = 'neutral') => {
@ -91,7 +91,7 @@ const FileEditor = ({
// Process uploaded files using context // Process uploaded files using context
const handleFileUpload = useCallback(async (uploadedFiles: File[]) => { const handleFileUpload = useCallback(async (uploadedFiles: File[]) => {
setError(null); _setError(null);
try { try {
const allExtractedFiles: File[] = []; const allExtractedFiles: File[] = [];
@ -224,7 +224,7 @@ const FileEditor = ({
// Update context (this automatically updates tool selection since they use the same action) // Update context (this automatically updates tool selection since they use the same action)
setSelectedFiles(newSelection); setSelectedFiles(newSelection);
}, [setSelectedFiles, toolMode, setStatus, activeStirlingFileStubs]); }, [setSelectedFiles, toolMode, _setStatus, activeStirlingFileStubs]);
// File reordering handler for drag and drop // File reordering handler for drag and drop
@ -281,7 +281,7 @@ const FileEditor = ({
// Update status // Update status
const moveCount = filesToMove.length; const moveCount = filesToMove.length;
showStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`); showStatus(`${moveCount > 1 ? `${moveCount} files` : 'File'} reordered`);
}, [activeStirlingFileStubs, reorderFiles, setStatus]); }, [activeStirlingFileStubs, reorderFiles, _setStatus]);
@ -306,7 +306,7 @@ const FileEditor = ({
if (record && file) { if (record && file) {
downloadBlob(file, file.name); downloadBlob(file, file.name);
} }
}, [activeStirlingFileStubs, selectors, setStatus]); }, [activeStirlingFileStubs, selectors, _setStatus]);
const handleViewFile = useCallback((fileId: FileId) => { const handleViewFile = useCallback((fileId: FileId) => {
const record = activeStirlingFileStubs.find(r => r.id === fileId); const record = activeStirlingFileStubs.find(r => r.id === fileId);
@ -417,7 +417,7 @@ const FileEditor = ({
onToggleFile={toggleFile} onToggleFile={toggleFile}
onDeleteFile={handleDeleteFile} onDeleteFile={handleDeleteFile}
onViewFile={handleViewFile} onViewFile={handleViewFile}
onSetStatus={showStatus} _onSetStatus={showStatus}
onReorderFiles={handleReorderFiles} onReorderFiles={handleReorderFiles}
onDownloadFile={handleDownloadFile} onDownloadFile={handleDownloadFile}
toolMode={toolMode} toolMode={toolMode}

View File

@ -29,7 +29,7 @@ interface FileEditorThumbnailProps {
onToggleFile: (fileId: FileId) => void; onToggleFile: (fileId: FileId) => void;
onDeleteFile: (fileId: FileId) => void; onDeleteFile: (fileId: FileId) => void;
onViewFile: (fileId: FileId) => void; onViewFile: (fileId: FileId) => void;
onSetStatus: (status: string) => void; _onSetStatus: (status: string) => void;
onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void; onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void;
onDownloadFile: (fileId: FileId) => void; onDownloadFile: (fileId: FileId) => void;
toolMode?: boolean; toolMode?: boolean;
@ -42,7 +42,7 @@ const FileEditorThumbnail = ({
selectedFiles, selectedFiles,
onToggleFile, onToggleFile,
onDeleteFile, onDeleteFile,
onSetStatus, _onSetStatus,
onReorderFiles, onReorderFiles,
onDownloadFile, onDownloadFile,
isSupported = true, isSupported = true,
@ -193,7 +193,7 @@ const FileEditorThumbnail = ({
if (!isSupported) return; if (!isSupported) return;
// Clear error state if file has an error (click to clear error) // Clear error state if file has an error (click to clear error)
if (hasError) { if (hasError) {
try { fileActions.clearFileError(file.id); } catch {} try { fileActions.clearFileError(file.id); } catch (_e) { void _e; }
} }
onToggleFile(file.id); onToggleFile(file.id);
}; };

View File

@ -13,6 +13,7 @@ import PageEditorControls from '../pageEditor/PageEditorControls';
import Viewer from '../viewer/Viewer'; import Viewer from '../viewer/Viewer';
import LandingPage from '../shared/LandingPage'; import LandingPage from '../shared/LandingPage';
import Footer from '../shared/Footer'; import Footer from '../shared/Footer';
import DismissAllErrorsButton from '../shared/DismissAllErrorsButton';
// No props needed - component uses contexts directly // No props needed - component uses contexts directly
export default function Workbench() { export default function Workbench() {
@ -151,6 +152,9 @@ export default function Workbench() {
selectedToolKey={selectedToolId} selectedToolKey={selectedToolId}
/> />
{/* Dismiss All Errors Button */}
<DismissAllErrorsButton />
{/* Main content area */} {/* Main content area */}
<Box <Box
className="flex-1 min-h-0 relative z-10 workbench-scrollable " className="flex-1 min-h-0 relative z-10 workbench-scrollable "

View File

@ -0,0 +1,51 @@
import React from 'react';
import { Button, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useFileState } from '../../contexts/FileContext';
import { useFileActions } from '../../contexts/file/fileHooks';
import CloseIcon from '@mui/icons-material/Close';
interface DismissAllErrorsButtonProps {
className?: string;
}
const DismissAllErrorsButton: React.FC<DismissAllErrorsButtonProps> = ({ className }) => {
const { t } = useTranslation();
const { state } = useFileState();
const { actions } = useFileActions();
// Check if there are any files in error state
const hasErrors = state.ui.errorFileIds.length > 0;
// Don't render if there are no errors
if (!hasErrors) {
return null;
}
const handleDismissAllErrors = () => {
actions.clearAllFileErrors();
};
return (
<Group className={className}>
<Button
variant="light"
color="red"
size="sm"
leftSection={<CloseIcon fontSize="small" />}
onClick={handleDismissAllErrors}
style={{
position: 'absolute',
top: '1rem',
right: '1rem',
zIndex: 1000,
pointerEvents: 'auto'
}}
>
{t('error.dismissAllErrors', 'Dismiss All Errors')} ({state.ui.errorFileIds.length})
</Button>
</Group>
);
};
export default DismissAllErrorsButton;

View File

@ -72,7 +72,7 @@ export default function RightRail() {
const allIds = state.files.ids; const allIds = state.files.ids;
setSelectedFiles(allIds); setSelectedFiles(allIds);
// Clear any previous error flags when selecting all // Clear any previous error flags when selecting all
try { fileActions.clearAllFileErrors(); } catch {} try { fileActions.clearAllFileErrors(); } catch (_e) { void _e; }
return; return;
} }
@ -86,7 +86,7 @@ export default function RightRail() {
if (currentView === 'fileEditor' || currentView === 'viewer') { if (currentView === 'fileEditor' || currentView === 'viewer') {
setSelectedFiles([]); setSelectedFiles([]);
// Clear any previous error flags when deselecting all // Clear any previous error flags when deselecting all
try { fileActions.clearAllFileErrors(); } catch {} try { fileActions.clearAllFileErrors(); } catch (_e) { void _e; }
return; return;
} }
if (currentView === 'pageEditor') { if (currentView === 'pageEditor') {

View File

@ -57,7 +57,7 @@ export function ToastProvider({ children }: { children: React.ReactNode }) {
progress: normalizeProgress(options.progressBarPercentage), progress: normalizeProgress(options.progressBarPercentage),
justCompleted: false, justCompleted: false,
expandable: hasButton ? false : (options.expandable !== false), expandable: hasButton ? false : (options.expandable !== false),
isExpanded: hasButton ? true : (options.expandable === false ? true : false), isExpanded: hasButton ? true : (options.expandable === false ? true : (options.alertType === 'error' ? true : false)),
createdAt: Date.now(), createdAt: Date.now(),
} as ToastInstance; } as ToastInstance;
setToasts(prev => { setToasts(prev => {

View File

@ -61,7 +61,11 @@ export const useToolApiCalls = <TParams = void>() => {
if (empty) { if (empty) {
console.warn('[processFiles] Empty output treated as failure', { name: file.name }); console.warn('[processFiles] Empty output treated as failure', { name: file.name });
failedFiles.push(file.name); failedFiles.push(file.name);
try { (markFileError as any)?.((file as any).fileId); } catch {} try {
(markFileError as any)?.((file as any).fileId);
} catch (e) {
console.debug('markFileError', e);
}
continue; continue;
} }
processedFiles.push(...responseFiles); processedFiles.push(...responseFiles);
@ -76,7 +80,11 @@ export const useToolApiCalls = <TParams = void>() => {
console.error('[processFiles] Failed', { name: file.name, error }); console.error('[processFiles] Failed', { name: file.name, error });
failedFiles.push(file.name); failedFiles.push(file.name);
// mark errored file so UI can highlight // mark errored file so UI can highlight
try { (markFileError as any)?.((file as any).fileId); } catch {} try {
(markFileError as any)?.((file as any).fileId);
} catch (e) {
console.debug('markFileError', e);
}
} }
} }

View File

@ -6,7 +6,7 @@ import { useToolState, type ProcessingProgress } from './useToolState';
import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls'; import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls';
import { useToolResources } from './useToolResources'; import { useToolResources } from './useToolResources';
import { extractErrorMessage } from '../../../utils/toolErrorHandler'; import { extractErrorMessage } from '../../../utils/toolErrorHandler';
import { StirlingFile, extractFiles, FileId, StirlingFileStub, createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext'; import { StirlingFile, extractFiles, FileId, StirlingFileStub, createStirlingFile } from '../../../types/fileContext';
import { FILE_EVENTS } from '../../../services/errorUtils'; import { FILE_EVENTS } from '../../../services/errorUtils';
import { ResponseHandler } from '../../../utils/toolResponseProcessor'; import { ResponseHandler } from '../../../utils/toolResponseProcessor';
import { createChildStub, generateProcessedFileMetadata } from '../../../contexts/file/fileActions'; import { createChildStub, generateProcessedFileMetadata } from '../../../contexts/file/fileActions';
@ -177,7 +177,9 @@ export const useToolOperation = <TParams>(
for (const f of zeroByteFiles) { for (const f of zeroByteFiles) {
(fileActions.markFileError as any)((f as any).fileId); (fileActions.markFileError as any)((f as any).fileId);
} }
} catch {} } catch (e) {
console.log('markFileError', e);
}
} }
const validFiles = selectedFiles.filter(file => (file as any)?.size > 0); const validFiles = selectedFiles.filter(file => (file as any)?.size > 0);
if (validFiles.length === 0) { if (validFiles.length === 0) {
@ -298,15 +300,15 @@ export const useToolOperation = <TParams>(
const okSet = new Set((successSourceIds as unknown as string[]) || []); const okSet = new Set((successSourceIds as unknown as string[]) || []);
// Clear errors on successes // Clear errors on successes
for (const okId of okSet) { for (const okId of okSet) {
try { (fileActions.clearFileError as any)(okId); } catch {} try { (fileActions.clearFileError as any)(okId); } catch (_e) { void _e; }
} }
// Mark errors on inputs that didn't succeed // Mark errors on inputs that didn't succeed
for (const id of allInputIds) { for (const id of allInputIds) {
if (!okSet.has(id)) { if (!okSet.has(id)) {
try { (fileActions.markFileError as any)(id); } catch {} try { (fileActions.markFileError as any)(id); } catch (_e) { void _e; }
} }
} }
} catch {} } catch (_e) { void _e; }
if (externalErrorFileIds.length > 0) { if (externalErrorFileIds.length > 0) {
// If backend told us which sources failed, prefer that mapping // If backend told us which sources failed, prefer that mapping
@ -318,7 +320,7 @@ export const useToolOperation = <TParams>(
for (const badId of externalErrorFileIds) { for (const badId of externalErrorFileIds) {
(fileActions.markFileError as any)(badId); (fileActions.markFileError as any)(badId);
} }
} catch {} } catch (_e) { void _e; }
} }
if (processedFiles.length > 0) { if (processedFiles.length > 0) {
@ -426,14 +428,14 @@ export const useToolOperation = <TParams>(
} }
if (ids && ids.length > 0) { if (ids && ids.length > 0) {
for (const badId of ids) { for (const badId of ids) {
try { (fileActions.markFileError as any)(badId); } catch {} try { (fileActions.markFileError as any)(badId); } catch (_e) { void _e; }
} }
actions.setStatus('Some files could not be processed'); actions.setStatus('Process failed due to invalid/corrupted file(s)');
// Avoid duplicating toast messaging here // Avoid duplicating toast messaging here
return; return;
} }
} }
} catch {} } catch (_e) { void _e; }
const errorMessage = config.getErrorMessage?.(error) || extractErrorMessage(error); const errorMessage = config.getErrorMessage?.(error) || extractErrorMessage(error);
actions.setError(errorMessage); actions.setError(errorMessage);

View File

@ -25,7 +25,7 @@ function titleForStatus(status?: number): string {
function extractAxiosErrorMessage(error: any): { title: string; body: string } { function extractAxiosErrorMessage(error: any): { title: string; body: string } {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
const status = error.response?.status; const status = error.response?.status;
const statusText = error.response?.statusText || ''; const _statusText = error.response?.statusText || '';
let parsed: any = undefined; let parsed: any = undefined;
const raw = error.response?.data; const raw = error.response?.data;
if (typeof raw === 'string') { if (typeof raw === 'string') {
@ -49,20 +49,37 @@ function extractAxiosErrorMessage(error: any): { title: string; body: string } {
if (typeof raw === 'string') return raw; if (typeof raw === 'string') return raw;
try { return JSON.stringify(data); } catch { return ''; } try { return JSON.stringify(data); } catch { return ''; }
})(); })();
const bodyMsg = isUnhelpfulMessage(body) ? FRIENDLY_FALLBACK : body; const ids = extractIds();
const title = titleForStatus(status); const title = titleForStatus(status);
if (ids && ids.length > 0) {
return { title, body: 'Process failed due to invalid/corrupted file(s)' };
}
if (status === 422) {
const fallbackMsg = 'Process failed due to invalid/corrupted file(s)';
const bodyMsg = isUnhelpfulMessage(body) ? fallbackMsg : body;
return { title, body: bodyMsg };
}
const bodyMsg = isUnhelpfulMessage(body) ? FRIENDLY_FALLBACK : body;
return { title, body: bodyMsg }; return { title, body: bodyMsg };
} }
try { try {
const msg = (error?.message || String(error)) as string; const msg = (error?.message || String(error)) as string;
return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg }; return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg };
} catch { } catch (e) {
// ignore extraction errors
console.debug('extractAxiosErrorMessage', e);
return { title: 'Network error', body: FRIENDLY_FALLBACK }; return { title: 'Network error', body: FRIENDLY_FALLBACK };
} }
} }
// Install Axios response error interceptor // Install Axios response error interceptor (guard against double-registration in HMR)
axios.interceptors.response.use( const __globalAny = (typeof window !== 'undefined' ? (window as any) : undefined);
if (__globalAny?.__SPDF_HTTP_ERR_INTERCEPTOR_ID !== undefined) {
try { axios.interceptors.response.eject(__globalAny.__SPDF_HTTP_ERR_INTERCEPTOR_ID); } catch (_e) { void _e; }
}
const __recentSpecialByEndpoint: Record<string, number> = (__globalAny?.__SPDF_RECENT_SPECIAL || {});
const __SPECIAL_SUPPRESS_MS = 1500; // brief window to suppress generic duplicate after special toast
const __INTERCEPTOR_ID__ = axios.interceptors.response.use(
(response) => response, (response) => response,
async (error) => { async (error) => {
const { title, body } = extractAxiosErrorMessage(error); const { title, body } = extractAxiosErrorMessage(error);
@ -71,12 +88,34 @@ axios.interceptors.response.use(
const raw = (error?.response?.data) as any; const raw = (error?.response?.data) as any;
const data = await normalizeAxiosErrorData(raw); const data = await normalizeAxiosErrorData(raw);
const ids = extractErrorFileIds(data); const ids = extractErrorFileIds(data);
if (ids && ids.length > 0) broadcastErroredFiles(ids); if (ids && ids.length > 0) {
} catch {} broadcastErroredFiles(ids);
}
} catch (_e) { void _e; }
// Generic-vs-special dedupe by endpoint
const url: string | undefined = error?.config?.url;
const status: number | undefined = error?.response?.status;
const now = Date.now();
const isSpecial = status === 422 || /Failed files:/.test(body) || /invalid\/corrupted file\(s\)/i.test(body);
if (isSpecial && url) {
__recentSpecialByEndpoint[url] = now;
if (__globalAny) __globalAny.__SPDF_RECENT_SPECIAL = __recentSpecialByEndpoint;
}
if (!isSpecial && url) {
const last = __recentSpecialByEndpoint[url] || 0;
if (now - last < __SPECIAL_SUPPRESS_MS) {
return Promise.reject(error);
}
}
alert({ alertType: 'error', title, body, expandable: true, isPersistentPopup: false }); alert({ alertType: 'error', title, body, expandable: true, isPersistentPopup: false });
return Promise.reject(error); return Promise.reject(error);
} }
); );
if (__globalAny) {
__globalAny.__SPDF_HTTP_ERR_INTERCEPTOR_ID = __INTERCEPTOR_ID__;
}
export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> { export async function apiFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const res = await fetch(input, { credentials: init?.credentials ?? 'include', ...init }); const res = await fetch(input, { credentials: init?.credentials ?? 'include', ...init });

View File

@ -12,7 +12,7 @@ export const extractErrorMessage = (error: any): string => {
if (error.message) { if (error.message) {
return error.message; return error.message;
} }
return 'Operation failed'; return 'There was an error processing your request.';
}; };
/** /**