mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-01 20:10:35 +01:00
Fix first batch of any types
This commit is contained in:
parent
00fb40fb74
commit
06c6b96992
@ -131,7 +131,7 @@ export default function AdminGeneralSection() {
|
||||
}
|
||||
|
||||
return {
|
||||
sectionData: {},
|
||||
sectionData: settings,
|
||||
deltaSettings
|
||||
};
|
||||
}
|
||||
|
||||
@ -76,7 +76,7 @@ export default function AutomationSelection({
|
||||
key={automation.id}
|
||||
title={automation.name}
|
||||
description={automation.description}
|
||||
badgeIcon={automation.icon}
|
||||
badgeIcon={automation.iconComponent}
|
||||
operations={automation.operations.map(op => op.operation)}
|
||||
onClick={() => onRun(automation)}
|
||||
showMenu={true}
|
||||
|
||||
@ -46,7 +46,7 @@ export function useSavedAutomations() {
|
||||
const { automationStorage } = await import('@app/services/automationStorage');
|
||||
|
||||
// Map suggested automation icons to MUI icon keys
|
||||
const getIconKey = (_suggestedIcon: {id: string}): string => {
|
||||
const getIconKey = (): string => {
|
||||
// Check the automation ID or name to determine the appropriate icon
|
||||
switch (suggestedAutomation.id) {
|
||||
case 'secure-pdf-ingestion':
|
||||
@ -65,7 +65,7 @@ export function useSavedAutomations() {
|
||||
const savedAutomation = {
|
||||
name: suggestedAutomation.name,
|
||||
description: suggestedAutomation.description,
|
||||
icon: getIconKey(suggestedAutomation.icon),
|
||||
icon: suggestedAutomation.icon ?? getIconKey(),
|
||||
operations: suggestedAutomation.operations
|
||||
};
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ const StarIcon = () => React.createElement(LocalIcon, { icon: 'star', width: '1.
|
||||
export function useSuggestedAutomations(): SuggestedAutomation[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const suggestedAutomations = useMemo<SuggestedAutomation[]>(() => {
|
||||
const suggestedAutomations = useMemo<SuggestedAutomation[]>(() => {
|
||||
const now = new Date().toISOString();
|
||||
return [
|
||||
{
|
||||
@ -65,7 +65,8 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
||||
],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
icon: SecurityIcon,
|
||||
icon: 'SecurityIcon',
|
||||
iconComponent: SecurityIcon,
|
||||
},
|
||||
{
|
||||
id: "email-preparation",
|
||||
@ -112,7 +113,8 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
||||
],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
icon: CompressIcon,
|
||||
icon: 'CompressIcon',
|
||||
iconComponent: CompressIcon,
|
||||
},
|
||||
{
|
||||
id: "secure-workflow",
|
||||
@ -151,7 +153,8 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
||||
],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
icon: SecurityIcon,
|
||||
icon: 'SecurityIcon',
|
||||
iconComponent: SecurityIcon,
|
||||
},
|
||||
{
|
||||
id: "process-images",
|
||||
@ -185,7 +188,8 @@ export function useSuggestedAutomations(): SuggestedAutomation[] {
|
||||
],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
icon: StarIcon,
|
||||
icon: 'StarIcon',
|
||||
iconComponent: StarIcon,
|
||||
},
|
||||
];
|
||||
}, [t]);
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { mergePendingSettings, isFieldPending, hasPendingChanges } from '@app/utils/settingsPendingHelper';
|
||||
import { mergePendingSettings, isFieldPending, hasPendingChanges, SettingsWithPending } from '@app/utils/settingsPendingHelper';
|
||||
|
||||
interface UseAdminSettingsOptions<T> {
|
||||
type SettingsRecord = Record<string, unknown>;
|
||||
type CombinedSettings<T extends object> = T & SettingsRecord;
|
||||
type PendingSettings<T extends object> = SettingsWithPending<CombinedSettings<T>> & CombinedSettings<T>;
|
||||
|
||||
interface UseAdminSettingsOptions<T extends object> {
|
||||
sectionName: string;
|
||||
/**
|
||||
* Optional transformer to combine data from multiple endpoints.
|
||||
@ -14,14 +18,14 @@ interface UseAdminSettingsOptions<T> {
|
||||
* Returns an object with sectionData and optionally deltaSettings.
|
||||
*/
|
||||
saveTransformer?: (settings: T) => {
|
||||
sectionData: any;
|
||||
deltaSettings?: Record<string, any>;
|
||||
sectionData: T;
|
||||
deltaSettings?: SettingsRecord;
|
||||
};
|
||||
}
|
||||
|
||||
interface UseAdminSettingsReturn<T> {
|
||||
interface UseAdminSettingsReturn<T extends object> {
|
||||
settings: T;
|
||||
rawSettings: any;
|
||||
rawSettings: PendingSettings<T> | null;
|
||||
loading: boolean;
|
||||
saving: boolean;
|
||||
setSettings: (settings: T) => void;
|
||||
@ -39,14 +43,14 @@ interface UseAdminSettingsReturn<T> {
|
||||
* const { settings, setSettings, saveSettings, isFieldPending } = useAdminSettings({
|
||||
* sectionName: 'legal'
|
||||
* });
|
||||
*/
|
||||
export function useAdminSettings<T = any>(
|
||||
*/
|
||||
export function useAdminSettings<T extends object>(
|
||||
options: UseAdminSettingsOptions<T>
|
||||
): UseAdminSettingsReturn<T> {
|
||||
const { sectionName, fetchTransformer, saveTransformer } = options;
|
||||
|
||||
const [settings, setSettings] = useState<T>({} as T);
|
||||
const [rawSettings, setRawSettings] = useState<any>(null);
|
||||
const [rawSettings, setRawSettings] = useState<PendingSettings<T> | null>(null);
|
||||
const [originalSettings, setOriginalSettings] = useState<T>({} as T); // Track original active values
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@ -55,15 +59,15 @@ export function useAdminSettings<T = any>(
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
let rawData: any;
|
||||
let rawData: PendingSettings<T>;
|
||||
|
||||
if (fetchTransformer) {
|
||||
// Use custom fetch logic for complex sections
|
||||
rawData = await fetchTransformer();
|
||||
rawData = (await fetchTransformer()) as PendingSettings<T>;
|
||||
} else {
|
||||
// Simple single-endpoint fetch
|
||||
const response = await apiClient.get(`/api/v1/admin/settings/section/${sectionName}`);
|
||||
rawData = response.data || {};
|
||||
rawData = (response.data || {}) as PendingSettings<T>;
|
||||
}
|
||||
|
||||
console.log(`[useAdminSettings:${sectionName}] Raw response:`, JSON.stringify(rawData, null, 2));
|
||||
@ -77,7 +81,7 @@ export function useAdminSettings<T = any>(
|
||||
console.log(`[useAdminSettings:${sectionName}] Original active settings:`, JSON.stringify(activeOnly, null, 2));
|
||||
|
||||
// Merge pending changes into settings for display
|
||||
const mergedSettings = mergePendingSettings(rawData);
|
||||
const mergedSettings = mergePendingSettings(rawData) as unknown as CombinedSettings<T>;
|
||||
console.log(`[useAdminSettings:${sectionName}] Merged settings:`, JSON.stringify(mergedSettings, null, 2));
|
||||
|
||||
setSettings(mergedSettings as T);
|
||||
@ -94,7 +98,10 @@ export function useAdminSettings<T = any>(
|
||||
setSaving(true);
|
||||
|
||||
// Compute delta: only include fields that changed from original
|
||||
const delta = computeDelta(originalSettings, settings);
|
||||
const delta = computeDelta(
|
||||
originalSettings as SettingsRecord,
|
||||
settings as SettingsRecord
|
||||
);
|
||||
console.log(`[useAdminSettings:${sectionName}] Delta (changed fields):`, JSON.stringify(delta, null, 2));
|
||||
|
||||
if (Object.keys(delta).length === 0) {
|
||||
@ -107,7 +114,10 @@ export function useAdminSettings<T = any>(
|
||||
const { sectionData, deltaSettings } = saveTransformer(settings);
|
||||
|
||||
// Save section data (with delta applied)
|
||||
const sectionDelta = computeDelta(originalSettings, sectionData);
|
||||
const sectionDelta = computeDelta(
|
||||
originalSettings as SettingsRecord,
|
||||
sectionData as unknown as SettingsRecord
|
||||
);
|
||||
if (Object.keys(sectionDelta).length > 0) {
|
||||
await apiClient.put(`/api/v1/admin/settings/section/${sectionName}`, sectionDelta);
|
||||
}
|
||||
@ -148,8 +158,8 @@ export function useAdminSettings<T = any>(
|
||||
* Compute delta between original and current settings.
|
||||
* Returns only fields that have changed.
|
||||
*/
|
||||
function computeDelta(original: any, current: any): any {
|
||||
const delta: any = {};
|
||||
function computeDelta(original: SettingsRecord, current: SettingsRecord): SettingsRecord {
|
||||
const delta: SettingsRecord = {};
|
||||
|
||||
for (const key in current) {
|
||||
if (!Object.prototype.hasOwnProperty.call(current, key)) continue;
|
||||
@ -182,7 +192,7 @@ function computeDelta(original: any, current: any): any {
|
||||
/**
|
||||
* Check if value is a plain object (not array, not null, not Date, etc.)
|
||||
*/
|
||||
function isPlainObject(value: any): boolean {
|
||||
function isPlainObject(value: unknown): value is SettingsRecord {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
|
||||
@ -4,14 +4,14 @@
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
type ToolParameterValues = Record<string, any>;
|
||||
type ToolParameterValues = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Register tool parameters and get current values
|
||||
*/
|
||||
export function useToolParameters(
|
||||
_toolName: string,
|
||||
_parameters: Record<string, any>
|
||||
_parameters: Record<string, unknown>
|
||||
): [ToolParameterValues, (updates: Partial<ToolParameterValues>) => void] {
|
||||
|
||||
// Return empty values and noop updater
|
||||
@ -24,10 +24,10 @@ export function useToolParameters(
|
||||
/**
|
||||
* Hook for managing a single tool parameter
|
||||
*/
|
||||
export function useToolParameter<T = any>(
|
||||
export function useToolParameter<T = unknown>(
|
||||
toolName: string,
|
||||
paramName: string,
|
||||
definition: any
|
||||
definition: unknown
|
||||
): [T, (value: T) => void] {
|
||||
const [allParams, updateParams] = useToolParameters(toolName, { [paramName]: definition });
|
||||
|
||||
|
||||
@ -4,23 +4,35 @@ export const FILE_EVENTS = {
|
||||
|
||||
const UUID_REGEX = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
|
||||
|
||||
export function tryParseJson<T = any>(input: unknown): T | undefined {
|
||||
export function tryParseJson<T = unknown>(input: unknown): T | undefined {
|
||||
if (typeof input !== 'string') return input as T | undefined;
|
||||
try { return JSON.parse(input) as T; } catch { return undefined; }
|
||||
}
|
||||
|
||||
export async function normalizeAxiosErrorData(data: any): Promise<any> {
|
||||
type TextProducer = { text: () => Promise<string> };
|
||||
|
||||
function hasTextMethod(value: unknown): value is TextProducer {
|
||||
return typeof value === 'object' && value !== null && 'text' in value && typeof (value as { text?: unknown }).text === 'function';
|
||||
}
|
||||
|
||||
export async function normalizeAxiosErrorData(data: unknown): Promise<unknown> {
|
||||
if (!data) return undefined;
|
||||
if (typeof data?.text === 'function') {
|
||||
if (hasTextMethod(data)) {
|
||||
const text = await data.text();
|
||||
return tryParseJson(text) ?? text;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function extractErrorFileIds(payload: any): string[] | undefined {
|
||||
function hasErrorFileIds(payload: unknown): payload is { errorFileIds?: unknown } {
|
||||
return typeof payload === 'object' && payload !== null && 'errorFileIds' in payload;
|
||||
}
|
||||
|
||||
export function extractErrorFileIds(payload: unknown): string[] | undefined {
|
||||
if (!payload) return undefined;
|
||||
if (Array.isArray(payload?.errorFileIds)) return payload.errorFileIds as string[];
|
||||
if (hasErrorFileIds(payload) && Array.isArray(payload.errorFileIds)) {
|
||||
return payload.errorFileIds.filter((value): value is string => typeof value === 'string');
|
||||
}
|
||||
if (typeof payload === 'string') {
|
||||
const matches = payload.match(UUID_REGEX);
|
||||
if (matches && matches.length > 0) return Array.from(new Set(matches));
|
||||
@ -33,15 +45,22 @@ export function broadcastErroredFiles(fileIds: string[]) {
|
||||
window.dispatchEvent(new CustomEvent(FILE_EVENTS.markError, { detail: { fileIds } }));
|
||||
}
|
||||
|
||||
type Sized = { size?: number };
|
||||
|
||||
function getFileSize(file: Sized | File | null | undefined): number | undefined {
|
||||
if (!file) return undefined;
|
||||
if (file instanceof File) return file.size;
|
||||
const { size } = file;
|
||||
return typeof size === 'number' ? size : undefined;
|
||||
}
|
||||
|
||||
export function isZeroByte(file: File | { size?: number } | null | undefined): boolean {
|
||||
if (!file) return true;
|
||||
const size = (file as any).size;
|
||||
const size = getFileSize(file);
|
||||
return typeof size === 'number' ? size <= 0 : true;
|
||||
}
|
||||
|
||||
export function isEmptyOutput(files: File[] | null | undefined): boolean {
|
||||
if (!files || files.length === 0) return true;
|
||||
return files.every(f => (f as any)?.size === 0);
|
||||
return files.every(file => getFileSize(file) === 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -19,6 +19,29 @@ interface PickerOptions {
|
||||
mimeTypes?: string | null;
|
||||
}
|
||||
|
||||
type GoogleTokenResponse = {
|
||||
access_token?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type GoogleTokenClient = {
|
||||
callback: (response: GoogleTokenResponse) => void;
|
||||
requestAccessToken: (options?: { prompt?: string }) => void;
|
||||
};
|
||||
|
||||
type PickerResponseObject = google.picker.ResponseObject;
|
||||
|
||||
interface PickerDocument {
|
||||
name?: string;
|
||||
mimeType?: string;
|
||||
lastModified?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
type DriveFileResponse = {
|
||||
body: string;
|
||||
};
|
||||
|
||||
// Expandable mime types for Google Picker
|
||||
const expandableMimeTypes: Record<string, string[]> = {
|
||||
'image/*': ['image/jpeg', 'image/png', 'image/svg+xml'],
|
||||
@ -51,7 +74,7 @@ function fileInputToGooglePickerMimeTypes(accept?: string): string | null {
|
||||
|
||||
class GoogleDrivePickerService {
|
||||
private config: GoogleDriveConfig | null = null;
|
||||
private tokenClient: any = null;
|
||||
private tokenClient: GoogleTokenClient | null = null;
|
||||
private accessToken: string | null = null;
|
||||
private gapiLoaded = false;
|
||||
private gisLoaded = false;
|
||||
@ -112,7 +135,7 @@ class GoogleDrivePickerService {
|
||||
client_id: this.config.clientId,
|
||||
scope: SCOPES,
|
||||
callback: () => {}, // Will be overridden during picker creation
|
||||
});
|
||||
}) as GoogleTokenClient;
|
||||
|
||||
this.gisLoaded = true;
|
||||
}
|
||||
@ -142,13 +165,14 @@ class GoogleDrivePickerService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tokenClient.callback = (response: any) => {
|
||||
if (response.error !== undefined) {
|
||||
this.tokenClient.callback = (response: GoogleTokenResponse) => {
|
||||
if (typeof response.error === 'string') {
|
||||
reject(new Error(response.error));
|
||||
return;
|
||||
}
|
||||
if(response.access_token == null){
|
||||
reject(new Error("No acces token in response"));
|
||||
if (!response.access_token) {
|
||||
reject(new Error('No access token in response'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.accessToken = response.access_token;
|
||||
@ -192,7 +216,9 @@ class GoogleDrivePickerService {
|
||||
.setOAuthToken(this.accessToken)
|
||||
.addView(view1)
|
||||
.addView(view2)
|
||||
.setCallback((data: any) => this.pickerCallback(data, resolve, reject));
|
||||
.setCallback((data: PickerResponseObject) => {
|
||||
void this.pickerCallback(data, resolve, reject);
|
||||
});
|
||||
|
||||
if (options.multiple) {
|
||||
builder.enableFeature(window.google.picker.Feature.MULTISELECT_ENABLED);
|
||||
@ -207,30 +233,55 @@ class GoogleDrivePickerService {
|
||||
* Handle picker selection callback
|
||||
*/
|
||||
private async pickerCallback(
|
||||
data: any,
|
||||
data: PickerResponseObject,
|
||||
resolve: (files: File[]) => void,
|
||||
reject: (error: Error) => void
|
||||
): Promise<void> {
|
||||
if (data.action === window.google.picker.Action.PICKED) {
|
||||
try {
|
||||
const documentKey = window.google.picker.Response.DOCUMENTS;
|
||||
const responseData = data as unknown as Record<string, unknown>;
|
||||
const documents = responseData[documentKey];
|
||||
if (!Array.isArray(documents)) {
|
||||
reject(new Error('Picker response missing documents'));
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await Promise.all(
|
||||
data[window.google.picker.Response.DOCUMENTS].map(async (pickedFile: any) => {
|
||||
const fileId = pickedFile[window.google.picker.Document.ID];
|
||||
(documents as PickerDocument[]).map(async (pickedFile) => {
|
||||
const record = pickedFile as PickerDocument;
|
||||
const fileIdValue = record[window.google.picker.Document.ID];
|
||||
if (typeof fileIdValue !== 'string') {
|
||||
throw new Error('Invalid Google Drive file identifier');
|
||||
}
|
||||
|
||||
const res = await window.gapi.client.drive.files.get({
|
||||
fileId: fileId,
|
||||
fileId: fileIdValue,
|
||||
alt: 'media',
|
||||
});
|
||||
const driveResponse = res as DriveFileResponse;
|
||||
if (typeof driveResponse.body !== 'string') {
|
||||
throw new Error('Unexpected Google Drive file response');
|
||||
}
|
||||
|
||||
// Convert response body to File object
|
||||
const file = new File(
|
||||
[new Uint8Array(res.body.length).map((_: any, i: number) => res.body.charCodeAt(i))],
|
||||
pickedFile.name,
|
||||
const buffer = new Uint8Array(driveResponse.body.length);
|
||||
for (let i = 0; i < driveResponse.body.length; i += 1) {
|
||||
buffer[i] = driveResponse.body.charCodeAt(i);
|
||||
}
|
||||
|
||||
const nameValue = record.name;
|
||||
const mimeTypeValue = record.mimeType;
|
||||
const lastModifiedValue = record.lastModified;
|
||||
|
||||
return new File(
|
||||
[buffer],
|
||||
typeof nameValue === 'string' ? nameValue : 'drive-file',
|
||||
{
|
||||
type: pickedFile.mimeType,
|
||||
lastModified: pickedFile.lastModified,
|
||||
type: typeof mimeTypeValue === 'string' ? mimeTypeValue : 'application/octet-stream',
|
||||
lastModified: typeof lastModifiedValue === 'number' ? lastModifiedValue : Date.now(),
|
||||
}
|
||||
);
|
||||
return file;
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
// frontend/src/services/httpErrorHandler.ts
|
||||
import axios from 'axios';
|
||||
import type { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
import { alert } from '@app/components/toast';
|
||||
import { broadcastErroredFiles, extractErrorFileIds, normalizeAxiosErrorData } from '@app/services/errorUtils';
|
||||
import { showSpecialErrorToast } from '@app/services/specialErrorToasts';
|
||||
@ -29,33 +30,59 @@ function titleForStatus(status?: number): string {
|
||||
return 'Request failed';
|
||||
}
|
||||
|
||||
function extractAxiosErrorMessage(error: any): { title: string; body: string } {
|
||||
if (axios.isAxiosError(error)) {
|
||||
type AxiosErrorData = {
|
||||
message?: string;
|
||||
errorFileIds?: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
function isErrorWithMessage(error: unknown): error is { message: string } {
|
||||
return typeof error === 'object' && error !== null && 'message' in error && typeof (error as { message?: unknown }).message === 'string';
|
||||
}
|
||||
|
||||
function extractAxiosErrorMessage(error: unknown): { title: string; body: string } {
|
||||
if (axios.isAxiosError<AxiosErrorData | string | undefined>(error)) {
|
||||
const status = error.response?.status;
|
||||
const _statusText = error.response?.statusText || '';
|
||||
let parsed: any = undefined;
|
||||
const raw = error.response?.data;
|
||||
let parsed: AxiosErrorData | string | undefined;
|
||||
|
||||
if (typeof raw === 'string') {
|
||||
try { parsed = JSON.parse(raw); } catch { /* keep as string */ }
|
||||
try {
|
||||
parsed = JSON.parse(raw) as AxiosErrorData;
|
||||
} catch {
|
||||
parsed = raw;
|
||||
}
|
||||
} else {
|
||||
parsed = raw;
|
||||
parsed = raw as AxiosErrorData | undefined;
|
||||
}
|
||||
|
||||
const extractIds = (): string[] | undefined => {
|
||||
if (Array.isArray(parsed?.errorFileIds)) return parsed.errorFileIds as string[];
|
||||
const rawText = typeof raw === 'string' ? raw : '';
|
||||
const uuidMatches = rawText.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g);
|
||||
return uuidMatches && uuidMatches.length > 0 ? Array.from(new Set(uuidMatches)) : undefined;
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
const idsCandidate = (parsed as AxiosErrorData).errorFileIds;
|
||||
if (Array.isArray(idsCandidate)) {
|
||||
return idsCandidate.filter((id): id is string => typeof id === 'string');
|
||||
}
|
||||
}
|
||||
if (typeof raw === 'string') {
|
||||
const uuidMatches = raw.match(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g);
|
||||
return uuidMatches && uuidMatches.length > 0 ? Array.from(new Set(uuidMatches)) : undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const body = ((): string => {
|
||||
const data = parsed;
|
||||
if (!data) return typeof raw === 'string' ? raw : '';
|
||||
if (!parsed) return typeof raw === 'string' ? raw : '';
|
||||
if (typeof parsed === 'string') return parsed;
|
||||
const ids = extractIds();
|
||||
if (ids && ids.length > 0) return `Failed files: ${ids.join(', ')}`;
|
||||
if (data?.message) return data.message as string;
|
||||
if (typeof raw === 'string') return raw;
|
||||
try { return JSON.stringify(data); } catch { return ''; }
|
||||
if (parsed.message) return parsed.message;
|
||||
try {
|
||||
return JSON.stringify(parsed);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
|
||||
const ids = extractIds();
|
||||
const title = titleForStatus(status);
|
||||
if (ids && ids.length > 0) {
|
||||
@ -69,12 +96,16 @@ function extractAxiosErrorMessage(error: any): { title: string; body: string } {
|
||||
const bodyMsg = isUnhelpfulMessage(body) ? FRIENDLY_FALLBACK : body;
|
||||
return { title, body: bodyMsg };
|
||||
}
|
||||
|
||||
try {
|
||||
const msg = (error?.message || String(error)) as string;
|
||||
return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg };
|
||||
} catch (e) {
|
||||
// ignore extraction errors
|
||||
console.debug('extractAxiosErrorMessage', e);
|
||||
if (isErrorWithMessage(error)) {
|
||||
const msg = error.message;
|
||||
return { title: 'Network error', body: isUnhelpfulMessage(msg) ? FRIENDLY_FALLBACK : msg };
|
||||
}
|
||||
const fallbackMessage = String(error);
|
||||
return { title: 'Network error', body: isUnhelpfulMessage(fallbackMessage) ? FRIENDLY_FALLBACK : fallbackMessage };
|
||||
} catch (parseError) {
|
||||
console.debug('extractAxiosErrorMessage', parseError);
|
||||
return { title: 'Network error', body: FRIENDLY_FALLBACK };
|
||||
}
|
||||
}
|
||||
@ -87,16 +118,35 @@ const SPECIAL_SUPPRESS_MS = 1500; // brief window to suppress generic duplicate
|
||||
* Handles HTTP errors with toast notifications and file error broadcasting
|
||||
* Returns true if the error should be suppressed (deduplicated), false otherwise
|
||||
*/
|
||||
export async function handleHttpError(error: any): Promise<boolean> {
|
||||
type ErrorWithConfig = {
|
||||
config?: (AxiosRequestConfig & { suppressErrorToast?: boolean }) | null;
|
||||
response?: {
|
||||
status?: number;
|
||||
data?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
function toAxiosError(error: unknown): AxiosError<unknown> | undefined {
|
||||
return axios.isAxiosError(error) ? error : undefined;
|
||||
}
|
||||
|
||||
export async function handleHttpError(error: unknown): Promise<boolean> {
|
||||
const axiosError = toAxiosError(error);
|
||||
const errorWithConfig = (axiosError ?? (typeof error === 'object' && error !== null ? (error as ErrorWithConfig) : undefined));
|
||||
|
||||
// Check if this error should skip the global toast (component will handle it)
|
||||
if (error?.config?.suppressErrorToast === true) {
|
||||
const suppressToast =
|
||||
!!errorWithConfig?.config &&
|
||||
typeof errorWithConfig.config === 'object' &&
|
||||
(errorWithConfig.config as { suppressErrorToast?: boolean }).suppressErrorToast === true;
|
||||
if (suppressToast) {
|
||||
return false; // Don't show global toast, but continue rejection
|
||||
}
|
||||
// Compute title/body (friendly) from the error object
|
||||
const { title, body } = extractAxiosErrorMessage(error);
|
||||
|
||||
// Normalize response data ONCE, reuse for both ID extraction and special-toast matching
|
||||
const raw = (error?.response?.data) as any;
|
||||
const raw = errorWithConfig?.response?.data;
|
||||
let normalized: unknown = raw;
|
||||
try { normalized = await normalizeAxiosErrorData(raw); } catch (e) { console.debug('normalizeAxiosErrorData', e); }
|
||||
|
||||
@ -111,8 +161,11 @@ export async function handleHttpError(error: any): Promise<boolean> {
|
||||
}
|
||||
|
||||
// 2) Generic-vs-special dedupe by endpoint
|
||||
const url: string | undefined = error?.config?.url;
|
||||
const status: number | undefined = error?.response?.status;
|
||||
const url =
|
||||
errorWithConfig?.config && typeof errorWithConfig.config === 'object'
|
||||
? (errorWithConfig.config as AxiosRequestConfig).url
|
||||
: undefined;
|
||||
const status: number | undefined = errorWithConfig?.response?.status;
|
||||
const now = Date.now();
|
||||
const isSpecial =
|
||||
status === 422 ||
|
||||
@ -148,4 +201,4 @@ export async function handleHttpError(error: any): Promise<boolean> {
|
||||
}
|
||||
|
||||
return false; // Error was handled with toast, continue normal rejection
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { GlobalWorkerOptions, getDocument, PDFDocumentProxy } from 'pdfjs-dist/legacy/build/pdf.mjs';
|
||||
import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api';
|
||||
|
||||
class PDFWorkerManager {
|
||||
private static instance: PDFWorkerManager;
|
||||
@ -57,32 +58,25 @@ class PDFWorkerManager {
|
||||
}
|
||||
|
||||
// Normalize input data to PDF.js format
|
||||
let pdfData: any;
|
||||
let params: DocumentInitParameters;
|
||||
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
||||
pdfData = { data };
|
||||
params = { data };
|
||||
} else if (typeof data === 'string') {
|
||||
pdfData = data; // URL string
|
||||
params = { url: data };
|
||||
} else if (data && typeof data === 'object' && 'data' in data) {
|
||||
pdfData = data; // Already in {data: ArrayBuffer} format
|
||||
const buffer = (data as { data: ArrayBuffer }).data;
|
||||
params = { data: buffer };
|
||||
} else {
|
||||
pdfData = data; // Pass through as-is
|
||||
params = (data as DocumentInitParameters) ?? {};
|
||||
}
|
||||
|
||||
const loadingTask = getDocument(
|
||||
typeof pdfData === 'string' ? {
|
||||
url: pdfData,
|
||||
disableAutoFetch: options.disableAutoFetch ?? true,
|
||||
disableStream: options.disableStream ?? true,
|
||||
stopAtErrors: options.stopAtErrors ?? false,
|
||||
verbosity: options.verbosity ?? 0
|
||||
} : {
|
||||
...pdfData,
|
||||
disableAutoFetch: options.disableAutoFetch ?? true,
|
||||
disableStream: options.disableStream ?? true,
|
||||
stopAtErrors: options.stopAtErrors ?? false,
|
||||
verbosity: options.verbosity ?? 0
|
||||
}
|
||||
);
|
||||
const loadingTask = getDocument({
|
||||
...params,
|
||||
disableAutoFetch: options.disableAutoFetch ?? true,
|
||||
disableStream: options.disableStream ?? true,
|
||||
stopAtErrors: options.stopAtErrors ?? false,
|
||||
verbosity: options.verbosity ?? 0
|
||||
});
|
||||
|
||||
try {
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
@ -4,10 +4,25 @@
|
||||
* without needing to make API calls
|
||||
*/
|
||||
|
||||
// PDF.js types (simplified)
|
||||
import React from 'react';
|
||||
import type {
|
||||
DocumentInitParameters,
|
||||
PDFDocumentLoadingTask,
|
||||
PDFDocumentProxy,
|
||||
PDFPageProxy
|
||||
} from 'pdfjs-dist/types/src/display/api';
|
||||
|
||||
type PdfMetadataResult = Awaited<ReturnType<PDFDocumentProxy['getMetadata']>>;
|
||||
type PdfMetadataInfo = PdfMetadataResult['info'];
|
||||
type PdfMetadata = PdfMetadataResult['metadata'];
|
||||
|
||||
interface PdfjsLib {
|
||||
getDocument: (src?: string | URL | Uint8Array | ArrayBuffer | DocumentInitParameters) => PDFDocumentLoadingTask;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
pdfjsLib?: any;
|
||||
pdfjsLib?: PdfjsLib;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,17 +54,18 @@ const detectSignaturesInFile = async (file: File): Promise<SignatureDetectionRes
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
// Load the PDF document
|
||||
const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
const loadingTask = window.pdfjsLib.getDocument({ data: arrayBuffer });
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
let totalSignatures = 0;
|
||||
|
||||
// Check each page for signature annotations
|
||||
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
|
||||
const page = await pdf.getPage(pageNum);
|
||||
const page: PDFPageProxy = await pdf.getPage(pageNum);
|
||||
const annotations = await page.getAnnotations();
|
||||
|
||||
// Count signature annotations (Type: /Sig)
|
||||
const signatureAnnotations = annotations.filter((annotation: any) =>
|
||||
const signatureAnnotations = annotations.filter(annotation =>
|
||||
annotation.subtype === 'Widget' &&
|
||||
annotation.fieldType === 'Sig'
|
||||
);
|
||||
@ -59,7 +75,20 @@ const detectSignaturesInFile = async (file: File): Promise<SignatureDetectionRes
|
||||
|
||||
// Also check for document-level signatures in AcroForm
|
||||
const metadata = await pdf.getMetadata();
|
||||
if (metadata?.info?.Signature || metadata?.metadata?.has('dc:signature')) {
|
||||
const info: PdfMetadataInfo | undefined = metadata?.info;
|
||||
const meta: PdfMetadata | undefined = metadata?.metadata;
|
||||
const hasInfoSignature =
|
||||
typeof info === 'object' &&
|
||||
info !== null &&
|
||||
'Signature' in info &&
|
||||
Boolean((info as { Signature?: unknown }).Signature);
|
||||
const hasMetadataSignature =
|
||||
typeof meta === 'object' &&
|
||||
meta !== null &&
|
||||
'has' in meta &&
|
||||
typeof (meta as { has: (key: string) => boolean }).has === 'function' &&
|
||||
(meta as { has: (key: string) => boolean }).has('dc:signature');
|
||||
if (hasInfoSignature || hasMetadataSignature) {
|
||||
totalSignatures = Math.max(totalSignatures, 1);
|
||||
}
|
||||
|
||||
@ -135,6 +164,3 @@ export const useSignatureDetection = () => {
|
||||
reset: () => setDetectionResults([])
|
||||
};
|
||||
};
|
||||
|
||||
// Import React for the hook
|
||||
import React from 'react';
|
||||
@ -40,8 +40,10 @@ export function showSpecialErrorToast(rawError: string | undefined, options?: {
|
||||
// Best-effort translation without hard dependency on i18n config
|
||||
let body = mapping.defaultMessage;
|
||||
try {
|
||||
const anyGlobal: any = (globalThis as any);
|
||||
const i18next = anyGlobal?.i18next;
|
||||
const globalWithI18n = globalThis as typeof globalThis & {
|
||||
i18next?: { t?: (key: string, options: { defaultValue: string }) => string };
|
||||
};
|
||||
const i18next = globalWithI18n?.i18next;
|
||||
if (i18next && typeof i18next.t === 'function') {
|
||||
body = i18next.t(mapping.i18nKey, { defaultValue: mapping.defaultMessage });
|
||||
}
|
||||
@ -54,4 +56,3 @@ export function showSpecialErrorToast(rawError: string | undefined, options?: {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
/**
|
||||
* Types for automation functionality
|
||||
*/
|
||||
|
||||
export type AutomationParameters = Record<string, unknown>;
|
||||
|
||||
export interface AutomationOperation {
|
||||
operation: string;
|
||||
parameters: Record<string, any>;
|
||||
parameters: AutomationParameters;
|
||||
}
|
||||
|
||||
export interface AutomationConfig {
|
||||
@ -22,7 +26,7 @@ export interface AutomationTool {
|
||||
operation: string;
|
||||
name: string;
|
||||
configured: boolean;
|
||||
parameters?: Record<string, any>;
|
||||
parameters?: AutomationParameters;
|
||||
}
|
||||
|
||||
export type AutomationStep = typeof import('@app/constants/automation').AUTOMATION_STEPS[keyof typeof import('@app/constants/automation').AUTOMATION_STEPS];
|
||||
@ -57,12 +61,6 @@ export enum AutomationMode {
|
||||
SUGGESTED = 'suggested'
|
||||
}
|
||||
|
||||
export interface SuggestedAutomation {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
operations: AutomationOperation[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
icon: any; // MUI Icon component
|
||||
export interface SuggestedAutomation extends AutomationConfig {
|
||||
iconComponent?: ComponentType;
|
||||
}
|
||||
|
||||
@ -1,31 +1,75 @@
|
||||
import axios from 'axios';
|
||||
import type { ToolOperationConfig, SingleFileToolOperationConfig, MultiFileToolOperationConfig } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import { ToolRegistry } from '@app/data/toolsTaxonomy';
|
||||
import { ToolId } from '@app/types/toolId';
|
||||
import { AUTOMATION_CONSTANTS } from '@app/constants/automation';
|
||||
import { AutomationFileProcessor } from '@app/utils/automationFileProcessor';
|
||||
import { AutomationFileProcessor, type AutomationFormParameters } from '@app/utils/automationFileProcessor';
|
||||
import { ToolType } from '@app/hooks/tools/shared/useToolOperation';
|
||||
import { processResponse } from '@app/utils/toolResponseProcessor';
|
||||
import type { AutomationConfig, AutomationParameters } from '@app/types/automation';
|
||||
|
||||
const getHeaderValue = (headers: unknown, key: string): string | undefined => {
|
||||
if (!headers || typeof headers !== 'object') return undefined;
|
||||
const value = (headers as Record<string, unknown>)[key];
|
||||
if (typeof value === 'string') return value;
|
||||
if (Array.isArray(value)) {
|
||||
return value.find((entry): entry is string => typeof entry === 'string' && entry.length > 0);
|
||||
}
|
||||
if (value && typeof value === 'object' && 'toString' in value) {
|
||||
const stringValue = String(value);
|
||||
return stringValue.length > 0 ? stringValue : undefined;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const isPdfResponse = (headers: unknown): boolean => {
|
||||
const contentType = getHeaderValue(headers, 'content-type');
|
||||
return typeof contentType === 'string' && contentType.includes('application/pdf');
|
||||
};
|
||||
|
||||
const formatErrorMessage = (error: unknown): string => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const responseData = error.response?.data;
|
||||
if (typeof responseData === 'string') {
|
||||
return responseData;
|
||||
}
|
||||
if (responseData && typeof responseData === 'object' && 'message' in responseData) {
|
||||
const message = (responseData as { message?: unknown }).message;
|
||||
if (typeof message === 'string') return message;
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
};
|
||||
|
||||
/**
|
||||
* Process multi-file tool response (handles ZIP or single PDF responses)
|
||||
*/
|
||||
const processMultiFileResponse = async (
|
||||
responseData: Blob,
|
||||
responseHeaders: any,
|
||||
responseHeaders: unknown,
|
||||
files: File[],
|
||||
filePrefix: string,
|
||||
preserveBackendFilename?: boolean
|
||||
): Promise<File[]> => {
|
||||
// Multi-file responses are typically ZIP files, but may be single files (e.g. split with merge=true)
|
||||
if (responseData.type === 'application/pdf' ||
|
||||
(responseHeaders && responseHeaders['content-type'] === 'application/pdf')) {
|
||||
isPdfResponse(responseHeaders)) {
|
||||
// Single PDF response - use processResponse to respect preserveBackendFilename
|
||||
const normalizedHeaders =
|
||||
typeof responseHeaders === 'object' && responseHeaders !== null
|
||||
? (responseHeaders as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
const processedFiles = await processResponse(
|
||||
responseData,
|
||||
files,
|
||||
filePrefix,
|
||||
undefined,
|
||||
preserveBackendFilename ? responseHeaders : undefined
|
||||
preserveBackendFilename ? normalizedHeaders : undefined
|
||||
);
|
||||
return processedFiles;
|
||||
} else {
|
||||
@ -75,9 +119,9 @@ const executeApiRequest = async (
|
||||
/**
|
||||
* Execute single-file tool operation (processes files one at a time)
|
||||
*/
|
||||
const executeSingleFileOperation = async (
|
||||
config: any,
|
||||
parameters: any,
|
||||
const executeSingleFileOperation = async <TParams extends AutomationFormParameters>(
|
||||
config: SingleFileToolOperationConfig<TParams>,
|
||||
parameters: TParams,
|
||||
files: File[],
|
||||
filePrefix: string
|
||||
): Promise<File[]> => {
|
||||
@ -88,7 +132,7 @@ const executeSingleFileOperation = async (
|
||||
? config.endpoint(parameters)
|
||||
: config.endpoint;
|
||||
|
||||
const formData = (config.buildFormData as (params: any, file: File) => FormData)(parameters, file);
|
||||
const formData = config.buildFormData(parameters, file);
|
||||
|
||||
const processedFiles = await executeApiRequest(
|
||||
endpoint,
|
||||
@ -106,9 +150,9 @@ const executeSingleFileOperation = async (
|
||||
/**
|
||||
* Execute multi-file tool operation (processes all files in one request)
|
||||
*/
|
||||
const executeMultiFileOperation = async (
|
||||
config: any,
|
||||
parameters: any,
|
||||
const executeMultiFileOperation = async <TParams extends AutomationFormParameters>(
|
||||
config: MultiFileToolOperationConfig<TParams>,
|
||||
parameters: TParams,
|
||||
files: File[],
|
||||
filePrefix: string
|
||||
): Promise<File[]> => {
|
||||
@ -116,7 +160,7 @@ const executeMultiFileOperation = async (
|
||||
? config.endpoint(parameters)
|
||||
: config.endpoint;
|
||||
|
||||
const formData = (config.buildFormData as (params: any, files: File[]) => FormData)(parameters, files);
|
||||
const formData = config.buildFormData(parameters, files);
|
||||
|
||||
return await executeApiRequest(
|
||||
endpoint,
|
||||
@ -131,9 +175,12 @@ const executeMultiFileOperation = async (
|
||||
/**
|
||||
* Execute a tool operation directly without using React hooks
|
||||
*/
|
||||
const toFormParameters = (parameters: AutomationParameters): AutomationFormParameters =>
|
||||
parameters as AutomationFormParameters;
|
||||
|
||||
export const executeToolOperation = async (
|
||||
operationName: string,
|
||||
parameters: any,
|
||||
parameters: AutomationParameters,
|
||||
files: File[],
|
||||
toolRegistry: ToolRegistry
|
||||
): Promise<File[]> => {
|
||||
@ -145,12 +192,12 @@ export const executeToolOperation = async (
|
||||
*/
|
||||
export const executeToolOperationWithPrefix = async (
|
||||
operationName: string,
|
||||
parameters: any,
|
||||
parameters: AutomationParameters,
|
||||
files: File[],
|
||||
toolRegistry: ToolRegistry,
|
||||
filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX
|
||||
): Promise<File[]> => {
|
||||
const config = toolRegistry[operationName as ToolId]?.operationConfig;
|
||||
const config = toolRegistry[operationName as ToolId]?.operationConfig as ToolOperationConfig<AutomationFormParameters> | undefined;
|
||||
if (!config) {
|
||||
throw new Error(`Tool operation not supported: ${operationName}`);
|
||||
}
|
||||
@ -162,16 +209,18 @@ export const executeToolOperationWithPrefix = async (
|
||||
return resultFiles;
|
||||
}
|
||||
|
||||
const formParameters = toFormParameters(parameters);
|
||||
|
||||
// Execute based on tool type
|
||||
if (config.toolType === ToolType.multiFile) {
|
||||
return await executeMultiFileOperation(config, parameters, files, filePrefix);
|
||||
return await executeMultiFileOperation(config, formParameters, files, filePrefix);
|
||||
} else {
|
||||
return await executeSingleFileOperation(config, parameters, files, filePrefix);
|
||||
return await executeSingleFileOperation(config, formParameters, files, filePrefix);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(`❌ ${operationName} failed:`, error);
|
||||
throw new Error(`${operationName} operation failed: ${error.response?.data || error.message}`);
|
||||
throw new Error(`${operationName} operation failed: ${formatErrorMessage(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -179,7 +228,7 @@ export const executeToolOperationWithPrefix = async (
|
||||
* Execute an entire automation sequence
|
||||
*/
|
||||
export const executeAutomationSequence = async (
|
||||
automation: any,
|
||||
automation: AutomationConfig,
|
||||
initialFiles: File[],
|
||||
toolRegistry: ToolRegistry,
|
||||
onStepStart?: (stepIndex: number, operationName: string) => void,
|
||||
@ -205,9 +254,11 @@ export const executeAutomationSequence = async (
|
||||
try {
|
||||
onStepStart?.(i, operation.operation);
|
||||
|
||||
const operationParameters: AutomationParameters = operation.parameters ?? {};
|
||||
|
||||
const resultFiles = await executeToolOperationWithPrefix(
|
||||
operation.operation,
|
||||
operation.parameters || {},
|
||||
operationParameters,
|
||||
currentFiles,
|
||||
toolRegistry,
|
||||
i === automation.operations.length - 1 ? automationPrefix : '' // Only add prefix to final step
|
||||
@ -217,10 +268,11 @@ export const executeAutomationSequence = async (
|
||||
currentFiles = resultFiles;
|
||||
onStepComplete?.(i, resultFiles);
|
||||
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error(`❌ Step ${i + 1} failed:`, error);
|
||||
onStepError?.(i, error.message);
|
||||
throw error;
|
||||
const message = formatErrorMessage(error);
|
||||
onStepError?.(i, message);
|
||||
throw error instanceof Error ? error : new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,8 @@ export interface AutomationProcessingResult {
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export type AutomationFormParameters = Record<string, unknown>;
|
||||
|
||||
export class AutomationFileProcessor {
|
||||
/**
|
||||
* Check if a blob is a ZIP file by examining its header
|
||||
@ -121,11 +123,11 @@ export class AutomationFileProcessor {
|
||||
files: [resultFile],
|
||||
errors: []
|
||||
};
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
files: [],
|
||||
errors: [`Automation step failed: ${error.response?.data || error.message}`]
|
||||
errors: [`Automation step failed: ${AutomationFileProcessor.formatError(error)}`]
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -154,11 +156,11 @@ export class AutomationFileProcessor {
|
||||
|
||||
// Multi-file responses are typically ZIP files
|
||||
return await this.extractAutomationZipFiles(response.data);
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
files: [],
|
||||
errors: [`Automation step failed: ${error.response?.data || error.message}`]
|
||||
errors: [`Automation step failed: ${AutomationFileProcessor.formatError(error)}`]
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -167,7 +169,7 @@ export class AutomationFileProcessor {
|
||||
* Build form data for automation tool operations
|
||||
*/
|
||||
static buildAutomationFormData(
|
||||
parameters: Record<string, any>,
|
||||
parameters: AutomationFormParameters,
|
||||
files: File | File[],
|
||||
fileFieldName: string = 'fileInput'
|
||||
): FormData {
|
||||
@ -183,12 +185,51 @@ export class AutomationFileProcessor {
|
||||
// Add parameters
|
||||
Object.entries(parameters).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => formData.append(key, item));
|
||||
value.forEach(item => {
|
||||
if (item !== undefined && item !== null) {
|
||||
formData.append(key, AutomationFileProcessor.normalizeFormValue(item));
|
||||
}
|
||||
});
|
||||
} else if (value !== undefined && value !== null) {
|
||||
formData.append(key, value);
|
||||
formData.append(key, AutomationFileProcessor.normalizeFormValue(value));
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
private static normalizeFormValue(value: unknown): string | Blob {
|
||||
if (value instanceof Blob || value instanceof File) {
|
||||
return value;
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
private static formatError(error: unknown): string {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const responseData = error.response?.data;
|
||||
if (typeof responseData === 'string') return responseData;
|
||||
if (responseData && typeof responseData === 'object' && 'message' in responseData) {
|
||||
const message = (responseData as { message?: unknown }).message;
|
||||
if (typeof message === 'string') {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,7 +143,7 @@ export function downloadTextAsFile(
|
||||
* @param data - Data to serialize and download
|
||||
* @param filename - Filename for the download
|
||||
*/
|
||||
export function downloadJsonAsFile(data: any, filename: string): void {
|
||||
export function downloadJsonAsFile(data: unknown, filename: string): void {
|
||||
const content = JSON.stringify(data, null, 2);
|
||||
downloadTextAsFile(content, filename, 'application/json');
|
||||
}
|
||||
|
||||
@ -11,9 +11,11 @@
|
||||
* }
|
||||
*/
|
||||
|
||||
export interface SettingsWithPending<T = any> {
|
||||
type PlainObject = Record<string, unknown>;
|
||||
|
||||
export interface SettingsWithPending<T extends PlainObject = PlainObject> {
|
||||
_pending?: Partial<T>;
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -23,15 +25,21 @@ export interface SettingsWithPending<T = any> {
|
||||
* @param settings Settings object from backend (may contain _pending block)
|
||||
* @returns Merged settings with pending values applied
|
||||
*/
|
||||
export function mergePendingSettings<T extends SettingsWithPending>(settings: T): Omit<T, '_pending'> {
|
||||
if (!settings || !settings._pending) {
|
||||
export function mergePendingSettings<T extends SettingsWithPending>(
|
||||
settings: T | null | undefined
|
||||
): Omit<T, '_pending'> {
|
||||
if (!settings) {
|
||||
return {} as Omit<T, '_pending'>;
|
||||
}
|
||||
|
||||
if (!settings._pending) {
|
||||
// No pending changes, return as-is (without _pending property)
|
||||
const { _pending, ...rest } = settings || {};
|
||||
const { _pending, ...rest } = settings;
|
||||
return rest as Omit<T, '_pending'>;
|
||||
}
|
||||
|
||||
// Deep merge pending changes
|
||||
const merged = deepMerge(settings, settings._pending);
|
||||
const merged = deepMerge(settings, settings._pending as PlainObject | undefined);
|
||||
|
||||
// Remove _pending from result
|
||||
const { _pending, ...result } = merged;
|
||||
@ -54,7 +62,7 @@ export function isFieldPending<T extends SettingsWithPending>(
|
||||
}
|
||||
|
||||
// Navigate the pending object using dot notation
|
||||
const value = getNestedValue(settings._pending, fieldPath);
|
||||
const value = getNestedValue(settings._pending as PlainObject | undefined, fieldPath);
|
||||
return value !== undefined;
|
||||
}
|
||||
|
||||
@ -80,12 +88,12 @@ export function hasPendingChanges<T extends SettingsWithPending>(
|
||||
export function getPendingValue<T extends SettingsWithPending>(
|
||||
settings: T | null | undefined,
|
||||
fieldPath: string
|
||||
): any {
|
||||
): unknown {
|
||||
if (!settings?._pending) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getNestedValue(settings._pending, fieldPath);
|
||||
return getNestedValue(settings._pending as PlainObject | undefined, fieldPath);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,14 +106,14 @@ export function getPendingValue<T extends SettingsWithPending>(
|
||||
export function getCurrentValue<T extends SettingsWithPending>(
|
||||
settings: T | null | undefined,
|
||||
fieldPath: string
|
||||
): any {
|
||||
): unknown {
|
||||
if (!settings) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get from settings, ignoring _pending
|
||||
const { _pending, ...activeSettings } = settings;
|
||||
return getNestedValue(activeSettings, fieldPath);
|
||||
return getNestedValue(activeSettings as PlainObject | undefined, fieldPath);
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
@ -113,11 +121,11 @@ export function getCurrentValue<T extends SettingsWithPending>(
|
||||
/**
|
||||
* Deep merge two objects. Second object takes priority.
|
||||
*/
|
||||
function deepMerge(target: any, source: any): any {
|
||||
if (!source) return target;
|
||||
function deepMerge(target: PlainObject | undefined, source: PlainObject | undefined): PlainObject {
|
||||
if (!source) return target ?? {};
|
||||
if (!target) return source;
|
||||
|
||||
const result = { ...target };
|
||||
const result: PlainObject = { ...target };
|
||||
|
||||
for (const key in source) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
@ -138,17 +146,20 @@ function deepMerge(target: any, source: any): any {
|
||||
/**
|
||||
* Get nested value using dot notation.
|
||||
*/
|
||||
function getNestedValue(obj: any, path: string): any {
|
||||
function getNestedValue(obj: PlainObject | undefined, path: string): unknown {
|
||||
if (!obj || !path) return undefined;
|
||||
|
||||
const parts = path.split('.');
|
||||
let current = obj;
|
||||
let current: unknown = obj;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === null || current === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
current = current[part];
|
||||
if (typeof current !== 'object' || !(part in current)) {
|
||||
return undefined;
|
||||
}
|
||||
current = (current as PlainObject)[part];
|
||||
}
|
||||
|
||||
return current;
|
||||
@ -157,7 +168,7 @@ function getNestedValue(obj: any, path: string): any {
|
||||
/**
|
||||
* Check if value is a plain object (not array, not null, not Date, etc.)
|
||||
*/
|
||||
function isPlainObject(value: any): boolean {
|
||||
function isPlainObject(value: unknown): value is PlainObject {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
|
||||
@ -2,32 +2,40 @@
|
||||
* Standardized error handling utilities for tool operations
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* Default error extractor that follows the standard pattern
|
||||
*/
|
||||
export const extractErrorMessage = (error: any): string => {
|
||||
if (error.response?.data && typeof error.response.data === 'string') {
|
||||
return error.response.data;
|
||||
const DEFAULT_MESSAGE = 'There was an error processing your request.';
|
||||
|
||||
const resolveErrorMessage = (error: unknown, fallbackMessage: string): string => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const responseData = error.response?.data;
|
||||
if (typeof responseData === 'string') {
|
||||
return responseData;
|
||||
}
|
||||
if (responseData && typeof responseData === 'object' && 'message' in responseData) {
|
||||
const message = (responseData as { message?: unknown }).message;
|
||||
if (typeof message === 'string') {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return error.message || fallbackMessage;
|
||||
}
|
||||
if (error.message) {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return 'There was an error processing your request.';
|
||||
return fallbackMessage;
|
||||
};
|
||||
|
||||
export const extractErrorMessage = (error: unknown): string => resolveErrorMessage(error, DEFAULT_MESSAGE);
|
||||
|
||||
/**
|
||||
* Creates a standardized error handler for tool operations
|
||||
* @param fallbackMessage - Message to show when no specific error can be extracted
|
||||
* @returns Error handler function that follows the standard pattern
|
||||
*/
|
||||
export const createStandardErrorHandler = (fallbackMessage: string) => {
|
||||
return (error: any): string => {
|
||||
if (error.response?.data && typeof error.response.data === 'string') {
|
||||
return error.response.data;
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
return fallbackMessage;
|
||||
};
|
||||
};
|
||||
return (error: unknown): string => resolveErrorMessage(error, fallbackMessage);
|
||||
};
|
||||
|
||||
@ -15,7 +15,7 @@ export async function processResponse(
|
||||
originalFiles: File[],
|
||||
filePrefix?: string,
|
||||
responseHandler?: ResponseHandler,
|
||||
responseHeaders?: Record<string, any>
|
||||
responseHeaders?: Record<string, unknown>
|
||||
): Promise<File[]> {
|
||||
if (responseHandler) {
|
||||
const out = await responseHandler(blob, originalFiles);
|
||||
@ -25,10 +25,14 @@ export async function processResponse(
|
||||
// Check if we should use the backend-provided filename from headers
|
||||
// Only when responseHeaders are explicitly provided (indicating the operation requested this)
|
||||
if (responseHeaders) {
|
||||
const contentDisposition = responseHeaders['content-disposition'];
|
||||
const contentDisposition = responseHeaders['content-disposition'] as string | undefined;
|
||||
const backendFilename = getFilenameFromHeaders(contentDisposition);
|
||||
if (backendFilename) {
|
||||
const type = blob.type || responseHeaders['content-type'] || 'application/octet-stream';
|
||||
const contentType = responseHeaders['content-type'];
|
||||
const type =
|
||||
blob.type ||
|
||||
(typeof contentType === 'string' ? contentType : undefined) ||
|
||||
'application/octet-stream';
|
||||
return [new File([blob], backendFilename, { type })];
|
||||
}
|
||||
// If preserveBackendFilename was requested but no Content-Disposition header found,
|
||||
|
||||
8
frontend/src/global.d.ts
vendored
8
frontend/src/global.d.ts
vendored
@ -3,9 +3,15 @@ declare module '*.module.css';
|
||||
|
||||
// Auto-generated icon set JSON import
|
||||
declare module 'assets/material-symbols-icons.json' {
|
||||
type IconDefinition = {
|
||||
body: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
const value: {
|
||||
prefix: string;
|
||||
icons: Record<string, any>;
|
||||
icons: Record<string, IconDefinition>;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user