Compare commits

..

No commits in common. "3eac1a0bf85e914393c82a02697fde68962e382f" and "1ac062155fe317a89804b957b3c755bf1a90206b" have entirely different histories.

21 changed files with 142 additions and 576 deletions

View File

@ -1081,17 +1081,24 @@
"changeMetadata": { "changeMetadata": {
"tags": "Title,author,date,creation,time,publisher,producer,stats", "tags": "Title,author,date,creation,time,publisher,producer,stats",
"header": "Change Metadata", "header": "Change Metadata",
"selectText": {
"1": "Please edit the variables you wish to change",
"2": "Delete all metadata",
"3": "Show Custom Metadata:",
"4": "Other Metadata:",
"5": "Add Custom Metadata Entry"
},
"submit": "Change", "submit": "Change",
"filenamePrefix": "metadata", "filenamePrefix": "metadata",
"settings": { "settings": {
"title": "Metadata Settings" "title": "Metadata Settings"
}, },
"standardFields": { "standardFields": {
"title": "Standard Fields" "title": "Standard Metadata"
}, },
"deleteAll": { "deleteAll": {
"label": "Remove Existing Metadata", "label": "Delete all metadata",
"checkbox": "Delete all metadata" "description": "Remove all metadata from the PDF document"
}, },
"title": { "title": {
"label": "Title", "label": "Title",
@ -1118,18 +1125,20 @@
"placeholder": "Document producer" "placeholder": "Document producer"
}, },
"dates": { "dates": {
"title": "Date Fields" "title": "Document Dates",
"format": "Format: yyyy/MM/dd HH:mm:ss"
}, },
"creationDate": { "creationDate": {
"label": "Creation Date", "label": "Creation Date",
"placeholder": "Creation date" "placeholder": "e.g. 2025/01/17 14:30:00"
}, },
"modificationDate": { "modificationDate": {
"label": "Modification Date", "label": "Modification Date",
"placeholder": "Modification date" "placeholder": "e.g. 2025/01/17 14:30:00"
}, },
"trapped": { "trapped": {
"label": "Trapped Status", "label": "Trapped Status",
"description": "Indicates whether the document has been trapped for high-quality printing",
"unknown": "Unknown", "unknown": "Unknown",
"true": "True", "true": "True",
"false": "False" "false": "False"
@ -1170,7 +1179,8 @@
"title": "Date Fields", "title": "Date Fields",
"text": "When the document was created and modified.", "text": "When the document was created and modified.",
"bullet1": "Creation Date: When original document was made", "bullet1": "Creation Date: When original document was made",
"bullet2": "Modification Date: When last changed" "bullet2": "Modification Date: When last changed",
"bullet3": "Format: yyyy/MM/dd HH:mm:ss (e.g., 2025/01/17 14:30:00)"
}, },
"options": { "options": {
"title": "Additional Options", "title": "Additional Options",
@ -1180,8 +1190,8 @@
"bullet3": "Delete All: Remove all metadata for privacy" "bullet3": "Delete All: Remove all metadata for privacy"
}, },
"deleteAll": { "deleteAll": {
"title": "Remove Existing Metadata", "title": "Delete All Metadata",
"text": "Complete metadata deletion to ensure privacy." "text": "Complete metadata removal for privacy and clean documents."
}, },
"customFields": { "customFields": {
"title": "Custom Metadata", "title": "Custom Metadata",
@ -1571,6 +1581,7 @@
"submit": "Submit" "submit": "Submit"
}, },
"scalePages": { "scalePages": {
"tags": "resize,modify,dimension,adapt",
"title": "Adjust page-scale", "title": "Adjust page-scale",
"header": "Adjust page-scale", "header": "Adjust page-scale",
"pageSize": "Size of a page of the document.", "pageSize": "Size of a page of the document.",
@ -1578,44 +1589,6 @@
"scaleFactor": "Zoom level (crop) of a page.", "scaleFactor": "Zoom level (crop) of a page.",
"submit": "Submit" "submit": "Submit"
}, },
"adjustPageScale": {
"tags": "resize,modify,dimension,adapt",
"title": "Adjust Page Scale",
"header": "Adjust Page Scale",
"scaleFactor": {
"label": "Scale Factor"
},
"pageSize": {
"label": "Target Page Size",
"keep": "Keep Original Size",
"letter": "Letter",
"legal": "Legal"
},
"submit": "Adjust Page Scale",
"error": {
"failed": "An error occurred while adjusting the page scale."
},
"tooltip": {
"header": {
"title": "Page Scale Settings Overview"
},
"description": {
"title": "Description",
"text": "Adjust the size of PDF content and change the page dimensions."
},
"scaleFactor": {
"title": "Scale Factor",
"text": "Controls how large or small the content appears on the page. Content is scaled and centred - if scaled content is larger than the page size, it may be cropped.",
"bullet1": "1.0 = Original size",
"bullet2": "0.5 = Half size (50% smaller)",
"bullet3": "2.0 = Double size (200% larger, may crop)"
},
"pageSize": {
"title": "Target Page Size",
"text": "Sets the dimensions of the output PDF pages. 'Keep Original Size' maintains current dimensions, whilst other options resize to standard paper sizes."
}
}
},
"add-page-numbers": { "add-page-numbers": {
"tags": "paginate,label,organize,index" "tags": "paginate,label,organize,index"
}, },

View File

@ -1,64 +0,0 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import AdjustPageScaleSettings from './AdjustPageScaleSettings';
import { AdjustPageScaleParameters, PageSize } from '../../../hooks/tools/adjustPageScale/useAdjustPageScaleParameters';
// Mock useTranslation with predictable return values
const mockT = vi.fn((key: string, fallback?: string) => fallback || `mock-${key}`);
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: mockT })
}));
// Wrapper component to provide Mantine context
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
<MantineProvider>{children}</MantineProvider>
);
describe('AdjustPageScaleSettings', () => {
const defaultParameters: AdjustPageScaleParameters = {
scaleFactor: 1.0,
pageSize: PageSize.KEEP,
};
const mockOnParameterChange = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
test('should render without crashing', () => {
render(
<TestWrapper>
<AdjustPageScaleSettings
parameters={defaultParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// Basic render test - component renders without throwing
expect(screen.getByText('Scale Factor')).toBeInTheDocument();
expect(screen.getByText('Target Page Size')).toBeInTheDocument();
});
test('should render with custom parameters', () => {
const customParameters: AdjustPageScaleParameters = {
scaleFactor: 2.5,
pageSize: PageSize.A4,
};
render(
<TestWrapper>
<AdjustPageScaleSettings
parameters={customParameters}
onParameterChange={mockOnParameterChange}
/>
</TestWrapper>
);
// Component renders successfully with custom parameters
expect(screen.getByText('Scale Factor')).toBeInTheDocument();
expect(screen.getByText('Target Page Size')).toBeInTheDocument();
});
});

View File

@ -1,55 +0,0 @@
import { Stack, NumberInput, Select } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AdjustPageScaleParameters, PageSize } from "../../../hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
interface AdjustPageScaleSettingsProps {
parameters: AdjustPageScaleParameters;
onParameterChange: <K extends keyof AdjustPageScaleParameters>(key: K, value: AdjustPageScaleParameters[K]) => void;
disabled?: boolean;
}
const AdjustPageScaleSettings = ({ parameters, onParameterChange, disabled = false }: AdjustPageScaleSettingsProps) => {
const { t } = useTranslation();
const pageSizeOptions = [
{ value: PageSize.KEEP, label: t('adjustPageScale.pageSize.keep', 'Keep Original Size') },
{ value: PageSize.A0, label: 'A0' },
{ value: PageSize.A1, label: 'A1' },
{ value: PageSize.A2, label: 'A2' },
{ value: PageSize.A3, label: 'A3' },
{ value: PageSize.A4, label: 'A4' },
{ value: PageSize.A5, label: 'A5' },
{ value: PageSize.A6, label: 'A6' },
{ value: PageSize.LETTER, label: t('adjustPageScale.pageSize.letter', 'Letter') },
{ value: PageSize.LEGAL, label: t('adjustPageScale.pageSize.legal', 'Legal') },
];
return (
<Stack gap="md">
<NumberInput
label={t('adjustPageScale.scaleFactor.label', 'Scale Factor')}
value={parameters.scaleFactor}
onChange={(value) => onParameterChange('scaleFactor', typeof value === 'number' ? value : 1.0)}
min={0.1}
max={10.0}
step={0.1}
decimalScale={2}
disabled={disabled}
/>
<Select
label={t('adjustPageScale.pageSize.label', 'Target Page Size')}
value={parameters.pageSize}
onChange={(value) => {
if (value && Object.values(PageSize).includes(value as PageSize)) {
onParameterChange('pageSize', value as PageSize);
}
}}
data={pageSizeOptions}
disabled={disabled}
/>
</Stack>
);
};
export default AdjustPageScaleSettings;

View File

@ -1,6 +1,6 @@
import { Stack, Divider, Text } from "@mantine/core"; import { Stack, Divider, Text } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ChangeMetadataParameters, createCustomMetadataFunctions } from "../../../hooks/tools/changeMetadata/useChangeMetadataParameters"; import { ChangeMetadataParameters } from "../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
import { useMetadataExtraction } from "../../../hooks/tools/changeMetadata/useMetadataExtraction"; import { useMetadataExtraction } from "../../../hooks/tools/changeMetadata/useMetadataExtraction";
import DeleteAllStep from "./steps/DeleteAllStep"; import DeleteAllStep from "./steps/DeleteAllStep";
import StandardMetadataStep from "./steps/StandardMetadataStep"; import StandardMetadataStep from "./steps/StandardMetadataStep";
@ -11,21 +11,21 @@ interface ChangeMetadataSingleStepProps {
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 ChangeMetadataSingleStep = ({ const ChangeMetadataSingleStep = ({
parameters, parameters,
onParameterChange, onParameterChange,
disabled = false disabled = false,
addCustomMetadata,
removeCustomMetadata,
updateCustomMetadata
}: ChangeMetadataSingleStepProps) => { }: ChangeMetadataSingleStepProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
// Get custom metadata functions using the utility
const { addCustomMetadata, removeCustomMetadata, updateCustomMetadata } = createCustomMetadataFunctions(
parameters,
onParameterChange
);
// Extract metadata from uploaded files // Extract metadata from uploaded files
const { isExtractingMetadata } = useMetadataExtraction({ const { isExtractingMetadata } = useMetadataExtraction({
updateParameter: onParameterChange, updateParameter: onParameterChange,

View File

@ -28,6 +28,7 @@ const AdvancedOptionsStep = ({
{/* Trapped Status */} {/* Trapped Status */}
<Select <Select
label={t('changeMetadata.trapped.label', 'Trapped Status')} 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} value={parameters.trapped}
onChange={(value) => { onChange={(value) => {
if (value) { if (value) {

View File

@ -17,7 +17,8 @@ const DeleteAllStep = ({
return ( return (
<Checkbox <Checkbox
label={t('changeMetadata.deleteAll.checkbox', 'Delete all metadata')} label={t('changeMetadata.deleteAll.label', 'Delete all metadata')}
description={t('changeMetadata.deleteAll.description', 'Remove all metadata from the PDF document')}
checked={parameters.deleteAll} checked={parameters.deleteAll}
onChange={(e) => onParameterChange('deleteAll', e.target.checked)} onChange={(e) => onParameterChange('deleteAll', e.target.checked)}
disabled={disabled} disabled={disabled}

View File

@ -1,4 +1,4 @@
import { Stack } from "@mantine/core"; import { Stack, Text } from "@mantine/core";
import { DateTimePicker } from "@mantine/dates"; import { DateTimePicker } from "@mantine/dates";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ChangeMetadataParameters } from "../../../../hooks/tools/changeMetadata/useChangeMetadataParameters"; import { ChangeMetadataParameters } from "../../../../hooks/tools/changeMetadata/useChangeMetadataParameters";
@ -16,22 +16,43 @@ const DocumentDatesStep = ({
}: DocumentDatesStepProps) => { }: DocumentDatesStepProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const parseDate = (dateString: string): Date | null => {
if (!dateString) return null;
const date = new Date(dateString.replace(/(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2})/, '$1-$2-$3T$4:$5:$6'));
return isNaN(date.getTime()) ? null : date;
};
const formatDate = (date: Date | null): string => {
if (!date) 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}`;
};
return ( return (
<Stack gap="md"> <Stack gap="md">
<Text size="xs" c="dimmed">
{t('changeMetadata.dates.format', 'Format: yyyy/MM/dd HH:mm:ss')}
</Text>
<DateTimePicker <DateTimePicker
label={t('changeMetadata.creationDate.label', 'Creation Date')} label={t('changeMetadata.creationDate.label', 'Creation Date')}
placeholder={t('changeMetadata.creationDate.placeholder', 'Creation date')} placeholder={t('changeMetadata.creationDate.placeholder', 'e.g. 2025/01/17 14:30:00')}
value={parameters.creationDate} value={parseDate(parameters.creationDate)}
onChange={(date) => onParameterChange('creationDate', date ? new Date(date) : null)} onChange={(date) => onParameterChange('creationDate', formatDate(parseDate(date)))}
disabled={disabled} disabled={disabled}
clearable clearable
/> />
<DateTimePicker <DateTimePicker
label={t('changeMetadata.modificationDate.label', 'Modification Date')} label={t('changeMetadata.modificationDate.label', 'Modification Date')}
placeholder={t('changeMetadata.modificationDate.placeholder', 'Modification date')} placeholder={t('changeMetadata.modificationDate.placeholder', 'e.g. 2025/01/17 14:30:00')}
value={parameters.modificationDate} value={parseDate(parameters.modificationDate)}
onChange={(date) => onParameterChange('modificationDate', date ? new Date(date) : null)} onChange={(date) => onParameterChange('modificationDate', formatDate(parseDate(date)))}
disabled={disabled} disabled={disabled}
clearable clearable
/> />

View File

@ -1,31 +0,0 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useAdjustPageScaleTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("adjustPageScale.tooltip.header.title", "Page Scale Settings Overview")
},
tips: [
{
title: t("adjustPageScale.tooltip.description.title", "Description"),
description: t("adjustPageScale.tooltip.description.text", "Adjust the size of PDF content and change the page dimensions.")
},
{
title: t("adjustPageScale.tooltip.scaleFactor.title", "Scale Factor"),
description: t("adjustPageScale.tooltip.scaleFactor.text", "Controls how large or small the content appears on the page. Content is scaled and centered - if scaled content is larger than the page size, it may be cropped."),
bullets: [
t("adjustPageScale.tooltip.scaleFactor.bullet1", "1.0 = Original size"),
t("adjustPageScale.tooltip.scaleFactor.bullet2", "0.5 = Half size (50% smaller)"),
t("adjustPageScale.tooltip.scaleFactor.bullet3", "2.0 = Double size (200% larger, may crop)")
]
},
{
title: t("adjustPageScale.tooltip.pageSize.title", "Target Page Size"),
description: t("adjustPageScale.tooltip.pageSize.text", "Sets the dimensions of the output PDF pages. 'Keep Original Size' maintains current dimensions, while other options resize to standard paper sizes.")
}
]
};
};

View File

@ -6,11 +6,12 @@ export const useDeleteAllTips = (): TooltipContent => {
return { return {
header: { header: {
title: t("changeMetadata.tooltip.deleteAll.title", "Remove Existing Metadata") title: t("changeMetadata.tooltip.deleteAll.title", "Delete All Metadata")
}, },
tips: [ tips: [
{ {
description: t("changeMetadata.tooltip.deleteAll.text", "Complete metadata deletion to ensure privacy."), title: t("changeMetadata.tooltip.deleteAll.title", "Delete All Metadata"),
description: t("changeMetadata.tooltip.deleteAll.text", "Complete metadata removal for privacy and clean documents."),
} }
] ]
}; };
@ -25,6 +26,7 @@ export const useStandardMetadataTips = (): TooltipContent => {
}, },
tips: [ tips: [
{ {
title: t("changeMetadata.tooltip.standardFields.title", "Standard Fields"),
description: t("changeMetadata.tooltip.standardFields.text", "Common PDF metadata fields that describe the document."), description: t("changeMetadata.tooltip.standardFields.text", "Common PDF metadata fields that describe the document."),
bullets: [ bullets: [
t("changeMetadata.tooltip.standardFields.bullet1", "Title: Document name or heading"), t("changeMetadata.tooltip.standardFields.bullet1", "Title: Document name or heading"),
@ -47,10 +49,12 @@ export const useDocumentDatesTips = (): TooltipContent => {
}, },
tips: [ tips: [
{ {
title: t("changeMetadata.tooltip.dates.title", "Date Fields"),
description: t("changeMetadata.tooltip.dates.text", "When the document was created and modified."), description: t("changeMetadata.tooltip.dates.text", "When the document was created and modified."),
bullets: [ bullets: [
t("changeMetadata.tooltip.dates.bullet1", "Creation Date: When original document was made"), t("changeMetadata.tooltip.dates.bullet1", "Creation Date: When original document was made"),
t("changeMetadata.tooltip.dates.bullet2", "Modification Date: When last changed"), 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)")
] ]
} }
] ]
@ -66,6 +70,7 @@ export const useCustomMetadataTips = (): TooltipContent => {
}, },
tips: [ tips: [
{ {
title: t("changeMetadata.tooltip.customFields.title", "Custom Metadata"),
description: t("changeMetadata.tooltip.customFields.text", "Add your own custom key-value metadata pairs."), description: t("changeMetadata.tooltip.customFields.text", "Add your own custom key-value metadata pairs."),
bullets: [ bullets: [
t("changeMetadata.tooltip.customFields.bullet1", "Add any custom fields relevant to your document"), t("changeMetadata.tooltip.customFields.bullet1", "Add any custom fields relevant to your document"),

View File

@ -51,11 +51,8 @@ import ChangePermissionsSettings from "../components/tools/changePermissions/Cha
import FlattenSettings from "../components/tools/flatten/FlattenSettings"; import FlattenSettings from "../components/tools/flatten/FlattenSettings";
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings"; import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
import Redact from "../tools/Redact"; import Redact from "../tools/Redact";
import AdjustPageScale from "../tools/AdjustPageScale";
import { ToolId } from "../types/toolId"; import { ToolId } from "../types/toolId";
import MergeSettings from '../components/tools/merge/MergeSettings'; import MergeSettings from '../components/tools/merge/MergeSettings';
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep"; import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
@ -347,14 +344,11 @@ export function useFlatToolRegistry(): ToolRegistry {
"adjust-page-size-scale": { "adjust-page-size-scale": {
icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="crop-free-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.scalePages.title", "Adjust page size/scale"), name: t("home.scalePages.title", "Adjust page size/scale"),
component: AdjustPageScale, component: null,
description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."), description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."),
categoryId: ToolCategoryId.STANDARD_TOOLS, categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING, subcategoryId: SubcategoryId.PAGE_FORMATTING,
maxFiles: -1,
endpoints: ["scale-pages"],
operationConfig: adjustPageScaleOperationConfig,
settingsComponent: AdjustPageScaleSettings,
}, },
addPageNumbers: { addPageNumbers: {
icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />,

View File

@ -1,30 +0,0 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation, ToolType } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { AdjustPageScaleParameters, defaultParameters } from './useAdjustPageScaleParameters';
export const buildAdjustPageScaleFormData = (parameters: AdjustPageScaleParameters, file: File): FormData => {
const formData = new FormData();
formData.append("fileInput", file);
formData.append("scaleFactor", parameters.scaleFactor.toString());
formData.append("pageSize", parameters.pageSize);
return formData;
};
export const adjustPageScaleOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildAdjustPageScaleFormData,
operationType: 'adjustPageScale',
endpoint: '/api/v1/general/scale-pages',
filePrefix: 'scaled_',
defaultParameters,
} as const;
export const useAdjustPageScaleOperation = () => {
const { t } = useTranslation();
return useToolOperation<AdjustPageScaleParameters>({
...adjustPageScaleOperationConfig,
getErrorMessage: createStandardErrorHandler(t('adjustPageScale.error.failed', 'An error occurred while adjusting the page scale.'))
});
};

View File

@ -1,142 +0,0 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useAdjustPageScaleParameters, defaultParameters, PageSize, AdjustPageScaleParametersHook } from './useAdjustPageScaleParameters';
describe('useAdjustPageScaleParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
expect(result.current.parameters.scaleFactor).toBe(1.0);
expect(result.current.parameters.pageSize).toBe(PageSize.KEEP);
});
test.each([
{ paramName: 'scaleFactor' as const, value: 0.5 },
{ paramName: 'scaleFactor' as const, value: 2.0 },
{ paramName: 'scaleFactor' as const, value: 10.0 },
{ paramName: 'pageSize' as const, value: PageSize.A4 },
{ paramName: 'pageSize' as const, value: PageSize.LETTER },
{ paramName: 'pageSize' as const, value: PageSize.LEGAL },
])('should update parameter $paramName to $value', ({ paramName, value }) => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
act(() => {
result.current.updateParameter(paramName, value);
});
expect(result.current.parameters[paramName]).toBe(value);
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
// First, change some parameters
act(() => {
result.current.updateParameter('scaleFactor', 2.5);
result.current.updateParameter('pageSize', PageSize.A3);
});
expect(result.current.parameters.scaleFactor).toBe(2.5);
expect(result.current.parameters.pageSize).toBe(PageSize.A3);
// Then reset
act(() => {
result.current.resetParameters();
});
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test('should return correct endpoint name', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
expect(result.current.getEndpointName()).toBe('scale-pages');
});
test.each([
{
description: 'with default parameters',
setup: () => {},
expected: true
},
{
description: 'with valid scale factor 0.1',
setup: (hook: AdjustPageScaleParametersHook) => {
hook.updateParameter('scaleFactor', 0.1);
},
expected: true
},
{
description: 'with valid scale factor 10.0',
setup: (hook: AdjustPageScaleParametersHook) => {
hook.updateParameter('scaleFactor', 10.0);
},
expected: true
},
{
description: 'with A4 page size',
setup: (hook: AdjustPageScaleParametersHook) => {
hook.updateParameter('pageSize', PageSize.A4);
},
expected: true
},
{
description: 'with invalid scale factor 0',
setup: (hook: AdjustPageScaleParametersHook) => {
hook.updateParameter('scaleFactor', 0);
},
expected: false
},
{
description: 'with negative scale factor',
setup: (hook: AdjustPageScaleParametersHook) => {
hook.updateParameter('scaleFactor', -0.5);
},
expected: false
}
])('should validate parameters correctly $description', ({ setup, expected }) => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
act(() => {
setup(result.current);
});
expect(result.current.validateParameters()).toBe(expected);
});
test('should handle all PageSize enum values', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
Object.values(PageSize).forEach(pageSize => {
act(() => {
result.current.updateParameter('pageSize', pageSize);
});
expect(result.current.parameters.pageSize).toBe(pageSize);
expect(result.current.validateParameters()).toBe(true);
});
});
test('should handle scale factor edge cases', () => {
const { result } = renderHook(() => useAdjustPageScaleParameters());
// Test very small valid scale factor
act(() => {
result.current.updateParameter('scaleFactor', 0.01);
});
expect(result.current.validateParameters()).toBe(true);
// Test scale factor just above zero
act(() => {
result.current.updateParameter('scaleFactor', 0.001);
});
expect(result.current.validateParameters()).toBe(true);
// Test exactly zero (invalid)
act(() => {
result.current.updateParameter('scaleFactor', 0);
});
expect(result.current.validateParameters()).toBe(false);
});
});

View File

@ -1,37 +0,0 @@
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export enum PageSize {
KEEP = 'KEEP',
A0 = 'A0',
A1 = 'A1',
A2 = 'A2',
A3 = 'A3',
A4 = 'A4',
A5 = 'A5',
A6 = 'A6',
LETTER = 'LETTER',
LEGAL = 'LEGAL'
}
export interface AdjustPageScaleParameters extends BaseParameters {
scaleFactor: number;
pageSize: PageSize;
}
export const defaultParameters: AdjustPageScaleParameters = {
scaleFactor: 1.0,
pageSize: PageSize.KEEP,
};
export type AdjustPageScaleParametersHook = BaseParametersHook<AdjustPageScaleParameters>;
export const useAdjustPageScaleParameters = (): AdjustPageScaleParametersHook => {
return useBaseParameters({
defaultParameters,
endpointName: 'scale-pages',
validateFn: (params) => {
return params.scaleFactor > 0;
},
});
};

View File

@ -13,8 +13,8 @@ describe('buildChangeMetadataFormData', () => {
keywords: '', keywords: '',
creator: '', creator: '',
producer: '', producer: '',
creationDate: null, creationDate: '',
modificationDate: null, modificationDate: '',
trapped: TrappedStatus.UNKNOWN, trapped: TrappedStatus.UNKNOWN,
customMetadata: [], customMetadata: [],
deleteAll: false, deleteAll: false,
@ -62,8 +62,8 @@ describe('buildChangeMetadataFormData', () => {
keywords: 'test, keywords', keywords: 'test, keywords',
creator: 'Test Creator', creator: 'Test Creator',
producer: 'Test Producer', producer: 'Test Producer',
creationDate: new Date('2025/01/17 14:30:00'), creationDate: '2025/01/17 14:30:00',
modificationDate: new Date('2025/01/17 15:30:00'), modificationDate: '2025/01/17 15:30:00',
trapped: TrappedStatus.TRUE, trapped: TrappedStatus.TRUE,
}, },
expectedFormData: { expectedFormData: {

View File

@ -3,18 +3,6 @@ import { useToolOperation, ToolType } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { ChangeMetadataParameters, defaultParameters } from './useChangeMetadataParameters'; import { ChangeMetadataParameters, defaultParameters } from './useChangeMetadataParameters';
// Helper function to format Date object to string
const formatDateForBackend = (date: Date | null): string => {
if (!date) 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}`;
};
// Static function that can be used by both the hook and automation executor // Static function that can be used by both the hook and automation executor
export const buildChangeMetadataFormData = (parameters: ChangeMetadataParameters, file: File): FormData => { export const buildChangeMetadataFormData = (parameters: ChangeMetadataParameters, file: File): FormData => {
const formData = new FormData(); const formData = new FormData();
@ -28,9 +16,9 @@ export const buildChangeMetadataFormData = (parameters: ChangeMetadataParameters
formData.append("creator", parameters.creator || ""); formData.append("creator", parameters.creator || "");
formData.append("producer", parameters.producer || ""); formData.append("producer", parameters.producer || "");
// Date fields - convert Date objects to strings // Date fields
formData.append("creationDate", formatDateForBackend(parameters.creationDate)); formData.append("creationDate", parameters.creationDate || "");
formData.append("modificationDate", formatDateForBackend(parameters.modificationDate)); formData.append("modificationDate", parameters.modificationDate || "");
// Trapped status // Trapped status
formData.append("trapped", parameters.trapped || ""); formData.append("trapped", parameters.trapped || "");

View File

@ -1,5 +1,5 @@
import { renderHook, act } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react';
import { defaultParameters, useChangeMetadataParameters } from './useChangeMetadataParameters'; import { useChangeMetadataParameters } from './useChangeMetadataParameters';
import { TrappedStatus } from '../../../types/metadata'; import { TrappedStatus } from '../../../types/metadata';
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
@ -7,7 +7,19 @@ describe('useChangeMetadataParameters', () => {
test('should initialize with default parameters', () => { test('should initialize with default parameters', () => {
const { result } = renderHook(() => useChangeMetadataParameters()); const { result } = renderHook(() => useChangeMetadataParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters); expect(result.current.parameters).toEqual({
title: '',
author: '',
subject: '',
keywords: '',
creator: '',
producer: '',
creationDate: '',
modificationDate: '',
trapped: TrappedStatus.UNKNOWN,
customMetadata: [],
deleteAll: false,
});
}); });
describe('parameter updates', () => { describe('parameter updates', () => {
@ -18,8 +30,8 @@ describe('useChangeMetadataParameters', () => {
{ paramName: 'keywords', value: 'test, metadata' }, { paramName: 'keywords', value: 'test, metadata' },
{ paramName: 'creator', value: 'Test Creator' }, { paramName: 'creator', value: 'Test Creator' },
{ paramName: 'producer', value: 'Test Producer' }, { paramName: 'producer', value: 'Test Producer' },
{ paramName: 'creationDate', value: new Date('2025/01/17 14:30:00') }, { paramName: 'creationDate', value: '2025/01/17 14:30:00' },
{ paramName: 'modificationDate', value: new Date('2025/01/17 15:30:00') }, { paramName: 'modificationDate', value: '2025/01/17 15:30:00' },
{ paramName: 'trapped', value: TrappedStatus.TRUE }, { paramName: 'trapped', value: TrappedStatus.TRUE },
{ paramName: 'deleteAll', value: true }, { paramName: 'deleteAll', value: true },
] as const)('should update $paramName parameter', ({ paramName, value }) => { ] as const)('should update $paramName parameter', ({ paramName, value }) => {
@ -42,8 +54,8 @@ describe('useChangeMetadataParameters', () => {
{ description: 'has keywords', updates: { keywords: 'test' }, expected: true }, { description: 'has keywords', updates: { keywords: 'test' }, expected: true },
{ description: 'has creator', updates: { creator: 'Test Creator' }, expected: true }, { description: 'has creator', updates: { creator: 'Test Creator' }, expected: true },
{ description: 'has producer', updates: { producer: 'Test Producer' }, expected: true }, { description: 'has producer', updates: { producer: 'Test Producer' }, expected: true },
{ description: 'has creation date', updates: { creationDate: new Date('2025/01/17 14:30:00') }, expected: true }, { description: 'has creation date', updates: { creationDate: '2025/01/17 14:30:00' }, expected: true },
{ description: 'has modification date', updates: { modificationDate: new Date('2025/01/17 14:30:00') }, expected: true }, { description: 'has modification date', updates: { modificationDate: '2025/01/17 14:30:00' }, expected: true },
{ description: 'has trapped status', updates: { trapped: TrappedStatus.TRUE }, expected: true }, { description: 'has trapped status', updates: { trapped: TrappedStatus.TRUE }, expected: true },
{ description: 'no meaningful content', updates: {}, expected: false }, { description: 'no meaningful content', updates: {}, expected: false },
{ description: 'whitespace only', updates: { title: ' ', author: ' ' }, expected: false }, { description: 'whitespace only', updates: { title: ' ', author: ' ' }, expected: false },
@ -60,9 +72,11 @@ describe('useChangeMetadataParameters', () => {
}); });
test.each([ test.each([
{ description: 'valid creation date', updates: { title: 'Test', creationDate: new Date('2025/01/17 14:30:00') }, expected: true }, { description: 'invalid creation date', updates: { title: 'Test', creationDate: 'invalid-date' }, expected: false },
{ description: 'valid modification date', updates: { title: 'Test', modificationDate: new Date('2025/01/17 14:30:00') }, expected: true }, { description: 'invalid modification date', updates: { title: 'Test', modificationDate: 'not-a-date' }, expected: false },
{ description: 'empty dates are valid', updates: { title: 'Test', creationDate: null, modificationDate: null }, expected: true }, { description: 'valid creation date', updates: { title: 'Test', creationDate: '2025/01/17 14:30:00' }, expected: true },
{ description: 'valid modification date', updates: { title: 'Test', modificationDate: '2025/01/17 14:30:00' }, expected: true },
{ description: 'empty dates are valid', updates: { title: 'Test', creationDate: '', modificationDate: '' }, expected: true },
])('should validate dates correctly with $description', ({ updates, expected }) => { ])('should validate dates correctly with $description', ({ updates, expected }) => {
const { result } = renderHook(() => useChangeMetadataParameters()); const { result } = renderHook(() => useChangeMetadataParameters());

View File

@ -11,9 +11,9 @@ export interface ChangeMetadataParameters extends BaseParameters {
creator: string; creator: string;
producer: string; producer: string;
// Date fields // Date fields (format: yyyy/MM/dd HH:mm:ss)
creationDate: Date | null; creationDate: string;
modificationDate: Date | null; modificationDate: string;
// Trapped status // Trapped status
trapped: TrappedStatus; trapped: TrappedStatus;
@ -32,8 +32,8 @@ export const defaultParameters: ChangeMetadataParameters = {
keywords: '', keywords: '',
creator: '', creator: '',
producer: '', producer: '',
creationDate: null, creationDate: '',
modificationDate: null, modificationDate: '',
trapped: TrappedStatus.UNKNOWN, trapped: TrappedStatus.UNKNOWN,
customMetadata: [], customMetadata: [],
deleteAll: false, deleteAll: false,
@ -42,45 +42,6 @@ export const defaultParameters: ChangeMetadataParameters = {
// Global counter for custom metadata IDs // Global counter for custom metadata IDs
let customMetadataIdCounter = 1; let customMetadataIdCounter = 1;
// Utility functions that can work with external parameters
export const createCustomMetadataFunctions = (
parameters: ChangeMetadataParameters,
onParameterChange: <K extends keyof ChangeMetadataParameters>(key: K, value: ChangeMetadataParameters[K]) => void
) => {
const addCustomMetadata = (key: string = '', value: string = '') => {
const newEntry: CustomMetadataEntry = {
key,
value,
id: `custom${customMetadataIdCounter++}`,
};
onParameterChange('customMetadata', [
...parameters.customMetadata,
newEntry,
]);
};
const removeCustomMetadata = (id: string) => {
onParameterChange('customMetadata',
parameters.customMetadata.filter(entry => entry.id !== id)
);
};
const updateCustomMetadata = (id: string, key: string, value: string) => {
onParameterChange('customMetadata',
parameters.customMetadata.map(entry =>
entry.id === id ? { ...entry, key, value } : entry
)
);
};
return {
addCustomMetadata,
removeCustomMetadata,
updateCustomMetadata
};
};
// Validation function // Validation function
const validateParameters = (params: ChangeMetadataParameters): boolean => { const validateParameters = (params: ChangeMetadataParameters): boolean => {
// If deleteAll is true, no other validation needed // If deleteAll is true, no other validation needed
@ -96,8 +57,8 @@ const validateParameters = (params: ChangeMetadataParameters): boolean => {
|| params.keywords.trim() || params.keywords.trim()
|| params.creator.trim() || params.creator.trim()
|| params.producer.trim() || params.producer.trim()
|| params.creationDate || params.creationDate.trim()
|| params.modificationDate || params.modificationDate.trim()
|| params.trapped !== TrappedStatus.UNKNOWN || params.trapped !== TrappedStatus.UNKNOWN
); );
@ -105,7 +66,12 @@ const validateParameters = (params: ChangeMetadataParameters): boolean => {
entry => entry.key.trim() && entry.value.trim() entry => entry.key.trim() && entry.value.trim()
); );
return hasStandardMetadata || hasCustomMetadata; // Date validation if provided
const datePattern = /^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/;
const isValidCreationDate = !params.creationDate.trim() || datePattern.test(params.creationDate);
const isValidModificationDate = !params.modificationDate.trim() || datePattern.test(params.modificationDate);
return (hasStandardMetadata || hasCustomMetadata) && isValidCreationDate && isValidModificationDate;
}; };
export type ChangeMetadataParametersHook = BaseParametersHook<ChangeMetadataParameters> & { export type ChangeMetadataParametersHook = BaseParametersHook<ChangeMetadataParameters> & {
@ -121,11 +87,32 @@ export const useChangeMetadataParameters = (): ChangeMetadataParametersHook => {
validateFn: validateParameters, validateFn: validateParameters,
}); });
// Use the utility functions with the hook's parameters and updateParameter const addCustomMetadata = (key: string = '', value: string = '') => {
const { addCustomMetadata, removeCustomMetadata, updateCustomMetadata } = createCustomMetadataFunctions( const newEntry: CustomMetadataEntry = {
base.parameters, key,
base.updateParameter, value,
); id: `custom${customMetadataIdCounter++}`,
};
base.updateParameter('customMetadata', [
...base.parameters.customMetadata,
newEntry,
]);
};
const removeCustomMetadata = (id: string) => {
base.updateParameter('customMetadata',
base.parameters.customMetadata.filter(entry => entry.id !== id)
);
};
const updateCustomMetadata = (id: string, key: string, value: string) => {
base.updateParameter('customMetadata',
base.parameters.customMetadata.map(entry =>
entry.id === id ? { ...entry, key, value } : entry
)
);
};
return { return {
...base, ...base,

View File

@ -47,8 +47,8 @@ export const useMetadataExtraction = (params: MetadataExtractionParams) => {
params.updateParameter('keywords', metadata.keywords); params.updateParameter('keywords', metadata.keywords);
params.updateParameter('creator', metadata.creator); params.updateParameter('creator', metadata.creator);
params.updateParameter('producer', metadata.producer); params.updateParameter('producer', metadata.producer);
params.updateParameter('creationDate', metadata.creationDate ? new Date(metadata.creationDate) : null); params.updateParameter('creationDate', metadata.creationDate);
params.updateParameter('modificationDate', metadata.modificationDate ? new Date(metadata.modificationDate) : null); params.updateParameter('modificationDate', metadata.modificationDate);
params.updateParameter('trapped', metadata.trapped); params.updateParameter('trapped', metadata.trapped);
params.updateParameter('customMetadata', metadata.customMetadata); params.updateParameter('customMetadata', metadata.customMetadata);

View File

@ -1,5 +1,4 @@
import '@mantine/core/styles.css'; import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '../vite-env.d.ts'; import '../vite-env.d.ts';
import './index.css'; // Import Tailwind CSS import './index.css'; // Import Tailwind CSS
import React from 'react'; import React from 'react';

View File

@ -1,58 +0,0 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
import { useAdjustPageScaleParameters } from "../hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
import { useAdjustPageScaleOperation } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "../types/tool";
import { useAdjustPageScaleTips } from "../components/tooltips/useAdjustPageScaleTips";
const AdjustPageScale = (props: BaseToolProps) => {
const { t } = useTranslation();
const adjustPageScaleTips = useAdjustPageScaleTips();
const base = useBaseTool(
'adjustPageScale',
useAdjustPageScaleParameters,
useAdjustPageScaleOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: "Settings",
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: adjustPageScaleTips,
content: (
<AdjustPageScaleSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("adjustPageScale.submit", "Adjust Page Scale"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("adjustPageScale.title", "Page Scale Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default AdjustPageScale as ToolComponent;

View File

@ -65,7 +65,7 @@ const ChangeMetadata = (props: BaseToolProps) => {
// Create step objects // Create step objects
const createStandardMetadataStep = () => ({ const createStandardMetadataStep = () => ({
title: t("changeMetadata.standardFields.title", "Standard Fields"), title: t("changeMetadata.standardFields.title", "Standard Metadata"),
isCollapsed: getActualCollapsedState(MetadataStep.STANDARD_METADATA), isCollapsed: getActualCollapsedState(MetadataStep.STANDARD_METADATA),
onCollapsedClick: () => handleStepToggle(MetadataStep.STANDARD_METADATA), onCollapsedClick: () => handleStepToggle(MetadataStep.STANDARD_METADATA),
tooltip: standardMetadataTips, tooltip: standardMetadataTips,
@ -79,7 +79,7 @@ const ChangeMetadata = (props: BaseToolProps) => {
}); });
const createDocumentDatesStep = () => ({ const createDocumentDatesStep = () => ({
title: t("changeMetadata.dates.title", "Date Fields"), title: t("changeMetadata.dates.title", "Document Dates"),
isCollapsed: getActualCollapsedState(MetadataStep.DOCUMENT_DATES), isCollapsed: getActualCollapsedState(MetadataStep.DOCUMENT_DATES),
onCollapsedClick: () => handleStepToggle(MetadataStep.DOCUMENT_DATES), onCollapsedClick: () => handleStepToggle(MetadataStep.DOCUMENT_DATES),
tooltip: documentDatesTips, tooltip: documentDatesTips,
@ -113,7 +113,7 @@ const ChangeMetadata = (props: BaseToolProps) => {
const buildSteps = () => { const buildSteps = () => {
const steps = [ const steps = [
{ {
title: t("changeMetadata.deleteAll.label", "Remove Existing Metadata"), title: t("changeMetadata.deleteAll.label", "Delete All Metadata"),
isCollapsed: getActualCollapsedState(MetadataStep.DELETE_ALL), isCollapsed: getActualCollapsedState(MetadataStep.DELETE_ALL),
onCollapsedClick: () => handleStepToggle(MetadataStep.DELETE_ALL), onCollapsedClick: () => handleStepToggle(MetadataStep.DELETE_ALL),
tooltip: deleteAllTips, tooltip: deleteAllTips,