Compare commits

..

No commits in common. "668c47d5a09b8910875116cd7086f5d04a9bbefc" and "55b8455b66cbc40b30da36258e21f2241530b420" have entirely different histories.

8 changed files with 204 additions and 211 deletions

View File

@ -5,6 +5,7 @@ import { useMetadataExtraction } from "../../../hooks/tools/changeMetadata/useMe
import DeleteAllStep from "./steps/DeleteAllStep"; import DeleteAllStep from "./steps/DeleteAllStep";
import StandardMetadataStep from "./steps/StandardMetadataStep"; import StandardMetadataStep from "./steps/StandardMetadataStep";
import DocumentDatesStep from "./steps/DocumentDatesStep"; import DocumentDatesStep from "./steps/DocumentDatesStep";
import CustomMetadataStep from "./steps/CustomMetadataStep";
import AdvancedOptionsStep from "./steps/AdvancedOptionsStep"; import AdvancedOptionsStep from "./steps/AdvancedOptionsStep";
interface ChangeMetadataSingleStepProps { interface ChangeMetadataSingleStepProps {
@ -26,10 +27,17 @@ const ChangeMetadataSingleStep = ({
}: ChangeMetadataSingleStepProps) => { }: ChangeMetadataSingleStepProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
// Extract metadata from uploaded files // Create a params object that matches the hook interface
const { isExtractingMetadata } = useMetadataExtraction({ const paramsHook = {
parameters,
updateParameter: onParameterChange, updateParameter: onParameterChange,
}); addCustomMetadata,
removeCustomMetadata,
updateCustomMetadata,
};
// Extract metadata from uploaded files
const { isExtractingMetadata } = useMetadataExtraction(paramsHook);
const isDeleteAllEnabled = parameters.deleteAll; const isDeleteAllEnabled = parameters.deleteAll;
const fieldsDisabled = disabled || isDeleteAllEnabled || isExtractingMetadata; const fieldsDisabled = disabled || isDeleteAllEnabled || isExtractingMetadata;
@ -78,6 +86,23 @@ const ChangeMetadataSingleStep = ({
<Divider /> <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 */} {/* Advanced Options */}
<Stack gap="md"> <Stack gap="md">
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
@ -87,9 +112,6 @@ const ChangeMetadataSingleStep = ({
parameters={parameters} parameters={parameters}
onParameterChange={onParameterChange} onParameterChange={onParameterChange}
disabled={fieldsDisabled} disabled={fieldsDisabled}
addCustomMetadata={addCustomMetadata}
removeCustomMetadata={removeCustomMetadata}
updateCustomMetadata={updateCustomMetadata}
/> />
</Stack> </Stack>
</Stack> </Stack>

View File

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

View File

@ -98,15 +98,6 @@ export const useAdvancedOptionsTips = (): TooltipContent => {
t("changeMetadata.tooltip.advanced.trapped.bullet2", "False: Document has not been trapped"), t("changeMetadata.tooltip.advanced.trapped.bullet2", "False: Document has not been trapped"),
t("changeMetadata.tooltip.advanced.trapped.bullet3", "Unknown: Trapped status is not specified") t("changeMetadata.tooltip.advanced.trapped.bullet3", "Unknown: Trapped status is not specified")
] ]
},
{
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.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")
]
} }
] ]
}; };

View File

