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.length > 0
+ ? t("AddAttachmentsRequest.addMoreFiles", "Add more files...")
+ : t("AddAttachmentsRequest.placeholder", "Choose files...")
+ }
+
+
+
+ {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.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;