From 9965ebcfa11e347fb803e4705fbedcaa2bb92143 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 10 Apr 2026 09:22:12 +1000 Subject: [PATCH] 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 --- docs/docs/app/settings.md | 2 +- docs/docs/concepts/parameters.md | 14 +-- docs/docs/concepts/units.md | 4 +- docs/docs/part/create.md | 2 +- docs/docs/part/index.md | 2 +- .../InvenTree/InvenTree/api_version.py | 5 +- src/backend/InvenTree/common/admin.py | 18 ++++ src/backend/InvenTree/common/api.py | 8 ++ src/backend/InvenTree/common/models.py | 34 ++++++- src/frontend/lib/enums/ApiEndpoints.tsx | 2 +- src/frontend/lib/enums/ModelInformation.tsx | 7 ++ src/frontend/lib/enums/ModelType.tsx | 1 + src/frontend/lib/types/Forms.tsx | 2 + .../forms/fields/RelatedModelField.tsx | 71 +++++++++------ .../src/components/render/Generic.tsx | 24 ++++- .../src/components/render/Instance.tsx | 2 + src/frontend/src/forms/CommonForms.tsx | 90 +++++++++++++------ .../tables/general/ParameterTemplateTable.tsx | 6 +- .../tables/general/ParametricDataTable.tsx | 2 +- src/frontend/tests/pages/pui_part.spec.ts | 32 ++++++- .../tests/pages/pui_purchasing.spec.ts | 46 ++++++++++ src/frontend/tests/pui_settings.spec.ts | 35 ++++++-- 22 files changed, 325 insertions(+), 84 deletions(-) diff --git a/docs/docs/app/settings.md b/docs/docs/app/settings.md index d415dce933..5c6e7815c7 100644 --- a/docs/docs/app/settings.md +++ b/docs/docs/app/settings.md @@ -77,7 +77,7 @@ The *Part Settings* view allows you to configure various options governing what | 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 | | Stock History | Enable display of stock history in the stock detail view | | Test Results | Enable display of test results in the stock detail view | diff --git a/docs/docs/concepts/parameters.md b/docs/docs/concepts/parameters.md index ca44989dab..47e42cec97 100644 --- a/docs/docs/concepts/parameters.md +++ b/docs/docs/concepts/parameters.md @@ -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: -{{ image("concepts/parameter-tab.png", "Part Parameters Example") }} +{{ image("concepts/parameter-tab.png", "Parameters Example") }} ## Parameter Templates @@ -40,9 +40,9 @@ Parameter templates are created and edited via the [admin interface](../settings To create a template: - Navigate to the "Settings" page -- Click on the "Part Parameters" tab +- Click on the "Parameters" tab - 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. 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. -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. @@ -132,7 +132,7 @@ The in-built conversion functionality means that parameter values can be input i ### 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") }} @@ -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. -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. diff --git a/docs/docs/concepts/units.md b/docs/docs/concepts/units.md index ce71012455..9e1c1a50d7 100644 --- a/docs/docs/concepts/units.md +++ b/docs/docs/concepts/units.md @@ -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 - 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 ### Unit Conversion @@ -61,7 +61,7 @@ The [supplier part](../part/index.md/#supplier-parts) model uses real-world unit ### 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 diff --git a/docs/docs/part/create.md b/docs/docs/part/create.md index d9b984e0e6..9aaa06e55e 100644 --- a/docs/docs/part/create.md +++ b/docs/docs/part/create.md @@ -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") }} -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. diff --git a/docs/docs/part/index.md b/docs/docs/part/index.md index e4848ece1f..076da58b25 100644 --- a/docs/docs/part/index.md +++ b/docs/docs/part/index.md @@ -81,7 +81,7 @@ Parts can be locked to prevent them from being modified. This is useful for part - Locked parts cannot be deleted - 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 diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 968e6b1792..50e2ea5ca4 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # 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.""" 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 - Adds "updated" and "updated_by" fields to the LabelTemplate and ReportTemplate API endpoints diff --git a/src/backend/InvenTree/common/admin.py b/src/backend/InvenTree/common/admin.py index a5461b7d02..f9ae5b71e5 100644 --- a/src/backend/InvenTree/common/admin.py +++ b/src/backend/InvenTree/common/admin.py @@ -32,6 +32,24 @@ class ParameterAdmin(admin.ModelAdmin): 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) class AttachmentAdmin(admin.ModelAdmin): """Admin interface for Attachment objects.""" diff --git a/src/backend/InvenTree/common/api.py b/src/backend/InvenTree/common/api.py index 802081af3d..63b75ddf52 100644 --- a/src/backend/InvenTree/common/api.py +++ b/src/backend/InvenTree/common/api.py @@ -1166,6 +1166,14 @@ class EntryMixin: class SelectionEntryList(EntryMixin, ListCreateAPI): """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): """Detail view for a SelectionEntry object.""" diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py index 7829180460..2cb7e33104 100644 --- a/src/backend/InvenTree/common/models.py +++ b/src/backend/InvenTree/common/models.py @@ -2320,11 +2320,39 @@ class SelectionList(InvenTree.models.MetadataMixin, InvenTree.models.InvenTreeMo """Return the API URL associated with the SelectionList model.""" return reverse('api-selectionlist-list') - def get_choices(self): - """Return the choices for the selection list.""" - choices = self.entries.filter(active=True) + def get_choices(self, active: Optional[bool] = True): + """Return the choices for the selection list. + + 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] + 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 which represents a single entry in a SelectionList. diff --git a/src/frontend/lib/enums/ApiEndpoints.tsx b/src/frontend/lib/enums/ApiEndpoints.tsx index e7adf3f0b8..8a1a80ad73 100644 --- a/src/frontend/lib/enums/ApiEndpoints.tsx +++ b/src/frontend/lib/enums/ApiEndpoints.tsx @@ -64,7 +64,7 @@ export enum ApiEndpoints { content_type_list = 'contenttype/', icons = 'icons/', selectionlist_list = 'selection/', - selectionlist_detail = 'selection/:id/', + selectionentry_list = 'selection/:id/entry/', // Barcode API endpoints barcode = 'barcode/', diff --git a/src/frontend/lib/enums/ModelInformation.tsx b/src/frontend/lib/enums/ModelInformation.tsx index 6f93a1ebeb..6db5b5672a 100644 --- a/src/frontend/lib/enums/ModelInformation.tsx +++ b/src/frontend/lib/enums/ModelInformation.tsx @@ -287,6 +287,13 @@ export const ModelInformationDict: ModelDict = { api_endpoint: ApiEndpoints.selectionlist_list, 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: { label: () => t`Error`, label_multiple: () => t`Errors`, diff --git a/src/frontend/lib/enums/ModelType.tsx b/src/frontend/lib/enums/ModelType.tsx index 57aa1b7205..ea85b542fc 100644 --- a/src/frontend/lib/enums/ModelType.tsx +++ b/src/frontend/lib/enums/ModelType.tsx @@ -35,5 +35,6 @@ export enum ModelType { pluginconfig = 'pluginconfig', contenttype = 'contenttype', selectionlist = 'selectionlist', + selectionentry = 'selectionentry', error = 'error' } diff --git a/src/frontend/lib/types/Forms.tsx b/src/frontend/lib/types/Forms.tsx index 42ba36acfd..2b229dcb45 100644 --- a/src/frontend/lib/types/Forms.tsx +++ b/src/frontend/lib/types/Forms.tsx @@ -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 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 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 = { label?: string; @@ -126,6 +127,7 @@ export type ApiFormFieldType = { addRow?: () => any; headers?: ApiFormFieldHeader[]; depends_on?: string[]; + singleFetchFunction?: (value: any) => Promise | null; }; export type ApiFormFieldSet = Record; diff --git a/src/frontend/src/components/forms/fields/RelatedModelField.tsx b/src/frontend/src/components/forms/fields/RelatedModelField.tsx index 9155bf3c4c..df711af1c1 100644 --- a/src/frontend/src/components/forms/fields/RelatedModelField.tsx +++ b/src/frontend/src/components/forms/fields/RelatedModelField.tsx @@ -75,42 +75,55 @@ export function RelatedModelField({ const [value, setValue] = useState(''); 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 const fetchSingleField = useCallback( - (pk: number) => { - if (!definition?.api_url) { - return; - } - - 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]); - } + (pk: number | string) => { + if (definition.singleFetchFunction) { + definition.singleFetchFunction(pk)?.then((instance: any) => { + fetchSingleCallback(instance); }); + } 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.filters, + definition.singleFetchFunction, definition.onValueChange, definition.pk_field, setValue, diff --git a/src/frontend/src/components/render/Generic.tsx b/src/frontend/src/components/render/Generic.tsx index 9c08837567..643f631e25 100644 --- a/src/frontend/src/components/render/Generic.tsx +++ b/src/frontend/src/components/render/Generic.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react'; +import { Group, Text } from '@mantine/core'; import { type InstanceRenderInterface, RenderInlineModel } from './Instance'; export function RenderParameterTemplate({ @@ -8,8 +9,12 @@ export function RenderParameterTemplate({ return ( + {instance.description} + {instance.units && [{instance.units}]} + + } /> ); } @@ -66,7 +71,20 @@ export function RenderSelectionList({ instance && ( + ) + ); +} + +export function RenderSelectionEntry({ + instance +}: Readonly): ReactNode { + return ( + instance && ( + ) ); diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx index 933a2c7f52..d368f7554c 100644 --- a/src/frontend/src/components/render/Instance.tsx +++ b/src/frontend/src/components/render/Instance.tsx @@ -39,6 +39,7 @@ import { RenderParameter, RenderParameterTemplate, RenderProjectCode, + RenderSelectionEntry, RenderSelectionList } from './Generic'; import { @@ -95,6 +96,7 @@ export const RendererLookup: ModelRendererDict = { [ModelType.pluginconfig]: RenderPlugin, [ModelType.contenttype]: RenderContentType, [ModelType.selectionlist]: RenderSelectionList, + [ModelType.selectionentry]: RenderSelectionEntry, [ModelType.error]: RenderError }; diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx index 530703959b..28b1ccee5d 100644 --- a/src/frontend/src/forms/CommonForms.tsx +++ b/src/frontend/src/forms/CommonForms.tsx @@ -1,8 +1,8 @@ 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 type { ModelType } from '@lib/enums/ModelType'; +import { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; import type { ApiFormFieldSet } from '@lib/types/Forms'; import { t } from '@lingui/core/macro'; @@ -106,18 +106,45 @@ export function useParameterFields({ }): ApiFormFieldSet { const api = useApi(); + const [selectionListId, setSelectionListId] = useState(null); + // Valid field choices const [choices, setChoices] = useState([]); // Field type for "data" input - const [fieldType, setFieldType] = useState<'string' | 'boolean' | 'choice'>( - 'string' - ); + const [fieldType, setFieldType] = useState< + 'string' | 'boolean' | 'choice' | 'related field' + >('string'); + // Memoized value for the "data" field const [data, setData] = useState(''); + 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 useEffect(() => { + setSelectionListId(null); setFieldType('string'); setChoices([]); setData(''); @@ -139,6 +166,8 @@ export function useParameterFields({ enabled: true }, onValueChange: (value: any, record: any) => { + setSelectionListId(record?.selectionlist || null); + // Adjust the type of the "data" field based on the selected template if (record?.checkbox) { // This is a "checkbox" field @@ -162,36 +191,42 @@ export function useParameterFields({ setFieldType('string'); } } else if (record?.selectionlist) { - api - .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'); + setFieldType('related field'); } } }, data: { value: data, - onValueChange: (value: any) => { - setData(value); + onValueChange: (value: any, record: any) => { + 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, field_type: fieldType, choices: fieldType === 'choice' ? choices : 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) => { // Coerce boolean value into a string (required by backend) @@ -204,9 +239,10 @@ export function useParameterFields({ } return v; - } + }, + singleFetchFunction: fetchSelectionEntry }, note: {} }; - }, [data, modelType, fieldType, choices, modelId]); + }, [data, modelType, fieldType, choices, modelId, selectionListId]); } diff --git a/src/frontend/src/tables/general/ParameterTemplateTable.tsx b/src/frontend/src/tables/general/ParameterTemplateTable.tsx index fbf8103664..ee37a464b5 100644 --- a/src/frontend/src/tables/general/ParameterTemplateTable.tsx +++ b/src/frontend/src/tables/general/ParameterTemplateTable.tsx @@ -38,7 +38,11 @@ export default function ParameterTemplateTable() { model_type: {}, choices: {}, checkbox: {}, - selectionlist: {}, + selectionlist: { + filters: { + active: true + } + }, enabled: {} }; }, []); diff --git a/src/frontend/src/tables/general/ParametricDataTable.tsx b/src/frontend/src/tables/general/ParametricDataTable.tsx index ead614ba7e..0bfa12a62c 100644 --- a/src/frontend/src/tables/general/ParametricDataTable.tsx +++ b/src/frontend/src/tables/general/ParametricDataTable.tsx @@ -144,7 +144,7 @@ export default function ParametricDataTable({ refetchOnMount: true }); - /* Store filters against selected part parameters. + /* Store filters against selected parameters. * These are stored in the format: * { * parameter_1: { diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index 71db1e1649..840edcb1bf 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -667,7 +667,37 @@ test('Parts - Parameters by Category', 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 await page diff --git a/src/frontend/tests/pages/pui_purchasing.spec.ts b/src/frontend/tests/pages/pui_purchasing.spec.ts index b451de8ce1..f7490ab73b 100644 --- a/src/frontend/tests/pages/pui_purchasing.spec.ts +++ b/src/frontend/tests/pages/pui_purchasing.spec.ts @@ -82,6 +82,52 @@ test('Purchasing - Index', async ({ browser }) => { .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 }) => { const page = await doCachedLogin(browser, { url: 'purchasing/index/manufacturer-parts' diff --git a/src/frontend/tests/pui_settings.spec.ts b/src/frontend/tests/pui_settings.spec.ts index 35a0975bcd..ca9258a087 100644 --- a/src/frontend/tests/pui_settings.spec.ts +++ b/src/frontend/tests/pui_settings.spec.ts @@ -402,12 +402,12 @@ test('Settings - Admin - Parameter', async ({ browser }) => { await loadTab(page, 'Parameters', true); - await page.waitForTimeout(1000); await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); // Clean old template data if exists await page - .getByRole('cell', { name: 'my custom parameter' }) + .getByRole('cell', { name: 'my custom parameter', exact: true }) .waitFor({ timeout: 500 }) .then(async (cell) => { await page @@ -420,11 +420,14 @@ test('Settings - Admin - Parameter', async ({ browser }) => { }) .catch(() => {}); - await page.getByRole('button', { name: 'Selection Lists' }).click(); // Allow time for the table to load - await page.waitForTimeout(1000); + await page.getByRole('button', { name: 'Selection Lists' }).click(); 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 await page .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('text-field-name').fill('some list'); 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('cell', { name: 'some list' }).waitFor(); @@ -494,9 +510,18 @@ test('Settings - Admin - Parameter', async ({ browser }) => { .filter({ hasText: /^Search\.\.\.$/ }) .locator('input') .fill('my custom parameter'); + 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(); + + // Check for the expected value + await page.getByRole('cell', { name: 'HW', exact: true }).waitFor(); }); test('Settings - Admin - Unauthorized', async ({ browser }) => {