mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-24 12:36:13 +00:00
Compare commits
No commits in common. "19eee1484b3da9c2134c04b12ad3839a5b62af66" and "ecbe2b8b40ff910351ed3a3a4b127c0f25503dd8" have entirely different histories.
19eee1484b
...
ecbe2b8b40
@ -1510,6 +1510,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.",
|
||||||
@ -1517,44 +1518,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"
|
||||||
},
|
},
|
||||||
|
@ -171,8 +171,7 @@ const PageEditor = ({
|
|||||||
},
|
},
|
||||||
() => splitPositions,
|
() => splitPositions,
|
||||||
setSplitPositions,
|
setSplitPositions,
|
||||||
() => getPageNumbersFromIds(selectedPageIds),
|
() => getPageNumbersFromIds(selectedPageIds)
|
||||||
closePdf
|
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(deleteCommand);
|
undoManagerRef.current.executeCommand(deleteCommand);
|
||||||
}
|
}
|
||||||
@ -229,8 +228,7 @@ const PageEditor = ({
|
|||||||
},
|
},
|
||||||
() => splitPositions,
|
() => splitPositions,
|
||||||
setSplitPositions,
|
setSplitPositions,
|
||||||
() => selectedPageNumbers,
|
() => selectedPageNumbers
|
||||||
closePdf
|
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(deleteCommand);
|
undoManagerRef.current.executeCommand(deleteCommand);
|
||||||
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers]);
|
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers]);
|
||||||
@ -248,8 +246,7 @@ const PageEditor = ({
|
|||||||
},
|
},
|
||||||
() => splitPositions,
|
() => splitPositions,
|
||||||
setSplitPositions,
|
setSplitPositions,
|
||||||
() => getPageNumbersFromIds(selectedPageIds),
|
() => getPageNumbersFromIds(selectedPageIds)
|
||||||
closePdf
|
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(deleteCommand);
|
undoManagerRef.current.executeCommand(deleteCommand);
|
||||||
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Flex } from '@mantine/core';
|
import { Button, Text, NumberInput, Group, Flex } from '@mantine/core';
|
||||||
import classes from './BulkSelectionPanel.module.css';
|
import classes from './BulkSelectionPanel.module.css';
|
||||||
import {
|
import {
|
||||||
appendExpression,
|
appendExpression,
|
||||||
@ -9,8 +9,6 @@ import {
|
|||||||
everyNthExpression,
|
everyNthExpression,
|
||||||
rangeExpression,
|
rangeExpression,
|
||||||
} from './BulkSelection';
|
} from './BulkSelection';
|
||||||
import SelectPages from './SelectPages';
|
|
||||||
import OperatorsSection from './OperatorsSection';
|
|
||||||
|
|
||||||
interface AdvancedSelectionPanelProps {
|
interface AdvancedSelectionPanelProps {
|
||||||
csvInput: string;
|
csvInput: string;
|
||||||
@ -27,32 +25,16 @@ const AdvancedSelectionPanel = ({
|
|||||||
maxPages,
|
maxPages,
|
||||||
advancedOpened,
|
advancedOpened,
|
||||||
}: AdvancedSelectionPanelProps) => {
|
}: AdvancedSelectionPanelProps) => {
|
||||||
|
// Visibility now controlled by parent
|
||||||
|
const [firstNValue, setFirstNValue] = useState<number | ''>('');
|
||||||
|
const [lastNValue, setLastNValue] = useState<number | ''>('');
|
||||||
|
const [everyNthValue, setEveryNthValue] = useState<number | ''>('');
|
||||||
|
const [rangeStart, setRangeStart] = useState<number | ''>('');
|
||||||
const [rangeEnd, setRangeEnd] = useState<number | ''>('');
|
const [rangeEnd, setRangeEnd] = useState<number | ''>('');
|
||||||
|
const [firstNError, setFirstNError] = useState<string | null>(null);
|
||||||
|
const [lastNError, setLastNError] = useState<string | null>(null);
|
||||||
|
const [rangeError, setRangeError] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleRangeEndChange = (val: string | number) => {
|
|
||||||
const next = typeof val === 'number' ? val : '';
|
|
||||||
setRangeEnd(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Named validation functions
|
|
||||||
const validatePositiveNumber = (value: number): string | null => {
|
|
||||||
return value <= 0 ? 'Enter a positive number' : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateRangeStart = (start: number): string | null => {
|
|
||||||
if (start <= 0) return 'Values must be positive';
|
|
||||||
if (typeof rangeEnd === 'number' && start > rangeEnd) {
|
|
||||||
return 'From must be less than or equal to To';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateRangeEnd = (end: number): string | null => {
|
|
||||||
if (end <= 0) return 'Values must be positive';
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Named callback functions
|
|
||||||
const applyExpression = (expr: string) => {
|
const applyExpression = (expr: string) => {
|
||||||
const nextInput = appendExpression(csvInput, expr);
|
const nextInput = appendExpression(csvInput, expr);
|
||||||
setCsvInput(nextInput);
|
setCsvInput(nextInput);
|
||||||
@ -64,28 +46,6 @@ const AdvancedSelectionPanel = ({
|
|||||||
setCsvInput(next);
|
setCsvInput(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFirstNApply = (value: number) => {
|
|
||||||
const expr = firstNExpression(value, maxPages);
|
|
||||||
if (expr) applyExpression(expr);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLastNApply = (value: number) => {
|
|
||||||
const expr = lastNExpression(value, maxPages);
|
|
||||||
if (expr) applyExpression(expr);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEveryNthApply = (value: number) => {
|
|
||||||
const expr = everyNthExpression(value);
|
|
||||||
if (expr) applyExpression(expr);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRangeApply = (start: number) => {
|
|
||||||
if (typeof rangeEnd !== 'number') return;
|
|
||||||
const expr = rangeExpression(start, rangeEnd, maxPages);
|
|
||||||
if (expr) applyExpression(expr);
|
|
||||||
setRangeEnd('');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Advanced section */}
|
{/* Advanced section */}
|
||||||
@ -94,47 +54,211 @@ const AdvancedSelectionPanel = ({
|
|||||||
<div className={classes.advancedContent}>
|
<div className={classes.advancedContent}>
|
||||||
{/* Cards row */}
|
{/* Cards row */}
|
||||||
<Flex direction="row" mb="xs" wrap="wrap">
|
<Flex direction="row" mb="xs" wrap="wrap">
|
||||||
<SelectPages
|
{/* First N Pages - Card Style */}
|
||||||
title="First N Pages"
|
<div className={classes.advancedCard}>
|
||||||
|
<Text size="sm" fw={600} c="var(--text-secondary)" mb="xs">First N Pages</Text>
|
||||||
|
{firstNError && (<Text size="xs" c="var(--text-brand-accent)" mb="xs">{firstNError}</Text>)}
|
||||||
|
<div className={classes.inputGroup}>
|
||||||
|
<Group gap="sm" align="flex-end" wrap="nowrap">
|
||||||
|
<NumberInput
|
||||||
|
size="sm"
|
||||||
|
value={firstNValue}
|
||||||
|
onChange={(val) => {
|
||||||
|
const next = typeof val === 'number' ? val : '';
|
||||||
|
setFirstNValue(next);
|
||||||
|
if (next === '') setFirstNError(null);
|
||||||
|
else if (typeof next === 'number' && next <= 0) setFirstNError('Enter a positive number');
|
||||||
|
else setFirstNError(null);
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
placeholder="Number of pages"
|
placeholder="Number of pages"
|
||||||
onApply={handleFirstNApply}
|
className={classes.fullWidthInput}
|
||||||
maxPages={maxPages}
|
error={Boolean(firstNError)}
|
||||||
validationFn={validatePositiveNumber}
|
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={classes.applyButton}
|
||||||
|
onClick={() => {
|
||||||
|
if (!firstNValue || typeof firstNValue !== 'number') return;
|
||||||
|
const expr = firstNExpression(firstNValue, maxPages);
|
||||||
|
if (expr) applyExpression(expr);
|
||||||
|
setFirstNValue('');
|
||||||
|
}}
|
||||||
|
disabled={Boolean(firstNError) || firstNValue === ''}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SelectPages
|
{/* Range - Card Style */}
|
||||||
title="Range"
|
<div className={classes.advancedCard}>
|
||||||
|
<Text size="sm" fw={600} c="var(--text-secondary)" mb="xs">Range</Text>
|
||||||
|
{rangeError && (<Text size="xs" c="var(--text-brand-accent)" mb="xs">{rangeError}</Text>)}
|
||||||
|
<div className={classes.inputGroup}>
|
||||||
|
<Group gap="sm" align="flex-end" wrap="nowrap">
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<NumberInput
|
||||||
|
size="sm"
|
||||||
|
value={rangeStart}
|
||||||
|
onChange={(val) => {
|
||||||
|
const next = typeof val === 'number' ? val : '';
|
||||||
|
setRangeStart(next);
|
||||||
|
const s = typeof next === 'number' ? next : null;
|
||||||
|
const e = typeof rangeEnd === 'number' ? rangeEnd : null;
|
||||||
|
if (s !== null && s <= 0) setRangeError('Values must be positive');
|
||||||
|
else if (s !== null && e !== null && s > e) setRangeError('From must be less than or equal to To');
|
||||||
|
else setRangeError(null);
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
placeholder="From"
|
placeholder="From"
|
||||||
onApply={handleRangeApply}
|
error={Boolean(rangeError)}
|
||||||
maxPages={maxPages}
|
|
||||||
validationFn={validateRangeStart}
|
|
||||||
isRange={true}
|
|
||||||
rangeEndValue={rangeEnd}
|
|
||||||
onRangeEndChange={handleRangeEndChange}
|
|
||||||
rangeEndPlaceholder="To"
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<NumberInput
|
||||||
|
size="sm"
|
||||||
|
value={rangeEnd}
|
||||||
|
onChange={(val) => {
|
||||||
|
const next = typeof val === 'number' ? val : '';
|
||||||
|
setRangeEnd(next);
|
||||||
|
const e = typeof next === 'number' ? next : null;
|
||||||
|
const s = typeof rangeStart === 'number' ? rangeStart : null;
|
||||||
|
if (e !== null && e <= 0) setRangeError('Values must be positive');
|
||||||
|
else if (s !== null && e !== null && s > e) setRangeError('From must be less than or equal to To');
|
||||||
|
else setRangeError(null);
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
|
placeholder="To"
|
||||||
|
error={Boolean(rangeError)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={classes.applyButton}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
rangeStart === '' || rangeEnd === '' ||
|
||||||
|
typeof rangeStart !== 'number' || typeof rangeEnd !== 'number'
|
||||||
|
) return;
|
||||||
|
const expr = rangeExpression(rangeStart, rangeEnd, maxPages);
|
||||||
|
if (expr) applyExpression(expr);
|
||||||
|
setRangeStart('');
|
||||||
|
setRangeEnd('');
|
||||||
|
}}
|
||||||
|
disabled={Boolean(rangeError) || rangeStart === '' || rangeEnd === ''}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SelectPages
|
{/* Last N Pages - Card Style */}
|
||||||
title="Last N Pages"
|
<div className={classes.advancedCard}>
|
||||||
|
<Text size="sm" fw={600} c="var(--text-secondary)" mb="xs">Last N Pages</Text>
|
||||||
|
{lastNError && (<Text size="xs" c="var(--text-brand-accent)" mb="xs">{lastNError}</Text>)}
|
||||||
|
<div className={classes.inputGroup}>
|
||||||
|
<Group gap="sm" align="flex-end" wrap="nowrap">
|
||||||
|
<NumberInput
|
||||||
|
size="sm"
|
||||||
|
value={lastNValue}
|
||||||
|
onChange={(val) => {
|
||||||
|
const next = typeof val === 'number' ? val : '';
|
||||||
|
setLastNValue(next);
|
||||||
|
if (next === '') setLastNError(null);
|
||||||
|
else if (typeof next === 'number' && next <= 0) setLastNError('Enter a positive number');
|
||||||
|
else setLastNError(null);
|
||||||
|
}}
|
||||||
|
min={1}
|
||||||
placeholder="Number of pages"
|
placeholder="Number of pages"
|
||||||
onApply={handleLastNApply}
|
className={classes.fullWidthInput}
|
||||||
maxPages={maxPages}
|
error={Boolean(lastNError)}
|
||||||
validationFn={validatePositiveNumber}
|
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={classes.applyButton}
|
||||||
|
onClick={() => {
|
||||||
|
if (!lastNValue || typeof lastNValue !== 'number') return;
|
||||||
|
const expr = lastNExpression(lastNValue, maxPages);
|
||||||
|
if (expr) applyExpression(expr);
|
||||||
|
setLastNValue('');
|
||||||
|
}}
|
||||||
|
disabled={Boolean(lastNError) || lastNValue === ''}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SelectPages
|
{/* Every Nth Page - Card Style */}
|
||||||
title="Every Nth Page"
|
<div className={classes.advancedCard}>
|
||||||
|
<Text size="sm" fw={600} c="var(--text-secondary)" mb="xs">Every Nth Page</Text>
|
||||||
|
<div className={classes.inputGroup}>
|
||||||
|
<Group gap="sm" align="flex-end" wrap="nowrap">
|
||||||
|
<NumberInput
|
||||||
|
size="sm"
|
||||||
|
value={everyNthValue}
|
||||||
|
onChange={(val) => setEveryNthValue(typeof val === 'number' ? val : '')}
|
||||||
|
min={1}
|
||||||
placeholder="Step size"
|
placeholder="Step size"
|
||||||
onApply={handleEveryNthApply}
|
className={classes.fullWidthInput}
|
||||||
maxPages={maxPages}
|
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className={classes.applyButton}
|
||||||
|
onClick={() => {
|
||||||
|
if (!everyNthValue || typeof everyNthValue !== 'number') return;
|
||||||
|
const expr = everyNthExpression(everyNthValue);
|
||||||
|
if (expr) applyExpression(expr);
|
||||||
|
setEveryNthValue('');
|
||||||
|
}}
|
||||||
|
disabled={!everyNthValue}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{/* Operators row at bottom */}
|
{/* Operators row at bottom */}
|
||||||
<OperatorsSection
|
<div>
|
||||||
csvInput={csvInput}
|
<Text size="xs" c="var(--text-muted)" fw={500} mb="xs">Add Operators:</Text>
|
||||||
onInsertOperator={insertOperator}
|
<Group gap="sm" wrap="nowrap">
|
||||||
/>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={classes.operatorChip}
|
||||||
|
onClick={() => insertOperator('and')}
|
||||||
|
disabled={!csvInput.trim()}
|
||||||
|
title="Combine selections (both conditions must be true)"
|
||||||
|
>
|
||||||
|
<Text size="xs" fw={500}>and</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={classes.operatorChip}
|
||||||
|
onClick={() => insertOperator('or')}
|
||||||
|
disabled={!csvInput.trim()}
|
||||||
|
title="Add to selection (either condition can be true)"
|
||||||
|
>
|
||||||
|
<Text size="xs" fw={500}>or</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className={classes.operatorChip}
|
||||||
|
onClick={() => insertOperator('not')}
|
||||||
|
disabled={!csvInput.trim()}
|
||||||
|
title="Exclude from selection"
|
||||||
|
>
|
||||||
|
<Text size="xs" fw={500}>not</Text>
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import { Button, Text, Group } from '@mantine/core';
|
|
||||||
import classes from './BulkSelectionPanel.module.css';
|
|
||||||
|
|
||||||
interface OperatorsSectionProps {
|
|
||||||
csvInput: string;
|
|
||||||
onInsertOperator: (op: 'and' | 'or' | 'not') => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const OperatorsSection = ({ csvInput, onInsertOperator }: OperatorsSectionProps) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Text size="xs" c="var(--text-muted)" fw={500} mb="xs">Add Operators:</Text>
|
|
||||||
<Group gap="sm" wrap="nowrap">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className={classes.operatorChip}
|
|
||||||
onClick={() => onInsertOperator('and')}
|
|
||||||
disabled={!csvInput.trim()}
|
|
||||||
title="Combine selections (both conditions must be true)"
|
|
||||||
>
|
|
||||||
<Text size="xs" fw={500}>and</Text>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className={classes.operatorChip}
|
|
||||||
onClick={() => onInsertOperator('or')}
|
|
||||||
disabled={!csvInput.trim()}
|
|
||||||
title="Add to selection (either condition can be true)"
|
|
||||||
>
|
|
||||||
<Text size="xs" fw={500}>or</Text>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className={classes.operatorChip}
|
|
||||||
onClick={() => onInsertOperator('not')}
|
|
||||||
disabled={!csvInput.trim()}
|
|
||||||
title="Exclude from selection"
|
|
||||||
>
|
|
||||||
<Text size="xs" fw={500}>not</Text>
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OperatorsSection;
|
|
@ -1,105 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Button, Text, NumberInput, Group } from '@mantine/core';
|
|
||||||
import classes from './BulkSelectionPanel.module.css';
|
|
||||||
|
|
||||||
interface SelectPagesProps {
|
|
||||||
title: string;
|
|
||||||
placeholder: string;
|
|
||||||
onApply: (value: number) => void;
|
|
||||||
maxPages: number;
|
|
||||||
validationFn?: (value: number) => string | null;
|
|
||||||
isRange?: boolean;
|
|
||||||
rangeEndValue?: number | '';
|
|
||||||
onRangeEndChange?: (value: string | number) => void;
|
|
||||||
rangeEndPlaceholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SelectPages = ({
|
|
||||||
title,
|
|
||||||
placeholder,
|
|
||||||
onApply,
|
|
||||||
maxPages,
|
|
||||||
validationFn,
|
|
||||||
isRange = false,
|
|
||||||
rangeEndValue,
|
|
||||||
onRangeEndChange,
|
|
||||||
rangeEndPlaceholder,
|
|
||||||
}: SelectPagesProps) => {
|
|
||||||
const [value, setValue] = useState<number | ''>('');
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleValueChange = (val: string | number) => {
|
|
||||||
const next = typeof val === 'number' ? val : '';
|
|
||||||
setValue(next);
|
|
||||||
|
|
||||||
if (validationFn && typeof next === 'number') {
|
|
||||||
setError(validationFn(next));
|
|
||||||
} else {
|
|
||||||
setError(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApply = () => {
|
|
||||||
if (value === '' || typeof value !== 'number') return;
|
|
||||||
onApply(value);
|
|
||||||
setValue('');
|
|
||||||
setError(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDisabled = Boolean(error) || value === '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.advancedCard}>
|
|
||||||
<Text size="sm" fw={600} c="var(--text-secondary)" mb="xs">{title}</Text>
|
|
||||||
{error && (<Text size="xs" c="var(--text-brand-accent)" mb="xs">{error}</Text>)}
|
|
||||||
<div className={classes.inputGroup}>
|
|
||||||
<Group gap="sm" align="flex-end" wrap="nowrap">
|
|
||||||
{isRange ? (
|
|
||||||
<>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<NumberInput
|
|
||||||
size="sm"
|
|
||||||
value={value}
|
|
||||||
onChange={handleValueChange}
|
|
||||||
min={1}
|
|
||||||
placeholder={placeholder}
|
|
||||||
error={Boolean(error)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<NumberInput
|
|
||||||
size="sm"
|
|
||||||
value={rangeEndValue}
|
|
||||||
onChange={onRangeEndChange}
|
|
||||||
min={1}
|
|
||||||
placeholder={rangeEndPlaceholder}
|
|
||||||
error={Boolean(error)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<NumberInput
|
|
||||||
size="sm"
|
|
||||||
value={value}
|
|
||||||
onChange={handleValueChange}
|
|
||||||
min={1}
|
|
||||||
placeholder={placeholder}
|
|
||||||
className={classes.fullWidthInput}
|
|
||||||
error={Boolean(error)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className={classes.applyButton}
|
|
||||||
onClick={handleApply}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
Apply
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectPages;
|
|
@ -59,7 +59,6 @@ export class DeletePagesCommand extends DOMCommand {
|
|||||||
private originalSelectedPages: number[] = [];
|
private originalSelectedPages: number[] = [];
|
||||||
private hasExecuted: boolean = false;
|
private hasExecuted: boolean = false;
|
||||||
private pageIdsToDelete: string[] = [];
|
private pageIdsToDelete: string[] = [];
|
||||||
private onAllPagesDeleted?: () => void;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private pagesToDelete: number[],
|
private pagesToDelete: number[],
|
||||||
@ -68,11 +67,9 @@ export class DeletePagesCommand extends DOMCommand {
|
|||||||
private setSelectedPages: (pages: number[]) => void,
|
private setSelectedPages: (pages: number[]) => void,
|
||||||
private getSplitPositions: () => Set<number>,
|
private getSplitPositions: () => Set<number>,
|
||||||
private setSplitPositions: (positions: Set<number>) => void,
|
private setSplitPositions: (positions: Set<number>) => void,
|
||||||
private getSelectedPages: () => number[],
|
private getSelectedPages: () => number[]
|
||||||
onAllPagesDeleted?: () => void
|
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.onAllPagesDeleted = onAllPagesDeleted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(): void {
|
||||||
@ -102,13 +99,7 @@ export class DeletePagesCommand extends DOMCommand {
|
|||||||
!this.pageIdsToDelete.includes(page.id)
|
!this.pageIdsToDelete.includes(page.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (remainingPages.length === 0) {
|
if (remainingPages.length === 0) return; // Safety check
|
||||||
// If all pages would be deleted, clear selection/splits and close PDF
|
|
||||||
this.setSelectedPages([]);
|
|
||||||
this.setSplitPositions(new Set());
|
|
||||||
this.onAllPagesDeleted?.();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renumber remaining pages
|
// Renumber remaining pages
|
||||||
remainingPages.forEach((page, index) => {
|
remainingPages.forEach((page, index) => {
|
||||||
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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;
|
|
@ -54,7 +54,7 @@ const renderTooltipTitle = (
|
|||||||
<Text fw={400} size="sm">
|
<Text fw={400} size="sm">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<LocalIcon icon="info-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />
|
<LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
@ -22,7 +22,7 @@ export function ToolWorkflowTitle({ title, tooltip, description }: ToolWorkflowT
|
|||||||
<Text fw={500} size="lg" p="xs">
|
<Text fw={500} size="lg" p="xs">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{tooltip && <LocalIcon icon="info-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />}
|
{tooltip && <LocalIcon icon="gpp-maybe-outline-rounded" width="1.25rem" height="1.25rem" style={{ color: 'var(--icon-files-color)' }} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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.")
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
@ -49,11 +49,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";
|
|
||||||
|
|
||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||||
|
|
||||||
@ -340,14 +337,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" />,
|
||||||
|
@ -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.'))
|
|
||||||
});
|
|
||||||
};
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
@ -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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
@ -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;
|
|
Loading…
x
Reference in New Issue
Block a user