From 0c087646692b5ae92fa208c8c3780d29dc3df10b Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:45:31 +0100 Subject: [PATCH] add attatchments tool (#4502) --- .../public/locales/en-GB/translation.json | 12 ++ .../public/locales/en-US/translation.json | 17 ++ .../src/data/useTranslatedToolRegistry.tsx | 10 +- .../useAddAttachmentsOperation.ts | 37 ++++ .../useAddAttachmentsParameters.ts | 35 ++++ frontend/src/tools/AddAttachments.tsx | 197 ++++++++++++++++++ 6 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 frontend/src/hooks/tools/addAttachments/useAddAttachmentsOperation.ts create mode 100644 frontend/src/hooks/tools/addAttachments/useAddAttachmentsParameters.ts create mode 100644 frontend/src/tools/AddAttachments.tsx diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 9a976c3af..8c0c6497d 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -3412,6 +3412,18 @@ "generateError": "We couldn't generate your API key." } }, + "AddAttachmentsRequest": { + "attachments": "Select Attachments", + "info": "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel.", + "selectFiles": "Select Files to Attach", + "placeholder": "Choose files...", + "addMoreFiles": "Add more files...", + "selectedFiles": "Selected Files", + "submit": "Add Attachments", + "results": { + "title": "Attachment Results" + } + }, "termsAndConditions": "Terms & Conditions", "logOut": "Log out" } \ No newline at end of file diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index f12e54fbf..5bdde8cb2 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -2362,5 +2362,22 @@ }, "automate": { "copyToSaved": "Copy to Saved" + }, + "AddAttachmentsRequest": { + "attachments": "Select Attachments", + "info": "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel.", + "selectFiles": "Select Files to Attach", + "placeholder": "Choose files...", + "addMoreFiles": "Add more files...", + "selectedFiles": "Selected Files", + "submit": "Add Attachments", + "results": { + "title": "Attachment Results" + } + }, + "addAttachments": { + "error": { + "failed": "An error occurred while adding attachments to the PDF." + } } } diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 32bf479b6..97aa9725c 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -15,6 +15,7 @@ import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy"; import { getSynonyms } from "../utils/toolSynonyms"; import AddWatermark from "../tools/AddWatermark"; import AddStamp from "../tools/AddStamp"; +import AddAttachments from "../tools/AddAttachments"; import Merge from '../tools/Merge'; import Repair from "../tools/Repair"; import AutoRename from "../tools/AutoRename"; @@ -35,6 +36,7 @@ import { sanitizeOperationConfig } from "../hooks/tools/sanitize/useSanitizeOper import { repairOperationConfig } from "../hooks/tools/repair/useRepairOperation"; import { addWatermarkOperationConfig } from "../hooks/tools/addWatermark/useAddWatermarkOperation"; import { addStampOperationConfig } from "../components/tools/addStamp/useAddStampOperation"; +import { addAttachmentsOperationConfig } from "../hooks/tools/addAttachments/useAddAttachmentsOperation"; import { unlockPdfFormsOperationConfig } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation"; import { singleLargePageOperationConfig } from "../hooks/tools/singleLargePage/useSingleLargePageOperation"; import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation"; @@ -461,12 +463,14 @@ export function useFlatToolRegistry(): ToolRegistry { addAttachments: { icon: , name: t("home.addAttachments.title", "Add Attachments"), - component: null, - + component: AddAttachments, description: t("home.addAttachments.desc", "Add or remove embedded files (attachments) to/from a PDF"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, - synonyms: getSynonyms(t, "addAttachments") + synonyms: getSynonyms(t, "addAttachments"), + maxFiles: 1, + endpoints: ["add-attachments"], + operationConfig: addAttachmentsOperationConfig, }, // Extraction diff --git a/frontend/src/hooks/tools/addAttachments/useAddAttachmentsOperation.ts b/frontend/src/hooks/tools/addAttachments/useAddAttachmentsOperation.ts new file mode 100644 index 000000000..91e216c18 --- /dev/null +++ b/frontend/src/hooks/tools/addAttachments/useAddAttachmentsOperation.ts @@ -0,0 +1,37 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation, ToolOperationConfig, ToolType } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { AddAttachmentsParameters } from './useAddAttachmentsParameters'; + +const buildFormData = (parameters: AddAttachmentsParameters, file: File): FormData => { + const formData = new FormData(); + + // Add the main PDF file (single file per request in singleFile mode) + if (file) { + formData.append("fileInput", file); + } + + // Add attachment files + (parameters.attachments || []).forEach((attachment) => { + if (attachment) formData.append("attachments", attachment); + }); + + return formData; +}; + +// Operation configuration for automation +export const addAttachmentsOperationConfig: ToolOperationConfig = { + toolType: ToolType.singleFile, + buildFormData, + operationType: 'addAttachments', + endpoint: '/api/v1/misc/add-attachments', +}; + +export const useAddAttachmentsOperation = () => { + const { t } = useTranslation(); + + return useToolOperation({ + ...addAttachmentsOperationConfig, + getErrorMessage: createStandardErrorHandler(t('addAttachments.error.failed', 'An error occurred while adding attachments to the PDF.')) + }); +}; diff --git a/frontend/src/hooks/tools/addAttachments/useAddAttachmentsParameters.ts b/frontend/src/hooks/tools/addAttachments/useAddAttachmentsParameters.ts new file mode 100644 index 000000000..ce21e3869 --- /dev/null +++ b/frontend/src/hooks/tools/addAttachments/useAddAttachmentsParameters.ts @@ -0,0 +1,35 @@ +import { useState } from 'react'; + +export interface AddAttachmentsParameters { + attachments: File[]; +} + +const defaultParameters: AddAttachmentsParameters = { + attachments: [] +}; + +export const useAddAttachmentsParameters = () => { + const [parameters, setParameters] = useState(defaultParameters); + + const updateParameter = ( + key: K, + value: AddAttachmentsParameters[K] + ) => { + setParameters(prev => ({ ...prev, [key]: value })); + }; + + const resetParameters = () => { + setParameters(defaultParameters); + }; + + const validateParameters = (): boolean => { + return parameters.attachments.length > 0; + }; + + return { + parameters, + updateParameter, + resetParameters, + validateParameters + }; +}; diff --git a/frontend/src/tools/AddAttachments.tsx b/frontend/src/tools/AddAttachments.tsx new file mode 100644 index 000000000..2b090e331 --- /dev/null +++ b/frontend/src/tools/AddAttachments.tsx @@ -0,0 +1,197 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useFileSelection } from "../contexts/FileContext"; +import { createToolFlow } from "../components/tools/shared/createToolFlow"; +import { BaseToolProps, ToolComponent } from "../types/tool"; +import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useAddAttachmentsParameters } from "../hooks/tools/addAttachments/useAddAttachmentsParameters"; +import { useAddAttachmentsOperation } from "../hooks/tools/addAttachments/useAddAttachmentsOperation"; +import { Stack, Text, Group, ActionIcon, Alert, ScrollArea, Button } from "@mantine/core"; +import LocalIcon from "../components/shared/LocalIcon"; +import { useAccordionSteps } from "../hooks/tools/shared/useAccordionSteps"; +// Removed FitText for two-line wrapping with clamping + +const AddAttachments = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { selectedFiles } = useFileSelection(); + + const params = useAddAttachmentsParameters(); + const operation = useAddAttachmentsOperation(); + + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("add-attachments"); + + useEffect(() => { + operation.resetResults(); + onPreviewFile?.(null); + }, [params.parameters]); + + const handleExecute = async () => { + try { + await operation.executeOperation(params.parameters, selectedFiles); + if (operation.files && onComplete) { + onComplete(operation.files); + } + } catch (error: any) { + onError?.(error?.message || t("AddAttachmentsRequest.error.failed", "Add attachments operation failed")); + } + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = operation.files.length > 0 || operation.downloadUrl !== null; + + enum AddAttachmentsStep { + NONE = 'none', + ATTACHMENTS = 'attachments' + } + + const accordion = useAccordionSteps({ + noneValue: AddAttachmentsStep.NONE, + initialStep: AddAttachmentsStep.ATTACHMENTS, + stateConditions: { + hasFiles, + hasResults: false // Don't collapse when there are results for add attachments + }, + afterResults: () => { + operation.resetResults(); + onPreviewFile?.(null); + } + }); + + const getSteps = () => { + const steps: any[] = []; + + // Step 1: Attachments Selection + steps.push({ + title: t("AddAttachmentsRequest.attachments", "Select Attachments"), + isCollapsed: accordion.getCollapsedState(AddAttachmentsStep.ATTACHMENTS), + onCollapsedClick: () => accordion.handleStepToggle(AddAttachmentsStep.ATTACHMENTS), + isVisible: true, + content: ( + + + + {t("AddAttachmentsRequest.info", "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel.")} + + + + + + {t("AddAttachmentsRequest.selectFiles", "Select Files to Attach")} + + { + const files = Array.from(e.target.files || []); + // Append to existing attachments instead of replacing + const newAttachments = [...params.parameters.attachments, ...files]; + params.updateParameter('attachments', newAttachments); + // Reset the input so the same file can be selected again + e.target.value = ''; + }} + disabled={endpointLoading} + style={{ display: 'none' }} + id="attachments-input" + /> + + + + {params.parameters.attachments && params.parameters.attachments.length > 0 && ( + + + {t("AddAttachmentsRequest.selectedFiles", "Selected Files")} ({params.parameters.attachments.length}) + + + + {params.parameters.attachments.map((file, index) => ( + + + {/* Filename (two-line clamp, wraps, no icon on the left) */} +
+
+ {file.name} +
+
+ + ({(file.size / 1024).toFixed(1)} KB) + +
+ { + const newAttachments = params.parameters.attachments.filter((_, i) => i !== index); + params.updateParameter('attachments', newAttachments); + }} + > + + +
+ ))} +
+
+
+ )} +
+ ), + }); + + return steps; + }; + + return createToolFlow({ + files: { + selectedFiles, + isCollapsed: hasResults, + }, + steps: getSteps(), + executeButton: { + text: t('AddAttachmentsRequest.submit', 'Add Attachments'), + isVisible: !hasResults, + loadingText: t('loading'), + onClick: handleExecute, + disabled: !params.validateParameters() || !hasFiles || !endpointEnabled, + }, + review: { + isVisible: hasResults, + operation: operation, + title: t('AddAttachmentsRequest.results.title', 'Attachment Results'), + onFileClick: (file) => onPreviewFile?.(file), + onUndo: async () => { + await operation.undoOperation(); + onPreviewFile?.(null); + }, + }, + }); +}; + +AddAttachments.tool = () => useAddAttachmentsOperation; + +export default AddAttachments as ToolComponent;