diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx
index bee2c7b431..c0d5d6fe3c 100644
--- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx
+++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx
@@ -1,14 +1,13 @@
import { ActionIcon, Anchor, Group, RollingNumber } from '@mantine/core';
import { IconExclamationCircle } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
-import { type ReactNode, useCallback, useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { type ReactNode, useMemo } from 'react';
+import { Link } from 'react-router-dom';
import { StylishText } from '@lib/components/StylishText';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
-import { navigateToLink } from '@lib/functions/Navigation';
import type { InvenTreeIconType } from '@lib/types/Icons';
import { useDocumentVisibility } from '@mantine/hooks';
import { useApi } from '../../../contexts/ApiContext';
@@ -32,7 +31,6 @@ function QueryCountWidget({
}>): ReactNode {
const api = useApi();
const user = useUserState();
- const navigate = useNavigate();
const visibility = useDocumentVisibility();
const modelProperties = ModelInformationDict[modelType];
@@ -62,27 +60,21 @@ function QueryCountWidget({
}
});
- const onFollowLink = useCallback(
- (event: any) => {
- if (!query.isFetched) {
- return;
- }
+ const url = useMemo(() => {
+ if (!modelProperties.url_overview) {
+ return undefined;
+ }
- if (modelProperties.url_overview) {
- let url = modelProperties.url_overview;
+ const searchParams = new URLSearchParams();
+ Object.entries(params ?? {}).forEach(([key, value]) => {
+ searchParams.set(key, String(value));
+ });
- if (params) {
- url += '?';
- for (const key in params) {
- url += `${key}=${params[key]}&`;
- }
- }
-
- navigateToLink(url, navigate, event);
- }
- },
- [query.isFetched, modelProperties, params]
- );
+ const queryString = searchParams.toString();
+ return queryString
+ ? `${modelProperties.url_overview}?${queryString}`
+ : modelProperties.url_overview;
+ }, [modelProperties.url_overview, params]);
const result: ReactNode = useMemo(() => {
if (query.isError) {
@@ -98,26 +90,32 @@ function QueryCountWidget({
}
}, [query.isFetching, query.isError, query.data]);
- return (
-
-
-
-
- {title}
-
-
-
- {result}
-
+ const content = (
+
+
+
+ {title}
+
+
+
+ {result}
+
+ );
+
+ return url ? (
+
+ {content}
+ ) : (
+ content
);
}
diff --git a/src/frontend/src/components/dashboard/widgets/QueryDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryDashboardWidget.tsx
index 527041c50a..1b8ac15558 100644
--- a/src/frontend/src/components/dashboard/widgets/QueryDashboardWidget.tsx
+++ b/src/frontend/src/components/dashboard/widgets/QueryDashboardWidget.tsx
@@ -1,11 +1,10 @@
import { Anchor, Group } from '@mantine/core';
-import { type ReactNode, useCallback } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { type ReactNode, useMemo } from 'react';
+import { Link } from 'react-router-dom';
import { StylishText } from '@lib/components/StylishText';
import { ModelInformationDict } from '@lib/enums/ModelInformation';
import type { ModelType } from '@lib/enums/ModelType';
-import { navigateToLink } from '@lib/functions/Navigation';
import type { InvenTreeIconType } from '@lib/types/Icons';
import { InvenTreeIcon } from '../../../functions/icons';
import type { DashboardWidgetProps } from '../DashboardWidget';
@@ -24,42 +23,45 @@ function QueryWidget({
icon?: keyof InvenTreeIconType;
params: any;
}>): ReactNode {
- const navigate = useNavigate();
const modelProperties = ModelInformationDict[modelType];
- const onFollowLink = useCallback(
- (event: any) => {
- if (modelProperties.url_overview) {
- let url = modelProperties.url_overview;
+ const url = useMemo(() => {
+ if (!modelProperties.url_overview) {
+ return undefined;
+ }
- if (params) {
- url += '?';
- for (const key in params) {
- url += `${key}=${params[key]}&`;
- }
- }
+ const searchParams = new URLSearchParams();
+ Object.entries(params ?? {}).forEach(([key, value]) => {
+ searchParams.set(key, String(value));
+ });
- navigateToLink(url, navigate, event);
- }
- },
- [modelProperties, params]
+ const query = searchParams.toString();
+ return query
+ ? `${modelProperties.url_overview}?${query}`
+ : modelProperties.url_overview;
+ }, [modelProperties.url_overview, params]);
+
+ const content = (
+
+
+
+ {title}
+
+
);
- return (
-
-
-
-
- {title}
-
-
+ return url ? (
+
+ {content}
+ ) : (
+ content
);
}
diff --git a/src/frontend/src/components/items/MenuLinks.tsx b/src/frontend/src/components/items/MenuLinks.tsx
index 3a8c8a1e21..9e76a1ee46 100644
--- a/src/frontend/src/components/items/MenuLinks.tsx
+++ b/src/frontend/src/components/items/MenuLinks.tsx
@@ -9,10 +9,9 @@ import {
UnstyledButton
} from '@mantine/core';
import { type JSX, useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import { StylishText } from '@lib/components/StylishText';
-import { navigateToLink } from '@lib/functions/Navigation';
import type { InvenTreeIconType } from '@lib/types/Icons';
import { InvenTreeIcon } from '../../functions/icons';
@@ -36,8 +35,6 @@ export function MenuLinks({
links: MenuLinkItem[];
beforeClick?: () => void;
}>) {
- const navigate = useNavigate();
-
// Filter out any hidden links
const visibleLinks = useMemo(
() => links.filter((item) => !item.hidden),
@@ -80,13 +77,38 @@ export function MenuLinks({
+ ) : item.link ? (
+ {
+ if (
+ event.button === 0 &&
+ !event.ctrlKey &&
+ !event.metaKey &&
+ !event.shiftKey &&
+ !event.altKey
+ ) {
+ beforeClick?.();
+ }
+ }}
+ >
+
+ {item.icon && (
+
+ )}
+
+ {item.title}
+
+
+
) : (
{
- if (item.link) {
- beforeClick?.();
- navigateToLink(item.link, navigate, event);
- } else if (item.action) {
+ if (item.action) {
beforeClick?.();
item.action();
}
diff --git a/src/frontend/src/components/nav/BreadcrumbList.tsx b/src/frontend/src/components/nav/BreadcrumbList.tsx
index e7d649f046..ca8d7c376d 100644
--- a/src/frontend/src/components/nav/BreadcrumbList.tsx
+++ b/src/frontend/src/components/nav/BreadcrumbList.tsx
@@ -1,10 +1,9 @@
import { ActionIcon, Anchor, Breadcrumbs, Group, Text } from '@mantine/core';
import { IconMenu2 } from '@tabler/icons-react';
import { useMemo } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import { identifierString } from '@lib/functions/Conversion';
-import { getBaseUrl, navigateToLink } from '@lib/functions/Navigation';
export type Breadcrumb = {
icon?: React.ReactNode;
@@ -22,10 +21,6 @@ export function BreadcrumbList({
breadcrumbs: Breadcrumb[];
navCallback?: () => void;
}>) {
- const navigate = useNavigate();
-
- const baseUrl = useMemo(() => getBaseUrl(), []);
-
const elements = useMemo(() => {
// Limit to 7 active elements
if (breadcrumbs.length > 7) {
@@ -58,22 +53,38 @@ export function BreadcrumbList({
)}
{elements.map((breadcrumb, index) => {
- return (
-
- breadcrumb.url &&
- navigateToLink(breadcrumb.url, navigate, event)
- }
- >
-
+ const content = (
+
+ {breadcrumb.icon}
+ {breadcrumb.name}
+
+ );
+ const ariaLabel = `breadcrumb-${index}-${identifierString(
+ breadcrumb.name
+ )}`;
+
+ if (breadcrumb.url === '#') {
+ return (
+
{breadcrumb.icon}
{breadcrumb.name}
+ );
+ }
+
+ return (
+
+ {content}
);
})}
diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx
index 3e4d34fd21..81e6adb671 100644
--- a/src/frontend/src/components/nav/Header.tsx
+++ b/src/frontend/src/components/nav/Header.tsx
@@ -7,31 +7,22 @@ import {
Paper,
Tabs,
Text,
- Tooltip,
- UnstyledButton
+ Tooltip
} from '@mantine/core';
import { useDisclosure, useDocumentVisibility } from '@mantine/hooks';
import { IconBell, IconSearch, IconUserBolt } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { type ReactNode, useEffect, useMemo, useState } from 'react';
-import {
- matchPath,
- useLocation,
- useMatch,
- useNavigate
-} from 'react-router-dom';
+import { Link, matchPath, useLocation, useMatch } from 'react-router-dom';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import { useInvenTreeHotkeys } from '@lib/functions/Events';
-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 { useLocalState } from '../../states/LocalState';
@@ -256,7 +247,6 @@ export function Header() {
function NavTabs() {
const user = useUserState();
- const navigate = useNavigate();
const match = useMatch(':tabName/*');
const tabValue = match?.params.tabName;
const navTabs = getNavTabs(user);
@@ -304,22 +294,15 @@ function NavTabs() {
}
leftSection={
withIcons &&
tab.icon && (
{tab.icon}
)
}
- onClick={(event: any) =>
- navigateToLink(`/${tab.name}`, navigate, event)
- }
>
-
- {tab.title}
-
+ {tab.title}
);
});
@@ -329,9 +312,9 @@ function NavTabs() {
- navigateToLink(nav.options.options.url, navigate, event)
- }
+ renderRoot={(props) => (
+
+ )}
>
{nav.options.title}
diff --git a/src/frontend/src/components/nav/NavigationTree.tsx b/src/frontend/src/components/nav/NavigationTree.tsx
index 43640b3888..6e9adc5470 100644
--- a/src/frontend/src/components/nav/NavigationTree.tsx
+++ b/src/frontend/src/components/nav/NavigationTree.tsx
@@ -39,18 +39,14 @@ import {
useMemo,
useState
} from 'react';
-import { useNavigate } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import { StylishText } from '@lib/components/StylishText';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import type { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import { resolveItem } from '@lib/functions/Conversion';
-import {
- eventModified,
- getDetailUrl,
- navigateToLink
-} from '@lib/functions/Navigation';
+import { eventModified, getDetailUrl } from '@lib/functions/Navigation';
import { t } from '@lingui/core/macro';
import { useApi } from '../../contexts/ApiContext';
import { useUserSettingsState } from '../../states/SettingsStates';
@@ -81,7 +77,6 @@ export default function NavigationTree({
children?: ReactNode;
}>) {
const api = useApi();
- const navigate = useNavigate();
const treeState = useTree();
const userSettings = useUserSettingsState();
@@ -233,19 +228,6 @@ export default function NavigationTree({
[loadingNodes, allNodes, api, endpoint, childIdentifier]
);
- const follow = useCallback(
- (node: TreeNodeData, event?: any) => {
- const url = getDetailUrl(modelType, node.value);
- if (eventModified(event)) {
- navigateToLink(url, navigate, event);
- } else {
- onClose();
- navigate(url);
- }
- },
- [modelType, navigate]
- );
-
// In search mode use the query results directly; in browse mode use the accumulated lazy-load list
const sourceNodes: any[] = useMemo(
() => (debouncedSearch ? (query.data ?? []) : allNodes),
@@ -356,7 +338,19 @@ export default function NavigationTree({
>
follow(payload.node, event)}
+ component={Link}
+ to={getDetailUrl(modelType, payload.node.value)}
+ onClick={(event) => {
+ event.stopPropagation();
+
+ if (
+ event.button === 0 &&
+ !eventModified(event) &&
+ !event.altKey
+ ) {
+ onClose();
+ }
+ }}
aria-label={`nav-tree-item-${payload.node.value}`}
c='var(--mantine-color-text)'
>
@@ -385,7 +379,8 @@ export default function NavigationTree({
[
treeState,
childIdentifier,
- follow,
+ modelType,
+ onClose,
loadingNodes,
fetchChildren,
debouncedSearch
diff --git a/src/frontend/src/components/nav/SearchDrawer.tsx b/src/frontend/src/components/nav/SearchDrawer.tsx
index e629b5a1be..11d9663a0f 100644
--- a/src/frontend/src/components/nav/SearchDrawer.tsx
+++ b/src/frontend/src/components/nav/SearchDrawer.tsx
@@ -22,7 +22,6 @@ import { useDebouncedValue } from '@mantine/hooks';
import {
IconAlertCircle,
IconBackspace,
- IconExclamationCircle,
IconRefresh,
IconSearch,
IconSettings,
@@ -31,7 +30,7 @@ import {
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
-import { type NavigateFunction, useNavigate } from 'react-router-dom';
+import { Link } from 'react-router-dom';
import { Boundary } from '@lib/components/Boundary';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
@@ -40,12 +39,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 {
- eventModified,
- getDetailUrl,
- navigateToLink
-} from '@lib/functions/Navigation';
-import { showNotification } from '@mantine/notifications';
+import { eventModified, getDetailUrl } from '@lib/functions/Navigation';
import { api } from '../../App';
import { openGlobalPreview } from '../../states/PreviewDrawerState';
import { useUserSettingsState } from '../../states/SettingsStates';
@@ -75,7 +69,6 @@ type SearchQuery = {
function QueryResultGroup({
searchText,
query,
- navigate,
onClose,
onRemove,
onResultClick,
@@ -83,7 +76,6 @@ function QueryResultGroup({
}: Readonly<{
searchText: string;
query: SearchQuery;
- navigate: NavigateFunction;
onClose: () => void;
onRemove: (query: ModelType) => void;
onResultClick: (query: ModelType, pk: number, event: any) => void;
@@ -100,34 +92,28 @@ function QueryResultGroup({
return modelInfo.url_overview;
}, [query, modelInfo]);
- // Callback function to view all results for a given query
- const viewResults = useCallback(
- (event: any) => {
- cancelEvent(event);
+ const resultsUrl = useMemo(() => {
+ if (!overviewUrl) {
+ return undefined;
+ }
- if (overviewUrl) {
- // Keep the notes-search context so results found via their notes
- // are also present in the full results table view
- const url = `${overviewUrl}?search=${searchText}${
- searchNotes ? '&search_notes=true' : ''
- }`;
+ const params = new URLSearchParams({ search: searchText });
+ if (searchNotes) {
+ params.set('search_notes', 'true');
+ }
- // Close drawer if opening in the same tab
- if (!eventModified(event)) {
- onClose();
- }
+ return `${overviewUrl}?${params.toString()}`;
+ }, [overviewUrl, searchText, searchNotes]);
- navigateToLink(url, navigate, event);
- } else {
- showNotification({
- title: t`No Overview Available`,
- message: t`No overview available for this model type`,
- color: 'red',
- icon:
- });
+ const handleViewAllClick = useCallback(
+ (event: React.MouseEvent) => {
+ event.stopPropagation();
+
+ if (event.button === 0 && !eventModified(event) && !event.altKey) {
+ onClose();
}
},
- [overviewUrl, searchText, searchNotes]
+ [onClose]
);
if (query.results.count == 0) {
@@ -140,16 +126,33 @@ function QueryResultGroup({
-
-
-
+ {resultsUrl ? (
+ (
+
+ )}
+ size='sm'
+ variant='transparent'
+ radius='xs'
+ aria-label={`view-all-results-${query.searchKey ?? query.model}`}
+ >
+
+
+ ) : (
+
+
+
+ )}
{query.title ?? modelInfo.label_multiple}
@@ -180,7 +183,8 @@ function QueryResultGroup({
onResultClick(query.model, result.pk, event)
}
@@ -470,12 +474,17 @@ export function SearchDrawer({
onClose();
}
- const navigate = useNavigate();
-
// Callback when one of the search results is clicked
function onResultClick(query: ModelType, pk: number, event: any) {
const targetModel = ModelInformationDict[query];
- if (targetModel.url_detail == undefined) {
+ if (
+ targetModel.url_detail == undefined ||
+ event.button !== 0 ||
+ event.ctrlKey ||
+ event.metaKey ||
+ event.shiftKey ||
+ event.altKey
+ ) {
return;
}
@@ -490,14 +499,7 @@ export function SearchDrawer({
return;
}
- if (eventModified(event)) {
- // Keep the drawer open in this condition
- } else {
- closeDrawer();
- }
-
- const url = targetModel.url_detail.replace(':pk', pk.toString());
- navigateToLink(url, navigate, event);
+ closeDrawer();
}
return (
@@ -591,7 +593,6 @@ export function SearchDrawer({
key={query.model}
searchText={searchText}
query={query}
- navigate={navigate}
onClose={closeDrawer}
searchNotes={searchNotes}
onRemove={(query) => removeResults(query)}
diff --git a/src/frontend/src/components/panels/PanelGroup.tsx b/src/frontend/src/components/panels/PanelGroup.tsx
index 05e74d23bc..f3ff7b8d80 100644
--- a/src/frontend/src/components/panels/PanelGroup.tsx
+++ b/src/frontend/src/components/panels/PanelGroup.tsx
@@ -12,8 +12,7 @@ import {
Tabs,
Text,
Title,
- Tooltip,
- UnstyledButton
+ Tooltip
} from '@mantine/core';
import {
IconExclamationCircle,
@@ -28,6 +27,7 @@ import {
useState
} from 'react';
import {
+ Link,
Navigate,
Route,
Routes,
@@ -45,7 +45,7 @@ import {
cancelEvent,
useInvenTreeHotkeys
} from '@lib/functions/Events';
-import { eventModified, getBaseUrl } from '@lib/functions/Navigation';
+import { eventModified } from '@lib/functions/Navigation';
import { navigateToLink } from '@lib/functions/Navigation';
import type {
PanelGroupType,
@@ -57,7 +57,6 @@ import { useDocumentVisibility, useWindowEvent } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import { useShallow } from 'zustand/react/shallow';
-import { generateUrl } from '../../functions/urls';
import { usePluginPanels } from '../../hooks/UsePluginPanels';
import { useLocalState } from '../../states/LocalState';
import { vars } from '../../theme';
@@ -160,6 +159,9 @@ function PanelTabComponent({
key={`panel-label-${panel.name}`}
w={'100%'}
value={panel.name}
+ renderRoot={(props) => (
+
+ )}
leftSection={
onClick(event)}
>
-
- {expanded && panel.label}
-
+ {expanded && panel.label}
diff --git a/src/frontend/tests/login.ts b/src/frontend/tests/login.ts
index 9db3a95197..031e288ab4 100644
--- a/src/frontend/tests/login.ts
+++ b/src/frontend/tests/login.ts
@@ -47,7 +47,7 @@ export const doLogin = async (page: Page, options?: LoginOptions) => {
await page.waitForTimeout(100);
await page.waitForLoadState('networkidle');
- await page.getByRole('link', { name: 'Dashboard' }).waitFor();
+ await page.getByRole('tab', { name: 'Dashboard' }).waitFor();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
await page.waitForURL(/\/web(\/home)?/);
await page.waitForLoadState('networkidle');
@@ -93,7 +93,7 @@ export const doCachedLogin = async (
waitUntil: 'networkidle'
});
- await page.getByRole('link', { name: 'Dashboard' }).waitFor();
+ await page.getByRole('tab', { name: 'Dashboard' }).waitFor();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
await page.waitForURL(/\/web(\/home)?/);
await page.waitForLoadState('networkidle');
diff --git a/src/frontend/tests/pages/pui_dashboard.spec.ts b/src/frontend/tests/pages/pui_dashboard.spec.ts
index 435d8e29cf..a6dd0e4d44 100644
--- a/src/frontend/tests/pages/pui_dashboard.spec.ts
+++ b/src/frontend/tests/pages/pui_dashboard.spec.ts
@@ -26,6 +26,8 @@ test('Dashboard - Basic', async ({ browser }) => {
await page.getByLabel('add-widget-ovr-so').click();
await page.getByLabel('add-widget-ovr-po').click();
+ await page.getByLabel('dashboard-widgets-filter-input').fill('latest parts');
+ await page.getByLabel('add-widget-latest-parts').click();
await page.getByLabel('dashboard-widgets-filter-clear').click();
@@ -37,6 +39,17 @@ test('Dashboard - Basic', async ({ browser }) => {
// Check that the widgets are visible
await page.getByText('Overdue Sales Orders').waitFor();
await page.getByText('Overdue Purchase Orders').waitFor();
+ await page.getByText('Latest parts').waitFor();
+
+ await expect(
+ page.getByRole('link', { name: /Overdue Sales Orders/ })
+ ).toHaveAttribute('href', /\/web\/sales\/index\/salesorders\?overdue=true$/);
+ await expect(
+ page.getByRole('link', { name: 'Latest parts' })
+ ).toHaveAttribute(
+ 'href',
+ /\/web\/part\/category\/index\/parts\?ordering=-id$/
+ );
// Let's remove one of the widgets
await page.getByLabel('dashboard-menu').click();
diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts
index 2990d4d49f..b8b6fe07ab 100644
--- a/src/frontend/tests/pages/pui_part.spec.ts
+++ b/src/frontend/tests/pages/pui_part.spec.ts
@@ -195,7 +195,7 @@ test('Parts - BOM', async ({ browser }) => {
await page.getByText('Robert Shuruncle').waitFor();
// Move the mouse away
- await page.getByRole('link', { name: 'Bill of Materials' }).hover();
+ await page.getByRole('tab', { name: 'Bill of Materials' }).hover();
// Test sub-assembly row expansion (rows are inserted into the same table)
const assemblyRow = page
diff --git a/src/frontend/tests/pages/pui_scan.spec.ts b/src/frontend/tests/pages/pui_scan.spec.ts
index 8a59b1845a..58c5d6c490 100644
--- a/src/frontend/tests/pages/pui_scan.spec.ts
+++ b/src/frontend/tests/pages/pui_scan.spec.ts
@@ -38,7 +38,7 @@ test('Barcode Scanning - Basic', async ({ browser }) => {
// Navigate to the 'scan' page
await page.getByLabel('navigation-menu').click();
- await page.getByRole('button', { name: 'Scan Barcode' }).click();
+ await page.getByRole('link', { name: 'Scan Barcode' }).click();
await page.getByText('Scan or enter barcode data').waitFor();
diff --git a/src/frontend/tests/pages/pui_stock.spec.ts b/src/frontend/tests/pages/pui_stock.spec.ts
index 517ae4da9e..30d0e7dea3 100644
--- a/src/frontend/tests/pages/pui_stock.spec.ts
+++ b/src/frontend/tests/pages/pui_stock.spec.ts
@@ -55,8 +55,16 @@ test('Stock - Location Tree', async ({ browser }) => {
await page.getByLabel('nav-breadcrumb-action').click();
await page.getByLabel('nav-tree-toggle-1}').click();
+
+ await expect(page.getByLabel('nav-tree-item-2')).toHaveAttribute(
+ 'href',
+ /\/web\/stock\/location\/2\/?$/
+ );
await page.getByLabel('nav-tree-item-2').click();
+ await expect(
+ page.getByRole('link', { name: 'breadcrumb-1-factory' })
+ ).toHaveAttribute('href', /\/web\/stock\/location\/1\/?$/);
await page.getByLabel('breadcrumb-2-storage-room-a').waitFor();
await page.getByLabel('breadcrumb-1-factory').click();
diff --git a/src/frontend/tests/pages/pui_transfer.spec.ts b/src/frontend/tests/pages/pui_transfer.spec.ts
index 83331cd1b4..420cf4694f 100644
--- a/src/frontend/tests/pages/pui_transfer.spec.ts
+++ b/src/frontend/tests/pages/pui_transfer.spec.ts
@@ -86,7 +86,7 @@ test('Transfer Order - Reference', async ({ browser }) => {
await page.getByText('Item Created').waitFor();
// go back to stock page
- await page.getByRole('link', { name: 'Stock', exact: true }).click();
+ await page.getByRole('tab', { name: 'Stock', exact: true }).click();
await page
.getByRole('button', { name: 'action-button-add-transfer-' })
.click();
diff --git a/src/frontend/tests/pui_general.spec.ts b/src/frontend/tests/pui_general.spec.ts
index 9e3eb2fab2..25c1feeec5 100644
--- a/src/frontend/tests/pui_general.spec.ts
+++ b/src/frontend/tests/pui_general.spec.ts
@@ -1,4 +1,4 @@
-import { test } from './baseFixtures.js';
+import { expect, test } from './baseFixtures.js';
import { adminuser, stevenuser } from './defaults.js';
import { globalSearch } from './helpers.js';
import { doCachedLogin } from './login.js';
@@ -39,8 +39,36 @@ test('General - Search', async ({ browser }) => {
await page.locator('a').filter({ hasText: 'Customer D' }).first().waitFor();
await page.locator('a').filter({ hasText: 'Customer E' }).first().waitFor();
+ const customerResult = page
+ .locator('a')
+ .filter({ hasText: 'Customer B' })
+ .first();
+ await expect(customerResult).toHaveAttribute('href', /\/web\/company\/\d+/);
+
+ const popupPromise = page.context().waitForEvent('page');
+ await customerResult.click({ modifiers: ['Control'] });
+ const popup = await popupPromise;
+ await popup.waitForURL('**/web/company/**');
+ await popup.close();
+
// Click through to the "Customer" results
- await page.getByRole('button', { name: 'view-all-results-customer' }).click();
+ const viewAllCustomers = page.getByRole('link', {
+ name: 'view-all-results-customer'
+ });
+ await expect(viewAllCustomers).toHaveAttribute(
+ 'href',
+ /\/web\/sales\/index\/customers\?search=another\+customer/
+ );
+
+ const overviewPopupPromise = page.context().waitForEvent('page');
+ await viewAllCustomers.click({ modifiers: ['Control'] });
+ const overviewPopup = await overviewPopupPromise;
+ await overviewPopup.waitForURL(
+ '**/web/sales/index/customers?search=another+customer'
+ );
+ await overviewPopup.close();
+
+ await viewAllCustomers.click();
await page.waitForURL('**/sales/index/customers**');
await page.getByText('Custom table filters are active').waitFor();
@@ -58,7 +86,9 @@ test('General - Search', async ({ browser }) => {
.first()
.waitFor();
- await page.getByLabel('view-all-results-manufacturerpart').click();
+ await page
+ .getByRole('link', { name: 'view-all-results-manufacturerpart' })
+ .click();
await page.waitForURL('**/purchasing/index/manufacturer-parts**');
await page.getByRole('cell', { name: 'RT0402BRD07100KL' }).waitFor();
});
diff --git a/src/frontend/tests/pui_login.spec.ts b/src/frontend/tests/pui_login.spec.ts
index aacd04b9be..6251aa3029 100644
--- a/src/frontend/tests/pui_login.spec.ts
+++ b/src/frontend/tests/pui_login.spec.ts
@@ -44,7 +44,7 @@ const loginAndMeasure = async (page: Page): Promise => {
const start = Date.now();
await page.getByRole('button', { name: 'Log In' }).click();
- await page.getByRole('link', { name: 'Dashboard' }).waitFor();
+ await page.getByRole('tab', { name: 'Dashboard' }).waitFor();
await page.getByRole('button', { name: 'navigation-menu' }).waitFor();
await page.waitForURL(/\/web(\/home)?/);
await page.waitForLoadState('networkidle');
diff --git a/src/frontend/tests/pui_modals.spec.ts b/src/frontend/tests/pui_modals.spec.ts
index b476c989ef..e9810e83ad 100644
--- a/src/frontend/tests/pui_modals.spec.ts
+++ b/src/frontend/tests/pui_modals.spec.ts
@@ -109,9 +109,9 @@ test('Spotlight - No Keys', async ({ browser }) => {
await page.getByText('About').first().waitFor();
await page
- .getByRole('button', { name: 'Notifications', exact: true })
+ .getByRole('link', { name: 'Notifications', exact: true })
.waitFor();
- await page.getByRole('button', { name: 'Dashboard', exact: true }).waitFor();
+ await page.getByRole('link', { name: 'Dashboard', exact: true }).waitFor();
// close the nav
await page.keyboard.press('Escape');
diff --git a/src/frontend/tests/pui_permissions.spec.ts b/src/frontend/tests/pui_permissions.spec.ts
index 60f71fec0c..77d00351b0 100644
--- a/src/frontend/tests/pui_permissions.spec.ts
+++ b/src/frontend/tests/pui_permissions.spec.ts
@@ -116,7 +116,7 @@ test('Permissions - Reader', async ({ browser }) => {
// Navigate to the user / group info (via the navigation menu)
await page.getByLabel('navigation-menu').click();
- await page.getByRole('button', { name: 'Users' }).click();
+ await page.getByRole('link', { name: 'Users' }).click();
await page.getByText('System Overview', { exact: true }).waitFor();
await loadTab(page, 'Users');
await loadTab(page, 'Groups');