mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-10 02:47:49 +00:00
[UI] Link fixing (#13117)
* Use <Link> in navbar * Use <Link> in PanelGroup * <Link> in main navigation menu * Update Breadcrumbs * Refactor search drawer results * Update NavigationTree and SearchDrawer * Fix Link elements for dashboard widgets * Adjust login helpers for playwright * update playwright tests
This commit is contained in:
1 parent
a0540d3185
commit
e6bc21b66f
18 files changed
+283
-230
No files matched your search
@@ -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 (
|
||||
<Anchor href='#' onClick={onFollowLink} underline='never'>
|
||||
<Group
|
||||
gap='xs'
|
||||
wrap='nowrap'
|
||||
justify='space-between'
|
||||
align='center'
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Group gap='xs'>
|
||||
<InvenTreeIcon icon={icon ?? modelProperties.icon} />
|
||||
<StylishText size='md'>{title}</StylishText>
|
||||
</Group>
|
||||
<Group gap='xs' wrap='nowrap' justify='space-apart'>
|
||||
<Group gap='xs' wrap='nowrap' justify='right'>
|
||||
{result}
|
||||
</Group>
|
||||
const content = (
|
||||
<Group
|
||||
gap='xs'
|
||||
wrap='nowrap'
|
||||
justify='space-between'
|
||||
align='center'
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Group gap='xs'>
|
||||
<InvenTreeIcon icon={icon ?? modelProperties.icon} />
|
||||
<StylishText size='md'>{title}</StylishText>
|
||||
</Group>
|
||||
<Group gap='xs' wrap='nowrap' justify='space-apart'>
|
||||
<Group gap='xs' wrap='nowrap' justify='right'>
|
||||
{result}
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
|
||||
return url ? (
|
||||
<Anchor component={Link} to={url} underline='never'>
|
||||
{content}
|
||||
</Anchor>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
<Group
|
||||
gap='xs'
|
||||
wrap='nowrap'
|
||||
justify='space-between'
|
||||
align='center'
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Group gap='xs'>
|
||||
<InvenTreeIcon icon={icon ?? modelProperties.icon} />
|
||||
<StylishText size='md'>{title}</StylishText>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
|
||||
return (
|
||||
<Anchor href='#' onClick={onFollowLink} underline='never'>
|
||||
<Group
|
||||
gap='xs'
|
||||
wrap='nowrap'
|
||||
justify='space-between'
|
||||
align='center'
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<Group gap='xs'>
|
||||
<InvenTreeIcon icon={icon ?? modelProperties.icon} />
|
||||
<StylishText size='md'>{title}</StylishText>
|
||||
</Group>
|
||||
</Group>
|
||||
return url ? (
|
||||
<Anchor component={Link} to={url} underline='never'>
|
||||
{content}
|
||||
</Anchor>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
</Text>
|
||||
</Group>
|
||||
</Anchor>
|
||||
) : item.link ? (
|
||||
<UnstyledButton
|
||||
component={Link}
|
||||
to={item.link}
|
||||
onClick={(event) => {
|
||||
if (
|
||||
event.button === 0 &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!event.shiftKey &&
|
||||
!event.altKey
|
||||
) {
|
||||
beforeClick?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Group wrap='nowrap'>
|
||||
{item.icon && (
|
||||
<InvenTreeIcon
|
||||
icon={item.icon}
|
||||
iconProps={{ size: '14' }}
|
||||
/>
|
||||
)}
|
||||
<Text fw={500} p={5}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
) : (
|
||||
<UnstyledButton
|
||||
onClick={(event) => {
|
||||
if (item.link) {
|
||||
beforeClick?.();
|
||||
navigateToLink(item.link, navigate, event);
|
||||
} else if (item.action) {
|
||||
if (item.action) {
|
||||
beforeClick?.();
|
||||
item.action();
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
)}
|
||||
<Breadcrumbs key='breadcrumbs' separator='>'>
|
||||
{elements.map((breadcrumb, index) => {
|
||||
return (
|
||||
<Anchor
|
||||
key={`${index}-${breadcrumb.name}`}
|
||||
href={`/${baseUrl}${breadcrumb.url}`}
|
||||
aria-label={`breadcrumb-${index}-${identifierString(
|
||||
breadcrumb.name
|
||||
)}`}
|
||||
onClick={(event: any) =>
|
||||
breadcrumb.url &&
|
||||
navigateToLink(breadcrumb.url, navigate, event)
|
||||
}
|
||||
>
|
||||
<Group gap={4} align='center'>
|
||||
const content = (
|
||||
<Group gap={4} align='center'>
|
||||
{breadcrumb.icon}
|
||||
<Text size='sm'>{breadcrumb.name}</Text>
|
||||
</Group>
|
||||
);
|
||||
const ariaLabel = `breadcrumb-${index}-${identifierString(
|
||||
breadcrumb.name
|
||||
)}`;
|
||||
|
||||
if (breadcrumb.url === '#') {
|
||||
return (
|
||||
<Group
|
||||
key={`${index}-${breadcrumb.name}`}
|
||||
gap={4}
|
||||
align='center'
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{breadcrumb.icon}
|
||||
<Text size='sm'>{breadcrumb.name}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Anchor
|
||||
key={`${index}-${breadcrumb.name}`}
|
||||
component={Link}
|
||||
to={breadcrumb.url}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{content}
|
||||
</Anchor>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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() {
|
||||
<Tabs.Tab
|
||||
value={tab.name}
|
||||
key={tab.name}
|
||||
renderRoot={(props) => <Link {...props} to={`/${tab.name}`} />}
|
||||
leftSection={
|
||||
withIcons &&
|
||||
tab.icon && (
|
||||
<ActionIcon variant='transparent'>{tab.icon}</ActionIcon>
|
||||
)
|
||||
}
|
||||
onClick={(event: any) =>
|
||||
navigateToLink(`/${tab.name}`, navigate, event)
|
||||
}
|
||||
>
|
||||
<UnstyledButton
|
||||
component={'a'}
|
||||
href={generateUrl(`/${getBaseUrl()}/${tab.name}`)}
|
||||
>
|
||||
{tab.title}
|
||||
</UnstyledButton>
|
||||
{tab.title}
|
||||
</Tabs.Tab>
|
||||
);
|
||||
});
|
||||
@@ -329,9 +312,9 @@ function NavTabs() {
|
||||
<Tabs.Tab
|
||||
value={nav.options.key}
|
||||
key={nav.options.key}
|
||||
onClick={(event: any) =>
|
||||
navigateToLink(nav.options.options.url, navigate, event)
|
||||
}
|
||||
renderRoot={(props) => (
|
||||
<Link {...props} to={nav.options.options.url} />
|
||||
)}
|
||||
>
|
||||
{nav.options.title}
|
||||
</Tabs.Tab>
|
||||
|
||||
@@ -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({
|
||||
>
|
||||
<HoverCard.Target>
|
||||
<Anchor
|
||||
onClick={(event: any) => 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
|
||||
|
||||
@@ -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: <IconExclamationCircle />
|
||||
});
|
||||
const handleViewAllClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
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({
|
||||
<Group justify='space-between'>
|
||||
<Group justify='left' gap={5} wrap='nowrap'>
|
||||
<Tooltip label={t`View all results`} position='top-start'>
|
||||
<ActionIcon
|
||||
size='sm'
|
||||
variant='transparent'
|
||||
radius='xs'
|
||||
aria-label={`view-all-results-${query.searchKey ?? query.model}`}
|
||||
disabled={!overviewUrl}
|
||||
onClick={viewResults}
|
||||
>
|
||||
<IconTableExport />
|
||||
</ActionIcon>
|
||||
{resultsUrl ? (
|
||||
<ActionIcon
|
||||
renderRoot={(props) => (
|
||||
<Link
|
||||
{...props}
|
||||
to={resultsUrl}
|
||||
onClick={handleViewAllClick}
|
||||
/>
|
||||
)}
|
||||
size='sm'
|
||||
variant='transparent'
|
||||
radius='xs'
|
||||
aria-label={`view-all-results-${query.searchKey ?? query.model}`}
|
||||
>
|
||||
<IconTableExport />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<ActionIcon
|
||||
size='sm'
|
||||
variant='transparent'
|
||||
radius='xs'
|
||||
aria-label={`view-all-results-${query.searchKey ?? query.model}`}
|
||||
disabled
|
||||
>
|
||||
<IconTableExport />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Tooltip>
|
||||
<Text size='lg'>{query.title ?? modelInfo.label_multiple}</Text>
|
||||
<Text size='sm' style={{ fontStyle: 'italic' }}>
|
||||
@@ -180,7 +183,8 @@ function QueryResultGroup({
|
||||
<Fragment key={`result-group-${query.model}-${result.pk}`}>
|
||||
<Anchor
|
||||
underline='never'
|
||||
href={getDetailUrl(query.model, result.pk, true)}
|
||||
component={Link}
|
||||
to={getDetailUrl(query.model, result.pk)}
|
||||
onClick={(event: any) =>
|
||||
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)}
|
||||
|
||||
@@ -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) => (
|
||||
<Link {...props} to={`../${panel.name}${location.search}`} />
|
||||
)}
|
||||
leftSection={
|
||||
<Indicator
|
||||
position='top-end'
|
||||
@@ -179,19 +181,7 @@ function PanelTabComponent({
|
||||
onClick={(event: any) => onClick(event)}
|
||||
>
|
||||
<Group justify='left' gap='xs' wrap='nowrap'>
|
||||
<UnstyledButton
|
||||
component={'a'}
|
||||
style={{
|
||||
textAlign: 'left'
|
||||
}}
|
||||
href={generateUrl(
|
||||
`/${getBaseUrl()}${location.pathname}/${panel.name}${
|
||||
location.search
|
||||
}`
|
||||
)}
|
||||
>
|
||||
{expanded && panel.label}
|
||||
</UnstyledButton>
|
||||
{expanded && panel.label}
|
||||
</Group>
|
||||
</Tabs.Tab>
|
||||
</Tooltip>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -44,7 +44,7 @@ const loginAndMeasure = async (page: Page): Promise<number> => {
|
||||
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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user