@ -1,63 +1,53 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect } from "react";
import { extractPDFMetadata } from "../../../services/pdfMetadataService"; import { PDFMetadataService } from "../../../services/pdfMetadataService";
import { useSelectedFiles } from "../../../contexts/file/fileHooks"; import { useSelectedFiles } from "../../../contexts/file/fileHooks";
import { ChangeMetadataParameters } from "./useChangeMetadataParameters"; import { ChangeMetadataParametersHook } from "./useChangeMetadataParameters";
interface MetadataExtractionParams { export const useMetadataExtraction = (params: ChangeMetadataParametersHook) => {
updateParameter: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void;
}
export const useMetadataExtraction = (params: MetadataExtractionParams) => {
const { selectedFiles } = useSelectedFiles(); const { selectedFiles } = useSelectedFiles();
const [isExtractingMetadata, setIsExtractingMetadata] = useState(false); const [isExtractingMetadata, setIsExtractingMetadata] = useState(false);
const [hasExtractedMetadata, setHasExtractedMetadata] = useState(false); const [hasExtractedMetadata, setHasExtractedMetadata] = useState(false);
const previousFileCountRef = useRef(0);
// Reset extraction state only when files are cleared (length goes to 0)
useEffect(() => {
if (previousFileCountRef.current > 0 && selectedFiles.length === 0) {
setHasExtractedMetadata(false);
}
previousFileCountRef.current = selectedFiles.length;
}, [selectedFiles]);
// Extract metadata from first file when files change // Extract metadata from first file when files change
useEffect(() => { useEffect(() => {
const extractMetadata = async () => { const extractMetadata = async () => {
if (selectedFiles.length === 0) { if (selectedFiles.length === 0 || hasExtractedMetadata) {
return; return;
} }
const firstFile = selectedFiles[0];
if (hasExtractedMetadata) { const firstFile = selectedFiles[0];
if (!firstFile) {
return; return;
} }
setIsExtractingMetadata(true); setIsExtractingMetadata(true);
try {
const result = await PDFMetadataService.extractMetadata(firstFile);
const result = await extractPDFMetadata(firstFile); if (result.success) {
const metadata = result.metadata;
if (result.success) { // Pre-populate all fields with extracted metadata
const metadata = result.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);
// Pre-populate all fields with extracted metadata // Set custom metadata entries directly to avoid state update timing issues
params.updateParameter('title', metadata.title); params.updateParameter('customMetadata', metadata.customMetadata);
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);
params.updateParameter('customMetadata', metadata.customMetadata);
setHasExtractedMetadata(true); setHasExtractedMetadata(true);
} else { }
console.warn('Failed to extract metadata:', result.error); } catch (error) {
console.warn('Failed to extract metadata:', error);
} finally {
setIsExtractingMetadata(false);
} }
setIsExtractingMetadata(false);
}; };
extractMetadata(); extractMetadata();

View File

@ -1,7 +1,6 @@
import { pdfWorkerManager } from './pdfWorkerManager'; import { pdfWorkerManager } from './pdfWorkerManager';
import { FileAnalyzer } from './fileAnalyzer'; import { FileAnalyzer } from './fileAnalyzer';
import { TrappedStatus, CustomMetadataEntry, ExtractedPDFMetadata } from '../types/metadata'; import { TrappedStatus, CustomMetadataEntry, ExtractedPDFMetadata } from '../types/metadata';
import { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
export interface MetadataExtractionResult { export interface MetadataExtractionResult {
success: true; success: true;
@ -19,45 +18,49 @@ export type MetadataExtractionResponse = MetadataExtractionResult | MetadataExtr
* Utility to format PDF date strings to required format (yyyy/MM/dd HH:mm:ss) * Utility to format PDF date strings to required format (yyyy/MM/dd HH:mm:ss)
* Handles PDF date format: "D:YYYYMMDDHHmmSSOHH'mm'" or standard date strings * Handles PDF date format: "D:YYYYMMDDHHmmSSOHH'mm'" or standard date strings
*/ */
function formatPDFDate(dateString: string): string { function formatPDFDate(dateString: unknown): string {
if (!dateString) { if (!dateString || typeof dateString !== 'string') {
return ''; return '';
} }
let date: Date; try {
let date: Date;
// Check if it's a PDF date format (starts with "D:") // Check if it's a PDF date format (starts with "D:")
if (dateString.startsWith('D:')) { if (dateString.startsWith('D:')) {
// Parse PDF date format: D:YYYYMMDDHHmmSSOHH'mm' // Parse PDF date format: D:YYYYMMDDHHmmSSOHH'mm'
const dateStr = dateString.substring(2); // Remove "D:" const dateStr = dateString.substring(2); // Remove "D:"
// Extract date parts // Extract date parts
const year = parseInt(dateStr.substring(0, 4)); const year = parseInt(dateStr.substring(0, 4));
const month = parseInt(dateStr.substring(4, 6)); const month = parseInt(dateStr.substring(4, 6));
const day = parseInt(dateStr.substring(6, 8)); const day = parseInt(dateStr.substring(6, 8));
const hour = parseInt(dateStr.substring(8, 10)) || 0; const hour = parseInt(dateStr.substring(8, 10)) || 0;
const minute = parseInt(dateStr.substring(10, 12)) || 0; const minute = parseInt(dateStr.substring(10, 12)) || 0;
const second = parseInt(dateStr.substring(12, 14)) || 0; const second = parseInt(dateStr.substring(12, 14)) || 0;
// Create date object (month is 0-indexed) // Create date object (month is 0-indexed)
date = new Date(year, month - 1, day, hour, minute, second); date = new Date(year, month - 1, day, hour, minute, second);
} else { } else {
// Try parsing as regular date string // Try parsing as regular date string
date = new Date(dateString); date = new Date(dateString);
} }
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
return '';
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
} catch {
return ''; return '';
} }
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
} }
/** /**
@ -77,14 +80,14 @@ function convertTrappedStatus(trapped: unknown): TrappedStatus {
* Extract custom metadata fields from PDF.js info object * Extract custom metadata fields from PDF.js info object
* Custom metadata is nested under the "Custom" key * Custom metadata is nested under the "Custom" key
*/ */
function extractCustomMetadata(custom: unknown): CustomMetadataEntry[] { function extractCustomMetadata(info: Record<string, unknown>): CustomMetadataEntry[] {
const customMetadata: CustomMetadataEntry[] = []; const customMetadata: CustomMetadataEntry[] = [];
let customIdCounter = 1; let customIdCounter = 1;
// Check if there's a Custom object containing the custom metadata // Check if there's a Custom object containing the custom metadata
if (typeof custom === 'object' && custom !== null) { if (info.Custom && typeof info.Custom === 'object' && info.Custom !== null) {
const customObj = custom as Record<string, unknown>; const customObj = info.Custom as Record<string, unknown>;
Object.entries(customObj).forEach(([key, value]) => { Object.entries(customObj).forEach(([key, value]) => {
if (value != null && value !== '') { if (value != null && value !== '') {
@ -102,80 +105,71 @@ function extractCustomMetadata(custom: unknown): CustomMetadataEntry[] {
} }
/** /**
* Safely cleanup PDF document with error handling * Service to extract metadata from PDF files using PDF.js
*/ */
function cleanupPdfDocument(pdfDoc: PDFDocumentProxy | null): void { export class PDFMetadataService {
if (pdfDoc) { /**
* Extract all metadata from a PDF file
* Returns a result object with success/error state
*/
static async extractMetadata(file: File): Promise<MetadataExtractionResponse> {
// Use existing PDF validation
const isValidPDF = await FileAnalyzer.isValidPDF(file);
if (!isValidPDF) {
return {
success: false,
error: 'File is not a valid PDF'
};
}
let pdfDoc: any = null;
try { try {
pdfWorkerManager.destroyDocument(pdfDoc); const arrayBuffer = await file.arrayBuffer();
} catch (cleanupError) { pdfDoc = await pdfWorkerManager.createDocument(arrayBuffer, {
console.warn('Failed to cleanup PDF document:', cleanupError); disableAutoFetch: true,
disableStream: true
});
const metadata = await pdfDoc.getMetadata();
const info = metadata.info || {};
// Safely extract metadata with proper type checking
const extractedMetadata: ExtractedPDFMetadata = {
title: typeof info.Title === 'string' ? info.Title : '',
author: typeof info.Author === 'string' ? info.Author : '',
subject: typeof info.Subject === 'string' ? info.Subject : '',
keywords: typeof info.Keywords === 'string' ? info.Keywords : '',
creator: typeof info.Creator === 'string' ? info.Creator : '',
producer: typeof info.Producer === 'string' ? info.Producer : '',
creationDate: formatPDFDate(info.CreationDate),
modificationDate: formatPDFDate(info.ModDate),
trapped: convertTrappedStatus(info.Trapped),
customMetadata: extractCustomMetadata(info)
};
return {
success: true,
metadata: extractedMetadata
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
success: false,
error: `Failed to extract PDF metadata: ${errorMessage}`
};
} finally {
// Ensure cleanup even if extraction fails
if (pdfDoc) {
try {
pdfWorkerManager.destroyDocument(pdfDoc);
} catch (cleanupError) {
console.warn('Failed to cleanup PDF document:', cleanupError);
}
}
} }
} }
} }
function getStringMetadata(info: Record<string, unknown>, key: string): string {
if (typeof info[key] === 'string') {
return info[key];
} else {
return '';
}
}
/**
* Extract all metadata from a PDF file
* Returns a result object with success/error state
*/
export async function extractPDFMetadata(file: File): Promise<MetadataExtractionResponse> {
// Use existing PDF validation
const isValidPDF = await FileAnalyzer.isValidPDF(file);
if (!isValidPDF) {
return {
success: false,
error: 'File is not a valid PDF'
};
}
let pdfDoc: PDFDocumentProxy | null = null;
let arrayBuffer: ArrayBuffer;
let metadata;
try {
arrayBuffer = await file.arrayBuffer();
pdfDoc = await pdfWorkerManager.createDocument(arrayBuffer, {
disableAutoFetch: true,
disableStream: true
});
metadata = await pdfDoc.getMetadata();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
cleanupPdfDocument(pdfDoc);
return {
success: false,
error: `Failed to read PDF: ${errorMessage}`
};
}
const info = metadata.info as Record<string, unknown>;
// Safely extract metadata with proper type checking
const extractedMetadata: ExtractedPDFMetadata = {
title: getStringMetadata(info, 'Title'),
author: getStringMetadata(info, 'Author'),
subject: getStringMetadata(info, 'Subject'),
keywords: getStringMetadata(info, 'Keywords'),
creator: getStringMetadata(info, 'Creator'),
producer: getStringMetadata(info, 'Producer'),
creationDate: formatPDFDate(getStringMetadata(info, 'CreationDate')),
modificationDate: formatPDFDate(getStringMetadata(info, 'ModDate')),
trapped: convertTrappedStatus(info.Trapped),
customMetadata: extractCustomMetadata(info.Custom),
};
cleanupPdfDocument(pdfDoc);
return {
success: true,
metadata: extractedMetadata
};
}

View File

@ -6,12 +6,11 @@
*/ */
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocumentProxy } from 'pdfjs-dist/types/src/display/api';
const { getDocument, GlobalWorkerOptions } = pdfjsLib; const { getDocument, GlobalWorkerOptions } = pdfjsLib;
class PDFWorkerManager { class PDFWorkerManager {
private static instance: PDFWorkerManager; private static instance: PDFWorkerManager;
private activeDocuments = new Set<PDFDocumentProxy>(); private activeDocuments = new Set<any>();
private workerCount = 0; private workerCount = 0;
private maxWorkers = 10; // Limit concurrent workers private maxWorkers = 10; // Limit concurrent workers
private isInitialized = false; private isInitialized = false;
@ -49,7 +48,7 @@ class PDFWorkerManager {
stopAtErrors?: boolean; stopAtErrors?: boolean;
verbosity?: number; verbosity?: number;
} = {} } = {}
): Promise<PDFDocumentProxy> { ): Promise<any> {
// Wait if we've hit the worker limit // Wait if we've hit the worker limit
if (this.activeDocuments.size >= this.maxWorkers) { if (this.activeDocuments.size >= this.maxWorkers) {
await this.waitForAvailableWorker(); await this.waitForAvailableWorker();
@ -105,7 +104,7 @@ class PDFWorkerManager {
/** /**
* Properly destroy a PDF document and clean up resources * Properly destroy a PDF document and clean up resources
*/ */
destroyDocument(pdf: PDFDocumentProxy): void { destroyDocument(pdf: any): void {
if (this.activeDocuments.has(pdf)) { if (this.activeDocuments.has(pdf)) {
try { try {
pdf.destroy(); pdf.destroy();

View File

@ -1,9 +1,10 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { createToolFlow } from "../components/tools/shared/createToolFlow";
import DeleteAllStep from "../components/tools/changeMetadata/steps/DeleteAllStep"; import DeleteAllStep from "../components/tools/changeMetadata/steps/DeleteAllStep";
import StandardMetadataStep from "../components/tools/changeMetadata/steps/StandardMetadataStep"; import StandardMetadataStep from "../components/tools/changeMetadata/steps/StandardMetadataStep";
import DocumentDatesStep from "../components/tools/changeMetadata/steps/DocumentDatesStep"; 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 AdvancedOptionsStep from "../components/tools/changeMetadata/steps/AdvancedOptionsStep";
import { useChangeMetadataParameters } from "../hooks/tools/changeMetadata/useChangeMetadataParameters"; import { useChangeMetadataParameters } from "../hooks/tools/changeMetadata/useChangeMetadataParameters";
import { useChangeMetadataOperation } from "../hooks/tools/changeMetadata/useChangeMetadataOperation"; import { useChangeMetadataOperation } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
@ -14,6 +15,7 @@ import {
useDeleteAllTips, useDeleteAllTips,
useStandardMetadataTips, useStandardMetadataTips,
useDocumentDatesTips, useDocumentDatesTips,
useCustomMetadataTips,
useAdvancedOptionsTips useAdvancedOptionsTips
} from "../components/tooltips/useChangeMetadataTips"; } from "../components/tooltips/useChangeMetadataTips";
@ -24,12 +26,14 @@ const ChangeMetadata = (props: BaseToolProps) => {
const deleteAllTips = useDeleteAllTips(); const deleteAllTips = useDeleteAllTips();
const standardMetadataTips = useStandardMetadataTips(); const standardMetadataTips = useStandardMetadataTips();
const documentDatesTips = useDocumentDatesTips(); const documentDatesTips = useDocumentDatesTips();
const customMetadataTips = useCustomMetadataTips();
const advancedOptionsTips = useAdvancedOptionsTips(); const advancedOptionsTips = useAdvancedOptionsTips();
// Individual step collapse states // Individual step collapse states
const [deleteAllCollapsed, setDeleteAllCollapsed] = useState(false); const [deleteAllCollapsed, setDeleteAllCollapsed] = useState(false);
const [standardMetadataCollapsed, setStandardMetadataCollapsed] = useState(false); const [standardMetadataCollapsed, setStandardMetadataCollapsed] = useState(false);
const [documentDatesCollapsed, setDocumentDatesCollapsed] = useState(true); const [documentDatesCollapsed, setDocumentDatesCollapsed] = useState(true);
const [customMetadataCollapsed, setCustomMetadataCollapsed] = useState(true);
const [advancedOptionsCollapsed, setAdvancedOptionsCollapsed] = useState(true); const [advancedOptionsCollapsed, setAdvancedOptionsCollapsed] = useState(true);
const base = useBaseTool( const base = useBaseTool(
@ -98,6 +102,24 @@ const ChangeMetadata = (props: BaseToolProps) => {
/> />
), ),
}, },
{
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"), title: t("changeMetadata.advanced.title", "Advanced Options"),
isCollapsed: getActualCollapsedState(advancedOptionsCollapsed), isCollapsed: getActualCollapsedState(advancedOptionsCollapsed),
@ -110,9 +132,6 @@ const ChangeMetadata = (props: BaseToolProps) => {
parameters={base.params.parameters} parameters={base.params.parameters}
onParameterChange={base.params.updateParameter} onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading || isExtractingMetadata} disabled={base.endpointLoading || isExtractingMetadata}
addCustomMetadata={base.params.addCustomMetadata}
removeCustomMetadata={base.params.removeCustomMetadata}
updateCustomMetadata={base.params.updateCustomMetadata}
/> />
), ),
}, },