2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-06 13:40:56 +00:00

[UI] Build order visual enhancements (#9931)

* Adjust UI wording

- Separation between "items" and "stock"

* Add info panel if build order has no required items

* Fixes for playwright testing

- Ensure cookies are completely cleaned  between sessions
- Fix base URL based on vite command
- Fix samesite cookie mode
- Prevent /static/ files being served by web server on :8000

* Remove gunicorn option

* Fix unit test

* Readjust base URL

* Simplify doCachedLogin

* Adjust text

* Ensure translations are extracted

- Otherwise, playwright will not find the right strings...

* Make admin test more reliable

* Remove asynciness

* Fix <AttachmentLink>

- Allow null "attachment" value

* Better implementation

* Cleanup
This commit is contained in:
Oliver
2025-07-03 12:15:22 +10:00
committed by GitHub
parent ccc62255c4
commit ee3a574029
8 changed files with 139 additions and 27 deletions

View File

@ -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

View File

@ -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 <IconFileUnknown size={sz} />;
}
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 (
<Group justify='left' gap='sm' wrap='nowrap'>
{external ? <IconLink /> : attachmentIcon(attachment)}
<Anchor href={url} target='_blank' rel='noopener noreferrer'>
{text}
</Anchor>
{!!attachment ? (
<Anchor href={url} target='_blank' rel='noopener noreferrer'>
{text}
</Anchor>
) : (
text
)}
</Group>
);
}

View File

@ -31,6 +31,7 @@ export function useInstance<T = any>({
params = {},
defaultValue = {},
pathParams,
disabled,
hasPrimaryKey = true,
refetchOnMount = true,
refetchOnWindowFocus = false,
@ -41,6 +42,7 @@ export function useInstance<T = any>({
hasPrimaryKey?: boolean;
params?: any;
pathParams?: PathParams;
disabled?: boolean;
defaultValue?: any;
refetchOnMount?: boolean;
refetchOnWindowFocus?: boolean;
@ -51,14 +53,20 @@ export function useInstance<T = any>({
const [instance, setInstance] = useState<T | undefined>(defaultValue);
const instanceQuery = useQuery<T>({
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 ||

View File

@ -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 (
<Alert color='blue' icon={<IconInfoCircle />} title={t`No Required Items`}>
<Stack gap='xs'>
<Text>{t`This build order does not have any required items.`}</Text>
<Text>{t`The assembled part may not have a Bill of Materials (BOM) defined, or the BOM is empty.`}</Text>
</Stack>
</Alert>
);
}
/**
* 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 <Skeleton w={'100%'} h={400} animate />;
}
if (!hasItems) {
return <NoItems />;
}
return <BuildLineTable build={build} />;
}
function BuildAllocationsPanel({
build,
isLoading,
hasItems
}: Readonly<{
build: any;
isLoading: boolean;
hasItems: boolean;
}>) {
if (isLoading || !build.pk) {
return <Skeleton w={'100%'} h={400} animate />;
}
if (!hasItems) {
return <NoItems />;
}
return <BuildAllocatedStockTable buildId={build.pk} showPartInfo allowEdit />;
}
/**
* 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: <IconListNumbers />,
content: build?.pk ? <BuildLineTable build={build} /> : <Skeleton />
content: (
<BuildLinesPanel
build={build}
isLoading={buildLineQuery.isFetching || buildLineQuery.isLoading}
hasItems={buildLineData?.count > 0}
/>
)
},
{
name: 'allocated-stock',
@ -345,10 +418,12 @@ export default function BuildDetail() {
hidden:
build.status == buildStatus.COMPLETE ||
build.status == buildStatus.CANCELLED,
content: build.pk ? (
<BuildAllocatedStockTable buildId={build.pk} showPartInfo allowEdit />
) : (
<Skeleton />
content: (
<BuildAllocationsPanel
build={build}
isLoading={buildLineQuery.isFetching || buildLineQuery.isLoading}
hasItems={buildLineData?.count > 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,

View File

@ -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

View File

@ -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

View File

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

View File

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