mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
add attatchments tool (#4502)
This commit is contained in:
parent
9758e871d4
commit
0c08764669
@ -3412,6 +3412,18 @@
|
|||||||
"generateError": "We couldn't generate your API key."
|
"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",
|
"termsAndConditions": "Terms & Conditions",
|
||||||
"logOut": "Log out"
|
"logOut": "Log out"
|
||||||
}
|
}
|
@ -2362,5 +2362,22 @@
|
|||||||
},
|
},
|
||||||
"automate": {
|
"automate": {
|
||||||
"copyToSaved": "Copy to Saved"
|
"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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
|
|||||||
import { getSynonyms } from "../utils/toolSynonyms";
|
import { getSynonyms } from "../utils/toolSynonyms";
|
||||||
import AddWatermark from "../tools/AddWatermark";
|
import AddWatermark from "../tools/AddWatermark";
|
||||||
import AddStamp from "../tools/AddStamp";
|
import AddStamp from "../tools/AddStamp";
|
||||||
|
import AddAttachments from "../tools/AddAttachments";
|
||||||
import Merge from '../tools/Merge';
|
import Merge from '../tools/Merge';
|
||||||
import Repair from "../tools/Repair";
|
import Repair from "../tools/Repair";
|
||||||
import AutoRename from "../tools/AutoRename";
|
import AutoRename from "../tools/AutoRename";
|
||||||
@ -35,6 +36,7 @@ import { sanitizeOperationConfig } from "../hooks/tools/sanitize/useSanitizeOper
|
|||||||
import { repairOperationConfig } from "../hooks/tools/repair/useRepairOperation";
|
import { repairOperationConfig } from "../hooks/tools/repair/useRepairOperation";
|
||||||
import { addWatermarkOperationConfig } from "../hooks/tools/addWatermark/useAddWatermarkOperation";
|
import { addWatermarkOperationConfig } from "../hooks/tools/addWatermark/useAddWatermarkOperation";
|
||||||
import { addStampOperationConfig } from "../components/tools/addStamp/useAddStampOperation";
|
import { addStampOperationConfig } from "../components/tools/addStamp/useAddStampOperation";
|
||||||
|
import { addAttachmentsOperationConfig } from "../hooks/tools/addAttachments/useAddAttachmentsOperation";
|
||||||
import { unlockPdfFormsOperationConfig } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
|
import { unlockPdfFormsOperationConfig } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
|
||||||
import { singleLargePageOperationConfig } from "../hooks/tools/singleLargePage/useSingleLargePageOperation";
|
import { singleLargePageOperationConfig } from "../hooks/tools/singleLargePage/useSingleLargePageOperation";
|
||||||
import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
|
import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
|
||||||
@ -461,12 +463,14 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
addAttachments: {
|
addAttachments: {
|
||||||
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.addAttachments.title", "Add Attachments"),
|
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"),
|
description: t("home.addAttachments.desc", "Add or remove embedded files (attachments) to/from a PDF"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
synonyms: getSynonyms(t, "addAttachments")
|
synonyms: getSynonyms(t, "addAttachments"),
|
||||||
|
maxFiles: 1,
|
||||||
|
endpoints: ["add-attachments"],
|
||||||
|
operationConfig: addAttachmentsOperationConfig,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Extraction
|
// Extraction
|
||||||
|
@ -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<AddAttachmentsParameters> = {
|
||||||
|
toolType: ToolType.singleFile,
|
||||||
|
buildFormData,
|
||||||
|
operationType: 'addAttachments',
|
||||||
|
endpoint: '/api/v1/misc/add-attachments',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddAttachmentsOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return useToolOperation<AddAttachmentsParameters>({
|
||||||
|
...addAttachmentsOperationConfig,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('addAttachments.error.failed', 'An error occurred while adding attachments to the PDF.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export interface AddAttachmentsParameters {
|
||||||
|
attachments: File[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultParameters: AddAttachmentsParameters = {
|
||||||
|
attachments: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddAttachmentsParameters = () => {
|
||||||
|
const [parameters, setParameters] = useState<AddAttachmentsParameters>(defaultParameters);
|
||||||
|
|
||||||
|
const updateParameter = <K extends keyof AddAttachmentsParameters>(
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
197
frontend/src/tools/AddAttachments.tsx
Normal file
197
frontend/src/tools/AddAttachments.tsx
Normal file
@ -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<AddAttachmentsStep>({
|
||||||
|
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: (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Alert color="blue" variant="light">
|
||||||
|
<Text size="sm">
|
||||||
|
{t("AddAttachmentsRequest.info", "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel.")}
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t("AddAttachmentsRequest.selectFiles", "Select Files to Attach")}
|
||||||
|
</Text>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
onChange={(e) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="blue"
|
||||||
|
component="label"
|
||||||
|
htmlFor="attachments-input"
|
||||||
|
disabled={endpointLoading}
|
||||||
|
leftSection={<LocalIcon icon="plus" width="14" height="14" />}
|
||||||
|
>
|
||||||
|
{params.parameters.attachments.length > 0
|
||||||
|
? t("AddAttachmentsRequest.addMoreFiles", "Add more files...")
|
||||||
|
: t("AddAttachmentsRequest.placeholder", "Choose files...")
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{params.parameters.attachments && params.parameters.attachments.length > 0 && (
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t("AddAttachmentsRequest.selectedFiles", "Selected Files")} ({params.parameters.attachments.length})
|
||||||
|
</Text>
|
||||||
|
<ScrollArea.Autosize mah={300} type="scroll" offsetScrollbars styles={{ viewport: { overflowX: 'hidden' } }}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
{params.parameters.attachments.map((file, index) => (
|
||||||
|
<Group key={index} justify="space-between" p="xs" style={{ border: '1px solid var(--mantine-color-gray-3)', borderRadius: 'var(--mantine-radius-sm)', alignItems: 'flex-start' }}>
|
||||||
|
<Group gap="xs" style={{ flex: 1, minWidth: 0, alignItems: 'flex-start' }}>
|
||||||
|
{/* Filename (two-line clamp, wraps, no icon on the left) */}
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 'var(--mantine-font-size-sm)',
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 2 as any,
|
||||||
|
WebkitBoxOrient: 'vertical' as any,
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
title={file.name}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Text size="xs" c="dimmed" style={{ flexShrink: 0 }}>
|
||||||
|
({(file.size / 1024).toFixed(1)} KB)
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<ActionIcon
|
||||||
|
size="sm"
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
|
onClick={() => {
|
||||||
|
const newAttachments = params.parameters.attachments.filter((_, i) => i !== index);
|
||||||
|
params.updateParameter('attachments', newAttachments);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LocalIcon icon="close-rounded" width="14" height="14" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea.Autosize>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
Loading…
Reference in New Issue
Block a user