diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ef4d663f6..602b88cf2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext"; import { SidebarProvider } from "./contexts/SidebarContext"; import ErrorBoundary from "./components/shared/ErrorBoundary"; import HomePage from "./pages/HomePage"; +import { ScarfPixel } from "./components/ScarfPixel"; // Import global styles import "./styles/tailwind.css"; @@ -36,6 +37,7 @@ export default function App() { + diff --git a/frontend/src/components/ScarfPixel.tsx b/frontend/src/components/ScarfPixel.tsx index 2f44077c0..4d8a40ddd 100644 --- a/frontend/src/components/ScarfPixel.tsx +++ b/frontend/src/components/ScarfPixel.tsx @@ -1,32 +1,29 @@ -import { useLocation } from "react-router-dom"; import { useEffect, useRef } from "react"; +import { useNavigationState } from "../contexts/NavigationContext"; export function ScarfPixel() { - const location = useLocation(); + const { workbench, selectedTool } = useNavigationState(); const lastUrlSent = useRef(null); // helps with React 18 StrictMode in dev useEffect(() => { - // Force reload of the tracking pixel on route change + // Get current pathname from browser location + const pathname = window.location.pathname; + const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7' - + '&path=' + encodeURIComponent(location.pathname) - + '&t=' + Date.now(); // cache-buster + + '&path=' + encodeURIComponent(pathname) + + '&t=' + Date.now(); // cache-buster + console.log("ScarfPixel: Navigation change", { workbench, selectedTool, pathname }); - // + '&machineType=' + machineType - // + '&appVersion=' + appVersion - // + '&licenseType=' + license - // + '&loginEnabled=' + loginEnabled; - console.log("ScarfPixel: reload " + location.pathname ); - - if (lastUrlSent.current !== url) { + if (lastUrlSent.current !== url) { lastUrlSent.current = url; const img = new Image(); img.referrerPolicy = "no-referrer-when-downgrade"; // optional img.src = url; - console.log("ScarfPixel: Fire to... " + location.pathname , url); + console.log("ScarfPixel: Fire to... " + pathname, url); } - }, [location.pathname]); + }, [workbench, selectedTool]); // Fire when navigation state changes return null; // Nothing visible in UI } diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index eee1eb1a7..8dfea1ff5 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -411,9 +411,9 @@ const FileEditor = ({ if (record) { // Set the file as selected in context and switch to viewer for preview setSelectedFiles([fileId]); - navActions.setMode('viewer'); + navActions.setWorkbench('viewer'); } - }, [activeFileRecords, setSelectedFiles, navActions.setMode]); + }, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]); const handleMergeFromHere = useCallback((fileId: string) => { const startIndex = activeFileRecords.findIndex(r => r.id === fileId); diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index 3884fdaf5..a77067d99 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -6,13 +6,13 @@ import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import { useFileHandler } from '../../hooks/useFileHandler'; import { useFileState, useFileActions } from '../../contexts/FileContext'; import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext'; +import { useToolManagement } from '../../hooks/useToolManagement'; import TopControls from '../shared/TopControls'; import FileEditor from '../fileEditor/FileEditor'; import PageEditor from '../pageEditor/PageEditor'; import PageEditorControls from '../pageEditor/PageEditorControls'; import Viewer from '../viewer/Viewer'; -import ToolRenderer from '../tools/ToolRenderer'; import LandingPage from '../shared/LandingPage'; // No props needed - component uses contexts directly @@ -23,9 +23,9 @@ export default function Workbench() { // Use context-based hooks to eliminate all prop drilling const { state } = useFileState(); const { actions } = useFileActions(); - const { currentMode: currentView } = useNavigationState(); + const { workbench: currentView } = useNavigationState(); const { actions: navActions } = useNavigationActions(); - const setCurrentView = navActions.setMode; + const setCurrentView = navActions.setWorkbench; const activeFiles = state.files.ids; const { previewFile, @@ -36,7 +36,14 @@ export default function Workbench() { setSidebarsVisible } = useToolWorkflow(); - const { selectedToolKey, selectedTool, handleToolSelect } = useToolWorkflow(); + const { handleToolSelect } = useToolWorkflow(); + + // Get navigation state - this is the source of truth + const { selectedTool: selectedToolId } = useNavigationState(); + + // Get tool registry to look up selected tool + const { toolRegistry } = useToolManagement(); + const selectedTool = selectedToolId ? toolRegistry[selectedToolId] : null; const { addToActiveFiles } = useFileHandler(); const handlePreviewClose = () => { @@ -69,11 +76,11 @@ export default function Workbench() { case "fileEditor": return ( { setCurrentView("pageEditor"); }, @@ -127,14 +134,6 @@ export default function Workbench() { ); default: - // Check if it's a tool view - if (selectedToolKey && selectedTool) { - return ( - - ); - } return ( ); @@ -154,7 +153,7 @@ export default function Workbench() { {/* Main content area */} diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index 69818d28d..be6223dc8 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -6,7 +6,6 @@ import { } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext"; -import { ModeType } from "../../contexts/NavigationContext"; import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor"; import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing"; import { pdfExportService } from "../../services/pdfExportService"; @@ -83,7 +82,7 @@ const PageEditor = ({ // Grid container ref for positioning split indicators const gridContainerRef = useRef(null); - + // State to trigger re-renders when container size changes const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 }); @@ -128,7 +127,7 @@ const PageEditor = ({ // Interface functions for parent component const displayDocument = editedDocument || mergedPdfDocument; - + // Utility functions to convert between page IDs and page numbers const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => { if (!displayDocument) return []; @@ -137,7 +136,7 @@ const PageEditor = ({ return page?.pageNumber || 0; }).filter(num => num > 0); }, [displayDocument]); - + const getPageIdsFromNumbers = useCallback((pageNumbers: number[]): string[] => { if (!displayDocument) return []; return pageNumbers.map(num => { @@ -145,10 +144,10 @@ const PageEditor = ({ return page?.id || ''; }).filter(id => id !== ''); }, [displayDocument]); - + // Convert selectedPageIds to numbers for components that still need numbers - const selectedPageNumbers = useMemo(() => - getPageNumbersFromIds(selectedPageIds), + const selectedPageNumbers = useMemo(() => + getPageNumbersFromIds(selectedPageIds), [selectedPageIds, getPageNumbersFromIds] ); @@ -234,7 +233,7 @@ const PageEditor = ({ const handleRotate = useCallback((direction: 'left' | 'right') => { if (!displayDocument || selectedPageIds.length === 0) return; const rotation = direction === 'left' ? -90 : 90; - + handleRotatePages(selectedPageIds, rotation); }, [displayDocument, selectedPageIds, handleRotatePages]); @@ -296,14 +295,14 @@ const PageEditor = ({ // Smart toggle logic: follow the majority, default to adding splits if equal const existingSplitsCount = selectedPositions.filter(pos => splitPositions.has(pos)).length; const noSplitsCount = selectedPositions.length - existingSplitsCount; - + // Remove splits only if majority already have splits // If equal (50/50), default to adding splits const shouldRemoveSplits = existingSplitsCount > noSplitsCount; - + const newSplitPositions = new Set(splitPositions); - + if (shouldRemoveSplits) { // Remove splits from all selected positions selectedPositions.forEach(pos => newSplitPositions.delete(pos)); @@ -316,8 +315,8 @@ const PageEditor = ({ const smartSplitCommand = { execute: () => setSplitPositions(newSplitPositions), undo: () => setSplitPositions(splitPositions), - description: shouldRemoveSplits - ? `Remove ${selectedPositions.length} split(s)` + description: shouldRemoveSplits + ? `Remove ${selectedPositions.length} split(s)` : `Add ${selectedPositions.length - existingSplitsCount} split(s)` }; @@ -343,13 +342,13 @@ const PageEditor = ({ // Smart toggle logic: follow the majority, default to adding splits if equal const existingSplitsCount = selectedPositions.filter(pos => splitPositions.has(pos)).length; const noSplitsCount = selectedPositions.length - existingSplitsCount; - + // Remove splits only if majority already have splits // If equal (50/50), default to adding splits const shouldRemoveSplits = existingSplitsCount > noSplitsCount; - + const newSplitPositions = new Set(splitPositions); - + if (shouldRemoveSplits) { // Remove splits from all selected positions selectedPositions.forEach(pos => newSplitPositions.delete(pos)); @@ -362,8 +361,8 @@ const PageEditor = ({ const smartSplitCommand = { execute: () => setSplitPositions(newSplitPositions), undo: () => setSplitPositions(splitPositions), - description: shouldRemoveSplits - ? `Remove ${selectedPositions.length} split(s)` + description: shouldRemoveSplits + ? `Remove ${selectedPositions.length} split(s)` : `Add ${selectedPositions.length - existingSplitsCount} split(s)` }; @@ -404,7 +403,7 @@ const PageEditor = ({ try { const targetPage = displayDocument.pages.find(p => p.pageNumber === insertAfterPage); if (!targetPage) return; - + await actions.addFiles(files, { insertAfterPageId: targetPage.id }); } catch (error) { console.error('Failed to insert files:', error); @@ -457,7 +456,7 @@ const PageEditor = ({ // Use multi-file export if we have multiple original files const hasInsertedFiles = false; const hasMultipleOriginalFiles = activeFileIds.length > 1; - + if (!hasInsertedFiles && !hasMultipleOriginalFiles) { return null; // Use single-file export method } @@ -499,7 +498,7 @@ const PageEditor = ({ // Step 2: Use the already selected page IDs // Filter to only include IDs that exist in the document with DOM state - const validSelectedPageIds = selectedPageIds.filter(pageId => + const validSelectedPageIds = selectedPageIds.filter(pageId => documentWithDOMState.pages.some(p => p.id === pageId) ); @@ -551,11 +550,11 @@ const PageEditor = ({ const sourceFiles = getSourceFiles(); const baseExportFilename = getExportFilename(); const baseName = baseExportFilename.replace(/\.pdf$/i, ''); - + for (let i = 0; i < processedDocuments.length; i++) { const doc = processedDocuments[i]; const partFilename = `${baseName}_part_${i + 1}.pdf`; - + const result = sourceFiles ? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { filename: partFilename }) : await pdfExportService.exportPDF(doc, [], { filename: partFilename }); @@ -723,23 +722,23 @@ const PageEditor = ({ const ITEM_WIDTH = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx; const ITEM_HEIGHT = parseFloat(GRID_CONSTANTS.ITEM_HEIGHT) * remToPx; const ITEM_GAP = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx; - + return Array.from(splitPositions).map((position) => { - + // Calculate items per row using DragDropGrid's logic const availableWidth = containerWidth - ITEM_GAP; // Account for first gap const itemWithGap = ITEM_WIDTH + ITEM_GAP; const itemsPerRow = Math.max(1, Math.floor(availableWidth / itemWithGap)); - + // Calculate position within the grid (same as DragDropGrid) const row = Math.floor(position / itemsPerRow); const col = position % itemsPerRow; - + // Position split line between pages (after the current page) // Calculate grid centering offset (same as DragDropGrid) const gridWidth = itemsPerRow * ITEM_WIDTH + (itemsPerRow - 1) * ITEM_GAP; const gridOffset = Math.max(0, (containerWidth - gridWidth) / 2); - + const leftPosition = gridOffset + col * itemWithGap + ITEM_WIDTH + (ITEM_GAP / 2); const topPosition = row * ITEM_HEIGHT + (ITEM_HEIGHT * 0.05); // Center vertically (5% offset since page is 90% height) diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx index 614a60cf3..b141ec493 100644 --- a/frontend/src/components/shared/RightRail.tsx +++ b/frontend/src/components/shared/RightRail.tsx @@ -25,7 +25,7 @@ export default function RightRail() { const [csvInput, setCsvInput] = useState(""); // Navigation view - const { currentMode: currentView } = useNavigationState(); + const { workbench: currentView } = useNavigationState(); // File state and selection const { state, selectors } = useFileState(); diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index 1a9dbbd9f..b5f5ff629 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -5,7 +5,7 @@ import rainbowStyles from '../../styles/rainbow.module.css'; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditNoteIcon from "@mui/icons-material/EditNote"; import FolderIcon from "@mui/icons-material/Folder"; -import { ModeType, isValidMode } from '../../contexts/NavigationContext'; +import { WorkbenchType, isValidWorkbench } from '../../types/navigation'; import { Tooltip } from "./Tooltip"; const viewOptionStyle = { @@ -19,7 +19,7 @@ const viewOptionStyle = { // Build view options showing text only for current view; others icon-only with tooltip -const createViewOptions = (currentView: ModeType, switchingTo: ModeType | null) => [ +const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => [ { label: (
@@ -70,8 +70,8 @@ const createViewOptions = (currentView: ModeType, switchingTo: ModeType | null) ]; interface TopControlsProps { - currentView: ModeType; - setCurrentView: (view: ModeType) => void; + currentView: WorkbenchType; + setCurrentView: (view: WorkbenchType) => void; selectedToolKey?: string | null; } @@ -81,25 +81,25 @@ const TopControls = ({ selectedToolKey, }: TopControlsProps) => { const { isRainbowMode } = useRainbowThemeContext(); - const [switchingTo, setSwitchingTo] = useState(null); + const [switchingTo, setSwitchingTo] = useState(null); const isToolSelected = selectedToolKey !== null; const handleViewChange = useCallback((view: string) => { - if (!isValidMode(view)) { - // Ignore invalid values defensively + if (!isValidWorkbench(view)) { return; } - const mode = view as ModeType; + + const workbench = view; // Show immediate feedback - setSwitchingTo(mode as ModeType); + setSwitchingTo(workbench); // Defer the heavy view change to next frame so spinner can render requestAnimationFrame(() => { // Give the spinner one more frame to show requestAnimationFrame(() => { - setCurrentView(mode as ModeType); + setCurrentView(workbench); // Clear the loading state after view change completes setTimeout(() => setSwitchingTo(null), 300); diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx index 32c7a86d0..535ae1fa1 100644 --- a/frontend/src/contexts/NavigationContext.tsx +++ b/frontend/src/contexts/NavigationContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useReducer, useCallback } from 'react'; -import { useNavigationUrlSync } from '../hooks/useUrlSync'; -import { ModeType, isValidMode, getDefaultMode } from '../types/navigation'; +import { WorkbenchType, ToolId, getDefaultWorkbench } from '../types/navigation'; +import { useFlatToolRegistry } from '../data/useTranslatedToolRegistry'; /** * NavigationContext - Complete navigation management system @@ -11,27 +11,38 @@ import { ModeType, isValidMode, getDefaultMode } from '../types/navigation'; */ // Navigation state -interface NavigationState { - currentMode: ModeType; +interface NavigationContextState { + workbench: WorkbenchType; + selectedTool: ToolId | null; hasUnsavedChanges: boolean; pendingNavigation: (() => void) | null; showNavigationWarning: boolean; - selectedToolKey: string | null; // Add tool selection to navigation state } // Navigation actions type NavigationAction = - | { type: 'SET_MODE'; payload: { mode: ModeType } } + | { type: 'SET_WORKBENCH'; payload: { workbench: WorkbenchType } } + | { type: 'SET_SELECTED_TOOL'; payload: { toolId: ToolId | null } } + | { type: 'SET_TOOL_AND_WORKBENCH'; payload: { toolId: ToolId | null; workbench: WorkbenchType } } | { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } } | { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } } - | { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } } - | { type: 'SET_SELECTED_TOOL'; payload: { toolKey: string | null } }; + | { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } }; // Navigation reducer -const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => { +const navigationReducer = (state: NavigationContextState, action: NavigationAction): NavigationContextState => { switch (action.type) { - case 'SET_MODE': - return { ...state, currentMode: action.payload.mode }; + case 'SET_WORKBENCH': + return { ...state, workbench: action.payload.workbench }; + + case 'SET_SELECTED_TOOL': + return { ...state, selectedTool: action.payload.toolId }; + + case 'SET_TOOL_AND_WORKBENCH': + return { + ...state, + selectedTool: action.payload.toolId, + workbench: action.payload.workbench + }; case 'SET_UNSAVED_CHANGES': return { ...state, hasUnsavedChanges: action.payload.hasChanges }; @@ -42,43 +53,41 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na case 'SHOW_NAVIGATION_WARNING': return { ...state, showNavigationWarning: action.payload.show }; - case 'SET_SELECTED_TOOL': - return { ...state, selectedToolKey: action.payload.toolKey }; - default: return state; } }; // Initial state -const initialState: NavigationState = { - currentMode: getDefaultMode(), +const initialState: NavigationContextState = { + workbench: getDefaultWorkbench(), + selectedTool: null, hasUnsavedChanges: false, pendingNavigation: null, - showNavigationWarning: false, - selectedToolKey: null + showNavigationWarning: false }; // Navigation context actions interface export interface NavigationContextActions { - setMode: (mode: ModeType) => void; + setWorkbench: (workbench: WorkbenchType) => void; + setSelectedTool: (toolId: ToolId | null) => void; + setToolAndWorkbench: (toolId: ToolId | null, workbench: WorkbenchType) => void; setHasUnsavedChanges: (hasChanges: boolean) => void; showNavigationWarning: (show: boolean) => void; requestNavigation: (navigationFn: () => void) => void; confirmNavigation: () => void; cancelNavigation: () => void; - selectTool: (toolKey: string) => void; clearToolSelection: () => void; handleToolSelect: (toolId: string) => void; } -// Split context values +// Context state values export interface NavigationContextStateValue { - currentMode: ModeType; + workbench: WorkbenchType; + selectedTool: ToolId | null; hasUnsavedChanges: boolean; pendingNavigation: (() => void) | null; showNavigationWarning: boolean; - selectedToolKey: string | null; } export interface NavigationContextActionsValue { @@ -95,10 +104,19 @@ export const NavigationProvider: React.FC<{ enableUrlSync?: boolean; }> = ({ children, enableUrlSync = true }) => { const [state, dispatch] = useReducer(navigationReducer, initialState); + const toolRegistry = useFlatToolRegistry(); const actions: NavigationContextActions = { - setMode: useCallback((mode: ModeType) => { - dispatch({ type: 'SET_MODE', payload: { mode } }); + setWorkbench: useCallback((workbench: WorkbenchType) => { + dispatch({ type: 'SET_WORKBENCH', payload: { workbench } }); + }, []), + + 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 } }); }, []), setHasUnsavedChanges: useCallback((hasChanges: boolean) => { @@ -110,77 +128,64 @@ export const NavigationProvider: React.FC<{ }, []), requestNavigation: useCallback((navigationFn: () => void) => { - // If no unsaved changes, navigate immediately if (!state.hasUnsavedChanges) { navigationFn(); return; } - // Otherwise, store the navigation and show warning dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } }); }, [state.hasUnsavedChanges]), confirmNavigation: useCallback(() => { - // Execute pending navigation if (state.pendingNavigation) { state.pendingNavigation(); } - // Clear navigation state dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); }, [state.pendingNavigation]), cancelNavigation: useCallback(() => { - // Clear navigation without executing dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); }, []), - selectTool: useCallback((toolKey: string) => { - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey } }); - }, []), - clearToolSelection: useCallback(() => { - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); - dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } }); + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } }); }, []), handleToolSelect: useCallback((toolId: string) => { - // Handle special cases if (toolId === 'allTools') { - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); - dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } }); + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } }); return; } - // Special-case: if tool is a dedicated reader tool, enter reader mode if (toolId === 'read' || toolId === 'view-pdf') { - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: 'viewer' } }); return; } - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: toolId } }); - dispatch({ type: 'SET_MODE', payload: { mode: 'fileEditor' as ModeType } }); - }, []) + // Look up the tool in the registry to get its proper workbench + const tool = toolRegistry[toolId]; + const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench(); + + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } }); + }, [toolRegistry]) }; const stateValue: NavigationContextStateValue = { - currentMode: state.currentMode, + workbench: state.workbench, + selectedTool: state.selectedTool, hasUnsavedChanges: state.hasUnsavedChanges, pendingNavigation: state.pendingNavigation, - showNavigationWarning: state.showNavigationWarning, - selectedToolKey: state.selectedToolKey + showNavigationWarning: state.showNavigationWarning }; const actionsValue: NavigationContextActionsValue = { actions }; - // Enable URL synchronization - useNavigationUrlSync(state.currentMode, actions.setMode, enableUrlSync); - return ( @@ -231,9 +236,6 @@ export const useNavigationGuard = () => { }; }; -// Re-export utility functions from types for backward compatibility -export { isValidMode, getDefaultMode, type ModeType } from '../types/navigation'; - // TODO: This will be expanded for URL-based routing system // - URL parsing utilities // - Route definitions diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 2ab92572b..6da6f5c3e 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -7,8 +7,8 @@ import React, { createContext, useContext, useReducer, useCallback, useMemo } fr import { useToolManagement } from '../hooks/useToolManagement'; import { PageEditorFunctions } from '../types/pageEditor'; import { ToolRegistryEntry } from '../data/toolsTaxonomy'; -import { useToolWorkflowUrlSync } from '../hooks/useUrlSync'; import { useNavigationActions, useNavigationState } from './NavigationContext'; +import { useNavigationUrlSync } from '../hooks/useUrlSync'; // State interface interface ToolWorkflowState { @@ -124,7 +124,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { } = useToolManagement(); // Get selected tool from navigation context - const selectedTool = getSelectedTool(navigationState.selectedToolKey); + const selectedTool = getSelectedTool(navigationState.selectedTool); // UI Action creators const setSidebarsVisible = useCallback((visible: boolean) => { @@ -142,7 +142,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { const setPreviewFile = useCallback((file: File | null) => { dispatch({ type: 'SET_PREVIEW_FILE', payload: file }); if (file) { - actions.setMode('viewer'); + actions.setWorkbench('viewer'); } }, [actions]); @@ -172,7 +172,16 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { // Workflow actions (compound actions that coordinate multiple state changes) const handleToolSelect = useCallback((toolId: string) => { - actions.handleToolSelect(toolId); + // Set the selected tool and determine the appropriate workbench + actions.setSelectedTool(toolId); + + // Get the tool from registry to determine workbench + const tool = getSelectedTool(toolId); + if (tool && tool.workbench) { + actions.setWorkbench(tool.workbench); + } else { + actions.setWorkbench('fileEditor'); // Default workbench + } // Clear search query when selecting a tool setSearchQuery(''); @@ -189,13 +198,13 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setLeftPanelView('toolContent'); setReaderMode(false); // Disable read mode when selecting tools } - }, [actions, setLeftPanelView, setReaderMode, setSearchQuery]); + }, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]); const handleBackToTools = useCallback(() => { setLeftPanelView('toolPicker'); setReaderMode(false); - actions.clearToolSelection(); - }, [setLeftPanelView, setReaderMode, actions]); + actions.setSelectedTool(null); + }, [setLeftPanelView, setReaderMode, actions.setSelectedTool]); const handleReaderToggle = useCallback(() => { setReaderMode(true); @@ -214,14 +223,23 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { [state.sidebarsVisible, state.readerMode] ); - // Enable URL synchronization for tool selection - useToolWorkflowUrlSync(navigationState.selectedToolKey, actions.selectTool, actions.clearToolSelection, true); + // URL sync for proper tool navigation + useNavigationUrlSync( + navigationState.workbench, + navigationState.selectedTool, + actions.setWorkbench, + actions.setSelectedTool, + handleToolSelect, + () => actions.setSelectedTool(null), + toolRegistry, + true + ); // Properly memoized context value const contextValue = useMemo((): ToolWorkflowContextValue => ({ // State ...state, - selectedToolKey: navigationState.selectedToolKey, + selectedToolKey: navigationState.selectedTool, selectedTool, toolRegistry, @@ -232,8 +250,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setPreviewFile, setPageEditorFunctions, setSearchQuery, - selectTool: actions.selectTool, - clearToolSelection: actions.clearToolSelection, + selectTool: actions.setSelectedTool, + clearToolSelection: () => actions.setSelectedTool(null), // Tool Reset Actions registerToolReset, @@ -249,7 +267,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { isPanelVisible, }), [ state, - navigationState.selectedToolKey, + navigationState.selectedTool, selectedTool, toolRegistry, setSidebarsVisible, @@ -258,8 +276,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setPreviewFile, setPageEditorFunctions, setSearchQuery, - actions.selectTool, - actions.clearToolSelection, + actions.setSelectedTool, registerToolReset, resetTool, handleToolSelect, diff --git a/frontend/src/data/toolsTaxonomy.ts b/frontend/src/data/toolsTaxonomy.ts index 0892b8b6c..95fbfa612 100644 --- a/frontend/src/data/toolsTaxonomy.ts +++ b/frontend/src/data/toolsTaxonomy.ts @@ -3,6 +3,7 @@ import React from 'react'; import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation'; import { BaseToolProps } from '../types/tool'; import { BaseParameters } from '../types/parameters'; +import { WorkbenchType } from '../types/navigation'; export enum SubcategoryId { SIGNING = 'signing', @@ -28,7 +29,6 @@ export type ToolRegistryEntry = { icon: React.ReactNode; name: string; component: React.ComponentType | null; - view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external'; description: string; categoryId: ToolCategoryId; subcategoryId: SubcategoryId; @@ -37,6 +37,10 @@ export type ToolRegistryEntry = { endpoints?: string[]; link?: string; type?: string; + // URL path for routing (e.g., '/split-pdfs', '/compress-pdf') + urlPath?: string; + // Workbench type for navigation + workbench?: WorkbenchType; // Operation configuration for automation operationConfig?: ToolOperationConfig; // Settings component for automation configuration @@ -107,3 +111,30 @@ export const getAllApplicationEndpoints = ( const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : []; return Array.from(new Set([...toolEp, ...convEp])); }; + +/** + * Default workbench for tools that don't specify one + * Returns null to trigger the default case in Workbench component (ToolRenderer) + */ +export const getDefaultToolWorkbench = (): WorkbenchType => 'fileEditor'; + +/** + * Get workbench type for a tool + */ +export const getToolWorkbench = (tool: ToolRegistryEntry): WorkbenchType => { + return tool.workbench || getDefaultToolWorkbench(); +}; + +/** + * Get URL path for a tool + */ +export const getToolUrlPath = (toolId: string, tool: ToolRegistryEntry): string => { + return tool.urlPath || `/${toolId.replace(/([A-Z])/g, '-$1').toLowerCase()}`; +}; + +/** + * Check if a tool ID exists in the registry + */ +export const isValidToolId = (toolId: string, registry: ToolRegistry): boolean => { + return toolId in registry; +}; diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 4e441f647..d1628a76e 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -1,66 +1,127 @@ -import React, { useMemo } from 'react'; -import LocalIcon from '../components/shared/LocalIcon'; -import { useTranslation } from 'react-i18next'; +import React, { useMemo } from "react"; +import LocalIcon from "../components/shared/LocalIcon"; +import { useTranslation } from "react-i18next"; import SplitPdfPanel from "../tools/Split"; import CompressPdfPanel from "../tools/Compress"; -import OCRPanel from '../tools/OCR'; -import ConvertPanel from '../tools/Convert'; -import Sanitize from '../tools/Sanitize'; -import AddPassword from '../tools/AddPassword'; -import ChangePermissions from '../tools/ChangePermissions'; -import RemovePassword from '../tools/RemovePassword'; -import { SubcategoryId, ToolCategoryId, ToolRegistry } from './toolsTaxonomy'; -import AddWatermark from '../tools/AddWatermark'; -import Repair from '../tools/Repair'; -import SingleLargePage from '../tools/SingleLargePage'; -import UnlockPdfForms from '../tools/UnlockPdfForms'; -import RemoveCertificateSign from '../tools/RemoveCertificateSign'; -import { compressOperationConfig } from '../hooks/tools/compress/useCompressOperation'; -import { splitOperationConfig } from '../hooks/tools/split/useSplitOperation'; -import { addPasswordOperationConfig } from '../hooks/tools/addPassword/useAddPasswordOperation'; -import { removePasswordOperationConfig } from '../hooks/tools/removePassword/useRemovePasswordOperation'; -import { sanitizeOperationConfig } from '../hooks/tools/sanitize/useSanitizeOperation'; -import { repairOperationConfig } from '../hooks/tools/repair/useRepairOperation'; -import { addWatermarkOperationConfig } from '../hooks/tools/addWatermark/useAddWatermarkOperation'; -import { unlockPdfFormsOperationConfig } from '../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation'; -import { singleLargePageOperationConfig } from '../hooks/tools/singleLargePage/useSingleLargePageOperation'; -import { ocrOperationConfig } from '../hooks/tools/ocr/useOCROperation'; -import { convertOperationConfig } from '../hooks/tools/convert/useConvertOperation'; -import { removeCertificateSignOperationConfig } from '../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation'; -import { changePermissionsOperationConfig } from '../hooks/tools/changePermissions/useChangePermissionsOperation'; -import CompressSettings from '../components/tools/compress/CompressSettings'; -import SplitSettings from '../components/tools/split/SplitSettings'; -import AddPasswordSettings from '../components/tools/addPassword/AddPasswordSettings'; -import RemovePasswordSettings from '../components/tools/removePassword/RemovePasswordSettings'; -import SanitizeSettings from '../components/tools/sanitize/SanitizeSettings'; -import RepairSettings from '../components/tools/repair/RepairSettings'; -import UnlockPdfFormsSettings from '../components/tools/unlockPdfForms/UnlockPdfFormsSettings'; -import AddWatermarkSingleStepSettings from '../components/tools/addWatermark/AddWatermarkSingleStepSettings'; -import OCRSettings from '../components/tools/ocr/OCRSettings'; -import ConvertSettings from '../components/tools/convert/ConvertSettings'; -import ChangePermissionsSettings from '../components/tools/changePermissions/ChangePermissionsSettings'; +import OCRPanel from "../tools/OCR"; +import ConvertPanel from "../tools/Convert"; +import Sanitize from "../tools/Sanitize"; +import AddPassword from "../tools/AddPassword"; +import ChangePermissions from "../tools/ChangePermissions"; +import RemovePassword from "../tools/RemovePassword"; +import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy"; +import AddWatermark from "../tools/AddWatermark"; +import Repair from "../tools/Repair"; +import SingleLargePage from "../tools/SingleLargePage"; +import UnlockPdfForms from "../tools/UnlockPdfForms"; +import RemoveCertificateSign from "../tools/RemoveCertificateSign"; +import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation"; +import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation"; +import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation"; +import { removePasswordOperationConfig } from "../hooks/tools/removePassword/useRemovePasswordOperation"; +import { sanitizeOperationConfig } from "../hooks/tools/sanitize/useSanitizeOperation"; +import { repairOperationConfig } from "../hooks/tools/repair/useRepairOperation"; +import { addWatermarkOperationConfig } from "../hooks/tools/addWatermark/useAddWatermarkOperation"; +import { unlockPdfFormsOperationConfig } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation"; +import { singleLargePageOperationConfig } from "../hooks/tools/singleLargePage/useSingleLargePageOperation"; +import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation"; +import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation"; +import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation"; +import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; +import CompressSettings from "../components/tools/compress/CompressSettings"; +import SplitSettings from "../components/tools/split/SplitSettings"; +import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings"; +import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings"; +import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings"; +import RepairSettings from "../components/tools/repair/RepairSettings"; +import UnlockPdfFormsSettings from "../components/tools/unlockPdfForms/UnlockPdfFormsSettings"; +import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/AddWatermarkSingleStepSettings"; +import OCRSettings from "../components/tools/ocr/OCRSettings"; +import ConvertSettings from "../components/tools/convert/ConvertSettings"; +import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings"; const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI // Convert tool supported file formats export const CONVERT_SUPPORTED_FORMATS = [ - // Microsoft Office - "doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx", - // OpenDocument - "odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg", - // Text formats - "txt", "text", "xml", "rtf", "html", "lwp", "md", - // Images - "bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp", - // StarOffice - "sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw", - // Email formats - "eml", - // Archive formats - "zip", - // Other - "dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf" - ]; + // Microsoft Office + "doc", + "docx", + "dot", + "dotx", + "csv", + "xls", + "xlsx", + "xlt", + "xltx", + "slk", + "dif", + "ppt", + "pptx", + // OpenDocument + "odt", + "ott", + "ods", + "ots", + "odp", + "otp", + "odg", + "otg", + // Text formats + "txt", + "text", + "xml", + "rtf", + "html", + "lwp", + "md", + // Images + "bmp", + "gif", + "jpeg", + "jpg", + "png", + "tif", + "tiff", + "pbm", + "pgm", + "ppm", + "ras", + "xbm", + "xpm", + "svg", + "svm", + "wmf", + "webp", + // StarOffice + "sda", + "sdc", + "sdd", + "sdw", + "stc", + "std", + "sti", + "stw", + "sxd", + "sxg", + "sxi", + "sxw", + // Email formats + "eml", + // Archive formats + "zip", + // Other + "dbf", + "fods", + "vsd", + "vor", + "vor3", + "vor4", + "uop", + "pct", + "ps", + "pdf", +]; // Hook to get the translated tool registry export function useFlatToolRegistry(): ToolRegistry { @@ -68,617 +129,578 @@ export function useFlatToolRegistry(): ToolRegistry { return useMemo(() => { const allTools: ToolRegistry = { - // Signing + // Signing - "certSign": { + certSign: { icon: , name: t("home.certSign.title", "Sign with Certificate"), component: null, - view: "sign", description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.SIGNING - }, - "sign": { + subcategoryId: SubcategoryId.SIGNING, + }, + sign: { icon: , name: t("home.sign.title", "Sign"), component: null, - view: "sign", description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.SIGNING - }, + subcategoryId: SubcategoryId.SIGNING, + }, + // Document Security - // Document Security - - "addPassword": { + addPassword: { icon: , name: t("home.addPassword.title", "Add Password"), component: AddPassword, - view: "security", description: t("home.addPassword.desc", "Add password protection and restrictions to PDF files"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"], operationConfig: addPasswordOperationConfig, - settingsComponent: AddPasswordSettings - }, - "watermark": { + settingsComponent: AddPasswordSettings, + }, + watermark: { icon: , name: t("home.watermark.title", "Add Watermark"), component: AddWatermark, - view: "format", maxFiles: -1, description: t("home.watermark.desc", "Add a custom watermark to your PDF document."), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, endpoints: ["add-watermark"], operationConfig: addWatermarkOperationConfig, - settingsComponent: AddWatermarkSingleStepSettings - }, - "add-stamp": { + settingsComponent: AddWatermarkSingleStepSettings, + }, + "add-stamp": { icon: , name: t("home.AddStampRequest.title", "Add Stamp to PDF"), component: null, - view: "format", description: t("home.AddStampRequest.desc", "Add text or add image stamps at set locations"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_SECURITY - }, - "sanitize": { + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, + }, + sanitize: { icon: , name: t("home.sanitize.title", "Sanitize"), component: Sanitize, - view: "security", maxFiles: -1, categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"), endpoints: ["sanitize-pdf"], operationConfig: sanitizeOperationConfig, - settingsComponent: SanitizeSettings - }, - "flatten": { + settingsComponent: SanitizeSettings, + }, + flatten: { icon: , name: t("home.flatten.title", "Flatten"), component: null, - view: "format", description: t("home.flatten.desc", "Remove all interactive elements and forms from a PDF"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_SECURITY - }, - "unlock-pdf-forms": { + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, + }, + "unlock-pdf-forms": { icon: , name: t("home.unlockPDFForms.title", "Unlock PDF Forms"), component: UnlockPdfForms, - view: "security", description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["unlock-pdf-forms"], operationConfig: unlockPdfFormsOperationConfig, - settingsComponent: UnlockPdfFormsSettings - }, - "manage-certificates": { + settingsComponent: UnlockPdfFormsSettings, + }, + "manage-certificates": { icon: , name: t("home.manageCertificates.title", "Manage Certificates"), component: null, - view: "security", - description: t("home.manageCertificates.desc", "Import, export, or delete digital certificate files used for signing PDFs."), + description: t( + "home.manageCertificates.desc", + "Import, export, or delete digital certificate files used for signing PDFs." + ), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_SECURITY - }, - "change-permissions": { + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, + }, + "change-permissions": { icon: , name: t("home.changePermissions.title", "Change Permissions"), component: ChangePermissions, - view: "security", description: t("home.changePermissions.desc", "Change document restrictions and permissions"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"], operationConfig: changePermissionsOperationConfig, - settingsComponent: ChangePermissionsSettings - }, - // Verification + settingsComponent: ChangePermissionsSettings, + }, + // Verification - "get-all-info-on-pdf": { + "get-all-info-on-pdf": { icon: , name: t("home.getPdfInfo.title", "Get ALL Info on PDF"), component: null, - view: "extract", description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.VERIFICATION - }, - "validate-pdf-signature": { + subcategoryId: SubcategoryId.VERIFICATION, + }, + "validate-pdf-signature": { icon: , name: t("home.validateSignature.title", "Validate PDF Signature"), component: null, - view: "security", description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.VERIFICATION - }, + subcategoryId: SubcategoryId.VERIFICATION, + }, + // Document Review - // Document Review - - "read": { + read: { icon: , name: t("home.read.title", "Read"), component: null, - view: "view", - description: t("home.read.desc", "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."), + workbench: "viewer", + description: t( + "home.read.desc", + "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration." + ), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_REVIEW - }, - "change-metadata": { + subcategoryId: SubcategoryId.DOCUMENT_REVIEW, + }, + "change-metadata": { icon: , name: t("home.changeMetadata.title", "Change Metadata"), component: null, - view: "format", description: t("home.changeMetadata.desc", "Change/Remove/Add metadata from a PDF document"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_REVIEW - }, - // Page Formatting + subcategoryId: SubcategoryId.DOCUMENT_REVIEW, + }, + // Page Formatting - "cropPdf": { + cropPdf: { icon: , name: t("home.crop.title", "Crop PDF"), component: null, - view: "format", + description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "rotate": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + rotate: { icon: , name: t("home.rotate.title", "Rotate"), component: null, - view: "format", + description: t("home.rotate.desc", "Easily rotate your PDFs."), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "splitPdf": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + splitPdf: { icon: , name: t("home.split.title", "Split"), component: SplitPdfPanel, - view: "split", + description: t("home.split.desc", "Split PDFs into multiple documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, operationConfig: splitOperationConfig, - settingsComponent: SplitSettings - }, - "reorganize-pages": { + settingsComponent: SplitSettings, + }, + "reorganize-pages": { icon: , name: t("home.reorganizePages.title", "Reorganize Pages"), component: null, - view: "pageEditor", - description: t("home.reorganizePages.desc", "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."), + workbench: "pageEditor", + description: t( + "home.reorganizePages.desc", + "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control." + ), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "adjust-page-size-scale": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + "adjust-page-size-scale": { icon: , name: t("home.scalePages.title", "Adjust page size/scale"), component: null, - view: "format", + description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "addPageNumbers": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + addPageNumbers: { icon: , name: t("home.addPageNumbers.title", "Add Page Numbers"), component: null, - view: "format", + description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "multi-page-layout": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + "multi-page-layout": { icon: , name: t("home.pageLayout.title", "Multi-Page Layout"), component: null, - view: "format", + description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "single-large-page": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + "single-large-page": { icon: , name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"), component: SingleLargePage, - view: "format", + description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["pdf-to-single-page"], - operationConfig: singleLargePageOperationConfig - }, - "add-attachments": { + operationConfig: singleLargePageOperationConfig, + }, + "add-attachments": { icon: , name: t("home.attachments.title", "Add Attachments"), component: null, - view: "format", + description: t("home.attachments.desc", "Add or remove embedded files (attachments) to/from a PDF"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, - }, + }, + // Extraction - // Extraction - - "extractPages": { + extractPages: { icon: , name: t("home.extractPages.title", "Extract Pages"), component: null, - view: "extract", description: t("home.extractPages.desc", "Extract specific pages from a PDF document"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.EXTRACTION - }, - "extract-images": { + subcategoryId: SubcategoryId.EXTRACTION, + }, + "extract-images": { icon: , name: t("home.extractImages.title", "Extract Images"), component: null, - view: "extract", description: t("home.extractImages.desc", "Extract images from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.EXTRACTION - }, + subcategoryId: SubcategoryId.EXTRACTION, + }, + // Removal - // Removal - - "removePages": { + removePages: { icon: , name: t("home.removePages.title", "Remove Pages"), component: null, - view: "remove", description: t("home.removePages.desc", "Remove specific pages from a PDF document"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.REMOVAL - }, - "remove-blank-pages": { + subcategoryId: SubcategoryId.REMOVAL, + }, + "remove-blank-pages": { icon: , name: t("home.removeBlanks.title", "Remove Blank Pages"), component: null, - view: "remove", description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.REMOVAL - }, - "remove-annotations": { + subcategoryId: SubcategoryId.REMOVAL, + }, + "remove-annotations": { icon: , name: t("home.removeAnnotations.title", "Remove Annotations"), component: null, - view: "remove", description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.REMOVAL - }, - "remove-image": { + subcategoryId: SubcategoryId.REMOVAL, + }, + "remove-image": { icon: , name: t("home.removeImagePdf.title", "Remove Image"), component: null, - view: "format", description: t("home.removeImagePdf.desc", "Remove images from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.REMOVAL - }, - "remove-password": { + subcategoryId: SubcategoryId.REMOVAL, + }, + "remove-password": { icon: , name: t("home.removePassword.title", "Remove Password"), component: RemovePassword, - view: "security", description: t("home.removePassword.desc", "Remove password protection from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.REMOVAL, endpoints: ["remove-password"], maxFiles: -1, operationConfig: removePasswordOperationConfig, - settingsComponent: RemovePasswordSettings - }, - "remove-certificate-sign": { + settingsComponent: RemovePasswordSettings, + }, + "remove-certificate-sign": { icon: , name: t("home.removeCertSign.title", "Remove Certificate Sign"), component: RemoveCertificateSign, - view: "security", description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-certificate-sign"], - operationConfig: removeCertificateSignOperationConfig - }, + operationConfig: removeCertificateSignOperationConfig, + }, + // Automation - // Automation - - "automate": { + automate: { icon: , name: t("home.automate.title", "Automate"), - component: React.lazy(() => import('../tools/Automate')), - view: "format", - description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."), + component: React.lazy(() => import("../tools/Automate")), + description: t( + "home.automate.desc", + "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks." + ), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.AUTOMATION, maxFiles: -1, supportedFormats: CONVERT_SUPPORTED_FORMATS, - endpoints: ["handleData"] - }, - "auto-rename-pdf-file": { + endpoints: ["handleData"], + }, + "auto-rename-pdf-file": { icon: , name: t("home.auto-rename.title", "Auto Rename PDF File"), component: null, - view: "format", description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.AUTOMATION - }, - "auto-split-pages": { + subcategoryId: SubcategoryId.AUTOMATION, + }, + "auto-split-pages": { icon: , name: t("home.autoSplitPDF.title", "Auto Split Pages"), component: null, - view: "format", description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.AUTOMATION - }, - "auto-split-by-size-count": { + subcategoryId: SubcategoryId.AUTOMATION, + }, + "auto-split-by-size-count": { icon: , name: t("home.autoSizeSplitPDF.title", "Auto Split by Size/Count"), component: null, - view: "format", description: t("home.autoSizeSplitPDF.desc", "Automatically split PDFs by file size or page count"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.AUTOMATION - }, + subcategoryId: SubcategoryId.AUTOMATION, + }, + // Advanced Formatting - // Advanced Formatting - - "adjustContrast": { + adjustContrast: { icon: , name: t("home.adjustContrast.title", "Adjust Colors/Contrast"), component: null, - view: "format", description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "repair": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + repair: { icon: , name: t("home.repair.title", "Repair"), component: Repair, - view: "format", description: t("home.repair.desc", "Repair corrupted or damaged PDF files"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["repair"], operationConfig: repairOperationConfig, - settingsComponent: RepairSettings - }, - "detect-split-scanned-photos": { + settingsComponent: RepairSettings, + }, + "detect-split-scanned-photos": { icon: , name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"), component: null, - view: "format", description: t("home.ScannerImageSplit.desc", "Detect and split scanned photos into separate pages"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "overlay-pdfs": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "overlay-pdfs": { icon: , name: t("home.overlay-pdfs.title", "Overlay PDFs"), component: null, - view: "format", description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "replace-and-invert-color": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "replace-and-invert-color": { icon: , name: t("home.replaceColorPdf.title", "Replace & Invert Color"), component: null, - view: "format", description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "add-image": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "add-image": { icon: , name: t("home.addImage.title", "Add Image"), component: null, - view: "format", description: t("home.addImage.desc", "Add images to PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "edit-table-of-contents": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "edit-table-of-contents": { icon: , name: t("home.editTableOfContents.title", "Edit Table of Contents"), component: null, - view: "format", description: t("home.editTableOfContents.desc", "Add or edit bookmarks and table of contents in PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "scanner-effect": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "scanner-effect": { icon: , name: t("home.fakeScan.title", "Scanner Effect"), component: null, - view: "format", description: t("home.fakeScan.desc", "Create a PDF that looks like it was scanned"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + // Developer Tools - // Developer Tools - - "show-javascript": { + "show-javascript": { icon: , name: t("home.showJS.title", "Show JavaScript"), component: null, - view: "extract", description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.DEVELOPER_TOOLS - }, - "dev-api": { - icon: , + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, + }, + "dev-api": { + icon: , name: t("home.devApi.title", "API"), component: null, - view: "external", description: t("home.devApi.desc", "Link to API documentation"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, - link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html" - }, - "dev-folder-scanning": { - icon: , + link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html", + }, + "dev-folder-scanning": { + icon: , name: t("home.devFolderScanning.title", "Automated Folder Scanning"), component: null, - view: "external", description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, - link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/" - }, - "dev-sso-guide": { - icon: , + link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/", + }, + "dev-sso-guide": { + icon: , name: t("home.devSsoGuide.title", "SSO Guide"), component: null, - view: "external", description: t("home.devSsoGuide.desc", "Link to SSO guide"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration", - }, - "dev-airgapped": { - icon: , + }, + "dev-airgapped": { + icon: , name: t("home.devAirgapped.title", "Air-gapped Setup"), component: null, - view: "external", description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, - link: "https://docs.stirlingpdf.com/Pro/#activation" - }, + link: "https://docs.stirlingpdf.com/Pro/#activation", + }, - - // Recommended Tools - "compare": { + // Recommended Tools + compare: { icon: , name: t("home.compare.title", "Compare"), component: null, - view: "format", description: t("home.compare.desc", "Compare two PDF documents and highlight differences"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, - subcategoryId: SubcategoryId.GENERAL - }, - "compress": { + subcategoryId: SubcategoryId.GENERAL, + }, + compress: { icon: , name: t("home.compress.title", "Compress"), component: CompressPdfPanel, - view: "compress", description: t("home.compress.desc", "Compress PDFs to reduce their file size."), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, operationConfig: compressOperationConfig, - settingsComponent: CompressSettings - }, - "convert": { + settingsComponent: CompressSettings, + }, + convert: { icon: , name: t("home.convert.title", "Convert"), component: ConvertPanel, - view: "convert", description: t("home.convert.desc", "Convert files to and from PDF format"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, supportedFormats: CONVERT_SUPPORTED_FORMATS, endpoints: [ - "pdf-to-img", - "img-to-pdf", - "pdf-to-word", - "pdf-to-presentation", - "pdf-to-text", - "pdf-to-html", - "pdf-to-xml", - "html-to-pdf", - "markdown-to-pdf", - "file-to-pdf", - "pdf-to-csv", - "pdf-to-markdown", - "pdf-to-pdfa", - "eml-to-pdf" + "pdf-to-img", + "img-to-pdf", + "pdf-to-word", + "pdf-to-presentation", + "pdf-to-text", + "pdf-to-html", + "pdf-to-xml", + "html-to-pdf", + "markdown-to-pdf", + "file-to-pdf", + "pdf-to-csv", + "pdf-to-markdown", + "pdf-to-pdfa", + "eml-to-pdf", ], operationConfig: convertOperationConfig, - settingsComponent: ConvertSettings - }, - "mergePdfs": { + settingsComponent: ConvertSettings, + }, + mergePdfs: { icon: , name: t("home.merge.title", "Merge"), component: null, - view: "merge", + description: t("home.merge.desc", "Merge multiple PDFs into a single document"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, - maxFiles: -1 - }, - "multi-tool": { + maxFiles: -1, + }, + "multi-tool": { icon: , name: t("home.multiTool.title", "Multi-Tool"), component: null, - view: "pageEditor", + workbench: "pageEditor", description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, - maxFiles: -1 - }, - "ocr": { + maxFiles: -1, + }, + ocr: { icon: , name: t("home.ocr.title", "OCR"), component: OCRPanel, - view: "convert", description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, operationConfig: ocrOperationConfig, - settingsComponent: OCRSettings - }, - "redact": { + settingsComponent: OCRSettings, + }, + redact: { icon: , name: t("home.redact.title", "Redact"), component: null, - view: "redact", description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, - subcategoryId: SubcategoryId.GENERAL - }, - }; + subcategoryId: SubcategoryId.GENERAL, + }, + }; if (showPlaceholderTools) { return allTools; } const filteredTools = Object.keys(allTools) - .filter(key => allTools[key].component !== null || allTools[key].link) + .filter((key) => allTools[key].component !== null || allTools[key].link) .reduce((obj, key) => { obj[key] = allTools[key]; return obj; diff --git a/frontend/src/hooks/useSuggestedTools.ts b/frontend/src/hooks/useSuggestedTools.ts index f3f04ae34..e66e78779 100644 --- a/frontend/src/hooks/useSuggestedTools.ts +++ b/frontend/src/hooks/useSuggestedTools.ts @@ -45,16 +45,16 @@ const ALL_SUGGESTED_TOOLS: Omit[] = [ export function useSuggestedTools(): SuggestedTool[] { const { actions } = useNavigationActions(); - const { selectedToolKey } = useNavigationState(); + const { selectedTool } = useNavigationState(); return useMemo(() => { // Filter out the current tool - const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedToolKey); + const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedTool); // Add navigation function to each tool return filteredTools.map(tool => ({ ...tool, - navigate: () => actions.handleToolSelect(tool.id) + navigate: () => actions.setSelectedTool(tool.id) })); - }, [selectedToolKey, actions]); + }, [selectedTool, actions]); } diff --git a/frontend/src/hooks/useUrlSync.ts b/frontend/src/hooks/useUrlSync.ts index 93dabfa36..57eefe843 100644 --- a/frontend/src/hooks/useUrlSync.ts +++ b/frontend/src/hooks/useUrlSync.ts @@ -1,123 +1,90 @@ /** - * URL synchronization hooks for tool routing + * URL synchronization hooks for tool routing with registry support */ import { useEffect, useCallback } from 'react'; -import { ModeType } from '../types/navigation'; +import { WorkbenchType, ToolId, ToolRoute } from '../types/navigation'; import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting'; +import { ToolRegistry } from '../data/toolsTaxonomy'; /** - * Hook to sync navigation mode with URL + * Hook to sync workbench and tool with URL using registry */ export function useNavigationUrlSync( - currentMode: ModeType, - setMode: (mode: ModeType) => void, + workbench: WorkbenchType, + selectedTool: ToolId | null, + setWorkbench: (workbench: WorkbenchType) => void, + setSelectedTool: (toolId: ToolId | null) => void, + handleToolSelect: (toolId: string) => void, + clearToolSelection: () => void, + registry: ToolRegistry, enableSync: boolean = true ) { - // Initialize mode from URL on mount + // Initialize workbench and tool from URL on mount useEffect(() => { if (!enableSync) return; - const route = parseToolRoute(); - if (route.mode !== currentMode) { - setMode(route.mode); + const route = parseToolRoute(registry); + if (route.toolId !== selectedTool) { + if (route.toolId) { + handleToolSelect(route.toolId); + } else { + clearToolSelection(); + } } }, []); // Only run on mount - // Update URL when mode changes + // Update URL when tool or workbench changes useEffect(() => { if (!enableSync) return; - // Only update URL for actual tool modes, not internal UI modes - // URL clearing is handled by useToolWorkflowUrlSync when selectedToolKey becomes null - if (currentMode !== 'fileEditor' && currentMode !== 'pageEditor' && currentMode !== 'viewer') { - updateToolRoute(currentMode, currentMode); + if (selectedTool) { + updateToolRoute(selectedTool, registry); + } else { + // Clear URL when no tool is selected + clearToolRoute(); } - }, [currentMode, enableSync]); + }, [selectedTool, registry, enableSync]); // Handle browser back/forward navigation useEffect(() => { if (!enableSync) return; const handlePopState = () => { - const route = parseToolRoute(); - if (route.mode !== currentMode) { - setMode(route.mode); + const route = parseToolRoute(registry); + if (route.toolId !== selectedTool) { + if (route.toolId) { + handleToolSelect(route.toolId); + } else { + clearToolSelection(); + } } }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); - }, [currentMode, setMode, enableSync]); + }, [selectedTool, handleToolSelect, clearToolSelection, registry, enableSync]); } /** - * Hook to sync tool workflow with URL + * Hook to programmatically navigate to tools with registry support */ -export function useToolWorkflowUrlSync( - selectedToolKey: string | null, - selectTool: (toolKey: string) => void, - clearTool: () => void, - enableSync: boolean = true -) { - // Initialize tool from URL on mount - useEffect(() => { - if (!enableSync) return; - - const route = parseToolRoute(); - if (route.toolKey && route.toolKey !== selectedToolKey) { - selectTool(route.toolKey); - } else if (!route.toolKey && selectedToolKey) { - clearTool(); - } - }, []); // Only run on mount - - // Update URL when tool changes - useEffect(() => { - if (!enableSync) return; - - if (selectedToolKey) { - const route = parseToolRoute(); - if (route.toolKey !== selectedToolKey) { - updateToolRoute(selectedToolKey as ModeType, selectedToolKey); - } - } else { - // Clear URL when no tool is selected - always clear regardless of current URL - clearToolRoute(); - } - }, [selectedToolKey, enableSync]); -} - -/** - * Hook to get current URL route information - */ -export function useCurrentRoute() { - const getCurrentRoute = useCallback(() => { - return parseToolRoute(); - }, []); - - return getCurrentRoute; -} - -/** - * Hook to programmatically navigate to tools - */ -export function useToolNavigation() { - const navigateToTool = useCallback((toolKey: string) => { - updateToolRoute(toolKey as ModeType, toolKey); +export function useToolNavigation(registry: ToolRegistry) { + const navigateToTool = useCallback((toolId: ToolId) => { + updateToolRoute(toolId, registry); // Dispatch a custom event to notify other components window.dispatchEvent(new CustomEvent('toolNavigation', { - detail: { toolKey } + detail: { toolId } })); - }, []); + }, [registry]); const navigateToHome = useCallback(() => { clearToolRoute(); // Dispatch a custom event to notify other components window.dispatchEvent(new CustomEvent('toolNavigation', { - detail: { toolKey: null } + detail: { toolId: null } })); }, []); @@ -126,3 +93,14 @@ export function useToolNavigation() { navigateToHome }; } + +/** + * Hook to get current URL route information with registry support + */ +export function useCurrentRoute(registry: ToolRegistry) { + const getCurrentRoute = useCallback(() => { + return parseToolRoute(registry); + }, [registry]); + + return getCurrentRoute; +} \ No newline at end of file diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index c99b26db5..9443f0be6 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -8,7 +8,6 @@ import { BrowserRouter } from 'react-router-dom'; import App from './App'; import './i18n'; // Initialize i18next import { PostHogProvider } from 'posthog-js/react'; -import { ScarfPixel } from './components/ScarfPixel'; // Compute initial color scheme function getInitialScheme(): 'light' | 'dark' { @@ -40,7 +39,6 @@ root.render( }} > - diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index d5cea69c8..74af39d2e 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -2,7 +2,7 @@ import React, { useState, useMemo, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useFileContext } from "../contexts/FileContext"; import { useFileSelection } from "../contexts/FileContext"; -import { useNavigation } from "../contexts/NavigationContext"; +import { useNavigationActions } from "../contexts/NavigationContext"; import { useToolWorkflow } from "../contexts/ToolWorkflowContext"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; @@ -21,7 +21,7 @@ import { AUTOMATION_STEPS } from "../constants/automation"; const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); const { selectedFiles } = useFileSelection(); - const { setMode } = useNavigation(); + const { actions } = useNavigationActions(); const { registerToolReset } = useToolWorkflow(); const [currentStep, setCurrentStep] = useState(AUTOMATION_STEPS.SELECTION); @@ -223,7 +223,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { title: t('automate.reviewTitle', 'Automation Results'), onFileClick: (file: File) => { onPreviewFile?.(file); - setMode('viewer'); + actions.setWorkbench('viewer'); } } }); diff --git a/frontend/src/tools/RemoveCertificateSign.tsx b/frontend/src/tools/RemoveCertificateSign.tsx index c9a3f4b82..5d7501696 100644 --- a/frontend/src/tools/RemoveCertificateSign.tsx +++ b/frontend/src/tools/RemoveCertificateSign.tsx @@ -43,7 +43,7 @@ const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolP const handleThumbnailClick = (file: File) => { onPreviewFile?.(file); sessionStorage.setItem("previousMode", "removeCertificateSign"); - actions.setMode("viewer"); + actions.setWorkbench("viewer"); }; const handleSettingsReset = () => { diff --git a/frontend/src/tools/Repair.tsx b/frontend/src/tools/Repair.tsx index f5f09017a..b36e5d7c3 100644 --- a/frontend/src/tools/Repair.tsx +++ b/frontend/src/tools/Repair.tsx @@ -43,7 +43,7 @@ const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const handleThumbnailClick = (file: File) => { onPreviewFile?.(file); sessionStorage.setItem("previousMode", "repair"); - actions.setMode("viewer"); + actions.setWorkbench("viewer"); }; const handleSettingsReset = () => { diff --git a/frontend/src/tools/SingleLargePage.tsx b/frontend/src/tools/SingleLargePage.tsx index d071c2b99..394c96da8 100644 --- a/frontend/src/tools/SingleLargePage.tsx +++ b/frontend/src/tools/SingleLargePage.tsx @@ -43,7 +43,7 @@ const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) const handleThumbnailClick = (file: File) => { onPreviewFile?.(file); sessionStorage.setItem("previousMode", "single-large-page"); - actions.setMode("viewer"); + actions.setWorkbench("viewer"); }; const handleSettingsReset = () => { diff --git a/frontend/src/tools/UnlockPdfForms.tsx b/frontend/src/tools/UnlockPdfForms.tsx index 6c5bd4cb5..56a39a490 100644 --- a/frontend/src/tools/UnlockPdfForms.tsx +++ b/frontend/src/tools/UnlockPdfForms.tsx @@ -43,7 +43,7 @@ const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) = const handleThumbnailClick = (file: File) => { onPreviewFile?.(file); sessionStorage.setItem("previousMode", "unlockPdfForms"); - actions.setMode("viewer"); + actions.setWorkbench("viewer"); }; const handleSettingsReset = () => { diff --git a/frontend/src/types/navigation.ts b/frontend/src/types/navigation.ts index 70d108c9a..4a58771f6 100644 --- a/frontend/src/types/navigation.ts +++ b/frontend/src/types/navigation.ts @@ -1,42 +1,31 @@ /** - * Shared navigation types to avoid circular dependencies + * Navigation types for workbench and tool separation */ -// Navigation mode types - complete list to match contexts -export type ModeType = - | 'viewer' - | 'pageEditor' - | 'fileEditor' - | 'merge' - | 'split' - | 'compress' - | 'ocr' - | 'convert' - | 'sanitize' - | 'addPassword' - | 'changePermissions' - | 'addWatermark' - | 'removePassword' - | 'single-large-page' - | 'repair' - | 'unlockPdfForms' - | 'removeCertificateSign'; +// Define workbench values once as source of truth +const WORKBENCH_TYPES = ['viewer', 'pageEditor', 'fileEditor'] as const; -// Utility functions for mode handling -export const isValidMode = (mode: string): mode is ModeType => { - const validModes: ModeType[] = [ - 'viewer', 'pageEditor', 'fileEditor', 'merge', 'split', - 'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', - 'sanitize', 'addWatermark', 'removePassword', 'single-large-page', - 'repair', 'unlockPdfForms', 'removeCertificateSign' - ]; - return validModes.includes(mode as ModeType); +// Workbench types - how the user interacts with content +export type WorkbenchType = typeof WORKBENCH_TYPES[number]; + +// Tool identity - what PDF operation we're performing (derived from registry) +export type ToolId = string; + +// Navigation state +export interface NavigationState { + workbench: WorkbenchType; + selectedTool: ToolId | null; +} + +export const getDefaultWorkbench = (): WorkbenchType => 'fileEditor'; + +// Type guard using the same source of truth - no duplication +export const isValidWorkbench = (value: string): value is WorkbenchType => { + return WORKBENCH_TYPES.includes(value as WorkbenchType); }; -export const getDefaultMode = (): ModeType => 'fileEditor'; - // Route parsing result export interface ToolRoute { - mode: ModeType; - toolKey: string | null; -} \ No newline at end of file + workbench: WorkbenchType; + toolId: ToolId | null; +} diff --git a/frontend/src/types/navigationActions.ts b/frontend/src/types/navigationActions.ts index b227dac9d..eb50708e6 100644 --- a/frontend/src/types/navigationActions.ts +++ b/frontend/src/types/navigationActions.ts @@ -2,10 +2,11 @@ * Navigation action interfaces to break circular dependencies */ -import { ModeType } from './navigation'; +import { WorkbenchType, ToolId } from './navigation'; export interface NavigationActions { - setMode: (mode: ModeType) => void; + setWorkbench: (workbench: WorkbenchType) => void; + setSelectedTool: (toolId: ToolId | null) => void; setHasUnsavedChanges: (hasChanges: boolean) => void; showNavigationWarning: (show: boolean) => void; requestNavigation: (navigationFn: () => void) => void; @@ -14,7 +15,8 @@ export interface NavigationActions { } export interface NavigationState { - currentMode: ModeType; + workbench: WorkbenchType; + selectedTool: ToolId | null; hasUnsavedChanges: boolean; pendingNavigation: (() => void) | null; showNavigationWarning: boolean; diff --git a/frontend/src/utils/urlRouting.ts b/frontend/src/utils/urlRouting.ts index 687350530..8432ff0a5 100644 --- a/frontend/src/utils/urlRouting.ts +++ b/frontend/src/utils/urlRouting.ts @@ -1,119 +1,64 @@ /** - * URL routing utilities for tool navigation - * Provides clean URL routing for the V2 tool system + * URL routing utilities for tool navigation with registry support */ -import { ModeType, isValidMode as isValidModeType, getDefaultMode, ToolRoute } from '../types/navigation'; +import { + ToolId, + ToolRoute, + getDefaultWorkbench +} from '../types/navigation'; +import { ToolRegistry, getToolWorkbench, getToolUrlPath, isValidToolId } from '../data/toolsTaxonomy'; /** * Parse the current URL to extract tool routing information */ -export function parseToolRoute(): ToolRoute { +export function parseToolRoute(registry: ToolRegistry): ToolRoute { const path = window.location.pathname; const searchParams = new URLSearchParams(window.location.search); - // Extract tool from URL path (e.g., /split-pdf -> split) - const toolMatch = path.match(/\/([a-zA-Z-]+)(?:-pdf)?$/); - if (toolMatch) { - const toolKey = toolMatch[1].toLowerCase(); - - // Map URL paths to tool keys and modes (excluding internal UI modes) - const toolMappings: Record = { - 'split-pdfs': { mode: 'split', toolKey: 'split' }, - 'split': { mode: 'split', toolKey: 'split' }, - 'merge-pdfs': { mode: 'merge', toolKey: 'merge' }, - 'compress-pdf': { mode: 'compress', toolKey: 'compress' }, - 'convert': { mode: 'convert', toolKey: 'convert' }, - 'convert-pdf': { mode: 'convert', toolKey: 'convert' }, - 'file-to-pdf': { mode: 'convert', toolKey: 'convert' }, - 'eml-to-pdf': { mode: 'convert', toolKey: 'convert' }, - 'html-to-pdf': { mode: 'convert', toolKey: 'convert' }, - 'markdown-to-pdf': { mode: 'convert', toolKey: 'convert' }, - 'pdf-to-csv': { mode: 'convert', toolKey: 'convert' }, - 'pdf-to-img': { mode: 'convert', toolKey: 'convert' }, - 'pdf-to-markdown': { mode: 'convert', toolKey: 'convert' }, - 'pdf-to-pdfa': { mode: 'convert', toolKey: 'convert' }, - 'pdf-to-word': { mode: 'convert', toolKey: 'convert' }, - 'pdf-to-xml': { mode: 'convert', toolKey: 'convert' }, - 'add-password': { mode: 'addPassword', toolKey: 'addPassword' }, - 'change-permissions': { mode: 'changePermissions', toolKey: 'changePermissions' }, - 'sanitize-pdf': { mode: 'sanitize', toolKey: 'sanitize' }, - 'ocr': { mode: 'ocr', toolKey: 'ocr' }, - 'ocr-pdf': { mode: 'ocr', toolKey: 'ocr' }, - 'add-watermark': { mode: 'addWatermark', toolKey: 'addWatermark' }, - 'remove-password': { mode: 'removePassword', toolKey: 'removePassword' }, - 'single-large-page': { mode: 'single-large-page', toolKey: 'single-large-page' }, - 'repair': { mode: 'repair', toolKey: 'repair' }, - 'unlock-pdf-forms': { mode: 'unlockPdfForms', toolKey: 'unlockPdfForms' }, - 'remove-certificate-sign': { mode: 'removeCertificateSign', toolKey: 'removeCertificateSign' }, - 'remove-cert-sign': { mode: 'removeCertificateSign', toolKey: 'removeCertificateSign' } - }; - - const mapping = toolMappings[toolKey]; - if (mapping) { + // Try to find tool by URL path + for (const [toolId, tool] of Object.entries(registry)) { + const toolUrlPath = getToolUrlPath(toolId, tool); + if (path === toolUrlPath) { return { - mode: mapping.mode, - toolKey: mapping.toolKey + workbench: getToolWorkbench(tool), + toolId }; } } // Check for query parameter fallback (e.g., ?tool=split) const toolParam = searchParams.get('tool'); - if (toolParam && isValidModeType(toolParam)) { + if (toolParam && isValidToolId(toolParam, registry)) { + const tool = registry[toolParam]; return { - mode: toolParam as ModeType, - toolKey: toolParam + workbench: getToolWorkbench(tool), + toolId: toolParam }; } - // Default to page editor for home page + // Default to fileEditor workbench for home page return { - mode: getDefaultMode(), - toolKey: null + workbench: getDefaultWorkbench(), + toolId: null }; } /** * Update the URL to reflect the current tool selection - * Internal UI modes (viewer, fileEditor, pageEditor) don't get URLs */ -export function updateToolRoute(mode: ModeType, toolKey?: string): void { +export function updateToolRoute(toolId: ToolId, registry: ToolRegistry): void { const currentPath = window.location.pathname; const searchParams = new URLSearchParams(window.location.search); - // Don't create URLs for internal UI modes - if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') { - // If we're switching to an internal mode, clear any existing tool URL - if (currentPath !== '/') { - clearToolRoute(); - } + const tool = registry[toolId]; + if (!tool) { + console.warn(`Tool ${toolId} not found in registry`); return; } - let newPath = '/'; - // Map modes to URL paths (only for actual tools) - if (toolKey) { - const pathMappings: Record = { - 'split': '/split-pdfs', - 'merge': '/merge-pdf', - 'compress': '/compress-pdf', - 'convert': '/convert-pdf', - 'addPassword': '/add-password-pdf', - 'changePermissions': '/change-permissions-pdf', - 'sanitize': '/sanitize-pdf', - 'ocr': '/ocr-pdf', - 'addWatermark': '/watermark', - 'removePassword': '/remove-password', - 'single-large-page': '/single-large-page', - 'repair': '/repair', - 'unlockPdfForms': '/unlock-pdf-forms', - 'removeCertificateSign': '/remove-certificate-sign' - }; - - newPath = pathMappings[toolKey] || `/${toolKey}`; - } + const newPath = getToolUrlPath(toolId, tool); // Remove tool query parameter since we're using path-based routing searchParams.delete('tool'); @@ -142,58 +87,25 @@ export function clearToolRoute(): void { } /** - * Get clean tool name for display purposes + * Get clean tool name for display purposes using registry */ -export function getToolDisplayName(toolKey: string): string { - const displayNames: Record = { - 'split': 'Split PDF', - 'merge': 'Merge PDF', - 'compress': 'Compress PDF', - 'convert': 'Convert PDF', - 'addPassword': 'Add Password', - 'changePermissions': 'Change Permissions', - 'sanitize': 'Sanitize PDF', - 'ocr': 'OCR PDF' - }; - - return displayNames[toolKey] || toolKey; +export function getToolDisplayName(toolId: ToolId, registry: ToolRegistry): string { + const tool = registry[toolId]; + return tool ? tool.name : toolId; } -// Note: isValidMode is now imported from types/navigation.ts - /** - * Generate shareable URL for current tool state - * Only generates URLs for actual tools, not internal UI modes + * Generate shareable URL for current tool state using registry */ -export function generateShareableUrl(mode: ModeType, toolKey?: string): string { +export function generateShareableUrl(toolId: ToolId | null, registry: ToolRegistry): string { const baseUrl = window.location.origin; - // Don't generate URLs for internal UI modes - if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') { + if (!toolId || !registry[toolId]) { return baseUrl; } - if (toolKey) { - const pathMappings: Record = { - 'split': '/split-pdf', - 'merge': '/merge-pdf', - 'compress': '/compress-pdf', - 'convert': '/convert-pdf', - 'addPassword': '/add-password-pdf', - 'changePermissions': '/change-permissions-pdf', - 'sanitize': '/sanitize-pdf', - 'ocr': '/ocr-pdf', - 'addWatermark': '/watermark', - 'removePassword': '/remove-password', - 'single-large-page': '/single-large-page', - 'repair': '/repair', - 'unlockPdfForms': '/unlock-pdf-forms', - 'removeCertificateSign': '/remove-certificate-sign' - }; + const tool = registry[toolId]; - const path = pathMappings[toolKey] || `/${toolKey}`; - return `${baseUrl}${path}`; - } - - return baseUrl; + const path = getToolUrlPath(toolId, tool); + return `${baseUrl}${path}`; }