From abc0988fdfb4281316f413270f05dda2f1b87917 Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:29:58 +0100 Subject: [PATCH] Feature/v2/reader-and-multitool-navigation (#4514) Co-authored-by: Connor Yoh --- .../public/locales/en-GB/translation.json | 8 ++- frontend/src/components/layout/Workbench.tsx | 1 - .../src/components/pageEditor/PageEditor.tsx | 71 ++++++++++++------- .../shared/NavigationWarningModal.tsx | 67 +++++++++-------- .../src/components/shared/TopControls.tsx | 12 ++-- .../quickAccessBar/ActiveToolButton.tsx | 5 +- .../shared/quickAccessBar/QuickAccessBar.ts | 4 +- frontend/src/components/tools/ToolPanel.tsx | 5 +- .../tools/toolPicker/ToolButton.tsx | 3 +- frontend/src/contexts/NavigationContext.tsx | 26 +++++-- frontend/src/contexts/ToolWorkflowContext.tsx | 50 +++++++------ .../src/data/useTranslatedToolRegistry.tsx | 51 ++++++------- frontend/src/hooks/useToolNavigation.ts | 5 +- frontend/src/hooks/useToolSections.ts | 5 +- frontend/src/hooks/useUrlSync.ts | 2 +- 15 files changed, 189 insertions(+), 126 deletions(-) diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index a633b9c0b..133f61112 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1,4 +1,10 @@ { + "unsavedChanges": "You have unsaved changes to your PDF. What would you like to do?", + "unsavedChangesTitle": "Unsaved Changes", + "keepWorking": "Keep Working", + "discardChanges": "Discard Changes", + "applyAndContinue": "Apply & Continue", + "exportAndContinue": "Export & Continue", "language": { "direction": "ltr" }, @@ -3449,4 +3455,4 @@ }, "termsAndConditions": "Terms & Conditions", "logOut": "Log out" -} \ No newline at end of file +} diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index 77a5082a1..8089d1a44 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -149,7 +149,6 @@ export default function Workbench() { {/* Dismiss All Errors Button */} diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index 8939f7db4..b24fb819a 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -1,6 +1,7 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core"; import { useFileState, useFileActions } from "../../contexts/FileContext"; +import { useNavigationGuard } from "../../contexts/NavigationContext"; import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor"; import { pdfExportService } from "../../services/pdfExportService"; import { documentManipulationService } from "../../services/documentManipulationService"; @@ -36,6 +37,9 @@ const PageEditor = ({ const { state, selectors } = useFileState(); const { actions } = useFileActions(); + // Navigation guard for unsaved changes + const { setHasUnsavedChanges } = useNavigationGuard(); + // Prefer IDs + selectors to avoid array identity churn const activeFileIds = state.files.ids; @@ -82,6 +86,12 @@ const PageEditor = ({ updateUndoRedoState(); }, [updateUndoRedoState]); + // Wrapper for executeCommand to track unsaved changes + const executeCommandWithTracking = useCallback((command: any) => { + undoManagerRef.current.executeCommand(command); + setHasUnsavedChanges(true); + }, [setHasUnsavedChanges]); + // Watch for container size changes to update split line positions useEffect(() => { const container = gridContainerRef.current; @@ -138,17 +148,16 @@ const PageEditor = ({ // DOM-first command handlers const handleRotatePages = useCallback((pageIds: string[], rotation: number) => { const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation); - undoManagerRef.current.executeCommand(bulkRotateCommand); - }, []); + executeCommandWithTracking(bulkRotateCommand); + }, [executeCommandWithTracking]); // Command factory functions for PageThumbnail const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({ execute: () => { const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation); - - undoManagerRef.current.executeCommand(bulkRotateCommand); + executeCommandWithTracking(bulkRotateCommand); } - }), []); + }), [executeCommandWithTracking]); const createDeleteCommand = useCallback((pageIds: string[]) => ({ execute: () => { @@ -174,10 +183,10 @@ const PageEditor = ({ () => getPageNumbersFromIds(selectedPageIds), closePdf ); - undoManagerRef.current.executeCommand(deleteCommand); + executeCommandWithTracking(deleteCommand); } } - }), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]); + }), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]); const createSplitCommand = useCallback((position: number) => ({ execute: () => { @@ -186,9 +195,9 @@ const PageEditor = ({ () => splitPositions, setSplitPositions ); - undoManagerRef.current.executeCommand(splitCommand); + executeCommandWithTracking(splitCommand); } -}), [splitPositions]); +}), [splitPositions, executeCommandWithTracking]); // Command executor for PageThumbnail const executeCommand = useCallback((command: any) => { @@ -232,8 +241,8 @@ const PageEditor = ({ () => selectedPageNumbers, closePdf ); - undoManagerRef.current.executeCommand(deleteCommand); - }, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers]); + executeCommandWithTracking(deleteCommand); + }, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers, executeCommandWithTracking]); const handleDeletePage = useCallback((pageNumber: number) => { if (!displayDocument) return; @@ -251,8 +260,8 @@ const PageEditor = ({ () => getPageNumbersFromIds(selectedPageIds), closePdf ); - undoManagerRef.current.executeCommand(deleteCommand); - }, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]); + executeCommandWithTracking(deleteCommand); + }, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]); const handleSplit = useCallback(() => { if (!displayDocument || selectedPageIds.length === 0) return; @@ -298,8 +307,8 @@ const PageEditor = ({ : `Add ${selectedPositions.length - existingSplitsCount} split(s)` }; - undoManagerRef.current.executeCommand(smartSplitCommand); - }, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]); + executeCommandWithTracking(smartSplitCommand); + }, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]); const handleSplitAll = useCallback(() => { if (!displayDocument || selectedPageIds.length === 0) return; @@ -344,8 +353,8 @@ const PageEditor = ({ : `Add ${selectedPositions.length - existingSplitsCount} split(s)` }; - undoManagerRef.current.executeCommand(smartSplitCommand); - }, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]); + executeCommandWithTracking(smartSplitCommand); + }, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]); const handlePageBreak = useCallback(() => { if (!displayDocument || selectedPageIds.length === 0) return; @@ -358,8 +367,8 @@ const PageEditor = ({ () => displayDocument, setEditedDocument ); - undoManagerRef.current.executeCommand(pageBreakCommand); - }, [selectedPageIds, displayDocument, getPageNumbersFromIds]); + executeCommandWithTracking(pageBreakCommand); + }, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]); const handlePageBreakAll = useCallback(() => { if (!displayDocument || selectedPageIds.length === 0) return; @@ -372,8 +381,8 @@ const PageEditor = ({ () => displayDocument, setEditedDocument ); - undoManagerRef.current.executeCommand(pageBreakCommand); - }, [selectedPageIds, displayDocument, getPageNumbersFromIds]); + executeCommandWithTracking(pageBreakCommand); + }, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]); const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => { if (!displayDocument || files.length === 0) return; @@ -416,8 +425,8 @@ const PageEditor = ({ () => displayDocument, setEditedDocument ); - undoManagerRef.current.executeCommand(reorderCommand); - }, [displayDocument, getPageNumbersFromIds]); + executeCommandWithTracking(reorderCommand); + }, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking]); // Helper function to collect source files for multi-file export const getSourceFiles = useCallback((): Map | null => { @@ -499,13 +508,14 @@ const PageEditor = ({ // Step 4: Download the result pdfExportService.downloadFile(result.blob, result.filename); + setHasUnsavedChanges(false); // Clear unsaved changes after successful export setExportLoading(false); } catch (error) { console.error('Export failed:', error); setExportLoading(false); } - }, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]); + }, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]); const onExportAll = useCallback(async () => { if (!displayDocument) return; @@ -552,6 +562,7 @@ const PageEditor = ({ const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip'); pdfExportService.downloadFile(zipBlob, zipFilename); + setHasUnsavedChanges(false); // Clear unsaved changes after successful export } else { // Single document - regular export const sourceFiles = getSourceFiles(); @@ -570,6 +581,7 @@ const PageEditor = ({ ); pdfExportService.downloadFile(result.blob, result.filename); + setHasUnsavedChanges(false); // Clear unsaved changes after successful export } setExportLoading(false); @@ -577,7 +589,7 @@ const PageEditor = ({ console.error('Export failed:', error); setExportLoading(false); } - }, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]); + }, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]); // Apply DOM changes to document state using dedicated service const applyChanges = useCallback(() => { @@ -779,7 +791,14 @@ const PageEditor = ({ )} - + { + applyChanges(); + }} + onExportAndContinue={async () => { + await onExportAll(); + }} + /> ); }; diff --git a/frontend/src/components/shared/NavigationWarningModal.tsx b/frontend/src/components/shared/NavigationWarningModal.tsx index c7f591c71..ede7b4bba 100644 --- a/frontend/src/components/shared/NavigationWarningModal.tsx +++ b/frontend/src/components/shared/NavigationWarningModal.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Modal, Text, Button, Group, Stack } from '@mantine/core'; import { useNavigationGuard } from '../../contexts/NavigationContext'; +import { useTranslation } from 'react-i18next'; interface NavigationWarningModalProps { onApplyAndContinue?: () => Promise; @@ -11,6 +12,8 @@ const NavigationWarningModal = ({ onApplyAndContinue, onExportAndContinue }: NavigationWarningModalProps) => { + + const { t } = useTranslation(); const { showNavigationWarning, hasUnsavedChanges, @@ -28,7 +31,7 @@ const NavigationWarningModal = ({ confirmNavigation(); }; - const handleApplyAndContinue = async () => { + const _handleApplyAndContinue = async () => { if (onApplyAndContinue) { await onApplyAndContinue(); } @@ -52,55 +55,59 @@ const NavigationWarningModal = ({ - You have unsaved changes to your PDF. What would you like to do? + {t("unsavedChanges", "You have unsaved changes to your PDF. What would you like to do?")} - - - - + + + - - {onApplyAndContinue && ( + + - )} - - {onExportAndContinue && ( - - )} + + {/* TODO:: Add this back in when it works */} + {/* {onApplyAndContinue && ( + + )} */} + + {onExportAndContinue && ( + + )} + ); }; -export default NavigationWarningModal; \ No newline at end of file +export default NavigationWarningModal; diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index bf42b773f..03eacc801 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -19,7 +19,7 @@ const viewOptionStyle = { // Build view options showing text always -const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null, isToolSelected: boolean) => { +const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => { const viewerOption = { label: (
@@ -75,7 +75,7 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp // Build options array conditionally return [ viewerOption, - ...(isToolSelected ? [] : [pageEditorOption]), + pageEditorOption, fileEditorOption, ]; }; @@ -83,19 +83,15 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp interface TopControlsProps { currentView: WorkbenchType; setCurrentView: (view: WorkbenchType) => void; - selectedToolKey?: string | null; } const TopControls = ({ currentView, setCurrentView, - selectedToolKey, -}: TopControlsProps) => { + }: TopControlsProps) => { const { isRainbowMode } = useRainbowThemeContext(); const [switchingTo, setSwitchingTo] = useState(null); - const isToolSelected = selectedToolKey !== null; - const handleViewChange = useCallback((view: string) => { if (!isValidWorkbench(view)) { return; @@ -122,7 +118,7 @@ const TopControls = ({
= ({ setActiveButton }) const { getHomeNavigation } = useSidebarNavigation(); // Determine if the indicator should be visible (do not require selectedTool to be resolved yet) + // Special case: multiTool should always show even when sidebars are hidden const indicatorShouldShow = Boolean( - selectedToolKey && leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey) + selectedToolKey && + ((leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)) || + selectedToolKey === 'multiTool') ); // Local animation and hover state diff --git a/frontend/src/components/shared/quickAccessBar/QuickAccessBar.ts b/frontend/src/components/shared/quickAccessBar/QuickAccessBar.ts index efadc4a72..03e028834 100644 --- a/frontend/src/components/shared/quickAccessBar/QuickAccessBar.ts +++ b/frontend/src/components/shared/quickAccessBar/QuickAccessBar.ts @@ -12,7 +12,7 @@ export const isNavButtonActive = ( isFilesModalOpen: boolean, configModalOpen: boolean, selectedToolKey?: string | null, - leftPanelView?: 'toolPicker' | 'toolContent' + leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden' ): boolean => { const isActiveByLocalState = config.type === 'navigation' && activeButton === config.id; const isActiveByContext = @@ -35,7 +35,7 @@ export const getNavButtonStyle = ( isFilesModalOpen: boolean, configModalOpen: boolean, selectedToolKey?: string | null, - leftPanelView?: 'toolPicker' | 'toolContent' + leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden' ) => { const isActive = isNavButtonActive( config, diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index d3eea3bd9..7f19482bd 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -7,6 +7,7 @@ import ToolSearch from './toolPicker/ToolSearch'; import { useSidebarContext } from "../../contexts/SidebarContext"; import rainbowStyles from '../../styles/rainbow.module.css'; import { ScrollArea } from '@mantine/core'; +import { ToolId } from '../../types/toolId'; // No props needed - component uses context @@ -71,7 +72,7 @@ export default function ToolPanel() {
handleToolSelect(id as ToolId)} searchQuery={searchQuery} />
@@ -80,7 +81,7 @@ export default function ToolPanel() {
handleToolSelect(id as ToolId)} filteredTools={filteredTools} isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)} /> diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx index 9f3d60d50..236cbb49f 100644 --- a/frontend/src/components/tools/toolPicker/ToolButton.tsx +++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx @@ -17,7 +17,8 @@ interface ToolButtonProps { } const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym }) => { - const isUnavailable = !tool.component && !tool.link; + // Special case: read and multiTool are navigational tools that are always available + const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; const { getToolNavigation } = useToolNavigation(); const handleClick = (id: string) => { diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx index f9e8ba9a5..b71664b6b 100644 --- a/frontend/src/contexts/NavigationContext.tsx +++ b/frontend/src/contexts/NavigationContext.tsx @@ -109,16 +109,34 @@ export const NavigationProvider: React.FC<{ const actions: NavigationContextActions = { setWorkbench: useCallback((workbench: WorkbenchType) => { - dispatch({ type: 'SET_WORKBENCH', payload: { workbench } }); - }, []), + // If we're leaving pageEditor workbench and have unsaved changes, request navigation + if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) { + const performWorkbenchChange = () => { + dispatch({ type: 'SET_WORKBENCH', payload: { workbench } }); + }; + dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } }); + dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } }); + } else { + dispatch({ type: 'SET_WORKBENCH', payload: { workbench } }); + } + }, [state.workbench, state.hasUnsavedChanges]), setSelectedTool: useCallback((toolId: ToolId | null) => { dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } }); }, []), setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => { - dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } }); - }, []), + // If we're leaving pageEditor workbench and have unsaved changes, request navigation + if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) { + const performWorkbenchChange = () => { + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } }); + }; + dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } }); + dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } }); + } else { + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } }); + } + }, [state.workbench, state.hasUnsavedChanges]), setHasUnsavedChanges: useCallback((hasChanges: boolean) => { dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } }); diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 106636692..e38375daa 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -17,7 +17,7 @@ import { filterToolRegistryByQuery } from '../utils/toolSearch'; interface ToolWorkflowState { // UI State sidebarsVisible: boolean; - leftPanelView: 'toolPicker' | 'toolContent'; + leftPanelView: 'toolPicker' | 'toolContent' | 'hidden'; readerMode: boolean; // File/Preview State @@ -31,7 +31,7 @@ interface ToolWorkflowState { // Actions type ToolWorkflowAction = | { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean } - | { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' } + | { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' | 'hidden' } | { type: 'SET_READER_MODE'; payload: boolean } | { type: 'SET_PREVIEW_FILE'; payload: File | null } | { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null } @@ -80,7 +80,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { // UI Actions setSidebarsVisible: (visible: boolean) => void; - setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void; + setLeftPanelView: (view: 'toolPicker' | 'toolContent' | 'hidden') => void; setReaderMode: (mode: boolean) => void; setPreviewFile: (file: File | null) => void; setPageEditorFunctions: (functions: PageEditorFunctions | null) => void; @@ -96,7 +96,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { resetTool: (toolId: string) => void; // Workflow Actions (compound actions) - handleToolSelect: (toolId: string) => void; + handleToolSelect: (toolId: ToolId) => void; handleBackToTools: () => void; handleReaderToggle: () => void; @@ -136,7 +136,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible }); }, []); - const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent') => { + const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent' | 'hidden') => { dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view }); }, []); @@ -180,7 +180,26 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { }, []); // Empty dependency array makes this stable // Workflow actions (compound actions that coordinate multiple state changes) - const handleToolSelect = useCallback((toolId: string) => { + const handleToolSelect = useCallback((toolId: ToolId) => { + // Handle read tool selection - should behave exactly like QuickAccessBar read button + if (toolId === 'read') { + setReaderMode(true); + actions.setSelectedTool('read'); + actions.setWorkbench('viewer'); + setSearchQuery(''); + return; + } + + // Handle multiTool selection - enable page editor workbench and hide left panel + if (toolId === 'multiTool') { + setReaderMode(false); + setLeftPanelView('hidden'); + actions.setSelectedTool('multiTool'); + actions.setWorkbench('pageEditor'); + setSearchQuery(''); + return; + } + // Set the selected tool and determine the appropriate workbench const validToolId = isValidToolId(toolId) ? toolId : null; actions.setSelectedTool(validToolId); @@ -195,19 +214,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { // Clear search query when selecting a tool setSearchQuery(''); - - // Handle view switching logic - if (toolId === 'allTools' || toolId === 'read' || toolId === 'view-pdf') { - setLeftPanelView('toolPicker'); - if (toolId === 'read' || toolId === 'view-pdf') { - setReaderMode(true); - } else { - setReaderMode(false); - } - } else { - setLeftPanelView('toolContent'); - setReaderMode(false); // Disable read mode when selecting tools - } + setLeftPanelView('toolContent'); + setReaderMode(false); // Disable read mode when selecting tools }, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]); const handleBackToTools = useCallback(() => { @@ -227,8 +235,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { }, [toolRegistry, state.searchQuery]); const isPanelVisible = useMemo(() => - state.sidebarsVisible && !state.readerMode, - [state.sidebarsVisible, state.readerMode] + state.sidebarsVisible && !state.readerMode && state.leftPanelView !== 'hidden', + [state.sidebarsVisible, state.readerMode, state.leftPanelView] ); // URL sync for proper tool navigation diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index c8c29821e..c36f22b2d 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -183,8 +183,32 @@ export function useFlatToolRegistry(): ToolRegistry { return useMemo(() => { const allTools: ToolRegistry = { + // Recommended Tools in order + multiTool: { + icon: , + name: t("home.multiTool.title", "Multi-Tool"), + component: null, + workbench: "pageEditor", + description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"), + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, + maxFiles: -1, + synonyms: getSynonyms(t, "multiTool"), + }, + merge: { + icon: , + name: t("home.merge.title", "Merge"), + component: Merge, + description: t("home.merge.desc", "Merge multiple PDFs into a single document"), + categoryId: ToolCategoryId.RECOMMENDED_TOOLS, + subcategoryId: SubcategoryId.GENERAL, + maxFiles: -1, + endpoints: ["merge-pdfs"], + operationConfig: mergeOperationConfig, + settingsComponent: MergeSettings, + synonyms: getSynonyms(t, "merge") + }, // Signing - certSign: { icon: , name: t("home.certSign.title", "Certificate Sign"), @@ -792,30 +816,7 @@ export function useFlatToolRegistry(): ToolRegistry { settingsComponent: ConvertSettings, synonyms: getSynonyms(t, "convert") }, - merge: { - icon: , - name: t("home.merge.title", "Merge"), - component: Merge, - description: t("home.merge.desc", "Merge multiple PDFs into a single document"), - categoryId: ToolCategoryId.RECOMMENDED_TOOLS, - subcategoryId: SubcategoryId.GENERAL, - maxFiles: -1, - endpoints: ["merge-pdfs"], - operationConfig: mergeOperationConfig, - settingsComponent: MergeSettings, - synonyms: getSynonyms(t, "merge") - }, - multiTool: { - icon: , - name: t("home.multiTool.title", "Multi-Tool"), - component: null, - workbench: "pageEditor", - description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"), - categoryId: ToolCategoryId.RECOMMENDED_TOOLS, - subcategoryId: SubcategoryId.GENERAL, - maxFiles: -1, - synonyms: getSynonyms(t, "multiTool"), - }, + ocr: { icon: , name: t("home.ocr.title", "OCR"), diff --git a/frontend/src/hooks/useToolNavigation.ts b/frontend/src/hooks/useToolNavigation.ts index 704fd5026..5c6fcf47c 100644 --- a/frontend/src/hooks/useToolNavigation.ts +++ b/frontend/src/hooks/useToolNavigation.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import { ToolRegistryEntry, getToolUrlPath } from '../data/toolsTaxonomy'; import { useToolWorkflow } from '../contexts/ToolWorkflowContext'; import { handleUnlessSpecialClick } from '../utils/clickHandlers'; +import { ToolId } from '../types/toolId'; export interface ToolNavigationProps { /** Full URL for the tool (for href attribute) */ @@ -34,7 +35,7 @@ export function useToolNavigation(): { } // Use SPA navigation for internal tools - handleToolSelect(toolId); + handleToolSelect(toolId as ToolId); }); }; @@ -42,4 +43,4 @@ export function useToolNavigation(): { }, [handleToolSelect]); return { getToolNavigation }; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useToolSections.ts b/frontend/src/hooks/useToolSections.ts index 088a1ba52..5c5be8773 100644 --- a/frontend/src/hooks/useToolSections.ts +++ b/frontend/src/hooks/useToolSections.ts @@ -72,7 +72,10 @@ export function useToolSections( const subcategoryId = s as SubcategoryId; if (!quick[subcategoryId]) quick[subcategoryId] = []; // Only include ready tools (have a component or external link) in Quick Access - const readyTools = tools.filter(({ tool }) => tool.component !== null || !!tool.link); + // Special case: read and multiTool are navigational tools that don't need components + const readyTools = tools.filter(({ tool, id }) => + tool.component !== null || !!tool.link || id === 'read' || id === 'multiTool' + ); quick[subcategoryId].push(...readyTools); }); } diff --git a/frontend/src/hooks/useUrlSync.ts b/frontend/src/hooks/useUrlSync.ts index a90ee4fbe..545285c5e 100644 --- a/frontend/src/hooks/useUrlSync.ts +++ b/frontend/src/hooks/useUrlSync.ts @@ -14,7 +14,7 @@ import { withBasePath } from '../constants/app'; */ export function useNavigationUrlSync( selectedTool: ToolId | null, - handleToolSelect: (toolId: string) => void, + handleToolSelect: (toolId: ToolId) => void, clearToolSelection: () => void, registry: ToolRegistry, enableSync: boolean = true