mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
Reset route on all tools
This commit is contained in:
parent
79c50f4bc5
commit
83400dc6a7
@ -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<NavigationContextStateValue | undef
|
||||
const NavigationActionsContext = createContext<NavigationContextActionsValue | undefined>(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
|
||||
// - Breadcrumb restoration from URL params
|
||||
|
@ -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<string, any> | 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<string, string>([
|
||||
['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 };
|
||||
}
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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<string, { mode: ModeType; toolKey: string }> = {
|
||||
'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<string, string> = {
|
||||
'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<string, string> = {
|
||||
'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<string, string> = {
|
||||
'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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user