mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
split up the BulkSelectionPanel into multiple components
This commit is contained in:
parent
bb13c24776
commit
53d305d41d
@ -1,18 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Group, TextInput, Button, Text, NumberInput, Flex } from '@mantine/core';
|
||||
import LocalIcon from '../shared/LocalIcon';
|
||||
import { Tooltip } from '../shared/Tooltip';
|
||||
import { usePageSelectionTips } from '../tooltips/usePageSelectionTips';
|
||||
import classes from './BulkSelectionPanel.module.css';
|
||||
import classes from './bulkSelectionPanel/BulkSelectionPanel.module.css';
|
||||
import { parseSelectionWithDiagnostics } from '../../utils/bulkselection/parseSelection';
|
||||
import {
|
||||
appendExpression,
|
||||
insertOperatorSmart,
|
||||
firstNExpression,
|
||||
lastNExpression,
|
||||
everyNthExpression,
|
||||
rangeExpression,
|
||||
} from './BulkSelection';
|
||||
import PageSelectionInput from './bulkSelectionPanel/PageSelectionInput';
|
||||
import SelectedPagesDisplay from './bulkSelectionPanel/SelectedPagesDisplay';
|
||||
import AdvancedSelectionPanel from './bulkSelectionPanel/AdvancedSelectionPanel';
|
||||
|
||||
interface BulkSelectionPanelProps {
|
||||
csvInput: string;
|
||||
@ -29,18 +20,7 @@ const BulkSelectionPanel = ({
|
||||
displayDocument,
|
||||
onUpdatePagesFromCSV,
|
||||
}: BulkSelectionPanelProps) => {
|
||||
const pageSelectionTips = usePageSelectionTips();
|
||||
const [advancedOpened, setAdvancedOpened] = useState<boolean>(false);
|
||||
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 [firstNError, setFirstNError] = useState<string | null>(null);
|
||||
const [lastNError, setLastNError] = useState<string | null>(null);
|
||||
const [rangeError, setRangeError] = useState<string | null>(null);
|
||||
const [syntaxError, setSyntaxError] = useState<string | null>(null);
|
||||
|
||||
const maxPages = displayDocument?.pages?.length ?? 0;
|
||||
|
||||
|
||||
@ -59,331 +39,33 @@ const BulkSelectionPanel = ({
|
||||
}
|
||||
}, [csvInput, maxPages]);
|
||||
|
||||
const applyExpression = (expr: string) => {
|
||||
const nextInput = appendExpression(csvInput, expr);
|
||||
setCsvInput(nextInput);
|
||||
onUpdatePagesFromCSV(nextInput);
|
||||
};
|
||||
|
||||
const handleNone = () => {
|
||||
const handleClear = () => {
|
||||
setCsvInput('');
|
||||
onUpdatePagesFromCSV();
|
||||
setFirstNValue('');
|
||||
setLastNValue('');
|
||||
setEveryNthValue('');
|
||||
};
|
||||
|
||||
const insertOperator = (op: 'and' | 'or' | 'not') => {
|
||||
const next = insertOperatorSmart(csvInput, op);
|
||||
setCsvInput(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.panelContainer}>
|
||||
{syntaxError && (
|
||||
<Text size="xs" className={classes.errorText}>{syntaxError}</Text>
|
||||
)}
|
||||
<Group className={classes.panelGroup}>
|
||||
<TextInput
|
||||
value={csvInput}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value;
|
||||
setCsvInput(next);
|
||||
onUpdatePagesFromCSV(next);
|
||||
}}
|
||||
placeholder="1,3,5-10"
|
||||
rightSection={
|
||||
csvInput && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={handleNone}
|
||||
style={{
|
||||
color: 'var(--mantine-color-gray-6)',
|
||||
minWidth: 'auto',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
padding: 0
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
label={
|
||||
<Tooltip
|
||||
position="left"
|
||||
offset={20}
|
||||
header={pageSelectionTips.header}
|
||||
portalTarget={document.body}
|
||||
pinOnClick={true}
|
||||
containerStyle={{ marginTop: "1rem"}}
|
||||
tips={pageSelectionTips.tips}
|
||||
>
|
||||
<Flex onClick={(e) => e.stopPropagation()} align="center" gap="xs" my="sm">
|
||||
<LocalIcon icon="gpp-maybe-outline-rounded" width="1rem" height="1rem" style={{ color: 'var(--primary-color, #3b82f6)' }} />
|
||||
<Text>Page Selection</Text>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onUpdatePagesFromCSV()}
|
||||
className={classes.textInput}
|
||||
/>
|
||||
</Group>
|
||||
<PageSelectionInput
|
||||
csvInput={csvInput}
|
||||
setCsvInput={setCsvInput}
|
||||
onUpdatePagesFromCSV={onUpdatePagesFromCSV}
|
||||
onClear={handleClear}
|
||||
/>
|
||||
|
||||
{/* Selected pages container */}
|
||||
{selectedPageIds.length > 0 && (
|
||||
<div className={classes.selectedList}>
|
||||
<Text size="sm" c="dimmed" className={classes.selectedText}>
|
||||
Selected: {selectedPageIds.length} pages ({displayDocument ? selectedPageIds.map(id => {
|
||||
const page = displayDocument.pages.find(p => p.id === id);
|
||||
return page?.pageNumber || 0;
|
||||
}).filter(n => n > 0).join(', ') : ''})
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<SelectedPagesDisplay
|
||||
selectedPageIds={selectedPageIds}
|
||||
displayDocument={displayDocument}
|
||||
syntaxError={syntaxError}
|
||||
/>
|
||||
|
||||
{/* Advanced button */}
|
||||
<div className={classes.dropdownContainer}>
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
onClick={() => setAdvancedOpened(!advancedOpened)}
|
||||
>
|
||||
Advanced
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Advanced section */}
|
||||
{advancedOpened && (
|
||||
<div className={classes.advancedSection}>
|
||||
<div className={classes.advancedHeader}>
|
||||
<Text size="sm" fw={500}>Advanced Selection</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => setAdvancedOpened(false)}
|
||||
className={classes.closeButton}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
<div className={classes.advancedContent}>
|
||||
<div className={classes.leftCol}>
|
||||
{/* First N Pages - Card Style */}
|
||||
<div className={classes.advancedCard}>
|
||||
<Text size="sm" fw={600} c="gray.7" mb="sm">First N Pages</Text>
|
||||
{firstNError && (<Text size="xs" c="red" mb="xs">{firstNError}</Text>)}
|
||||
<div className={classes.inputGroup}>
|
||||
<Text size="xs" c="gray.6" mb="xs">Number of pages:</Text>
|
||||
<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="10"
|
||||
className={classes.fullWidthInput}
|
||||
error={Boolean(firstNError)}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* Range - Card Style */}
|
||||
<div className={classes.advancedCard}>
|
||||
<Text size="sm" fw={600} c="gray.7" mb="sm">Range</Text>
|
||||
{rangeError && (<Text size="xs" c="red" mb="xs">{rangeError}</Text>)}
|
||||
<div className={classes.inputGroup}>
|
||||
<Group gap="sm" align="flex-end" wrap="nowrap" mb="sm">
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="xs" c="gray.6" mb="xs">From:</Text>
|
||||
<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="5"
|
||||
error={Boolean(rangeError)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="xs" c="gray.6" mb="xs">To:</Text>
|
||||
<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="10"
|
||||
error={Boolean(rangeError)}
|
||||
/>
|
||||
</div>
|
||||
</Group>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last N Pages - Card Style */}
|
||||
<div className={classes.advancedCard}>
|
||||
<Text size="sm" fw={600} c="gray.7" mb="sm">Last N Pages</Text>
|
||||
{lastNError && (<Text size="xs" c="red" mb="xs">{lastNError}</Text>)}
|
||||
<div className={classes.inputGroup}>
|
||||
<Text size="xs" c="gray.6" mb="xs">Number of pages:</Text>
|
||||
<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="10"
|
||||
className={classes.fullWidthInput}
|
||||
error={Boolean(lastNError)}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* Every Nth Page - Card Style */}
|
||||
<div className={classes.advancedCard}>
|
||||
<Text size="sm" fw={600} c="gray.7" mb="sm">Every Nth Page</Text>
|
||||
<div className={classes.inputGroup}>
|
||||
<Text size="xs" c="gray.6" mb="xs">Step size:</Text>
|
||||
<Group gap="sm" align="flex-end" wrap="nowrap">
|
||||
<NumberInput
|
||||
size="sm"
|
||||
value={everyNthValue}
|
||||
onChange={(val) => setEveryNthValue(typeof val === 'number' ? val : '')}
|
||||
min={1}
|
||||
placeholder="5"
|
||||
className={classes.fullWidthInput}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<div className={classes.rightCol}>
|
||||
<Text size="xs" c="gray.6" fw={500} mb="sm">Add Operators:</Text>
|
||||
<div className={classes.operatorGroup}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AdvancedSelectionPanel
|
||||
csvInput={csvInput}
|
||||
setCsvInput={setCsvInput}
|
||||
onUpdatePagesFromCSV={onUpdatePagesFromCSV}
|
||||
maxPages={maxPages}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,293 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Text, NumberInput, Group } from '@mantine/core';
|
||||
import classes from './BulkSelectionPanel.module.css';
|
||||
import {
|
||||
appendExpression,
|
||||
insertOperatorSmart,
|
||||
firstNExpression,
|
||||
lastNExpression,
|
||||
everyNthExpression,
|
||||
rangeExpression,
|
||||
} from './BulkSelection';
|
||||
|
||||
interface AdvancedSelectionPanelProps {
|
||||
csvInput: string;
|
||||
setCsvInput: (value: string) => void;
|
||||
onUpdatePagesFromCSV: (override?: string) => void;
|
||||
maxPages: number;
|
||||
}
|
||||
|
||||
const AdvancedSelectionPanel = ({
|
||||
csvInput,
|
||||
setCsvInput,
|
||||
onUpdatePagesFromCSV,
|
||||
maxPages,
|
||||
}: AdvancedSelectionPanelProps) => {
|
||||
const [advancedOpened, setAdvancedOpened] = useState<boolean>(false);
|
||||
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 [firstNError, setFirstNError] = useState<string | null>(null);
|
||||
const [lastNError, setLastNError] = useState<string | null>(null);
|
||||
const [rangeError, setRangeError] = useState<string | null>(null);
|
||||
|
||||
const applyExpression = (expr: string) => {
|
||||
const nextInput = appendExpression(csvInput, expr);
|
||||
setCsvInput(nextInput);
|
||||
onUpdatePagesFromCSV(nextInput);
|
||||
};
|
||||
|
||||
const insertOperator = (op: 'and' | 'or' | 'not') => {
|
||||
const next = insertOperatorSmart(csvInput, op);
|
||||
setCsvInput(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Advanced button */}
|
||||
<div className={classes.dropdownContainer}>
|
||||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
onClick={() => setAdvancedOpened(!advancedOpened)}
|
||||
>
|
||||
Advanced
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Advanced section */}
|
||||
{advancedOpened && (
|
||||
<div className={classes.advancedSection}>
|
||||
<div className={classes.advancedHeader}>
|
||||
<Text size="sm" fw={500}>Advanced Selection</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => setAdvancedOpened(false)}
|
||||
className={classes.closeButton}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
<div className={classes.advancedContent}>
|
||||
<div className={classes.leftCol}>
|
||||
{/* First N Pages - Card Style */}
|
||||
<div className={classes.advancedCard}>
|
||||
<Text size="sm" fw={600} c="gray.7" mb="sm">First N Pages</Text>
|
||||
{firstNError && (<Text size="xs" c="red" mb="xs">{firstNError}</Text>)}
|
||||
<div className={classes.inputGroup}>
|
||||
<Text size="xs" c="gray.6" mb="xs">Number of pages:</Text>
|
||||
<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="10"
|
||||
className={classes.fullWidthInput}
|
||||
error={Boolean(firstNError)}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* Range - Card Style */}
|
||||
<div className={classes.advancedCard}>
|
||||
<Text size="sm" fw={600} c="gray.7" mb="sm">Range</Text>
|
||||
{rangeError && (<Text size="xs" c="red" mb="xs">{rangeError}</Text>)}
|
||||
<div className={classes.inputGroup}>
|
||||
<Group gap="sm" align="flex-end" wrap="nowrap" mb="sm">
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="xs" c="gray.6" mb="xs">From:</Text>
|
||||
<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="5"
|
||||
error={Boolean(rangeError)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="xs" c="gray.6" mb="xs">To:</Text>
|
||||
<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="10"
|
||||
error={Boolean(rangeError)}
|
||||
/>
|
||||
</div>
|
||||
</Group>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last N Pages - Card Style */}
|
||||
<div className={classes.advancedCard}>
|
||||
<Text size="sm" fw={600} c="gray.7" mb="sm">Last N Pages</Text>
|
||||
{lastNError && (<Text size="xs" c="red" mb="xs">{lastNError}</Text>)}
|
||||
<div className={classes.inputGroup}>
|
||||
<Text size="xs" c="gray.6" mb="xs">Number of pages:</Text>
|
||||
<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="10"
|
||||
className={classes.fullWidthInput}
|
||||
error={Boolean(lastNError)}
|
||||
/>
|
||||
<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>
|
||||
|
||||
{/* Every Nth Page - Card Style */}
|
||||
<div className={classes.advancedCard}>
|
||||
<Text size="sm" fw={600} c="gray.7" mb="sm">Every Nth Page</Text>
|
||||
<div className={classes.inputGroup}>
|
||||
<Text size="xs" c="gray.6" mb="xs">Step size:</Text>
|
||||
<Group gap="sm" align="flex-end" wrap="nowrap">
|
||||
<NumberInput
|
||||
size="sm"
|
||||
value={everyNthValue}
|
||||
onChange={(val) => setEveryNthValue(typeof val === 'number' ? val : '')}
|
||||
min={1}
|
||||
placeholder="5"
|
||||
className={classes.fullWidthInput}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<div className={classes.rightCol}>
|
||||
<Text size="xs" c="gray.6" fw={500} mb="sm">Add Operators:</Text>
|
||||
<div className={classes.operatorGroup}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedSelectionPanel;
|
@ -0,0 +1,73 @@
|
||||
import { Group, TextInput, Button, Text, Flex } from '@mantine/core';
|
||||
import LocalIcon from '../../shared/LocalIcon';
|
||||
import { Tooltip } from '../../shared/Tooltip';
|
||||
import { usePageSelectionTips } from '../../tooltips/usePageSelectionTips';
|
||||
import classes from './BulkSelectionPanel.module.css';
|
||||
|
||||
interface PageSelectionInputProps {
|
||||
csvInput: string;
|
||||
setCsvInput: (value: string) => void;
|
||||
onUpdatePagesFromCSV: (override?: string) => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
const PageSelectionInput = ({
|
||||
csvInput,
|
||||
setCsvInput,
|
||||
onUpdatePagesFromCSV,
|
||||
onClear,
|
||||
}: PageSelectionInputProps) => {
|
||||
const pageSelectionTips = usePageSelectionTips();
|
||||
|
||||
return (
|
||||
<Group className={classes.panelGroup}>
|
||||
<TextInput
|
||||
value={csvInput}
|
||||
onChange={(e) => {
|
||||
const next = e.target.value;
|
||||
setCsvInput(next);
|
||||
onUpdatePagesFromCSV(next);
|
||||
}}
|
||||
placeholder="1,3,5-10"
|
||||
rightSection={
|
||||
csvInput && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
onClick={onClear}
|
||||
style={{
|
||||
color: 'var(--mantine-color-gray-6)',
|
||||
minWidth: 'auto',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
padding: 0
|
||||
}}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
label={
|
||||
<Tooltip
|
||||
position="left"
|
||||
offset={20}
|
||||
header={pageSelectionTips.header}
|
||||
portalTarget={document.body}
|
||||
pinOnClick={true}
|
||||
containerStyle={{ marginTop: "1rem"}}
|
||||
tips={pageSelectionTips.tips}
|
||||
>
|
||||
<Flex onClick={(e) => e.stopPropagation()} align="center" gap="xs" my="sm">
|
||||
<LocalIcon icon="gpp-maybe-outline-rounded" width="1rem" height="1rem" style={{ color: 'var(--primary-color, #3b82f6)' }} />
|
||||
<Text>Page Selection</Text>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onUpdatePagesFromCSV()}
|
||||
className={classes.textInput}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSelectionInput;
|
@ -0,0 +1,35 @@
|
||||
import { Text } from '@mantine/core';
|
||||
import classes from './BulkSelectionPanel.module.css';
|
||||
|
||||
interface SelectedPagesDisplayProps {
|
||||
selectedPageIds: string[];
|
||||
displayDocument?: { pages: { id: string; pageNumber: number }[] };
|
||||
syntaxError: string | null;
|
||||
}
|
||||
|
||||
const SelectedPagesDisplay = ({
|
||||
selectedPageIds,
|
||||
displayDocument,
|
||||
syntaxError,
|
||||
}: SelectedPagesDisplayProps) => {
|
||||
if (selectedPageIds.length === 0 && !syntaxError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.selectedList}>
|
||||
{syntaxError ? (
|
||||
<Text size="xs" className={classes.errorText}>{syntaxError}</Text>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed" className={classes.selectedText}>
|
||||
Selected: {selectedPageIds.length} pages ({displayDocument ? selectedPageIds.map(id => {
|
||||
const page = displayDocument.pages.find(p => p.id === id);
|
||||
return page?.pageNumber || 0;
|
||||
}).filter(n => n > 0).join(', ') : ''})
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectedPagesDisplay;
|
Loading…
x
Reference in New Issue
Block a user