mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
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:
parent
1898df0df9
commit
11d23a2d43
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
@ -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
|
||||||
|
@ -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 />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>)
|
||||||
|
22
frontend/src/components/tooltips/useAutoRenameTips.ts
Normal file
22
frontend/src/components/tooltips/useAutoRenameTips.ts
Normal 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")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
@ -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,
|
||||||
|
@ -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.'))
|
||||||
|
});
|
||||||
|
};
|
@ -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',
|
||||||
|
});
|
||||||
|
};
|
@ -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);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
44
frontend/src/tools/AutoRename.tsx
Normal file
44
frontend/src/tools/AutoRename.tsx
Normal 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;
|
@ -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 {
|
||||||
|
@ -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`);
|
||||||
|
@ -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';
|
||||||
|
Loading…
Reference in New Issue
Block a user