2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-06 09:43:38 +00:00

[UI] BOM Editing (#10210)

* Support minimum column width

* Adjust DescriptionColumn and StatusColumn

* Column refactoring

* Refactor PartColumn

* Refactor LineItemsProgerssColumn

* Tweaks

* Ensure "can_build" value is not negative

* Render row expansion icon

* Add subassembly table for BOM

* Add controls for BOM editing

* Fix row click context

* Improve rendering for BOM sub-rows

* Hide BOM actions unless editing

* Disable row expansion for now

* Revert gitleaks changes

* Remove gitleaks tags

* Remove dead code

* Remove commented code

* Adjust playwright tests

Co-authored-by: Copilot <copilot@github.com>

* Update docs

Co-authored-by: Copilot <copilot@github.com>

* Further playwright fixes

---------

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Oliver
2026-04-26 12:49:21 +10:00
committed by GitHub
parent f9cda95427
commit ffe6c13edf
5 changed files with 107 additions and 40 deletions
+61 -33
View File
@@ -1,3 +1,19 @@
import { t } from '@lingui/core/macro';
import { Alert, Group, Stack, Text } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import {
IconArrowRight,
IconCircleCheck,
IconEdit,
IconFileUpload,
IconLock,
IconPlus,
IconSwitch3
} from '@tabler/icons-react';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ActionButton } from '@lib/components/ActionButton';
import {
type RowAction,
RowDeleteAction,
@@ -12,20 +28,6 @@ import { navigateToLink } from '@lib/functions/Navigation';
import useTable from '@lib/hooks/UseTable';
import type { TableFilter } from '@lib/types/Filters';
import type { TableColumn } from '@lib/types/Tables';
import { t } from '@lingui/core/macro';
import { Alert, Group, Stack, Text } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import {
IconArrowRight,
IconCircleCheck,
IconFileUpload,
IconLock,
IconPlus,
IconSwitch3
} from '@tabler/icons-react';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Thumbnail } from '../../components/images/Thumbnail';
import { ActionDropdown } from '../../components/items/ActionDropdown';
import { RenderPart } from '../../components/render/Part';
import { useApi } from '../../contexts/ApiContext';
@@ -45,7 +47,8 @@ import {
DescriptionColumn,
IPNColumn,
NoteColumn,
ReferenceColumn
ReferenceColumn,
RenderPartColumn
} from '../ColumnRenderers';
import { PartCategoryFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -80,27 +83,35 @@ export function BomTable({
const user = useUserState();
const table = useTable('bom');
const navigate = useNavigate();
const openImporter = useImporterState((state) => state.openImporter);
const [isEditing, setIsEditing] = useState<boolean>(false);
const tableColumns: TableColumn[] = useMemo(() => {
return [
{
accessor: 'sub_part',
switchable: false,
sortable: true,
minWidth: 250,
render: (record: any) => {
const part = record.sub_part_detail;
const extra = [];
if (record.part != partId) {
if (partId && record.part != partId) {
extra.push(
<Text key='different-parent'>{t`This BOM item is defined for a different parent`}</Text>
<Text
key='different-parent'
size='sm'
>{t`This BOM item is defined for a different parent`}</Text>
);
}
if (!record.validated) {
extra.push(
<Text key='not-validated' c='red'>
<Text key='not-validated' c='red' size='sm'>
{t`This BOM item has not been validated`}
</Text>
);
@@ -109,13 +120,7 @@ export function BomTable({
return (
part && (
<TableHoverCard
value={
<Thumbnail
src={part.thumbnail || part.image}
alt={part.description}
text={part.full_name}
/>
}
value={<RenderPartColumn part={part} />}
iconColor={record.validated ? undefined : 'red'}
extra={extra}
title={t`Part Information`}
@@ -376,7 +381,8 @@ export function BomTable({
return '-';
}
const can_build = Math.trunc(record.can_build);
const can_build = Math.max(0, Math.trunc(record.can_build));
const value = (
<Text
fs={record.consumable && 'italic'}
@@ -403,7 +409,7 @@ export function BomTable({
},
NoteColumn({})
];
}, [partId, params]);
}, [isEditing, partId, params]);
const tableFilters: TableFilter[] = useMemo(() => {
return [
@@ -614,7 +620,7 @@ export function BomTable({
})
];
},
[partId, partLocked, user]
[isEditing, partId, partLocked, user]
);
const tableActions = useMemo(() => {
@@ -624,7 +630,7 @@ export function BomTable({
tooltip={t`Add BOM Items`}
position='bottom-start'
icon={<IconPlus />}
hidden={partLocked || !user.hasAddRole(UserRoles.part)}
hidden={!isEditing || partLocked || !user.hasAddRole(UserRoles.part)}
actions={[
{
name: t`Add BOM Item`,
@@ -639,9 +645,29 @@ export function BomTable({
onClick: () => importBomItem.open()
}
]}
/>,
<ActionButton
key='edit-bom'
hidden={partLocked || !user.hasChangeRole(UserRoles.part) || isEditing}
tooltip={t`Edit BOM`}
icon={<IconEdit />}
onClick={() => {
setIsEditing(true);
}}
/>,
<ActionButton
key='finish-editing'
hidden={!isEditing}
color='green'
tooltip={t`Finish Editing BOM`}
icon={<IconCircleCheck />}
onClick={() => {
setIsEditing(false);
table.refreshTable();
}}
/>
];
}, [partLocked, user]);
}, [isEditing, partLocked, user]);
return (
<>
@@ -678,9 +704,11 @@ export function BomTable({
tableFilters: tableFilters,
modelType: ModelType.part,
modelField: 'sub_part',
rowActions: rowActions,
enableSelection: !partLocked,
enableBulkDelete: !partLocked && user.hasDeleteRole(UserRoles.part),
onCellClick: () => {},
rowActions: isEditing ? rowActions : undefined,
enableSelection: isEditing && !partLocked,
enableBulkDelete:
isEditing && !partLocked && user.hasDeleteRole(UserRoles.part),
enableDownload: true
}}
/>
+30 -5
View File
@@ -179,10 +179,15 @@ test('Parts - BOM', async ({ browser }) => {
// Move the mouse away
await page.getByRole('link', { name: 'Bill of Materials' }).hover();
const cell = await page.getByRole('cell', {
name: 'Small plastic enclosure, black',
exact: true
});
// Enable BOM editing
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.waitFor();
const cell = await page
.getByRole('cell', { name: 'Thumbnail 1551ABK' })
.first();
await clickOnRowMenu(cell);
@@ -202,6 +207,12 @@ test('Parts - BOM', async ({ browser }) => {
await page.getByRole('button', { name: 'Add Substitute' }).waitFor();
await page.getByRole('button', { name: 'Close' }).click();
// Finish editing the BOM
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.click();
await page.getByRole('button', { name: 'action-button-edit-bom' }).waitFor();
});
/**
@@ -211,8 +222,15 @@ test('Parts - BOM', async ({ browser }) => {
test('Parts - BOM Validation', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/107/bom' });
// Enable BOM editing
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.waitFor();
// Edit line item, to ensure BOM is not valid
const cell = await page.getByRole('cell', { name: 'Red paint Red Paint' });
const cell = await page.getByRole('cell', { name: 'Thumbnail Red Paint' });
await clickOnRowMenu(cell);
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
@@ -288,6 +306,13 @@ test('Parts - Locking', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/104/bom' });
await loadTab(page, 'Bill of Materials');
// Enable BOM editing
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.waitFor();
await page
.getByRole('button', { name: 'action-menu-add-bom-items' })
.waitFor();
+6
View File
@@ -76,6 +76,12 @@ test('Importing - BOM', async ({ browser }) => {
url: 'part/109/bom'
});
// Enable BOM editing
await page.getByRole('button', { name: 'action-button-edit-bom' }).click();
await page
.getByRole('button', { name: 'action-button-finish-editing-' })
.waitFor();
// Open the BOM importer wizard
await page.getByRole('button', { name: 'action-menu-add-bom-items' }).click();
+1
View File
@@ -9,6 +9,7 @@ from inventree.api import InvenTreeAPI
server = os.environ.get('INVENTREE_PYTHON_TEST_SERVER', 'http://127.0.0.1:12345')
user = os.environ.get('INVENTREE_PYTHON_TEST_USERNAME', 'testuser')
pwd = os.environ.get('INVENTREE_PYTHON_TEST_PASSWORD', 'testpassword')
api_client = InvenTreeAPI(
server,
username=user,