2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-04-14 15:28:52 +00:00

Selection lists updates (#11705)

* Add search capability to selection list entry endpoint

* Use API lookup for selection entries

* Add renderer func

* Allow API filtering

* Fetch selectionentry data related to the selected data item

* remove now unneeded entry

* add missing modelinfo

* fix ref

* add api bump

* Provide optional single fetch function to API forms

- Useful if we need to perform a custom API call for initial data

* django-admin support for SelectionList

* Docstring improvements

* Apply 'active' filter

* Tweak api version entry

* Playwright tests

* Tweak docs wording

* Fix incorrect docstring

* Adjust playwright tests

---------

Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
Oliver
2026-04-10 09:22:12 +10:00
committed by GitHub
parent 6701f4085d
commit 9965ebcfa1
22 changed files with 325 additions and 84 deletions

View File

@@ -77,7 +77,7 @@ The *Part Settings* view allows you to configure various options governing what
| Option | Description | | Option | Description |
| --- | --- | | --- | --- |
| Parameters | Enable display of part parameters in the part detail view | | Parameters | Enable display of parameters in the part detail view |
| BOM | Enable bill of materials display in the part detail view | | BOM | Enable bill of materials display in the part detail view |
| Stock History | Enable display of stock history in the stock detail view | | Stock History | Enable display of stock history in the stock detail view |
| Test Results | Enable display of test results in the stock detail view | | Test Results | Enable display of test results in the stock detail view |

View File

@@ -15,7 +15,7 @@ Parameters can be associated with various InvenTree models.
Any model which supports parameters will have a "Parameters" tab on its detail page. This tab displays all parameters associated with that object: Any model which supports parameters will have a "Parameters" tab on its detail page. This tab displays all parameters associated with that object:
{{ image("concepts/parameter-tab.png", "Part Parameters Example") }} {{ image("concepts/parameter-tab.png", "Parameters Example") }}
## Parameter Templates ## Parameter Templates
@@ -40,9 +40,9 @@ Parameter templates are created and edited via the [admin interface](../settings
To create a template: To create a template:
- Navigate to the "Settings" page - Navigate to the "Settings" page
- Click on the "Part Parameters" tab - Click on the "Parameters" tab
- Click on the "New Parameter" button - Click on the "New Parameter" button
- Fill out the `Create Part Parameter Template` form: `Name` (required) and `Units` (optional) fields - Fill out the `Create Parameter Template` form: `Name` (required) and `Units` (optional) fields
- Click on the "Submit" button. - Click on the "Submit" button.
An existing template can be edited by clicking on the "Edit" button associated with that template: An existing template can be edited by clicking on the "Edit" button associated with that template:
@@ -53,9 +53,9 @@ An existing template can be edited by clicking on the "Edit" button associated w
After [creating a template](#create-template) or using the existing templates, you can add parameters to any part. After [creating a template](#create-template) or using the existing templates, you can add parameters to any part.
To add a parameter, navigate to a specific part detail page, click on the "Parameters" tab then click on the "New Parameters" button, the `Create Part Parameter` form will be displayed: To add a parameter, navigate to a specific part detail page, click on the "Parameters" tab then click on the "New Parameters" button, the `Create Parameter` form will be displayed:
{{ image("part/create_part_parameter.png", "Create Part Parameter Form") }} {{ image("part/create_part_parameter.png", "Create Parameter Form") }}
Select the parameter `Template` you would like to use for this parameter, fill-out the `Data` field (value of this specific parameter) and click the "Submit" button. Select the parameter `Template` you would like to use for this parameter, fill-out the `Data` field (value of this specific parameter) and click the "Submit" button.
@@ -132,7 +132,7 @@ The in-built conversion functionality means that parameter values can be input i
### Incompatible Units ### Incompatible Units
If a part parameter is created with a value which is incompatible with the units specified for the template, it will be rejected: If a parameter is created with a value which is incompatible with the units specified for the template, it will be rejected:
{{ image("part/part_invalid_units.png", "Invalid Parameter Units") }} {{ image("part/part_invalid_units.png", "Invalid Parameter Units") }}
@@ -151,4 +151,4 @@ Selection Lists can be used to add a large number of predefined values to a para
It is possible that plugins lock selection lists to ensure a known state. It is possible that plugins lock selection lists to ensure a known state.
Administration of lists can be done through the Part Parameter section in the [Admin Center](../settings/admin.md#admin-center) or via the API. Administration of lists can be done through the `Parameter` section in the [Admin Center](../settings/admin.md#admin-center) or via the API.

View File

@@ -8,7 +8,7 @@ Support for real-world "physical" units of measure is implemented using the [pin
- Ensures consistent use of real units for your inventory management - Ensures consistent use of real units for your inventory management
- Convert between compatible units of measure from suppliers - Convert between compatible units of measure from suppliers
- Enforce use of compatible units when creating part parameters - Enforce use of compatible units when creating parameters
- Enable custom units as required - Enable custom units as required
### Unit Conversion ### Unit Conversion
@@ -61,7 +61,7 @@ The [supplier part](../part/index.md/#supplier-parts) model uses real-world unit
### Parameter ### Parameter
The [parameter template](../concepts/parameters.md#parameter-templates) model can specify units of measure, and part parameters can be specified against these templates with compatible units The [parameter template](../concepts/parameters.md#parameter-templates) model can specify units of measure, and parameters can be specified against these templates with compatible units
## Custom Units ## Custom Units

View File

@@ -20,7 +20,7 @@ New parts can be created manually by selecting the *Create Part* option from the
{{ image("part/part_create_form.png", "New part form") }} {{ image("part/part_create_form.png", "New part form") }}
Fill out the required part parameters and then press *Submit* to create the new part. If there are any form errors, you must fix these before the form can be successfully submitted. Fill out the required attributes and then press *Submit* to create the new part. If there are any form errors, you must fix these before the form can be successfully submitted.
Once the form is completed, the browser window is redirected to the new part detail page. Once the form is completed, the browser window is redirected to the new part detail page.

View File

@@ -81,7 +81,7 @@ Parts can be locked to prevent them from being modified. This is useful for part
- Locked parts cannot be deleted - Locked parts cannot be deleted
- BOM items cannot be created, edited, or deleted when they are part of a locked assembly - BOM items cannot be created, edited, or deleted when they are part of a locked assembly
- Part parameters linked to a locked part cannot be created, edited or deleted - Parameters linked to a locked part cannot be created, edited or deleted
## Active Parts ## Active Parts

View File

@@ -1,11 +1,14 @@
"""InvenTree API version information.""" """InvenTree API version information."""
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 475 INVENTREE_API_VERSION = 476
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" """Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """ INVENTREE_API_TEXT = """
v476 -> 2026-04-09 : https://github.com/inventree/InvenTree/pull/11705
- Adds sorting / filtering / searching functionality to the SelectionListEntry API endpoint
v475 -> 2026-04-09 : https://github.com/inventree/InvenTree/pull/11702 v475 -> 2026-04-09 : https://github.com/inventree/InvenTree/pull/11702
- Adds "updated" and "updated_by" fields to the LabelTemplate and ReportTemplate API endpoints - Adds "updated" and "updated_by" fields to the LabelTemplate and ReportTemplate API endpoints

View File

@@ -32,6 +32,24 @@ class ParameterAdmin(admin.ModelAdmin):
search_fields = ('template__name', 'data', 'note') search_fields = ('template__name', 'data', 'note')
class SelectionListEntryInlineAdmin(admin.StackedInline):
"""Inline admin class for the SelectionListEntry model."""
model = common.models.SelectionListEntry
extra = 0
@admin.register(common.models.SelectionList)
class SelectionListAdmin(admin.ModelAdmin):
"""Admin interface for SelectionList objects."""
list_display = ('name', 'description')
search_fields = ('name', 'description')
list_filter = ('active', 'locked')
inlines = [SelectionListEntryInlineAdmin]
@admin.register(common.models.Attachment) @admin.register(common.models.Attachment)
class AttachmentAdmin(admin.ModelAdmin): class AttachmentAdmin(admin.ModelAdmin):
"""Admin interface for Attachment objects.""" """Admin interface for Attachment objects."""

View File

@@ -1166,6 +1166,14 @@ class EntryMixin:
class SelectionEntryList(EntryMixin, ListCreateAPI): class SelectionEntryList(EntryMixin, ListCreateAPI):
"""List view for SelectionEntry objects.""" """List view for SelectionEntry objects."""
filter_backends = SEARCH_ORDER_FILTER
ordering_fields = ['list', 'label', 'active']
search_fields = ['label', 'description']
filterset_fields = ['active', 'value', 'list']
class SelectionEntryDetail(EntryMixin, RetrieveUpdateDestroyAPI): class SelectionEntryDetail(EntryMixin, RetrieveUpdateDestroyAPI):
"""Detail view for a SelectionEntry object.""" """Detail view for a SelectionEntry object."""

View File

@@ -2320,11 +2320,39 @@ class SelectionList(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeMo
"""Return the API URL associated with the SelectionList model.""" """Return the API URL associated with the SelectionList model."""
return reverse('api-selectionlist-list') return reverse('api-selectionlist-list')
def get_choices(self): def get_choices(self, active: Optional[bool] = True):
"""Return the choices for the selection list.""" """Return the choices for the selection list.
choices = self.entries.filter(active=True)
Arguments:
active: If specified, filter choices by active status
Returns:
List of choice values for this selection list
"""
choices = self.entries.all()
if active is not None:
choices = choices.filter(active=active)
return [c.value for c in choices] return [c.value for c in choices]
def has_choice(self, value: str, active: Optional[bool] = None):
"""Check if the selection list has a particular choice.
Arguments:
value: The value to check for
active: If specified, filter choices by active status
Returns:
True if the choice exists in the selection list, False otherwise
"""
choices = self.entries.all()
if active is not None:
choices = choices.filter(active=active)
return choices.filter(value=value).exists()
class SelectionListEntry(models.Model): class SelectionListEntry(models.Model):
"""Class which represents a single entry in a SelectionList. """Class which represents a single entry in a SelectionList.

View File

@@ -64,7 +64,7 @@ export enum ApiEndpoints {
content_type_list = 'contenttype/', content_type_list = 'contenttype/',
icons = 'icons/', icons = 'icons/',
selectionlist_list = 'selection/', selectionlist_list = 'selection/',
selectionlist_detail = 'selection/:id/', selectionentry_list = 'selection/:id/entry/',
// Barcode API endpoints // Barcode API endpoints
barcode = 'barcode/', barcode = 'barcode/',

View File

@@ -287,6 +287,13 @@ export const ModelInformationDict: ModelDict = {
api_endpoint: ApiEndpoints.selectionlist_list, api_endpoint: ApiEndpoints.selectionlist_list,
icon: 'list_details' icon: 'list_details'
}, },
selectionentry: {
label: () => t`Selection Entry`,
label_multiple: () => t`Selection Entries`,
url_overview: '/settings/admin/part-parameters',
api_endpoint: ApiEndpoints.selectionentry_list,
icon: 'list_details'
},
error: { error: {
label: () => t`Error`, label: () => t`Error`,
label_multiple: () => t`Errors`, label_multiple: () => t`Errors`,

View File

@@ -35,5 +35,6 @@ export enum ModelType {
pluginconfig = 'pluginconfig', pluginconfig = 'pluginconfig',
contenttype = 'contenttype', contenttype = 'contenttype',
selectionlist = 'selectionlist', selectionlist = 'selectionlist',
selectionentry = 'selectionentry',
error = 'error' error = 'error'
} }

View File

@@ -68,6 +68,7 @@ export type ApiFormFieldHeader = {
* @param adjustValue : Callback function to adjust the value of the field before it is sent to the API * @param adjustValue : Callback function to adjust the value of the field before it is sent to the API
* @param addRow : Callback function to add a new row to a table field * @param addRow : Callback function to add a new row to a table field
* @param onKeyDown : Callback function to get which key was pressed in the form to handle submission on enter * @param onKeyDown : Callback function to get which key was pressed in the form to handle submission on enter
* @param singleFetchFunction : Optional function to fetch a single value for this field (used for fetching the initial value when editing an existing object)
*/ */
export type ApiFormFieldType = { export type ApiFormFieldType = {
label?: string; label?: string;
@@ -126,6 +127,7 @@ export type ApiFormFieldType = {
addRow?: () => any; addRow?: () => any;
headers?: ApiFormFieldHeader[]; headers?: ApiFormFieldHeader[];
depends_on?: string[]; depends_on?: string[];
singleFetchFunction?: (value: any) => Promise<any> | null;
}; };
export type ApiFormFieldSet = Record<string, ApiFormFieldType>; export type ApiFormFieldSet = Record<string, ApiFormFieldType>;

View File

@@ -75,42 +75,55 @@ export function RelatedModelField({
const [value, setValue] = useState<string>(''); const [value, setValue] = useState<string>('');
const [searchText] = useDebouncedValue(value, 250); const [searchText] = useDebouncedValue(value, 250);
// Response to fetching a single instance
const fetchSingleCallback = useCallback(
(instance: any) => {
const pk_field = definition.pk_field ?? 'pk';
if (instance?.[pk_field]) {
// Convert the response into the format expected by the select field
const value = {
value: instance[pk_field],
data: instance
};
// Run custom callback for this field (if provided)
if (definition.onValueChange) {
definition.onValueChange(instance[pk_field], instance);
}
setInitialData(value);
dataRef.current = [value];
setPk(instance[pk_field]);
}
},
[definition.pk_field, definition.onValueChange, setInitialData, setPk]
);
// Fetch a single field by primary key, using the provided API filters // Fetch a single field by primary key, using the provided API filters
const fetchSingleField = useCallback( const fetchSingleField = useCallback(
(pk: number) => { (pk: number | string) => {
if (!definition?.api_url) { if (definition.singleFetchFunction) {
return; definition.singleFetchFunction(pk)?.then((instance: any) => {
} fetchSingleCallback(instance);
const params = definition?.filters ?? {};
const url = `${definition.api_url}${pk}/`;
api
.get(url, {
params: params
})
.then((response) => {
const pk_field = definition.pk_field ?? 'pk';
if (response.data?.[pk_field]) {
const value = {
value: response.data[pk_field],
data: response.data
};
// Run custom callback for this field (if provided)
if (definition.onValueChange) {
definition.onValueChange(response.data[pk_field], response.data);
}
setInitialData(value);
dataRef.current = [value];
setPk(response.data[pk_field]);
}
}); });
} else if (!!definition.api_url) {
const params = definition?.filters ?? {};
const url = `${definition.api_url}${pk}/`;
api.get(url, { params: params }).then((response) => {
const instance = response.data;
fetchSingleCallback(instance);
});
} else {
console.error(
`No API URL provided for related field ${fieldName}, cannot fetch data`
);
}
}, },
[ [
definition.api_url, definition.api_url,
definition.filters, definition.filters,
definition.singleFetchFunction,
definition.onValueChange, definition.onValueChange,
definition.pk_field, definition.pk_field,
setValue, setValue,

View File

@@ -1,5 +1,6 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Group, Text } from '@mantine/core';
import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance';
export function RenderParameterTemplate({ export function RenderParameterTemplate({
@@ -8,8 +9,12 @@ export function RenderParameterTemplate({
return ( return (
<RenderInlineModel <RenderInlineModel
primary={instance.name} primary={instance.name}
secondary={instance.description} suffix={
suffix={instance.units} <Group gap='xs'>
<Text size='xs'>{instance.description}</Text>
{instance.units && <Text size='xs'>[{instance.units}]</Text>}
</Group>
}
/> />
); );
} }
@@ -66,7 +71,20 @@ export function RenderSelectionList({
instance && ( instance && (
<RenderInlineModel <RenderInlineModel
primary={instance.name} primary={instance.name}
secondary={instance.description} suffix={instance.description}
/>
)
);
}
export function RenderSelectionEntry({
instance
}: Readonly<InstanceRenderInterface>): ReactNode {
return (
instance && (
<RenderInlineModel
primary={instance.label}
suffix={instance.description}
/> />
) )
); );

View File

@@ -39,6 +39,7 @@ import {
RenderParameter, RenderParameter,
RenderParameterTemplate, RenderParameterTemplate,
RenderProjectCode, RenderProjectCode,
RenderSelectionEntry,
RenderSelectionList RenderSelectionList
} from './Generic'; } from './Generic';
import { import {
@@ -95,6 +96,7 @@ export const RendererLookup: ModelRendererDict = {
[ModelType.pluginconfig]: RenderPlugin, [ModelType.pluginconfig]: RenderPlugin,
[ModelType.contenttype]: RenderContentType, [ModelType.contenttype]: RenderContentType,
[ModelType.selectionlist]: RenderSelectionList, [ModelType.selectionlist]: RenderSelectionList,
[ModelType.selectionentry]: RenderSelectionEntry,
[ModelType.error]: RenderError [ModelType.error]: RenderError
}; };

View File

@@ -1,8 +1,8 @@
import { IconUsers } from '@tabler/icons-react'; import { IconUsers } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import type { ModelType } from '@lib/enums/ModelType'; import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api'; import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldSet } from '@lib/types/Forms'; import type { ApiFormFieldSet } from '@lib/types/Forms';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
@@ -106,18 +106,45 @@ export function useParameterFields({
}): ApiFormFieldSet { }): ApiFormFieldSet {
const api = useApi(); const api = useApi();
const [selectionListId, setSelectionListId] = useState<number | null>(null);
// Valid field choices // Valid field choices
const [choices, setChoices] = useState<any[]>([]); const [choices, setChoices] = useState<any[]>([]);
// Field type for "data" input // Field type for "data" input
const [fieldType, setFieldType] = useState<'string' | 'boolean' | 'choice'>( const [fieldType, setFieldType] = useState<
'string' 'string' | 'boolean' | 'choice' | 'related field'
); >('string');
// Memoized value for the "data" field
const [data, setData] = useState<string>(''); const [data, setData] = useState<string>('');
const fetchSelectionEntry = useCallback(
(value: any) => {
if (!value || !selectionListId) {
return null;
}
return api
.get(apiUrl(ApiEndpoints.selectionentry_list, selectionListId), {
params: {
value: value
}
})
.then((response) => {
if (response.data && response.data.length == 1) {
return response.data[0];
} else {
return null;
}
});
},
[selectionListId]
);
// Reset the field type and choices when the model changes // Reset the field type and choices when the model changes
useEffect(() => { useEffect(() => {
setSelectionListId(null);
setFieldType('string'); setFieldType('string');
setChoices([]); setChoices([]);
setData(''); setData('');
@@ -139,6 +166,8 @@ export function useParameterFields({
enabled: true enabled: true
}, },
onValueChange: (value: any, record: any) => { onValueChange: (value: any, record: any) => {
setSelectionListId(record?.selectionlist || null);
// Adjust the type of the "data" field based on the selected template // Adjust the type of the "data" field based on the selected template
if (record?.checkbox) { if (record?.checkbox) {
// This is a "checkbox" field // This is a "checkbox" field
@@ -162,36 +191,42 @@ export function useParameterFields({
setFieldType('string'); setFieldType('string');
} }
} else if (record?.selectionlist) { } else if (record?.selectionlist) {
api setFieldType('related field');
.get(
apiUrl(ApiEndpoints.selectionlist_detail, record.selectionlist)
)
.then((res) => {
setChoices(
res.data.choices.map((item: any) => {
return {
value: item.value,
display_name: item.label
};
})
);
setFieldType('choice');
});
} else {
setChoices([]);
setFieldType('string');
} }
} }
}, },
data: { data: {
value: data, value: data,
onValueChange: (value: any) => { onValueChange: (value: any, record: any) => {
setData(value); if (fieldType === 'related field' && selectionListId) {
// For related fields, we need to store the selected primary key value (not the string representation)
setData(record?.value ?? value);
} else {
setData(value);
}
}, },
type: fieldType, type: fieldType,
field_type: fieldType, field_type: fieldType,
choices: fieldType === 'choice' ? choices : undefined, choices: fieldType === 'choice' ? choices : undefined,
default: fieldType === 'boolean' ? false : undefined, default: fieldType === 'boolean' ? false : undefined,
pk_field:
fieldType === 'related field' && selectionListId
? 'value'
: undefined,
model:
fieldType === 'related field' && selectionListId
? ModelType.selectionentry
: undefined,
api_url:
fieldType === 'related field' && selectionListId
? apiUrl(ApiEndpoints.selectionentry_list, selectionListId)
: undefined,
filters:
fieldType === 'related field'
? {
active: true
}
: undefined,
adjustValue: (value: any) => { adjustValue: (value: any) => {
// Coerce boolean value into a string (required by backend) // Coerce boolean value into a string (required by backend)
@@ -204,9 +239,10 @@ export function useParameterFields({
} }
return v; return v;
} },
singleFetchFunction: fetchSelectionEntry
}, },
note: {} note: {}
}; };
}, [data, modelType, fieldType, choices, modelId]); }, [data, modelType, fieldType, choices, modelId, selectionListId]);
} }

View File

@@ -38,7 +38,11 @@ export default function ParameterTemplateTable() {
model_type: {}, model_type: {},
choices: {}, choices: {},
checkbox: {}, checkbox: {},
selectionlist: {}, selectionlist: {
filters: {
active: true
}
},
enabled: {} enabled: {}
}; };
}, []); }, []);

View File

@@ -144,7 +144,7 @@ export default function ParametricDataTable({
refetchOnMount: true refetchOnMount: true
}); });
/* Store filters against selected part parameters. /* Store filters against selected parameters.
* These are stored in the format: * These are stored in the format:
* { * {
* parameter_1: { * parameter_1: {

View File

@@ -667,7 +667,37 @@ test('Parts - Parameters by Category', async ({ browser }) => {
}); });
test('Parts - Parameters', async ({ browser }) => { test('Parts - Parameters', async ({ browser }) => {
const page = await doCachedLogin(browser, { url: 'part/69/parameters' }); const page = await doCachedLogin(browser, { url: 'part/915/parameters' });
// Edit parameter defined with a SelectionListEntry
const animalCell = await page.getByRole('cell', {
name: 'Animal',
exact: true
});
await clickOnRowMenu(animalCell);
await page.getByRole('menuitem', { name: 'Edit' }).click();
// Check the data field, which should be populated with the options defined in the "Animal" parameter template
// If both values are present, we know that the correct SelectionListEntry has been loaded
await page
.getByText('Data *Parameter')
.getByText(/Armadillo/i)
.waitFor();
await page
.getByText('Data *Parameter')
.getByText(/A mammal known for/i)
.waitFor();
// Check for other values
await page
.getByRole('combobox', { name: 'related-field-data' })
.fill('horse');
await page.getByText('The offspring of a male donkey').waitFor();
await page.getByText('A small marine fish with a head').waitFor();
await page.getByText('Zebra').click();
// Close the edit dialog
await page.getByRole('button', { name: 'Cancel' }).click();
// check that "is polarized" parameter is not already present - if it is, delete it before proceeding with the rest of the test // check that "is polarized" parameter is not already present - if it is, delete it before proceeding with the rest of the test
await page await page

View File

@@ -82,6 +82,52 @@ test('Purchasing - Index', async ({ browser }) => {
.waitFor(); .waitFor();
}); });
test('Purchasing - Parameters', async ({ browser }) => {
const page = await doCachedLogin(browser, {
url: 'purchasing/purchase-order/11/parameters'
});
// Create a new parameter against this purchase order
// We will use a "SelectionList" to choose the value here
await page
.getByRole('button', { name: 'action-menu-add-parameters' })
.click();
await page
.getByRole('menuitem', {
name: 'action-menu-add-parameters-create-parameter'
})
.click();
// Select the template
await page
.getByRole('combobox', { name: 'related-field-template' })
.fill('animal');
await page.getByRole('option', { name: 'Animal Select an animal' }).click();
// Select an animal
await page.getByRole('combobox', { name: 'related-field-data' }).fill('cat');
await page.getByRole('option', { name: 'Caracal' }).click();
// Add a note and submit the form
await page
.getByRole('textbox', { name: 'text-field-note' })
.fill('The caracal is an interesting beast');
await page.getByRole('button', { name: 'Submit' }).click();
// Let's edit this parameter to ensure the "edit" form works as expected
await clickOnRowMenu(await page.getByRole('cell', { name: 'Caracal' }));
await page.getByRole('menuitem', { name: 'Edit' }).click();
await page.getByRole('combobox', { name: 'related-field-data' }).fill('ox');
await page.getByRole('option', { name: 'Fennec Fox' }).click();
await page.getByRole('button', { name: 'Submit' }).click();
// Finally, delete the parameter
await clickOnRowMenu(await page.getByRole('cell', { name: 'Fennec Fox' }));
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Delete', exact: true }).click();
});
test('Purchasing - Manufacturer Parts', async ({ browser }) => { test('Purchasing - Manufacturer Parts', async ({ browser }) => {
const page = await doCachedLogin(browser, { const page = await doCachedLogin(browser, {
url: 'purchasing/index/manufacturer-parts' url: 'purchasing/index/manufacturer-parts'

View File

@@ -402,12 +402,12 @@ test('Settings - Admin - Parameter', async ({ browser }) => {
await loadTab(page, 'Parameters', true); await loadTab(page, 'Parameters', true);
await page.waitForTimeout(1000);
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Clean old template data if exists // Clean old template data if exists
await page await page
.getByRole('cell', { name: 'my custom parameter' }) .getByRole('cell', { name: 'my custom parameter', exact: true })
.waitFor({ timeout: 500 }) .waitFor({ timeout: 500 })
.then(async (cell) => { .then(async (cell) => {
await page await page
@@ -420,11 +420,14 @@ test('Settings - Admin - Parameter', async ({ browser }) => {
}) })
.catch(() => {}); .catch(() => {});
await page.getByRole('button', { name: 'Selection Lists' }).click();
// Allow time for the table to load // Allow time for the table to load
await page.waitForTimeout(1000); await page.getByRole('button', { name: 'Selection Lists' }).click();
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Check for expected entry
await page.getByRole('cell', { name: 'Animals', exact: true }).waitFor();
await page.getByText('Various animals and descriptions thereof').waitFor();
// Clean old list data if exists // Clean old list data if exists
await page await page
.getByRole('cell', { name: 'some list' }) .getByRole('cell', { name: 'some list' })
@@ -445,6 +448,19 @@ test('Settings - Admin - Parameter', async ({ browser }) => {
await page.getByLabel('action-button-add-selection-').click(); await page.getByLabel('action-button-add-selection-').click();
await page.getByLabel('text-field-name').fill('some list'); await page.getByLabel('text-field-name').fill('some list');
await page.getByLabel('text-field-description').fill('Listdescription'); await page.getByLabel('text-field-description').fill('Listdescription');
// Add an entry to the selection list
await page.getByRole('button', { name: 'action-button-add-new-row' }).click();
await page.getByRole('textbox', { name: 'text-field-value' }).fill('HW');
await page
.getByRole('textbox', { name: 'text-field-label' })
.fill('Hardwood');
await page
.getByRole('row', { name: 'boolean-field-active action-' })
.getByLabel('text-field-description')
.fill('Hardwood materials');
await page.getByRole('cell', { name: 'boolean-field-active' }).click();
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('cell', { name: 'some list' }).waitFor(); await page.getByRole('cell', { name: 'some list' }).waitFor();
@@ -494,9 +510,18 @@ test('Settings - Admin - Parameter', async ({ browser }) => {
.filter({ hasText: /^Search\.\.\.$/ }) .filter({ hasText: /^Search\.\.\.$/ })
.locator('input') .locator('input')
.fill('my custom parameter'); .fill('my custom parameter');
await page.getByRole('option', { name: 'my custom parameter' }).click(); await page.getByRole('option', { name: 'my custom parameter' }).click();
await page.getByLabel('choice-field-data').fill('2');
// Finally, select value from the SelectionList data
await page.getByRole('combobox', { name: 'related-field-data' }).fill('wood');
await page
.getByRole('option', { name: 'Hardwood Hardwood materials' })
.click();
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
// Check for the expected value
await page.getByRole('cell', { name: 'HW', exact: true }).waitFor();
}); });
test('Settings - Admin - Unauthorized', async ({ browser }) => { test('Settings - Admin - Unauthorized', async ({ browser }) => {