Initial commit of Merge UI

This commit is contained in:
James Brunton 2025-08-19 12:15:21 +01:00
parent 23d86deae7
commit 86831928c7
9 changed files with 525 additions and 0 deletions

View File

@ -0,0 +1,59 @@
import React from 'react';
import { Stack, Select, Checkbox, Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { MergeParameters } from '../../../hooks/tools/merge/useMergeParameters';
interface MergeSettingsProps {
parameters: MergeParameters;
onParameterChange: <K extends keyof MergeParameters>(key: K, value: MergeParameters[K]) => void;
disabled?: boolean;
}
const MergeSettings: React.FC<MergeSettingsProps> = ({
parameters,
onParameterChange,
disabled = false,
}) => {
const { t } = useTranslation();
const mergeOrderOptions = [
{ value: 'orderProvided', label: t('merge.orderBy.orderProvided', 'Dragging Files') },
{ value: 'byFileName', label: t('merge.orderBy.byFileName', 'By File Name') },
{ value: 'byDateModified', label: t('merge.orderBy.byDateModified', 'By Date Modified') },
{ value: 'byDateCreated', label: t('merge.orderBy.byDateCreated', 'By Date Created') },
{ value: 'byPDFTitle', label: t('merge.orderBy.byPDFTitle', 'By PDF Title') },
];
return (
<Stack gap="md">
<div>
<Text size="sm" fw={500} mb="xs">
{t('merge.orderBy.title', 'Merge Order')}
</Text>
<Select
data={mergeOrderOptions}
value={parameters.mergeOrder}
onChange={(value) => onParameterChange('mergeOrder', value as MergeParameters['mergeOrder'])}
disabled={disabled}
placeholder={t('merge.orderBy.placeholder', 'Select merge order')}
/>
</div>
<Checkbox
label={t('merge.removeDigitalSignature', 'Remove digital signature in the merged file?')}
checked={parameters.removeDigitalSignature}
onChange={(event) => onParameterChange('removeDigitalSignature', event.currentTarget.checked)}
disabled={disabled}
/>
<Checkbox
label={t('merge.generateTableOfContents', 'Generate table of contents in the merged file?')}
checked={parameters.generateTableOfContents}
onChange={(event) => onParameterChange('generateTableOfContents', event.currentTarget.checked)}
disabled={disabled}
/>
</Stack>
);
};
export default MergeSettings;

View File

@ -0,0 +1,35 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation, ResponseHandler } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { MergeParameters } from './useMergeParameters';
const buildFormData = (parameters: MergeParameters, files: File[]): FormData => {
const formData = new FormData();
files.forEach((file) => {
formData.append("fileInput", file);
});
formData.append("sortType", parameters.mergeOrder);
formData.append("removeCertSign", parameters.removeDigitalSignature.toString());
formData.append("generateToc", parameters.generateTableOfContents.toString());
return formData;
};
const mergeResponseHandler: ResponseHandler = (blob: Blob, originalFiles: File[]): File[] => {
return [new File([blob], 'merged.pdf', { type: 'application/pdf' })];
};
export const useMergeOperation = () => {
const { t } = useTranslation();
return useToolOperation<MergeParameters>({
operationType: 'merge',
endpoint: '/api/v1/general/merge-pdfs',
buildFormData,
filePrefix: 'merged_',
multiFileEndpoint: true, // Single API call with all files
responseHandler: mergeResponseHandler, // Handle single PDF response
getErrorMessage: createStandardErrorHandler(t('merge.error.failed', 'An error occurred while merging the PDFs.'))
});
};

View File

@ -0,0 +1,39 @@
import { useState, useCallback } from 'react';
export interface MergeParameters {
mergeOrder: 'orderProvided' | 'byFileName' | 'byDateModified' | 'byDateCreated' | 'byPDFTitle';
removeDigitalSignature: boolean;
generateTableOfContents: boolean;
}
export const defaultMergeParameters: MergeParameters = {
mergeOrder: 'orderProvided',
removeDigitalSignature: false,
generateTableOfContents: false,
};
export const useMergeParameters = () => {
const [parameters, setParameters] = useState<MergeParameters>(defaultMergeParameters);
const updateParameter = useCallback(<K extends keyof MergeParameters>(
key: K,
value: MergeParameters[K]
) => {
setParameters(prev => ({ ...prev, [key]: value }));
}, []);
const validateParameters = useCallback((): boolean => {
return true; // Merge has no required parameters
}, []);
const resetParameters = useCallback(() => {
setParameters(defaultMergeParameters);
}, []);
return {
parameters,
updateParameter,
validateParameters,
resetParameters,
};
};

View File

