diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b8f77bbc..7c231b4ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -139,5 +139,8 @@ "app/core/src/main/java", "app/common/src/main/java", "app/proprietary/src/main/java" - ] + ], + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + } } diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 637ab59e1..b3c50fc13 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1428,7 +1428,28 @@ "tags": "auto-detect,header-based,organize,relabel", "title": "Auto Rename", "header": "Auto Rename PDF", - "submit": "Auto Rename" + "submit": "Auto Rename", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred whilst auto-renaming the PDF." + }, + "results": { + "title": "Auto-Rename Results" + }, + "tooltip": { + "header": { + "title": "How Auto-Rename Works" + }, + "howItWorks": { + "title": "Smart Renaming", + "text": "Automatically finds the best title from your PDF content and uses it as the filename.", + "bullet1": "Looks for text that appears to be a title or heading", + "bullet2": "Creates a clean, valid filename from the detected title", + "bullet3": "Keeps the original name if no suitable title is found" + } + } }, "adjust-contrast": { "tags": "color-correction,tune,modify,enhance,colour-correction" diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index b0a19539a..4b93181b8 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -1113,7 +1113,28 @@ "tags": "auto-detect,header-based,organize,relabel", "title": "Auto Rename", "header": "Auto Rename PDF", - "submit": "Auto Rename" + "submit": "Auto Rename", + "files": { + "placeholder": "Select a PDF file in the main view to get started" + }, + "error": { + "failed": "An error occurred while auto-renaming the PDF." + }, + "results": { + "title": "Auto-Rename Results" + }, + "tooltip": { + "header": { + "title": "How Auto-Rename Works" + }, + "howItWorks": { + "title": "Smart Renaming", + "text": "Automatically finds the best title from your PDF content and uses it as the filename.", + "bullet1": "Looks for text that appears to be a title or heading", + "bullet2": "Creates a clean, valid filename from the detected title", + "bullet3": "Keeps the original name if no suitable title is found" + } + } }, "adjust-contrast": { "tags": "color-correction,tune,modify,enhance" diff --git a/frontend/src/components/tools/autoRename/AutoRenameSettings.tsx b/frontend/src/components/tools/autoRename/AutoRenameSettings.tsx new file mode 100644 index 000000000..a069879d2 --- /dev/null +++ b/frontend/src/components/tools/autoRename/AutoRenameSettings.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { AutoRenameParameters } from '../../../hooks/tools/autoRename/useAutoRenameParameters'; + +interface AutoRenameSettingsProps { + parameters: AutoRenameParameters; + onParameterChange: (parameter: K, value: AutoRenameParameters[K]) => void; + disabled?: boolean; +} + +const AutoRenameSettings: React.FC = ({ + parameters, + onParameterChange, // Used for parameter changes + disabled = false +}) => { + const { t } = useTranslation(); + + return ( +
+

+ {t('autoRename.description', 'This tool will automatically rename PDF files based on their content. It analyzes the document to find the most suitable title from the text.')} +

