Refactor into tool steps

This commit is contained in:
James Brunton 2025-09-11 17:19:15 +01:00
parent 6d04a80216
commit 55b8455b66
12 changed files with 637 additions and 266 deletions

View File

@ -1143,6 +1143,9 @@
"true": "True",
"false": "False"
},
"advanced": {
"title": "Advanced Options"
},
"customFields": {
"title": "Custom Metadata",
"description": "Add custom metadata fields to the document",
@ -1185,6 +1188,27 @@
"bullet1": "Custom Metadata: Add your own key-value pairs",
"bullet2": "Trapped Status: High-quality printing setting",
"bullet3": "Delete All: Remove all metadata for privacy"
},
"deleteAll": {
"title": "Delete All Metadata",
"text": "Complete metadata removal for privacy and clean documents."
},
"customFields": {
"title": "Custom Metadata",
"text": "Add your own custom key-value metadata pairs.",
"bullet1": "Add any custom fields relevant to your document",
"bullet2": "Examples: Department, Project, Version, Status",
"bullet3": "Both key and value are required for each entry"
},
"advanced": {
"title": "Advanced Options",
"trapped": {
"title": "Trapped Status",
"description": "Indicates if document is prepared for high-quality printing.",
"bullet1": "True: Document has been trapped for printing",
"bullet2": "False: Document has not been trapped",
"bullet3": "Unknown: Trapped status is not specified"
}
}
}
},

View File

