diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 91c7090fe..a41910031 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1207,6 +1207,29 @@ "every3rd": "Every 3rd", "oddWithinExcluding": "Odd within 1-20 excluding 5-7", "combineSets": "Combine sets" + }, + "firstNPages": { + "title": "First N Pages", + "placeholder": "Number of pages" + }, + "lastNPages": { + "title": "Last N Pages", + "placeholder": "Number of pages" + }, + "everyNthPage": { + "title": "Every Nth Page", + "placeholder": "Step size" + }, + "range": { + "title": "Range", + "fromPlaceholder": "From", + "toPlaceholder": "To" + }, + "keywords": { + "title": "Keywords" + }, + "advanced": { + "title": "Advanced" } }, "compressPdfs": { diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 6e27db01a..052c6f658 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -852,6 +852,29 @@ "every3rd": "Every 3rd", "oddWithinExcluding": "Odd within 1-20 excluding 5-7", "combineSets": "Combine sets" + }, + "firstNPages": { + "title": "First N Pages", + "placeholder": "Number of pages" + }, + "lastNPages": { + "title": "Last N Pages", + "placeholder": "Number of pages" + }, + "everyNthPage": { + "title": "Every Nth Page", + "placeholder": "Step size" + }, + "range": { + "title": "Range", + "fromPlaceholder": "From", + "toPlaceholder": "To" + }, + "keywords": { + "title": "Keywords" + }, + "advanced": { + "title": "Advanced" } }, "compressPdfs": { diff --git a/frontend/src/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.tsx b/frontend/src/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.tsx index d58d28e4d..e61caab6a 100644 --- a/frontend/src/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.tsx +++ b/frontend/src/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { Flex } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; import classes from './BulkSelectionPanel.module.css'; import { appendExpression, @@ -8,6 +9,7 @@ import { lastNExpression, everyNthExpression, rangeExpression, + LogicalOperator, } from './BulkSelection'; import SelectPages from './SelectPages'; import OperatorsSection from './OperatorsSection'; @@ -27,6 +29,7 @@ const AdvancedSelectionPanel = ({ maxPages, advancedOpened, }: AdvancedSelectionPanelProps) => { + const { t } = useTranslation(); const [rangeEnd, setRangeEnd] = useState(''); const handleRangeEndChange = (val: string | number) => { @@ -47,11 +50,6 @@ const AdvancedSelectionPanel = ({ 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 nextInput = appendExpression(csvInput, expr); @@ -59,9 +57,13 @@ const AdvancedSelectionPanel = ({ onUpdatePagesFromCSV(nextInput); }; - const insertOperator = (op: 'and' | 'or' | 'not') => { + const insertOperator = (op: LogicalOperator) => { const next = insertOperatorSmart(csvInput, op); setCsvInput(next); + // Trigger visual selection update for 'even' and 'odd' operators + if (op === 'even' || op === 'odd') { + onUpdatePagesFromCSV(next); + } }; const handleFirstNApply = (value: number) => { @@ -95,36 +97,36 @@ const AdvancedSelectionPanel = ({ {/* Cards row */} diff --git a/frontend/src/components/pageEditor/bulkSelectionPanel/BulkSelection.ts b/frontend/src/components/pageEditor/bulkSelectionPanel/BulkSelection.ts index a2e3897e2..8b54b9d2b 100644 --- a/frontend/src/components/pageEditor/bulkSelectionPanel/BulkSelection.ts +++ b/frontend/src/components/pageEditor/bulkSelectionPanel/BulkSelection.ts @@ -1,6 +1,6 @@ // Pure helper utilities for the BulkSelectionPanel UI -export type LogicalOperator = 'and' | 'or' | 'not'; +export type LogicalOperator = 'and' | 'or' | 'not' | 'even' | 'odd'; // Returns a new CSV expression with expr appended. // If current ends with an operator token, expr is appended directly. @@ -9,7 +9,12 @@ export function appendExpression(currentInput: string, expr: string): string { const current = (currentInput || '').trim(); if (!current) return expr; const endsWithOperator = /(\b(and|not|or)\s*|[&|,!]\s*)$/i.test(current); - return endsWithOperator ? `${current}${expr}` : `${current} or ${expr}`; + // Add space if operator doesn't already have one + if (endsWithOperator) { + const needsSpace = !current.endsWith(' '); + return `${current}${needsSpace ? ' ' : ''}${expr}`; + } + return `${current} or ${expr}`; } // Smartly inserts/normalizes a logical operator at the end of the current input. @@ -18,6 +23,18 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator): const text = (currentInput || '').trim(); if (text.length === 0) return `${op} `; + // Handle 'even' and 'odd' as page selection expressions, not logical operators + if (op === 'even' || op === 'odd') { + // If current input ends with a logical operator, append the page selection with proper spacing + const endsWithOperator = /(\b(and|not|or)\s*|[&|,!]\s*)$/i.test(text); + if (endsWithOperator) { + // Add space if the operator doesn't already have one + const needsSpace = !text.endsWith(' '); + return `${text}${needsSpace ? ' ' : ''}${op} `; + } + return `${text} or ${op} `; + } + // Extract up to the last two operator tokens (words or symbols) from the end const tokens: string[] = []; let rest = text; @@ -39,19 +56,19 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator): // Normalize to allowed set const phrase = tokens.join(' '); - const allowed = new Set(['and', 'or', 'not', 'and or', 'and not']); + const allowed = new Set(['and', 'or', 'not', 'and not', 'or not']); // Helpers for transitions from a single trailing token const fromSingle = (t: string): string => { if (t === 'and') { if (click === 'and') return 'and'; - if (click === 'or') return 'and or'; + if (click === 'or') return 'or'; // 'and or' is invalid, so just use 'or' return 'and not'; } if (t === 'or') { if (click === 'and') return 'and'; if (click === 'or') return 'or'; - return 'not'; + return 'or not'; } // t === 'not' if (click === 'and') return 'and'; @@ -61,17 +78,19 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator): // From combined phrase const fromCombo = (p: string): string => { - if (p === 'and or') { - if (click === 'or') return 'and or'; - if (click === 'and') return 'and'; - return 'and not'; - } if (p === 'and not') { if (click === 'not') return 'and not'; if (click === 'and') return 'and'; - return 'and or'; + if (click === 'or') return 'or'; // 'and not or' is invalid, so just use 'or' + return 'and not'; } - // Invalid combos (e.g., 'not and', 'not or', 'or and') → collapse to clicked op + if (p === 'or not') { + if (click === 'not') return 'or not'; + if (click === 'or') return 'or'; + if (click === 'and') return 'and'; // 'or not and' is invalid, so just use 'and' + return 'or not'; + } + // Invalid combos (e.g., 'not and', 'not or', 'or and', 'and or') → collapse to clicked op return click; }; diff --git a/frontend/src/components/pageEditor/bulkSelectionPanel/OperatorsSection.tsx b/frontend/src/components/pageEditor/bulkSelectionPanel/OperatorsSection.tsx index dbe5bb97e..61cd508bb 100644 --- a/frontend/src/components/pageEditor/bulkSelectionPanel/OperatorsSection.tsx +++ b/frontend/src/components/pageEditor/bulkSelectionPanel/OperatorsSection.tsx @@ -1,15 +1,19 @@ -import { Button, Text, Group } from '@mantine/core'; +import { Button, Text, Group, Divider } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; import classes from './BulkSelectionPanel.module.css'; +import { LogicalOperator } from './BulkSelection'; interface OperatorsSectionProps { csvInput: string; - onInsertOperator: (op: 'and' | 'or' | 'not') => void; + onInsertOperator: (op: LogicalOperator) => void; } const OperatorsSection = ({ csvInput, onInsertOperator }: OperatorsSectionProps) => { + const { t } = useTranslation(); + return (
- Add Operators: + {t('bulkSelection.keywords.title', 'Keywords')}: + + + + +
); }; diff --git a/frontend/src/components/pageEditor/bulkSelectionPanel/PageSelectionInput.tsx b/frontend/src/components/pageEditor/bulkSelectionPanel/PageSelectionInput.tsx index b163fc1da..46652d85a 100644 --- a/frontend/src/components/pageEditor/bulkSelectionPanel/PageSelectionInput.tsx +++ b/frontend/src/components/pageEditor/bulkSelectionPanel/PageSelectionInput.tsx @@ -1,4 +1,5 @@ import { TextInput, Button, Text, Flex, Switch } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; import LocalIcon from '../../shared/LocalIcon'; import { Tooltip } from '../../shared/Tooltip'; import { usePageSelectionTips } from '../../tooltips/usePageSelectionTips'; @@ -21,6 +22,7 @@ const PageSelectionInput = ({ advancedOpened, onToggleAdvanced, }: PageSelectionInputProps) => { + const { t } = useTranslation(); const pageSelectionTips = usePageSelectionTips(); return ( @@ -43,12 +45,12 @@ const PageSelectionInput = ({ {typeof advancedOpened === 'boolean' && ( - Advanced + {t('bulkSelection.advanced.title', 'Advanced')} onToggleAdvanced?.(e.currentTarget.checked)} - title="Advanced" + title={t('bulkSelection.advanced.title', 'Advanced')} className={classes.advancedSwitch} />