mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
Rework navigation
This commit is contained in:
parent
83400dc6a7
commit
e59e73ceb0
@ -7,6 +7,7 @@ import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext";
|
|||||||
import { SidebarProvider } from "./contexts/SidebarContext";
|
import { SidebarProvider } from "./contexts/SidebarContext";
|
||||||
import ErrorBoundary from "./components/shared/ErrorBoundary";
|
import ErrorBoundary from "./components/shared/ErrorBoundary";
|
||||||
import HomePage from "./pages/HomePage";
|
import HomePage from "./pages/HomePage";
|
||||||
|
import { ScarfPixel } from "./components/ScarfPixel";
|
||||||
|
|
||||||
// Import global styles
|
// Import global styles
|
||||||
import "./styles/tailwind.css";
|
import "./styles/tailwind.css";
|
||||||
@ -36,6 +37,7 @@ export default function App() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||||
<NavigationProvider>
|
<NavigationProvider>
|
||||||
|
<ScarfPixel />
|
||||||
<FilesModalProvider>
|
<FilesModalProvider>
|
||||||
<ToolWorkflowProvider>
|
<ToolWorkflowProvider>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
|
@ -1,32 +1,29 @@
|
|||||||
import { useLocation } from "react-router-dom";
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useNavigationState } from "../contexts/NavigationContext";
|
||||||
|
|
||||||
export function ScarfPixel() {
|
export function ScarfPixel() {
|
||||||
const location = useLocation();
|
const { workbench, selectedTool } = useNavigationState();
|
||||||
const lastUrlSent = useRef<string | null>(null); // helps with React 18 StrictMode in dev
|
const lastUrlSent = useRef<string | null>(null); // helps with React 18 StrictMode in dev
|
||||||
|
|
||||||
useEffect(() => {
|
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'
|
const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7'
|
||||||
+ '&path=' + encodeURIComponent(location.pathname)
|
+ '&path=' + encodeURIComponent(pathname)
|
||||||
+ '&t=' + Date.now(); // cache-buster
|
+ '&t=' + Date.now(); // cache-buster
|
||||||
|
|
||||||
|
console.log("ScarfPixel: Navigation change", { workbench, selectedTool, pathname });
|
||||||
|
|
||||||
// + '&machineType=' + machineType
|
if (lastUrlSent.current !== url) {
|
||||||
// + '&appVersion=' + appVersion
|
|
||||||
// + '&licenseType=' + license
|
|
||||||
// + '&loginEnabled=' + loginEnabled;
|
|
||||||
console.log("ScarfPixel: reload " + location.pathname );
|
|
||||||
|
|
||||||
if (lastUrlSent.current !== url) {
|
|
||||||
lastUrlSent.current = url;
|
lastUrlSent.current = url;
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.referrerPolicy = "no-referrer-when-downgrade"; // optional
|
img.referrerPolicy = "no-referrer-when-downgrade"; // optional
|
||||||
img.src = url;
|
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
|
return null; // Nothing visible in UI
|
||||||
}
|
}
|
||||||
|
@ -411,9 +411,9 @@ const FileEditor = ({
|
|||||||
if (record) {
|
if (record) {
|
||||||
// Set the file as selected in context and switch to viewer for preview
|
// Set the file as selected in context and switch to viewer for preview
|
||||||
setSelectedFiles([fileId]);
|
setSelectedFiles([fileId]);
|
||||||
navActions.setMode('viewer');
|
navActions.setWorkbench('viewer');
|
||||||
}
|
}
|
||||||
}, [activeFileRecords, setSelectedFiles, navActions.setMode]);
|
}, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]);
|
||||||
|
|
||||||
const handleMergeFromHere = useCallback((fileId: string) => {
|
const handleMergeFromHere = useCallback((fileId: string) => {
|
||||||
const startIndex = activeFileRecords.findIndex(r => r.id === fileId);
|
const startIndex = activeFileRecords.findIndex(r => r.id === fileId);
|
||||||
|
@ -6,13 +6,13 @@ import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
|||||||
import { useFileHandler } from '../../hooks/useFileHandler';
|
import { useFileHandler } from '../../hooks/useFileHandler';
|
||||||
import { useFileState, useFileActions } from '../../contexts/FileContext';
|
import { useFileState, useFileActions } from '../../contexts/FileContext';
|
||||||
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
|
import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext';
|
||||||
|
import { useToolManagement } from '../../hooks/useToolManagement';
|
||||||
|
|
||||||
import TopControls from '../shared/TopControls';
|
import TopControls from '../shared/TopControls';
|
||||||
import FileEditor from '../fileEditor/FileEditor';
|
import FileEditor from '../fileEditor/FileEditor';
|
||||||
import PageEditor from '../pageEditor/PageEditor';
|
import PageEditor from '../pageEditor/PageEditor';
|
||||||
import PageEditorControls from '../pageEditor/PageEditorControls';
|
import PageEditorControls from '../pageEditor/PageEditorControls';
|
||||||
import Viewer from '../viewer/Viewer';
|
import Viewer from '../viewer/Viewer';
|
||||||
import ToolRenderer from '../tools/ToolRenderer';
|
|
||||||
import LandingPage from '../shared/LandingPage';
|
import LandingPage from '../shared/LandingPage';
|
||||||
|
|
||||||
// No props needed - component uses contexts directly
|
// No props needed - component uses contexts directly
|
||||||
@ -23,9 +23,9 @@ export default function Workbench() {
|
|||||||
// Use context-based hooks to eliminate all prop drilling
|
// Use context-based hooks to eliminate all prop drilling
|
||||||
const { state } = useFileState();
|
const { state } = useFileState();
|
||||||
const { actions } = useFileActions();
|
const { actions } = useFileActions();
|
||||||
const { currentMode: currentView } = useNavigationState();
|
const { workbench: currentView } = useNavigationState();
|
||||||
const { actions: navActions } = useNavigationActions();
|
const { actions: navActions } = useNavigationActions();
|
||||||
const setCurrentView = navActions.setMode;
|
const setCurrentView = navActions.setWorkbench;
|
||||||
const activeFiles = state.files.ids;
|
const activeFiles = state.files.ids;
|
||||||
const {
|
const {
|
||||||
previewFile,
|
previewFile,
|
||||||
@ -36,7 +36,14 @@ export default function Workbench() {
|
|||||||
setSidebarsVisible
|
setSidebarsVisible
|
||||||
} = useToolWorkflow();
|
} = 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 { addToActiveFiles } = useFileHandler();
|
||||||
|
|
||||||
const handlePreviewClose = () => {
|
const handlePreviewClose = () => {
|
||||||
@ -69,11 +76,11 @@ export default function Workbench() {
|
|||||||
case "fileEditor":
|
case "fileEditor":
|
||||||
return (
|
return (
|
||||||
<FileEditor
|
<FileEditor
|
||||||
toolMode={!!selectedToolKey}
|
toolMode={!!selectedToolId}
|
||||||
showUpload={true}
|
showUpload={true}
|
||||||
showBulkActions={!selectedToolKey}
|
showBulkActions={!selectedToolId}
|
||||||
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
|
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
|
||||||
{...(!selectedToolKey && {
|
{...(!selectedToolId && {
|
||||||
onOpenPageEditor: (file) => {
|
onOpenPageEditor: (file) => {
|
||||||
setCurrentView("pageEditor");
|
setCurrentView("pageEditor");
|
||||||
},
|
},
|
||||||
@ -127,14 +134,6 @@ export default function Workbench() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Check if it's a tool view
|
|
||||||
if (selectedToolKey && selectedTool) {
|
|
||||||
return (
|
|
||||||
<ToolRenderer
|
|
||||||
selectedToolKey={selectedToolKey}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<LandingPage/>
|
<LandingPage/>
|
||||||
);
|
);
|
||||||
@ -154,7 +153,7 @@ export default function Workbench() {
|
|||||||
<TopControls
|
<TopControls
|
||||||
currentView={currentView}
|
currentView={currentView}
|
||||||
setCurrentView={setCurrentView}
|
setCurrentView={setCurrentView}
|
||||||
selectedToolKey={selectedToolKey}
|
selectedToolKey={selectedToolId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext";
|
import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext";
|
||||||
import { ModeType } from "../../contexts/NavigationContext";
|
|
||||||
import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor";
|
import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor";
|
||||||
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
||||||
import { pdfExportService } from "../../services/pdfExportService";
|
import { pdfExportService } from "../../services/pdfExportService";
|
||||||
@ -83,7 +82,7 @@ const PageEditor = ({
|
|||||||
|
|
||||||
// Grid container ref for positioning split indicators
|
// Grid container ref for positioning split indicators
|
||||||
const gridContainerRef = useRef<HTMLDivElement>(null);
|
const gridContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// State to trigger re-renders when container size changes
|
// State to trigger re-renders when container size changes
|
||||||
const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 });
|
const [containerDimensions, setContainerDimensions] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
@ -128,7 +127,7 @@ const PageEditor = ({
|
|||||||
|
|
||||||
// Interface functions for parent component
|
// Interface functions for parent component
|
||||||
const displayDocument = editedDocument || mergedPdfDocument;
|
const displayDocument = editedDocument || mergedPdfDocument;
|
||||||
|
|
||||||
// Utility functions to convert between page IDs and page numbers
|
// Utility functions to convert between page IDs and page numbers
|
||||||
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
|
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
|
||||||
if (!displayDocument) return [];
|
if (!displayDocument) return [];
|
||||||
@ -137,7 +136,7 @@ const PageEditor = ({
|
|||||||
return page?.pageNumber || 0;
|
return page?.pageNumber || 0;
|
||||||
}).filter(num => num > 0);
|
}).filter(num => num > 0);
|
||||||
}, [displayDocument]);
|
}, [displayDocument]);
|
||||||
|
|
||||||
const getPageIdsFromNumbers = useCallback((pageNumbers: number[]): string[] => {
|
const getPageIdsFromNumbers = useCallback((pageNumbers: number[]): string[] => {
|
||||||
if (!displayDocument) return [];
|
if (!displayDocument) return [];
|
||||||
return pageNumbers.map(num => {
|
return pageNumbers.map(num => {
|
||||||
@ -145,10 +144,10 @@ const PageEditor = ({
|
|||||||
return page?.id || '';
|
return page?.id || '';
|
||||||
}).filter(id => id !== '');
|
}).filter(id => id !== '');
|
||||||
}, [displayDocument]);
|
}, [displayDocument]);
|
||||||
|
|
||||||
// Convert selectedPageIds to numbers for components that still need numbers
|
// Convert selectedPageIds to numbers for components that still need numbers
|
||||||
const selectedPageNumbers = useMemo(() =>
|
const selectedPageNumbers = useMemo(() =>
|
||||||
getPageNumbersFromIds(selectedPageIds),
|
getPageNumbersFromIds(selectedPageIds),
|
||||||
[selectedPageIds, getPageNumbersFromIds]
|
[selectedPageIds, getPageNumbersFromIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -234,7 +233,7 @@ const PageEditor = ({
|
|||||||
const handleRotate = useCallback((direction: 'left' | 'right') => {
|
const handleRotate = useCallback((direction: 'left' | 'right') => {
|
||||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||||
const rotation = direction === 'left' ? -90 : 90;
|
const rotation = direction === 'left' ? -90 : 90;
|
||||||
|
|
||||||
handleRotatePages(selectedPageIds, rotation);
|
handleRotatePages(selectedPageIds, rotation);
|
||||||
}, [displayDocument, selectedPageIds, handleRotatePages]);
|
}, [displayDocument, selectedPageIds, handleRotatePages]);
|
||||||
|
|
||||||
@ -296,14 +295,14 @@ const PageEditor = ({
|
|||||||
// Smart toggle logic: follow the majority, default to adding splits if equal
|
// Smart toggle logic: follow the majority, default to adding splits if equal
|
||||||
const existingSplitsCount = selectedPositions.filter(pos => splitPositions.has(pos)).length;
|
const existingSplitsCount = selectedPositions.filter(pos => splitPositions.has(pos)).length;
|
||||||
const noSplitsCount = selectedPositions.length - existingSplitsCount;
|
const noSplitsCount = selectedPositions.length - existingSplitsCount;
|
||||||
|
|
||||||
// Remove splits only if majority already have splits
|
// Remove splits only if majority already have splits
|
||||||
// If equal (50/50), default to adding splits
|
// If equal (50/50), default to adding splits
|
||||||
const shouldRemoveSplits = existingSplitsCount > noSplitsCount;
|
const shouldRemoveSplits = existingSplitsCount > noSplitsCount;
|
||||||
|
|
||||||
|
|
||||||
const newSplitPositions = new Set(splitPositions);
|
const newSplitPositions = new Set(splitPositions);
|
||||||
|
|
||||||
if (shouldRemoveSplits) {
|
if (shouldRemoveSplits) {
|
||||||
// Remove splits from all selected positions
|
// Remove splits from all selected positions
|
||||||
selectedPositions.forEach(pos => newSplitPositions.delete(pos));
|
selectedPositions.forEach(pos => newSplitPositions.delete(pos));
|
||||||
@ -316,8 +315,8 @@ const PageEditor = ({
|
|||||||
const smartSplitCommand = {
|
const smartSplitCommand = {
|
||||||
execute: () => setSplitPositions(newSplitPositions),
|
execute: () => setSplitPositions(newSplitPositions),
|
||||||
undo: () => setSplitPositions(splitPositions),
|
undo: () => setSplitPositions(splitPositions),
|
||||||
description: shouldRemoveSplits
|
description: shouldRemoveSplits
|
||||||
? `Remove ${selectedPositions.length} split(s)`
|
? `Remove ${selectedPositions.length} split(s)`
|
||||||
: `Add ${selectedPositions.length - existingSplitsCount} 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
|
// Smart toggle logic: follow the majority, default to adding splits if equal
|
||||||
const existingSplitsCount = selectedPositions.filter(pos => splitPositions.has(pos)).length;
|
const existingSplitsCount = selectedPositions.filter(pos => splitPositions.has(pos)).length;
|
||||||
const noSplitsCount = selectedPositions.length - existingSplitsCount;
|
const noSplitsCount = selectedPositions.length - existingSplitsCount;
|
||||||
|
|
||||||
// Remove splits only if majority already have splits
|
// Remove splits only if majority already have splits
|
||||||
// If equal (50/50), default to adding splits
|
// If equal (50/50), default to adding splits
|
||||||
const shouldRemoveSplits = existingSplitsCount > noSplitsCount;
|
const shouldRemoveSplits = existingSplitsCount > noSplitsCount;
|
||||||
|
|
||||||
const newSplitPositions = new Set(splitPositions);
|
const newSplitPositions = new Set(splitPositions);
|
||||||
|
|
||||||
if (shouldRemoveSplits) {
|
if (shouldRemoveSplits) {
|
||||||
// Remove splits from all selected positions
|
// Remove splits from all selected positions
|
||||||
selectedPositions.forEach(pos => newSplitPositions.delete(pos));
|
selectedPositions.forEach(pos => newSplitPositions.delete(pos));
|
||||||
@ -362,8 +361,8 @@ const PageEditor = ({
|
|||||||
const smartSplitCommand = {
|
const smartSplitCommand = {
|
||||||
execute: () => setSplitPositions(newSplitPositions),
|
execute: () => setSplitPositions(newSplitPositions),
|
||||||
undo: () => setSplitPositions(splitPositions),
|
undo: () => setSplitPositions(splitPositions),
|
||||||
description: shouldRemoveSplits
|
description: shouldRemoveSplits
|
||||||
? `Remove ${selectedPositions.length} split(s)`
|
? `Remove ${selectedPositions.length} split(s)`
|
||||||
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -404,7 +403,7 @@ const PageEditor = ({
|
|||||||
try {
|
try {
|
||||||
const targetPage = displayDocument.pages.find(p => p.pageNumber === insertAfterPage);
|
const targetPage = displayDocument.pages.find(p => p.pageNumber === insertAfterPage);
|
||||||
if (!targetPage) return;
|
if (!targetPage) return;
|
||||||
|
|
||||||
await actions.addFiles(files, { insertAfterPageId: targetPage.id });
|
await actions.addFiles(files, { insertAfterPageId: targetPage.id });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to insert files:', error);
|
console.error('Failed to insert files:', error);
|
||||||
@ -457,7 +456,7 @@ const PageEditor = ({
|
|||||||
// Use multi-file export if we have multiple original files
|
// Use multi-file export if we have multiple original files
|
||||||
const hasInsertedFiles = false;
|
const hasInsertedFiles = false;
|
||||||
const hasMultipleOriginalFiles = activeFileIds.length > 1;
|
const hasMultipleOriginalFiles = activeFileIds.length > 1;
|
||||||
|
|
||||||
if (!hasInsertedFiles && !hasMultipleOriginalFiles) {
|
if (!hasInsertedFiles && !hasMultipleOriginalFiles) {
|
||||||
return null; // Use single-file export method
|
return null; // Use single-file export method
|
||||||
}
|
}
|
||||||
@ -499,7 +498,7 @@ const PageEditor = ({
|
|||||||
|
|
||||||
// Step 2: Use the already selected page IDs
|
// Step 2: Use the already selected page IDs
|
||||||
// Filter to only include IDs that exist in the document with DOM state
|
// 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)
|
documentWithDOMState.pages.some(p => p.id === pageId)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -551,11 +550,11 @@ const PageEditor = ({
|
|||||||
const sourceFiles = getSourceFiles();
|
const sourceFiles = getSourceFiles();
|
||||||
const baseExportFilename = getExportFilename();
|
const baseExportFilename = getExportFilename();
|
||||||
const baseName = baseExportFilename.replace(/\.pdf$/i, '');
|
const baseName = baseExportFilename.replace(/\.pdf$/i, '');
|
||||||
|
|
||||||
for (let i = 0; i < processedDocuments.length; i++) {
|
for (let i = 0; i < processedDocuments.length; i++) {
|
||||||
const doc = processedDocuments[i];
|
const doc = processedDocuments[i];
|
||||||
const partFilename = `${baseName}_part_${i + 1}.pdf`;
|
const partFilename = `${baseName}_part_${i + 1}.pdf`;
|
||||||
|
|
||||||
const result = sourceFiles
|
const result = sourceFiles
|
||||||
? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { filename: partFilename })
|
? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { filename: partFilename })
|
||||||
: await pdfExportService.exportPDF(doc, [], { 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_WIDTH = parseFloat(GRID_CONSTANTS.ITEM_WIDTH) * remToPx;
|
||||||
const ITEM_HEIGHT = parseFloat(GRID_CONSTANTS.ITEM_HEIGHT) * remToPx;
|
const ITEM_HEIGHT = parseFloat(GRID_CONSTANTS.ITEM_HEIGHT) * remToPx;
|
||||||
const ITEM_GAP = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx;
|
const ITEM_GAP = parseFloat(GRID_CONSTANTS.ITEM_GAP) * remToPx;
|
||||||
|
|
||||||
return Array.from(splitPositions).map((position) => {
|
return Array.from(splitPositions).map((position) => {
|
||||||
|
|
||||||
// Calculate items per row using DragDropGrid's logic
|
// Calculate items per row using DragDropGrid's logic
|
||||||
const availableWidth = containerWidth - ITEM_GAP; // Account for first gap
|
const availableWidth = containerWidth - ITEM_GAP; // Account for first gap
|
||||||
const itemWithGap = ITEM_WIDTH + ITEM_GAP;
|
const itemWithGap = ITEM_WIDTH + ITEM_GAP;
|
||||||
const itemsPerRow = Math.max(1, Math.floor(availableWidth / itemWithGap));
|
const itemsPerRow = Math.max(1, Math.floor(availableWidth / itemWithGap));
|
||||||
|
|
||||||
// Calculate position within the grid (same as DragDropGrid)
|
// Calculate position within the grid (same as DragDropGrid)
|
||||||
const row = Math.floor(position / itemsPerRow);
|
const row = Math.floor(position / itemsPerRow);
|
||||||
const col = position % itemsPerRow;
|
const col = position % itemsPerRow;
|
||||||
|
|
||||||
// Position split line between pages (after the current page)
|
// Position split line between pages (after the current page)
|
||||||
// Calculate grid centering offset (same as DragDropGrid)
|
// Calculate grid centering offset (same as DragDropGrid)
|
||||||
const gridWidth = itemsPerRow * ITEM_WIDTH + (itemsPerRow - 1) * ITEM_GAP;
|
const gridWidth = itemsPerRow * ITEM_WIDTH + (itemsPerRow - 1) * ITEM_GAP;
|
||||||
const gridOffset = Math.max(0, (containerWidth - gridWidth) / 2);
|
const gridOffset = Math.max(0, (containerWidth - gridWidth) / 2);
|
||||||
|
|
||||||
const leftPosition = gridOffset + col * itemWithGap + ITEM_WIDTH + (ITEM_GAP / 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)
|
const topPosition = row * ITEM_HEIGHT + (ITEM_HEIGHT * 0.05); // Center vertically (5% offset since page is 90% height)
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export default function RightRail() {
|
|||||||
const [csvInput, setCsvInput] = useState<string>("");
|
const [csvInput, setCsvInput] = useState<string>("");
|
||||||
|
|
||||||
// Navigation view
|
// Navigation view
|
||||||
const { currentMode: currentView } = useNavigationState();
|
const { workbench: currentView } = useNavigationState();
|
||||||
|
|
||||||
// File state and selection
|
// File state and selection
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
|
@ -5,7 +5,7 @@ import rainbowStyles from '../../styles/rainbow.module.css';
|
|||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import EditNoteIcon from "@mui/icons-material/EditNote";
|
import EditNoteIcon from "@mui/icons-material/EditNote";
|
||||||
import FolderIcon from "@mui/icons-material/Folder";
|
import FolderIcon from "@mui/icons-material/Folder";
|
||||||
import { ModeType, isValidMode } from '../../contexts/NavigationContext';
|
import { WorkbenchType, isValidWorkbench } from '../../types/navigation';
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
const viewOptionStyle = {
|
const viewOptionStyle = {
|
||||||
@ -19,7 +19,7 @@ const viewOptionStyle = {
|
|||||||
|
|
||||||
|
|
||||||
// Build view options showing text only for current view; others icon-only with tooltip
|
// 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: (
|
label: (
|
||||||
<div style={viewOptionStyle as React.CSSProperties}>
|
<div style={viewOptionStyle as React.CSSProperties}>
|
||||||
@ -70,8 +70,8 @@ const createViewOptions = (currentView: ModeType, switchingTo: ModeType | null)
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface TopControlsProps {
|
interface TopControlsProps {
|
||||||
currentView: ModeType;
|
currentView: WorkbenchType;
|
||||||
setCurrentView: (view: ModeType) => void;
|
setCurrentView: (view: WorkbenchType) => void;
|
||||||
selectedToolKey?: string | null;
|
selectedToolKey?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,25 +81,25 @@ const TopControls = ({
|
|||||||
selectedToolKey,
|
selectedToolKey,
|
||||||
}: TopControlsProps) => {
|
}: TopControlsProps) => {
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const [switchingTo, setSwitchingTo] = useState<ModeType | null>(null);
|
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
|
||||||
|
|
||||||
const isToolSelected = selectedToolKey !== null;
|
const isToolSelected = selectedToolKey !== null;
|
||||||
|
|
||||||
const handleViewChange = useCallback((view: string) => {
|
const handleViewChange = useCallback((view: string) => {
|
||||||
if (!isValidMode(view)) {
|
if (!isValidWorkbench(view)) {
|
||||||
// Ignore invalid values defensively
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mode = view as ModeType;
|
|
||||||
|
const workbench = view;
|
||||||
|
|
||||||
// Show immediate feedback
|
// Show immediate feedback
|
||||||
setSwitchingTo(mode as ModeType);
|
setSwitchingTo(workbench);
|
||||||
|
|
||||||
// Defer the heavy view change to next frame so spinner can render
|
// Defer the heavy view change to next frame so spinner can render
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// Give the spinner one more frame to show
|
// Give the spinner one more frame to show
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setCurrentView(mode as ModeType);
|
setCurrentView(workbench);
|
||||||
|
|
||||||
// Clear the loading state after view change completes
|
// Clear the loading state after view change completes
|
||||||
setTimeout(() => setSwitchingTo(null), 300);
|
setTimeout(() => setSwitchingTo(null), 300);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { createContext, useContext, useReducer, useCallback } from 'react';
|
import React, { createContext, useContext, useReducer, useCallback } from 'react';
|
||||||
import { useNavigationUrlSync } from '../hooks/useUrlSync';
|
import { WorkbenchType, ToolId, getDefaultWorkbench } from '../types/navigation';
|
||||||
import { ModeType, isValidMode, getDefaultMode } from '../types/navigation';
|
import { useFlatToolRegistry } from '../data/useTranslatedToolRegistry';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NavigationContext - Complete navigation management system
|
* NavigationContext - Complete navigation management system
|
||||||
@ -11,27 +11,38 @@ import { ModeType, isValidMode, getDefaultMode } from '../types/navigation';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Navigation state
|
// Navigation state
|
||||||
interface NavigationState {
|
interface NavigationContextState {
|
||||||
currentMode: ModeType;
|
workbench: WorkbenchType;
|
||||||
|
selectedTool: ToolId | null;
|
||||||
hasUnsavedChanges: boolean;
|
hasUnsavedChanges: boolean;
|
||||||
pendingNavigation: (() => void) | null;
|
pendingNavigation: (() => void) | null;
|
||||||
showNavigationWarning: boolean;
|
showNavigationWarning: boolean;
|
||||||
selectedToolKey: string | null; // Add tool selection to navigation state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigation actions
|
// Navigation actions
|
||||||
type NavigationAction =
|
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_UNSAVED_CHANGES'; payload: { hasChanges: boolean } }
|
||||||
| { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } }
|
| { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } }
|
||||||
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } }
|
| { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } };
|
||||||
| { type: 'SET_SELECTED_TOOL'; payload: { toolKey: string | null } };
|
|
||||||
|
|
||||||
// Navigation reducer
|
// Navigation reducer
|
||||||
const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => {
|
const navigationReducer = (state: NavigationContextState, action: NavigationAction): NavigationContextState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_MODE':
|
case 'SET_WORKBENCH':
|
||||||
return { ...state, currentMode: action.payload.mode };
|
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':
|
case 'SET_UNSAVED_CHANGES':
|
||||||
return { ...state, hasUnsavedChanges: action.payload.hasChanges };
|
return { ...state, hasUnsavedChanges: action.payload.hasChanges };
|
||||||
@ -42,43 +53,41 @@ const navigationReducer = (state: NavigationState, action: NavigationAction): Na
|
|||||||
case 'SHOW_NAVIGATION_WARNING':
|
case 'SHOW_NAVIGATION_WARNING':
|
||||||
return { ...state, showNavigationWarning: action.payload.show };
|
return { ...state, showNavigationWarning: action.payload.show };
|
||||||
|
|
||||||
case 'SET_SELECTED_TOOL':
|
|
||||||
return { ...state, selectedToolKey: action.payload.toolKey };
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
const initialState: NavigationState = {
|
const initialState: NavigationContextState = {
|
||||||
currentMode: getDefaultMode(),
|
workbench: getDefaultWorkbench(),
|
||||||
|
selectedTool: null,
|
||||||
hasUnsavedChanges: false,
|
hasUnsavedChanges: false,
|
||||||
pendingNavigation: null,
|
pendingNavigation: null,
|
||||||
showNavigationWarning: false,
|
showNavigationWarning: false
|
||||||
selectedToolKey: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigation context actions interface
|
// Navigation context actions interface
|
||||||
export interface NavigationContextActions {
|
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;
|
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||||
showNavigationWarning: (show: boolean) => void;
|
showNavigationWarning: (show: boolean) => void;
|
||||||
requestNavigation: (navigationFn: () => void) => void;
|
requestNavigation: (navigationFn: () => void) => void;
|
||||||
confirmNavigation: () => void;
|
confirmNavigation: () => void;
|
||||||
cancelNavigation: () => void;
|
cancelNavigation: () => void;
|
||||||
selectTool: (toolKey: string) => void;
|
|
||||||
clearToolSelection: () => void;
|
clearToolSelection: () => void;
|
||||||
handleToolSelect: (toolId: string) => void;
|
handleToolSelect: (toolId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split context values
|
// Context state values
|
||||||
export interface NavigationContextStateValue {
|
export interface NavigationContextStateValue {
|
||||||
currentMode: ModeType;
|
workbench: WorkbenchType;
|
||||||
|
selectedTool: ToolId | null;
|
||||||
hasUnsavedChanges: boolean;
|
hasUnsavedChanges: boolean;
|
||||||
pendingNavigation: (() => void) | null;
|
pendingNavigation: (() => void) | null;
|
||||||
showNavigationWarning: boolean;
|
showNavigationWarning: boolean;
|
||||||
selectedToolKey: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigationContextActionsValue {
|
export interface NavigationContextActionsValue {
|
||||||
@ -95,10 +104,19 @@ export const NavigationProvider: React.FC<{
|
|||||||
enableUrlSync?: boolean;
|
enableUrlSync?: boolean;
|
||||||
}> = ({ children, enableUrlSync = true }) => {
|
}> = ({ children, enableUrlSync = true }) => {
|
||||||
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
const [state, dispatch] = useReducer(navigationReducer, initialState);
|
||||||
|
const toolRegistry = useFlatToolRegistry();
|
||||||
|
|
||||||
const actions: NavigationContextActions = {
|
const actions: NavigationContextActions = {
|
||||||
setMode: useCallback((mode: ModeType) => {
|
setWorkbench: useCallback((workbench: WorkbenchType) => {
|
||||||
dispatch({ type: 'SET_MODE', payload: { mode } });
|
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) => {
|
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
|
||||||
@ -110,77 +128,64 @@ export const NavigationProvider: React.FC<{
|
|||||||
}, []),
|
}, []),
|
||||||
|
|
||||||
requestNavigation: useCallback((navigationFn: () => void) => {
|
requestNavigation: useCallback((navigationFn: () => void) => {
|
||||||
// If no unsaved changes, navigate immediately
|
|
||||||
if (!state.hasUnsavedChanges) {
|
if (!state.hasUnsavedChanges) {
|
||||||
navigationFn();
|
navigationFn();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, store the navigation and show warning
|
|
||||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } });
|
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } });
|
||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
||||||
}, [state.hasUnsavedChanges]),
|
}, [state.hasUnsavedChanges]),
|
||||||
|
|
||||||
confirmNavigation: useCallback(() => {
|
confirmNavigation: useCallback(() => {
|
||||||
// Execute pending navigation
|
|
||||||
if (state.pendingNavigation) {
|
if (state.pendingNavigation) {
|
||||||
state.pendingNavigation();
|
state.pendingNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear navigation state
|
|
||||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
||||||
}, [state.pendingNavigation]),
|
}, [state.pendingNavigation]),
|
||||||
|
|
||||||
cancelNavigation: useCallback(() => {
|
cancelNavigation: useCallback(() => {
|
||||||
// Clear navigation without executing
|
|
||||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } });
|
||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } });
|
||||||
}, []),
|
}, []),
|
||||||
|
|
||||||
selectTool: useCallback((toolKey: string) => {
|
|
||||||
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey } });
|
|
||||||
}, []),
|
|
||||||
|
|
||||||
clearToolSelection: useCallback(() => {
|
clearToolSelection: useCallback(() => {
|
||||||
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
|
||||||
dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } });
|
|
||||||
}, []),
|
}, []),
|
||||||
|
|
||||||
handleToolSelect: useCallback((toolId: string) => {
|
handleToolSelect: useCallback((toolId: string) => {
|
||||||
// Handle special cases
|
|
||||||
if (toolId === 'allTools') {
|
if (toolId === 'allTools') {
|
||||||
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } });
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } });
|
||||||
dispatch({ type: 'SET_MODE', payload: { mode: getDefaultMode() } });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special-case: if tool is a dedicated reader tool, enter reader mode
|
|
||||||
if (toolId === 'read' || toolId === 'view-pdf') {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: toolId } });
|
// Look up the tool in the registry to get its proper workbench
|
||||||
dispatch({ type: 'SET_MODE', payload: { mode: 'fileEditor' as ModeType } });
|
const tool = toolRegistry[toolId];
|
||||||
}, [])
|
const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench();
|
||||||
|
|
||||||
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
||||||
|
}, [toolRegistry])
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateValue: NavigationContextStateValue = {
|
const stateValue: NavigationContextStateValue = {
|
||||||
currentMode: state.currentMode,
|
workbench: state.workbench,
|
||||||
|
selectedTool: state.selectedTool,
|
||||||
hasUnsavedChanges: state.hasUnsavedChanges,
|
hasUnsavedChanges: state.hasUnsavedChanges,
|
||||||
pendingNavigation: state.pendingNavigation,
|
pendingNavigation: state.pendingNavigation,
|
||||||
showNavigationWarning: state.showNavigationWarning,
|
showNavigationWarning: state.showNavigationWarning
|
||||||
selectedToolKey: state.selectedToolKey
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionsValue: NavigationContextActionsValue = {
|
const actionsValue: NavigationContextActionsValue = {
|
||||||
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}>
|
||||||
@ -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
|
// TODO: This will be expanded for URL-based routing system
|
||||||
// - URL parsing utilities
|
// - URL parsing utilities
|
||||||
// - Route definitions
|
// - Route definitions
|
||||||
|
@ -7,8 +7,8 @@ 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';
|
|
||||||
import { useNavigationActions, useNavigationState } from './NavigationContext';
|
import { useNavigationActions, useNavigationState } from './NavigationContext';
|
||||||
|
import { useNavigationUrlSync } from '../hooks/useUrlSync';
|
||||||
|
|
||||||
// State interface
|
// State interface
|
||||||
interface ToolWorkflowState {
|
interface ToolWorkflowState {
|
||||||
@ -124,7 +124,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
} = useToolManagement();
|
} = useToolManagement();
|
||||||
|
|
||||||
// Get selected tool from navigation context
|
// Get selected tool from navigation context
|
||||||
const selectedTool = getSelectedTool(navigationState.selectedToolKey);
|
const selectedTool = getSelectedTool(navigationState.selectedTool);
|
||||||
|
|
||||||
// UI Action creators
|
// UI Action creators
|
||||||
const setSidebarsVisible = useCallback((visible: boolean) => {
|
const setSidebarsVisible = useCallback((visible: boolean) => {
|
||||||
@ -142,7 +142,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
const setPreviewFile = useCallback((file: File | null) => {
|
const setPreviewFile = useCallback((file: File | null) => {
|
||||||
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
|
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
|
||||||
if (file) {
|
if (file) {
|
||||||
actions.setMode('viewer');
|
actions.setWorkbench('viewer');
|
||||||
}
|
}
|
||||||
}, [actions]);
|
}, [actions]);
|
||||||
|
|
||||||
@ -172,7 +172,16 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
|
|
||||||
// Workflow actions (compound actions that coordinate multiple state changes)
|
// Workflow actions (compound actions that coordinate multiple state changes)
|
||||||
const handleToolSelect = useCallback((toolId: string) => {
|
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
|
// Clear search query when selecting a tool
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
@ -189,13 +198,13 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
setLeftPanelView('toolContent');
|
setLeftPanelView('toolContent');
|
||||||
setReaderMode(false); // Disable read mode when selecting tools
|
setReaderMode(false); // Disable read mode when selecting tools
|
||||||
}
|
}
|
||||||
}, [actions, setLeftPanelView, setReaderMode, setSearchQuery]);
|
}, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]);
|
||||||
|
|
||||||
const handleBackToTools = useCallback(() => {
|
const handleBackToTools = useCallback(() => {
|
||||||
setLeftPanelView('toolPicker');
|
setLeftPanelView('toolPicker');
|
||||||
setReaderMode(false);
|
setReaderMode(false);
|
||||||
actions.clearToolSelection();
|
actions.setSelectedTool(null);
|
||||||
}, [setLeftPanelView, setReaderMode, actions]);
|
}, [setLeftPanelView, setReaderMode, actions.setSelectedTool]);
|
||||||
|
|
||||||
const handleReaderToggle = useCallback(() => {
|
const handleReaderToggle = useCallback(() => {
|
||||||
setReaderMode(true);
|
setReaderMode(true);
|
||||||
@ -214,14 +223,23 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
[state.sidebarsVisible, state.readerMode]
|
[state.sidebarsVisible, state.readerMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Enable URL synchronization for tool selection
|
// URL sync for proper tool navigation
|
||||||
useToolWorkflowUrlSync(navigationState.selectedToolKey, actions.selectTool, actions.clearToolSelection, true);
|
useNavigationUrlSync(
|
||||||
|
navigationState.workbench,
|
||||||
|
navigationState.selectedTool,
|
||||||
|
actions.setWorkbench,
|
||||||
|
actions.setSelectedTool,
|
||||||
|
handleToolSelect,
|
||||||
|
() => actions.setSelectedTool(null),
|
||||||
|
toolRegistry,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
// Properly memoized context value
|
// Properly memoized context value
|
||||||
const contextValue = useMemo((): ToolWorkflowContextValue => ({
|
const contextValue = useMemo((): ToolWorkflowContextValue => ({
|
||||||
// State
|
// State
|
||||||
...state,
|
...state,
|
||||||
selectedToolKey: navigationState.selectedToolKey,
|
selectedToolKey: navigationState.selectedTool,
|
||||||
selectedTool,
|
selectedTool,
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
|
|
||||||
@ -232,8 +250,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
setPreviewFile,
|
setPreviewFile,
|
||||||
setPageEditorFunctions,
|
setPageEditorFunctions,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
selectTool: actions.selectTool,
|
selectTool: actions.setSelectedTool,
|
||||||
clearToolSelection: actions.clearToolSelection,
|
clearToolSelection: () => actions.setSelectedTool(null),
|
||||||
|
|
||||||
// Tool Reset Actions
|
// Tool Reset Actions
|
||||||
registerToolReset,
|
registerToolReset,
|
||||||
@ -249,7 +267,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
isPanelVisible,
|
isPanelVisible,
|
||||||
}), [
|
}), [
|
||||||
state,
|
state,
|
||||||
navigationState.selectedToolKey,
|
navigationState.selectedTool,
|
||||||
selectedTool,
|
selectedTool,
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
setSidebarsVisible,
|
setSidebarsVisible,
|
||||||
@ -258,8 +276,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
setPreviewFile,
|
setPreviewFile,
|
||||||
setPageEditorFunctions,
|
setPageEditorFunctions,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
actions.selectTool,
|
actions.setSelectedTool,
|
||||||
actions.clearToolSelection,
|
|
||||||
registerToolReset,
|
registerToolReset,
|
||||||
resetTool,
|
resetTool,
|
||||||
handleToolSelect,
|
handleToolSelect,
|
||||||
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
|
import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
|
||||||
import { BaseToolProps } from '../types/tool';
|
import { BaseToolProps } from '../types/tool';
|
||||||
import { BaseParameters } from '../types/parameters';
|
import { BaseParameters } from '../types/parameters';
|
||||||
|
import { WorkbenchType } from '../types/navigation';
|
||||||
|
|
||||||
export enum SubcategoryId {
|
export enum SubcategoryId {
|
||||||
SIGNING = 'signing',
|
SIGNING = 'signing',
|
||||||
@ -28,7 +29,6 @@ export type ToolRegistryEntry = {
|
|||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
name: string;
|
name: string;
|
||||||
component: React.ComponentType<BaseToolProps> | null;
|
component: React.ComponentType<BaseToolProps> | null;
|
||||||
view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external';
|
|
||||||
description: string;
|
description: string;
|
||||||
categoryId: ToolCategoryId;
|
categoryId: ToolCategoryId;
|
||||||
subcategoryId: SubcategoryId;
|
subcategoryId: SubcategoryId;
|
||||||
@ -37,6 +37,10 @@ export type ToolRegistryEntry = {
|
|||||||
endpoints?: string[];
|
endpoints?: string[];
|
||||||
link?: string;
|
link?: string;
|
||||||
type?: 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
|
// Operation configuration for automation
|
||||||
operationConfig?: ToolOperationConfig<any>;
|
operationConfig?: ToolOperationConfig<any>;
|
||||||
// Settings component for automation configuration
|
// Settings component for automation configuration
|
||||||
@ -107,3 +111,30 @@ export const getAllApplicationEndpoints = (
|
|||||||
const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : [];
|
const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : [];
|
||||||
return Array.from(new Set([...toolEp, ...convEp]));
|
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;
|
||||||
|
};
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -45,16 +45,16 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
|
|||||||
|
|
||||||
export function useSuggestedTools(): SuggestedTool[] {
|
export function useSuggestedTools(): SuggestedTool[] {
|
||||||
const { actions } = useNavigationActions();
|
const { actions } = useNavigationActions();
|
||||||
const { selectedToolKey } = useNavigationState();
|
const { selectedTool } = useNavigationState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
// Filter out the current tool
|
// 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
|
// Add navigation function to each tool
|
||||||
return filteredTools.map(tool => ({
|
return filteredTools.map(tool => ({
|
||||||
...tool,
|
...tool,
|
||||||
navigate: () => actions.handleToolSelect(tool.id)
|
navigate: () => actions.setSelectedTool(tool.id)
|
||||||
}));
|
}));
|
||||||
}, [selectedToolKey, actions]);
|
}, [selectedTool, actions]);
|
||||||
}
|
}
|
||||||
|
@ -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 { useEffect, useCallback } from 'react';
|
||||||
import { ModeType } from '../types/navigation';
|
import { WorkbenchType, ToolId, ToolRoute } from '../types/navigation';
|
||||||
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
|
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(
|
export function useNavigationUrlSync(
|
||||||
currentMode: ModeType,
|
workbench: WorkbenchType,
|
||||||
setMode: (mode: ModeType) => void,
|
selectedTool: ToolId | null,
|
||||||
|
setWorkbench: (workbench: WorkbenchType) => void,
|
||||||
|
setSelectedTool: (toolId: ToolId | null) => void,
|
||||||
|
handleToolSelect: (toolId: string) => void,
|
||||||
|
clearToolSelection: () => void,
|
||||||
|
registry: ToolRegistry,
|
||||||
enableSync: boolean = true
|
enableSync: boolean = true
|
||||||
) {
|
) {
|
||||||
// Initialize mode from URL on mount
|
// Initialize workbench and tool from URL on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enableSync) return;
|
if (!enableSync) return;
|
||||||
|
|
||||||
const route = parseToolRoute();
|
const route = parseToolRoute(registry);
|
||||||
if (route.mode !== currentMode) {
|
if (route.toolId !== selectedTool) {
|
||||||
setMode(route.mode);
|
if (route.toolId) {
|
||||||
|
handleToolSelect(route.toolId);
|
||||||
|
} else {
|
||||||
|
clearToolSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []); // Only run on mount
|
}, []); // Only run on mount
|
||||||
|
|
||||||
// Update URL when mode changes
|
// Update URL when tool or workbench changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enableSync) return;
|
if (!enableSync) return;
|
||||||
|
|
||||||
// Only update URL for actual tool modes, not internal UI modes
|
if (selectedTool) {
|
||||||
// URL clearing is handled by useToolWorkflowUrlSync when selectedToolKey becomes null
|
updateToolRoute(selectedTool, registry);
|
||||||
if (currentMode !== 'fileEditor' && currentMode !== 'pageEditor' && currentMode !== 'viewer') {
|
} else {
|
||||||
updateToolRoute(currentMode, currentMode);
|
// Clear URL when no tool is selected
|
||||||
|
clearToolRoute();
|
||||||
}
|
}
|
||||||
}, [currentMode, enableSync]);
|
}, [selectedTool, registry, enableSync]);
|
||||||
|
|
||||||
// Handle browser back/forward navigation
|
// Handle browser back/forward navigation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enableSync) return;
|
if (!enableSync) return;
|
||||||
|
|
||||||
const handlePopState = () => {
|
const handlePopState = () => {
|
||||||
const route = parseToolRoute();
|
const route = parseToolRoute(registry);
|
||||||
if (route.mode !== currentMode) {
|
if (route.toolId !== selectedTool) {
|
||||||
setMode(route.mode);
|
if (route.toolId) {
|
||||||
|
handleToolSelect(route.toolId);
|
||||||
|
} else {
|
||||||
|
clearToolSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('popstate', handlePopState);
|
window.addEventListener('popstate', handlePopState);
|
||||||
return () => window.removeEventListener('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(
|
export function useToolNavigation(registry: ToolRegistry) {
|
||||||
selectedToolKey: string | null,
|
const navigateToTool = useCallback((toolId: ToolId) => {
|
||||||
selectTool: (toolKey: string) => void,
|
updateToolRoute(toolId, registry);
|
||||||
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);
|
|
||||||
|
|
||||||
// Dispatch a custom event to notify other components
|
// Dispatch a custom event to notify other components
|
||||||
window.dispatchEvent(new CustomEvent('toolNavigation', {
|
window.dispatchEvent(new CustomEvent('toolNavigation', {
|
||||||
detail: { toolKey }
|
detail: { toolId }
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, [registry]);
|
||||||
|
|
||||||
const navigateToHome = useCallback(() => {
|
const navigateToHome = useCallback(() => {
|
||||||
clearToolRoute();
|
clearToolRoute();
|
||||||
|
|
||||||
// Dispatch a custom event to notify other components
|
// Dispatch a custom event to notify other components
|
||||||
window.dispatchEvent(new CustomEvent('toolNavigation', {
|
window.dispatchEvent(new CustomEvent('toolNavigation', {
|
||||||
detail: { toolKey: null }
|
detail: { toolId: null }
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -126,3 +93,14 @@ export function useToolNavigation() {
|
|||||||
navigateToHome
|
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;
|
||||||
|
}
|
@ -8,7 +8,6 @@ import { BrowserRouter } from 'react-router-dom';
|
|||||||
import App from './App';
|
import App from './App';
|
||||||
import './i18n'; // Initialize i18next
|
import './i18n'; // Initialize i18next
|
||||||
import { PostHogProvider } from 'posthog-js/react';
|
import { PostHogProvider } from 'posthog-js/react';
|
||||||
import { ScarfPixel } from './components/ScarfPixel';
|
|
||||||
|
|
||||||
// Compute initial color scheme
|
// Compute initial color scheme
|
||||||
function getInitialScheme(): 'light' | 'dark' {
|
function getInitialScheme(): 'light' | 'dark' {
|
||||||
@ -40,7 +39,6 @@ root.render(
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<ScarfPixel />
|
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</PostHogProvider>
|
</PostHogProvider>
|
||||||
|
@ -2,7 +2,7 @@ import React, { useState, useMemo, useEffect } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFileContext } from "../contexts/FileContext";
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
import { useFileSelection } from "../contexts/FileContext";
|
import { useFileSelection } from "../contexts/FileContext";
|
||||||
import { useNavigation } from "../contexts/NavigationContext";
|
import { useNavigationActions } from "../contexts/NavigationContext";
|
||||||
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
import { useToolWorkflow } from "../contexts/ToolWorkflowContext";
|
||||||
|
|
||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
@ -21,7 +21,7 @@ import { AUTOMATION_STEPS } from "../constants/automation";
|
|||||||
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { selectedFiles } = useFileSelection();
|
const { selectedFiles } = useFileSelection();
|
||||||
const { setMode } = useNavigation();
|
const { actions } = useNavigationActions();
|
||||||
const { registerToolReset } = useToolWorkflow();
|
const { registerToolReset } = useToolWorkflow();
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION);
|
const [currentStep, setCurrentStep] = useState<AutomationStep>(AUTOMATION_STEPS.SELECTION);
|
||||||
@ -223,7 +223,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
title: t('automate.reviewTitle', 'Automation Results'),
|
title: t('automate.reviewTitle', 'Automation Results'),
|
||||||
onFileClick: (file: File) => {
|
onFileClick: (file: File) => {
|
||||||
onPreviewFile?.(file);
|
onPreviewFile?.(file);
|
||||||
setMode('viewer');
|
actions.setWorkbench('viewer');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -43,7 +43,7 @@ const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolP
|
|||||||
const handleThumbnailClick = (file: File) => {
|
const handleThumbnailClick = (file: File) => {
|
||||||
onPreviewFile?.(file);
|
onPreviewFile?.(file);
|
||||||
sessionStorage.setItem("previousMode", "removeCertificateSign");
|
sessionStorage.setItem("previousMode", "removeCertificateSign");
|
||||||
actions.setMode("viewer");
|
actions.setWorkbench("viewer");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSettingsReset = () => {
|
const handleSettingsReset = () => {
|
||||||
|
@ -43,7 +43,7 @@ const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
const handleThumbnailClick = (file: File) => {
|
const handleThumbnailClick = (file: File) => {
|
||||||
onPreviewFile?.(file);
|
onPreviewFile?.(file);
|
||||||
sessionStorage.setItem("previousMode", "repair");
|
sessionStorage.setItem("previousMode", "repair");
|
||||||
actions.setMode("viewer");
|
actions.setWorkbench("viewer");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSettingsReset = () => {
|
const handleSettingsReset = () => {
|
||||||
|
@ -43,7 +43,7 @@ const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps)
|
|||||||
const handleThumbnailClick = (file: File) => {
|
const handleThumbnailClick = (file: File) => {
|
||||||
onPreviewFile?.(file);
|
onPreviewFile?.(file);
|
||||||
sessionStorage.setItem("previousMode", "single-large-page");
|
sessionStorage.setItem("previousMode", "single-large-page");
|
||||||
actions.setMode("viewer");
|
actions.setWorkbench("viewer");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSettingsReset = () => {
|
const handleSettingsReset = () => {
|
||||||
|
@ -43,7 +43,7 @@ const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =
|
|||||||
const handleThumbnailClick = (file: File) => {
|
const handleThumbnailClick = (file: File) => {
|
||||||
onPreviewFile?.(file);
|
onPreviewFile?.(file);
|
||||||
sessionStorage.setItem("previousMode", "unlockPdfForms");
|
sessionStorage.setItem("previousMode", "unlockPdfForms");
|
||||||
actions.setMode("viewer");
|
actions.setWorkbench("viewer");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSettingsReset = () => {
|
const handleSettingsReset = () => {
|
||||||
|
@ -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
|
// Define workbench values once as source of truth
|
||||||
export type ModeType =
|
const WORKBENCH_TYPES = ['viewer', 'pageEditor', 'fileEditor'] as const;
|
||||||
| 'viewer'
|
|
||||||
| 'pageEditor'
|
|
||||||
| 'fileEditor'
|
|
||||||
| 'merge'
|
|
||||||
| 'split'
|
|
||||||
| 'compress'
|
|
||||||
| 'ocr'
|
|
||||||
| 'convert'
|
|
||||||
| 'sanitize'
|
|
||||||
| 'addPassword'
|
|
||||||
| 'changePermissions'
|
|
||||||
| 'addWatermark'
|
|
||||||
| 'removePassword'
|
|
||||||
| 'single-large-page'
|
|
||||||
| 'repair'
|
|
||||||
| 'unlockPdfForms'
|
|
||||||
| 'removeCertificateSign';
|
|
||||||
|
|
||||||
// Utility functions for mode handling
|
// Workbench types - how the user interacts with content
|
||||||
export const isValidMode = (mode: string): mode is ModeType => {
|
export type WorkbenchType = typeof WORKBENCH_TYPES[number];
|
||||||
const validModes: ModeType[] = [
|
|
||||||
'viewer', 'pageEditor', 'fileEditor', 'merge', 'split',
|
// Tool identity - what PDF operation we're performing (derived from registry)
|
||||||
'compress', 'ocr', 'convert', 'addPassword', 'changePermissions',
|
export type ToolId = string;
|
||||||
'sanitize', 'addWatermark', 'removePassword', 'single-large-page',
|
|
||||||
'repair', 'unlockPdfForms', 'removeCertificateSign'
|
// Navigation state
|
||||||
];
|
export interface NavigationState {
|
||||||
return validModes.includes(mode as ModeType);
|
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
|
// Route parsing result
|
||||||
export interface ToolRoute {
|
export interface ToolRoute {
|
||||||
mode: ModeType;
|
workbench: WorkbenchType;
|
||||||
toolKey: string | null;
|
toolId: ToolId | null;
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
* Navigation action interfaces to break circular dependencies
|
* Navigation action interfaces to break circular dependencies
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ModeType } from './navigation';
|
import { WorkbenchType, ToolId } from './navigation';
|
||||||
|
|
||||||
export interface NavigationActions {
|
export interface NavigationActions {
|
||||||
setMode: (mode: ModeType) => void;
|
setWorkbench: (workbench: WorkbenchType) => void;
|
||||||
|
setSelectedTool: (toolId: ToolId | null) => void;
|
||||||
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
||||||
showNavigationWarning: (show: boolean) => void;
|
showNavigationWarning: (show: boolean) => void;
|
||||||
requestNavigation: (navigationFn: () => void) => void;
|
requestNavigation: (navigationFn: () => void) => void;
|
||||||
@ -14,7 +15,8 @@ export interface NavigationActions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigationState {
|
export interface NavigationState {
|
||||||
currentMode: ModeType;
|
workbench: WorkbenchType;
|
||||||
|
selectedTool: ToolId | null;
|
||||||
hasUnsavedChanges: boolean;
|
hasUnsavedChanges: boolean;
|
||||||
pendingNavigation: (() => void) | null;
|
pendingNavigation: (() => void) | null;
|
||||||
showNavigationWarning: boolean;
|
showNavigationWarning: boolean;
|
||||||
|
@ -1,119 +1,64 @@
|
|||||||
/**
|
/**
|
||||||
* URL routing utilities for tool navigation
|
* URL routing utilities for tool navigation with registry support
|
||||||
* Provides clean URL routing for the V2 tool system
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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
|
* 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 path = window.location.pathname;
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
// Extract tool from URL path (e.g., /split-pdf -> split)
|
// Try to find tool by URL path
|
||||||
const toolMatch = path.match(/\/([a-zA-Z-]+)(?:-pdf)?$/);
|
for (const [toolId, tool] of Object.entries(registry)) {
|
||||||
if (toolMatch) {
|
const toolUrlPath = getToolUrlPath(toolId, tool);
|
||||||
const toolKey = toolMatch[1].toLowerCase();
|
if (path === toolUrlPath) {
|
||||||
|
|
||||||
// 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' },
|
|
||||||
'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) {
|
|
||||||
return {
|
return {
|
||||||
mode: mapping.mode,
|
workbench: getToolWorkbench(tool),
|
||||||
toolKey: mapping.toolKey
|
toolId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for query parameter fallback (e.g., ?tool=split)
|
// Check for query parameter fallback (e.g., ?tool=split)
|
||||||
const toolParam = searchParams.get('tool');
|
const toolParam = searchParams.get('tool');
|
||||||
if (toolParam && isValidModeType(toolParam)) {
|
if (toolParam && isValidToolId(toolParam, registry)) {
|
||||||
|
const tool = registry[toolParam];
|
||||||
return {
|
return {
|
||||||
mode: toolParam as ModeType,
|
workbench: getToolWorkbench(tool),
|
||||||
toolKey: toolParam
|
toolId: toolParam
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to page editor for home page
|
// Default to fileEditor workbench for home page
|
||||||
return {
|
return {
|
||||||
mode: getDefaultMode(),
|
workbench: getDefaultWorkbench(),
|
||||||
toolKey: null
|
toolId: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the URL to reflect the current tool selection
|
* 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 currentPath = window.location.pathname;
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
// Don't create URLs for internal UI modes
|
const tool = registry[toolId];
|
||||||
if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') {
|
if (!tool) {
|
||||||
// If we're switching to an internal mode, clear any existing tool URL
|
console.warn(`Tool ${toolId} not found in registry`);
|
||||||
if (currentPath !== '/') {
|
|
||||||
clearToolRoute();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newPath = '/';
|
|
||||||
|
|
||||||
// Map modes to URL paths (only for actual tools)
|
const newPath = getToolUrlPath(toolId, tool);
|
||||||
if (toolKey) {
|
|
||||||
const pathMappings: Record<string, string> = {
|
|
||||||
'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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove tool query parameter since we're using path-based routing
|
// Remove tool query parameter since we're using path-based routing
|
||||||
searchParams.delete('tool');
|
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 {
|
export function getToolDisplayName(toolId: ToolId, registry: ToolRegistry): string {
|
||||||
const displayNames: Record<string, string> = {
|
const tool = registry[toolId];
|
||||||
'split': 'Split PDF',
|
return tool ? tool.name : toolId;
|
||||||
'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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: isValidMode is now imported from types/navigation.ts
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate shareable URL for current tool state
|
* Generate shareable URL for current tool state using registry
|
||||||
* Only generates URLs for actual tools, not internal UI modes
|
|
||||||
*/
|
*/
|
||||||
export function generateShareableUrl(mode: ModeType, toolKey?: string): string {
|
export function generateShareableUrl(toolId: ToolId | null, registry: ToolRegistry): string {
|
||||||
const baseUrl = window.location.origin;
|
const baseUrl = window.location.origin;
|
||||||
|
|
||||||
// Don't generate URLs for internal UI modes
|
if (!toolId || !registry[toolId]) {
|
||||||
if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') {
|
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolKey) {
|
const tool = registry[toolId];
|
||||||
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',
|
|
||||||
'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}`;
|
const path = getToolUrlPath(toolId, tool);
|
||||||
return `${baseUrl}${path}`;
|
return `${baseUrl}${path}`;
|
||||||
}
|
|
||||||
|
|
||||||
return baseUrl;
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user