@ -1,246 +0,0 @@
import { Stack, TextInput, Select, Checkbox, Button, Group, Divider, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useEffect, useState } from "react";
import { ChangeMetadataParameters } from "../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
import { TrappedStatus } from "../../../types/metadata";
import { PDFMetadataService } from "../../../services/pdfMetadataService";
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
interface ChangeMetadataSettingsProps {
parameters: ChangeMetadataParameters;
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
disabled?: boolean;
addCustomMetadata: (key?: string, value?: string) => void;
removeCustomMetadata: (id: string) => void;
updateCustomMetadata: (id: string, key: string, value: string) => void;
}
const ChangeMetadataSettings = ({
parameters,
onParameterChange,
disabled = false,
addCustomMetadata,
removeCustomMetadata,
updateCustomMetadata
}: ChangeMetadataSettingsProps) => {
const { t } = useTranslation();
const { selectedFiles } = useSelectedFiles();
const [isExtractingMetadata, setIsExtractingMetadata] = useState(false);
const [hasExtractedMetadata, setHasExtractedMetadata] = useState(false);
const isDeleteAllEnabled = parameters.deleteAll;
const fieldsDisabled = disabled || isDeleteAllEnabled || isExtractingMetadata;
// Extract metadata from first file when files change
useEffect(() => {
const extractMetadata = async () => {
if (selectedFiles.length === 0 || hasExtractedMetadata) {
return;
}
const firstFile = selectedFiles[0];
if (!firstFile) {
return;
}
setIsExtractingMetadata(true);
try {
const result = await PDFMetadataService.extractMetadata(firstFile);
if (result.success) {
const metadata = result.metadata;
// Pre-populate all fields with extracted metadata
onParameterChange('title', metadata.title);
onParameterChange('author', metadata.author);
onParameterChange('subject', metadata.subject);
onParameterChange('keywords', metadata.keywords);
onParameterChange('creator', metadata.creator);
onParameterChange('producer', metadata.producer);
onParameterChange('creationDate', metadata.creationDate);
onParameterChange('modificationDate', metadata.modificationDate);
onParameterChange('trapped', metadata.trapped);
// Set custom metadata entries directly to avoid state update timing issues
onParameterChange('customMetadata', metadata.customMetadata);
setHasExtractedMetadata(true);
}
} catch (error) {
console.warn('Failed to extract metadata:', error);
} finally {
setIsExtractingMetadata(false);
}
};
extractMetadata();
}, [selectedFiles, hasExtractedMetadata, onParameterChange, addCustomMetadata, updateCustomMetadata, removeCustomMetadata, parameters.customMetadata]);
return (
<Stack gap="md">
{/* Delete All Option */}
<Checkbox
label={t('changeMetadata.deleteAll.label', 'Delete all metadata')}
description={t('changeMetadata.deleteAll.description', 'Remove all metadata from the PDF document')}
checked={parameters.deleteAll}
onChange={(e) => onParameterChange('deleteAll', e.target.checked)}
disabled={disabled}
/>
<Divider />
{/* Standard Metadata Fields */}
<Stack gap="md">
<Text size="sm" fw={500}>
{t('changeMetadata.standardFields.title', 'Standard Metadata')}
</Text>
<TextInput
label={t('changeMetadata.title.label', 'Title')}
placeholder={t('changeMetadata.title.placeholder', 'Document title')}
value={parameters.title}
onChange={(e) => onParameterChange('title', e.target.value)}
disabled={fieldsDisabled}
/>
<TextInput
label={t('changeMetadata.author.label', 'Author')}
placeholder={t('changeMetadata.author.placeholder', 'Document author')}
value={parameters.author}
onChange={(e) => onParameterChange('author', e.target.value)}
disabled={fieldsDisabled}
/>
<TextInput
label={t('changeMetadata.subject.label', 'Subject')}
placeholder={t('changeMetadata.subject.placeholder', 'Document subject')}
value={parameters.subject}
onChange={(e) => onParameterChange('subject', e.target.value)}
disabled={fieldsDisabled}
/>
<TextInput
label={t('changeMetadata.keywords.label', 'Keywords')}
placeholder={t('changeMetadata.keywords.placeholder', 'Document keywords')}
value={parameters.keywords}
onChange={(e) => onParameterChange('keywords', e.target.value)}
disabled={fieldsDisabled}
/>
<TextInput
label={t('changeMetadata.creator.label', 'Creator')}
placeholder={t('changeMetadata.creator.placeholder', 'Document creator')}
value={parameters.creator}
onChange={(e) => onParameterChange('creator', e.target.value)}
disabled={fieldsDisabled}
/>
<TextInput
label={t('changeMetadata.producer.label', 'Producer')}
placeholder={t('changeMetadata.producer.placeholder', 'Document producer')}
value={parameters.producer}
onChange={(e) => onParameterChange('producer', e.target.value)}
disabled={fieldsDisabled}
/>
<Divider />
{/* Date Fields */}
<Text size="sm" fw={500}>
{t('changeMetadata.dates.title', 'Document Dates')}
</Text>
<Text size="xs" c="dimmed">
{t('changeMetadata.dates.format', 'Format: yyyy/MM/dd HH:mm:ss')}
</Text>
<TextInput
label={t('changeMetadata.creationDate.label', 'Creation Date')}
placeholder={t('changeMetadata.creationDate.placeholder', 'e.g. 2025/01/17 14:30:00')}
value={parameters.creationDate}
onChange={(e) => onParameterChange('creationDate', e.target.value)}
disabled={fieldsDisabled}
/>
<TextInput
label={t('changeMetadata.modificationDate.label', 'Modification Date')}
placeholder={t('changeMetadata.modificationDate.placeholder', 'e.g. 2025/01/17 14:30:00')}
value={parameters.modificationDate}
onChange={(e) => onParameterChange('modificationDate', e.target.value)}
disabled={fieldsDisabled}
/>
{/* Trapped Status */}
<Select
label={t('changeMetadata.trapped.label', 'Trapped Status')}
description={t('changeMetadata.trapped.description', 'Indicates whether the document has been trapped for high-quality printing')}
value={parameters.trapped}
onChange={(value) => {
if (value) {
onParameterChange('trapped', value as TrappedStatus);
}
}}
disabled={fieldsDisabled}
data={[
{ value: TrappedStatus.UNKNOWN, label: t('changeMetadata.trapped.unknown', 'Unknown') },
{ value: TrappedStatus.TRUE, label: t('changeMetadata.trapped.true', 'True') },
{ value: TrappedStatus.FALSE, label: t('changeMetadata.trapped.false', 'False') }
]}
/>
<Divider />
{/* Custom Metadata */}
<Stack gap="sm">
<Group justify="space-between" align="center">
<Text size="sm" fw={500}>
{t('changeMetadata.customFields.title', 'Custom Metadata')}
</Text>
<Button
size="xs"
variant="light"
onClick={() => addCustomMetadata()}
disabled={fieldsDisabled}
>
{t('changeMetadata.customFields.add', 'Add Field')}
</Button>
</Group>
{parameters.customMetadata.length > 0 && (
<Text size="xs" c="dimmed">
{t('changeMetadata.customFields.description', 'Add custom metadata fields to the document')}
</Text>
)}
{parameters.customMetadata.map((entry) => (
<Stack key={entry.id} gap="xs">
<TextInput
placeholder={t('changeMetadata.customFields.keyPlaceholder', 'Custom key')}
value={entry.key}
onChange={(e) => updateCustomMetadata(entry.id, e.target.value, entry.value)}
disabled={fieldsDisabled}
/>
<TextInput
placeholder={t('changeMetadata.customFields.valuePlaceholder', 'Custom value')}
value={entry.value}
onChange={(e) => updateCustomMetadata(entry.id, entry.key, e.target.value)}
disabled={fieldsDisabled}
/>
<Button
size="xs"
variant="light"
color="red"
onClick={() => removeCustomMetadata(entry.id)}
disabled={fieldsDisabled}
>
{t('changeMetadata.customFields.remove', 'Remove')}
</Button>
</Stack>
))}
</Stack>
</Stack>
</Stack>
);
};
export default ChangeMetadataSettings;

