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();