From 11d23a2d434585b71984b5562cf3eda9da039f23 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:12:52 +0100 Subject: [PATCH 1/5] V2 Auto rename (#4244) # Description of Changes This pull request introduces the new "Auto Rename PDF" tool to the frontend, enabling users to automatically rename PDF files based on their content. The implementation includes UI components, parameter handling, operation logic, localization, and enhancements to the file response utilities to support backend-provided filenames. Below are the most important changes grouped by theme: **Feature: Auto Rename PDF Tool** - Added the main `AutoRename` tool component (`AutoRename.tsx`) and registered it in the tool registry, enabling selection and execution of the auto-rename operation in the UI. [[1]](diffhunk://#diff-3647ca39d46d109d122d4cd6cbfe981beb4189d05b1b446e5c46824eb98a4a88R1-R80) [[2]](diffhunk://#diff-0a3e636736c137356dd9354ff3cacbd302ebda40147545e13c62d073525d1969R17) [[3]](diffhunk://#diff-0a3e636736c137356dd9354ff3cacbd302ebda40147545e13c62d073525d1969L359-R366) [[4]](diffhunk://#diff-29427b8d06a23772c56645fc4b72af2980c813605abc162e3d47c2e39d026d06L25-R26) - Implemented the settings panel (`AutoRenameSettings.tsx`) and parameter management hook (`useAutoRenameParameters.ts`), allowing users to configure options such as using the first text as a fallback for the filename. [[1]](diffhunk://#diff-b2f9474c8e5a7a42df00a12ffd2d31a785895fe1096e8ca515e6af5633a4d648R1-R27) [[2]](diffhunk://#diff-8798a1ef451233bf3a1bf8825c12c5b434ad1a17a1beb1ca21fd972fdaceb50cR1-R19) - Created the operation hook (`useAutoRenameOperation.ts`) to handle API requests, error handling, and result processing for the auto-rename feature. **Localization** - Added English (US and GB) translations for the new tool, including UI labels, descriptions, error messages, and settings. [[1]](diffhunk://#diff-e4d543afa388d9eb8a423e45dfebb91641e3558d00848d70b285ebb91c40b249R1048-R1066) [[2]](diffhunk://#diff-14c707e28788a3a84ed5293ff6689be73d4bca00e155beaf090f9b37c978babbR1321-R1339) **File Response Handling Enhancements** - Updated the file response processor and related hooks to support preserving backend-provided filenames via the `Content-Disposition` header, ensuring files are renamed according to backend results. [[1]](diffhunk://#diff-97ea1c842d4b269c566a3085d8555ded7f9b462d9ce8dc73706bec79fe3973e0R11) [[2]](diffhunk://#diff-97ea1c842d4b269c566a3085d8555ded7f9b462d9ce8dc73706bec79fe3973e0L49-R51) [[3]](diffhunk://#diff-d44da7f96721d9829f3c20bf9c7ac5b9e156b647d2c75d76e861c8c09abc5191R52-R58) [[4]](diffhunk://#diff-d44da7f96721d9829f3c20bf9c7ac5b9e156b647d2c75d76e861c8c09abc5191L175-R183) [[5]](diffhunk://#diff-fa8af80f4d87370d58e3a5b79df675d201f0c3aa753eda89cec03ff027c4213dL13-R21) [[6]](diffhunk://#diff-efa525dbdeceaeb5701aa3d2303bf1d533541f65a92d985f94f33b8e87b036d1R2-R37) These changes collectively deliver a new advanced tool for users to automatically rename PDFs, with robust parameter handling, user interface integration, and proper handling of filenames as determined by backend logic. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Connor Yoh Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com> --- .vscode/settings.json | 5 ++- .../public/locales/en-GB/translation.json | 24 +++++++++- .../public/locales/en-US/translation.json | 23 +++++++++- .../tools/autoRename/AutoRenameSettings.tsx | 24 ++++++++++ .../tools/automate/ToolSelector.tsx | 6 +-- .../tools/shared/ToolWorkflowTitle.tsx | 44 +++++++++---------- .../tools/shared/renderToolButtons.tsx | 4 +- .../tools/toolPicker/ToolButton.tsx | 7 +-- .../components/tooltips/useAutoRenameTips.ts | 22 ++++++++++ .../src/data/useTranslatedToolRegistry.tsx | 7 ++- .../autoRename/useAutoRenameOperation.ts | 43 ++++++++++++++++++ .../autoRename/useAutoRenameParameters.ts | 19 ++++++++ .../src/hooks/tools/shared/useToolApiCalls.ts | 4 +- .../hooks/tools/shared/useToolOperation.ts | 10 ++++- frontend/src/tools/AutoRename.tsx | 44 +++++++++++++++++++ frontend/src/types/fileContext.ts | 3 +- frontend/src/utils/automationExecutor.ts | 33 ++++++++------ frontend/src/utils/toolResponseProcessor.ts | 19 +++++++- 18 files changed, 291 insertions(+), 50 deletions(-) create mode 100644 frontend/src/components/tools/autoRename/AutoRenameSettings.tsx create mode 100644 frontend/src/components/tooltips/useAutoRenameTips.ts create mode 100644 frontend/src/hooks/tools/autoRename/useAutoRenameOperation.ts create mode 100644 frontend/src/hooks/tools/autoRename/useAutoRenameParameters.ts create mode 100644 frontend/src/tools/AutoRename.tsx 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'; From 316be5eac5b9935c054e922afd0d1d93bb4a6d7b Mon Sep 17 00:00:00 2001 From: James Brunton Date: Mon, 8 Sep 2025 09:55:30 +0100 Subject: [PATCH 2/5] Fix types of onParameterChange methods (#4415) # Description of Changes Fix types of onParameterChange methods --- .../tools/addPassword/AddPasswordSettings.tsx | 2 +- .../ChangePermissionsSettings.tsx | 2 +- .../tools/compress/CompressSettings.tsx | 2 +- .../convert/ConvertFromEmailSettings.tsx | 44 +++++++++---------- .../convert/ConvertFromImageSettings.tsx | 2 +- .../tools/convert/ConvertFromWebSettings.tsx | 26 +++++------ .../tools/convert/ConvertSettings.tsx | 2 +- .../tools/convert/ConvertToImageSettings.tsx | 2 +- .../tools/convert/ConvertToPdfaSettings.tsx | 20 ++++----- .../tools/ocr/AdvancedOCRSettings.tsx | 8 ++-- .../src/components/tools/ocr/OCRSettings.tsx | 2 +- .../removePassword/RemovePasswordSettings.tsx | 2 +- .../tools/sanitize/SanitizeSettings.tsx | 2 +- .../components/tools/split/SplitSettings.tsx | 6 +-- 14 files changed, 61 insertions(+), 61 deletions(-) diff --git a/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx b/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx index 36ef3ce01..beb8c432c 100644 --- a/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx +++ b/frontend/src/components/tools/addPassword/AddPasswordSettings.tsx @@ -4,7 +4,7 @@ import { AddPasswordParameters } from "../../../hooks/tools/addPassword/useAddPa interface AddPasswordSettingsProps { parameters: AddPasswordParameters; - onParameterChange: (key: keyof AddPasswordParameters, value: any) => void; + onParameterChange: (key: K, value: AddPasswordParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx b/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx index 071e27cfd..06ac6ac69 100644 --- a/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx +++ b/frontend/src/components/tools/changePermissions/ChangePermissionsSettings.tsx @@ -4,7 +4,7 @@ import { ChangePermissionsParameters } from "../../../hooks/tools/changePermissi interface ChangePermissionsSettingsProps { parameters: ChangePermissionsParameters; - onParameterChange: (key: keyof ChangePermissionsParameters, value: boolean) => void; + onParameterChange: (key: K, value: ChangePermissionsParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/compress/CompressSettings.tsx b/frontend/src/components/tools/compress/CompressSettings.tsx index 42d270abb..28035bfe3 100644 --- a/frontend/src/components/tools/compress/CompressSettings.tsx +++ b/frontend/src/components/tools/compress/CompressSettings.tsx @@ -5,7 +5,7 @@ import { CompressParameters } from "../../../hooks/tools/compress/useCompressPar interface CompressSettingsProps { parameters: CompressParameters; - onParameterChange: (key: keyof CompressParameters, value: any) => void; + onParameterChange: (key: K, value: CompressParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/convert/ConvertFromEmailSettings.tsx b/frontend/src/components/tools/convert/ConvertFromEmailSettings.tsx index 59fa824ee..943e0feed 100644 --- a/frontend/src/components/tools/convert/ConvertFromEmailSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertFromEmailSettings.tsx @@ -5,40 +5,40 @@ import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParame interface ConvertFromEmailSettingsProps { parameters: ConvertParameters; - onParameterChange: (key: keyof ConvertParameters, value: any) => void; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; disabled?: boolean; } -const ConvertFromEmailSettings = ({ - parameters, - onParameterChange, - disabled = false +const ConvertFromEmailSettings = ({ + parameters, + onParameterChange, + disabled = false }: ConvertFromEmailSettingsProps) => { const { t } = useTranslation(); return ( {t("convert.emailOptions", "Email to PDF Options")}: - + onParameterChange('emailOptions', { - ...parameters.emailOptions, - includeAttachments: event.currentTarget.checked + onChange={(event) => onParameterChange('emailOptions', { + ...parameters.emailOptions, + includeAttachments: event.currentTarget.checked })} disabled={disabled} data-testid="include-attachments-checkbox" /> - + {parameters.emailOptions.includeAttachments && ( {t("convert.maxAttachmentSize", "Maximum attachment size (MB)")}: onParameterChange('emailOptions', { - ...parameters.emailOptions, - maxAttachmentSizeMB: Number(value) || 10 + onChange={(value) => onParameterChange('emailOptions', { + ...parameters.emailOptions, + maxAttachmentSizeMB: Number(value) || 10 })} min={1} max={100} @@ -48,24 +48,24 @@ const ConvertFromEmailSettings = ({ /> )} - + onParameterChange('emailOptions', { - ...parameters.emailOptions, - includeAllRecipients: event.currentTarget.checked + onChange={(event) => onParameterChange('emailOptions', { + ...parameters.emailOptions, + includeAllRecipients: event.currentTarget.checked })} disabled={disabled} data-testid="include-all-recipients-checkbox" /> - + onParameterChange('emailOptions', { - ...parameters.emailOptions, - downloadHtml: event.currentTarget.checked + onChange={(event) => onParameterChange('emailOptions', { + ...parameters.emailOptions, + downloadHtml: event.currentTarget.checked })} disabled={disabled} data-testid="download-html-checkbox" @@ -74,4 +74,4 @@ const ConvertFromEmailSettings = ({ ); }; -export default ConvertFromEmailSettings; \ No newline at end of file +export default ConvertFromEmailSettings; diff --git a/frontend/src/components/tools/convert/ConvertFromImageSettings.tsx b/frontend/src/components/tools/convert/ConvertFromImageSettings.tsx index 0681821fd..eb0457f13 100644 --- a/frontend/src/components/tools/convert/ConvertFromImageSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertFromImageSettings.tsx @@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame interface ConvertFromImageSettingsProps { parameters: ConvertParameters; - onParameterChange: (key: keyof ConvertParameters, value: any) => void; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/convert/ConvertFromWebSettings.tsx b/frontend/src/components/tools/convert/ConvertFromWebSettings.tsx index 270980f82..f6101d1c1 100644 --- a/frontend/src/components/tools/convert/ConvertFromWebSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertFromWebSettings.tsx @@ -5,28 +5,28 @@ import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParame interface ConvertFromWebSettingsProps { parameters: ConvertParameters; - onParameterChange: (key: keyof ConvertParameters, value: any) => void; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; disabled?: boolean; } -const ConvertFromWebSettings = ({ - parameters, - onParameterChange, - disabled = false +const ConvertFromWebSettings = ({ + parameters, + onParameterChange, + disabled = false }: ConvertFromWebSettingsProps) => { const { t } = useTranslation(); return ( {t("convert.webOptions", "Web to PDF Options")}: - + {t("convert.zoomLevel", "Zoom Level")}: onParameterChange('htmlOptions', { - ...parameters.htmlOptions, - zoomLevel: Number(value) || 1.0 + onChange={(value) => onParameterChange('htmlOptions', { + ...parameters.htmlOptions, + zoomLevel: Number(value) || 1.0 })} min={0.1} max={3.0} @@ -36,9 +36,9 @@ const ConvertFromWebSettings = ({ /> onParameterChange('htmlOptions', { - ...parameters.htmlOptions, - zoomLevel: value + onChange={(value) => onParameterChange('htmlOptions', { + ...parameters.htmlOptions, + zoomLevel: value })} min={0.1} max={3.0} @@ -51,4 +51,4 @@ const ConvertFromWebSettings = ({ ); }; -export default ConvertFromWebSettings; \ No newline at end of file +export default ConvertFromWebSettings; diff --git a/frontend/src/components/tools/convert/ConvertSettings.tsx b/frontend/src/components/tools/convert/ConvertSettings.tsx index 3a019f8da..2b1de9302 100644 --- a/frontend/src/components/tools/convert/ConvertSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertSettings.tsx @@ -26,7 +26,7 @@ import { StirlingFile } from "../../../types/fileContext"; interface ConvertSettingsProps { parameters: ConvertParameters; - onParameterChange: (key: keyof ConvertParameters, value: any) => void; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>; selectedFiles: StirlingFile[]; disabled?: boolean; diff --git a/frontend/src/components/tools/convert/ConvertToImageSettings.tsx b/frontend/src/components/tools/convert/ConvertToImageSettings.tsx index 9d67bfbf6..887685501 100644 --- a/frontend/src/components/tools/convert/ConvertToImageSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertToImageSettings.tsx @@ -6,7 +6,7 @@ import { ConvertParameters } from "../../../hooks/tools/convert/useConvertParame interface ConvertToImageSettingsProps { parameters: ConvertParameters; - onParameterChange: (key: keyof ConvertParameters, value: any) => void; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; disabled?: boolean; } diff --git a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx index 49e057a1c..b9a572b8d 100644 --- a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx @@ -7,16 +7,16 @@ import { StirlingFile } from '../../../types/fileContext'; interface ConvertToPdfaSettingsProps { parameters: ConvertParameters; - onParameterChange: (key: keyof ConvertParameters, value: any) => void; + onParameterChange: (key: K, value: ConvertParameters[K]) => void; selectedFiles: StirlingFile[]; disabled?: boolean; } -const ConvertToPdfaSettings = ({ - parameters, +const ConvertToPdfaSettings = ({ + parameters, onParameterChange, selectedFiles, - disabled = false + disabled = false }: ConvertToPdfaSettingsProps) => { const { t } = useTranslation(); const { hasDigitalSignatures, isChecking } = usePdfSignatureDetection(selectedFiles); @@ -29,7 +29,7 @@ const ConvertToPdfaSettings = ({ return ( {t("convert.pdfaOptions", "PDF/A Options")}: - + {hasDigitalSignatures && ( @@ -37,14 +37,14 @@ const ConvertToPdfaSettings = ({ )} - + {t("convert.outputFormat", "Output Format")}: v && onParameterChange('splitType', v)} + onChange={(v) => isSplitType(v) && onParameterChange('splitType', v)} disabled={disabled} data={[ { value: SPLIT_TYPES.SIZE, label: t("split-by-size-or-count.type.size", "By Size") }, From c25985e49ee51f37d5e0022d699f7657ebd4627a Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 08:58:22 +0000 Subject: [PATCH 3/5] Update Frontend 3rd Party Licenses (#4319) Auto-generated by stirlingbot[bot] This PR updates the frontend license report based on changes to package.json dependencies. Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- frontend/src/assets/3rdPartyLicenses.json | 44 ++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/frontend/src/assets/3rdPartyLicenses.json b/frontend/src/assets/3rdPartyLicenses.json index 2f19f5db6..70aacd3b2 100644 --- a/frontend/src/assets/3rdPartyLicenses.json +++ b/frontend/src/assets/3rdPartyLicenses.json @@ -385,6 +385,13 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "@posthog/core", + "moduleUrl": "https://github.com/PostHog/posthog-js", + "moduleVersion": "1.0.2", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "@tailwindcss/node", "moduleUrl": "https://github.com/tailwindlabs/tailwindcss", @@ -742,6 +749,13 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "core-js", + "moduleUrl": "https://github.com/zloirock/core-js", + "moduleVersion": "3.45.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "core-util-is", "moduleUrl": "https://github.com/isaacs/core-util-is", @@ -924,6 +938,13 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "fflate", + "moduleUrl": "https://github.com/101arrowz/fflate", + "moduleVersion": "0.4.8", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "file-selector", "moduleUrl": "https://github.com/react-dropzone/file-selector", @@ -1533,6 +1554,20 @@ "moduleLicense": "MIT", "moduleLicenseUrl": "https://opensource.org/licenses/MIT" }, + { + "moduleName": "posthog-js", + "moduleUrl": "https://github.com/PostHog/posthog-js", + "moduleVersion": "1.261.0", + "moduleLicense": "MIT*", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, + { + "moduleName": "preact", + "moduleUrl": "https://github.com/preactjs/preact", + "moduleVersion": "10.27.1", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/licenses/MIT" + }, { "moduleName": "pretty-format", "moduleUrl": "https://github.com/facebook/jest", @@ -1928,7 +1963,7 @@ { "moduleName": "typescript", "moduleUrl": "https://github.com/microsoft/TypeScript", - "moduleVersion": "5.8.3", + "moduleVersion": "5.9.2", "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, @@ -1995,6 +2030,13 @@ "moduleLicense": "Apache-2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, + { + "moduleName": "web-vitals", + "moduleUrl": "https://github.com/GoogleChrome/web-vitals", + "moduleVersion": "4.2.4", + "moduleLicense": "Apache-2.0", + "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" + }, { "moduleName": "webidl-conversions", "moduleUrl": "https://github.com/jsdom/webidl-conversions", From e8af4f6b35d24e0e778b3339a8d7ccb29a701e6f Mon Sep 17 00:00:00 2001 From: Ludy Date: Mon, 8 Sep 2025 11:05:49 +0200 Subject: [PATCH 4/5] Set i18n to load only current language (#4359) This pull request introduces a minor configuration change to the i18n setup in the frontend. The change improves language loading behavior by ensuring only the current language is loaded, which can help optimize performance and prevent unnecessary resource usage. * Added the `load: 'currentOnly'` option to the i18n initialization in `frontend/src/i18n.ts`, so only the current language is loaded. Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> --- frontend/src/i18n.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 454bb4cbc..b0ce8fdf7 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -59,6 +59,7 @@ i18n .init({ fallbackLng: 'en-GB', supportedLngs: Object.keys(supportedLanguages), + load: 'currentOnly', nonExplicitSupportedLngs: false, debug: process.env.NODE_ENV === 'development', From 494ef801a2d270d9d951644041c1d2d773ee7301 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Tue, 9 Sep 2025 16:18:09 +0100 Subject: [PATCH 5/5] Improve npm scripts (#4424) # Description of Changes Change NPM scripts so they call each other (single source of truth) and add a command to run type checking, linting and tests (to give confidence CI will pass). --- frontend/package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index d73e9ad97..0b14a8ffc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,16 +38,18 @@ }, "scripts": { "predev": "npm run generate-icons", - "dev": "npx tsc --noEmit && vite", + "dev": "npm run typecheck && vite", "prebuild": "npm run generate-icons", - "lint": "npx eslint", - "build": "npx tsc --noEmit && vite build", + "lint": "eslint", + "build": "npm run typecheck && vite build", "preview": "vite preview", "typecheck": "tsc --noEmit", + "check": "npm run typecheck && npm run lint && npm run test:run", "generate-licenses": "node scripts/generate-licenses.js", "generate-icons": "node scripts/generate-icons.js", "generate-icons:verbose": "node scripts/generate-icons.js --verbose", "test": "vitest", + "test:run": "vitest run", "test:watch": "vitest --watch", "test:coverage": "vitest --coverage", "test:e2e": "playwright test",