From 442b373ff4c81d5b694c55b80b251f34b606357d Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 28 Aug 2025 10:59:38 +0100 Subject: [PATCH] V2 reduce tool boilerplate (#4313) # Description of Changes Reduce boilerplate in tool frontends by creating a base frontend hook for the simple tools to use. I've done all the simple tools here. It'd be nice to add in some of the more complex tools as well in the future if we can figure out how. --- .../src/hooks/tools/shared/useBaseTool.ts | 118 ++++++++++++++++++ frontend/src/tools/ChangePermissions.tsx | 83 ++++-------- frontend/src/tools/Compress.tsx | 82 ++++-------- frontend/src/tools/RemoveCertificateSign.tsx | 73 +++-------- frontend/src/tools/RemovePassword.tsx | 82 ++++-------- frontend/src/tools/Repair.tsx | 71 +++-------- frontend/src/tools/Sanitize.tsx | 79 ++++-------- frontend/src/tools/SingleLargePage.tsx | 73 +++-------- frontend/src/tools/Split.tsx | 91 ++++---------- frontend/src/tools/UnlockPdfForms.tsx | 73 +++-------- 10 files changed, 291 insertions(+), 534 deletions(-) create mode 100644 frontend/src/hooks/tools/shared/useBaseTool.ts diff --git a/frontend/src/hooks/tools/shared/useBaseTool.ts b/frontend/src/hooks/tools/shared/useBaseTool.ts new file mode 100644 index 000000000..c18444546 --- /dev/null +++ b/frontend/src/hooks/tools/shared/useBaseTool.ts @@ -0,0 +1,118 @@ +import { useEffect, useCallback } from 'react'; +import { useFileSelection } from '../../../contexts/FileContext'; +import { useEndpointEnabled } from '../../useEndpointConfig'; +import { BaseToolProps } from '../../../types/tool'; +import { ToolOperationHook } from './useToolOperation'; +import { BaseParametersHook } from './useBaseParameters'; + +interface BaseToolReturn { + // File management + selectedFiles: File[]; + + // Tool-specific hooks + params: BaseParametersHook; + operation: ToolOperationHook; + + // Endpoint validation + endpointEnabled: boolean | null; + endpointLoading: boolean; + + // Standard handlers + handleExecute: () => Promise; + handleThumbnailClick: (file: File) => void; + handleSettingsReset: () => void; + + // Standard computed state + hasFiles: boolean; + hasResults: boolean; + settingsCollapsed: boolean; +} + +/** + * Base tool hook for tool components. Manages standard behaviour for tools. + */ +export function useBaseTool( + toolName: string, + useParams: () => BaseParametersHook, + useOperation: () => ToolOperationHook, + props: BaseToolProps, +): BaseToolReturn { + const { onPreviewFile, onComplete, onError } = props; + + // File selection + const { selectedFiles } = useFileSelection(); + + // Tool-specific hooks + const params = useParams(); + const operation = useOperation(); + + // Endpoint validation using parameters hook + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(params.getEndpointName()); + + // Reset results when parameters change + useEffect(() => { + operation.resetResults(); + onPreviewFile?.(null); + }, [params.parameters]); + + // Reset results when selected files change + useEffect(() => { + if (selectedFiles.length > 0) { + operation.resetResults(); + onPreviewFile?.(null); + } + }, [selectedFiles.length]); + + // Standard handlers + const handleExecute = useCallback(async () => { + try { + await operation.executeOperation(params.parameters, selectedFiles); + if (operation.files && onComplete) { + onComplete(operation.files); + } + } catch (error) { + if (onError) { + const message = error instanceof Error ? error.message : `${toolName} operation failed`; + onError(message); + } + } + }, [operation, params.parameters, selectedFiles, onComplete, onError, toolName]); + + const handleThumbnailClick = useCallback((file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem('previousMode', toolName); + }, [onPreviewFile, toolName]); + + const handleSettingsReset = useCallback(() => { + operation.resetResults(); + onPreviewFile?.(null); + }, [operation, onPreviewFile]); + + // Standard computed state + const hasFiles = selectedFiles.length > 0; + const hasResults = operation.files.length > 0 || operation.downloadUrl !== null; + const settingsCollapsed = !hasFiles || hasResults; + + return { + // File management + selectedFiles, + + // Tool-specific hooks + params, + operation, + + // Endpoint validation + endpointEnabled, + endpointLoading, + + // Handlers + handleExecute, + handleThumbnailClick, + handleSettingsReset, + + // State + hasFiles, + hasResults, + settingsCollapsed + }; +} diff --git a/frontend/src/tools/ChangePermissions.tsx b/frontend/src/tools/ChangePermissions.tsx index e9a4eaa80..5f6582a76 100644 --- a/frontend/src/tools/ChangePermissions.tsx +++ b/frontend/src/tools/ChangePermissions.tsx @@ -1,96 +1,55 @@ -import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; -import { useFileSelection } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; - import { createToolFlow } from "../components/tools/shared/createToolFlow"; - import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings"; - import { useChangePermissionsParameters } from "../hooks/tools/changePermissions/useChangePermissionsParameters"; import { useChangePermissionsOperation } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; import { useChangePermissionsTips } from "../components/tooltips/useChangePermissionsTips"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "../types/tool"; -const ChangePermissions = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { +const ChangePermissions = (props: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); - const { selectedFiles } = useFileSelection(); - - const changePermissionsParams = useChangePermissionsParameters(); - const changePermissionsOperation = useChangePermissionsOperation(); const changePermissionsTips = useChangePermissionsTips(); - // Endpoint validation - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(changePermissionsParams.getEndpointName()); - - useEffect(() => { - changePermissionsOperation.resetResults(); - onPreviewFile?.(null); - }, [changePermissionsParams.parameters]); - - const handleChangePermissions = async () => { - try { - await changePermissionsOperation.executeOperation(changePermissionsParams.parameters, selectedFiles); - if (changePermissionsOperation.files && onComplete) { - onComplete(changePermissionsOperation.files); - } - } catch (error) { - if (onError) { - onError( - error instanceof Error ? error.message : t("changePermissions.error.failed", "Change permissions operation failed") - ); - } - } - }; - - const handleThumbnailClick = (file: File) => { - onPreviewFile?.(file); - sessionStorage.setItem("previousMode", "changePermissions"); - }; - - const handleSettingsReset = () => { - changePermissionsOperation.resetResults(); - onPreviewFile?.(null); - }; - - const hasFiles = selectedFiles.length > 0; - const hasResults = changePermissionsOperation.files.length > 0 || changePermissionsOperation.downloadUrl !== null; - const settingsCollapsed = !hasFiles || hasResults; + const base = useBaseTool( + 'changePermissions', + useChangePermissionsParameters, + useChangePermissionsOperation, + props + ); return createToolFlow({ files: { - selectedFiles, - isCollapsed: hasResults, + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, }, steps: [ { title: t("changePermissions.title", "Document Permissions"), - isCollapsed: settingsCollapsed, - onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined, + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined, tooltip: changePermissionsTips, content: ( ), }, ], executeButton: { text: t("changePermissions.submit", "Change Permissions"), - isVisible: !hasResults, + isVisible: !base.hasResults, loadingText: t("loading"), - onClick: handleChangePermissions, - disabled: !changePermissionsParams.validateParameters() || !hasFiles || !endpointEnabled, + onClick: base.handleExecute, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, }, review: { - isVisible: hasResults, - operation: changePermissionsOperation, + isVisible: base.hasResults, + operation: base.operation, title: t("changePermissions.results.title", "Modified PDFs"), - onFileClick: handleThumbnailClick, + onFileClick: base.handleThumbnailClick, }, }); }; diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx index 91dd89e86..bb57e70a4 100644 --- a/frontend/src/tools/Compress.tsx +++ b/frontend/src/tools/Compress.tsx @@ -1,95 +1,55 @@ -import React, { use, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; -import { useFileSelection } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; - import { createToolFlow } from "../components/tools/shared/createToolFlow"; - import CompressSettings from "../components/tools/compress/CompressSettings"; - import { useCompressParameters } from "../hooks/tools/compress/useCompressParameters"; import { useCompressOperation } from "../hooks/tools/compress/useCompressOperation"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "../types/tool"; import { useCompressTips } from "../components/tooltips/useCompressTips"; -const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { +const Compress = (props: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); - const { selectedFiles } = useFileSelection(); - - const compressParams = useCompressParameters(); - const compressOperation = useCompressOperation(); const compressTips = useCompressTips(); - // Endpoint validation - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf"); - - useEffect(() => { - compressOperation.resetResults(); - onPreviewFile?.(null); - }, [compressParams.parameters]); - - const handleCompress = async () => { - try { - await compressOperation.executeOperation(compressParams.parameters, selectedFiles); - if (compressOperation.files && onComplete) { - onComplete(compressOperation.files); - } - } catch (error) { - if (onError) { - onError(error instanceof Error ? error.message : "Compress operation failed"); - } - } - }; - - const handleThumbnailClick = (file: File) => { - onPreviewFile?.(file); - sessionStorage.setItem("previousMode", "compress"); - }; - - const handleSettingsReset = () => { - compressOperation.resetResults(); - onPreviewFile?.(null); }; - - - - const hasFiles = selectedFiles.length > 0; - const hasResults = compressOperation.files.length > 0 || compressOperation.downloadUrl !== null; - const settingsCollapsed = !hasFiles || hasResults; + const base = useBaseTool( + 'compress', + useCompressParameters, + useCompressOperation, + props + ); return createToolFlow({ files: { - selectedFiles, - isCollapsed: hasResults, + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, }, steps: [ { title: "Settings", - isCollapsed: settingsCollapsed, - onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined, + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined, tooltip: compressTips, content: ( ), }, ], executeButton: { text: t("compress.submit", "Compress"), - isVisible: !hasResults, + isVisible: !base.hasResults, loadingText: t("loading"), - onClick: handleCompress, - disabled: !compressParams.validateParameters() || !hasFiles || !endpointEnabled, + onClick: base.handleExecute, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, }, review: { - isVisible: hasResults, - operation: compressOperation, + isVisible: base.hasResults, + operation: base.operation, title: t("compress.title", "Compression Results"), - onFileClick: handleThumbnailClick, + onFileClick: base.handleThumbnailClick, }, }); }; diff --git a/frontend/src/tools/RemoveCertificateSign.tsx b/frontend/src/tools/RemoveCertificateSign.tsx index c9a3f4b82..8583b34c6 100644 --- a/frontend/src/tools/RemoveCertificateSign.tsx +++ b/frontend/src/tools/RemoveCertificateSign.tsx @@ -1,78 +1,39 @@ -import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; -import { useFileContext } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; -import { useFileSelection } from "../contexts/file/fileHooks"; - import { createToolFlow } from "../components/tools/shared/createToolFlow"; - import { useRemoveCertificateSignParameters } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters"; import { useRemoveCertificateSignOperation } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "../types/tool"; -const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { +const RemoveCertificateSign = (props: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); - const { selectedFiles } = useFileSelection(); - const removeCertificateSignParams = useRemoveCertificateSignParameters(); - const removeCertificateSignOperation = useRemoveCertificateSignOperation(); - - // Endpoint validation - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(removeCertificateSignParams.getEndpointName()); - - useEffect(() => { - removeCertificateSignOperation.resetResults(); - onPreviewFile?.(null); - }, [removeCertificateSignParams.parameters]); - - const handleRemoveSignature = async () => { - try { - await removeCertificateSignOperation.executeOperation(removeCertificateSignParams.parameters, selectedFiles); - if (removeCertificateSignOperation.files && onComplete) { - onComplete(removeCertificateSignOperation.files); - } - } catch (error) { - if (onError) { - onError(error instanceof Error ? error.message : t("removeCertSign.error.failed", "Remove certificate signature operation failed")); - } - } - }; - - const handleThumbnailClick = (file: File) => { - onPreviewFile?.(file); - sessionStorage.setItem("previousMode", "removeCertificateSign"); - actions.setMode("viewer"); - }; - - const handleSettingsReset = () => { - removeCertificateSignOperation.resetResults(); - onPreviewFile?.(null); - }; - - const hasFiles = selectedFiles.length > 0; - const hasResults = removeCertificateSignOperation.files.length > 0 || removeCertificateSignOperation.downloadUrl !== null; + const base = useBaseTool( + 'removeCertificateSign', + useRemoveCertificateSignParameters, + useRemoveCertificateSignOperation, + props + ); return createToolFlow({ files: { - selectedFiles, - isCollapsed: hasFiles || hasResults, + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, placeholder: t("removeCertSign.files.placeholder", "Select a PDF file in the main view to get started"), }, steps: [], executeButton: { text: t("removeCertSign.submit", "Remove Signature"), - isVisible: !hasResults, + isVisible: !base.hasResults, loadingText: t("loading"), - onClick: handleRemoveSignature, - disabled: !removeCertificateSignParams.validateParameters() || !hasFiles || !endpointEnabled, + onClick: base.handleExecute, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, }, review: { - isVisible: hasResults, - operation: removeCertificateSignOperation, + isVisible: base.hasResults, + operation: base.operation, title: t("removeCertSign.results.title", "Certificate Removal Results"), - onFileClick: handleThumbnailClick, + onFileClick: base.handleThumbnailClick, }, }); }; @@ -80,4 +41,4 @@ const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolP // Static method to get the operation hook for automation RemoveCertificateSign.tool = () => useRemoveCertificateSignOperation; -export default RemoveCertificateSign as ToolComponent; \ No newline at end of file +export default RemoveCertificateSign as ToolComponent; diff --git a/frontend/src/tools/RemovePassword.tsx b/frontend/src/tools/RemovePassword.tsx index e84cf6e6c..9e8b1f5f4 100644 --- a/frontend/src/tools/RemovePassword.tsx +++ b/frontend/src/tools/RemovePassword.tsx @@ -1,95 +1,55 @@ -import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; -import { useFileSelection } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; - import { createToolFlow } from "../components/tools/shared/createToolFlow"; - import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings"; - import { useRemovePasswordParameters } from "../hooks/tools/removePassword/useRemovePasswordParameters"; import { useRemovePasswordOperation } from "../hooks/tools/removePassword/useRemovePasswordOperation"; import { useRemovePasswordTips } from "../components/tooltips/useRemovePasswordTips"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "../types/tool"; -const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { +const RemovePassword = (props: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); - const { selectedFiles } = useFileSelection(); - - const removePasswordParams = useRemovePasswordParameters(); - const removePasswordOperation = useRemovePasswordOperation(); const removePasswordTips = useRemovePasswordTips(); - // Endpoint validation - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(removePasswordParams.getEndpointName()); - - - useEffect(() => { - removePasswordOperation.resetResults(); - onPreviewFile?.(null); - }, [removePasswordParams.parameters]); - - const handleRemovePassword = async () => { - try { - await removePasswordOperation.executeOperation(removePasswordParams.parameters, selectedFiles); - if (removePasswordOperation.files && onComplete) { - onComplete(removePasswordOperation.files); - } - } catch (error) { - if (onError) { - onError(error instanceof Error ? error.message : t("removePassword.error.failed", "Remove password operation failed")); - } - } - }; - - const handleThumbnailClick = (file: File) => { - onPreviewFile?.(file); - sessionStorage.setItem("previousMode", "removePassword"); - }; - - const handleSettingsReset = () => { - removePasswordOperation.resetResults(); - onPreviewFile?.(null); - }; - - const hasFiles = selectedFiles.length > 0; - const hasResults = removePasswordOperation.files.length > 0 || removePasswordOperation.downloadUrl !== null; - const passwordCollapsed = !hasFiles || hasResults; + const base = useBaseTool( + 'removePassword', + useRemovePasswordParameters, + useRemovePasswordOperation, + props + ); return createToolFlow({ files: { - selectedFiles, - isCollapsed: hasResults, + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, }, steps: [ { title: t("removePassword.password.stepTitle", "Remove Password"), - isCollapsed: passwordCollapsed, - onCollapsedClick: hasResults ? handleSettingsReset : undefined, + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined, tooltip: removePasswordTips, content: ( ), }, ], executeButton: { text: t("removePassword.submit", "Remove Password"), - isVisible: !hasResults, + isVisible: !base.hasResults, loadingText: t("loading"), - onClick: handleRemovePassword, - disabled: !removePasswordParams.validateParameters() || !hasFiles || !endpointEnabled, + onClick: base.handleExecute, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, }, review: { - isVisible: hasResults, - operation: removePasswordOperation, + isVisible: base.hasResults, + operation: base.operation, title: t("removePassword.results.title", "Decrypted PDFs"), - onFileClick: handleThumbnailClick, + onFileClick: base.handleThumbnailClick, }, }); }; diff --git a/frontend/src/tools/Repair.tsx b/frontend/src/tools/Repair.tsx index f5f09017a..61d3986b6 100644 --- a/frontend/src/tools/Repair.tsx +++ b/frontend/src/tools/Repair.tsx @@ -1,78 +1,39 @@ -import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; -import { useFileContext } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; -import { useFileSelection } from "../contexts/file/fileHooks"; - import { createToolFlow } from "../components/tools/shared/createToolFlow"; - import { useRepairParameters } from "../hooks/tools/repair/useRepairParameters"; import { useRepairOperation } from "../hooks/tools/repair/useRepairOperation"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "../types/tool"; -const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { +const Repair = (props: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); - const { selectedFiles } = useFileSelection(); - const repairParams = useRepairParameters(); - const repairOperation = useRepairOperation(); - - // Endpoint validation - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(repairParams.getEndpointName()); - - useEffect(() => { - repairOperation.resetResults(); - onPreviewFile?.(null); - }, [repairParams.parameters]); - - const handleRepair = async () => { - try { - await repairOperation.executeOperation(repairParams.parameters, selectedFiles); - if (repairOperation.files && onComplete) { - onComplete(repairOperation.files); - } - } catch (error) { - if (onError) { - onError(error instanceof Error ? error.message : t("repair.error.failed", "Repair operation failed")); - } - } - }; - - const handleThumbnailClick = (file: File) => { - onPreviewFile?.(file); - sessionStorage.setItem("previousMode", "repair"); - actions.setMode("viewer"); - }; - - const handleSettingsReset = () => { - repairOperation.resetResults(); - onPreviewFile?.(null); - }; - - const hasFiles = selectedFiles.length > 0; - const hasResults = repairOperation.files.length > 0 || repairOperation.downloadUrl !== null; + const base = useBaseTool( + 'repair', + useRepairParameters, + useRepairOperation, + props + ); return createToolFlow({ files: { - selectedFiles, - isCollapsed: hasResults, + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, placeholder: t("repair.files.placeholder", "Select a PDF file in the main view to get started"), }, steps: [], executeButton: { text: t("repair.submit", "Repair PDF"), - isVisible: !hasResults, + isVisible: !base.hasResults, loadingText: t("loading"), - onClick: handleRepair, - disabled: !repairParams.validateParameters() || !hasFiles || !endpointEnabled, + onClick: base.handleExecute, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, }, review: { - isVisible: hasResults, - operation: repairOperation, + isVisible: base.hasResults, + operation: base.operation, title: t("repair.results.title", "Repair Results"), - onFileClick: handleThumbnailClick, + onFileClick: base.handleThumbnailClick, }, }); }; diff --git a/frontend/src/tools/Sanitize.tsx b/frontend/src/tools/Sanitize.tsx index 274331fd6..d6f9bbe5d 100644 --- a/frontend/src/tools/Sanitize.tsx +++ b/frontend/src/tools/Sanitize.tsx @@ -1,90 +1,53 @@ -import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; -import { useFileSelection } from "../contexts/FileContext"; - import { createToolFlow } from "../components/tools/shared/createToolFlow"; import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings"; - import { useSanitizeParameters } from "../hooks/tools/sanitize/useSanitizeParameters"; import { useSanitizeOperation } from "../hooks/tools/sanitize/useSanitizeOperation"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "../types/tool"; -const Sanitize = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { +const Sanitize = (props: BaseToolProps) => { const { t } = useTranslation(); - const { selectedFiles } = useFileSelection(); - - const sanitizeParams = useSanitizeParameters(); - const sanitizeOperation = useSanitizeOperation(); - - // Endpoint validation - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(sanitizeParams.getEndpointName()); - - useEffect(() => { - sanitizeOperation.resetResults(); - onPreviewFile?.(null); - }, [sanitizeParams.parameters]); - - const handleSanitize = async () => { - try { - await sanitizeOperation.executeOperation(sanitizeParams.parameters, selectedFiles); - if (sanitizeOperation.files && onComplete) { - onComplete(sanitizeOperation.files); - } - } catch (error) { - if (onError) { - onError(error instanceof Error ? error.message : t("sanitize.error.generic", "Sanitization failed")); - } - } - }; - - const handleSettingsReset = () => { - sanitizeOperation.resetResults(); - onPreviewFile?.(null); - }; - - const handleThumbnailClick = (file: File) => { - onPreviewFile?.(file); - sessionStorage.setItem("previousMode", "sanitize"); - }; - - const hasFiles = selectedFiles.length > 0; - const hasResults = sanitizeOperation.files.length > 0; - const settingsCollapsed = !hasFiles || hasResults; + const base = useBaseTool( + 'sanitize', + useSanitizeParameters, + useSanitizeOperation, + props + ); return createToolFlow({ files: { - selectedFiles, - isCollapsed: hasResults, + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, placeholder: t("sanitize.files.placeholder", "Select a PDF file in the main view to get started"), }, steps: [ { title: t("sanitize.steps.settings", "Settings"), - isCollapsed: settingsCollapsed, - onCollapsedClick: settingsCollapsed ? handleSettingsReset : undefined, + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined, content: ( ), }, ], executeButton: { text: t("sanitize.submit", "Sanitize PDF"), - isVisible: !hasResults, + isVisible: !base.hasResults, loadingText: t("loading"), - onClick: handleSanitize, - disabled: !sanitizeParams.validateParameters() || !hasFiles || !endpointEnabled, + onClick: base.handleExecute, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, }, review: { - isVisible: hasResults, - operation: sanitizeOperation, + isVisible: base.hasResults, + operation: base.operation, title: t("sanitize.sanitizationResults", "Sanitization Results"), - onFileClick: handleThumbnailClick, + onFileClick: base.handleThumbnailClick, }, }); }; diff --git a/frontend/src/tools/SingleLargePage.tsx b/frontend/src/tools/SingleLargePage.tsx index d071c2b99..dc71c83cd 100644 --- a/frontend/src/tools/SingleLargePage.tsx +++ b/frontend/src/tools/SingleLargePage.tsx @@ -1,78 +1,39 @@ -import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; -import { useFileContext } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; -import { useFileSelection } from "../contexts/file/fileHooks"; - import { createToolFlow } from "../components/tools/shared/createToolFlow"; - import { useSingleLargePageParameters } from "../hooks/tools/singleLargePage/useSingleLargePageParameters"; import { useSingleLargePageOperation } from "../hooks/tools/singleLargePage/useSingleLargePageOperation"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "../types/tool"; -const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { +const SingleLargePage = (props: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); - const { selectedFiles } = useFileSelection(); - const singleLargePageParams = useSingleLargePageParameters(); - const singleLargePageOperation = useSingleLargePageOperation(); - - // Endpoint validation - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(singleLargePageParams.getEndpointName()); - - useEffect(() => { - singleLargePageOperation.resetResults(); - onPreviewFile?.(null); - }, [singleLargePageParams.parameters]); - - const handleConvert = async () => { - try { - await singleLargePageOperation.executeOperation(singleLargePageParams.parameters, selectedFiles); - if (singleLargePageOperation.files && onComplete) { - onComplete(singleLargePageOperation.files); - } - } catch (error) { - if (onError) { - onError(error instanceof Error ? error.message : t("pdfToSinglePage.error.failed", "Single large page operation failed")); - } - } - }; - - const handleThumbnailClick = (file: File) => { - onPreviewFile?.(file); - sessionStorage.setItem("previousMode", "single-large-page"); - actions.setMode("viewer"); - }; - - const handleSettingsReset = () => { - singleLargePageOperation.resetResults(); - onPreviewFile?.(null); - }; - - const hasFiles = selectedFiles.length > 0; - const hasResults = singleLargePageOperation.files.length > 0 || singleLargePageOperation.downloadUrl !== null; + const base = useBaseTool( + 'singleLargePage', + useSingleLargePageParameters, + useSingleLargePageOperation, + props + ); return createToolFlow({ files: { - selectedFiles, - isCollapsed: hasFiles || hasResults, + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, placeholder: t("pdfToSinglePage.files.placeholder", "Select a PDF file in the main view to get started"), }, steps: [], executeButton: { text: t("pdfToSinglePage.submit", "Convert To Single Page"), - isVisible: !hasResults, + isVisible: !base.hasResults, loadingText: t("loading"), - onClick: handleConvert, - disabled: !singleLargePageParams.validateParameters() || !hasFiles || !endpointEnabled, + onClick: base.handleExecute, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, }, review: { - isVisible: hasResults, - operation: singleLargePageOperation, + isVisible: base.hasResults, + operation: base.operation, title: t("pdfToSinglePage.results.title", "Single Page Results"), - onFileClick: handleThumbnailClick, + onFileClick: base.handleThumbnailClick, }, }); }; @@ -80,4 +41,4 @@ const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) // Static method to get the operation hook for automation SingleLargePage.tool = () => useSingleLargePageOperation; -export default SingleLargePage as ToolComponent; \ No newline at end of file +export default SingleLargePage as ToolComponent; diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index 06c7743fd..0c94b6cef 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -1,84 +1,37 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; -import { useFileSelection } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; - import { createToolFlow } from "../components/tools/shared/createToolFlow"; import SplitSettings from "../components/tools/split/SplitSettings"; - import { useSplitParameters } from "../hooks/tools/split/useSplitParameters"; import { useSplitOperation } from "../hooks/tools/split/useSplitOperation"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "../types/tool"; -const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { +const Split = (props: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); - const { selectedFiles } = useFileSelection(); - const splitParams = useSplitParameters(); - const splitOperation = useSplitOperation(); - - // Endpoint validation - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(splitParams.getEndpointName()); - - useEffect(() => { - // Only reset results when parameters change, not when files change - splitOperation.resetResults(); - onPreviewFile?.(null); - }, [splitParams.parameters]); - - useEffect(() => { - // Reset results when selected files change (user selected different files) - if (selectedFiles.length > 0) { - splitOperation.resetResults(); - onPreviewFile?.(null); - } - }, [selectedFiles]); - - const handleSplit = async () => { - try { - await splitOperation.executeOperation(splitParams.parameters, selectedFiles); - if (splitOperation.files && onComplete) { - onComplete(splitOperation.files); - } - } catch (error) { - if (onError) { - onError(error instanceof Error ? error.message : "Split operation failed"); - } - } - }; - - const handleThumbnailClick = (file: File) => { - onPreviewFile?.(file); - sessionStorage.setItem("previousMode", "split"); - }; - - const handleSettingsReset = () => { - splitOperation.resetResults(); - onPreviewFile?.(null); - }; - - const hasFiles = selectedFiles.length > 0; - const hasResults = splitOperation.files.length > 0 || splitOperation.downloadUrl !== null; - const settingsCollapsed = !hasFiles || hasResults; + const base = useBaseTool( + 'split', + useSplitParameters, + useSplitOperation, + props + ); return createToolFlow({ files: { - selectedFiles, - isCollapsed: hasResults, - placeholder: "Select a PDF file in the main view to get started", + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, }, steps: [ { title: "Settings", - isCollapsed: settingsCollapsed, - onCollapsedClick: hasResults ? handleSettingsReset : undefined, + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined, content: ( ), }, @@ -86,15 +39,15 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { executeButton: { text: t("split.submit", "Split PDF"), loadingText: t("loading"), - onClick: handleSplit, - isVisible: !hasResults, - disabled: !splitParams.validateParameters() || !hasFiles || !endpointEnabled, + onClick: base.handleExecute, + isVisible: !base.hasResults, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, }, review: { - isVisible: hasResults, - operation: splitOperation, + isVisible: base.hasResults, + operation: base.operation, title: "Split Results", - onFileClick: handleThumbnailClick, + onFileClick: base.handleThumbnailClick, }, }); }; diff --git a/frontend/src/tools/UnlockPdfForms.tsx b/frontend/src/tools/UnlockPdfForms.tsx index 6c5bd4cb5..65445290f 100644 --- a/frontend/src/tools/UnlockPdfForms.tsx +++ b/frontend/src/tools/UnlockPdfForms.tsx @@ -1,78 +1,39 @@ -import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; -import { useFileContext } from "../contexts/FileContext"; -import { useNavigationActions } from "../contexts/NavigationContext"; -import { useFileSelection } from "../contexts/file/fileHooks"; - import { createToolFlow } from "../components/tools/shared/createToolFlow"; - import { useUnlockPdfFormsParameters } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters"; import { useUnlockPdfFormsOperation } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { BaseToolProps, ToolComponent } from "../types/tool"; -const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { +const UnlockPdfForms = (props: BaseToolProps) => { const { t } = useTranslation(); - const { actions } = useNavigationActions(); - const { selectedFiles } = useFileSelection(); - const unlockPdfFormsParams = useUnlockPdfFormsParameters(); - const unlockPdfFormsOperation = useUnlockPdfFormsOperation(); - - // Endpoint validation - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(unlockPdfFormsParams.getEndpointName()); - - useEffect(() => { - unlockPdfFormsOperation.resetResults(); - onPreviewFile?.(null); - }, [unlockPdfFormsParams.parameters]); - - const handleUnlock = async () => { - try { - await unlockPdfFormsOperation.executeOperation(unlockPdfFormsParams.parameters, selectedFiles); - if (unlockPdfFormsOperation.files && onComplete) { - onComplete(unlockPdfFormsOperation.files); - } - } catch (error) { - if (onError) { - onError(error instanceof Error ? error.message : t("unlockPDFForms.error.failed", "Unlock PDF forms operation failed")); - } - } - }; - - const handleThumbnailClick = (file: File) => { - onPreviewFile?.(file); - sessionStorage.setItem("previousMode", "unlockPdfForms"); - actions.setMode("viewer"); - }; - - const handleSettingsReset = () => { - unlockPdfFormsOperation.resetResults(); - onPreviewFile?.(null); - }; - - const hasFiles = selectedFiles.length > 0; - const hasResults = unlockPdfFormsOperation.files.length > 0 || unlockPdfFormsOperation.downloadUrl !== null; + const base = useBaseTool( + 'unlockPdfForms', + useUnlockPdfFormsParameters, + useUnlockPdfFormsOperation, + props + ); return createToolFlow({ files: { - selectedFiles, - isCollapsed: hasFiles || hasResults, + selectedFiles: base.selectedFiles, + isCollapsed: base.hasFiles || base.hasResults, placeholder: t("unlockPDFForms.files.placeholder", "Select a PDF file in the main view to get started"), }, steps: [], executeButton: { text: t("unlockPDFForms.submit", "Unlock Forms"), - isVisible: !hasResults, + isVisible: !base.hasResults, loadingText: t("loading"), - onClick: handleUnlock, - disabled: !unlockPdfFormsParams.validateParameters() || !hasFiles || !endpointEnabled, + onClick: base.handleExecute, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, }, review: { - isVisible: hasResults, - operation: unlockPdfFormsOperation, + isVisible: base.hasResults, + operation: base.operation, title: t("unlockPDFForms.results.title", "Unlocked Forms Results"), - onFileClick: handleThumbnailClick, + onFileClick: base.handleThumbnailClick, }, }); }; @@ -80,4 +41,4 @@ const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = // Static method to get the operation hook for automation UnlockPdfForms.tool = () => useUnlockPdfFormsOperation; -export default UnlockPdfForms as ToolComponent; \ No newline at end of file +export default UnlockPdfForms as ToolComponent;