From b6d8f5cece1907842c294ba7799dea94b229c49b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Wed, 3 Sep 2025 11:54:50 +0100 Subject: [PATCH 1/5] Urls for tools for scrapers and open in new tab support --- .../components/shared/AllToolsNavButton.tsx | 61 +++++++++----- .../src/components/shared/QuickAccessBar.tsx | 84 ++++++++++++++----- .../quickAccessBar/ActiveToolButton.tsx | 60 ++++++++----- .../tools/shared/SuggestedToolsSection.tsx | 31 ++++--- .../tools/toolPicker/ToolButton.tsx | 64 ++++++++++---- .../src/data/useTranslatedToolRegistry.tsx | 2 + frontend/src/hooks/useSidebarNavigation.ts | 53 ++++++++++++ frontend/src/hooks/useSuggestedTools.ts | 37 +++++--- frontend/src/hooks/useToolNavigation.ts | 52 ++++++++++++ 9 files changed, 341 insertions(+), 103 deletions(-) create mode 100644 frontend/src/hooks/useSidebarNavigation.ts create mode 100644 frontend/src/hooks/useToolNavigation.ts diff --git a/frontend/src/components/shared/AllToolsNavButton.tsx b/frontend/src/components/shared/AllToolsNavButton.tsx index c1b5774d2..39563fa06 100644 --- a/frontend/src/components/shared/AllToolsNavButton.tsx +++ b/frontend/src/components/shared/AllToolsNavButton.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { ActionIcon } from '@mantine/core'; +import { ActionIcon, Anchor } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { Tooltip } from './Tooltip'; import AppsIcon from '@mui/icons-material/AppsRounded'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; +import { useSidebarNavigation } from '../../hooks/useSidebarNavigation'; interface AllToolsNavButtonProps { activeButton: string; @@ -13,6 +14,7 @@ interface AllToolsNavButtonProps { const AllToolsNavButton: React.FC = ({ activeButton, setActiveButton }) => { const { t } = useTranslation(); const { handleReaderToggle, handleBackToTools, selectedToolKey, leftPanelView } = useToolWorkflow(); + const { getHomeNavigation } = useSidebarNavigation(); const handleClick = () => { setActiveButton('tools'); @@ -24,6 +26,19 @@ const AllToolsNavButton: React.FC = ({ activeButton, set // Do not highlight All Tools when a specific tool is open (indicator is shown) const isActive = activeButton === 'tools' && !selectedToolKey && leftPanelView === 'toolPicker'; + const navProps = getHomeNavigation(); + + const handleNavClick = (e: React.MouseEvent) => { + // Check if it's a special click (middle click, ctrl+click, etc.) + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; // Let browser handle it via href + } + + // For regular clicks, prevent default and use SPA navigation + e.preventDefault(); + handleClick(); + }; + const iconNode = ( @@ -31,27 +46,31 @@ const AllToolsNavButton: React.FC = ({ activeButton, set ); return ( - -
- - {iconNode} - - - {t("quickAccess.allTools", "All Tools")} - -
+ +
+ + {iconNode} + + + {t("quickAccess.allTools", "All Tools")} + +
+
); }; diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index e65843fa4..95134d90f 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, forwardRef, useEffect } from "react"; -import { ActionIcon, Stack, Divider } from "@mantine/core"; +import { ActionIcon, Stack, Divider, Anchor } from "@mantine/core"; import { useTranslation } from 'react-i18next'; import LocalIcon from './LocalIcon'; import { useRainbowThemeContext } from "./RainbowThemeProvider"; @@ -7,6 +7,7 @@ import AppConfigModal from './AppConfigModal'; import { useIsOverflowing } from '../../hooks/useIsOverflowing'; import { useFilesModalContext } from '../../contexts/FilesModalContext'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; +import { useSidebarNavigation } from '../../hooks/useSidebarNavigation'; import { ButtonConfig } from '../../types/sidebar'; import './quickAccessBar/QuickAccessBar.css'; import AllToolsNavButton from './AllToolsNavButton'; @@ -23,6 +24,7 @@ const QuickAccessBar = forwardRef(({ const { isRainbowMode } = useRainbowThemeContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); const { handleReaderToggle, handleBackToTools, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow(); + const { getToolNavigation } = useSidebarNavigation(); const [configModalOpen, setConfigModalOpen] = useState(false); const [activeButton, setActiveButton] = useState('tools'); const scrollableRef = useRef(null); @@ -37,6 +39,64 @@ const QuickAccessBar = forwardRef(({ openFilesModal(); }; + // Helper function to render navigation buttons with URL support + const renderNavButton = (config: ButtonConfig, index: number) => { + const isActive = isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView); + + // Check if this button has URL navigation support + const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate') + ? getToolNavigation(config.id) + : null; + + const handleClick = (e?: React.MouseEvent) => { + if (navProps && e) { + // Check if it's a special click (middle click, ctrl+click, etc.) + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; // Let browser handle it via href + } + // For regular clicks, prevent default and use SPA navigation + e.preventDefault(); + } + config.onClick(); + }; + + const buttonElement = ( +
+ handleClick()} + style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)} + className={isActive ? 'activeIconScale' : ''} + data-testid={`${config.id}-button`} + > + + {config.icon} + + + + {config.name} + +
+ ); + + // Wrap with Anchor if it has URL navigation + if (navProps) { + return ( + + {buttonElement} + + ); + } + + return
{buttonElement}
; + }; + const buttonConfigs: ButtonConfig[] = [ { @@ -153,27 +213,7 @@ const QuickAccessBar = forwardRef(({ {buttonConfigs.slice(0, -1).map((config, index) => ( - -
- { - config.onClick(); - }} - style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)} - className={isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView) ? 'activeIconScale' : ''} - data-testid={`${config.id}-button`} - > - - {config.icon} - - - - {config.name} - -
- + {renderNavButton(config, index)} {/* Add divider after Automate button (index 1) and Files button (index 2) */} {index === 1 && ( diff --git a/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx b/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx index 8362c8224..45fd649ff 100644 --- a/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx +++ b/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx @@ -13,9 +13,10 @@ */ import React, { useEffect, useRef, useState } from 'react'; -import { ActionIcon } from '@mantine/core'; +import { ActionIcon, Anchor } from '@mantine/core'; import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; import { useToolWorkflow } from '../../../contexts/ToolWorkflowContext'; +import { useSidebarNavigation } from '../../../hooks/useSidebarNavigation'; import FitText from '../FitText'; import { Tooltip } from '../Tooltip'; @@ -28,6 +29,7 @@ const NAV_IDS = ['read', 'sign', 'automate']; const ActiveToolButton: React.FC = ({ activeButton, setActiveButton }) => { const { selectedTool, selectedToolKey, leftPanelView, handleBackToTools } = useToolWorkflow(); + const { getHomeNavigation } = useSidebarNavigation(); // Determine if the indicator should be visible (do not require selectedTool to be resolved yet) const indicatorShouldShow = Boolean( @@ -141,32 +143,44 @@ const ActiveToolButton: React.FC = ({ activeButton, setAc
- setIsBackHover(true)} - onMouseLeave={() => setIsBackHover(false)} - onClick={() => { + { + // Check if it's a special click (middle click, ctrl+click, etc.) + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; // Let browser handle it via href + } + + // For regular clicks, prevent default and use SPA navigation + e.preventDefault(); setActiveButton('tools'); handleBackToTools(); }} - aria-label={isBackHover ? 'Back to all tools' : indicatorTool.name} - style={{ - backgroundColor: isBackHover ? 'var(--color-gray-300)' : 'var(--icon-tools-bg)', - color: isBackHover ? '#fff' : 'var(--icon-tools-color)', - border: 'none', - borderRadius: '8px', - cursor: 'pointer' - }} + style={{ textDecoration: 'none', color: 'inherit' }} > - - {isBackHover ? ( - - ) : ( - indicatorTool.icon - )} - - + setIsBackHover(true)} + onMouseLeave={() => setIsBackHover(false)} + aria-label={isBackHover ? 'Back to all tools' : indicatorTool.name} + style={{ + backgroundColor: isBackHover ? 'var(--color-gray-300)' : 'var(--icon-tools-bg)', + color: isBackHover ? '#fff' : 'var(--icon-tools-color)', + border: 'none', + borderRadius: '8px', + cursor: 'pointer' + }} + > + + {isBackHover ? ( + + ) : ( + indicatorTool.icon + )} + + + { const IconComponent = tool.icon; return ( - - - - - {tool.title} - - - + + + + + {tool.title} + + + + ); })} diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx index bd54f7e13..8e88a2eb7 100644 --- a/frontend/src/components/tools/toolPicker/ToolButton.tsx +++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx @@ -1,7 +1,8 @@ import React from "react"; -import { Button } from "@mantine/core"; +import { Button, Anchor } from "@mantine/core"; import { Tooltip } from "../../shared/Tooltip"; import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; +import { useToolNavigation } from "../../../hooks/useToolNavigation"; import FitText from "../../shared/FitText"; interface ToolButtonProps { @@ -14,6 +15,8 @@ interface ToolButtonProps { const ToolButton: React.FC = ({ id, tool, isSelected, onSelect }) => { const isUnavailable = !tool.component && !tool.link; + const { getToolNavigation } = useToolNavigation(); + const handleClick = (id: string) => { if (isUnavailable) return; if (tool.link) { @@ -25,32 +28,65 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect onSelect(id); }; + // Get navigation props for URL support + const navProps = !isUnavailable && !tool.link ? getToolNavigation(id, tool) : null; + const tooltipContent = isUnavailable ? (Coming soon: {tool.description}) : tool.description; - return ( - + const buttonContent = ( + <> +
{tool.icon}
+ + + ); + + const buttonElement = navProps ? ( + // For tools with URLs, wrap in anchor for proper link behavior + + + ) : ( + // For external links and unavailable tools, use regular button + + ); + + return ( + + {buttonElement} ); }; diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 68883fe92..e19d121dc 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -355,6 +355,7 @@ export function useFlatToolRegistry(): ToolRegistry { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, + urlPath: '/pdf-to-single-page', endpoints: ["pdf-to-single-page"], operationConfig: singleLargePageOperationConfig, }, @@ -681,6 +682,7 @@ export function useFlatToolRegistry(): ToolRegistry { categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, + urlPath: '/ocr-pdf', operationConfig: ocrOperationConfig, settingsComponent: OCRSettings, }, diff --git a/frontend/src/hooks/useSidebarNavigation.ts b/frontend/src/hooks/useSidebarNavigation.ts new file mode 100644 index 000000000..99f60ce44 --- /dev/null +++ b/frontend/src/hooks/useSidebarNavigation.ts @@ -0,0 +1,53 @@ +import { useCallback } from 'react'; +import { useToolNavigation } from './useToolNavigation'; +import { useToolManagement } from './useToolManagement'; + +export interface SidebarNavigationProps { + /** Full URL for the navigation (for href attribute) */ + href: string; + /** Click handler that maintains SPA behavior */ + onClick: (e: React.MouseEvent) => void; +} + +/** + * Hook that provides URL and navigation handlers for sidebar navigation buttons + * Supports special routes like home ('/') and specific tool routes + */ +export function useSidebarNavigation(): { + getHomeNavigation: () => SidebarNavigationProps; + getToolNavigation: (toolId: string) => SidebarNavigationProps | null; +} { + const { getToolNavigation: getToolNavProps } = useToolNavigation(); + const { getSelectedTool } = useToolManagement(); + + const getHomeNavigation = useCallback((): SidebarNavigationProps => { + const href = window.location.origin + '/'; + + const onClick = (e: React.MouseEvent) => { + // Check if it's a special click (middle click, ctrl+click, etc.) + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; // Let browser handle it via href + } + + // For regular clicks, prevent default and handle via SPA navigation + e.preventDefault(); + // The existing click handler will be called after this + }; + + return { href, onClick }; + }, []); + + const getToolNavigation = useCallback((toolId: string): SidebarNavigationProps | null => { + const tool = getSelectedTool(toolId); + if (!tool) { + return null; + } + + return getToolNavProps(toolId, tool); + }, [getToolNavProps, getSelectedTool]); + + return { + getHomeNavigation, + getToolNavigation + }; +} \ No newline at end of file diff --git a/frontend/src/hooks/useSuggestedTools.ts b/frontend/src/hooks/useSuggestedTools.ts index 8478bbc6b..377cf1245 100644 --- a/frontend/src/hooks/useSuggestedTools.ts +++ b/frontend/src/hooks/useSuggestedTools.ts @@ -1,5 +1,7 @@ import { useMemo } from 'react'; -import { useNavigationActions, useNavigationState } from '../contexts/NavigationContext'; +import { useNavigationState } from '../contexts/NavigationContext'; +import { useToolNavigation } from './useToolNavigation'; +import { useToolManagement } from './useToolManagement'; import { ToolId } from '../types/toolId'; // Material UI Icons @@ -13,10 +15,11 @@ export interface SuggestedTool { id: ToolId; title: string; icon: React.ComponentType; - navigate: () => void; + href: string; + onClick: (e: React.MouseEvent) => void; } -const ALL_SUGGESTED_TOOLS: Omit[] = [ +const ALL_SUGGESTED_TOOLS: Omit[] = [ { id: 'compress', title: 'Compress', @@ -45,17 +48,31 @@ const ALL_SUGGESTED_TOOLS: Omit[] = [ ]; export function useSuggestedTools(): SuggestedTool[] { - const { actions } = useNavigationActions(); const { selectedTool } = useNavigationState(); + const { getToolNavigation } = useToolNavigation(); + const { getSelectedTool } = useToolManagement(); return useMemo(() => { // Filter out the current tool const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedTool); - // Add navigation function to each tool - return filteredTools.map(tool => ({ - ...tool, - navigate: () => actions.setSelectedTool(tool.id) - })); - }, [selectedTool, actions]); + // Add navigation props to each tool + return filteredTools.map(tool => { + const toolRegistryEntry = getSelectedTool(tool.id); + if (!toolRegistryEntry) { + // Fallback for tools not in registry + return { + ...tool, + href: `/${tool.id}`, + onClick: (e: React.MouseEvent) => { e.preventDefault(); } + }; + } + + const navProps = getToolNavigation(tool.id, toolRegistryEntry); + return { + ...tool, + ...navProps + }; + }); + }, [selectedTool, getToolNavigation, getSelectedTool]); } diff --git a/frontend/src/hooks/useToolNavigation.ts b/frontend/src/hooks/useToolNavigation.ts new file mode 100644 index 000000000..4e010a297 --- /dev/null +++ b/frontend/src/hooks/useToolNavigation.ts @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; +import { ToolId } from '../types/toolId'; +import { ToolRegistryEntry, getToolUrlPath } from '../data/toolsTaxonomy'; +import { useNavigationActions } from '../contexts/NavigationContext'; +import { useToolWorkflow } from '../contexts/ToolWorkflowContext'; + +export interface ToolNavigationProps { + /** Full URL for the tool (for href attribute) */ + href: string; + /** Click handler that maintains SPA behavior */ + onClick: (e: React.MouseEvent) => void; +} + +/** + * Hook that provides URL and navigation handlers for tools + * Enables right-click "Open in New Tab" while maintaining SPA behavior for regular clicks + */ +export function useToolNavigation(): { + getToolNavigation: (toolId: string, tool: ToolRegistryEntry) => ToolNavigationProps; +} { + const { actions } = useNavigationActions(); + const { handleToolSelect } = useToolWorkflow(); + + const getToolNavigation = useCallback((toolId: string, tool: ToolRegistryEntry): ToolNavigationProps => { + // Generate the full URL for href attribute + const path = getToolUrlPath(toolId, tool); + const href = `${window.location.origin}${path}`; + + // Click handler that maintains SPA behavior + const onClick = (e: React.MouseEvent) => { + // Check if it's a special click (middle click, ctrl+click, etc.) + // These should use the default browser behavior to open in new tab + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; // Let browser handle it via href + } + + // Handle external links normally + if (tool.link) { + window.open(tool.link, '_blank', 'noopener,noreferrer'); + return; + } + + // For regular clicks, prevent default and use SPA navigation + e.preventDefault(); + handleToolSelect(toolId); + }; + + return { href, onClick }; + }, [actions, handleToolSelect]); + + return { getToolNavigation }; +} \ No newline at end of file From 66ea1c96b2028268856f00db6f1f2475e7d304f2 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Wed, 3 Sep 2025 12:29:14 +0100 Subject: [PATCH 2/5] address reviews and add external link support --- .../components/shared/AllToolsNavButton.tsx | 46 +++++++-------- .../src/components/shared/QuickAccessBar.tsx | 45 ++++++++------ .../quickAccessBar/ActiveToolButton.tsx | 48 ++++++++------- .../tools/toolPicker/ToolButton.tsx | 58 ++++++++++++++----- frontend/src/hooks/useToolNavigation.ts | 2 +- 5 files changed, 115 insertions(+), 84 deletions(-) diff --git a/frontend/src/components/shared/AllToolsNavButton.tsx b/frontend/src/components/shared/AllToolsNavButton.tsx index 39563fa06..d25893f6c 100644 --- a/frontend/src/components/shared/AllToolsNavButton.tsx +++ b/frontend/src/components/shared/AllToolsNavButton.tsx @@ -47,30 +47,28 @@ const AllToolsNavButton: React.FC = ({ activeButton, set return ( - -
- - {iconNode} - - - {t("quickAccess.allTools", "All Tools")} - -
-
+
+ + {iconNode} + + + {t("quickAccess.allTools", "All Tools")} + +
); }; diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index 95134d90f..90533dca8 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -60,8 +60,33 @@ const QuickAccessBar = forwardRef(({ config.onClick(); }; - const buttonElement = ( -
+ // Render with URL navigation if available, otherwise regular div + if (navProps) { + return ( +
+ handleClick(e)} + size={isActive ? (config.size || 'lg') : 'lg'} + variant="subtle" + style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)} + className={isActive ? 'activeIconScale' : ''} + data-testid={`${config.id}-button`} + > + + {config.icon} + + + + {config.name} + +
+ ); + } + + return ( +
(({
); - - // Wrap with Anchor if it has URL navigation - if (navProps) { - return ( - - {buttonElement} - - ); - } - - return
{buttonElement}
; }; diff --git a/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx b/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx index 45fd649ff..ae2dd1c51 100644 --- a/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx +++ b/frontend/src/components/shared/quickAccessBar/ActiveToolButton.tsx @@ -143,7 +143,8 @@ const ActiveToolButton: React.FC = ({ activeButton, setAc
- { // Check if it's a special click (middle click, ctrl+click, etc.) @@ -156,31 +157,28 @@ const ActiveToolButton: React.FC = ({ activeButton, setAc setActiveButton('tools'); handleBackToTools(); }} - style={{ textDecoration: 'none', color: 'inherit' }} + size={'xl'} + variant="subtle" + onMouseEnter={() => setIsBackHover(true)} + onMouseLeave={() => setIsBackHover(false)} + aria-label={isBackHover ? 'Back to all tools' : indicatorTool.name} + style={{ + backgroundColor: isBackHover ? 'var(--color-gray-300)' : 'var(--icon-tools-bg)', + color: isBackHover ? '#fff' : 'var(--icon-tools-color)', + border: 'none', + borderRadius: '8px', + cursor: 'pointer', + textDecoration: 'none' + }} > - setIsBackHover(true)} - onMouseLeave={() => setIsBackHover(false)} - aria-label={isBackHover ? 'Back to all tools' : indicatorTool.name} - style={{ - backgroundColor: isBackHover ? 'var(--color-gray-300)' : 'var(--icon-tools-bg)', - color: isBackHover ? '#fff' : 'var(--icon-tools-color)', - border: 'none', - borderRadius: '8px', - cursor: 'pointer' - }} - > - - {isBackHover ? ( - - ) : ( - indicatorTool.icon - )} - - - + + {isBackHover ? ( + + ) : ( + indicatorTool.icon + )} + + = ({ id, tool, isSelected, onSelect ); + const handleExternalClick = (e: React.MouseEvent) => { + // Check if it's a special click (middle click, ctrl+click, etc.) + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; // Let browser handle it via href + } + + // For regular clicks, prevent default and use window.open + e.preventDefault(); + handleClick(id); + }; + const buttonElement = navProps ? ( - // For tools with URLs, wrap in anchor for proper link behavior - - - + {buttonContent} + + ) : tool.link && !isUnavailable ? ( + // For external links, render Button as an anchor with proper href + ) : ( - // For external links and unavailable tools, use regular button + // For unavailable tools, use regular button