From 83b6653d78366520a878469e850b1947bad3fac5 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 25 Jun 2025 17:07:32 +0200 Subject: [PATCH] feat(PUI): Make header tabs links to simpilfy new tab behaviour (#8779) * add anchor element to tabs to enable opening in new tab * simplify * use unstyled button instead * also enable linking on nav panels * make sure metakey also works (reduces duplication) * remove headers changes * move check for modified key to lib * render nav items as link --------- Co-authored-by: Oliver --- src/frontend/lib/functions/Navigation.tsx | 13 ++++++++++++- .../src/components/buttons/AdminButton.tsx | 3 ++- src/frontend/src/components/nav/Header.tsx | 12 ++++++++++-- .../src/components/nav/NavigationTree.tsx | 9 ++++++--- .../src/components/nav/SearchDrawer.tsx | 6 +++--- .../src/components/panels/PanelGroup.tsx | 18 ++++++++++++++---- 6 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/frontend/lib/functions/Navigation.tsx b/src/frontend/lib/functions/Navigation.tsx index 4e879f4959..9b8e91587a 100644 --- a/src/frontend/lib/functions/Navigation.tsx +++ b/src/frontend/lib/functions/Navigation.tsx @@ -72,7 +72,7 @@ export const navigateToLink = ( const base = `/${getBaseUrl()}`; - if (event?.ctrlKey || event?.shiftKey) { + if (eventModified(event)) { // Open the link in a new tab let url = link; if (link.startsWith('/') && !link.startsWith(base)) { @@ -91,3 +91,14 @@ export const navigateToLink = ( navigate(url); } }; + +/** + * Check if the event is modified (e.g. ctrl, shift, or meta key pressed) + * @param event - The event to check + * @returns true if the event was modified + */ +export const eventModified = ( + event: React.MouseEvent +) => { + return event?.ctrlKey || event?.shiftKey || event?.metaKey; +}; diff --git a/src/frontend/src/components/buttons/AdminButton.tsx b/src/frontend/src/components/buttons/AdminButton.tsx index df00dcee84..c5b7e4d40c 100644 --- a/src/frontend/src/components/buttons/AdminButton.tsx +++ b/src/frontend/src/components/buttons/AdminButton.tsx @@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react'; import { ModelInformationDict } from '@lib/enums/ModelInformation'; import type { ModelType } from '@lib/enums/ModelType'; +import { eventModified } from '@lib/functions/Navigation'; import { generateUrl } from '../../functions/urls'; import { useServerApiState } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; @@ -66,7 +67,7 @@ export default function AdminButton(props: Readonly) { `${server.server.django_admin}${modelDef.admin_url}${props.id}/` ); - if (event?.ctrlKey || event?.shiftKey) { + if (eventModified(event)) { // Open the link in a new tab window.open(url, '_blank'); } else { diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index d6e2b12eb8..a5b33b6d80 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -5,7 +5,8 @@ import { Indicator, Tabs, Text, - Tooltip + Tooltip, + UnstyledButton } from '@mantine/core'; import { useDisclosure, useDocumentVisibility } from '@mantine/hooks'; import { IconBell, IconSearch } from '@tabler/icons-react'; @@ -15,12 +16,14 @@ import { useMatch, useNavigate } from 'react-router-dom'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; +import { getBaseUrl } from '@lib/functions/Navigation'; import { navigateToLink } from '@lib/functions/Navigation'; import { t } from '@lingui/core/macro'; import { useShallow } from 'zustand/react/shallow'; import { api } from '../../App'; import type { NavigationUIFeature } from '../../components/plugins/PluginUIFeatureTypes'; import { getNavTabs } from '../../defaults/links'; +import { generateUrl } from '../../functions/urls'; import { usePluginUIFeature } from '../../hooks/UsePluginUIFeature'; import * as classes from '../../main.css'; import { useServerApiState } from '../../states/ApiState'; @@ -215,7 +218,12 @@ function NavTabs() { navigateToLink(`/${tab.name}`, navigate, event) } > - {tab.title} + + {tab.title} + ); }); diff --git a/src/frontend/src/components/nav/NavigationTree.tsx b/src/frontend/src/components/nav/NavigationTree.tsx index 805ee96ef0..1840d8ca6a 100644 --- a/src/frontend/src/components/nav/NavigationTree.tsx +++ b/src/frontend/src/components/nav/NavigationTree.tsx @@ -24,8 +24,11 @@ import { useNavigate } from 'react-router-dom'; import type { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import type { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; -import { getDetailUrl } from '@lib/functions/Navigation'; -import { navigateToLink } from '@lib/functions/Navigation'; +import { + eventModified, + getDetailUrl, + navigateToLink +} from '@lib/functions/Navigation'; import { useApi } from '../../contexts/ApiContext'; import { ApiIcon } from '../items/ApiIcon'; import { StylishText } from '../items/StylishText'; @@ -73,7 +76,7 @@ export default function NavigationTree({ const follow = useCallback( (node: TreeNodeData, event?: any) => { const url = getDetailUrl(modelType, node.value); - if (event?.shiftKey || event?.ctrlKey) { + if (eventModified(event)) { navigateToLink(url, navigate, event); } else { onClose(); diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx index 481078e0c4..70de9a777f 100644 --- a/src/frontend/src/components/nav/SearchDrawer.tsx +++ b/src/frontend/src/components/nav/SearchDrawer.tsx @@ -38,7 +38,7 @@ import { ModelType } from '@lib/enums/ModelType'; import { UserRoles } from '@lib/enums/Roles'; import { apiUrl } from '@lib/functions/Api'; import { cancelEvent } from '@lib/functions/Events'; -import { navigateToLink } from '@lib/functions/Navigation'; +import { eventModified, navigateToLink } from '@lib/functions/Navigation'; import { showNotification } from '@mantine/notifications'; import { api } from '../../App'; import { useUserSettingsState } from '../../states/SettingsState'; @@ -96,7 +96,7 @@ function QueryResultGroup({ const url = `${overviewUrl}?search=${searchText}`; // Close drawer if opening in the same tab - if (!(event?.ctrlKey || event?.shiftKey)) { + if (!eventModified(event)) { onClose(); } @@ -456,7 +456,7 @@ export function SearchDrawer({ return; } - if (event?.ctrlKey || event?.shiftKey) { + if (eventModified(event)) { // Keep the drawer open in this condition } else { closeDrawer(); diff --git a/src/frontend/src/components/panels/PanelGroup.tsx b/src/frontend/src/components/panels/PanelGroup.tsx index 424128369e..4bf75e8235 100644 --- a/src/frontend/src/components/panels/PanelGroup.tsx +++ b/src/frontend/src/components/panels/PanelGroup.tsx @@ -8,7 +8,8 @@ import { Stack, Tabs, Text, - Tooltip + Tooltip, + UnstyledButton } from '@mantine/core'; import { IconLayoutSidebarLeftCollapse, @@ -32,10 +33,12 @@ import { import type { ModelType } from '@lib/enums/ModelType'; import { cancelEvent } from '@lib/functions/Events'; +import { eventModified, getBaseUrl } from '@lib/functions/Navigation'; import { navigateToLink } from '@lib/functions/Navigation'; import { t } from '@lingui/core/macro'; import { useShallow } from 'zustand/react/shallow'; import { identifierString } from '../../functions/conversion'; +import { generateUrl } from '../../functions/urls'; import { usePluginPanels } from '../../hooks/UsePluginPanels'; import { useLocalState } from '../../states/LocalState'; import { vars } from '../../theme'; @@ -170,9 +173,9 @@ function BasePanelGroup({ // Callback when the active panel changes const handlePanelChange = useCallback( (targetPanel: string, event?: any) => { - if (event && (event?.ctrlKey || event?.shiftKey)) { + cancelEvent(event); + if (event && eventModified(event)) { const url = `${location.pathname}/../${targetPanel}`; - cancelEvent(event); navigateToLink(url, navigate, event); } else { navigate(`../${targetPanel}`); @@ -252,7 +255,14 @@ function BasePanelGroup({ handlePanelChange(panel.name, event) } > - {expanded && panel.label} + + {expanded && panel.label} + )