mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Feature/v2/reader-and-multitool-navigation (#4514)
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
This commit is contained in:
parent
c7e0ea5b5b
commit
abc0988fdf
@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"unsavedChanges": "You have unsaved changes to your PDF. What would you like to do?",
|
||||||
|
"unsavedChangesTitle": "Unsaved Changes",
|
||||||
|
"keepWorking": "Keep Working",
|
||||||
|
"discardChanges": "Discard Changes",
|
||||||
|
"applyAndContinue": "Apply & Continue",
|
||||||
|
"exportAndContinue": "Export & Continue",
|
||||||
"language": {
|
"language": {
|
||||||
"direction": "ltr"
|
"direction": "ltr"
|
||||||
},
|
},
|
||||||
|
@ -149,7 +149,6 @@ export default function Workbench() {
|
|||||||
<TopControls
|
<TopControls
|
||||||
currentView={currentView}
|
currentView={currentView}
|
||||||
setCurrentView={setCurrentView}
|
setCurrentView={setCurrentView}
|
||||||
selectedToolKey={selectedToolId}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Dismiss All Errors Button */}
|
{/* Dismiss All Errors Button */}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState, useCallback, useRef, useEffect } from "react";
|
import { useState, useCallback, useRef, useEffect } from "react";
|
||||||
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
||||||
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
||||||
|
import { useNavigationGuard } from "../../contexts/NavigationContext";
|
||||||
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
||||||
import { pdfExportService } from "../../services/pdfExportService";
|
import { pdfExportService } from "../../services/pdfExportService";
|
||||||
import { documentManipulationService } from "../../services/documentManipulationService";
|
import { documentManipulationService } from "../../services/documentManipulationService";
|
||||||
@ -36,6 +37,9 @@ const PageEditor = ({
|
|||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
const { actions } = useFileActions();
|
const { actions } = useFileActions();
|
||||||
|
|
||||||
|
// Navigation guard for unsaved changes
|
||||||
|
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||||
|
|
||||||
// Prefer IDs + selectors to avoid array identity churn
|
// Prefer IDs + selectors to avoid array identity churn
|
||||||
const activeFileIds = state.files.ids;
|
const activeFileIds = state.files.ids;
|
||||||
|
|
||||||
@ -82,6 +86,12 @@ const PageEditor = ({
|
|||||||
updateUndoRedoState();
|
updateUndoRedoState();
|
||||||
}, [updateUndoRedoState]);
|
}, [updateUndoRedoState]);
|
||||||
|
|
||||||
|
// Wrapper for executeCommand to track unsaved changes
|
||||||
|
const executeCommandWithTracking = useCallback((command: any) => {
|
||||||
|
undoManagerRef.current.executeCommand(command);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
}, [setHasUnsavedChanges]);
|
||||||
|
|
||||||
// Watch for container size changes to update split line positions
|
// Watch for container size changes to update split line positions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = gridContainerRef.current;
|
const container = gridContainerRef.current;
|
||||||
@ -138,17 +148,16 @@ const PageEditor = ({
|
|||||||
// DOM-first command handlers
|
// DOM-first command handlers
|
||||||
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
|
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
|
||||||
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
||||||
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
executeCommandWithTracking(bulkRotateCommand);
|
||||||
}, []);
|
}, [executeCommandWithTracking]);
|
||||||
|
|
||||||
// Command factory functions for PageThumbnail
|
// Command factory functions for PageThumbnail
|
||||||
const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({
|
const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({
|
||||||
execute: () => {
|
execute: () => {
|
||||||
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
||||||
|
executeCommandWithTracking(bulkRotateCommand);
|
||||||
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
|
||||||
}
|
}
|
||||||
}), []);
|
}), [executeCommandWithTracking]);
|
||||||
|
|
||||||
const createDeleteCommand = useCallback((pageIds: string[]) => ({
|
const createDeleteCommand = useCallback((pageIds: string[]) => ({
|
||||||
execute: () => {
|
execute: () => {
|
||||||
@ -174,10 +183,10 @@ const PageEditor = ({
|
|||||||
() => getPageNumbersFromIds(selectedPageIds),
|
() => getPageNumbersFromIds(selectedPageIds),
|
||||||
closePdf
|
closePdf
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(deleteCommand);
|
executeCommandWithTracking(deleteCommand);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||||
|
|
||||||
const createSplitCommand = useCallback((position: number) => ({
|
const createSplitCommand = useCallback((position: number) => ({
|
||||||
execute: () => {
|
execute: () => {
|
||||||
@ -186,9 +195,9 @@ const PageEditor = ({
|
|||||||
() => splitPositions,
|
() => splitPositions,
|
||||||
setSplitPositions
|
setSplitPositions
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(splitCommand);
|
executeCommandWithTracking(splitCommand);
|
||||||
}
|
}
|
||||||
}), [splitPositions]);
|
}), [splitPositions, executeCommandWithTracking]);
|
||||||
|
|
||||||
// Command executor for PageThumbnail
|
// Command executor for PageThumbnail
|
||||||
const executeCommand = useCallback((command: any) => {
|
const executeCommand = useCallback((command: any) => {
|
||||||
@ -232,8 +241,8 @@ const PageEditor = ({
|
|||||||
() => selectedPageNumbers,
|
() => selectedPageNumbers,
|
||||||
closePdf
|
closePdf
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(deleteCommand);
|
executeCommandWithTracking(deleteCommand);
|
||||||
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers]);
|
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers, executeCommandWithTracking]);
|
||||||
|
|
||||||
const handleDeletePage = useCallback((pageNumber: number) => {
|
const handleDeletePage = useCallback((pageNumber: number) => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
@ -251,8 +260,8 @@ const PageEditor = ({
|
|||||||
() => getPageNumbersFromIds(selectedPageIds),
|
() => getPageNumbersFromIds(selectedPageIds),
|
||||||
closePdf
|
closePdf
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(deleteCommand);
|
executeCommandWithTracking(deleteCommand);
|
||||||
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||||
|
|
||||||
const handleSplit = useCallback(() => {
|
const handleSplit = useCallback(() => {
|
||||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||||
@ -298,8 +307,8 @@ const PageEditor = ({
|
|||||||
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
||||||
};
|
};
|
||||||
|
|
||||||
undoManagerRef.current.executeCommand(smartSplitCommand);
|
executeCommandWithTracking(smartSplitCommand);
|
||||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
|
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||||
|
|
||||||
const handleSplitAll = useCallback(() => {
|
const handleSplitAll = useCallback(() => {
|
||||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||||
@ -344,8 +353,8 @@ const PageEditor = ({
|
|||||||
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
||||||
};
|
};
|
||||||
|
|
||||||
undoManagerRef.current.executeCommand(smartSplitCommand);
|
executeCommandWithTracking(smartSplitCommand);
|
||||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
|
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||||
|
|
||||||
const handlePageBreak = useCallback(() => {
|
const handlePageBreak = useCallback(() => {
|
||||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||||
@ -358,8 +367,8 @@ const PageEditor = ({
|
|||||||
() => displayDocument,
|
() => displayDocument,
|
||||||
setEditedDocument
|
setEditedDocument
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
executeCommandWithTracking(pageBreakCommand);
|
||||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
|
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||||
|
|
||||||
const handlePageBreakAll = useCallback(() => {
|
const handlePageBreakAll = useCallback(() => {
|
||||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||||
@ -372,8 +381,8 @@ const PageEditor = ({
|
|||||||
() => displayDocument,
|
() => displayDocument,
|
||||||
setEditedDocument
|
setEditedDocument
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
executeCommandWithTracking(pageBreakCommand);
|
||||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
|
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||||
|
|
||||||
const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => {
|
const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => {
|
||||||
if (!displayDocument || files.length === 0) return;
|
if (!displayDocument || files.length === 0) return;
|
||||||
@ -416,8 +425,8 @@ const PageEditor = ({
|
|||||||
() => displayDocument,
|
() => displayDocument,
|
||||||
setEditedDocument
|
setEditedDocument
|
||||||
);
|
);
|
||||||
undoManagerRef.current.executeCommand(reorderCommand);
|
executeCommandWithTracking(reorderCommand);
|
||||||
}, [displayDocument, getPageNumbersFromIds]);
|
}, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||||
|
|
||||||
// Helper function to collect source files for multi-file export
|
// Helper function to collect source files for multi-file export
|
||||||
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
|
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
|
||||||
@ -499,13 +508,14 @@ const PageEditor = ({
|
|||||||
|
|
||||||
// Step 4: Download the result
|
// Step 4: Download the result
|
||||||
pdfExportService.downloadFile(result.blob, result.filename);
|
pdfExportService.downloadFile(result.blob, result.filename);
|
||||||
|
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
|
||||||
|
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Export failed:', error);
|
console.error('Export failed:', error);
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
|
}, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
|
||||||
|
|
||||||
const onExportAll = useCallback(async () => {
|
const onExportAll = useCallback(async () => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
@ -552,6 +562,7 @@ const PageEditor = ({
|
|||||||
const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip');
|
const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip');
|
||||||
|
|
||||||
pdfExportService.downloadFile(zipBlob, zipFilename);
|
pdfExportService.downloadFile(zipBlob, zipFilename);
|
||||||
|
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
|
||||||
} else {
|
} else {
|
||||||
// Single document - regular export
|
// Single document - regular export
|
||||||
const sourceFiles = getSourceFiles();
|
const sourceFiles = getSourceFiles();
|
||||||
@ -570,6 +581,7 @@ const PageEditor = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
pdfExportService.downloadFile(result.blob, result.filename);
|
pdfExportService.downloadFile(result.blob, result.filename);
|
||||||
|
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
|
||||||
}
|
}
|
||||||
|
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
@ -577,7 +589,7 @@ const PageEditor = ({
|
|||||||
console.error('Export failed:', error);
|
console.error('Export failed:', error);
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
|
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
|
||||||
|
|
||||||
// Apply DOM changes to document state using dedicated service
|
// Apply DOM changes to document state using dedicated service
|
||||||
const applyChanges = useCallback(() => {
|
const applyChanges = useCallback(() => {
|
||||||
@ -779,7 +791,14 @@ const PageEditor = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<NavigationWarningModal />
|
<NavigationWarningModal
|
||||||
|
onApplyAndContinue={async () => {
|
||||||
|
applyChanges();
|
||||||
|
}}
|
||||||
|
onExportAndContinue={async () => {
|
||||||
|
await onExportAll();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
|
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
|
||||||
import { useNavigationGuard } from '../../contexts/NavigationContext';
|
import { useNavigationGuard } from '../../contexts/NavigationContext';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface NavigationWarningModalProps {
|
interface NavigationWarningModalProps {
|
||||||
onApplyAndContinue?: () => Promise<void>;
|
onApplyAndContinue?: () => Promise<void>;
|
||||||
@ -11,6 +12,8 @@ const NavigationWarningModal = ({
|
|||||||
onApplyAndContinue,
|
onApplyAndContinue,
|
||||||
onExportAndContinue
|
onExportAndContinue
|
||||||
}: NavigationWarningModalProps) => {
|
}: NavigationWarningModalProps) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
showNavigationWarning,
|
showNavigationWarning,
|
||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
@ -28,7 +31,7 @@ const NavigationWarningModal = ({
|
|||||||
confirmNavigation();
|
confirmNavigation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApplyAndContinue = async () => {
|
const _handleApplyAndContinue = async () => {
|
||||||
if (onApplyAndContinue) {
|
if (onApplyAndContinue) {
|
||||||
await onApplyAndContinue();
|
await onApplyAndContinue();
|
||||||
}
|
}
|
||||||
@ -52,52 +55,56 @@ const NavigationWarningModal = ({
|
|||||||
<Modal
|
<Modal
|
||||||
opened={showNavigationWarning}
|
opened={showNavigationWarning}
|
||||||
onClose={handleKeepWorking}
|
onClose={handleKeepWorking}
|
||||||
title="Unsaved Changes"
|
title={t("unsavedChangesTitle", "Unsaved Changes")}
|
||||||
centered
|
centered
|
||||||
|
size="lg"
|
||||||
closeOnClickOutside={false}
|
closeOnClickOutside={false}
|
||||||
closeOnEscape={false}
|
closeOnEscape={false}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text>
|
<Text>
|
||||||
You have unsaved changes to your PDF. What would you like to do?
|
{t("unsavedChanges", "You have unsaved changes to your PDF. What would you like to do?")}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Group justify="flex-end" gap="sm">
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
color="gray"
|
|
||||||
onClick={handleKeepWorking}
|
|
||||||
>
|
|
||||||
Keep Working
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
|
<Group justify="space-between" gap="sm">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
onClick={handleDiscardChanges}
|
onClick={handleDiscardChanges}
|
||||||
>
|
>
|
||||||
Discard Changes
|
{t("discardChanges", "Discard Changes")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{onApplyAndContinue && (
|
<Group gap="sm">
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
color="var(--mantine-color-gray-8)"
|
||||||
|
onClick={handleKeepWorking}
|
||||||
|
>
|
||||||
|
{t("keepWorking", "Keep Working")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* TODO:: Add this back in when it works */}
|
||||||
|
{/* {onApplyAndContinue && (
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
onClick={handleApplyAndContinue}
|
onClick={handleApplyAndContinue}
|
||||||
>
|
>
|
||||||
Apply & Continue
|
{t("applyAndContinue", "Apply & Continue")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{onExportAndContinue && (
|
{onExportAndContinue && (
|
||||||
<Button
|
<Button
|
||||||
color="green"
|
|
||||||
onClick={handleExportAndContinue}
|
onClick={handleExportAndContinue}
|
||||||
>
|
>
|
||||||
Export & Continue
|
{t("exportAndContinue", "Export & Continue")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
@ -19,7 +19,7 @@ const viewOptionStyle = {
|
|||||||
|
|
||||||
|
|
||||||
// Build view options showing text always
|
// Build view options showing text always
|
||||||
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null, isToolSelected: boolean) => {
|
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => {
|
||||||
const viewerOption = {
|
const viewerOption = {
|
||||||
label: (
|
label: (
|
||||||
<div style={viewOptionStyle as React.CSSProperties}>
|
<div style={viewOptionStyle as React.CSSProperties}>
|
||||||
@ -75,7 +75,7 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
|||||||
// Build options array conditionally
|
// Build options array conditionally
|
||||||
return [
|
return [
|
||||||
viewerOption,
|
viewerOption,
|
||||||
...(isToolSelected ? [] : [pageEditorOption]),
|
pageEditorOption,
|
||||||
fileEditorOption,
|
fileEditorOption,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@ -83,19 +83,15 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
|||||||
interface TopControlsProps {
|
interface TopControlsProps {
|
||||||
currentView: WorkbenchType;
|
currentView: WorkbenchType;
|
||||||
setCurrentView: (view: WorkbenchType) => void;
|
setCurrentView: (view: WorkbenchType) => void;
|
||||||
selectedToolKey?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TopControls = ({
|
const TopControls = ({
|
||||||
currentView,
|
currentView,
|
||||||
setCurrentView,
|
setCurrentView,
|
||||||
selectedToolKey,
|
|
||||||
}: TopControlsProps) => {
|
}: TopControlsProps) => {
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
|
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
|
||||||
|
|
||||||
const isToolSelected = selectedToolKey !== null;
|
|
||||||
|
|
||||||
const handleViewChange = useCallback((view: string) => {
|
const handleViewChange = useCallback((view: string) => {
|
||||||
if (!isValidWorkbench(view)) {
|
if (!isValidWorkbench(view)) {
|
||||||
return;
|
return;
|
||||||
@ -122,7 +118,7 @@ const TopControls = ({
|
|||||||
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
|
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
|
||||||
<div className="flex justify-center mt-[0.5rem]">
|
<div className="flex justify-center mt-[0.5rem]">
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
data={createViewOptions(currentView, switchingTo, isToolSelected)}
|
data={createViewOptions(currentView, switchingTo)}
|
||||||
value={currentView}
|
value={currentView}
|
||||||
onChange={handleViewChange}
|
onChange={handleViewChange}
|
||||||
color="blue"
|
color="blue"
|
||||||
|
@ -33,8 +33,11 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
|||||||
const { getHomeNavigation } = useSidebarNavigation();
|
const { getHomeNavigation } = useSidebarNavigation();
|
||||||
|
|
||||||
// Determine if the indicator should be visible (do not require selectedTool to be resolved yet)
|
// Determine if the indicator should be visible (do not require selectedTool to be resolved yet)
|
||||||
|
// Special case: multiTool should always show even when sidebars are hidden
|
||||||
const indicatorShouldShow = Boolean(
|
const indicatorShouldShow = Boolean(
|
||||||
selectedToolKey && leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)
|
selectedToolKey &&
|
||||||
|
((leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)) ||
|
||||||
|
selectedToolKey === 'multiTool')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Local animation and hover state
|
// Local animation and hover state
|
||||||
|
@ -12,7 +12,7 @@ export const isNavButtonActive = (
|
|||||||
isFilesModalOpen: boolean,
|
isFilesModalOpen: boolean,
|
||||||
configModalOpen: boolean,
|
configModalOpen: boolean,
|
||||||
selectedToolKey?: string | null,
|
selectedToolKey?: string | null,
|
||||||
leftPanelView?: 'toolPicker' | 'toolContent'
|
leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden'
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const isActiveByLocalState = config.type === 'navigation' && activeButton === config.id;
|
const isActiveByLocalState = config.type === 'navigation' && activeButton === config.id;
|
||||||
const isActiveByContext =
|
const isActiveByContext =
|
||||||
@ -35,7 +35,7 @@ export const getNavButtonStyle = (
|
|||||||
isFilesModalOpen: boolean,
|
isFilesModalOpen: boolean,
|
||||||
configModalOpen: boolean,
|
configModalOpen: boolean,
|
||||||
selectedToolKey?: string | null,
|
selectedToolKey?: string | null,
|
||||||
leftPanelView?: 'toolPicker' | 'toolContent'
|
leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden'
|
||||||
) => {
|
) => {
|
||||||
const isActive = isNavButtonActive(
|
const isActive = isNavButtonActive(
|
||||||
config,
|
config,
|
||||||
|
@ -7,6 +7,7 @@ import ToolSearch from './toolPicker/ToolSearch';
|
|||||||
import { useSidebarContext } from "../../contexts/SidebarContext";
|
import { useSidebarContext } from "../../contexts/SidebarContext";
|
||||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||||
import { ScrollArea } from '@mantine/core';
|
import { ScrollArea } from '@mantine/core';
|
||||||
|
import { ToolId } from '../../types/toolId';
|
||||||
|
|
||||||
// No props needed - component uses context
|
// No props needed - component uses context
|
||||||
|
|
||||||
@ -71,7 +72,7 @@ export default function ToolPanel() {
|
|||||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||||
<SearchResults
|
<SearchResults
|
||||||
filteredTools={filteredTools}
|
filteredTools={filteredTools}
|
||||||
onSelect={handleToolSelect}
|
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -80,7 +81,7 @@ export default function ToolPanel() {
|
|||||||
<div className="flex-1 flex flex-col overflow-auto">
|
<div className="flex-1 flex flex-col overflow-auto">
|
||||||
<ToolPicker
|
<ToolPicker
|
||||||
selectedToolKey={selectedToolKey}
|
selectedToolKey={selectedToolKey}
|
||||||
onSelect={handleToolSelect}
|
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||||
filteredTools={filteredTools}
|
filteredTools={filteredTools}
|
||||||
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
|
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
|
||||||
/>
|
/>
|
||||||
|
@ -17,7 +17,8 @@ interface ToolButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym }) => {
|
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym }) => {
|
||||||
const isUnavailable = !tool.component && !tool.link;
|
// Special case: read and multiTool are navigational tools that are always available
|
||||||
|
const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
|
||||||
const { getToolNavigation } = useToolNavigation();
|
const { getToolNavigation } = useToolNavigation();
|
||||||
|
|
||||||
const handleClick = (id: string) => {
|
const handleClick = (id: string) => {
|
||||||
|
@ -109,16 +109,34 @@ export const NavigationProvider: React.FC<{
|
|||||||
|
|
||||||
const actions: NavigationContextActions = {
|
const actions: NavigationContextActions = {
|
||||||
setWorkbench: useCallback((workbench: WorkbenchType) => {
|
setWorkbench: useCallback((workbench: WorkbenchType) => {
|
||||||
|
// If we're leaving pageEditor workbench and have unsaved changes, request navigation
|
||||||
|
if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
|
||||||
|
const performWorkbenchChange = () => {
|
||||||
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
||||||
}, []),
|
};
|
||||||
|
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
|
||||||
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
||||||
|
}
|
||||||
|
}, [state.workbench, state.hasUnsavedChanges]),
|
||||||
|
|
||||||
setSelectedTool: useCallback((toolId: ToolId | null) => {
|
setSelectedTool: useCallback((toolId: ToolId | null) => {
|
||||||
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } });
|
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } });
|
||||||
}, []),
|
}, []),
|
||||||
|
|
||||||
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
|
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
|
||||||
|
// If we're leaving pageEditor workbench and have unsaved changes, request navigation
|
||||||
|
if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
|
||||||
|
const performWorkbenchChange = () => {
|
||||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
||||||
}, []),
|
};
|
||||||
|
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
|
||||||
|
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
||||||
|
} else {
|
||||||
|
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
||||||
|
}
|
||||||
|
}, [state.workbench, state.hasUnsavedChanges]),
|
||||||
|
|
||||||
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
|
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
|
||||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
||||||
|
@ -17,7 +17,7 @@ import { filterToolRegistryByQuery } from '../utils/toolSearch';
|
|||||||
interface ToolWorkflowState {
|
interface ToolWorkflowState {
|
||||||
// UI State
|
// UI State
|
||||||
sidebarsVisible: boolean;
|
sidebarsVisible: boolean;
|
||||||
leftPanelView: 'toolPicker' | 'toolContent';
|
leftPanelView: 'toolPicker' | 'toolContent' | 'hidden';
|
||||||
readerMode: boolean;
|
readerMode: boolean;
|
||||||
|
|
||||||
// File/Preview State
|
// File/Preview State
|
||||||
@ -31,7 +31,7 @@ interface ToolWorkflowState {
|
|||||||
// Actions
|
// Actions
|
||||||
type ToolWorkflowAction =
|
type ToolWorkflowAction =
|
||||||
| { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean }
|
| { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean }
|
||||||
| { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' }
|
| { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' | 'hidden' }
|
||||||
| { type: 'SET_READER_MODE'; payload: boolean }
|
| { type: 'SET_READER_MODE'; payload: boolean }
|
||||||
| { type: 'SET_PREVIEW_FILE'; payload: File | null }
|
| { type: 'SET_PREVIEW_FILE'; payload: File | null }
|
||||||
| { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null }
|
| { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null }
|
||||||
@ -80,7 +80,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
|||||||
|
|
||||||
// UI Actions
|
// UI Actions
|
||||||
setSidebarsVisible: (visible: boolean) => void;
|
setSidebarsVisible: (visible: boolean) => void;
|
||||||
setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void;
|
setLeftPanelView: (view: 'toolPicker' | 'toolContent' | 'hidden') => void;
|
||||||
setReaderMode: (mode: boolean) => void;
|
setReaderMode: (mode: boolean) => void;
|
||||||
setPreviewFile: (file: File | null) => void;
|
setPreviewFile: (file: File | null) => void;
|
||||||
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
|
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
|
||||||
@ -96,7 +96,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
|||||||
resetTool: (toolId: string) => void;
|
resetTool: (toolId: string) => void;
|
||||||
|
|
||||||
// Workflow Actions (compound actions)
|
// Workflow Actions (compound actions)
|
||||||
handleToolSelect: (toolId: string) => void;
|
handleToolSelect: (toolId: ToolId) => void;
|
||||||
handleBackToTools: () => void;
|
handleBackToTools: () => void;
|
||||||
handleReaderToggle: () => void;
|
handleReaderToggle: () => void;
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
|
dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent') => {
|
const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent' | 'hidden') => {
|
||||||
dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view });
|
dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -180,7 +180,26 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
}, []); // Empty dependency array makes this stable
|
}, []); // Empty dependency array makes this stable
|
||||||
|
|
||||||
// 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: ToolId) => {
|
||||||
|
// Handle read tool selection - should behave exactly like QuickAccessBar read button
|
||||||
|
if (toolId === 'read') {
|
||||||
|
setReaderMode(true);
|
||||||
|
actions.setSelectedTool('read');
|
||||||
|
actions.setWorkbench('viewer');
|
||||||
|
setSearchQuery('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multiTool selection - enable page editor workbench and hide left panel
|
||||||
|
if (toolId === 'multiTool') {
|
||||||
|
setReaderMode(false);
|
||||||
|
setLeftPanelView('hidden');
|
||||||
|
actions.setSelectedTool('multiTool');
|
||||||
|
actions.setWorkbench('pageEditor');
|
||||||
|
setSearchQuery('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set the selected tool and determine the appropriate workbench
|
// Set the selected tool and determine the appropriate workbench
|
||||||
const validToolId = isValidToolId(toolId) ? toolId : null;
|
const validToolId = isValidToolId(toolId) ? toolId : null;
|
||||||
actions.setSelectedTool(validToolId);
|
actions.setSelectedTool(validToolId);
|
||||||
@ -195,19 +214,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
|
|
||||||
// Clear search query when selecting a tool
|
// Clear search query when selecting a tool
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
|
|
||||||
// Handle view switching logic
|
|
||||||
if (toolId === 'allTools' || toolId === 'read' || toolId === 'view-pdf') {
|
|
||||||
setLeftPanelView('toolPicker');
|
|
||||||
if (toolId === 'read' || toolId === 'view-pdf') {
|
|
||||||
setReaderMode(true);
|
|
||||||
} else {
|
|
||||||
setReaderMode(false);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setLeftPanelView('toolContent');
|
setLeftPanelView('toolContent');
|
||||||
setReaderMode(false); // Disable read mode when selecting tools
|
setReaderMode(false); // Disable read mode when selecting tools
|
||||||
}
|
|
||||||
}, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]);
|
}, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]);
|
||||||
|
|
||||||
const handleBackToTools = useCallback(() => {
|
const handleBackToTools = useCallback(() => {
|
||||||
@ -227,8 +235,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
}, [toolRegistry, state.searchQuery]);
|
}, [toolRegistry, state.searchQuery]);
|
||||||
|
|
||||||
const isPanelVisible = useMemo(() =>
|
const isPanelVisible = useMemo(() =>
|
||||||
state.sidebarsVisible && !state.readerMode,
|
state.sidebarsVisible && !state.readerMode && state.leftPanelView !== 'hidden',
|
||||||
[state.sidebarsVisible, state.readerMode]
|
[state.sidebarsVisible, state.readerMode, state.leftPanelView]
|
||||||
);
|
);
|
||||||
|
|
||||||
// URL sync for proper tool navigation
|
// URL sync for proper tool navigation
|
||||||
|
@ -183,8 +183,32 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const allTools: ToolRegistry = {
|
const allTools: ToolRegistry = {
|
||||||
|
// Recommended Tools in order
|
||||||
|
multiTool: {
|
||||||
|
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
|
name: t("home.multiTool.title", "Multi-Tool"),
|
||||||
|
component: null,
|
||||||
|
workbench: "pageEditor",
|
||||||
|
description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"),
|
||||||
|
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||||
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
|
maxFiles: -1,
|
||||||
|
synonyms: getSynonyms(t, "multiTool"),
|
||||||
|
},
|
||||||
|
merge: {
|
||||||
|
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
|
name: t("home.merge.title", "Merge"),
|
||||||
|
component: Merge,
|
||||||
|
description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
|
||||||
|
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||||
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
|
maxFiles: -1,
|
||||||
|
endpoints: ["merge-pdfs"],
|
||||||
|
operationConfig: mergeOperationConfig,
|
||||||
|
settingsComponent: MergeSettings,
|
||||||
|
synonyms: getSynonyms(t, "merge")
|
||||||
|
},
|
||||||
// Signing
|
// Signing
|
||||||
|
|
||||||
certSign: {
|
certSign: {
|
||||||
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.certSign.title", "Certificate Sign"),
|
name: t("home.certSign.title", "Certificate Sign"),
|
||||||
@ -792,30 +816,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
settingsComponent: ConvertSettings,
|
settingsComponent: ConvertSettings,
|
||||||
synonyms: getSynonyms(t, "convert")
|
synonyms: getSynonyms(t, "convert")
|
||||||
},
|
},
|
||||||
merge: {
|
|
||||||
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
|
|
||||||
name: t("home.merge.title", "Merge"),
|
|
||||||
component: Merge,
|
|
||||||
description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
|
|
||||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
|
||||||
subcategoryId: SubcategoryId.GENERAL,
|
|
||||||
maxFiles: -1,
|
|
||||||
endpoints: ["merge-pdfs"],
|
|
||||||
operationConfig: mergeOperationConfig,
|
|
||||||
settingsComponent: MergeSettings,
|
|
||||||
synonyms: getSynonyms(t, "merge")
|
|
||||||
},
|
|
||||||
multiTool: {
|
|
||||||
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
|
|
||||||
name: t("home.multiTool.title", "Multi-Tool"),
|
|
||||||
component: null,
|
|
||||||
workbench: "pageEditor",
|
|
||||||
description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"),
|
|
||||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
|
||||||
subcategoryId: SubcategoryId.GENERAL,
|
|
||||||
maxFiles: -1,
|
|
||||||
synonyms: getSynonyms(t, "multiTool"),
|
|
||||||
},
|
|
||||||
ocr: {
|
ocr: {
|
||||||
icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.ocr.title", "OCR"),
|
name: t("home.ocr.title", "OCR"),
|
||||||
|
@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { ToolRegistryEntry, getToolUrlPath } from '../data/toolsTaxonomy';
|
import { ToolRegistryEntry, getToolUrlPath } from '../data/toolsTaxonomy';
|
||||||
import { useToolWorkflow } from '../contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '../contexts/ToolWorkflowContext';
|
||||||
import { handleUnlessSpecialClick } from '../utils/clickHandlers';
|
import { handleUnlessSpecialClick } from '../utils/clickHandlers';
|
||||||
|
import { ToolId } from '../types/toolId';
|
||||||
|
|
||||||
export interface ToolNavigationProps {
|
export interface ToolNavigationProps {
|
||||||
/** Full URL for the tool (for href attribute) */
|
/** Full URL for the tool (for href attribute) */
|
||||||
@ -34,7 +35,7 @@ export function useToolNavigation(): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use SPA navigation for internal tools
|
// Use SPA navigation for internal tools
|
||||||
handleToolSelect(toolId);
|
handleToolSelect(toolId as ToolId);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,7 +72,10 @@ export function useToolSections(
|
|||||||
const subcategoryId = s as SubcategoryId;
|
const subcategoryId = s as SubcategoryId;
|
||||||
if (!quick[subcategoryId]) quick[subcategoryId] = [];
|
if (!quick[subcategoryId]) quick[subcategoryId] = [];
|
||||||
// Only include ready tools (have a component or external link) in Quick Access
|
// Only include ready tools (have a component or external link) in Quick Access
|
||||||
const readyTools = tools.filter(({ tool }) => tool.component !== null || !!tool.link);
|
// Special case: read and multiTool are navigational tools that don't need components
|
||||||
|
const readyTools = tools.filter(({ tool, id }) =>
|
||||||
|
tool.component !== null || !!tool.link || id === 'read' || id === 'multiTool'
|
||||||
|
);
|
||||||
quick[subcategoryId].push(...readyTools);
|
quick[subcategoryId].push(...readyTools);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import { withBasePath } from '../constants/app';
|
|||||||
*/
|
*/
|
||||||
export function useNavigationUrlSync(
|
export function useNavigationUrlSync(
|
||||||
selectedTool: ToolId | null,
|
selectedTool: ToolId | null,
|
||||||
handleToolSelect: (toolId: string) => void,
|
handleToolSelect: (toolId: ToolId) => void,
|
||||||
clearToolSelection: () => void,
|
clearToolSelection: () => void,
|
||||||
registry: ToolRegistry,
|
registry: ToolRegistry,
|
||||||
enableSync: boolean = true
|
enableSync: boolean = true
|
||||||
|
Loading…
Reference in New Issue
Block a user