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 <connor@stirlingpdf.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
This commit is contained in:
Anthony Stirling 2025-09-05 17:12:52 +01:00 committed by GitHub
parent 1898df0df9
commit 11d23a2d43
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 291 additions and 50 deletions

View File

@ -139,5 +139,8 @@
"app/core/src/main/java", "app/core/src/main/java",
"app/common/src/main/java", "app/common/src/main/java",
"app/proprietary/src/main/java" "app/proprietary/src/main/java"
] ],
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
} }

View File

@ -1469,7 +1469,29 @@
"tags": "auto-detect,header-based,organize,relabel", "tags": "auto-detect,header-based,organize,relabel",
"title": "Auto Rename", "title": "Auto Rename",
"header": "Auto Rename PDF", "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": { "adjust-contrast": {
"tags": "color-correction,tune,modify,enhance,colour-correction" "tags": "color-correction,tune,modify,enhance,colour-correction"

View File

@ -1113,7 +1113,28 @@
"tags": "auto-detect,header-based,organize,relabel", "tags": "auto-detect,header-based,organize,relabel",
"title": "Auto Rename", "title": "Auto Rename",
"header": "Auto Rename PDF", "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": { "adjust-contrast": {
"tags": "color-correction,tune,modify,enhance" "tags": "color-correction,tune,modify,enhance"

View File

@ -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: <K extends keyof AutoRenameParameters>(parameter: K, value: AutoRenameParameters[K]) => void;
disabled?: boolean;
}
const AutoRenameSettings: React.FC<AutoRenameSettingsProps> = (
) => {
const { t } = useTranslation();
return (
<div className="auto-rename-settings">
<p className="text-muted">
{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.')}
</p>
</div>
);
};
export default AutoRenameSettings;

View File

@ -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 { useTranslation } from 'react-i18next';
import { Stack, Text, ScrollArea } from '@mantine/core'; import { Stack, Text, ScrollArea } from '@mantine/core';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
@ -93,7 +93,7 @@ export default function ToolSelector({
const renderedTools = useMemo(() => const renderedTools = useMemo(() =>
displayGroups.map((subcategory) => displayGroups.map((subcategory) =>
renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching) renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching, true)
), [displayGroups, handleToolSelect, isSearching, t] ), [displayGroups, handleToolSelect, isSearching, t]
); );
@ -150,7 +150,7 @@ export default function ToolSelector({
<div onClick={handleSearchFocus} style={{ cursor: 'pointer', <div onClick={handleSearchFocus} style={{ cursor: 'pointer',
borderRadius: "var(--mantine-radius-lg)" }}> borderRadius: "var(--mantine-radius-lg)" }}>
<ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false} <ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false}
onSelect={()=>{}} rounded={true}></ToolButton> onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
</div> </div>
) : ( ) : (
// Show search input when no tool selected OR when dropdown is opened // Show search input when no tool selected OR when dropdown is opened

View File

@ -5,6 +5,7 @@ import { Tooltip } from '../../shared/Tooltip';
export interface ToolWorkflowTitleProps { export interface ToolWorkflowTitleProps {
title: string; title: string;
description?: string;
tooltip?: { tooltip?: {
content?: React.ReactNode; content?: React.ReactNode;
tips?: any[]; tips?: any[];
@ -15,10 +16,19 @@ export interface ToolWorkflowTitleProps {
}; };
} }
export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) { export function ToolWorkflowTitle({ title, tooltip, description }: ToolWorkflowTitleProps) {
if (tooltip) { const titleContent = (
return ( <Flex align="center" gap="xs" onClick={(e) => e.stopPropagation()}>
<> <Text fw={500} size="lg" p="xs">
{title}
</Text>
{tooltip && <LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />}
</Flex>
);
return (
<>
{tooltip ? (
<Flex justify="center" w="100%"> <Flex justify="center" w="100%">
<Tooltip <Tooltip
content={tooltip.content} content={tooltip.content}
@ -26,27 +36,17 @@ export function ToolWorkflowTitle({ title, tooltip }: ToolWorkflowTitleProps) {
header={tooltip.header} header={tooltip.header}
sidebarTooltip={true} sidebarTooltip={true}
> >
<Flex align="center" gap="xs" onClick={(e) => e.stopPropagation()}> {titleContent}
<Text fw={500} size="xl" p="md">
{title}
</Text>
<LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />
</Flex>
</Tooltip> </Tooltip>
</Flex> </Flex>
<Divider /> ) : (
</> titleContent
); )}
}
return ( <Text size="sm" mb="md" p="sm" style={{borderRadius:'var(--mantine-radius-md)', background: 'var(--color-gray-200)', color: 'var(--mantine-color-text)' }}>
<> {description}
<Flex justify="center" w="100%"> </Text>
<Text fw={500} size="xl" p="md"> <Divider mb="sm" />
{title}
</Text>
</Flex>
<Divider />
</> </>
); );
} }

View File

@ -12,7 +12,8 @@ export const renderToolButtons = (
subcategory: SubcategoryGroup, subcategory: SubcategoryGroup,
selectedToolKey: string | null, selectedToolKey: string | null,
onSelect: (id: string) => void, onSelect: (id: string) => void,
showSubcategoryHeader: boolean = true showSubcategoryHeader: boolean = true,
disableNavigation: boolean = false
) => ( ) => (
<Box key={subcategory.subcategoryId} w="100%"> <Box key={subcategory.subcategoryId} w="100%">
{showSubcategoryHeader && ( {showSubcategoryHeader && (
@ -26,6 +27,7 @@ export const renderToolButtons = (
tool={tool} tool={tool}
isSelected={selectedToolKey === id} isSelected={selectedToolKey === id}
onSelect={onSelect} onSelect={onSelect}
disableNavigation={disableNavigation}
/> />
))} ))}
</div> </div>

View File

@ -12,9 +12,10 @@ interface ToolButtonProps {
isSelected: boolean; isSelected: boolean;
onSelect: (id: string) => void; onSelect: (id: string) => void;
rounded?: boolean; rounded?: boolean;
disableNavigation?: boolean;
} }
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => { const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false }) => {
const isUnavailable = !tool.component && !tool.link; const isUnavailable = !tool.component && !tool.link;
const { getToolNavigation } = useToolNavigation(); const { getToolNavigation } = useToolNavigation();
@ -29,8 +30,8 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect
onSelect(id); onSelect(id);
}; };
// Get navigation props for URL support // Get navigation props for URL support (only if navigation is not disabled)
const navProps = !isUnavailable && !tool.link ? getToolNavigation(id, tool) : null; const navProps = !isUnavailable && !tool.link && !disableNavigation ? getToolNavigation(id, tool) : null;
const tooltipContent = isUnavailable const tooltipContent = isUnavailable
? (<span><strong>Coming soon:</strong> {tool.description}</span>) ? (<span><strong>Coming soon:</strong> {tool.description}</span>)

View File

@ -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")
]
}
]
};
};

