Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions src/frontend/src/tables/bom/BomSubassemblyTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { ApiEndpoints, apiUrl } from '@lib/index';
import { Group, Space } from '@mantine/core';
import { useLocalStorage } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query';
import {
DataTable,
type DataTableColumn,
type DataTableRowExpansionProps
} from 'mantine-datatable';
import { useMemo, useState } from 'react';
import { api } from '../../App';
import { RenderPartColumn } from '../ColumnRenderers';
import RowExpansionIcon from '../RowExpansionIcon';

/**
* Display a nested subassembly table for a Bill of Materials (BOM).
* - This component is used to render a subassembly table within a BOM.
* - It is designed to be used within a larger BOM table structure.
* - It may be rendered recursively, for multi-level subassemblies.
*/
export default function BomSubassemblyTable({
columns,
partId,
depth
}: {
columns: any[];
partId: number | string;
depth: number;
}) {
const [expandedRecords, setExpandedRecords] = useState<string[]>([]);

// Observe column widths from top-level BOM table
const [columnWidths] = useLocalStorage({
key: 'table-bom-columns-width',
defaultValue: []
});

const assemblyColumns: DataTableColumn[] = useMemo(() => {
return columns.map((col) => {
// Handle the 'part' column differently based on depth and subassembly ID
const column = { ...col };

if (col.accessor == 'sub_part') {
column.render = (record: any) => (
<Group wrap='nowrap'>
<Space w={depth * 10} />
{record.sub_part_detail?.assembly && (
<RowExpansionIcon
enabled={record.sub_part_detail?.assembly}
expanded={expandedRecords.includes(record.pk)}
/>
)}
<RenderPartColumn part={record.sub_part_detail} />
</Group>
);
}

// Find matching column width
const matchingWidth = columnWidths.find(
(cw: any) => Object.keys(cw)[0] === col.accessor
);

if (matchingWidth) {
column.cellsStyle = (record: any, index: number) => {
return {
width: Object.values(matchingWidth)[0]
};
};
}

return column;
});
}, [columns, columnWidths, expandedRecords]);

const subassemblyData = useQuery({
enabled: !!partId,
queryKey: ['bomSubassembly', partId],
queryFn: async () => {
return api
.get(apiUrl(ApiEndpoints.bom_list), {
params: {
part: partId,
sub_part_detail: true
}
})
.then((res) => res.data)
.catch((err) => {
console.error('Error fetching BOM subassembly data:', err);
return [];
});
}
});

const rowExpansionProps: DataTableRowExpansionProps<any> = useMemo(() => {
return {
allowMultiple: true,
expandable: ({ record }: { record: any }) =>
record.sub_part_detail?.assembly,
expanded: {
recordIds: expandedRecords,
onRecordIdsChange: setExpandedRecords
},
content: ({ record }: { record: any }) => (
<BomSubassemblyTable
columns={columns}
partId={record.sub_part}
depth={depth + 1}
/>
)
};
}, [columns, depth, expandedRecords]);

return (
<DataTable
noHeader
withColumnBorders
columns={assemblyColumns}
// columns={tableColumns.effectiveColumns}
records={subassemblyData.data || []}
rowExpansion={rowExpansionProps}
/>
);
}
100 changes: 77 additions & 23 deletions src/frontend/src/tables/bom/BomTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { showNotification } from '@mantine/notifications';
import {
IconArrowRight,
IconCircleCheck,
IconEdit,
IconExclamationCircle,
IconFileArrowLeft,
IconLock,
Expand All @@ -27,7 +28,7 @@ import { apiUrl } from '@lib/functions/Api';
import { navigateToLink } from '@lib/functions/Navigation';
import type { TableFilter } from '@lib/types/Filters';
import type { TableColumn } from '@lib/types/Tables';
import { Thumbnail } from '../../components/images/Thumbnail';
import type { DataTableRowExpansionProps } from 'mantine-datatable';
import ImporterDrawer from '../../components/importer/ImporterDrawer';
import { RenderPart } from '../../components/render/Part';
import { useApi } from '../../contexts/ApiContext';
Expand All @@ -45,11 +46,14 @@ import {
BooleanColumn,
DescriptionColumn,
NoteColumn,
ReferenceColumn
ReferenceColumn,
RenderPartColumn
} from '../ColumnRenderers';
import { PartCategoryFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import RowExpansionIcon from '../RowExpansionIcon';
import { TableHoverCard } from '../TableHoverCard';
import BomSubassemblyTable from './BomSubassemblyTable';

// Calculate the total stock quantity available for a given BomItem
function availableStockQuantity(record: any): number {
Expand Down Expand Up @@ -81,6 +85,8 @@ export function BomTable({
const table = useTable('bom');
const navigate = useNavigate();

const [isEditing, setIsEditing] = useState<boolean>(false);

const [importOpened, setImportOpened] = useState<boolean>(false);

const [selectedSession, setSelectedSession] = useState<number | undefined>(
Expand All @@ -93,6 +99,7 @@ export function BomTable({
accessor: 'sub_part',
switchable: false,
sortable: true,
minWidth: 250,
render: (record: any) => {
const part = record.sub_part_detail;
const extra = [];
Expand All @@ -105,18 +112,16 @@ export function BomTable({

return (
part && (
<Group gap='xs' justify='space-between' wrap='nowrap'>
<TableHoverCard
value={
<Thumbnail
src={part.thumbnail || part.image}
alt={part.description}
text={part.full_name}
<Group justify='space-between'>
<Group gap='xs' wrap='nowrap'>
{!isEditing && part?.assembly && (
<RowExpansionIcon
enabled={part?.assembly}
expanded={table.isRowExpanded(record.pk)}
/>
}
extra={extra}
title={t`Part Information`}
/>
)}
<RenderPartColumn part={part} />
</Group>
{!record.validated && (
<Tooltip label={t`This BOM item has not been validated`}>
<ActionIcon color='red' variant='transparent' size='sm'>
Expand Down Expand Up @@ -363,7 +368,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'}
Expand All @@ -390,7 +396,7 @@ export function BomTable({
},
NoteColumn({})
];
}, [partId, params]);
}, [isEditing, partId, params]);

const tableFilters: TableFilter[] = useMemo(() => {
return [
Expand Down Expand Up @@ -595,26 +601,71 @@ export function BomTable({
})
];
},
[partId, partLocked, user]
[isEditing, partId, partLocked, user]
);

const tableActions = useMemo(() => {
return [
<ActionButton
key='import-bom'
hidden={partLocked || !user.hasAddRole(UserRoles.part)}
hidden={!isEditing || partLocked || !user.hasAddRole(UserRoles.part)}
tooltip={t`Import BOM Data`}
icon={<IconFileArrowLeft />}
onClick={() => importBomItem.open()}
/>,
<AddItemButton
key='add-bom-item'
hidden={partLocked || !user.hasAddRole(UserRoles.part)}
hidden={!isEditing || partLocked || !user.hasAddRole(UserRoles.part)}
tooltip={t`Add BOM Item`}
onClick={() => newBomItem.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]);

// If not in "editing" mode, the BOM can be expanded to show subassemblies
const rowExpansionProps: DataTableRowExpansionProps<any> | undefined =
useMemo(() => {
if (isEditing) {
return undefined;
}

const subassemblyColumns: any[] = tableColumns.filter(
(col) => !col.hidden && !table.hiddenColumns.includes(col.accessor)
);

return {
allowMultiple: true,
expandable: ({ record }: { record: any }) =>
table.isRowExpanded(record.pk) || record.sub_part_detail?.assembly,
content: ({ record }: { record: any }) => (
<BomSubassemblyTable
columns={subassemblyColumns}
partId={record.sub_part}
depth={0}
/>
)
};
}, [isEditing, tableColumns, table.hiddenColumns, table.isRowExpanded]);

return (
<>
Expand Down Expand Up @@ -649,10 +700,13 @@ export function BomTable({
tableFilters: tableFilters,
modelType: ModelType.part,
modelField: 'sub_part',
rowActions: rowActions,
enableSelection: !partLocked,
enableBulkDelete: !partLocked && user.hasDeleteRole(UserRoles.part),
enableDownload: true
onCellClick: () => {},
rowActions: isEditing ? rowActions : undefined,
enableSelection: isEditing && !partLocked,
enableBulkDelete:
isEditing && !partLocked && user.hasDeleteRole(UserRoles.part),
enableDownload: true,
rowExpansion: rowExpansionProps
}}
/>
</Stack>
Expand Down
Loading