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:
@@ -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
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user