+
+ ); +}; + +export default AutoRenameSettings; \ No newline at end of file diff --git a/frontend/src/components/tooltips/useAutoRenameTips.ts b/frontend/src/components/tooltips/useAutoRenameTips.ts new file mode 100644 index 000000000..394cb68e3 --- /dev/null +++ b/frontend/src/components/tooltips/useAutoRenameTips.ts @@ -0,0 +1,23 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent } from '../../types/tips'; + +export const useAutoRenameTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("auto-rename.tooltip.header.title", "How Auto-Rename Works") + }, + tips: [ + { + title: t("auto-rename.tooltip.howItWorks.title", "Smart Renaming"), + description: t("auto-rename.tooltip.howItWorks.text", "Automatically finds the best title from your PDF content and uses it as the filename."), + bullets: [ + t("auto-rename.tooltip.howItWorks.bullet1", "Looks for text that appears to be a title or heading"), + t("auto-rename.tooltip.howItWorks.bullet2", "Creates a clean, valid filename from the detected title"), + t("auto-rename.tooltip.howItWorks.bullet3", "Keeps the original name if no suitable title is found") + ] + } + ] + }; +}; \ No newline at end of file diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 68883fe92..5f98798c8 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -12,6 +12,7 @@ import RemovePassword from "../tools/RemovePassword"; import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy"; import AddWatermark from "../tools/AddWatermark"; import Repair from "../tools/Repair"; +import AutoRename from "../tools/AutoRename"; import SingleLargePage from "../tools/SingleLargePage"; import UnlockPdfForms from "../tools/UnlockPdfForms"; import RemoveCertificateSign from "../tools/RemoveCertificateSign"; @@ -28,6 +29,7 @@ import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation"; import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation"; import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation"; import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; +import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation"; import CompressSettings from "../components/tools/compress/CompressSettings"; import SplitSettings from "../components/tools/split/SplitSettings"; import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings"; @@ -464,7 +466,10 @@ export function useFlatToolRegistry(): ToolRegistry { "auto-rename-pdf-file": { icon: , name: t("home.auto-rename.title", "Auto Rename PDF File"), - component: null, + component: AutoRename, + maxFiles: -1, + endpoints: ["remove-certificate-sign"], + operationConfig: autoRenameOperationConfig, description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.AUTOMATION, diff --git a/frontend/src/hooks/tools/autoRename/useAutoRenameOperation.ts b/frontend/src/hooks/tools/autoRename/useAutoRenameOperation.ts new file mode 100644 index 000000000..e0d868a7d --- /dev/null +++ b/frontend/src/hooks/tools/autoRename/useAutoRenameOperation.ts @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next'; +import { ToolType, useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { AutoRenameParameters, defaultParameters } from './useAutoRenameParameters'; + +export const getFormData = ((parameters: AutoRenameParameters) => + Object.entries(parameters).map(([key, value]) => + [key, value.toString()] + ) as string[][] +); + +// Static function that can be used by both the hook and automation executor +export const buildAutoRenameFormData = (parameters: AutoRenameParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + + // Add all permission parameters + getFormData(parameters).forEach(([key, value]) => { + formData.append(key, value); + }); + + return formData; +}; + +// Static configuration object +export const autoRenameOperationConfig = { + toolType: ToolType.singleFile, + buildFormData: buildAutoRenameFormData, + operationType: 'autoRename', + endpoint: '/api/v1/misc/auto-rename', + filePrefix: 'autoRename_', + preserveBackendFilename: true, // Use filename from backend response headers + defaultParameters, +} as const; + +export const useAutoRenameOperation = () => { + const { t } = useTranslation(); + + return useToolOperation({ + ...autoRenameOperationConfig, + getErrorMessage: createStandardErrorHandler(t('auto-rename.error.failed', 'An error occurred while auto-renaming the PDF.')) + }); +}; diff --git a/frontend/src/hooks/tools/autoRename/useAutoRenameParameters.ts b/frontend/src/hooks/tools/autoRename/useAutoRenameParameters.ts new file mode 100644 index 000000000..ede570c33 --- /dev/null +++ b/frontend/src/hooks/tools/autoRename/useAutoRenameParameters.ts @@ -0,0 +1,19 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; + +export interface AutoRenameParameters extends BaseParameters { + useFirstTextAsFallback: boolean; +} + +export const defaultParameters: AutoRenameParameters = { + useFirstTextAsFallback: false, +}; + +export type AutoRenameParametersHook = BaseParametersHook; + +export const useAutoRenameParameters = (): AutoRenameParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'auto-rename', + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/shared/useToolApiCalls.ts b/frontend/src/hooks/tools/shared/useToolApiCalls.ts index ec0f398aa..67e3e6c15 100644 --- a/frontend/src/hooks/tools/shared/useToolApiCalls.ts +++ b/frontend/src/hooks/tools/shared/useToolApiCalls.ts @@ -8,6 +8,7 @@ export interface ApiCallsConfig { buildFormData: (params: TParams, file: File) => FormData; filePrefix: string; responseHandler?: ResponseHandler; + preserveBackendFilename?: boolean; } export const useToolApiCalls = () => { @@ -46,7 +47,8 @@ export const useToolApiCalls = () => { response.data, [file], config.filePrefix, - config.responseHandler + config.responseHandler, + config.preserveBackendFilename ? response.headers : undefined ); processedFiles.push(...responseFiles); diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index d8d35176d..bb3385f71 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -35,6 +35,13 @@ interface BaseToolOperationConfig { /** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */ filePrefix: string; + /** + * Whether to preserve the filename provided by the backend in response headers. + * When true, ignores filePrefix and uses the filename from Content-Disposition header. + * Useful for tools like auto-rename where the backend determines the final filename. + */ + preserveBackendFilename?: boolean; + /** How to handle API responses (e.g., ZIP extraction, single file response) */ responseHandler?: ResponseHandler; @@ -180,7 +187,8 @@ export const useToolOperation = ( endpoint: config.endpoint, buildFormData: config.buildFormData, filePrefix: config.filePrefix, - responseHandler: config.responseHandler + responseHandler: config.responseHandler, + preserveBackendFilename: config.preserveBackendFilename }; processedFiles = await processFiles( params, diff --git a/frontend/src/tools/AutoRename.tsx b/frontend/src/tools/AutoRename.tsx new file mode 100644 index 000000000..b8a46af2c --- /dev/null +++ b/frontend/src/tools/AutoRename.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from "react-i18next"; +import { createToolFlow } from "../components/tools/shared/createToolFlow"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; + +import { useAutoRenameParameters } from "../hooks/tools/autoRename/useAutoRenameParameters"; +import { useAutoRenameOperation } from "../hooks/tools/autoRename/useAutoRenameOperation"; +import { useAutoRenameTips } from "../components/tooltips/useAutoRenameTips"; + +const AutoRename =(props: BaseToolProps) => { + const { t } = useTranslation(); + + const base = useBaseTool( + '"auto-rename-pdf-file', + useAutoRenameParameters, + useAutoRenameOperation, + props + ); + +return createToolFlow({ + title: { title:t("auto-rename.title", "Auto Rename PDF"), tooltip: useAutoRenameTips()}, + files: { + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, + }, + steps: [], + executeButton: { + text: t("auto-rename.submit", "Auto Rename"), + isVisible: !base.hasResults, + loadingText: t("loading"), + onClick: base.handleExecute, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, + }, + review: { + isVisible: base.hasResults, + operation: base.operation, + title: t("auto-rename.results.title", "Auto-Rename Results"), + onFileClick: base.handleThumbnailClick, + }, + }); +}; + +export default AutoRename; diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 9210f9ce9..42ebd0764 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -22,7 +22,8 @@ export type ModeType = | 'single-large-page' | 'repair' | 'unlockPdfForms' - | 'removeCertificateSign'; + | 'removeCertificateSign' + | 'auto-rename-pdf-file'; // Normalized state types export interface ProcessedFilePage { diff --git a/frontend/src/utils/fileResponseUtils.ts b/frontend/src/utils/fileResponseUtils.ts index 472cccb05..fd3454c52 100644 --- a/frontend/src/utils/fileResponseUtils.ts +++ b/frontend/src/utils/fileResponseUtils.ts @@ -11,8 +11,7 @@ export const getFilenameFromHeaders = (contentDisposition: string = ''): string const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); if (match && match[1]) { const filename = match[1].replace(/['"]/g, ''); - - // Decode URL-encoded characters (e.g., %20 -> space) + // Decode URL-encoded characters (e.g., %20 for spaces) try { return decodeURIComponent(filename); } catch (error) { @@ -37,9 +36,9 @@ export const createFileFromApiResponse = ( ): File => { const contentType = headers?.['content-type'] || 'application/octet-stream'; const contentDisposition = headers?.['content-disposition'] || ''; - + const filename = getFilenameFromHeaders(contentDisposition) || fallbackFilename; const blob = new Blob([responseData], { type: contentType }); - + return new File([blob], filename, { type: contentType }); -}; \ No newline at end of file +}; diff --git a/frontend/src/utils/toolResponseProcessor.ts b/frontend/src/utils/toolResponseProcessor.ts index fe2f11242..683f8cd79 100644 --- a/frontend/src/utils/toolResponseProcessor.ts +++ b/frontend/src/utils/toolResponseProcessor.ts @@ -1,23 +1,40 @@ // Note: This utility should be used with useToolResources for ZIP operations +import { getFilenameFromHeaders } from './fileResponseUtils'; export type ResponseHandler = (blob: Blob, originalFiles: File[]) => Promise | File[]; /** * Processes a blob response into File(s). * - If a tool-specific responseHandler is provided, it is used. + * - If responseHeaders provided and contains Content-Disposition, uses that filename. * - Otherwise, create a single file using the filePrefix + original name. */ export async function processResponse( blob: Blob, originalFiles: File[], filePrefix: string, - responseHandler?: ResponseHandler + responseHandler?: ResponseHandler, + responseHeaders?: Record ): Promise { if (responseHandler) { const out = await responseHandler(blob, originalFiles); return Array.isArray(out) ? out : [out as unknown as File]; } + // 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 backendFilename = getFilenameFromHeaders(contentDisposition); + if (backendFilename) { + const type = blob.type || responseHeaders['content-type'] || 'application/octet-stream'; + return [new File([blob], backendFilename, { type })]; + } + // If preserveBackendFilename was requested but no Content-Disposition header found, + // fall back to default behavior (this handles cases where backend doesn't set the header) + } + + // Default behavior: use filePrefix + original name const original = originalFiles[0]?.name ?? 'result.pdf'; const name = `${filePrefix}${original}`; const type = blob.type || 'application/octet-stream';