diff --git a/src/frontend/src/components/errors/ClientError.tsx b/src/frontend/src/components/errors/ClientError.tsx
new file mode 100644
index 0000000000..85811ac736
--- /dev/null
+++ b/src/frontend/src/components/errors/ClientError.tsx
@@ -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 ;
+ case 403:
+ return ;
+ case 404:
+ return ;
+ default:
+ break;
+ }
+
+ // Generic client error
+ return (
+
+ );
+}
diff --git a/src/frontend/src/components/errors/GenericErrorPage.tsx b/src/frontend/src/components/errors/GenericErrorPage.tsx
new file mode 100644
index 0000000000..94405ba281
--- /dev/null
+++ b/src/frontend/src/components/errors/GenericErrorPage.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+ {message}
+ {status && (
+
+ Status Code: {status}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/frontend/src/components/errors/NotAuthenticated.tsx b/src/frontend/src/components/errors/NotAuthenticated.tsx
new file mode 100644
index 0000000000..2d8920fd44
--- /dev/null
+++ b/src/frontend/src/components/errors/NotAuthenticated.tsx
@@ -0,0 +1,12 @@
+import { t } from '@lingui/macro';
+
+import GenericErrorPage from './GenericErrorPage';
+
+export default function NotAuthenticated() {
+ return (
+
+ );
+}
diff --git a/src/frontend/src/components/errors/NotFound.tsx b/src/frontend/src/components/errors/NotFound.tsx
new file mode 100644
index 0000000000..4d3f427f32
--- /dev/null
+++ b/src/frontend/src/components/errors/NotFound.tsx
@@ -0,0 +1,12 @@
+import { t } from '@lingui/macro';
+
+import GenericErrorPage from './GenericErrorPage';
+
+export default function NotFound() {
+ return (
+
+ );
+}
diff --git a/src/frontend/src/components/errors/PermissionDenied.tsx b/src/frontend/src/components/errors/PermissionDenied.tsx
new file mode 100644
index 0000000000..dd6e1e38fb
--- /dev/null
+++ b/src/frontend/src/components/errors/PermissionDenied.tsx
@@ -0,0 +1,12 @@
+import { t } from '@lingui/macro';
+
+import GenericErrorPage from './GenericErrorPage';
+
+export default function PermissionDenied() {
+ return (
+
+ );
+}
diff --git a/src/frontend/src/components/errors/ServerError.tsx b/src/frontend/src/components/errors/ServerError.tsx
new file mode 100644
index 0000000000..4539645563
--- /dev/null
+++ b/src/frontend/src/components/errors/ServerError.tsx
@@ -0,0 +1,13 @@
+import { t } from '@lingui/macro';
+
+import GenericErrorPage from './GenericErrorPage';
+
+export default function ServerError({ status }: { status?: number }) {
+ return (
+
+ );
+}
diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx
index c9b660174e..a6385b7250 100644
--- a/src/frontend/src/components/forms/ApiForm.tsx
+++ b/src/frontend/src/components/forms/ApiForm.tsx
@@ -308,7 +308,7 @@ export function ApiForm({
return response;
} 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
throw error;
}
diff --git a/src/frontend/src/components/nav/InstanceDetail.tsx b/src/frontend/src/components/nav/InstanceDetail.tsx
new file mode 100644
index 0000000000..3ee9a1cf51
--- /dev/null
+++ b/src/frontend/src/components/nav/InstanceDetail.tsx
@@ -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 ;
+ }
+
+ if (status >= 500) {
+ return ;
+ }
+
+ if (status >= 400) {
+ return ;
+ }
+
+ return <>{children}>;
+}
diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx
index 88298cc1d0..e7c0c573b1 100644
--- a/src/frontend/src/hooks/UseInstance.tsx
+++ b/src/frontend/src/hooks/UseInstance.tsx
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
-import { useCallback, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { api } from '../App';
import { ApiEndpoints } from '../enums/ApiEndpoints';
@@ -39,6 +39,8 @@ export function useInstance({
}) {
const [instance, setInstance] = useState(defaultValue);
+ const [requestStatus, setRequestStatus] = useState(0);
+
const instanceQuery = useQuery({
queryKey: ['instance', endpoint, pk, params, pathParams],
queryFn: async () => {
@@ -62,6 +64,7 @@ export function useInstance({
params: params
})
.then((response) => {
+ setRequestStatus(response.status);
switch (response.status) {
case 200:
setInstance(response.data);
@@ -72,8 +75,9 @@ export function useInstance({
}
})
.catch((error) => {
+ setRequestStatus(error.response?.status || 0);
setInstance(defaultValue);
- console.error(`Error fetching instance ${url}:`, error);
+ console.error(`ERR: Error fetching instance ${url}:`, error);
if (throwError) throw error;
@@ -89,5 +93,5 @@ export function useInstance({
instanceQuery.refetch();
}, []);
- return { instance, refreshInstance, instanceQuery };
+ return { instance, refreshInstance, instanceQuery, requestStatus };
}
diff --git a/src/frontend/src/pages/ErrorPage.tsx b/src/frontend/src/pages/ErrorPage.tsx
index 624fb3bad6..db52eb7685 100644
--- a/src/frontend/src/pages/ErrorPage.tsx
+++ b/src/frontend/src/pages/ErrorPage.tsx
@@ -1,10 +1,9 @@
-import { Trans, t } from '@lingui/macro';
-import { Container, Text, Title } from '@mantine/core';
+import { t } from '@lingui/macro';
import { useDocumentTitle } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { useRouteError } from 'react-router-dom';
-import { LanguageContext } from '../contexts/LanguageContext';
+import GenericErrorPage from '../components/errors/GenericErrorPage';
import { ErrorResponse } from '../states/states';
export default function ErrorPage() {
@@ -19,18 +18,9 @@ export default function ErrorPage() {
}, [error]);
return (
-
-
-
- Error
-
-
- Sorry, an unexpected error has occurred.
-
-
- {error.statusText || error.message}
-
-
-
+
);
}
diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
index a5d16d530a..2b801566f1 100644
--- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
+++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx
@@ -17,11 +17,13 @@ import {
} from '@tabler/icons-react';
import { lazy, useMemo } from 'react';
+import PermissionDenied from '../../../../components/errors/PermissionDenied';
import { PlaceholderPill } from '../../../../components/items/Placeholder';
import { PanelGroup, PanelType } from '../../../../components/nav/PanelGroup';
import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
import { GlobalSettingList } from '../../../../components/settings/SettingList';
import { Loadable } from '../../../../functions/loading';
+import { useUserState } from '../../../../states/UserState';
const ReportTemplatePanel = Loadable(
lazy(() => import('./ReportTemplatePanel'))
@@ -74,6 +76,8 @@ const CurrencyTable = Loadable(
);
export default function AdminCenter() {
+ const user = useUserState();
+
const adminCenterPanels: PanelType[] = useMemo(() => {
return [
{
@@ -187,19 +191,25 @@ export default function AdminCenter() {
);
return (
-
-
-
-
-
+ <>
+ {user.isStaff() ? (
+
+
+
+
+
+ ) : (
+
+ )}
+ >
);
}
diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
index bd0ac884b8..9a25f715d6 100644
--- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
+++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx
@@ -19,11 +19,13 @@ import {
} from '@tabler/icons-react';
import { useMemo } from 'react';
+import PermissionDenied from '../../../components/errors/PermissionDenied';
import { PlaceholderPanel } from '../../../components/items/Placeholder';
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
import { GlobalSettingList } from '../../../components/settings/SettingList';
import { useServerApiState } from '../../../states/ApiState';
+import { useUserState } from '../../../states/UserState';
/**
* System settings page
@@ -295,19 +297,26 @@ export default function SystemSettings() {
}
];
}, []);
+
+ const user = useUserState();
+
const [server] = useServerApiState((state) => [state.server]);
return (
<>
-
- Switch to User Setting}
- />
-
-
+ {user.isStaff() ? (
+
+ Switch to User Setting}
+ />
+
+
+ ) : (
+
+ )}
>
);
}
diff --git a/src/frontend/src/pages/NotFound.tsx b/src/frontend/src/pages/NotFound.tsx
deleted file mode 100644
index 53c1595a15..0000000000
--- a/src/frontend/src/pages/NotFound.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
- Not Found
-
-
- Sorry, this page is not known or was moved.
-
-
-
-
-
-
- );
-}
diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx
index ec49228af8..28e9929b9c 100644
--- a/src/frontend/src/pages/build/BuildDetail.tsx
+++ b/src/frontend/src/pages/build/BuildDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
+import { Grid, Skeleton, Stack } from '@mantine/core';
import {
IconClipboardCheck,
IconClipboardList,
@@ -30,6 +30,7 @@ import {
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer';
@@ -61,7 +62,8 @@ export default function BuildDetail() {
const {
instance: build,
refreshInstance,
- instanceQuery
+ instanceQuery,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.build_order_list,
pk: id,
@@ -410,21 +412,22 @@ export default function BuildDetail() {
{editBuild.modal}
{duplicateBuild.modal}
{cancelBuild.modal}
-
-
-
-
-
+
+
+
+
+
+
>
);
}
diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx
index d8102cecfb..3d542142bd 100644
--- a/src/frontend/src/pages/company/CompanyDetail.tsx
+++ b/src/frontend/src/pages/company/CompanyDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
+import { Grid, Skeleton, Stack } from '@mantine/core';
import {
IconBuildingFactory2,
IconBuildingWarehouse,
@@ -30,6 +30,7 @@ import {
EditItemAction
} from '../../components/items/ActionDropdown';
import { Breadcrumb } from '../../components/nav/BreadcrumbList';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
@@ -66,7 +67,8 @@ export default function CompanyDetail(props: Readonly) {
const {
instance: company,
refreshInstance,
- instanceQuery
+ instanceQuery,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.company_list,
pk: id,
@@ -320,18 +322,19 @@ export default function CompanyDetail(props: Readonly) {
return (
<>
{editCompany.modal}
-
-
-
-
-
+
+
+
+
+
+
>
);
}
diff --git a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx
index 7a145ad9b7..49a8681064 100644
--- a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx
+++ b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
+import { Grid, Skeleton, Stack } from '@mantine/core';
import {
IconBuildingWarehouse,
IconDots,
@@ -20,6 +20,7 @@ import {
DuplicateItemAction,
EditItemAction
} from '../../components/items/ActionDropdown';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
@@ -44,7 +45,8 @@ export default function ManufacturerPartDetail() {
const {
instance: manufacturerPart,
instanceQuery,
- refreshInstance
+ refreshInstance,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.manufacturer_part_list,
pk: id,
@@ -244,17 +246,18 @@ export default function ManufacturerPartDetail() {
return (
<>
{editManufacturerPart.modal}
-
-
-
-
-
+
+
+
+
+
+
>
);
}
diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx
index bef8168c48..d6e917e21e 100644
--- a/src/frontend/src/pages/company/SupplierPartDetail.tsx
+++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
+import { Grid, Skeleton, Stack } from '@mantine/core';
import {
IconCurrencyDollar,
IconDots,
@@ -21,6 +21,7 @@ import {
DuplicateItemAction,
EditItemAction
} from '../../components/items/ActionDropdown';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
@@ -46,7 +47,8 @@ export default function SupplierPartDetail() {
const {
instance: supplierPart,
instanceQuery,
- refreshInstance
+ refreshInstance,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.supplier_part_list,
pk: id,
@@ -312,18 +314,19 @@ export default function SupplierPartDetail() {
return (
<>
{editSuppliertPart.modal}
-
-
-
-
-
+
+
+
+
+
+
>
);
}
diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx
index a94999e396..2a5dca6661 100644
--- a/src/frontend/src/pages/part/CategoryDetail.tsx
+++ b/src/frontend/src/pages/part/CategoryDetail.tsx
@@ -18,6 +18,7 @@ import {
DeleteItemAction,
EditItemAction
} from '../../components/items/ActionDropdown';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
@@ -56,7 +57,8 @@ export default function CategoryDetail({}: {}) {
const {
instance: category,
refreshInstance,
- instanceQuery
+ instanceQuery,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.category_list,
hasPrimaryKey: true,
@@ -275,29 +277,34 @@ export default function CategoryDetail({}: {}) {
<>
{editCategory.modal}
{deleteCategory.modal}
-
-
- {
- setTreeOpen(false);
- }}
- selectedId={category?.pk}
- />
- {
- setTreeOpen(true);
- }}
- actions={categoryActions}
- />
-
-
+
+
+
+ {
+ setTreeOpen(false);
+ }}
+ selectedId={category?.pk}
+ />
+ {
+ setTreeOpen(true);
+ }}
+ actions={categoryActions}
+ />
+
+
+
>
);
}
diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx
index 3eafa7b23f..838d304b11 100644
--- a/src/frontend/src/pages/part/PartDetail.tsx
+++ b/src/frontend/src/pages/part/PartDetail.tsx
@@ -1,13 +1,5 @@
import { t } from '@lingui/macro';
-import {
- Alert,
- Divider,
- Grid,
- LoadingOverlay,
- Skeleton,
- Stack,
- Table
-} from '@mantine/core';
+import { Alert, Grid, Skeleton, Stack, Table } from '@mantine/core';
import {
IconBookmarks,
IconBuilding,
@@ -31,7 +23,7 @@ import {
IconVersions
} from '@tabler/icons-react';
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 { api } from '../../App';
@@ -54,6 +46,7 @@ import {
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
@@ -105,7 +98,8 @@ export default function PartDetail() {
const {
instance: part,
refreshInstance,
- instanceQuery
+ instanceQuery,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.part_list,
pk: id,
@@ -821,33 +815,34 @@ export default function PartDetail() {
{duplicatePart.modal}
{editPart.modal}
{deletePart.modal}
-
-
- {
- setTreeOpen(false);
- }}
- selectedId={part?.category}
- />
- {
- setTreeOpen(true);
- }}
- actions={partActions}
- />
-
- {transferStockItems.modal}
- {countStockItems.modal}
-
+
+
+ {
+ setTreeOpen(false);
+ }}
+ selectedId={part?.category}
+ />
+ {
+ setTreeOpen(true);
+ }}
+ actions={partActions}
+ />
+
+ {transferStockItems.modal}
+ {countStockItems.modal}
+
+
>
);
}
diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
index fe79915276..9fab1e1ccf 100644
--- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
+++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
+import { Grid, Skeleton, Stack } from '@mantine/core';
import {
IconDots,
IconInfoCircle,
@@ -27,6 +27,7 @@ import {
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer';
@@ -40,7 +41,6 @@ import {
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
-import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { AttachmentTable } from '../../tables/general/AttachmentTable';
import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable';
@@ -57,7 +57,8 @@ export default function PurchaseOrderDetail() {
const {
instance: order,
instanceQuery,
- refreshInstance
+ refreshInstance,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.purchase_order_list,
pk: id,
@@ -355,18 +356,19 @@ export default function PurchaseOrderDetail() {
return (
<>
{editPurchaseOrder.modal}
-
-
-
-
-
+
+
+
+
+
+
>
);
}
diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx
index 4d62470f1a..c65f778faa 100644
--- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx
+++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
+import { Grid, Skeleton, Stack } from '@mantine/core';
import {
IconDots,
IconInfoCircle,
@@ -23,6 +23,7 @@ import {
EditItemAction
} from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer';
@@ -36,7 +37,6 @@ import {
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
-import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { AttachmentTable } from '../../tables/general/AttachmentTable';
@@ -51,7 +51,8 @@ export default function ReturnOrderDetail() {
const {
instance: order,
instanceQuery,
- refreshInstance
+ refreshInstance,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.return_order_list,
pk: id,
@@ -320,18 +321,19 @@ export default function ReturnOrderDetail() {
<>
{editReturnOrder.modal}
{duplicateReturnOrder.modal}
-
-
-
-
-
+
+
+
+
+
+
>
);
}
diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx
index 12b31bb9e6..2222f7cbd2 100644
--- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx
+++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
+import { Grid, Skeleton, Stack } from '@mantine/core';
import {
IconDots,
IconInfoCircle,
@@ -26,6 +26,7 @@ import {
EditItemAction
} from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StatusRenderer } from '../../components/render/StatusRenderer';
@@ -39,7 +40,6 @@ import {
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
-import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
import { AttachmentTable } from '../../tables/general/AttachmentTable';
@@ -55,7 +55,8 @@ export default function SalesOrderDetail() {
const {
instance: order,
instanceQuery,
- refreshInstance
+ refreshInstance,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.sales_order_list,
pk: id,
@@ -344,18 +345,19 @@ export default function SalesOrderDetail() {
return (
<>
{editSalesOrder.modal}
-
-
-
-
-
+
+
+
+
+
+
>
);
}
diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx
index 744e782e5b..4ad6330286 100644
--- a/src/frontend/src/pages/stock/LocationDetail.tsx
+++ b/src/frontend/src/pages/stock/LocationDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core';
+import { Skeleton, Stack, Text } from '@mantine/core';
import {
IconDots,
IconInfoCircle,
@@ -23,6 +23,7 @@ import {
UnlinkBarcodeAction,
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
@@ -63,7 +64,8 @@ export default function Stock() {
const {
instance: location,
refreshInstance,
- instanceQuery
+ instanceQuery,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.stock_location_list,
hasPrimaryKey: true,
@@ -355,29 +357,33 @@ export default function Stock() {
<>
{editLocation.modal}
{deleteLocation.modal}
-
-
- setTreeOpen(false)}
- selectedId={location?.pk}
- />
- {
- setTreeOpen(true);
- }}
- />
-
- {transferStockItems.modal}
- {countStockItems.modal}
-
+
+
+ setTreeOpen(false)}
+ selectedId={location?.pk}
+ />
+ {
+ setTreeOpen(true);
+ }}
+ />
+
+ {transferStockItems.modal}
+ {countStockItems.modal}
+
+
>
);
}
diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx
index 14face5201..c07814af48 100644
--- a/src/frontend/src/pages/stock/StockDetail.tsx
+++ b/src/frontend/src/pages/stock/StockDetail.tsx
@@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
-import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core';
+import { Grid, Skeleton, Stack } from '@mantine/core';
import {
IconBookmark,
IconBoxPadding,
@@ -33,6 +33,7 @@ import {
ViewBarcodeAction
} from '../../components/items/ActionDropdown';
import { PlaceholderPanel } from '../../components/items/Placeholder';
+import InstanceDetail from '../../components/nav/InstanceDetail';
import NavigationTree from '../../components/nav/NavigationTree';
import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
@@ -76,7 +77,8 @@ export default function StockDetail() {
const {
instance: stockitem,
refreshInstance,
- instanceQuery
+ instanceQuery,
+ requestStatus
} = useInstance({
endpoint: ApiEndpoints.stock_item_list,
pk: id,
@@ -548,35 +550,36 @@ export default function StockDetail() {
}, [stockitem, instanceQuery]);
return (
-
-
- setTreeOpen(false)}
- selectedId={stockitem?.location}
- />
- {
- setTreeOpen(true);
- }}
- actions={stockActions}
- />
-
- {editStockItem.modal}
- {duplicateStockItem.modal}
- {deleteStockItem.modal}
- {countStockItem.modal}
- {addStockItem.modal}
- {removeStockItem.modal}
- {transferStockItem.modal}
-
+
+
+ setTreeOpen(false)}
+ selectedId={stockitem?.location}
+ />
+ {
+ setTreeOpen(true);
+ }}
+ actions={stockActions}
+ />
+
+ {editStockItem.modal}
+ {duplicateStockItem.modal}
+ {deleteStockItem.modal}
+ {countStockItem.modal}
+ {addStockItem.modal}
+ {removeStockItem.modal}
+ {transferStockItem.modal}
+
+
);
}
diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx
index a80a0fdce1..b7a040458b 100644
--- a/src/frontend/src/router.tsx
+++ b/src/frontend/src/router.tsx
@@ -101,7 +101,9 @@ export const AdminCenter = Loadable(
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 Logout = Loadable(lazy(() => import('./pages/Auth/Logout')));
export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In')));
diff --git a/src/frontend/tests/baseFixtures.ts b/src/frontend/tests/baseFixtures.ts
index 841a91acaf..5789f7e975 100644
--- a/src/frontend/tests/baseFixtures.ts
+++ b/src/frontend/tests/baseFixtures.ts
@@ -64,7 +64,8 @@ export const test = baseTest.extend({
url != 'http://localhost:8000/api/barcode/' &&
url != 'http://localhost:8000/api/news/?search=&offset=0&limit=25' &&
url != 'https://docs.inventree.org/en/versions.json' &&
- !url.startsWith('chrome://')
+ !url.startsWith('chrome://') &&
+ url.indexOf('99999') < 0
)
messages.push(msg);
});
diff --git a/src/frontend/tests/login.ts b/src/frontend/tests/login.ts
index 0be2945163..d4cef1d681 100644
--- a/src/frontend/tests/login.ts
+++ b/src/frontend/tests/login.ts
@@ -31,10 +31,14 @@ export const doQuickLogin = async (
password = password ?? user.password;
url = url ?? baseUrl;
- // await page.goto(logoutUrl);
await page.goto(`${url}/login/?login=${username}&password=${password}`);
await page.waitForURL('**/platform/home');
await page
.getByRole('heading', { name: 'Welcome to your Dashboard,' })
.waitFor();
};
+
+export const doLogout = async (page) => {
+ await page.goto(`${baseUrl}/logout/`);
+ await page.waitForURL('**/platform/login');
+};
diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts
index d411738184..bcf8d86ff1 100644
--- a/src/frontend/tests/pages/pui_part.spec.ts
+++ b/src/frontend/tests/pages/pui_part.spec.ts
@@ -224,3 +224,13 @@ test('PUI - Pages - Part - Notes', async ({ page }) => {
// Check that the original notes are still present
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());
+});
diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts
index e7d8c3d74c..fa23b82279 100644
--- a/src/frontend/tests/pui_basic.spec.ts
+++ b/src/frontend/tests/pui_basic.spec.ts
@@ -54,4 +54,5 @@ test('PUI - Quick Login Test', async ({ page }) => {
// Logout (via URL)
await page.goto(`${baseUrl}/logout/`);
await page.waitForURL('**/platform/login');
+ await page.getByLabel('username');
});
diff --git a/src/frontend/tests/pui_general.spec.ts b/src/frontend/tests/pui_general.spec.ts
index c436308c5e..a62547aca3 100644
--- a/src/frontend/tests/pui_general.spec.ts
+++ b/src/frontend/tests/pui_general.spec.ts
@@ -1,6 +1,6 @@
import { test } from './baseFixtures.js';
import { baseUrl } from './defaults.js';
-import { doQuickLogin } from './login.js';
+import { doLogout, doQuickLogin } from './login.js';
test('PUI - Parts', async ({ page }) => {
await doQuickLogin(page);
@@ -141,57 +141,6 @@ test('PUI - Scanning', async ({ page }) => {
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 }) => {
await doQuickLogin(page);
diff --git a/src/frontend/tests/pui_settings.spec.ts b/src/frontend/tests/pui_settings.spec.ts
new file mode 100644
index 0000000000..b0962dcd36
--- /dev/null
+++ b/src/frontend/tests/pui_settings.spec.ts
@@ -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();
+});