Restore URl for tools

This commit is contained in:
Reece Browne 2025-08-19 17:51:27 +01:00
parent 83dee1c0b5
commit 511bdee7db
4 changed files with 320 additions and 2 deletions

View File

@ -1,4 +1,5 @@
import React, { createContext, useContext, useReducer, useCallback } from 'react'; import React, { createContext, useContext, useReducer, useCallback } from 'react';
import { useNavigationUrlSync } from '../hooks/useUrlSync';
/** /**
* NavigationContext - Complete navigation management system * NavigationContext - Complete navigation management system
@ -92,7 +93,10 @@ const NavigationStateContext = createContext<NavigationContextStateValue | undef
const NavigationActionsContext = createContext<NavigationContextActionsValue | undefined>(undefined); const NavigationActionsContext = createContext<NavigationContextActionsValue | undefined>(undefined);
// Provider component // Provider component
export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const NavigationProvider: React.FC<{
children: React.ReactNode;
enableUrlSync?: boolean;
}> = ({ children, enableUrlSync = true }) => {
const [state, dispatch] = useReducer(navigationReducer, initialState); const [state, dispatch] = useReducer(navigationReducer, initialState);
const actions: NavigationContextActions = { const actions: NavigationContextActions = {
@ -149,6 +153,9 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
actions actions
}; };
// Enable URL synchronization
useNavigationUrlSync(state.currentMode, actions.setMode, enableUrlSync);
return ( return (
<NavigationStateContext.Provider value={stateValue}> <NavigationStateContext.Provider value={stateValue}>
<NavigationActionsContext.Provider value={actionsValue}> <NavigationActionsContext.Provider value={actionsValue}>

View File

@ -7,6 +7,7 @@ import React, { createContext, useContext, useReducer, useCallback, useMemo } fr
import { useToolManagement } from '../hooks/useToolManagement'; import { useToolManagement } from '../hooks/useToolManagement';
import { PageEditorFunctions } from '../types/pageEditor'; import { PageEditorFunctions } from '../types/pageEditor';
import { ToolRegistryEntry } from '../data/toolsTaxonomy'; import { ToolRegistryEntry } from '../data/toolsTaxonomy';
import { useToolWorkflowUrlSync } from '../hooks/useUrlSync';
// State interface // State interface
interface ToolWorkflowState { interface ToolWorkflowState {
@ -101,9 +102,11 @@ interface ToolWorkflowProviderProps {
children: React.ReactNode; children: React.ReactNode;
/** Handler for view changes (passed from parent) */ /** Handler for view changes (passed from parent) */
onViewChange?: (view: string) => void; onViewChange?: (view: string) => void;
/** Enable URL synchronization for tool selection */
enableUrlSync?: boolean;
} }
export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowProviderProps) { export function ToolWorkflowProvider({ children, onViewChange, enableUrlSync = true }: ToolWorkflowProviderProps) {
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState); const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
// Tool management hook // Tool management hook
@ -182,6 +185,9 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro
[state.sidebarsVisible, state.readerMode] [state.sidebarsVisible, state.readerMode]
); );
// Enable URL synchronization for tool selection
useToolWorkflowUrlSync(selectedToolKey, selectTool, clearToolSelection, enableUrlSync);
// Simple context value with basic memoization // Simple context value with basic memoization
const contextValue = useMemo((): ToolWorkflowContextValue => ({ const contextValue = useMemo((): ToolWorkflowContextValue => ({
// State // State

View File

@ -0,0 +1,125 @@
/**
* URL synchronization hooks for tool routing
*/
import { useEffect, useCallback } from 'react';
import { ModeType } from '../contexts/NavigationContext';
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
/**
* Hook to sync navigation mode with URL
*/
export function useNavigationUrlSync(
currentMode: ModeType,
setMode: (mode: ModeType) => void,
enableSync: boolean = true
) {
// Initialize mode from URL on mount
useEffect(() => {
if (!enableSync) return;
const route = parseToolRoute();
if (route.mode !== currentMode) {
setMode(route.mode);
}
}, []); // Only run on mount
// Update URL when mode changes
useEffect(() => {
if (!enableSync) return;
if (currentMode === 'pageEditor') {
clearToolRoute();
} else {
updateToolRoute(currentMode, currentMode);
}
}, [currentMode, enableSync]);
// Handle browser back/forward navigation
useEffect(() => {
if (!enableSync) return;
const handlePopState = () => {
const route = parseToolRoute();
if (route.mode !== currentMode) {
setMode(route.mode);
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [currentMode, setMode, enableSync]);
}
/**
* Hook to sync tool workflow with URL
*/
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);
}
}
}, [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);
// Dispatch a custom event to notify other components
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 }
}));
}, []);
return {
navigateToTool,
navigateToHome
};
}

View File

@ -0,0 +1,180 @@
/**
* URL routing utilities for tool navigation
* Provides clean URL routing for the V2 tool system
*/
import { ModeType } from '../contexts/NavigationContext';
export interface ToolRoute {
mode: ModeType;
toolKey?: string;
}
/**
* Parse the current URL to extract tool routing information
*/
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<string, { mode: ModeType; toolKey: string }> = {
'split': { mode: 'split', toolKey: 'split' },
'merge': { mode: 'merge', toolKey: 'merge' },
'compress': { mode: 'compress', toolKey: 'compress' },
'convert': { mode: 'convert', toolKey: 'convert' },
'add-password': { mode: 'addPassword', toolKey: 'addPassword' },
'change-permissions': { mode: 'changePermissions', toolKey: 'changePermissions' },
'sanitize': { mode: 'sanitize', toolKey: 'sanitize' },
'ocr': { mode: 'ocr', toolKey: 'ocr' }
};
const mapping = toolMappings[toolKey];
if (mapping) {
return {
mode: mapping.mode,
toolKey: mapping.toolKey
};
}
}
// Check for query parameter fallback (e.g., ?tool=split)
const toolParam = searchParams.get('tool');
if (toolParam && isValidMode(toolParam)) {
return {
mode: toolParam as ModeType,
toolKey: toolParam
};
}
// Default to page editor for home page
return {
mode: 'pageEditor'
};
}
/**
* 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 {
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();
}
return;
}
let newPath = '/';
// Map modes to URL paths (only for actual tools)
if (toolKey) {
const pathMappings: Record<string, string> = {
'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'
};
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);
}
}
/**
* Clear tool routing and return to home page
*/
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);
}
/**
* Get clean tool name for display purposes
*/
export function getToolDisplayName(toolKey: string): string {
const displayNames: Record<string, string> = {
'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;
}
/**
* Check if a mode is valid
*/
function isValidMode(mode: string): mode is ModeType {
const validModes: ModeType[] = [
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', 'sanitize'
];
return validModes.includes(mode as ModeType);
}
/**
* Generate shareable URL for current tool state
* Only generates URLs for actual tools, not internal UI modes
*/
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<string, string> = {
'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'
};
const path = pathMappings[toolKey] || `/${toolKey}`;
return `${baseUrl}${path}`;
}
return baseUrl;
}