mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Restore URl for tools
This commit is contained in:
parent
83dee1c0b5
commit
511bdee7db
@ -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}>
|
||||||
|
@ -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
|
||||||
|
125
frontend/src/hooks/useUrlSync.ts
Normal file
125
frontend/src/hooks/useUrlSync.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
180
frontend/src/utils/urlRouting.ts
Normal file
180
frontend/src/utils/urlRouting.ts
Normal 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;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user