View File

@ -0,0 +1,121 @@
import { Stack, Divider, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ChangeMetadataParameters } from "../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
import { useMetadataExtraction } from "../../../hooks/tools/changeMetadata/useMetadataExtraction";
import DeleteAllStep from "./steps/DeleteAllStep";
import StandardMetadataStep from "./steps/StandardMetadataStep";
import DocumentDatesStep from "./steps/DocumentDatesStep";
import CustomMetadataStep from "./steps/CustomMetadataStep";
import AdvancedOptionsStep from "./steps/AdvancedOptionsStep";
interface ChangeMetadataSingleStepProps {
parameters: ChangeMetadataParameters;
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
disabled?: boolean;
addCustomMetadata: (key?: string, value?: string) => void;
removeCustomMetadata: (id: string) => void;
updateCustomMetadata: (id: string, key: string, value: string) => void;
}
const ChangeMetadataSingleStep = ({
parameters,
onParameterChange,
disabled = false,
addCustomMetadata,
removeCustomMetadata,
updateCustomMetadata
}: ChangeMetadataSingleStepProps) => {
const { t } = useTranslation();
// Create a params object that matches the hook interface
const paramsHook = {
parameters,
updateParameter: onParameterChange,
addCustomMetadata,
removeCustomMetadata,
updateCustomMetadata,
};
// Extract metadata from uploaded files
const { isExtractingMetadata } = useMetadataExtraction(paramsHook);
const isDeleteAllEnabled = parameters.deleteAll;
const fieldsDisabled = disabled || isDeleteAllEnabled || isExtractingMetadata;
return (
<Stack gap="md">
{/* Delete All */}
<Stack gap="md">
<Text size="sm" fw={500}>
{t('changeMetadata.deleteAll.label', 'Delete All Metadata')}
</Text>
<DeleteAllStep
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</Stack>
<Divider />
{/* Standard Metadata Fields */}
<Stack gap="md">
<Text size="sm" fw={500}>
{t('changeMetadata.standardFields.title', 'Standard Metadata')}
</Text>
<StandardMetadataStep
parameters={parameters}
onParameterChange={onParameterChange}
disabled={fieldsDisabled}
/>
</Stack>
<Divider />
{/* Document Dates */}
<Stack gap="md">
<Text size="sm" fw={500}>
{t('changeMetadata.dates.title', 'Document Dates')}
</Text>
<DocumentDatesStep
parameters={parameters}
onParameterChange={onParameterChange}
disabled={fieldsDisabled}
/>
</Stack>
<Divider />
{/* Custom Metadata */}
<Stack gap="md">
<Text size="sm" fw={500}>
{t('changeMetadata.customFields.title', 'Custom Metadata')}
</Text>
<CustomMetadataStep
parameters={parameters}
onParameterChange={onParameterChange}
disabled={fieldsDisabled}
addCustomMetadata={addCustomMetadata}
removeCustomMetadata={removeCustomMetadata}
updateCustomMetadata={updateCustomMetadata}
/>
</Stack>
<Divider />
{/* Advanced Options */}
<Stack gap="md">
<Text size="sm" fw={500}>
{t('changeMetadata.advanced.title', 'Advanced Options')}
</Text>
<AdvancedOptionsStep
parameters={parameters}
onParameterChange={onParameterChange}
disabled={fieldsDisabled}
/>
</Stack>
</Stack>
);
};
export default ChangeMetadataSingleStep;

