mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-14 07:18:44 +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:
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/',
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -35,5 +35,6 @@ export enum ModelType {
|
||||
pluginconfig = 'pluginconfig',
|
||||
contenttype = 'contenttype',
|
||||
selectionlist = 'selectionlist',
|
||||
selectionentry = 'selectionentry',
|
||||
error = 'error'
|
||||
}
|
||||
|
||||
@@ -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<any> | null;
|
||||
};
|
||||
|
||||
export type ApiFormFieldSet = Record<string, ApiFormFieldType>;
|
||||
|
||||
@@ -75,42 +75,55 @@ export function RelatedModelField({
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [searchText] = useDebouncedValue(value, 250);
|
||||
|
||||
// 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) => {
|
||||
// Response to fetching a single instance
|
||||
const fetchSingleCallback = useCallback(
|
||||
(instance: any) => {
|
||||
const pk_field = definition.pk_field ?? 'pk';
|
||||
if (response.data?.[pk_field]) {
|
||||
|
||||
if (instance?.[pk_field]) {
|
||||
// Convert the response into the format expected by the select field
|
||||
const value = {
|
||||
value: response.data[pk_field],
|
||||
data: response.data
|
||||
value: instance[pk_field],
|
||||
data: instance
|
||||
};
|
||||
|
||||
// Run custom callback for this field (if provided)
|
||||
if (definition.onValueChange) {
|
||||
definition.onValueChange(response.data[pk_field], response.data);
|
||||
definition.onValueChange(instance[pk_field], instance);
|
||||
}
|
||||
|
||||
setInitialData(value);
|
||||
dataRef.current = [value];
|
||||
setPk(response.data[pk_field]);
|
||||
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 | 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,
|
||||
|
||||
@@ -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 (
|
||||
<RenderInlineModel
|
||||
primary={instance.name}
|
||||
secondary={instance.description}
|
||||
suffix={instance.units}
|
||||
suffix={
|
||||
<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 && (
|
||||
<RenderInlineModel
|
||||
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}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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<number | null>(null);
|
||||
|
||||
// Valid field choices
|
||||
const [choices, setChoices] = useState<any[]>([]);
|
||||
|
||||
// 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<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
|
||||
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) => {
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,11 @@ export default function ParameterTemplateTable() {
|
||||
model_type: {},
|
||||
choices: {},
|
||||
checkbox: {},
|
||||
selectionlist: {},
|
||||
selectionlist: {
|
||||
filters: {
|
||||
active: true
|
||||
}
|
||||
},
|
||||
enabled: {}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user