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 5f61f7544..5c2aeb3c2 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1469,7 +1469,29 @@ "tags": "auto-detect,header-based,organize,relabel", "title": "Auto Rename", "header": "Auto Rename PDF", - "submit": "Auto Rename" + "description": "Automatically finds the title from your PDF content and uses it as the filename.", + "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 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..9c3ec4de6 --- /dev/null +++ b/frontend/src/components/tools/autoRename/AutoRenameSettings.tsx @@ -0,0 +1,24 @@ +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 = ( + ) => { + 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; diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx index 4fb87548f..7d5faafc5 100644 --- a/frontend/src/components/tools/automate/ToolSelector.tsx +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react'; +import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Stack, Text, ScrollArea } from '@mantine/core'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; @@ -93,7 +93,7 @@ export default function ToolSelector({ const renderedTools = useMemo(() => displayGroups.map((subcategory) => - renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching) + renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching, true) ), [displayGroups, handleToolSelect, isSearching, t] ); @@ -150,7 +150,7 @@ export default function ToolSelector({
{}} rounded={true}> + onSelect={()=>{}} rounded={true} disableNavigation={true}>
) : ( // Show search input when no tool selected OR when dropdown is opened diff --git a/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx b/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx index 6ed949442..c11726fdb 100644 --- a/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx +++ b/frontend/src/components/tools/shared/ToolWorkflowTitle.tsx @@ -5,6 +5,7 @@ import { Tooltip } from '../../shared/Tooltip'; export interface ToolWorkflowTitleProps { title: string; + description?: string; tooltip?: { content?: React.ReactNode; tips?: any[]; @@ -15,10 +16,19 @@ export interface ToolWorkflowTitleProps { }; } -export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) { - if (tooltip) { - return ( - <> +export function ToolWorkflowTitle({ title, tooltip, description }: ToolWorkflowTitleProps) { + const titleContent = ( + e.stopPropagation()}> + + {title} + + {tooltip && } + + ); + + return ( + <> + {tooltip ? ( - e.stopPropagation()}> - - {title} - - - + {titleContent} - - - ); - } + ) : ( + titleContent + )} - return ( - <> - - - {title} - - - + + {description} + + ); } diff --git a/frontend/src/components/tools/shared/renderToolButtons.tsx b/frontend/src/components/tools/shared/renderToolButtons.tsx index 4d92d4798..340ad559d 100644 --- a/frontend/src/components/tools/shared/renderToolButtons.tsx +++ b/frontend/src/components/tools/shared/renderToolButtons.tsx @@ -12,7 +12,8 @@ export const renderToolButtons = ( subcategory: SubcategoryGroup, selectedToolKey: string | null, onSelect: (id: string) => void, - showSubcategoryHeader: boolean = true + showSubcategoryHeader: boolean = true, + disableNavigation: boolean = false ) => ( {showSubcategoryHeader && ( @@ -26,6 +27,7 @@ export const renderToolButtons = ( tool={tool} isSelected={selectedToolKey === id} onSelect={onSelect} + disableNavigation={disableNavigation} /> ))} diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx index ee9c6062c..f84fa9189 100644 --- a/frontend/src/components/tools/toolPicker/ToolButton.tsx +++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx @@ -12,9 +12,10 @@ interface ToolButtonProps { isSelected: boolean; onSelect: (id: string) => void; rounded?: boolean; + disableNavigation?: boolean; } -const ToolButton: React.FC = ({ id, tool, isSelected, onSelect }) => { +const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, disableNavigation = false }) => { const isUnavailable = !tool.component && !tool.link; const { getToolNavigation } = useToolNavigation(); @@ -29,8 +30,8 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect onSelect(id); }; - // Get navigation props for URL support - const navProps = !isUnavailable && !tool.link ? getToolNavigation(id, tool) : null; + // Get navigation props for URL support (only if navigation is not disabled) + const navProps = !isUnavailable && !tool.link && !disableNavigation ? getToolNavigation(id, tool) : null; const tooltipContent = isUnavailable ? (Coming soon: {tool.description}) diff --git a/frontend/src/components/tooltips/useAutoRenameTips.ts b/frontend/src/components/tooltips/useAutoRenameTips.ts new file mode 100644 index 000000000..50e8ea9ce --- /dev/null +++ b/frontend/src/components/tooltips/useAutoRenameTips.ts @@ -0,0 +1,22 @@ +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"), + 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") + ] + } + ] + }; +}; diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 282653f8f..5ee8490a1 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"; @@ -29,6 +30,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 { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation"; import CompressSettings from "../components/tools/compress/CompressSettings"; import SplitSettings from "../components/tools/split/SplitSettings"; @@ -472,7 +474,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 263217e42..ff91fde32 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -33,6 +33,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; @@ -178,7 +185,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..c00f880b3 --- /dev/null +++ b/frontend/src/tools/AutoRename.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { createToolFlow } from "../components/tools/shared/createToolFlow"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; +import { BaseToolProps } 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"), description: t("auto-rename.description", "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, + onUndo: base.handleUndo, + }, + }); +}; + +export default AutoRename; diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index f5d4cef0a..3f61576e4 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -25,7 +25,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/automationExecutor.ts b/frontend/src/utils/automationExecutor.ts index 3feb9b412..9fef8dec4 100644 --- a/frontend/src/utils/automationExecutor.ts +++ b/frontend/src/utils/automationExecutor.ts @@ -2,8 +2,8 @@ import axios from 'axios'; import { ToolRegistry } from '../data/toolsTaxonomy'; import { AUTOMATION_CONSTANTS } from '../constants/automation'; import { AutomationFileProcessor } from './automationFileProcessor'; -import { ResourceManager } from './resourceManager'; import { ToolType } from '../hooks/tools/shared/useToolOperation'; +import { processResponse } from './toolResponseProcessor'; /** @@ -68,12 +68,17 @@ export const executeToolOperationWithPrefix = async ( let result; if (response.data.type === 'application/pdf' || (response.headers && response.headers['content-type'] === 'application/pdf')) { - // Single PDF response (e.g. split with merge option) - use original filename - const originalFileName = files[0]?.name || 'document.pdf'; - const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' }); + // Single PDF response (e.g. split with merge option) - use processResponse to respect preserveBackendFilename + const processedFiles = await processResponse( + response.data, + files, + filePrefix, + undefined, + config.preserveBackendFilename ? response.headers : undefined + ); result = { success: true, - files: [singleFile], + files: processedFiles, errors: [] }; } else { @@ -85,7 +90,8 @@ export const executeToolOperationWithPrefix = async ( console.warn(`⚠️ File processing warnings:`, result.errors); } // Apply prefix to files, replacing any existing prefix - const processedFiles = filePrefix + // Skip prefixing if preserveBackendFilename is true and backend provided a filename + const processedFiles = filePrefix && !config.preserveBackendFilename ? result.files.map(file => { const nameWithoutPrefix = file.name.replace(/^[^_]*_/, ''); return new File([file], `${filePrefix}${nameWithoutPrefix}`, { type: file.type }); @@ -117,15 +123,16 @@ export const executeToolOperationWithPrefix = async ( console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`); - // Create result file with automation prefix - - const resultFile = ResourceManager.createResultFile( + // Create result file using processResponse to respect preserveBackendFilename setting + const processedFiles = await processResponse( response.data, - file.name, - filePrefix + [file], + filePrefix, + undefined, + config.preserveBackendFilename ? response.headers : undefined ); - resultFiles.push(resultFile); - console.log(`✅ Created result file: ${resultFile.name}`); + resultFiles.push(...processedFiles); + console.log(`✅ Created result file(s): ${processedFiles.map(f => f.name).join(', ')}`); } console.log(`🎉 Single-file processing complete: ${resultFiles.length} files`); 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';