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."
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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: <LocalIcon icon="attachment-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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
|
||||
|
@ -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