View File

@ -12,6 +12,7 @@ import RemovePassword from "../tools/RemovePassword";
import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy"; import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
import AddWatermark from "../tools/AddWatermark"; import AddWatermark from "../tools/AddWatermark";
import Repair from "../tools/Repair"; import Repair from "../tools/Repair";
import AutoRename from "../tools/AutoRename";
import SingleLargePage from "../tools/SingleLargePage"; import SingleLargePage from "../tools/SingleLargePage";
import UnlockPdfForms from "../tools/UnlockPdfForms"; import UnlockPdfForms from "../tools/UnlockPdfForms";
import RemoveCertificateSign from "../tools/RemoveCertificateSign"; import RemoveCertificateSign from "../tools/RemoveCertificateSign";
@ -29,6 +30,7 @@ import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation"; import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation";
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation"; import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation"; import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation";
import CompressSettings from "../components/tools/compress/CompressSettings"; import CompressSettings from "../components/tools/compress/CompressSettings";
import SplitSettings from "../components/tools/split/SplitSettings"; import SplitSettings from "../components/tools/split/SplitSettings";
@ -472,7 +474,10 @@ export function useFlatToolRegistry(): ToolRegistry {
"auto-rename-pdf-file": { "auto-rename-pdf-file": {
icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="match-word-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.auto-rename.title", "Auto Rename PDF File"), 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"), description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.AUTOMATION, subcategoryId: SubcategoryId.AUTOMATION,

View File

@ -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.'))
});
};

View File

@ -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<AutoRenameParameters>;
export const useAutoRenameParameters = (): AutoRenameParametersHook => {
return useBaseParameters({
defaultParameters,
endpointName: 'auto-rename',
});
};

View File

