diff --git a/frontend/src/components/tools/deletePages/DeletePagesSettings.tsx b/frontend/src/components/tools/deletePages/DeletePagesSettings.tsx new file mode 100644 index 000000000..fd549518c --- /dev/null +++ b/frontend/src/components/tools/deletePages/DeletePagesSettings.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Stack, Text, TextInput, Divider } from "@mantine/core"; +import { useTranslation } from 'react-i18next'; +import { DeletePagesParameters } from '../../../hooks/tools/deletePages/useDeletePagesParameters'; + +interface DeletePagesSettingsProps { + parameters: DeletePagesParameters; + onParameterChange: (parameter: K, value: DeletePagesParameters[K]) => void; + disabled?: boolean; +} + +const DeletePagesSettings: React.FC = ({ + parameters, + onParameterChange, + disabled = false +}) => { + const { t } = useTranslation(); + + return ( + + + + + {t('removePages.pageNumbers', 'Pages to Remove')} + + onParameterChange('pageNumbers', e.target.value.replace(/\s+/g, ''))} + placeholder={t('removePages.pageNumbersPlaceholder', 'e.g. 1,3,5-7')} + disabled={disabled} + description={t('removePages.pageNumbersHelp', 'Enter page numbers separated by commas, or ranges like 1-5. Example: 1,3,5-7')} + /> + + + ); +}; + +export default DeletePagesSettings; \ No newline at end of file diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 1a53cf651..9b3ff6a81 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -11,6 +11,7 @@ import RemovePassword from '../tools/RemovePassword'; import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy'; import AddWatermark from '../tools/AddWatermark'; import Repair from '../tools/Repair'; +import DeletePages from '../tools/DeletePages'; // Hook to get the translated tool registry export function useFlatToolRegistry(): ToolRegistry { @@ -275,11 +276,13 @@ export function useFlatToolRegistry(): ToolRegistry { "remove": { icon: delete, name: t("home.removePages.title", "Remove Pages"), - component: null, + component: DeletePages, view: "remove", description: t("home.removePages.desc", "Remove specific pages from a PDF document"), category: ToolCategory.STANDARD_TOOLS, - subcategory: SubcategoryId.REMOVAL + subcategory: SubcategoryId.REMOVAL, + maxFiles: -1, + endpoints: ["remove-pages"] }, "remove-blank-pages": { icon: scan_delete, diff --git a/frontend/src/hooks/tools/deletePages/useDeletePagesOperation.ts b/frontend/src/hooks/tools/deletePages/useDeletePagesOperation.ts new file mode 100644 index 000000000..4e464869e --- /dev/null +++ b/frontend/src/hooks/tools/deletePages/useDeletePagesOperation.ts @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { DeletePagesParameters } from './useDeletePagesParameters'; + +export const useDeletePagesOperation = () => { + const { t } = useTranslation(); + + const buildFormData = (parameters: DeletePagesParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + formData.append("pageNumbers", parameters.pageNumbers); + return formData; + }; + + return useToolOperation({ + operationType: 'deletePages', + endpoint: '/api/v1/general/remove-pages', + buildFormData, + filePrefix: t('removePages.filenamePrefix', 'pages_removed') + '_', + multiFileEndpoint: false, + getErrorMessage: createStandardErrorHandler(t('removePages.error.failed', 'An error occurred while removing pages.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/deletePages/useDeletePagesParameters.ts b/frontend/src/hooks/tools/deletePages/useDeletePagesParameters.ts new file mode 100644 index 000000000..f93bcd8e6 --- /dev/null +++ b/frontend/src/hooks/tools/deletePages/useDeletePagesParameters.ts @@ -0,0 +1,45 @@ +import { useState } from 'react'; + +export interface DeletePagesParameters { + pageNumbers: string; +} + +export interface DeletePagesParametersHook { + parameters: DeletePagesParameters; + updateParameter: (parameter: keyof DeletePagesParameters, value: string) => void; + resetParameters: () => void; + validateParameters: () => boolean; + getEndpointName: () => string; +} + +const initialParameters: DeletePagesParameters = { + pageNumbers: "1" +}; + +export const useDeletePagesParameters = (): DeletePagesParametersHook => { + const [parameters, setParameters] = useState(initialParameters); + + const updateParameter = (parameter: keyof DeletePagesParameters, value: string) => { + setParameters(prev => ({ ...prev, [parameter]: value })); + }; + + const resetParameters = () => { + setParameters(initialParameters); + }; + + const validateParameters = () => { + return parameters.pageNumbers.trim() !== ""; + }; + + const getEndpointName = () => { + return 'remove-pages'; + }; + + return { + parameters, + updateParameter, + resetParameters, + validateParameters, + getEndpointName, + }; +}; \ No newline at end of file diff --git a/frontend/src/tools/DeletePages.tsx b/frontend/src/tools/DeletePages.tsx new file mode 100644 index 000000000..6c60dbb11 --- /dev/null +++ b/frontend/src/tools/DeletePages.tsx @@ -0,0 +1,99 @@ +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useFileContext } from "../contexts/FileContext"; +import { useToolFileSelection } from "../contexts/FileSelectionContext"; + +import { createToolFlow } from "../components/tools/shared/createToolFlow"; + +import { useDeletePagesParameters } from "../hooks/tools/deletePages/useDeletePagesParameters"; +import { useDeletePagesOperation } from "../hooks/tools/deletePages/useDeletePagesOperation"; +import DeletePagesSettings from "../components/tools/deletePages/DeletePagesSettings"; +import { usePageSelectionTips } from "../components/tooltips/usePageSelectionTips"; +import { BaseToolProps } from "../types/tool"; + +const DeletePages = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const deletePagesParams = useDeletePagesParameters(); + const deletePagesOperation = useDeletePagesOperation(); + const pageSelectionTips = usePageSelectionTips(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(deletePagesParams.getEndpointName()); + + useEffect(() => { + deletePagesOperation.resetResults(); + onPreviewFile?.(null); + }, [deletePagesParams.parameters]); + + const handleDeletePages = async () => { + try { + await deletePagesOperation.executeOperation(deletePagesParams.parameters, selectedFiles); + if (deletePagesOperation.files && onComplete) { + onComplete(deletePagesOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t("removePages.error.failed", "Delete pages operation failed")); + } + } + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem("previousMode", "deletePages"); + setCurrentMode("viewer"); + }; + + const handleSettingsReset = () => { + deletePagesOperation.resetResults(); + onPreviewFile?.(null); + setCurrentMode("deletePages"); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = deletePagesOperation.files.length > 0 || deletePagesOperation.downloadUrl !== null; + + const settingsCollapsed = !hasFiles || hasResults; + + return createToolFlow({ + files: { + selectedFiles, + isCollapsed: hasFiles && !hasResults, + placeholder: t("removePages.files.placeholder", "Select a PDF file in the main view to get started"), + }, + steps: [ + { + title: t("removePages.settings.title", "Page Selection"), + isCollapsed: settingsCollapsed, + onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined, + tooltip: pageSelectionTips, + content: ( + + ), + } + ], + executeButton: { + text: t("removePages.submit", "Remove Pages"), + isVisible: !hasResults, + loadingText: t("loading"), + onClick: handleDeletePages, + disabled: !deletePagesParams.validateParameters() || !hasFiles || !endpointEnabled, + }, + review: { + isVisible: hasResults, + operation: deletePagesOperation, + title: t("removePages.results.title", "Page Removal Results"), + onFileClick: handleThumbnailClick, + }, + }); +}; + +export default DeletePages; \ No newline at end of file diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 1c513b523..037bd5bb1 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -19,7 +19,8 @@ export type ModeType = | 'changePermissions' | 'watermark' | 'removePassword' - | 'repair'; + | 'repair' + | 'deletePages'; export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; diff --git a/frontend/src/types/parameters.ts b/frontend/src/types/parameters.ts index 6f8856a8b..ca2369497 100644 --- a/frontend/src/types/parameters.ts +++ b/frontend/src/types/parameters.ts @@ -4,4 +4,4 @@ export interface BaseParameters { // Base interface that all tool parameters should extend // Provides a foundation for adding common properties across all tools // Examples of future additions: userId, sessionId, commonFlags, etc. -} \ No newline at end of file +}