View File

@ -0,0 +1,39 @@
import { Select } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ChangeMetadataParameters } from "../../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
import { TrappedStatus } from "../../../../types/metadata";
interface AdvancedOptionsStepProps {
parameters: ChangeMetadataParameters;
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
disabled?: boolean;
}
const AdvancedOptionsStep = ({
parameters,
onParameterChange,
disabled = false
}: AdvancedOptionsStepProps) => {
const { t } = useTranslation();
return (
<Select
label={t('changeMetadata.trapped.label', 'Trapped Status')}
description={t('changeMetadata.trapped.description', 'Indicates whether the document has been trapped for high-quality printing')}
value={parameters.trapped}
onChange={(value) => {
if (value) {
onParameterChange('trapped', value as TrappedStatus);
}
}}
disabled={disabled || parameters.deleteAll}
data={[
{ value: TrappedStatus.UNKNOWN, label: t('changeMetadata.trapped.unknown', 'Unknown') },
{ value: TrappedStatus.TRUE, label: t('changeMetadata.trapped.true', 'True') },
{ value: TrappedStatus.FALSE, label: t('changeMetadata.trapped.false', 'False') }
]}
/>
);
};
export default AdvancedOptionsStep;

View File

@ -0,0 +1,74 @@
import { Stack, TextInput, Button, Group, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ChangeMetadataParameters } from "../../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
interface CustomMetadataStepProps {
parameters: ChangeMetadataParameters;
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
disabled?: boolean;
addCustomMetadata: (key?: string, value?: string) => void;
removeCustomMetadata: (id: string) => void;
updateCustomMetadata: (id: string, key: string, value: string) => void;
}
const CustomMetadataStep = ({
parameters,
disabled = false,
addCustomMetadata,
removeCustomMetadata,
updateCustomMetadata
}: CustomMetadataStepProps) => {
const { t } = useTranslation();
return (
<Stack gap="sm">
<Group justify="space-between" align="center">
<Text size="sm" fw={500}>
{t('changeMetadata.customFields.title', 'Custom Metadata')}
</Text>
<Button
size="xs"
variant="light"
onClick={() => addCustomMetadata()}
disabled={disabled}
>
{t('changeMetadata.customFields.add', 'Add Field')}
</Button>
</Group>
{parameters.customMetadata.length > 0 && (
<Text size="xs" c="dimmed">
{t('changeMetadata.customFields.description', 'Add custom metadata fields to the document')}
</Text>
)}
{parameters.customMetadata.map((entry) => (
<Stack key={entry.id} gap="xs">
<TextInput
placeholder={t('changeMetadata.customFields.keyPlaceholder', 'Custom key')}
value={entry.key}
onChange={(e) => updateCustomMetadata(entry.id, e.target.value, entry.value)}
disabled={disabled}
/>
<TextInput
placeholder={t('changeMetadata.customFields.valuePlaceholder', 'Custom value')}
value={entry.value}
onChange={(e) => updateCustomMetadata(entry.id, entry.key, e.target.value)}
disabled={disabled}
/>
<Button
size="xs"
variant="light"
color="red"
onClick={() => removeCustomMetadata(entry.id)}
disabled={disabled}
>
{t('changeMetadata.customFields.remove', 'Remove')}
</Button>
</Stack>
))}
</Stack>
);
};
export default CustomMetadataStep;

