From 83400dc6a7b5fac80b5b044ab4897ceec2af0301 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Wed, 27 Aug 2025 14:15:09 +0100 Subject: [PATCH] Reset route on all tools --- frontend/src/contexts/NavigationContext.tsx | 26 ++-- frontend/src/hooks/useToolUrlRouting.ts | 129 -------------------- frontend/src/hooks/useUrlSync.ts | 33 ++--- frontend/src/utils/urlRouting.ts | 64 ++++++---- 4 files changed, 70 insertions(+), 182 deletions(-) delete mode 100644 frontend/src/hooks/useToolUrlRouting.ts diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx index 532eb20bb..32c7a86d0 100644 --- a/frontend/src/contexts/NavigationContext.tsx +++ b/frontend/src/contexts/NavigationContext.tsx @@ -4,9 +4,9 @@ import { ModeType, isValidMode, getDefaultMode } from '../types/navigation'; /** * NavigationContext - Complete navigation management system - * + * * Handles navigation modes, navigation guards for unsaved changes, - * and breadcrumb/history navigation. Separated from FileContext to + * and breadcrumb/history navigation. Separated from FileContext to * maintain clear separation of concerns. */ @@ -32,19 +32,19 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na switch (action.type) { case 'SET_MODE': return { ...state, currentMode: action.payload.mode }; - + case 'SET_UNSAVED_CHANGES': return { ...state, hasUnsavedChanges: action.payload.hasChanges }; - + case 'SET_PENDING_NAVIGATION': return { ...state, pendingNavigation: action.payload.navigationFn }; - + case 'SHOW_NAVIGATION_WARNING': return { ...state, showNavigationWarning: action.payload.show }; - + case 'SET_SELECTED_TOOL': return { ...state, selectedToolKey: action.payload.toolKey }; - + default: return state; } @@ -90,7 +90,7 @@ const NavigationStateContext = createContext(undefined); // Provider component -export const NavigationProvider: React.FC<{ +export const NavigationProvider: React.FC<{ children: React.ReactNode; enableUrlSync?: boolean; }> = ({ children, enableUrlSync = true }) => { @@ -126,7 +126,7 @@ export const NavigationProvider: React.FC<{ if (state.pendingNavigation) { state.pendingNavigation(); } - + // Clear navigation state dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); @@ -144,12 +144,14 @@ export const NavigationProvider: React.FC<{ clearToolSelection: useCallback(() => { dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); + dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } }); }, []), 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() } }); return; } @@ -216,7 +218,7 @@ export const useNavigation = () => { export const useNavigationGuard = () => { const state = useNavigationState(); const { actions } = useNavigationActions(); - + return { pendingNavigation: state.pendingNavigation, showNavigationWarning: state.showNavigationWarning, @@ -234,7 +236,7 @@ export { isValidMode, getDefaultMode, type ModeType } from '../types/navigation' // TODO: This will be expanded for URL-based routing system // - URL parsing utilities -// - Route definitions +// - Route definitions // - Navigation hooks with URL sync // - History management -// - Breadcrumb restoration from URL params \ No newline at end of file +// - Breadcrumb restoration from URL params diff --git a/frontend/src/hooks/useToolUrlRouting.ts b/frontend/src/hooks/useToolUrlRouting.ts deleted file mode 100644 index 57e61d9e0..000000000 --- a/frontend/src/hooks/useToolUrlRouting.ts +++ /dev/null @@ -1,129 +0,0 @@ -// src/hooks/useToolUrlRouting.ts -// Focused hook for URL <-> tool-key mapping and browser history sync. - -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -export interface UseToolUrlRoutingOpts { - /** Currently selected tool key (from context). */ - selectedToolKey: string | null; - /** Registry of available tools (key -> tool metadata). */ - toolRegistry: Record | null | undefined; - /** Select a tool (no extra side-effects). */ - selectTool: (toolKey: string) => void; - /** Clear selection. */ - clearToolSelection: () => void; - /** Called once during initialization if URL contains a tool; may trigger UI changes. */ - onInitSelect?: (toolKey: string) => void; - /** Called when navigating via back/forward (popstate). Defaults to selectTool. */ - onPopStateSelect?: (toolKey: string) => void; - /** Optional base path if the app isn't served at "/" (no trailing slash). Default: "" (root). */ - basePath?: string; -} - -export function useToolUrlRouting(opts: UseToolUrlRoutingOpts) { - const { - selectedToolKey, - toolRegistry, - selectTool, - clearToolSelection, - onInitSelect, - onPopStateSelect, - basePath = '', - } = opts; - - // Central slug map; keep here to co-locate routing policy. - const urlMap = useMemo( - () => - new Map([ - ['compress', 'compress-pdf'], - ['split', 'split-pdf'], - ['convert', 'convert-pdf'], - ['ocr', 'ocr-pdf'], - ['merge', 'merge-pdf'], - ['rotate', 'rotate-pdf'], - ]), - [] - ); - - const getToolUrlSlug = useCallback( - (toolKey: string) => urlMap.get(toolKey) ?? toolKey, - [urlMap] - ); - - const getToolKeyFromSlug = useCallback( - (slug: string) => { - for (const [key, value] of urlMap) { - if (value === slug) return key; - } - return slug; // fall back to raw key - }, - [urlMap] - ); - - // Internal flag to avoid clearing URL on initial mount. - const [hasInitialized, setHasInitialized] = useState(false); - - // Normalize a pathname by stripping basePath and leading slash. - const normalizePath = useCallback( - (fullPath: string) => { - let p = fullPath; - if (basePath && p.startsWith(basePath)) { - p = p.slice(basePath.length); - } - if (p.startsWith('/')) p = p.slice(1); - return p; - }, - [basePath] - ); - - // Update URL when tool changes (but not on first paint before any selection happens). - useEffect(() => { - if (selectedToolKey) { - const slug = getToolUrlSlug(selectedToolKey); - const newUrl = `${basePath}/${slug}`.replace(/\/+/, '/'); - window.history.replaceState({}, '', newUrl); - setHasInitialized(true); - } else if (hasInitialized) { - const rootUrl = basePath || '/'; - window.history.replaceState({}, '', rootUrl); - } - }, [selectedToolKey, getToolUrlSlug, hasInitialized, basePath]); - - // Initialize from URL when the registry is ready and nothing is selected yet. - useEffect(() => { - if (!toolRegistry || Object.keys(toolRegistry).length === 0) return; - if (selectedToolKey) return; // don't override explicit selection - - const currentPath = normalizePath(window.location.pathname); - if (currentPath) { - const toolKey = getToolKeyFromSlug(currentPath); - if (toolRegistry[toolKey]) { - (onInitSelect ?? selectTool)(toolKey); - } - } - }, [toolRegistry, selectedToolKey, getToolKeyFromSlug, selectTool, onInitSelect, normalizePath]); - - // Handle browser back/forward. NOTE: useRef needs an initial value in TS. - const popHandlerRef = useRef<((this: Window, ev: PopStateEvent) => any) | null>(null); - - useEffect(() => { - popHandlerRef.current = () => { - const path = normalizePath(window.location.pathname); - if (path) { - const toolKey = getToolKeyFromSlug(path); - if (toolRegistry && toolRegistry[toolKey]) { - (onPopStateSelect ?? selectTool)(toolKey); - return; - } - } - clearToolSelection(); - }; - - const handler = (e: PopStateEvent) => popHandlerRef.current?.call(window, e); - window.addEventListener('popstate', handler); - return () => window.removeEventListener('popstate', handler); - }, [toolRegistry, selectTool, clearToolSelection, getToolKeyFromSlug, onPopStateSelect, normalizePath]); - - // Expose pure helpers if you want them elsewhere (optional). - return { getToolUrlSlug, getToolKeyFromSlug }; -} diff --git a/frontend/src/hooks/useUrlSync.ts b/frontend/src/hooks/useUrlSync.ts index 4a50a36ba..93dabfa36 100644 --- a/frontend/src/hooks/useUrlSync.ts +++ b/frontend/src/hooks/useUrlSync.ts @@ -17,7 +17,7 @@ export function useNavigationUrlSync( // Initialize mode from URL on mount useEffect(() => { if (!enableSync) return; - + const route = parseToolRoute(); if (route.mode !== currentMode) { setMode(route.mode); @@ -27,10 +27,10 @@ export function useNavigationUrlSync( // Update URL when mode changes useEffect(() => { if (!enableSync) return; - - if (currentMode === 'pageEditor') { - clearToolRoute(); - } else { + + // 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); } }, [currentMode, enableSync]); @@ -38,7 +38,7 @@ export function useNavigationUrlSync( // Handle browser back/forward navigation useEffect(() => { if (!enableSync) return; - + const handlePopState = () => { const route = parseToolRoute(); if (route.mode !== currentMode) { @@ -63,7 +63,7 @@ export function useToolWorkflowUrlSync( // Initialize tool from URL on mount useEffect(() => { if (!enableSync) return; - + const route = parseToolRoute(); if (route.toolKey && route.toolKey !== selectedToolKey) { selectTool(route.toolKey); @@ -75,12 +75,15 @@ export function useToolWorkflowUrlSync( // 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]); } @@ -102,19 +105,19 @@ export function useCurrentRoute() { export function useToolNavigation() { const navigateToTool = useCallback((toolKey: string) => { updateToolRoute(toolKey as ModeType, toolKey); - + // Dispatch a custom event to notify other components - window.dispatchEvent(new CustomEvent('toolNavigation', { - detail: { toolKey } + window.dispatchEvent(new CustomEvent('toolNavigation', { + detail: { toolKey } })); }, []); const navigateToHome = useCallback(() => { clearToolRoute(); - + // Dispatch a custom event to notify other components - window.dispatchEvent(new CustomEvent('toolNavigation', { - detail: { toolKey: null } + window.dispatchEvent(new CustomEvent('toolNavigation', { + detail: { toolKey: null } })); }, []); @@ -122,4 +125,4 @@ export function useToolNavigation() { navigateToTool, navigateToHome }; -} \ No newline at end of file +} diff --git a/frontend/src/utils/urlRouting.ts b/frontend/src/utils/urlRouting.ts index 74d229701..687350530 100644 --- a/frontend/src/utils/urlRouting.ts +++ b/frontend/src/utils/urlRouting.ts @@ -11,12 +11,12 @@ import { ModeType, isValidMode as isValidModeType, getDefaultMode, ToolRoute } f export function parseToolRoute(): 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' }, @@ -48,7 +48,7 @@ export function parseToolRoute(): ToolRoute { 'remove-certificate-sign': { mode: 'removeCertificateSign', toolKey: 'removeCertificateSign' }, 'remove-cert-sign': { mode: 'removeCertificateSign', toolKey: 'removeCertificateSign' } }; - + const mapping = toolMappings[toolKey]; if (mapping) { return { @@ -57,7 +57,7 @@ export function parseToolRoute(): ToolRoute { }; } } - + // Check for query parameter fallback (e.g., ?tool=split) const toolParam = searchParams.get('tool'); if (toolParam && isValidModeType(toolParam)) { @@ -66,7 +66,7 @@ export function parseToolRoute(): ToolRoute { toolKey: toolParam }; } - + // Default to page editor for home page return { mode: getDefaultMode(), @@ -81,7 +81,7 @@ export function parseToolRoute(): ToolRoute { export function updateToolRoute(mode: ModeType, toolKey?: string): 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 @@ -90,32 +90,38 @@ export function updateToolRoute(mode: ModeType, toolKey?: string): void { } return; } - + let newPath = '/'; - + // Map modes to URL paths (only for actual tools) if (toolKey) { const pathMappings: Record = { - 'split': '/split-pdf', - 'merge': '/merge-pdf', + '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' + '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}`; } - + // Remove tool query parameter since we're using path-based routing searchParams.delete('tool'); - + // Construct final URL const queryString = searchParams.toString(); const fullUrl = newPath + (queryString ? `?${queryString}` : ''); - + // Update URL without triggering page reload if (currentPath !== newPath || window.location.search !== (queryString ? `?${queryString}` : '')) { window.history.replaceState(null, '', fullUrl); @@ -128,10 +134,10 @@ export function updateToolRoute(mode: ModeType, toolKey?: string): void { export function clearToolRoute(): void { const searchParams = new URLSearchParams(window.location.search); searchParams.delete('tool'); - + const queryString = searchParams.toString(); const url = '/' + (queryString ? `?${queryString}` : ''); - + window.history.replaceState(null, '', url); } @@ -142,14 +148,14 @@ export function getToolDisplayName(toolKey: string): string { const displayNames: Record = { 'split': 'Split PDF', 'merge': 'Merge PDF', - 'compress': 'Compress PDF', + 'compress': 'Compress PDF', 'convert': 'Convert PDF', 'addPassword': 'Add Password', 'changePermissions': 'Change Permissions', 'sanitize': 'Sanitize PDF', 'ocr': 'OCR PDF' }; - + return displayNames[toolKey] || toolKey; } @@ -161,27 +167,33 @@ export function getToolDisplayName(toolKey: string): string { */ export function generateShareableUrl(mode: ModeType, toolKey?: string): string { const baseUrl = window.location.origin; - + // Don't generate URLs for internal UI modes if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') { return baseUrl; } - + if (toolKey) { const pathMappings: Record = { 'split': '/split-pdf', 'merge': '/merge-pdf', - 'compress': '/compress-pdf', + 'compress': '/compress-pdf', 'convert': '/convert-pdf', 'addPassword': '/add-password-pdf', 'changePermissions': '/change-permissions-pdf', 'sanitize': '/sanitize-pdf', - 'ocr': '/ocr-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 path = pathMappings[toolKey] || `/${toolKey}`; return `${baseUrl}${path}`; } - + return baseUrl; -} \ No newline at end of file +}