[PUI] Error pages (#7554)

* Add <PermissionDenied /> page

* Check permissions for admin center

* Wrap <PartDetail> page in an error handler

- Display client or server errors

* Add error handlers to other detail pages

* Refactor error pages

* Add playwright tests

* Refactor component locations

* Get test to work
This commit is contained in:
Oliver 2024-07-04 00:34:52 +10:00 committed by GitHub
parent 0c293fa896
commit b15eb35273
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 591 additions and 355 deletions

View File

@ -0,0 +1,28 @@
import { t } from '@lingui/macro';
import GenericErrorPage from './GenericErrorPage';
import NotAuthenticated from './NotAuthenticated';
import NotFound from './NotFound';
import PermissionDenied from './PermissionDenied';
export default function ClientError({ status }: { status?: number }) {
switch (status) {
case 401:
return <NotAuthenticated />;
case 403:
return <PermissionDenied />;
case 404:
return <NotFound />;
default:
break;
}
// Generic client error
return (
<GenericErrorPage
title={t`Client Error`}
message={t`Client error occurred`}
status={status}
/>
);
}

View File

@ -0,0 +1,74 @@
import { Trans } from '@lingui/macro';
import {
ActionIcon,
Alert,
Button,
Card,
Center,
Container,
Divider,
Group,
Stack,
Text
} from '@mantine/core';
import { IconArrowBack, IconExclamationCircle } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';
import { LanguageContext } from '../../contexts/LanguageContext';
export default function ErrorPage({
title,
message,
status
}: {
title: string;
message: string;
status?: number;
redirectMessage?: string;
redirectTarget?: string;
}) {
const navigate = useNavigate();
return (
<LanguageContext>
<Center>
<Container w="md" miw={400}>
<Card withBorder shadow="xs" padding="xl" radius="sm">
<Card.Section p="lg">
<Group gap="xs">
<ActionIcon color="red" variant="transparent" size="xl">
<IconExclamationCircle />
</ActionIcon>
<Text size="xl">{title}</Text>
</Group>
</Card.Section>
<Divider />
<Card.Section p="lg">
<Stack gap="md">
<Text size="lg">{message}</Text>
{status && (
<Text>
<Trans>Status Code</Trans>: {status}
</Text>
)}
</Stack>
</Card.Section>
<Divider />
<Card.Section p="lg">
<Center>
<Button
variant="outline"
color="green"
onClick={() => navigate('/')}
>
<Trans>Return to the index page</Trans>
<IconArrowBack />
</Button>
</Center>
</Card.Section>
</Card>
</Container>
</Center>
</LanguageContext>
);
}

View File

@ -0,0 +1,12 @@
import { t } from '@lingui/macro';
import GenericErrorPage from './GenericErrorPage';
export default function NotAuthenticated() {
return (
<GenericErrorPage
title={t`Not Authenticated`}
message={t`You are not logged in.`}
/>
);
}

View File

@ -0,0 +1,12 @@
import { t } from '@lingui/macro';
import GenericErrorPage from './GenericErrorPage';
export default function NotFound() {
return (
<GenericErrorPage
title={t`Page Not Found`}
message={t`This page does not exist`}
/>
);
}

View File

@ -0,0 +1,12 @@
import { t } from '@lingui/macro';
import GenericErrorPage from './GenericErrorPage';
export default function PermissionDenied() {
return (
<GenericErrorPage
title={t`Permission Denied`}
message={t`You do not have permission to view this page.`}
/>
);
}

View File

@ -0,0 +1,13 @@
import { t } from '@lingui/macro';
import GenericErrorPage from './GenericErrorPage';
export default function ServerError({ status }: { status?: number }) {
return (
<GenericErrorPage
title={t`Server Error`}
message={t`A server error occurred`}
status={status}
/>
);
}

View File

@ -308,7 +308,7 @@ export function ApiForm({
return response; return response;
} catch (error) { } catch (error) {
console.error('Error fetching initial data:', error); console.error('ERR: Error fetching initial data:', error);
// Re-throw error to allow react-query to handle error // Re-throw error to allow react-query to handle error
throw error; throw error;
} }

View File

@ -0,0 +1,28 @@
import { LoadingOverlay } from '@mantine/core';
import ClientError from '../errors/ClientError';
import ServerError from '../errors/ServerError';
export default function InstanceDetail({
status,
loading,
children
}: {
status: number;
loading: boolean;
children: React.ReactNode;
}) {
if (loading) {
return <LoadingOverlay />;
}
if (status >= 500) {
return <ServerError status={status} />;
}
if (status >= 400) {
return <ClientError status={status} />;
}
return <>{children}</>;
}

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { api } from '../App'; import { api } from '../App';
import { ApiEndpoints } from '../enums/ApiEndpoints'; import { ApiEndpoints } from '../enums/ApiEndpoints';
@ -39,6 +39,8 @@ export function useInstance<T = any>({
}) { }) {
const [instance, setInstance] = useState<T | undefined>(defaultValue); const [instance, setInstance] = useState<T | undefined>(defaultValue);
const [requestStatus, setRequestStatus] = useState<number>(0);
const instanceQuery = useQuery<T>({ const instanceQuery = useQuery<T>({
queryKey: ['instance', endpoint, pk, params, pathParams], queryKey: ['instance', endpoint, pk, params, pathParams],
queryFn: async () => { queryFn: async () => {
@ -62,6 +64,7 @@ export function useInstance<T = any>({
params: params params: params
}) })
.then((response) => { .then((response) => {
setRequestStatus(response.status);
switch (response.status) { switch (response.status) {
case 200: case 200:
setInstance(response.data); setInstance(response.data);
@ -72,8 +75,9 @@ export function useInstance<T = any>({
} }
}) })
.catch((error) => { .catch((error) => {
setRequestStatus(error.response?.status || 0);
setInstance(defaultValue); setInstance(defaultValue);
console.error(`Error fetching instance ${url}:`, error); console.error(`ERR: Error fetching instance ${url}:`, error);
if (throwError) throw error; if (throwError) throw error;
@ -89,5 +93,5 @@ export function useInstance<T = any>({
instanceQuery.refetch(); instanceQuery.refetch();
}, []); }, []);
return { instance, refreshInstance, instanceQuery }; return { instance, refreshInstance, instanceQuery, requestStatus };
} }

View File

@ -1,10 +1,9 @@
import { Trans, t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Container, Text, Title } from '@mantine/core';
import { useDocumentTitle } from '@mantine/hooks'; import { useDocumentTitle } from '@mantine/hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouteError } from 'react-router-dom'; import { useRouteError } from 'react-router-dom';
import { LanguageContext } from '../contexts/LanguageContext'; import GenericErrorPage from '../components/errors/GenericErrorPage';
import { ErrorResponse } from '../states/states'; import { ErrorResponse } from '../states/states';
export default function ErrorPage() { export default function ErrorPage() {
@ -19,18 +18,9 @@ export default function ErrorPage() {
}, [error]); }, [error]);
return ( return (
<LanguageContext> <GenericErrorPage
<Container> title={title}
<Title> message={t`An unexpected error has occurred`}
<Trans>Error</Trans> />
</Title>
<Text>
<Trans>Sorry, an unexpected error has occurred.</Trans>
</Text>
<Text>
<i>{error.statusText || error.message}</i>
</Text>
</Container>
</LanguageContext>
); );
} }

View File

@ -17,11 +17,13 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { lazy, useMemo } from 'react'; import { lazy, useMemo } from 'react';
import PermissionDenied from '../../../../components/errors/PermissionDenied';
import { PlaceholderPill } from '../../../../components/items/Placeholder'; import { PlaceholderPill } from '../../../../components/items/Placeholder';
import { PanelGroup, PanelType } from '../../../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../../../components/nav/PanelGroup';
import { SettingsHeader } from '../../../../components/nav/SettingsHeader'; import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
import { GlobalSettingList } from '../../../../components/settings/SettingList'; import { GlobalSettingList } from '../../../../components/settings/SettingList';
import { Loadable } from '../../../../functions/loading'; import { Loadable } from '../../../../functions/loading';
import { useUserState } from '../../../../states/UserState';
const ReportTemplatePanel = Loadable( const ReportTemplatePanel = Loadable(
lazy(() => import('./ReportTemplatePanel')) lazy(() => import('./ReportTemplatePanel'))
@ -74,6 +76,8 @@ const CurrencyTable = Loadable(
); );
export default function AdminCenter() { export default function AdminCenter() {
const user = useUserState();
const adminCenterPanels: PanelType[] = useMemo(() => { const adminCenterPanels: PanelType[] = useMemo(() => {
return [ return [
{ {
@ -187,19 +191,25 @@ export default function AdminCenter() {
); );
return ( return (
<Stack gap="xs"> <>
<SettingsHeader {user.isStaff() ? (
title={t`Admin Center`} <Stack gap="xs">
subtitle={t`Advanced Options`} <SettingsHeader
switch_link="/settings/system" title={t`Admin Center`}
switch_text="System Settings" subtitle={t`Advanced Options`}
/> switch_link="/settings/system"
<QuickAction /> switch_text="System Settings"
<PanelGroup />
pageKey="admin-center" <QuickAction />
panels={adminCenterPanels} <PanelGroup
collapsible={true} pageKey="admin-center"
/> panels={adminCenterPanels}
</Stack> collapsible={true}
/>
</Stack>
) : (
<PermissionDenied />
)}
</>
); );
} }

View File

@ -19,11 +19,13 @@ import {
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import PermissionDenied from '../../../components/errors/PermissionDenied';
import { PlaceholderPanel } from '../../../components/items/Placeholder'; import { PlaceholderPanel } from '../../../components/items/Placeholder';
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
import { SettingsHeader } from '../../../components/nav/SettingsHeader'; import { SettingsHeader } from '../../../components/nav/SettingsHeader';
import { GlobalSettingList } from '../../../components/settings/SettingList'; import { GlobalSettingList } from '../../../components/settings/SettingList';
import { useServerApiState } from '../../../states/ApiState'; import { useServerApiState } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
/** /**
* System settings page * System settings page
@ -295,19 +297,26 @@ export default function SystemSettings() {
} }
]; ];
}, []); }, []);
const user = useUserState();
const [server] = useServerApiState((state) => [state.server]); const [server] = useServerApiState((state) => [state.server]);
return ( return (
<> <>
<Stack gap="xs"> {user.isStaff() ? (
<SettingsHeader <Stack gap="xs">
title={t`System Settings`} <SettingsHeader
subtitle={server.instance || ''} title={t`System Settings`}
switch_link="/settings/user" subtitle={server.instance || ''}
switch_text={<Trans>Switch to User Setting</Trans>} switch_link="/settings/user"
/> switch_text={<Trans>Switch to User Setting</Trans>}
<PanelGroup pageKey="system-settings" panels={systemSettingsPanels} /> />
</Stack> <PanelGroup pageKey="system-settings" panels={systemSettingsPanels} />
</Stack>
) : (
<PermissionDenied />
)}
</> </>
); );
} }

View File

@ -1,35 +0,0 @@
import { Trans } from '@lingui/macro';
import { Button, Center, Container, Stack, Text, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';
import { LanguageContext } from '../contexts/LanguageContext';
export default function NotFound() {
const navigate = useNavigate();
return (
<LanguageContext>
<Center mih="100vh">
<Container w="md" miw={400}>
<Stack>
<Title>
<Trans>Not Found</Trans>
</Title>
<Text>
<Trans>Sorry, this page is not known or was moved.</Trans>
</Text>
<Button
variant="outline"
color="green"
onClick={() => navigate('/')}
>
<Trans>Go to the start page</Trans>
<IconArrowBack />
</Button>
</Stack>
</Container>
</Center>
</LanguageContext>
);
}

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, Skeleton, Stack } from '@mantine/core';
import { import {
IconClipboardCheck, IconClipboardCheck,
IconClipboardList, IconClipboardList,
@ -30,6 +30,7 @@ import {
UnlinkBarcodeAction, UnlinkBarcodeAction,
ViewBarcodeAction ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer'; import { StatusRenderer } from '../../components/render/StatusRenderer';
@ -61,7 +62,8 @@ export default function BuildDetail() {
const { const {
instance: build, instance: build,
refreshInstance, refreshInstance,
instanceQuery instanceQuery,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.build_order_list, endpoint: ApiEndpoints.build_order_list,
pk: id, pk: id,
@ -410,21 +412,22 @@ export default function BuildDetail() {
{editBuild.modal} {editBuild.modal}
{duplicateBuild.modal} {duplicateBuild.modal}
{cancelBuild.modal} {cancelBuild.modal}
<Stack gap="xs"> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<LoadingOverlay visible={instanceQuery.isFetching} /> <Stack gap="xs">
<PageDetail <PageDetail
title={build.reference} title={build.reference}
subtitle={build.title} subtitle={build.title}
badges={buildBadges} badges={buildBadges}
imageUrl={build.part_detail?.image ?? build.part_detail?.thumbnail} imageUrl={build.part_detail?.image ?? build.part_detail?.thumbnail}
breadcrumbs={[ breadcrumbs={[
{ name: t`Build Orders`, url: '/build' }, { name: t`Build Orders`, url: '/build' },
{ name: build.reference, url: `/build/${build.pk}` } { name: build.reference, url: `/build/${build.pk}` }
]} ]}
actions={buildActions} actions={buildActions}
/> />
<PanelGroup pageKey="build" panels={buildPanels} /> <PanelGroup pageKey="build" panels={buildPanels} />
</Stack> </Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, Skeleton, Stack } from '@mantine/core';
import { import {
IconBuildingFactory2, IconBuildingFactory2,
IconBuildingWarehouse, IconBuildingWarehouse,
@ -30,6 +30,7 @@ import {
EditItemAction EditItemAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { Breadcrumb } from '../../components/nav/BreadcrumbList'; import { Breadcrumb } from '../../components/nav/BreadcrumbList';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
@ -66,7 +67,8 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
const { const {
instance: company, instance: company,
refreshInstance, refreshInstance,
instanceQuery instanceQuery,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.company_list, endpoint: ApiEndpoints.company_list,
pk: id, pk: id,
@ -320,18 +322,19 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
return ( return (
<> <>
{editCompany.modal} {editCompany.modal}
<Stack gap="xs"> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<LoadingOverlay visible={instanceQuery.isFetching} /> <Stack gap="xs">
<PageDetail <PageDetail
title={t`Company` + `: ${company.name}`} title={t`Company` + `: ${company.name}`}
subtitle={company.description} subtitle={company.description}
actions={companyActions} actions={companyActions}
imageUrl={company.image} imageUrl={company.image}
breadcrumbs={props.breadcrumbs} breadcrumbs={props.breadcrumbs}
badges={badges} badges={badges}
/> />
<PanelGroup pageKey="company" panels={companyPanels} /> <PanelGroup pageKey="company" panels={companyPanels} />
</Stack> </Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, Skeleton, Stack } from '@mantine/core';
import { import {
IconBuildingWarehouse, IconBuildingWarehouse,
IconDots, IconDots,
@ -20,6 +20,7 @@ import {
DuplicateItemAction, DuplicateItemAction,
EditItemAction EditItemAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
@ -44,7 +45,8 @@ export default function ManufacturerPartDetail() {
const { const {
instance: manufacturerPart, instance: manufacturerPart,
instanceQuery, instanceQuery,
refreshInstance refreshInstance,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.manufacturer_part_list, endpoint: ApiEndpoints.manufacturer_part_list,
pk: id, pk: id,
@ -244,17 +246,18 @@ export default function ManufacturerPartDetail() {
return ( return (
<> <>
{editManufacturerPart.modal} {editManufacturerPart.modal}
<Stack gap="xs"> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<LoadingOverlay visible={instanceQuery.isFetching} /> <Stack gap="xs">
<PageDetail <PageDetail
title={t`ManufacturerPart`} title={t`ManufacturerPart`}
subtitle={`${manufacturerPart.MPN} - ${manufacturerPart.part_detail?.name}`} subtitle={`${manufacturerPart.MPN} - ${manufacturerPart.part_detail?.name}`}
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
actions={manufacturerPartActions} actions={manufacturerPartActions}
imageUrl={manufacturerPart?.part_detail?.thumbnail} imageUrl={manufacturerPart?.part_detail?.thumbnail}
/> />
<PanelGroup pageKey="manufacturerpart" panels={panels} /> <PanelGroup pageKey="manufacturerpart" panels={panels} />
</Stack> </Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, Skeleton, Stack } from '@mantine/core';
import { import {
IconCurrencyDollar, IconCurrencyDollar,
IconDots, IconDots,
@ -21,6 +21,7 @@ import {
DuplicateItemAction, DuplicateItemAction,
EditItemAction EditItemAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
@ -46,7 +47,8 @@ export default function SupplierPartDetail() {
const { const {
instance: supplierPart, instance: supplierPart,
instanceQuery, instanceQuery,
refreshInstance refreshInstance,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.supplier_part_list, endpoint: ApiEndpoints.supplier_part_list,
pk: id, pk: id,
@ -312,18 +314,19 @@ export default function SupplierPartDetail() {
return ( return (
<> <>
{editSuppliertPart.modal} {editSuppliertPart.modal}
<Stack gap="xs"> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<LoadingOverlay visible={instanceQuery.isFetching} /> <Stack gap="xs">
<PageDetail <PageDetail
title={t`Supplier Part`} title={t`Supplier Part`}
subtitle={`${supplierPart.SKU} - ${supplierPart?.part_detail?.name}`} subtitle={`${supplierPart.SKU} - ${supplierPart?.part_detail?.name}`}
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
badges={badges} badges={badges}
actions={supplierPartActions} actions={supplierPartActions}
imageUrl={supplierPart?.part_detail?.thumbnail} imageUrl={supplierPart?.part_detail?.thumbnail}
/> />
<PanelGroup pageKey="supplierpart" panels={panels} /> <PanelGroup pageKey="supplierpart" panels={panels} />
</Stack> </Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -18,6 +18,7 @@ import {
DeleteItemAction, DeleteItemAction,
EditItemAction EditItemAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
import NavigationTree from '../../components/nav/NavigationTree'; import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
@ -56,7 +57,8 @@ export default function CategoryDetail({}: {}) {
const { const {
instance: category, instance: category,
refreshInstance, refreshInstance,
instanceQuery instanceQuery,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.category_list, endpoint: ApiEndpoints.category_list,
hasPrimaryKey: true, hasPrimaryKey: true,
@ -275,29 +277,34 @@ export default function CategoryDetail({}: {}) {
<> <>
{editCategory.modal} {editCategory.modal}
{deleteCategory.modal} {deleteCategory.modal}
<Stack gap="xs"> <InstanceDetail
<LoadingOverlay visible={instanceQuery.isFetching} /> status={requestStatus}
<NavigationTree loading={id ? instanceQuery.isFetching : false}
modelType={ModelType.partcategory} >
title={t`Part Categories`} <Stack gap="xs">
endpoint={ApiEndpoints.category_tree} <LoadingOverlay visible={instanceQuery.isFetching} />
opened={treeOpen} <NavigationTree
onClose={() => { modelType={ModelType.partcategory}
setTreeOpen(false); title={t`Part Categories`}
}} endpoint={ApiEndpoints.category_tree}
selectedId={category?.pk} opened={treeOpen}
/> onClose={() => {
<PageDetail setTreeOpen(false);
title={t`Part Category`} }}
subtitle={category?.name} selectedId={category?.pk}
breadcrumbs={breadcrumbs} />
breadcrumbAction={() => { <PageDetail
setTreeOpen(true); title={t`Part Category`}
}} subtitle={category?.name}
actions={categoryActions} breadcrumbs={breadcrumbs}
/> breadcrumbAction={() => {
<PanelGroup pageKey="partcategory" panels={categoryPanels} /> setTreeOpen(true);
</Stack> }}
actions={categoryActions}
/>
<PanelGroup pageKey="partcategory" panels={categoryPanels} />
</Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -1,13 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { Alert, Grid, Skeleton, Stack, Table } from '@mantine/core';
Alert,
Divider,
Grid,
LoadingOverlay,
Skeleton,
Stack,
Table
} from '@mantine/core';
import { import {
IconBookmarks, IconBookmarks,
IconBuilding, IconBuilding,
@ -31,7 +23,7 @@ import {
IconVersions IconVersions
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useSuspenseQuery } from '@tanstack/react-query'; import { useSuspenseQuery } from '@tanstack/react-query';
import { ReactNode, useMemo, useState } from 'react'; import { ReactNode, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
@ -54,6 +46,7 @@ import {
ViewBarcodeAction ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PlaceholderPanel } from '../../components/items/Placeholder';
import InstanceDetail from '../../components/nav/InstanceDetail';
import NavigationTree from '../../components/nav/NavigationTree'; import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
@ -105,7 +98,8 @@ export default function PartDetail() {
const { const {
instance: part, instance: part,
refreshInstance, refreshInstance,
instanceQuery instanceQuery,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.part_list, endpoint: ApiEndpoints.part_list,
pk: id, pk: id,
@ -821,33 +815,34 @@ export default function PartDetail() {
{duplicatePart.modal} {duplicatePart.modal}
{editPart.modal} {editPart.modal}
{deletePart.modal} {deletePart.modal}
<Stack gap="xs"> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<LoadingOverlay visible={instanceQuery.isFetching} /> <Stack gap="xs">
<NavigationTree <NavigationTree
title={t`Part Categories`} title={t`Part Categories`}
modelType={ModelType.partcategory} modelType={ModelType.partcategory}
endpoint={ApiEndpoints.category_tree} endpoint={ApiEndpoints.category_tree}
opened={treeOpen} opened={treeOpen}
onClose={() => { onClose={() => {
setTreeOpen(false); setTreeOpen(false);
}} }}
selectedId={part?.category} selectedId={part?.category}
/> />
<PageDetail <PageDetail
title={t`Part` + ': ' + part.full_name} title={t`Part` + ': ' + part.full_name}
subtitle={part.description} subtitle={part.description}
imageUrl={part.image} imageUrl={part.image}
badges={badges} badges={badges}
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
breadcrumbAction={() => { breadcrumbAction={() => {
setTreeOpen(true); setTreeOpen(true);
}} }}
actions={partActions} actions={partActions}
/> />
<PanelGroup pageKey="part" panels={partPanels} /> <PanelGroup pageKey="part" panels={partPanels} />
{transferStockItems.modal} {transferStockItems.modal}
{countStockItems.modal} {countStockItems.modal}
</Stack> </Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, Skeleton, Stack } from '@mantine/core';
import { import {
IconDots, IconDots,
IconInfoCircle, IconInfoCircle,
@ -27,6 +27,7 @@ import {
UnlinkBarcodeAction, UnlinkBarcodeAction,
ViewBarcodeAction ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer'; import { StatusRenderer } from '../../components/render/StatusRenderer';
@ -40,7 +41,6 @@ import {
useEditApiFormModal useEditApiFormModal
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable';
import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable'; import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable';
@ -57,7 +57,8 @@ export default function PurchaseOrderDetail() {
const { const {
instance: order, instance: order,
instanceQuery, instanceQuery,
refreshInstance refreshInstance,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.purchase_order_list, endpoint: ApiEndpoints.purchase_order_list,
pk: id, pk: id,
@ -355,18 +356,19 @@ export default function PurchaseOrderDetail() {
return ( return (
<> <>
{editPurchaseOrder.modal} {editPurchaseOrder.modal}
<Stack gap="xs"> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<LoadingOverlay visible={instanceQuery.isFetching} /> <Stack gap="xs">
<PageDetail <PageDetail
title={t`Purchase Order` + `: ${order.reference}`} title={t`Purchase Order` + `: ${order.reference}`}
subtitle={order.description} subtitle={order.description}
imageUrl={order.supplier_detail?.image} imageUrl={order.supplier_detail?.image}
breadcrumbs={[{ name: t`Purchasing`, url: '/purchasing/' }]} breadcrumbs={[{ name: t`Purchasing`, url: '/purchasing/' }]}
actions={poActions} actions={poActions}
badges={orderBadges} badges={orderBadges}
/> />
<PanelGroup pageKey="purchaseorder" panels={orderPanels} /> <PanelGroup pageKey="purchaseorder" panels={orderPanels} />
</Stack> </Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, Skeleton, Stack } from '@mantine/core';
import { import {
IconDots, IconDots,
IconInfoCircle, IconInfoCircle,
@ -23,6 +23,7 @@ import {
EditItemAction EditItemAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PlaceholderPanel } from '../../components/items/Placeholder';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer'; import { StatusRenderer } from '../../components/render/StatusRenderer';
@ -36,7 +37,6 @@ import {
useEditApiFormModal useEditApiFormModal
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable';
@ -51,7 +51,8 @@ export default function ReturnOrderDetail() {
const { const {
instance: order, instance: order,
instanceQuery, instanceQuery,
refreshInstance refreshInstance,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.return_order_list, endpoint: ApiEndpoints.return_order_list,
pk: id, pk: id,
@ -320,18 +321,19 @@ export default function ReturnOrderDetail() {
<> <>
{editReturnOrder.modal} {editReturnOrder.modal}
{duplicateReturnOrder.modal} {duplicateReturnOrder.modal}
<Stack gap="xs"> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<LoadingOverlay visible={instanceQuery.isFetching} /> <Stack gap="xs">
<PageDetail <PageDetail
title={t`Return Order` + `: ${order.reference}`} title={t`Return Order` + `: ${order.reference}`}
subtitle={order.description} subtitle={order.description}
imageUrl={order.customer_detail?.image} imageUrl={order.customer_detail?.image}
badges={orderBadges} badges={orderBadges}
actions={orderActions} actions={orderActions}
breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]} breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]}
/> />
<PanelGroup pageKey="returnorder" panels={orderPanels} /> <PanelGroup pageKey="returnorder" panels={orderPanels} />
</Stack> </Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, Skeleton, Stack } from '@mantine/core';
import { import {
IconDots, IconDots,
IconInfoCircle, IconInfoCircle,
@ -26,6 +26,7 @@ import {
EditItemAction EditItemAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PlaceholderPanel } from '../../components/items/Placeholder';
import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer'; import { StatusRenderer } from '../../components/render/StatusRenderer';
@ -39,7 +40,6 @@ import {
useEditApiFormModal useEditApiFormModal
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable';
@ -55,7 +55,8 @@ export default function SalesOrderDetail() {
const { const {
instance: order, instance: order,
instanceQuery, instanceQuery,
refreshInstance refreshInstance,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.sales_order_list, endpoint: ApiEndpoints.sales_order_list,
pk: id, pk: id,
@ -344,18 +345,19 @@ export default function SalesOrderDetail() {
return ( return (
<> <>
{editSalesOrder.modal} {editSalesOrder.modal}
<Stack gap="xs"> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<LoadingOverlay visible={instanceQuery.isFetching} /> <Stack gap="xs">
<PageDetail <PageDetail
title={t`Sales Order` + `: ${order.reference}`} title={t`Sales Order` + `: ${order.reference}`}
subtitle={order.description} subtitle={order.description}
imageUrl={order.customer_detail?.image} imageUrl={order.customer_detail?.image}
badges={orderBadges} badges={orderBadges}
actions={soActions} actions={soActions}
breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]} breadcrumbs={[{ name: t`Sales`, url: '/sales/' }]}
/> />
<PanelGroup pageKey="salesorder" panels={orderPanels} /> <PanelGroup pageKey="salesorder" panels={orderPanels} />
</Stack> </Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core'; import { Skeleton, Stack, Text } from '@mantine/core';
import { import {
IconDots, IconDots,
IconInfoCircle, IconInfoCircle,
@ -23,6 +23,7 @@ import {
UnlinkBarcodeAction, UnlinkBarcodeAction,
ViewBarcodeAction ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import InstanceDetail from '../../components/nav/InstanceDetail';
import NavigationTree from '../../components/nav/NavigationTree'; import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
@ -63,7 +64,8 @@ export default function Stock() {
const { const {
instance: location, instance: location,
refreshInstance, refreshInstance,
instanceQuery instanceQuery,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.stock_location_list, endpoint: ApiEndpoints.stock_location_list,
hasPrimaryKey: true, hasPrimaryKey: true,
@ -355,29 +357,33 @@ export default function Stock() {
<> <>
{editLocation.modal} {editLocation.modal}
{deleteLocation.modal} {deleteLocation.modal}
<Stack> <InstanceDetail
<LoadingOverlay visible={instanceQuery.isFetching} /> status={requestStatus}
<NavigationTree loading={id ? instanceQuery.isFetching : false}
title={t`Stock Locations`} >
modelType={ModelType.stocklocation} <Stack>
endpoint={ApiEndpoints.stock_location_tree} <NavigationTree
opened={treeOpen} title={t`Stock Locations`}
onClose={() => setTreeOpen(false)} modelType={ModelType.stocklocation}
selectedId={location?.pk} endpoint={ApiEndpoints.stock_location_tree}
/> opened={treeOpen}
<PageDetail onClose={() => setTreeOpen(false)}
title={t`Stock Items`} selectedId={location?.pk}
subtitle={location?.name} />
actions={locationActions} <PageDetail
breadcrumbs={breadcrumbs} title={t`Stock Items`}
breadcrumbAction={() => { subtitle={location?.name}
setTreeOpen(true); actions={locationActions}
}} breadcrumbs={breadcrumbs}
/> breadcrumbAction={() => {
<PanelGroup pageKey="stocklocation" panels={locationPanels} /> setTreeOpen(true);
{transferStockItems.modal} }}
{countStockItems.modal} />
</Stack> <PanelGroup pageKey="stocklocation" panels={locationPanels} />
{transferStockItems.modal}
{countStockItems.modal}
</Stack>
</InstanceDetail>
</> </>
); );
} }

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; import { Grid, Skeleton, Stack } from '@mantine/core';
import { import {
IconBookmark, IconBookmark,
IconBoxPadding, IconBoxPadding,
@ -33,6 +33,7 @@ import {
ViewBarcodeAction ViewBarcodeAction
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder'; import { PlaceholderPanel } from '../../components/items/Placeholder';
import InstanceDetail from '../../components/nav/InstanceDetail';
import NavigationTree from '../../components/nav/NavigationTree'; import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
@ -76,7 +77,8 @@ export default function StockDetail() {
const { const {
instance: stockitem, instance: stockitem,
refreshInstance, refreshInstance,
instanceQuery instanceQuery,
requestStatus
} = useInstance({ } = useInstance({
endpoint: ApiEndpoints.stock_item_list, endpoint: ApiEndpoints.stock_item_list,
pk: id, pk: id,
@ -548,35 +550,36 @@ export default function StockDetail() {
}, [stockitem, instanceQuery]); }, [stockitem, instanceQuery]);
return ( return (
<Stack> <InstanceDetail status={requestStatus} loading={instanceQuery.isFetching}>
<LoadingOverlay visible={instanceQuery.isFetching} /> <Stack>
<NavigationTree <NavigationTree
title={t`Stock Locations`} title={t`Stock Locations`}
modelType={ModelType.stocklocation} modelType={ModelType.stocklocation}
endpoint={ApiEndpoints.stock_location_tree} endpoint={ApiEndpoints.stock_location_tree}
opened={treeOpen} opened={treeOpen}
onClose={() => setTreeOpen(false)} onClose={() => setTreeOpen(false)}
selectedId={stockitem?.location} selectedId={stockitem?.location}
/> />
<PageDetail <PageDetail
title={t`Stock Item`} title={t`Stock Item`}
subtitle={stockitem.part_detail?.full_name} subtitle={stockitem.part_detail?.full_name}
imageUrl={stockitem.part_detail?.thumbnail} imageUrl={stockitem.part_detail?.thumbnail}
badges={stockBadges} badges={stockBadges}
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
breadcrumbAction={() => { breadcrumbAction={() => {
setTreeOpen(true); setTreeOpen(true);
}} }}
actions={stockActions} actions={stockActions}
/> />
<PanelGroup pageKey="stockitem" panels={stockPanels} /> <PanelGroup pageKey="stockitem" panels={stockPanels} />
{editStockItem.modal} {editStockItem.modal}
{duplicateStockItem.modal} {duplicateStockItem.modal}
{deleteStockItem.modal} {deleteStockItem.modal}
{countStockItem.modal} {countStockItem.modal}
{addStockItem.modal} {addStockItem.modal}
{removeStockItem.modal} {removeStockItem.modal}
{transferStockItem.modal} {transferStockItem.modal}
</Stack> </Stack>
</InstanceDetail>
); );
} }

View File

@ -101,7 +101,9 @@ export const AdminCenter = Loadable(
lazy(() => import('./pages/Index/Settings/AdminCenter/Index')) lazy(() => import('./pages/Index/Settings/AdminCenter/Index'))
); );
export const NotFound = Loadable(lazy(() => import('./pages/NotFound'))); export const NotFound = Loadable(
lazy(() => import('./components/errors/NotFound'))
);
export const Login = Loadable(lazy(() => import('./pages/Auth/Login'))); export const Login = Loadable(lazy(() => import('./pages/Auth/Login')));
export const Logout = Loadable(lazy(() => import('./pages/Auth/Logout'))); export const Logout = Loadable(lazy(() => import('./pages/Auth/Logout')));
export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In'))); export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In')));

View File

@ -64,7 +64,8 @@ export const test = baseTest.extend({
url != 'http://localhost:8000/api/barcode/' && url != 'http://localhost:8000/api/barcode/' &&
url != 'http://localhost:8000/api/news/?search=&offset=0&limit=25' && url != 'http://localhost:8000/api/news/?search=&offset=0&limit=25' &&
url != 'https://docs.inventree.org/en/versions.json' && url != 'https://docs.inventree.org/en/versions.json' &&
!url.startsWith('chrome://') !url.startsWith('chrome://') &&
url.indexOf('99999') < 0
) )
messages.push(msg); messages.push(msg);
}); });

View File

@ -31,10 +31,14 @@ export const doQuickLogin = async (
password = password ?? user.password; password = password ?? user.password;
url = url ?? baseUrl; url = url ?? baseUrl;
// await page.goto(logoutUrl);
await page.goto(`${url}/login/?login=${username}&password=${password}`); await page.goto(`${url}/login/?login=${username}&password=${password}`);
await page.waitForURL('**/platform/home'); await page.waitForURL('**/platform/home');
await page await page
.getByRole('heading', { name: 'Welcome to your Dashboard,' }) .getByRole('heading', { name: 'Welcome to your Dashboard,' })
.waitFor(); .waitFor();
}; };
export const doLogout = async (page) => {
await page.goto(`${baseUrl}/logout/`);
await page.waitForURL('**/platform/login');
};

View File

@ -224,3 +224,13 @@ test('PUI - Pages - Part - Notes', async ({ page }) => {
// Check that the original notes are still present // Check that the original notes are still present
await page.getByText('This is some data').waitFor(); await page.getByText('This is some data').waitFor();
}); });
test('PUI - Pages - Part - 404', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/part/99999/`);
await page.getByText('Page Not Found', { exact: true }).waitFor();
// Clear out any console error messages
await page.evaluate(() => console.clear());
});

View File

@ -54,4 +54,5 @@ test('PUI - Quick Login Test', async ({ page }) => {
// Logout (via URL) // Logout (via URL)
await page.goto(`${baseUrl}/logout/`); await page.goto(`${baseUrl}/logout/`);
await page.waitForURL('**/platform/login'); await page.waitForURL('**/platform/login');
await page.getByLabel('username');
}); });

View File

@ -1,6 +1,6 @@
import { test } from './baseFixtures.js'; import { test } from './baseFixtures.js';
import { baseUrl } from './defaults.js'; import { baseUrl } from './defaults.js';
import { doQuickLogin } from './login.js'; import { doLogout, doQuickLogin } from './login.js';
test('PUI - Parts', async ({ page }) => { test('PUI - Parts', async ({ page }) => {
await doQuickLogin(page); await doQuickLogin(page);
@ -141,57 +141,6 @@ test('PUI - Scanning', async ({ page }) => {
await page.getByRole('option', { name: 'Manual input' }).click(); await page.getByRole('option', { name: 'Manual input' }).click();
}); });
test('PUI - Admin', async ({ page }) => {
// Note here we login with admin access
await doQuickLogin(page, 'admin', 'inventree');
// User settings
await page.getByRole('button', { name: 'admin' }).click();
await page.getByRole('menuitem', { name: 'Account settings' }).click();
await page.getByRole('tab', { name: 'Security' }).click();
//await page.getByRole('tab', { name: 'Dashboard' }).click();
await page.getByRole('tab', { name: 'Display Options' }).click();
await page.getByText('Date Format').waitFor();
await page.getByRole('tab', { name: 'Search' }).click();
await page.getByText('Regex Search').waitFor();
await page.getByRole('tab', { name: 'Notifications' }).click();
await page.getByRole('tab', { name: 'Reporting' }).click();
await page.getByText('Inline report display').waitFor();
// System Settings
await page.getByRole('link', { name: 'Switch to System Setting' }).click();
await page.getByText('Base URL', { exact: true }).waitFor();
await page.getByRole('tab', { name: 'Login' }).click();
await page.getByRole('tab', { name: 'Barcodes' }).click();
await page.getByRole('tab', { name: 'Notifications' }).click();
await page.getByRole('tab', { name: 'Pricing' }).click();
await page.getByRole('tab', { name: 'Labels' }).click();
await page.getByRole('tab', { name: 'Reporting' }).click();
await page.getByRole('tab', { name: 'Part Categories' }).click();
//wait page.locator('#mantine-9hqbwrml8-tab-parts').click();
//await page.locator('#mantine-9hqbwrml8-tab-stock').click();
await page.getByRole('tab', { name: 'Stocktake' }).click();
await page.getByRole('tab', { name: 'Build Orders' }).click();
await page.getByRole('tab', { name: 'Purchase Orders' }).click();
await page.getByRole('tab', { name: 'Sales Orders' }).click();
await page.getByRole('tab', { name: 'Return Orders' }).click();
// Admin Center
await page.getByRole('button', { name: 'admin' }).click();
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
await page.getByRole('tab', { name: 'Background Tasks' }).click();
await page.getByRole('tab', { name: 'Error Reports' }).click();
await page.getByRole('tab', { name: 'Currencies' }).click();
await page.getByRole('tab', { name: 'Project Codes' }).click();
await page.getByRole('tab', { name: 'Custom Units' }).click();
await page.getByRole('tab', { name: 'Part Parameters' }).click();
await page.getByRole('tab', { name: 'Category Parameters' }).click();
await page.getByRole('tab', { name: 'Label Templates' }).click();
await page.getByRole('tab', { name: 'Report Templates' }).click();
await page.getByRole('tab', { name: 'Plugins' }).click();
await page.getByRole('tab', { name: 'Machines' }).click();
});
test('PUI - Language / Color', async ({ page }) => { test('PUI - Language / Color', async ({ page }) => {
await doQuickLogin(page); await doQuickLogin(page);

View File

@ -0,0 +1,83 @@
import { test } from './baseFixtures.js';
import { baseUrl } from './defaults.js';
import { doLogout, doQuickLogin } from './login.js';
test('PUI - Admin', async ({ page }) => {
// Note here we login with admin access
await doQuickLogin(page, 'admin', 'inventree');
// User settings
await page.getByRole('button', { name: 'admin' }).click();
await page.getByRole('menuitem', { name: 'Account settings' }).click();
await page.getByRole('tab', { name: 'Security' }).click();
await page.getByRole('tab', { name: 'Display Options' }).click();
await page.getByText('Date Format').waitFor();
await page.getByRole('tab', { name: 'Search' }).click();
await page.getByText('Regex Search').waitFor();
await page.getByRole('tab', { name: 'Notifications' }).click();
await page.getByRole('tab', { name: 'Reporting' }).click();
await page.getByText('Inline report display').waitFor();
// System Settings
await page.getByRole('link', { name: 'Switch to System Setting' }).click();
await page.getByText('Base URL', { exact: true }).waitFor();
await page.getByRole('tab', { name: 'Login' }).click();
await page.getByRole('tab', { name: 'Barcodes' }).click();
await page.getByRole('tab', { name: 'Notifications' }).click();
await page.getByRole('tab', { name: 'Pricing' }).click();
await page.getByRole('tab', { name: 'Labels' }).click();
await page.getByRole('tab', { name: 'Reporting' }).click();
await page.getByRole('tab', { name: 'Part Categories' }).click();
await page.getByRole('tab', { name: 'Stocktake' }).click();
await page.getByRole('tab', { name: 'Build Orders' }).click();
await page.getByRole('tab', { name: 'Purchase Orders' }).click();
await page.getByRole('tab', { name: 'Sales Orders' }).click();
await page.getByRole('tab', { name: 'Return Orders' }).click();
// Admin Center
await page.getByRole('button', { name: 'admin' }).click();
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
await page.getByRole('tab', { name: 'Background Tasks' }).click();
await page.getByRole('tab', { name: 'Error Reports' }).click();
await page.getByRole('tab', { name: 'Currencies' }).click();
await page.getByRole('tab', { name: 'Project Codes' }).click();
await page.getByRole('tab', { name: 'Custom Units' }).click();
await page.getByRole('tab', { name: 'Part Parameters' }).click();
await page.getByRole('tab', { name: 'Category Parameters' }).click();
await page.getByRole('tab', { name: 'Label Templates' }).click();
await page.getByRole('tab', { name: 'Report Templates' }).click();
await page.getByRole('tab', { name: 'Plugins' }).click();
await page.getByRole('tab', { name: 'Machines' }).click();
});
test('PUI - Admin - Unauthorized', async ({ page }) => {
// Try to access "admin" page with a non-staff user
await doQuickLogin(page, 'allaccess', 'nolimits');
await page.goto(`${baseUrl}/settings/admin/`);
await page.waitForURL('**/settings/admin/**');
// Should get a permission denied message
await page.getByText('Permission Denied').waitFor();
await page
.getByRole('button', { name: 'Return to the index page' })
.waitFor();
// Try to access user settings page (should be accessible)
await page.goto(`${baseUrl}/settings/user/`);
await page.waitForURL('**/settings/user/**');
await page.getByRole('tab', { name: 'Display Options' }).click();
await page.getByRole('tab', { name: 'Account' }).click();
// Try to access global settings page
await page.goto(`${baseUrl}/settings/system/`);
await page.waitForURL('**/settings/system/**');
await page.getByText('Permission Denied').waitFor();
await page
.getByRole('button', { name: 'Return to the index page' })
.waitFor();
});