View File

@ -0,0 +1,29 @@
import { Checkbox } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ChangeMetadataParameters } from "../../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
interface DeleteAllStepProps {
parameters: ChangeMetadataParameters;
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
disabled?: boolean;
}
const DeleteAllStep = ({
parameters,
onParameterChange,
disabled = false
}: DeleteAllStepProps) => {
const { t } = useTranslation();
return (
<Checkbox
label={t('changeMetadata.deleteAll.label', 'Delete all metadata')}
description={t('changeMetadata.deleteAll.description', 'Remove all metadata from the PDF document')}
checked={parameters.deleteAll}
onChange={(e) => onParameterChange('deleteAll', e.target.checked)}
disabled={disabled}
/>
);
};
export default DeleteAllStep;

View File

@ -0,0 +1,43 @@
import { Stack, TextInput, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ChangeMetadataParameters } from "../../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
interface DocumentDatesStepProps {
parameters: ChangeMetadataParameters;
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
disabled?: boolean;
}
const DocumentDatesStep = ({
parameters,
onParameterChange,
disabled = false
}: DocumentDatesStepProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
<Text size="xs" c="dimmed">
{t('changeMetadata.dates.format', 'Format: yyyy/MM/dd HH:mm:ss')}
</Text>
<TextInput
label={t('changeMetadata.creationDate.label', 'Creation Date')}
placeholder={t('changeMetadata.creationDate.placeholder', 'e.g. 2025/01/17 14:30:00')}
value={parameters.creationDate}
onChange={(e) => onParameterChange('creationDate', e.target.value)}
disabled={disabled}
/>
<TextInput
label={t('changeMetadata.modificationDate.label', 'Modification Date')}
placeholder={t('changeMetadata.modificationDate.placeholder', 'e.g. 2025/01/17 14:30:00')}
value={parameters.modificationDate}
onChange={(e) => onParameterChange('modificationDate', e.target.value)}
disabled={disabled}
/>
</Stack>
);
};
export default DocumentDatesStep;

View File

@ -0,0 +1,71 @@
import { Stack, TextInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ChangeMetadataParameters } from "../../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
interface StandardMetadataStepProps {
parameters: ChangeMetadataParameters;
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
disabled?: boolean;
}
const StandardMetadataStep = ({
parameters,
onParameterChange,
disabled = false
}: StandardMetadataStepProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
<TextInput
label={t('changeMetadata.title.label', 'Title')}
placeholder={t('changeMetadata.title.placeholder', 'Document title')}
value={parameters.title}
onChange={(e) => onParameterChange('title', e.target.value)}
disabled={disabled}
/>
<TextInput
label={t('changeMetadata.author.label', 'Author')}
placeholder={t('changeMetadata.author.placeholder', 'Document author')}
value={parameters.author}
onChange={(e) => onParameterChange('author', e.target.value)}
disabled={disabled}
/>
<TextInput
label={t('changeMetadata.subject.label', 'Subject')}
placeholder={t('changeMetadata.subject.placeholder', 'Document subject')}
value={parameters.subject}
onChange={(e) => onParameterChange('subject', e.target.value)}
disabled={disabled}
/>
<TextInput
label={t('changeMetadata.keywords.label', 'Keywords')}
placeholder={t('changeMetadata.keywords.placeholder', 'Document keywords')}
value={parameters.keywords}
onChange={(e) => onParameterChange('keywords', e.target.value)}
disabled={disabled}
/>
<TextInput
label={t('changeMetadata.creator.label', 'Creator')}
placeholder={t('changeMetadata.creator.placeholder', 'Document creator')}
value={parameters.creator}
onChange={(e) => onParameterChange('creator', e.target.value)}
disabled={disabled}
/>
<TextInput
label={t('changeMetadata.producer.label', 'Producer')}
placeholder={t('changeMetadata.producer.placeholder', 'Document producer')}
value={parameters.producer}
onChange={(e) => onParameterChange('producer', e.target.value)}
disabled={disabled}
/>
</Stack>
);
};
export default StandardMetadataStep;

