mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Feature/v2/extract pages (#4828)
# Description of Changes - Add the extract pages tool - Componentize our bulk selection logic and warning messaages --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
parent
00fb40fb74
commit
138949caa7
@ -1418,6 +1418,26 @@
|
|||||||
},
|
},
|
||||||
"submit": "Remove Pages"
|
"submit": "Remove Pages"
|
||||||
},
|
},
|
||||||
|
"extractPages": {
|
||||||
|
"title": "Extract Pages",
|
||||||
|
"pageNumbers": {
|
||||||
|
"label": "Pages to Extract",
|
||||||
|
"placeholder": "e.g., 1,3,5-8 or odd & 1-10"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"description": "Extracts the selected pages into a new PDF, preserving order."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "Failed to extract pages"
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Pages Extracted"
|
||||||
|
},
|
||||||
|
"submit": "Extract Pages"
|
||||||
|
},
|
||||||
"pageSelection": {
|
"pageSelection": {
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"header": {
|
"header": {
|
||||||
@ -1494,6 +1514,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bulkSelection": {
|
"bulkSelection": {
|
||||||
|
"syntaxError": "There is a syntax issue. See Page Selection tips for help.",
|
||||||
"header": {
|
"header": {
|
||||||
"title": "Page Selection Guide"
|
"title": "Page Selection Guide"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -893,6 +893,26 @@
|
|||||||
},
|
},
|
||||||
"submit": "Remove Pages"
|
"submit": "Remove Pages"
|
||||||
},
|
},
|
||||||
|
"extractPages": {
|
||||||
|
"title": "Extract Pages",
|
||||||
|
"pageNumbers": {
|
||||||
|
"label": "Pages to Extract",
|
||||||
|
"placeholder": "e.g., 1,3,5-8 or odd & 1-10"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"description": "Extracts the selected pages into a new PDF, preserving order."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "Failed to extract pages"
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Pages Extracted"
|
||||||
|
},
|
||||||
|
"submit": "Extract Pages"
|
||||||
|
},
|
||||||
"pageSelection": {
|
"pageSelection": {
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"header": {
|
"header": {
|
||||||
@ -958,6 +978,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bulkSelection": {
|
"bulkSelection": {
|
||||||
|
"syntaxError": "There is a syntax issue. See Page Selection tips for help.",
|
||||||
"header": { "title": "Page Selection Guide" },
|
"header": { "title": "Page Selection Guide" },
|
||||||
"syntax": {
|
"syntax": {
|
||||||
"title": "Syntax Basics",
|
"title": "Syntax Basics",
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
|
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
|
||||||
import { parseSelectionWithDiagnostics } from '@app/utils/bulkselection/parseSelection';
|
|
||||||
import PageSelectionInput from '@app/components/pageEditor/bulkSelectionPanel/PageSelectionInput';
|
import PageSelectionInput from '@app/components/pageEditor/bulkSelectionPanel/PageSelectionInput';
|
||||||
import SelectedPagesDisplay from '@app/components/pageEditor/bulkSelectionPanel/SelectedPagesDisplay';
|
import SelectedPagesDisplay from '@app/components/pageEditor/bulkSelectionPanel/SelectedPagesDisplay';
|
||||||
|
import PageSelectionSyntaxHint from '@app/components/shared/PageSelectionSyntaxHint';
|
||||||
import AdvancedSelectionPanel from '@app/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel';
|
import AdvancedSelectionPanel from '@app/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel';
|
||||||
|
|
||||||
interface BulkSelectionPanelProps {
|
interface BulkSelectionPanelProps {
|
||||||
@ -20,26 +20,9 @@ const BulkSelectionPanel = ({
|
|||||||
displayDocument,
|
displayDocument,
|
||||||
onUpdatePagesFromCSV,
|
onUpdatePagesFromCSV,
|
||||||
}: BulkSelectionPanelProps) => {
|
}: BulkSelectionPanelProps) => {
|
||||||
const [syntaxError, setSyntaxError] = useState<string | null>(null);
|
|
||||||
const [advancedOpened, setAdvancedOpened] = useState<boolean>(false);
|
const [advancedOpened, setAdvancedOpened] = useState<boolean>(false);
|
||||||
const maxPages = displayDocument?.pages?.length ?? 0;
|
const maxPages = displayDocument?.pages?.length ?? 0;
|
||||||
|
|
||||||
|
|
||||||
// Validate input syntax and show lightweight feedback
|
|
||||||
useEffect(() => {
|
|
||||||
const text = (csvInput || '').trim();
|
|
||||||
if (!text) {
|
|
||||||
setSyntaxError(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { warning } = parseSelectionWithDiagnostics(text, maxPages);
|
|
||||||
setSyntaxError(warning ? 'There is a syntax issue. See Page Selection tips for help.' : null);
|
|
||||||
} catch {
|
|
||||||
setSyntaxError('There is a syntax issue. See Page Selection tips for help.');
|
|
||||||
}
|
|
||||||
}, [csvInput, maxPages]);
|
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
setCsvInput('');
|
setCsvInput('');
|
||||||
onUpdatePagesFromCSV('');
|
onUpdatePagesFromCSV('');
|
||||||
@ -56,10 +39,12 @@ const BulkSelectionPanel = ({
|
|||||||
onToggleAdvanced={setAdvancedOpened}
|
onToggleAdvanced={setAdvancedOpened}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageSelectionSyntaxHint input={csvInput} maxPages={maxPages} variant="panel" />
|
||||||
|
|
||||||
<SelectedPagesDisplay
|
<SelectedPagesDisplay
|
||||||
selectedPageIds={selectedPageIds}
|
selectedPageIds={selectedPageIds}
|
||||||
displayDocument={displayDocument}
|
displayDocument={displayDocument}
|
||||||
syntaxError={syntaxError}
|
syntaxError={null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdvancedSelectionPanel
|
<AdvancedSelectionPanel
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
everyNthExpression,
|
everyNthExpression,
|
||||||
rangeExpression,
|
rangeExpression,
|
||||||
LogicalOperator,
|
LogicalOperator,
|
||||||
} from '@app/components/pageEditor/bulkSelectionPanel/BulkSelection';
|
} from '@app/utils/bulkselection/selectionBuilders';
|
||||||
import SelectPages from '@app/components/pageEditor/bulkSelectionPanel/SelectPages';
|
import SelectPages from '@app/components/pageEditor/bulkSelectionPanel/SelectPages';
|
||||||
import OperatorsSection from '@app/components/pageEditor/bulkSelectionPanel/OperatorsSection';
|
import OperatorsSection from '@app/components/pageEditor/bulkSelectionPanel/OperatorsSection';
|
||||||
|
|
||||||
|
|||||||
@ -252,6 +252,25 @@
|
|||||||
color: var(--text-brand-accent);
|
color: var(--text-brand-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact error container for inline tool settings */
|
||||||
|
.errorCompact {
|
||||||
|
background-color: var(--bg-raised);
|
||||||
|
border: 0.0625rem solid var(--border-default);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Two-line clamp for compact error text */
|
||||||
|
.errorTextClamp {
|
||||||
|
color: var(--text-brand-accent);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dark-mode adjustments */
|
/* Dark-mode adjustments */
|
||||||
:global([data-mantine-color-scheme='dark']) .selectedList {
|
:global([data-mantine-color-scheme='dark']) .selectedList {
|
||||||
background-color: var(--bg-raised);
|
background-color: var(--bg-raised);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Button, Text, Group, Divider } from '@mantine/core';
|
import { Button, Text, Group, Divider } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
|
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
|
||||||
import { LogicalOperator } from '@app/components/pageEditor/bulkSelectionPanel/BulkSelection';
|
import { LogicalOperator } from '@app/utils/bulkselection/selectionBuilders';
|
||||||
|
|
||||||
interface OperatorsSectionProps {
|
interface OperatorsSectionProps {
|
||||||
csvInput: string;
|
csvInput: string;
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Text } from '@mantine/core';
|
||||||
|
import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css';
|
||||||
|
import { parseSelectionWithDiagnostics } from '@app/utils/bulkselection/parseSelection';
|
||||||
|
|
||||||
|
interface PageSelectionSyntaxHintProps {
|
||||||
|
input: string;
|
||||||
|
/** Optional known page count; if not provided, a large max is used for syntax-only checks */
|
||||||
|
maxPages?: number;
|
||||||
|
/** panel = full bulk panel style, compact = inline tool style */
|
||||||
|
variant?: 'panel' | 'compact';
|
||||||
|
}
|
||||||
|
|
||||||
|
const FALLBACK_MAX_PAGES = 100000; // large upper bound for syntax validation without a document
|
||||||
|
|
||||||
|
const PageSelectionSyntaxHint = ({ input, maxPages, variant = 'panel' }: PageSelectionSyntaxHintProps) => {
|
||||||
|
const [syntaxError, setSyntaxError] = useState<string | null>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const text = (input || '').trim();
|
||||||
|
if (!text) {
|
||||||
|
setSyntaxError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { warning } = parseSelectionWithDiagnostics(text, maxPages && maxPages > 0 ? maxPages : FALLBACK_MAX_PAGES);
|
||||||
|
setSyntaxError(warning ? t('bulkSelection.syntaxError', 'There is a syntax issue. See Page Selection tips for help.') : null);
|
||||||
|
} catch {
|
||||||
|
setSyntaxError(t('bulkSelection.syntaxError', 'There is a syntax issue. See Page Selection tips for help.'));
|
||||||
|
}
|
||||||
|
}, [input, maxPages]);
|
||||||
|
|
||||||
|
if (!syntaxError) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={variant === 'panel' ? classes.selectedList : classes.errorCompact}>
|
||||||
|
<Text size="xs" className={variant === 'panel' ? classes.errorText : classes.errorTextClamp}>{syntaxError}</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageSelectionSyntaxHint;
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { Stack, TextInput } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ExtractPagesParameters } from "@app/hooks/tools/extractPages/useExtractPagesParameters";
|
||||||
|
import PageSelectionSyntaxHint from "@app/components/shared/PageSelectionSyntaxHint";
|
||||||
|
|
||||||
|
interface ExtractPagesSettingsProps {
|
||||||
|
parameters: ExtractPagesParameters;
|
||||||
|
onParameterChange: <K extends keyof ExtractPagesParameters>(key: K, value: ExtractPagesParameters[K]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtractPagesSettings = ({ parameters, onParameterChange, disabled = false }: ExtractPagesSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
onParameterChange('pageNumbers', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput
|
||||||
|
label={t('extractPages.pageNumbers.label', 'Pages to Extract')}
|
||||||
|
value={parameters.pageNumbers || ''}
|
||||||
|
onChange={(event) => handleChange(event.currentTarget.value)}
|
||||||
|
placeholder={t('extractPages.pageNumbers.placeholder', 'e.g., 1,3,5-8 or odd & 1-10')}
|
||||||
|
disabled={disabled}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<PageSelectionSyntaxHint input={parameters.pageNumbers || ''} variant="compact" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExtractPagesSettings;
|
||||||
|
|
||||||
|
|
||||||
22
frontend/src/core/components/tooltips/useExtractPagesTips.ts
Normal file
22
frontend/src/core/components/tooltips/useExtractPagesTips.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent } from '@app/types/tips';
|
||||||
|
import { usePageSelectionTips } from '@app/components/tooltips/usePageSelectionTips';
|
||||||
|
|
||||||
|
export const useExtractPagesTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const base = usePageSelectionTips();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: base.header,
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
description: t('extractPages.tooltip.description', 'Extracts the selected pages into a new PDF, preserving order.')
|
||||||
|
},
|
||||||
|
...(base.tips || [])
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useExtractPagesTips;
|
||||||
|
|
||||||
|
|
||||||
@ -79,6 +79,7 @@ import { overlayPdfsOperationConfig } from "@app/hooks/tools/overlayPdfs/useOver
|
|||||||
import { adjustPageScaleOperationConfig } from "@app/hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
import { adjustPageScaleOperationConfig } from "@app/hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
||||||
import { scannerImageSplitOperationConfig } from "@app/hooks/tools/scannerImageSplit/useScannerImageSplitOperation";
|
import { scannerImageSplitOperationConfig } from "@app/hooks/tools/scannerImageSplit/useScannerImageSplitOperation";
|
||||||
import { addPageNumbersOperationConfig } from "@app/components/tools/addPageNumbers/useAddPageNumbersOperation";
|
import { addPageNumbersOperationConfig } from "@app/components/tools/addPageNumbers/useAddPageNumbersOperation";
|
||||||
|
import { extractPagesOperationConfig } from "@app/hooks/tools/extractPages/useExtractPagesOperation";
|
||||||
import CompressSettings from "@app/components/tools/compress/CompressSettings";
|
import CompressSettings from "@app/components/tools/compress/CompressSettings";
|
||||||
import AddPasswordSettings from "@app/components/tools/addPassword/AddPasswordSettings";
|
import AddPasswordSettings from "@app/components/tools/addPassword/AddPasswordSettings";
|
||||||
import RemovePasswordSettings from "@app/components/tools/removePassword/RemovePasswordSettings";
|
import RemovePasswordSettings from "@app/components/tools/removePassword/RemovePasswordSettings";
|
||||||
@ -105,7 +106,9 @@ import AddPageNumbers from "@app/tools/AddPageNumbers";
|
|||||||
import RemoveAnnotations from "@app/tools/RemoveAnnotations";
|
import RemoveAnnotations from "@app/tools/RemoveAnnotations";
|
||||||
import PageLayoutSettings from "@app/components/tools/pageLayout/PageLayoutSettings";
|
import PageLayoutSettings from "@app/components/tools/pageLayout/PageLayoutSettings";
|
||||||
import ExtractImages from "@app/tools/ExtractImages";
|
import ExtractImages from "@app/tools/ExtractImages";
|
||||||
|
import ExtractPages from "@app/tools/ExtractPages";
|
||||||
import ExtractImagesSettings from "@app/components/tools/extractImages/ExtractImagesSettings";
|
import ExtractImagesSettings from "@app/components/tools/extractImages/ExtractImagesSettings";
|
||||||
|
import ExtractPagesSettings from "@app/components/tools/extractPages/ExtractPagesSettings";
|
||||||
import ReplaceColorSettings from "@app/components/tools/replaceColor/ReplaceColorSettings";
|
import ReplaceColorSettings from "@app/components/tools/replaceColor/ReplaceColorSettings";
|
||||||
import AddStampAutomationSettings from "@app/components/tools/addStamp/AddStampAutomationSettings";
|
import AddStampAutomationSettings from "@app/components/tools/addStamp/AddStampAutomationSettings";
|
||||||
import CertSignAutomationSettings from "@app/components/tools/certSign/CertSignAutomationSettings";
|
import CertSignAutomationSettings from "@app/components/tools/certSign/CertSignAutomationSettings";
|
||||||
@ -474,12 +477,14 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
|
|||||||
extractPages: {
|
extractPages: {
|
||||||
icon: <LocalIcon icon="upload-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="upload-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.extractPages.title", "Extract Pages"),
|
name: t("home.extractPages.title", "Extract Pages"),
|
||||||
component: null,
|
component: ExtractPages,
|
||||||
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
|
description: t("home.extractPages.desc", "Extract specific pages from a PDF document"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.EXTRACTION,
|
subcategoryId: SubcategoryId.EXTRACTION,
|
||||||
synonyms: getSynonyms(t, "extractPages"),
|
synonyms: getSynonyms(t, "extractPages"),
|
||||||
automationSettings: null,
|
automationSettings: ExtractPagesSettings,
|
||||||
|
operationConfig: extractPagesOperationConfig,
|
||||||
|
endpoints: ["rearrange-pages"],
|
||||||
},
|
},
|
||||||
extractImages: {
|
extractImages: {
|
||||||
icon: <LocalIcon icon="photo-library-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="photo-library-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
import apiClient from '@app/services/apiClient';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ToolType, useToolOperation } from '@app/hooks/tools/shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '@app/utils/toolErrorHandler';
|
||||||
|
import { ExtractPagesParameters, defaultParameters } from '@app/hooks/tools/extractPages/useExtractPagesParameters';
|
||||||
|
import { pdfWorkerManager } from '@app/services/pdfWorkerManager';
|
||||||
|
import { parseSelection } from '@app/utils/bulkselection/parseSelection';
|
||||||
|
|
||||||
|
// Convert advanced page selection expression into CSV of explicit one-based page numbers
|
||||||
|
async function resolveSelectionToCsv(expression: string, file: File): Promise<string> {
|
||||||
|
// Load PDF to determine max pages
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const pdf = await pdfWorkerManager.createDocument(arrayBuffer, { disableAutoFetch: true, disableStream: true });
|
||||||
|
try {
|
||||||
|
const maxPages = pdf.numPages;
|
||||||
|
const pages = parseSelection(expression || '', maxPages);
|
||||||
|
return pages.join(',');
|
||||||
|
} finally {
|
||||||
|
pdfWorkerManager.destroyDocument(pdf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractPagesOperationConfig = {
|
||||||
|
toolType: ToolType.custom,
|
||||||
|
operationType: 'extractPages',
|
||||||
|
customProcessor: async (parameters: ExtractPagesParameters, files: File[]): Promise<File[]> => {
|
||||||
|
const outputs: File[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Resolve selection into CSV acceptable by backend
|
||||||
|
const csv = await resolveSelectionToCsv(parameters.pageNumbers, file);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('fileInput', file);
|
||||||
|
formData.append('pageNumbers', csv);
|
||||||
|
|
||||||
|
const response = await apiClient.post('/api/v1/general/rearrange-pages', formData, { responseType: 'blob' });
|
||||||
|
|
||||||
|
// Name output file with suffix
|
||||||
|
const base = (file.name || 'document.pdf').replace(/\.[^.]+$/, '');
|
||||||
|
const outName = `${base}_extracted_pages.pdf`;
|
||||||
|
const outFile = new File([response.data], outName, { type: 'application/pdf' });
|
||||||
|
outputs.push(outFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs;
|
||||||
|
},
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const useExtractPagesOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return useToolOperation<ExtractPagesParameters>({
|
||||||
|
...extractPagesOperationConfig,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('extractPages.error.failed', 'Failed to extract pages'))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { BaseParameters } from '@app/types/parameters';
|
||||||
|
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface ExtractPagesParameters extends BaseParameters {
|
||||||
|
pageNumbers: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: ExtractPagesParameters = {
|
||||||
|
pageNumbers: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtractPagesParametersHook = BaseParametersHook<ExtractPagesParameters>;
|
||||||
|
|
||||||
|
export const useExtractPagesParameters = (): ExtractPagesParametersHook => {
|
||||||
|
return useBaseParameters({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'rearrange-pages',
|
||||||
|
validateFn: (p) => (p.pageNumbers || '').trim().length > 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
62
frontend/src/core/tools/ExtractPages.tsx
Normal file
62
frontend/src/core/tools/ExtractPages.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
|
||||||
|
import { BaseToolProps, ToolComponent } from "@app/types/tool";
|
||||||
|
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
|
||||||
|
import { useExtractPagesParameters } from "@app/hooks/tools/extractPages/useExtractPagesParameters";
|
||||||
|
import { useExtractPagesOperation } from "@app/hooks/tools/extractPages/useExtractPagesOperation";
|
||||||
|
import ExtractPagesSettings from "@app/components/tools/extractPages/ExtractPagesSettings";
|
||||||
|
import useExtractPagesTips from "@app/components/tooltips/useExtractPagesTips";
|
||||||
|
|
||||||
|
const ExtractPages = (props: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tooltipContent = useExtractPagesTips();
|
||||||
|
|
||||||
|
const base = useBaseTool(
|
||||||
|
'extract-pages',
|
||||||
|
useExtractPagesParameters,
|
||||||
|
useExtractPagesOperation,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
const settingsContent = (
|
||||||
|
<ExtractPagesSettings
|
||||||
|
parameters={base.params.parameters}
|
||||||
|
onParameterChange={base.params.updateParameter}
|
||||||
|
disabled={base.endpointLoading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles: base.selectedFiles,
|
||||||
|
isCollapsed: base.hasResults,
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: t("extractPages.settings.title", "Settings"),
|
||||||
|
isCollapsed: base.settingsCollapsed,
|
||||||
|
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||||
|
content: settingsContent,
|
||||||
|
tooltip: tooltipContent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
executeButton: {
|
||||||
|
text: t("extractPages.submit", "Extract Pages"),
|
||||||
|
loadingText: t("loading"),
|
||||||
|
onClick: base.handleExecute,
|
||||||
|
isVisible: !base.hasResults,
|
||||||
|
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: base.hasResults,
|
||||||
|
operation: base.operation,
|
||||||
|
title: t("extractPages.results.title", "Pages Extracted"),
|
||||||
|
onFileClick: base.handleThumbnailClick,
|
||||||
|
onUndo: base.handleUndo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExtractPages as ToolComponent;
|
||||||
|
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
// Pure helper utilities for the BulkSelectionPanel UI
|
// Pure helper utilities for building and manipulating bulk page selection expressions
|
||||||
|
|
||||||
export type LogicalOperator = 'and' | 'or' | 'not' | 'even' | 'odd';
|
export type LogicalOperator = 'and' | 'or' | 'not' | 'even' | 'odd';
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator):
|
|||||||
}
|
}
|
||||||
return `${text} or ${op} `;
|
return `${text} or ${op} `;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text.length === 0) return `${op} `;
|
if (text.length === 0) return `${op} `;
|
||||||
|
|
||||||
// Extract up to the last two operator tokens (words or symbols) from the end
|
// Extract up to the last two operator tokens (words or symbols) from the end
|
||||||
Loading…
Reference in New Issue
Block a user