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/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", diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 724c14398..05cce1823 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1584,7 +1584,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 5265483fc..7ee731585 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -1129,7 +1129,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/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", 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/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/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") }, 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 ebc734b8c..340b6b27c 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"; @@ -33,6 +34,7 @@ import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCerti import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation"; import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation"; +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"; @@ -492,7 +494,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/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', 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';