View File

@ -1,12 +1,28 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useChangeMetadataTips = (): TooltipContent => {
export const useDeleteAllTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("changeMetadata.tooltip.header.title", "PDF Metadata Overview")
title: t("changeMetadata.tooltip.deleteAll.title", "Delete All Metadata")
},
tips: [
{
title: t("changeMetadata.tooltip.deleteAll.title", "Delete All Metadata"),
description: t("changeMetadata.tooltip.deleteAll.text", "Complete metadata removal for privacy and clean documents."),
}
]
};
};
export const useStandardMetadataTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("changeMetadata.tooltip.standardFields.title", "Standard Fields")
},
tips: [
{
@ -19,7 +35,19 @@ export const useChangeMetadataTips = (): TooltipContent => {
t("changeMetadata.tooltip.standardFields.bullet4", "Keywords: Search terms for the document"),
t("changeMetadata.tooltip.standardFields.bullet5", "Creator/Producer: Software used to create the PDF")
]
}
]
};
};
export const useDocumentDatesTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("changeMetadata.tooltip.dates.title", "Date Fields")
},
tips: [
{
title: t("changeMetadata.tooltip.dates.title", "Date Fields"),
description: t("changeMetadata.tooltip.dates.text", "When the document was created and modified."),
@ -28,14 +56,47 @@ export const useChangeMetadataTips = (): TooltipContent => {
t("changeMetadata.tooltip.dates.bullet2", "Modification Date: When last changed"),
t("changeMetadata.tooltip.dates.bullet3", "Format: yyyy/MM/dd HH:mm:ss (e.g., 2025/01/17 14:30:00)")
]
}
]
};
};
export const useCustomMetadataTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("changeMetadata.tooltip.customFields.title", "Custom Metadata")
},
tips: [
{
title: t("changeMetadata.tooltip.options.title", "Additional Options"),
description: t("changeMetadata.tooltip.options.text", "Custom fields and privacy controls."),
title: t("changeMetadata.tooltip.customFields.title", "Custom Metadata"),
description: t("changeMetadata.tooltip.customFields.text", "Add your own custom key-value metadata pairs."),
bullets: [
t("changeMetadata.tooltip.options.bullet1", "Custom Metadata: Add your own key-value pairs"),
t("changeMetadata.tooltip.options.bullet2", "Trapped Status: High-quality printing setting"),
t("changeMetadata.tooltip.options.bullet3", "Delete All: Remove all metadata for privacy")
t("changeMetadata.tooltip.customFields.bullet1", "Add any custom fields relevant to your document"),
t("changeMetadata.tooltip.customFields.bullet2", "Examples: Department, Project, Version, Status"),
t("changeMetadata.tooltip.customFields.bullet3", "Both key and value are required for each entry")
]
}
]
};
};
export const useAdvancedOptionsTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("changeMetadata.tooltip.advanced.title", "Advanced Options")
},
tips: [
{
title: t("changeMetadata.tooltip.advanced.trapped.title", "Trapped Status"),
description: t("changeMetadata.tooltip.advanced.trapped.description", "Indicates if document is prepared for high-quality printing."),
bullets: [
t("changeMetadata.tooltip.advanced.trapped.bullet1", "True: Document has been trapped for printing"),
t("changeMetadata.tooltip.advanced.trapped.bullet2", "False: Document has not been trapped"),
t("changeMetadata.tooltip.advanced.trapped.bullet3", "Unknown: Trapped status is not specified")
]
}
]

View File

