diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml
index c413d45ea6..039fb837a4 100644
--- a/.github/workflows/qc_checks.yaml
+++ b/.github/workflows/qc_checks.yaml
@@ -609,7 +609,7 @@ jobs:
invoke int.rebuild-thumbnails
- name: Install dependencies
run: |
- invoke int.frontend-compile
+ invoke int.frontend-compile --extract
cd src/frontend && npx playwright install --with-deps
- name: Run Playwright tests
id: tests
diff --git a/src/frontend/src/components/items/AttachmentLink.tsx b/src/frontend/src/components/items/AttachmentLink.tsx
index 928c61809b..8922e0cb37 100644
--- a/src/frontend/src/components/items/AttachmentLink.tsx
+++ b/src/frontend/src/components/items/AttachmentLink.tsx
@@ -6,6 +6,7 @@ import {
IconFileTypePdf,
IconFileTypeXls,
IconFileTypeZip,
+ IconFileUnknown,
IconLink,
IconPhoto
} from '@tabler/icons-react';
@@ -17,6 +18,11 @@ import { generateUrl } from '../../functions/urls';
*/
export function attachmentIcon(attachment: string): ReactNode {
const sz = 18;
+
+ if (!attachment) {
+ return ;
+ }
+
const suffix = attachment.split('.').pop()?.toLowerCase() ?? '';
switch (suffix) {
case 'pdf':
@@ -58,8 +64,6 @@ export function AttachmentLink({
attachment: string;
external?: boolean;
}>): ReactNode {
- const text = external ? attachment : attachment.split('/').pop();
-
const url = useMemo(() => {
if (external) {
return attachment;
@@ -68,12 +72,24 @@ export function AttachmentLink({
return generateUrl(attachment);
}, [attachment, external]);
+ const text: string = useMemo(() => {
+ if (!attachment) {
+ return '-';
+ }
+
+ return external ? attachment : (attachment.split('/').pop() ?? '-');
+ }, [attachment, external]);
+
return (
{external ? : attachmentIcon(attachment)}
-
- {text}
-
+ {!!attachment ? (
+
+ {text}
+
+ ) : (
+ text
+ )}
);
}
diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx
index 4ff36fbc97..2bdf564f1d 100644
--- a/src/frontend/src/hooks/UseInstance.tsx
+++ b/src/frontend/src/hooks/UseInstance.tsx
@@ -31,6 +31,7 @@ export function useInstance({
params = {},
defaultValue = {},
pathParams,
+ disabled,
hasPrimaryKey = true,
refetchOnMount = true,
refetchOnWindowFocus = false,
@@ -41,6 +42,7 @@ export function useInstance({
hasPrimaryKey?: boolean;
params?: any;
pathParams?: PathParams;
+ disabled?: boolean;
defaultValue?: any;
refetchOnMount?: boolean;
refetchOnWindowFocus?: boolean;
@@ -51,14 +53,20 @@ export function useInstance({
const [instance, setInstance] = useState(defaultValue);
const instanceQuery = useQuery({
+ enabled: !disabled,
queryKey: [
'instance',
endpoint,
pk,
JSON.stringify(params),
- JSON.stringify(pathParams)
+ JSON.stringify(pathParams),
+ disabled
],
queryFn: async () => {
+ if (disabled) {
+ return defaultValue;
+ }
+
if (hasPrimaryKey) {
if (
pk == null ||
diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx
index 6d4c43e44b..8bfd10ad28 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/core/macro';
-import { Grid, Skeleton, Stack } from '@mantine/core';
+import { Alert, Grid, Skeleton, Stack, Text } from '@mantine/core';
import {
IconChecklist,
IconClipboardCheck,
@@ -61,6 +61,60 @@ import BuildOutputTable from '../../tables/build/BuildOutputTable';
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
import { StockItemTable } from '../../tables/stock/StockItemTable';
+function NoItems() {
+ return (
+ } title={t`No Required Items`}>
+
+ {t`This build order does not have any required items.`}
+ {t`The assembled part may not have a Bill of Materials (BOM) defined, or the BOM is empty.`}
+
+
+ );
+}
+
+/**
+ * Panel to display the lines of a build order
+ */
+function BuildLinesPanel({
+ build,
+ isLoading,
+ hasItems
+}: Readonly<{
+ build: any;
+ isLoading: boolean;
+ hasItems: boolean;
+}>) {
+ if (isLoading || !build.pk) {
+ return ;
+ }
+
+ if (!hasItems) {
+ return ;
+ }
+
+ return ;
+}
+
+function BuildAllocationsPanel({
+ build,
+ isLoading,
+ hasItems
+}: Readonly<{
+ build: any;
+ isLoading: boolean;
+ hasItems: boolean;
+}>) {
+ if (isLoading || !build.pk) {
+ return ;
+ }
+
+ if (!hasItems) {
+ return ;
+ }
+
+ return ;
+}
+
/**
* Detail page for a single Build Order
*/
@@ -70,6 +124,19 @@ export default function BuildDetail() {
const user = useUserState();
const globalSettings = useGlobalSettingsState();
+ // Fetch the number of BOM items associated with the build order
+ const { instance: buildLineData, instanceQuery: buildLineQuery } =
+ useInstance({
+ endpoint: ApiEndpoints.build_line_list,
+ params: {
+ build: id,
+ limit: 1
+ },
+ disabled: !id,
+ hasPrimaryKey: false,
+ defaultValue: {}
+ });
+
const buildStatus = useStatusCodes({ modelType: ModelType.build });
const {
@@ -334,9 +401,15 @@ export default function BuildDetail() {
},
{
name: 'line-items',
- label: t`Required Stock`,
+ label: t`Required Parts`,
icon: ,
- content: build?.pk ? :
+ content: (
+ 0}
+ />
+ )
},
{
name: 'allocated-stock',
@@ -345,10 +418,12 @@ export default function BuildDetail() {
hidden:
build.status == buildStatus.COMPLETE ||
build.status == buildStatus.CANCELLED,
- content: build.pk ? (
-
- ) : (
-
+ content: (
+ 0}
+ />
)
},
{
@@ -438,7 +513,16 @@ export default function BuildDetail() {
model_id: build.pk
})
];
- }, [build, id, user, buildStatus, globalSettings]);
+ }, [
+ build,
+ id,
+ user,
+ buildStatus,
+ globalSettings,
+ buildLineQuery.isFetching,
+ buildLineQuery.isLoading,
+ buildLineData
+ ]);
const editBuildOrderFields = useBuildOrderFields({
create: false,
diff --git a/src/frontend/tests/helpers.ts b/src/frontend/tests/helpers.ts
index cf270e5112..494e284412 100644
--- a/src/frontend/tests/helpers.ts
+++ b/src/frontend/tests/helpers.ts
@@ -105,6 +105,8 @@ export const loadTab = async (page, tabName) => {
.getByLabel(/panel-tabs-/)
.getByRole('tab', { name: tabName })
.click();
+
+ await page.waitForLoadState('networkidle');
};
// Activate "table" view in certain contexts
diff --git a/src/frontend/tests/pages/pui_build.spec.ts b/src/frontend/tests/pages/pui_build.spec.ts
index b9efd6b433..25c8e0b458 100644
--- a/src/frontend/tests/pages/pui_build.spec.ts
+++ b/src/frontend/tests/pages/pui_build.spec.ts
@@ -66,7 +66,7 @@ test('Build Order - Basic Tests', async ({ browser }) => {
await loadTab(page, 'Attachments');
await loadTab(page, 'Notes');
await loadTab(page, 'Incomplete Outputs');
- await loadTab(page, 'Required Stock');
+ await loadTab(page, 'Required Parts');
await loadTab(page, 'Allocated Stock');
// Check for expected text in the table
diff --git a/src/frontend/tests/pui_permissions.spec.ts b/src/frontend/tests/pui_permissions.spec.ts
index 4ab994377e..9c0f09d8bd 100644
--- a/src/frontend/tests/pui_permissions.spec.ts
+++ b/src/frontend/tests/pui_permissions.spec.ts
@@ -3,7 +3,7 @@
*/
import test from '@playwright/test';
-import { loadTab } from './helpers';
+import { clickOnRowMenu, loadTab } from './helpers';
import { doCachedLogin } from './login';
/**
@@ -29,10 +29,10 @@ test('Permissions - Admin', async ({ browser, request }) => {
await page.getByRole('button', { name: 'Cancel' }).click();
// Change password
- await page.getByRole('cell', { name: 'Ian', exact: true }).click({
- button: 'right'
- });
- await page.getByRole('button', { name: 'Change Password' }).click();
+ await clickOnRowMenu(
+ await page.getByRole('cell', { name: 'Ian', exact: true })
+ );
+ await page.getByRole('menuitem', { name: 'Change Password' }).click();
await page.getByLabel('text-field-password').fill('123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText("['This password is too short").waitFor();
@@ -46,10 +46,10 @@ test('Permissions - Admin', async ({ browser, request }) => {
await page.getByText('Password updated').click();
// Open profile
- await page.getByRole('cell', { name: 'Ian', exact: true }).click({
- button: 'right'
- });
- await page.getByRole('button', { name: 'Open Profile' }).click();
+ await clickOnRowMenu(
+ await page.getByRole('cell', { name: 'Ian', exact: true })
+ );
+ await page.getByRole('menuitem', { name: 'Open Profile' }).click();
await page.getByText('User: ian', { exact: true }).click();
});
diff --git a/src/frontend/tests/pui_settings.spec.ts b/src/frontend/tests/pui_settings.spec.ts
index e8a706f299..5499c6a64e 100644
--- a/src/frontend/tests/pui_settings.spec.ts
+++ b/src/frontend/tests/pui_settings.spec.ts
@@ -197,17 +197,19 @@ test('Settings - Admin - Barcode History', async ({ browser, request }) => {
// Scan some barcodes (via API calls)
const barcodes = ['ABC1234', 'XYZ5678', 'QRS9012'];
- barcodes.forEach(async (barcode) => {
+ for (let i = 0; i < barcodes.length; i++) {
+ const barcode = barcodes[i];
const url = new URL('barcode/', apiUrl).toString();
await request.post(url, {
data: {
barcode: barcode
},
+ timeout: 5000,
headers: {
Authorization: `Basic ${btoa('admin:inventree')}`
}
});
- });
+ }
await page.getByRole('button', { name: 'admin' }).click();
await page.getByRole('menuitem', { name: 'Admin Center' }).click();