2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-16 08:18:53 +00:00

Updates to part revision support (#11670)

* Update revision validation

* Refactor UI display

* Fix for usePartFields

* Rearrange part settings

* Better visuals

* Update docs

* use 'full_name' field

* Update playwright tests

* Adjust unit test

* Fix playwright tests
This commit is contained in:
Oliver
2026-04-04 00:10:25 +11:00
committed by GitHub
parent 9c1d8c1b1d
commit bb3293ef31
7 changed files with 73 additions and 113 deletions

View File

@@ -16,8 +16,6 @@ export function usePartFields({
duplicatePartInstance?: any;
create?: boolean;
}): ApiFormFieldSet {
const settings = useGlobalSettingsState();
const globalSettings = useGlobalSettingsState();
const [virtual, setVirtual] = useState<boolean | undefined>(undefined);
@@ -38,8 +36,10 @@ export function usePartFields({
revision: {},
revision_of: {
filters: {
is_revision: false,
is_template: false
is_template: false,
assembly: globalSettings.isSet('PART_REVISION_ASSEMBLY_ONLY')
? true
: undefined
}
},
variant_of: {
@@ -155,14 +155,14 @@ export function usePartFields({
value: true
},
copy_bom: {
value: settings.isSet('PART_COPY_BOM'),
value: globalSettings.isSet('PART_COPY_BOM'),
hidden: !duplicatePartInstance?.assembly
},
copy_notes: {
value: true
},
copy_parameters: {
value: settings.isSet('PART_COPY_PARAMETERS')
value: globalSettings.isSet('PART_COPY_PARAMETERS')
},
copy_tests: {
value: true,
@@ -172,18 +172,18 @@ export function usePartFields({
};
}
if (settings.isSet('PART_REVISION_ASSEMBLY_ONLY')) {
if (globalSettings.isSet('PART_REVISION_ASSEMBLY_ONLY')) {
fields.revision_of.filters['assembly'] = true;
}
// Pop 'revision' field if PART_ENABLE_REVISION is False
if (!settings.isSet('PART_ENABLE_REVISION')) {
if (!globalSettings.isSet('PART_ENABLE_REVISION')) {
delete fields['revision'];
delete fields['revision_of'];
}
// Pop 'expiry' field if expiry not enabled
if (!settings.isSet('STOCK_ENABLE_EXPIRY')) {
if (!globalSettings.isSet('STOCK_ENABLE_EXPIRY')) {
delete fields['default_expiry'];
}
@@ -198,8 +198,7 @@ export function usePartFields({
purchaseable,
create,
globalSettings,
duplicatePartInstance,
settings
duplicatePartInstance
]);
}

View File

@@ -199,13 +199,13 @@ export default function SystemSettings() {
content: (
<GlobalSettingList
keys={[
'PART_NAME_FORMAT',
'PART_IPN_REGEX',
'PART_ALLOW_DUPLICATE_IPN',
'PART_ALLOW_EDIT_IPN',
'PART_ALLOW_DELETE_FROM_ASSEMBLY',
'PART_ENABLE_REVISION',
'PART_REVISION_ASSEMBLY_ONLY',
'PART_NAME_FORMAT',
'PART_SHOW_RELATED',
'PART_CREATE_INITIAL',
'PART_CREATE_SUPPLIER',

View File

@@ -8,6 +8,7 @@ import {
HoverCard,
Loader,
type MantineColor,
Paper,
Skeleton,
Stack,
Text
@@ -325,55 +326,24 @@ export default function PartDetail() {
refetchOnMount: true
});
// Fetch information on part revision
const revisionsEnabled = useMemo(
() => globalSettings.isSet('PART_ENABLE_REVISION'),
[globalSettings]
);
// Fetch information on parts which are revisions of *this* part
const partRevisionQuery = useQuery({
refetchOnMount: true,
queryKey: [
'part_revisions',
part.pk,
part.revision_of,
part.revision_count
],
queryFn: async () => {
if (!part.revision_of && !part.revision_count) {
return [];
}
const revisions = [];
// First, fetch information for the top-level part
if (part.revision_of) {
await api
.get(apiUrl(ApiEndpoints.part_list, part.revision_of))
.then((response) => {
revisions.push(response.data);
});
} else {
revisions.push(part);
}
const url = apiUrl(ApiEndpoints.part_list);
await api
.get(url, {
enabled: revisionsEnabled && !!part && !!part.revision_count,
queryKey: ['part_revisions', part.pk, part.revision_count],
queryFn: async () =>
api
.get(apiUrl(ApiEndpoints.part_list), {
params: {
revision_of: part.revision_of || part.pk
revision_of: part.pk
}
})
.then((response) => {
switch (response.status) {
case 200:
response.data.forEach((r: any) => {
revisions.push(r);
});
break;
default:
break;
}
});
return revisions;
}
.then((response) => response.data)
});
const partRevisionOptions: any[] = useMemo(() => {
@@ -393,26 +363,14 @@ export default function PartDetail() {
};
});
// Add this part if not already available
if (!options.find((o) => o.value == part.pk)) {
options.push({
value: part.pk,
label: part.full_name,
part: part
});
}
return options.sort((a, b) => {
return `${a.part.revision}`.localeCompare(b.part.revision);
});
}, [part, partRevisionQuery.isFetching, partRevisionQuery.data]);
const enableRevisionSelection: boolean = useMemo(() => {
return (
partRevisionOptions.length > 0 &&
globalSettings.isSet('PART_ENABLE_REVISION')
);
}, [partRevisionOptions, globalSettings]);
return partRevisionOptions.length > 0 && revisionsEnabled;
}, [partRevisionOptions, revisionsEnabled]);
const detailsPanel = useMemo(() => {
if (instanceQuery.isFetching) {
@@ -482,6 +440,7 @@ export default function PartDetail() {
name: 'variant_of',
label: t`Variant of`,
model: ModelType.part,
model_field: 'full_name',
hidden: !part.variant_of
},
{
@@ -489,6 +448,7 @@ export default function PartDetail() {
name: 'revision_of',
label: t`Revision of`,
model: ModelType.part,
model_field: 'full_name',
hidden: !part.revision_of
},
{
@@ -763,10 +723,17 @@ export default function PartDetail() {
</Grid.Col>
</Grid>
{enableRevisionSelection && (
<Stack gap='xs'>
<Text>{t`Select Part Revision`}</Text>
<RevisionSelector part={part} options={partRevisionOptions} />
</Stack>
<Paper p='sm' withBorder>
<Stack gap='xs'>
<Group gap='xs'>
<ActionIcon variant='transparent'>
<IconVersions />
</ActionIcon>
<Text>{t`Select Part Revision`}</Text>
</Group>
<RevisionSelector part={part} options={partRevisionOptions} />
</Stack>
</Paper>
)}
</Stack>
<DetailsTable fields={tr} item={data} />

View File

@@ -745,13 +745,12 @@ test('Parts - Parameter Filtering', async ({ browser }) => {
await clearTableFilters(page);
// All parts should be available (no filters applied)
await page.getByText(/\/ 42\d/).waitFor();
await page.getByText(/\/ 43\d/).waitFor();
const clearParamFilter = async (name: string) => {
await clickOnParamFilter(page, name);
await page.getByLabel(`clear-filter-${name}`).waitFor();
await page.getByLabel(`clear-filter-${name}`).click();
// await page.getByLabel(`clear-filter-${name}`).click();
};
// Let's filter by color
@@ -764,7 +763,7 @@ test('Parts - Parameter Filtering', async ({ browser }) => {
// Reset the filter
await clearParamFilter('Color');
await page.getByText(/\/ 42\d/).waitFor();
await page.getByText(/\/ 43\d/).waitFor();
});
test('Parts - Test Results', async ({ browser }) => {
@@ -804,20 +803,26 @@ test('Parts - 404', async ({ browser }) => {
await page.evaluate(() => console.clear());
});
test('Parts - Revision', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/906/details' });
test('Parts - Revisions', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/917/details' });
await page.getByText('Revision of').waitFor();
await page.getByText('ENCAB | Encabulator | C').first().waitFor();
await page.getByText('Select Part Revision').waitFor();
// Link to the "revision_of" part
await page.getByRole('cell', { name: 'ENCAB | Encabulator | B' }).waitFor();
// Select a revision
await page.getByText('ENCAB | Encabulator | CNo').click();
await page
.getByText('Green Round Table (revision B) | B', { exact: true })
.click();
await page
.getByRole('option', { name: 'Thumbnail Green Round Table No stock' })
.getByRole('option', {
name: 'Thumbnail ENCAB | Encabulator | C4 No stock'
})
.click();
await page.waitForURL('**/web/part/101/**');
await page.getByText('Select Part Revision').waitFor();
await page.waitForURL('**/web/part/920/**');
await page.getByText('Part: ENCAB | Encabulator | C4').first().waitFor();
await page.getByRole('link', { name: 'ENCAB | Encabulator | C' }).waitFor();
});
test('Parts - Bulk Edit', async ({ browser }) => {