@ -53,7 +53,7 @@ import RedactSingleStepSettings from "../components/tools/redact/RedactSingleSte
import Redact from "../tools/Redact";
import { ToolId } from "../types/toolId";
import MergeSettings from '../components/tools/merge/MergeSettings';
import ChangeMetadataSettings from "../components/tools/changeMetadata/ChangeMetadataSettings";
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
@ -299,7 +299,7 @@ export function useFlatToolRegistry(): ToolRegistry {
maxFiles: -1,
endpoints: ["update-metadata"],
operationConfig: changeMetadataOperationConfig,
settingsComponent: ChangeMetadataSettings,
settingsComponent: ChangeMetadataSingleStep,
},
// Page Formatting

View File

@ -0,0 +1,60 @@
import { useState, useEffect } from "react";
import { PDFMetadataService } from "../../../services/pdfMetadataService";
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
import { ChangeMetadataParametersHook } from "./useChangeMetadataParameters";
export const useMetadataExtraction = (params: ChangeMetadataParametersHook) => {
const { selectedFiles } = useSelectedFiles();
const [isExtractingMetadata, setIsExtractingMetadata] = useState(false);
const [hasExtractedMetadata, setHasExtractedMetadata] = useState(false);
// Extract metadata from first file when files change
useEffect(() => {
const extractMetadata = async () => {
if (selectedFiles.length === 0 || hasExtractedMetadata) {
return;
}
const firstFile = selectedFiles[0];
if (!firstFile) {
return;
}
setIsExtractingMetadata(true);
try {
const result = await PDFMetadataService.extractMetadata(firstFile);
if (result.success) {
const metadata = result.metadata;
// Pre-populate all fields with extracted metadata
params.updateParameter('title', metadata.title);
params.updateParameter('author', metadata.author);
params.updateParameter('subject', metadata.subject);
params.updateParameter('keywords', metadata.keywords);
params.updateParameter('creator', metadata.creator);
params.updateParameter('producer', metadata.producer);
params.updateParameter('creationDate', metadata.creationDate);
params.updateParameter('modificationDate', metadata.modificationDate);
params.updateParameter('trapped', metadata.trapped);
// Set custom metadata entries directly to avoid state update timing issues
params.updateParameter('customMetadata', metadata.customMetadata);
setHasExtractedMetadata(true);
}
} catch (error) {
console.warn('Failed to extract metadata:', error);
} finally {
setIsExtractingMetadata(false);
}
};
extractMetadata();
}, [selectedFiles, hasExtractedMetadata, params]);
return {
isExtractingMetadata,
hasExtractedMetadata,
};
};

View File