@ -8,6 +8,7 @@ export interface ApiCallsConfig<TParams = void> {
buildFormData: (params: TParams, file: File) => FormData; buildFormData: (params: TParams, file: File) => FormData;
filePrefix: string; filePrefix: string;
responseHandler?: ResponseHandler; responseHandler?: ResponseHandler;
preserveBackendFilename?: boolean;
} }
export const useToolApiCalls = <TParams = void>() => { export const useToolApiCalls = <TParams = void>() => {
@ -46,7 +47,8 @@ export const useToolApiCalls = <TParams = void>() => {
response.data, response.data,
[file], [file],
config.filePrefix, config.filePrefix,
config.responseHandler config.responseHandler,
config.preserveBackendFilename ? response.headers : undefined
); );
processedFiles.push(...responseFiles); processedFiles.push(...responseFiles);

View File

@ -33,6 +33,13 @@ interface BaseToolOperationConfig<TParams> {
/** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */ /** Prefix added to processed filenames (e.g., 'compressed_', 'split_') */
filePrefix: string; 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) */ /** How to handle API responses (e.g., ZIP extraction, single file response) */
responseHandler?: ResponseHandler; responseHandler?: ResponseHandler;
@ -178,7 +185,8 @@ export const useToolOperation = <TParams>(
endpoint: config.endpoint, endpoint: config.endpoint,
buildFormData: config.buildFormData, buildFormData: config.buildFormData,
filePrefix: config.filePrefix, filePrefix: config.filePrefix,
responseHandler: config.responseHandler responseHandler: config.responseHandler,
preserveBackendFilename: config.preserveBackendFilename
}; };
processedFiles = await processFiles( processedFiles = await processFiles(
params, params,

View File

@ -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;

View File

@ -25,7 +25,8 @@ export type ModeType =
| 'single-large-page' | 'single-large-page'
| 'repair' | 'repair'
| 'unlockPdfForms' | 'unlockPdfForms'
| 'removeCertificateSign'; | 'removeCertificateSign'
| 'auto-rename-pdf-file';
// Normalized state types // Normalized state types
export interface ProcessedFilePage { export interface ProcessedFilePage {

View File

@ -2,8 +2,8 @@ import axios from 'axios';
import { ToolRegistry } from '../data/toolsTaxonomy'; import { ToolRegistry } from '../data/toolsTaxonomy';
import { AUTOMATION_CONSTANTS } from '../constants/automation'; import { AUTOMATION_CONSTANTS } from '../constants/automation';
import { AutomationFileProcessor } from './automationFileProcessor'; import { AutomationFileProcessor } from './automationFileProcessor';
import { ResourceManager } from './resourceManager';
import { ToolType } from '../hooks/tools/shared/useToolOperation'; import { ToolType } from '../hooks/tools/shared/useToolOperation';
import { processResponse } from './toolResponseProcessor';
/** /**
@ -68,12 +68,17 @@ export const executeToolOperationWithPrefix = async (
let result; let result;
if (response.data.type === 'application/pdf' || if (response.data.type === 'application/pdf' ||
(response.headers && response.headers['content-type'] === 'application/pdf')) { (response.headers && response.headers['content-type'] === 'application/pdf')) {
// Single PDF response (e.g. split with merge option) - use original filename // Single PDF response (e.g. split with merge option) - use processResponse to respect preserveBackendFilename
const originalFileName = files[0]?.name || 'document.pdf'; const processedFiles = await processResponse(
const singleFile = new File([response.data], originalFileName, { type: 'application/pdf' }); response.data,
files,
filePrefix,
undefined,
config.preserveBackendFilename ? response.headers : undefined
);
result = { result = {
success: true, success: true,
files: [singleFile], files: processedFiles,
errors: [] errors: []
}; };
} else { } else {
@ -85,7 +90,8 @@ export const executeToolOperationWithPrefix = async (
console.warn(`⚠️ File processing warnings:`, result.errors); console.warn(`⚠️ File processing warnings:`, result.errors);
} }
// Apply prefix to files, replacing any existing prefix // 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 => { ? result.files.map(file => {
const nameWithoutPrefix = file.name.replace(/^[^_]*_/, ''); const nameWithoutPrefix = file.name.replace(/^[^_]*_/, '');
return new File([file], `${filePrefix}${nameWithoutPrefix}`, { type: file.type }); 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`); console.log(`📥 Response ${i+1} status: ${response.status}, size: ${response.data.size} bytes`);
// Create result file with automation prefix // Create result file using processResponse to respect preserveBackendFilename setting
const processedFiles = await processResponse(
const resultFile = ResourceManager.createResultFile(
response.data, response.data,
file.name, [file],
filePrefix filePrefix,
undefined,
config.preserveBackendFilename ? response.headers : undefined
); );
resultFiles.push(resultFile); resultFiles.push(...processedFiles);
console.log(`✅ Created result file: ${resultFile.name}`); console.log(`✅ Created result file(s): ${processedFiles.map(f => f.name).join(', ')}`);
} }
console.log(`🎉 Single-file processing complete: ${resultFiles.length} files`); console.log(`🎉 Single-file processing complete: ${resultFiles.length} files`);

View File

@ -1,23 +1,40 @@
// Note: This utility should be used with useToolResources for ZIP operations // Note: This utility should be used with useToolResources for ZIP operations
import { getFilenameFromHeaders } from './fileResponseUtils';
export type ResponseHandler = (blob: Blob, originalFiles: File[]) => Promise<File[]> | File[]; export type ResponseHandler = (blob: Blob, originalFiles: File[]) => Promise<File[]> | File[];
/** /**
* Processes a blob response into File(s). * Processes a blob response into File(s).
* - If a tool-specific responseHandler is provided, it is used. * - 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. * - Otherwise, create a single file using the filePrefix + original name.
*/ */
export async function processResponse( export async function processResponse(
blob: Blob, blob: Blob,
originalFiles: File[], originalFiles: File[],
filePrefix: string, filePrefix: string,
responseHandler?: ResponseHandler responseHandler?: ResponseHandler,
responseHeaders?: Record<string, any>
): Promise<File[]> { ): Promise<File[]> {
if (responseHandler) { if (responseHandler) {
const out = await responseHandler(blob, originalFiles); const out = await responseHandler(blob, originalFiles);
return Array.isArray(out) ? out : [out as unknown as File]; 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 original = originalFiles[0]?.name ?? 'result.pdf';
const name = `${filePrefix}${original}`; const name = `${filePrefix}${original}`;
const type = blob.type || 'application/octet-stream'; const type = blob.type || 'application/octet-stream';