@ -2,6 +2,7 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy";
import MergeIcon from "@mui/icons-material/Merge";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
interface ToolManagementResult {

View File

@ -0,0 +1,95 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
import { useFileContext } from "../contexts/FileContext";
import { useToolFileSelection } from "../contexts/FileSelectionContext";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import MergeSettings from "../components/tools/merge/MergeSettings";
import { useMergeParameters } from "../hooks/tools/merge/useMergeParameters";
import { useMergeOperation } from "../hooks/tools/merge/useMergeOperation";
import { BaseToolProps } from "../types/tool";
const Merge = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
const { setCurrentMode } = useFileContext();
const { selectedFiles } = useToolFileSelection();
const mergeParams = useMergeParameters();
const mergeOperation = useMergeOperation();
// Endpoint validation
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("merge-pdfs");
useEffect(() => {
mergeOperation.resetResults();
onPreviewFile?.(null);
}, [mergeParams.parameters]);
const handleMerge = async () => {
try {
await mergeOperation.executeOperation(mergeParams.parameters, selectedFiles);
if (mergeOperation.files && onComplete) {
onComplete(mergeOperation.files);
}
} catch (error) {
if (onError) {
onError(error instanceof Error ? error.message : "Merge operation failed");
}
}
};
const handleThumbnailClick = (file: File) => {
onPreviewFile?.(file);
sessionStorage.setItem("previousMode", "merge");
setCurrentMode("viewer");
};
const handleSettingsReset = () => {
mergeOperation.resetResults();
onPreviewFile?.(null);
setCurrentMode("merge");
};
const hasFiles = selectedFiles.length > 1; // Merge requires at least 2 files
const hasResults = mergeOperation.files.length > 0 || mergeOperation.downloadUrl !== null;
const settingsCollapsed = !hasFiles || hasResults;
return createToolFlow({
files: {
selectedFiles,
isCollapsed: hasFiles && !hasResults,
placeholder: "Select multiple PDF files to merge",
},
steps: [
{
title: "Settings",
isCollapsed: settingsCollapsed,
onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined,
content: (
<MergeSettings
parameters={mergeParams.parameters}
onParameterChange={mergeParams.updateParameter}
disabled={endpointLoading}
/>
),
},
],
executeButton: {
text: t("merge.submit", "Merge PDFs"),
isVisible: !hasResults,
loadingText: t("loading"),
onClick: handleMerge,
disabled: !mergeParams.validateParameters() || !hasFiles || !endpointEnabled,
},
review: {
isVisible: hasResults,
operation: mergeOperation,
title: t("merge.title", "Merge Results"),
onFileClick: handleThumbnailClick,
},
});
};
export default Merge;

74
testing/test_pdf_1.pdf Normal file
View File

@ -0,0 +1,74 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 612 792 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (anonymous) /CreationDate (D:20250819094504+01'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250819094504+01'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 147
>>
stream
GarW00abco&4HDcidm(mI,3'DZY:^WQ,7!K+Bf&Mo_p+bJu"KZ.3A(M3%pEBpBe"=Bb3[h-Xt2ROZoe^Q)8NH>;#5qqB`Oee86NZp2V9^`:9`Y'Dq([aoCS4Veh*jH9C%+DV`*GHUK^ngc-TW~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000333 00000 n
0000000526 00000 n
0000000594 00000 n
0000000890 00000 n
0000000949 00000 n
trailer
<<
/ID
[<cb35d644a26f0c9be3597a7f8189b123><cb35d644a26f0c9be3597a7f8189b123>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1186
%%EOF

74
testing/test_pdf_2.pdf Normal file
View File

@ -0,0 +1,74 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 612 792 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (anonymous) /CreationDate (D:20250819094504+01'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250819094504+01'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 147
>>
stream
GarW00abco&4HDcidm(mI,3'DZY:^WQ,7!K+Bf&Mo_p+bJu"KZ.3A(M3%pEBpBe"=Bb3[h-Xt2ROZoe^Q)8NH>;#5qqB`Oee86NZp3Iif`:9`Y'Dq([aoCS4Veh*jH9C%+DV`*GHUK^ngmo`i~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000333 00000 n
0000000526 00000 n
0000000594 00000 n
0000000890 00000 n
0000000949 00000 n
trailer
<<
/ID
[<46f6a3460762da2956d1d3fc19ab996f><46f6a3460762da2956d1d3fc19ab996f>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1186
%%EOF

74
testing/test_pdf_3.pdf Normal file
View File

@ -0,0 +1,74 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 612 792 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (anonymous) /CreationDate (D:20250819094504+01'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250819094504+01'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 147
>>
stream
GarW0YmS?5&4HDC`<2TCEOpM_A^cO6ZEVtG&1rQ7k5R.W5uPe>'T[Ma*9KfZqZs*-57""%'<u)dPtNs!.p_7Cem+LKojd:CaF,4$g:S_<`9sPL'Dq([aoCSX;_^WU4Wa'KgNd255,.iQh#\m&~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000333 00000 n
0000000526 00000 n
0000000594 00000 n
0000000890 00000 n
0000000949 00000 n
trailer
<<
/ID
[<8c4eba11c30780ded30147f80c0aa46f><8c4eba11c30780ded30147f80c0aa46f>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1186
%%EOF

74
testing/test_pdf_4.pdf Normal file
View File

@ -0,0 +1,74 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 8 0 R /MediaBox [ 0 0 612 792 ] /Parent 7 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/PageMode /UseNone /Pages 7 0 R /Type /Catalog
>>
endobj
6 0 obj
<<
/Author (anonymous) /CreationDate (D:20250819094504+01'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250819094504+01'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
7 0 obj
<<
/Count 1 /Kids [ 4 0 R ] /Type /Pages
>>
endobj
8 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 147
>>
stream
GarW00abco&4HDcidm(mI,3'DZY:^WQ,7!K+Bf&Mo_p+bJu"KZ.3A(M3%pEBpBe"=Bb3[h-Xt2ROZoe^Q)8NH>;#5qqB`Oee86NZp3%Qb`:9`Y'Dq([aoCS4Veh*jH9C%+DV`*GHUK^nh.J$8~>endstream
endobj
xref
0 9
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000333 00000 n
0000000526 00000 n
0000000594 00000 n
0000000890 00000 n
0000000949 00000 n
trailer
<<
/ID
[<ade40b97468692afaf20f74813f90619><ade40b97468692afaf20f74813f90619>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 6 0 R
/Root 5 0 R
/Size 9
>>
startxref
1186
%%EOF