@ -1,15 +1,40 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import ChangeMetadataSettings from "../components/tools/changeMetadata/ChangeMetadataSettings";
import DeleteAllStep from "../components/tools/changeMetadata/steps/DeleteAllStep";
import StandardMetadataStep from "../components/tools/changeMetadata/steps/StandardMetadataStep";
import DocumentDatesStep from "../components/tools/changeMetadata/steps/DocumentDatesStep";
import CustomMetadataStep from "../components/tools/changeMetadata/steps/CustomMetadataStep";
import AdvancedOptionsStep from "../components/tools/changeMetadata/steps/AdvancedOptionsStep";
import { useChangeMetadataParameters } from "../hooks/tools/changeMetadata/useChangeMetadataParameters";
import { useChangeMetadataOperation } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
import { useMetadataExtraction } from "../hooks/tools/changeMetadata/useMetadataExtraction";
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "../types/tool";
import { useChangeMetadataTips } from "../components/tooltips/useChangeMetadataTips";
import {
useDeleteAllTips,
useStandardMetadataTips,
useDocumentDatesTips,
useCustomMetadataTips,
useAdvancedOptionsTips
} from "../components/tooltips/useChangeMetadataTips";
const ChangeMetadata = (props: BaseToolProps) => {
const { t } = useTranslation();
const changeMetadataTips = useChangeMetadataTips();
// Individual tooltips for each step
const deleteAllTips = useDeleteAllTips();
const standardMetadataTips = useStandardMetadataTips();
const documentDatesTips = useDocumentDatesTips();
const customMetadataTips = useCustomMetadataTips();
const advancedOptionsTips = useAdvancedOptionsTips();
// Individual step collapse states
const [deleteAllCollapsed, setDeleteAllCollapsed] = useState(false);
const [standardMetadataCollapsed, setStandardMetadataCollapsed] = useState(false);
const [documentDatesCollapsed, setDocumentDatesCollapsed] = useState(true);
const [customMetadataCollapsed, setCustomMetadataCollapsed] = useState(true);
const [advancedOptionsCollapsed, setAdvancedOptionsCollapsed] = useState(true);
const base = useBaseTool(
'changeMetadata',
@ -18,6 +43,14 @@ const ChangeMetadata = (props: BaseToolProps) => {
props,
);
// Extract metadata from uploaded files
const { isExtractingMetadata } = useMetadataExtraction(base.params);
// Compute actual collapsed state based on results and user state
const getActualCollapsedState = (userCollapsed: boolean) => {
return (!base.hasFiles || base.hasResults) ? true : userCollapsed; // Force collapse when results are shown
};
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
@ -25,21 +58,83 @@ const ChangeMetadata = (props: BaseToolProps) => {
},
steps: [
{
title: t("changeMetadata.settings.title", "Metadata Settings"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: changeMetadataTips,
title: t("changeMetadata.deleteAll.label", "Delete All Metadata"),
isCollapsed: getActualCollapsedState(deleteAllCollapsed),
onCollapsedClick: base.hasResults
? (base.settingsCollapsed ? base.handleSettingsReset : undefined)
: () => setDeleteAllCollapsed(!deleteAllCollapsed),
tooltip: deleteAllTips,
content: (
<ChangeMetadataSettings
<DeleteAllStep
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
disabled={base.endpointLoading || isExtractingMetadata}
/>
),
},
{
title: t("changeMetadata.standardFields.title", "Standard Metadata"),
isCollapsed: getActualCollapsedState(standardMetadataCollapsed),
onCollapsedClick: base.hasResults
? (base.settingsCollapsed ? base.handleSettingsReset : undefined)
: () => setStandardMetadataCollapsed(!standardMetadataCollapsed),
tooltip: standardMetadataTips,
content: (
<StandardMetadataStep
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading || base.params.parameters.deleteAll || isExtractingMetadata}
/>
),
},
{
title: t("changeMetadata.dates.title", "Document Dates"),
isCollapsed: getActualCollapsedState(documentDatesCollapsed),
onCollapsedClick: base.hasResults
? (base.settingsCollapsed ? base.handleSettingsReset : undefined)
: () => setDocumentDatesCollapsed(!documentDatesCollapsed),
tooltip: documentDatesTips,
content: (
<DocumentDatesStep
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading || base.params.parameters.deleteAll || isExtractingMetadata}
/>
),
},
{
title: t("changeMetadata.customFields.title", "Custom Metadata"),
isCollapsed: getActualCollapsedState(customMetadataCollapsed),
onCollapsedClick: base.hasResults
? (base.settingsCollapsed ? base.handleSettingsReset : undefined)
: () => setCustomMetadataCollapsed(!customMetadataCollapsed),
tooltip: customMetadataTips,
content: (
<CustomMetadataStep
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading || base.params.parameters.deleteAll || isExtractingMetadata}
addCustomMetadata={base.params.addCustomMetadata}
removeCustomMetadata={base.params.removeCustomMetadata}
updateCustomMetadata={base.params.updateCustomMetadata}
/>
),
},
{
title: t("changeMetadata.advanced.title", "Advanced Options"),
isCollapsed: getActualCollapsedState(advancedOptionsCollapsed),
onCollapsedClick: base.hasResults
? (base.settingsCollapsed ? base.handleSettingsReset : undefined)
: () => setAdvancedOptionsCollapsed(!advancedOptionsCollapsed),
tooltip: advancedOptionsTips,
content: (
<AdvancedOptionsStep
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading || isExtractingMetadata}
/>
),
},
],
executeButton: {
text: t("changeMetadata.submit", "Update Metadata"),