mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-04 14:10:52 +00:00
SelectionList Updates (#12139)
* Adjust panel layout * edit list on click * Optionally fetch selection list items * Display in DetailDrawer * Fix component locations * Refactor entry table * Add new entry * Disable if locked * Only validate choices if provided via API * Mark "choices" as read-only * Prevent delete of locked items * Add more API unit tests * Bump API version * Adjust unit tests * Default include choices * Updated playwright test * Improve test robustness
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 503
|
INVENTREE_API_VERSION = 504
|
||||||
"""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 = """
|
||||||
|
|
||||||
|
v504 -> 2026-06-13 : https://github.com/inventree/InvenTree/pull/12139
|
||||||
|
- Adjustments to the SelectionList and SelectionListEntry API endpoints to support more efficient queries and data retrieval
|
||||||
|
|
||||||
v503 -> 2026-06-11 : https://github.com/inventree/InvenTree/pull/12155
|
v503 -> 2026-06-11 : https://github.com/inventree/InvenTree/pull/12155
|
||||||
- Adds additional filtering and sorting options to the LabelTemplate API endpoint
|
- Adds additional filtering and sorting options to the LabelTemplate API endpoint
|
||||||
- Adds additional filtering and sorting options to the ReportTemplate API endpoint
|
- Adds additional filtering and sorting options to the ReportTemplate API endpoint
|
||||||
|
|||||||
@@ -1207,29 +1207,25 @@ class IconList(ListAPI):
|
|||||||
return list(get_icon_packs().values())
|
return list(get_icon_packs().values())
|
||||||
|
|
||||||
|
|
||||||
class SelectionListList(ListCreateAPI):
|
class SelectionListMixin(OutputOptionsMixin):
|
||||||
|
"""Mixin for SelectionList views."""
|
||||||
|
|
||||||
|
queryset = common.models.SelectionList.objects.all()
|
||||||
|
serializer_class = common.serializers.SelectionListSerializer
|
||||||
|
permission_classes = [IsAuthenticatedOrReadScope]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Override the queryset method to include entry count."""
|
||||||
|
return self.serializer_class.annotate_queryset(super().get_queryset())
|
||||||
|
|
||||||
|
|
||||||
|
class SelectionListList(SelectionListMixin, ListCreateAPI):
|
||||||
"""List view for SelectionList objects."""
|
"""List view for SelectionList objects."""
|
||||||
|
|
||||||
queryset = common.models.SelectionList.objects.all()
|
|
||||||
serializer_class = common.serializers.SelectionListSerializer
|
|
||||||
permission_classes = [IsAuthenticatedOrReadScope]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
class SelectionListDetail(SelectionListMixin, RetrieveUpdateDestroyAPI):
|
||||||
"""Override the queryset method to include entry count."""
|
|
||||||
return self.serializer_class.annotate_queryset(super().get_queryset())
|
|
||||||
|
|
||||||
|
|
||||||
class SelectionListDetail(RetrieveUpdateDestroyAPI):
|
|
||||||
"""Detail view for a SelectionList object."""
|
"""Detail view for a SelectionList object."""
|
||||||
|
|
||||||
queryset = common.models.SelectionList.objects.all()
|
|
||||||
serializer_class = common.serializers.SelectionListSerializer
|
|
||||||
permission_classes = [IsAuthenticatedOrReadScope]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
"""Override the queryset method to include entry count."""
|
|
||||||
return self.serializer_class.annotate_queryset(super().get_queryset())
|
|
||||||
|
|
||||||
|
|
||||||
class EntryMixin:
|
class EntryMixin:
|
||||||
"""Mixin for SelectionEntry views."""
|
"""Mixin for SelectionEntry views."""
|
||||||
@@ -1246,6 +1242,12 @@ class EntryMixin:
|
|||||||
queryset = queryset.prefetch_related('list')
|
queryset = queryset.prefetch_related('list')
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def perform_destroy(self, instance):
|
||||||
|
"""Prevent deletion of entries belonging to a locked selection list."""
|
||||||
|
if instance.list.locked:
|
||||||
|
raise PermissionDenied(_('Selection list is locked'))
|
||||||
|
super().perform_destroy(instance)
|
||||||
|
|
||||||
|
|
||||||
class SelectionEntryList(EntryMixin, ListCreateAPI):
|
class SelectionEntryList(EntryMixin, ListCreateAPI):
|
||||||
"""List view for SelectionEntry objects."""
|
"""List view for SelectionEntry objects."""
|
||||||
|
|||||||
@@ -1004,15 +1004,17 @@ class SelectionEntrySerializer(InvenTreeModelSerializer):
|
|||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
"""Ensure that the selection list is not locked."""
|
"""Ensure that the selection list is not locked."""
|
||||||
ret = super().validate(attrs)
|
ret = super().validate(attrs)
|
||||||
if self.instance and self.instance.list.locked:
|
list_obj = attrs.get('list') or (self.instance and self.instance.list)
|
||||||
|
if list_obj and list_obj.locked:
|
||||||
raise serializers.ValidationError({'list': _('Selection list is locked')})
|
raise serializers.ValidationError({'list': _('Selection list is locked')})
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class SelectionListSerializer(InvenTreeModelSerializer):
|
class SelectionListSerializer(FilterableSerializerMixin, InvenTreeModelSerializer):
|
||||||
"""Serializer for a selection list."""
|
"""Serializer for a selection list."""
|
||||||
|
|
||||||
_choices_validated: dict = {}
|
_choices_validated: dict = {}
|
||||||
|
_choices_provided: bool = False
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta options for SelectionListSerializer."""
|
"""Meta options for SelectionListSerializer."""
|
||||||
@@ -1029,81 +1031,25 @@ class SelectionListSerializer(InvenTreeModelSerializer):
|
|||||||
'default',
|
'default',
|
||||||
'created',
|
'created',
|
||||||
'last_updated',
|
'last_updated',
|
||||||
'choices',
|
|
||||||
'entry_count',
|
'entry_count',
|
||||||
|
'choices',
|
||||||
]
|
]
|
||||||
|
|
||||||
default = SelectionEntrySerializer(read_only=True, allow_null=True, many=False)
|
default = SelectionEntrySerializer(read_only=True, allow_null=True, many=False)
|
||||||
choices = SelectionEntrySerializer(source='entries', many=True, required=False)
|
|
||||||
entry_count = serializers.IntegerField(read_only=True)
|
entry_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
choices = OptionalField(
|
||||||
|
serializer_class=SelectionEntrySerializer,
|
||||||
|
serializer_kwargs={'source': 'entries', 'many': True, 'read_only': True},
|
||||||
|
prefetch_fields=['entries'],
|
||||||
|
default_include=True,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Add count of entries for each selection list."""
|
"""Add count of entries for each selection list."""
|
||||||
return queryset.annotate(entry_count=Count('entries'))
|
return queryset.annotate(entry_count=Count('entries'))
|
||||||
|
|
||||||
def is_valid(self, *, raise_exception=False):
|
|
||||||
"""Validate the selection list. Choices are validated separately."""
|
|
||||||
choices = (
|
|
||||||
self.initial_data.pop('choices')
|
|
||||||
if self.initial_data.get('choices') is not None
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate the choices
|
|
||||||
_choices_validated = []
|
|
||||||
db_entries = (
|
|
||||||
{a.id: a for a in self.instance.entries.all()} if self.instance else {}
|
|
||||||
)
|
|
||||||
|
|
||||||
for choice in choices:
|
|
||||||
current_inst = db_entries.get(choice.get('id'))
|
|
||||||
serializer = SelectionEntrySerializer(
|
|
||||||
instance=current_inst,
|
|
||||||
data={'list': current_inst.list.pk if current_inst else None, **choice},
|
|
||||||
)
|
|
||||||
serializer.is_valid(raise_exception=raise_exception)
|
|
||||||
_choices_validated.append({
|
|
||||||
**serializer.validated_data,
|
|
||||||
'id': choice.get('id'),
|
|
||||||
})
|
|
||||||
self._choices_validated = _choices_validated
|
|
||||||
|
|
||||||
return super().is_valid(raise_exception=raise_exception)
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
"""Create a new selection list. Save the choices separately."""
|
|
||||||
list_entry = common_models.SelectionList.objects.create(**validated_data)
|
|
||||||
for choice_data in self._choices_validated:
|
|
||||||
common_models.SelectionListEntry.objects.create(**{
|
|
||||||
**choice_data,
|
|
||||||
'list': list_entry,
|
|
||||||
})
|
|
||||||
return list_entry
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
"""Update an existing selection list. Save the choices separately."""
|
|
||||||
inst_mapping = {inst.id: inst for inst in instance.entries.all()}
|
|
||||||
existing_ids = {a.get('id') for a in self._choices_validated}
|
|
||||||
|
|
||||||
# Perform creations and updates.
|
|
||||||
ret = []
|
|
||||||
for data in self._choices_validated:
|
|
||||||
list_inst = data.get('list', None)
|
|
||||||
inst = inst_mapping.get(data.get('id'))
|
|
||||||
if inst is None:
|
|
||||||
if list_inst is None:
|
|
||||||
data['list'] = instance
|
|
||||||
ret.append(SelectionEntrySerializer().create(data))
|
|
||||||
else:
|
|
||||||
ret.append(SelectionEntrySerializer().update(inst, data))
|
|
||||||
|
|
||||||
# Perform deletions.
|
|
||||||
for entry_id in inst_mapping.keys() - existing_ids:
|
|
||||||
inst_mapping[entry_id].delete()
|
|
||||||
|
|
||||||
return super().update(instance, validated_data)
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
"""Ensure that the selection list is not locked."""
|
"""Ensure that the selection list is not locked."""
|
||||||
ret = super().validate(attrs)
|
ret = super().validate(attrs)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from PIL import Image
|
|||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
from common.models import SelectionList, SelectionListEntry
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
|
|
||||||
|
|
||||||
@@ -1167,3 +1168,69 @@ class TagAPITests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.assertIn(self.part_a.pk, pks)
|
self.assertIn(self.part_a.pk, pks)
|
||||||
self.assertNotIn(self.part_b.pk, pks)
|
self.assertNotIn(self.part_b.pk, pks)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectionListLockedTest(InvenTreeAPITestCase):
|
||||||
|
"""Tests that a locked SelectionList rejects all entry mutations."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Create a locked SelectionList with one entry."""
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.sel_list = SelectionList.objects.create(name='Locked List', locked=True)
|
||||||
|
self.entry = SelectionListEntry.objects.create(
|
||||||
|
list=self.sel_list, value='v1', label='Entry 1'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.list_url = reverse(
|
||||||
|
'api-selectionlist-detail', kwargs={'pk': self.sel_list.pk}
|
||||||
|
)
|
||||||
|
self.entry_list_url = reverse(
|
||||||
|
'api-selectionlistentry-list', kwargs={'pk': self.sel_list.pk}
|
||||||
|
)
|
||||||
|
self.entry_detail_url = reverse(
|
||||||
|
'api-selectionlistentry-detail',
|
||||||
|
kwargs={'pk': self.sel_list.pk, 'entrypk': self.entry.pk},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_create_entry_locked(self):
|
||||||
|
"""POST a new entry to a locked list should be rejected."""
|
||||||
|
response = self.post(
|
||||||
|
self.entry_list_url,
|
||||||
|
{'list': self.sel_list.pk, 'value': 'v2', 'label': 'Entry 2'},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
self.assertIn('list', response.data)
|
||||||
|
self.assertIn('locked', str(response.data['list']).lower())
|
||||||
|
|
||||||
|
def test_update_entry_locked(self):
|
||||||
|
"""PATCH an entry on a locked list should be rejected."""
|
||||||
|
response = self.patch(
|
||||||
|
self.entry_detail_url, {'label': 'Changed'}, expected_code=400
|
||||||
|
)
|
||||||
|
self.assertIn('list', response.data)
|
||||||
|
self.assertIn('locked', str(response.data['list']).lower())
|
||||||
|
|
||||||
|
def test_delete_entry_locked(self):
|
||||||
|
"""DELETE an entry from a locked list should be rejected."""
|
||||||
|
self.delete(self.entry_detail_url, expected_code=403)
|
||||||
|
self.assertTrue(SelectionListEntry.objects.filter(pk=self.entry.pk).exists())
|
||||||
|
|
||||||
|
def test_patch_list_with_choices_locked(self):
|
||||||
|
"""PATCH the list with a choices payload should be rejected when locked."""
|
||||||
|
response = self.patch(
|
||||||
|
self.list_url,
|
||||||
|
{'choices': [{'value': 'v2', 'label': 'New'}]},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
self.assertIn('locked', response.data)
|
||||||
|
|
||||||
|
def test_patch_list_without_choices_preserves_entries(self):
|
||||||
|
"""PATCH the list without choices should not touch entries (even when unlocked)."""
|
||||||
|
self.sel_list.locked = False
|
||||||
|
self.sel_list.save()
|
||||||
|
|
||||||
|
self.patch(self.list_url, {'name': 'Renamed List'}, expected_code=200)
|
||||||
|
|
||||||
|
# Entry must still exist — omitting choices must not delete entries
|
||||||
|
self.assertTrue(SelectionListEntry.objects.filter(pk=self.entry.pk).exists())
|
||||||
|
|||||||
@@ -2103,43 +2103,58 @@ class SelectionListTest(InvenTreeAPITestCase):
|
|||||||
# Test adding a new list via the API
|
# Test adding a new list via the API
|
||||||
response = self.post(
|
response = self.post(
|
||||||
reverse('api-selectionlist-list'),
|
reverse('api-selectionlist-list'),
|
||||||
{
|
{'name': 'New List', 'active': True},
|
||||||
'name': 'New List',
|
|
||||||
'active': True,
|
|
||||||
'choices': [{'value': '1', 'label': 'Test Entry'}],
|
|
||||||
},
|
|
||||||
expected_code=201,
|
expected_code=201,
|
||||||
)
|
)
|
||||||
list_pk = response.data['pk']
|
list_pk = response.data['pk']
|
||||||
self.assertEqual(response.data['name'], 'New List')
|
self.assertEqual(response.data['name'], 'New List')
|
||||||
self.assertTrue(response.data['active'])
|
self.assertTrue(response.data['active'])
|
||||||
|
|
||||||
|
entry_list_url = reverse('api-selectionlistentry-list', kwargs={'pk': list_pk})
|
||||||
|
|
||||||
|
# Add an entry via the entry API
|
||||||
|
response = self.post(
|
||||||
|
entry_list_url,
|
||||||
|
{'list': list_pk, 'value': '1', 'label': 'Test Entry'},
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
entry_pk = response.data['id']
|
||||||
|
self.assertEqual(response.data['value'], '1')
|
||||||
|
self.assertEqual(response.data['label'], 'Test Entry')
|
||||||
|
|
||||||
|
# Verify the entry appears in the list's choices
|
||||||
|
response = self.get(
|
||||||
|
reverse('api-selectionlist-detail', kwargs={'pk': list_pk}),
|
||||||
|
data={'choices': True},
|
||||||
|
expected_code=200,
|
||||||
|
)
|
||||||
self.assertEqual(len(response.data['choices']), 1)
|
self.assertEqual(len(response.data['choices']), 1)
|
||||||
self.assertEqual(response.data['choices'][0]['value'], '1')
|
self.assertEqual(response.data['choices'][0]['value'], '1')
|
||||||
|
|
||||||
# Test editing the list choices via the API (remove and add in same call)
|
# Edit the entry via the entry detail API
|
||||||
response = self.patch(
|
entry_url = reverse(
|
||||||
reverse('api-selectionlist-detail', kwargs={'pk': list_pk}),
|
'api-selectionlistentry-detail', kwargs={'pk': list_pk, 'entrypk': entry_pk}
|
||||||
{'choices': [{'value': '2', 'label': 'New Label'}]},
|
|
||||||
expected_code=200,
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.data['name'], 'New List')
|
response = self.patch(entry_url, {'label': 'Updated Label'}, expected_code=200)
|
||||||
self.assertTrue(response.data['active'])
|
self.assertEqual(response.data['value'], '1')
|
||||||
self.assertEqual(len(response.data['choices']), 1)
|
self.assertEqual(response.data['label'], 'Updated Label')
|
||||||
self.assertEqual(response.data['choices'][0]['value'], '2')
|
|
||||||
self.assertEqual(response.data['choices'][0]['label'], 'New Label')
|
|
||||||
entry_id = response.data['choices'][0]['id']
|
|
||||||
|
|
||||||
# Test changing an entry via list API
|
# Add a second entry, then delete the first via the entry detail API
|
||||||
response = self.patch(
|
self.post(
|
||||||
|
entry_list_url,
|
||||||
|
{'list': list_pk, 'value': '2', 'label': 'Second Entry'},
|
||||||
|
expected_code=201,
|
||||||
|
)
|
||||||
|
self.delete(entry_url, expected_code=204)
|
||||||
|
|
||||||
|
# Verify only the second entry remains
|
||||||
|
response = self.get(
|
||||||
reverse('api-selectionlist-detail', kwargs={'pk': list_pk}),
|
reverse('api-selectionlist-detail', kwargs={'pk': list_pk}),
|
||||||
{'choices': [{'id': entry_id, 'value': '2', 'label': 'New Label Text'}]},
|
data={'choices': True},
|
||||||
expected_code=200,
|
expected_code=200,
|
||||||
)
|
)
|
||||||
self.assertEqual(response.data['name'], 'New List')
|
|
||||||
self.assertTrue(response.data['active'])
|
|
||||||
self.assertEqual(len(response.data['choices']), 1)
|
self.assertEqual(len(response.data['choices']), 1)
|
||||||
self.assertEqual(response.data['choices'][0]['value'], '2')
|
self.assertEqual(response.data['choices'][0]['value'], '2')
|
||||||
self.assertEqual(response.data['choices'][0]['label'], 'New Label Text')
|
|
||||||
|
|
||||||
def test_api_locked(self):
|
def test_api_locked(self):
|
||||||
"""Test editing with locked/unlocked list."""
|
"""Test editing with locked/unlocked list."""
|
||||||
|
|||||||
@@ -284,3 +284,22 @@ export function useParameterFields({
|
|||||||
user
|
user
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectionListFields(): ApiFormFieldSet {
|
||||||
|
return {
|
||||||
|
name: {},
|
||||||
|
description: {},
|
||||||
|
active: {},
|
||||||
|
source_plugin: {},
|
||||||
|
source_string: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectionEntryFields(): ApiFormFieldSet {
|
||||||
|
return {
|
||||||
|
value: {},
|
||||||
|
label: {},
|
||||||
|
description: {},
|
||||||
|
active: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
|
|
||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { Table } from '@mantine/core';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
|
||||||
import { StandaloneField } from '../components/forms/StandaloneField';
|
|
||||||
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
|
|
||||||
|
|
||||||
function BuildAllocateLineRow({
|
|
||||||
props
|
|
||||||
}: Readonly<{
|
|
||||||
props: TableFieldRowProps;
|
|
||||||
}>) {
|
|
||||||
const valueField: ApiFormFieldType = useMemo(() => {
|
|
||||||
return {
|
|
||||||
field_type: 'string',
|
|
||||||
name: 'value',
|
|
||||||
required: true,
|
|
||||||
value: props.item.value,
|
|
||||||
onValueChange: (value: any) => {
|
|
||||||
props.changeFn(props.idx, 'value', value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
const labelField: ApiFormFieldType = useMemo(() => {
|
|
||||||
return {
|
|
||||||
field_type: 'string',
|
|
||||||
name: 'label',
|
|
||||||
required: true,
|
|
||||||
value: props.item.label,
|
|
||||||
onValueChange: (value: any) => {
|
|
||||||
props.changeFn(props.idx, 'label', value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
const descriptionField: ApiFormFieldType = useMemo(() => {
|
|
||||||
return {
|
|
||||||
field_type: 'string',
|
|
||||||
name: 'description',
|
|
||||||
required: true,
|
|
||||||
value: props.item.description,
|
|
||||||
onValueChange: (value: any) => {
|
|
||||||
props.changeFn(props.idx, 'description', value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
const activeField: ApiFormFieldType = useMemo(() => {
|
|
||||||
return {
|
|
||||||
field_type: 'boolean',
|
|
||||||
name: 'active',
|
|
||||||
required: true,
|
|
||||||
value: props.item.active,
|
|
||||||
onValueChange: (value: any) => {
|
|
||||||
props.changeFn(props.idx, 'active', value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [props]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table.Tr key={`table-row-${props.item.id ?? props.idx}`}>
|
|
||||||
<Table.Td>
|
|
||||||
<StandaloneField fieldName='value' fieldDefinition={valueField} />
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<StandaloneField fieldName='label' fieldDefinition={labelField} />
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<StandaloneField
|
|
||||||
fieldName='description'
|
|
||||||
fieldDefinition={descriptionField}
|
|
||||||
/>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<StandaloneField fieldName='active' fieldDefinition={activeField} />
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
|
||||||
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function selectionListFields(): ApiFormFieldSet {
|
|
||||||
return {
|
|
||||||
name: {},
|
|
||||||
description: {},
|
|
||||||
active: {},
|
|
||||||
locked: {},
|
|
||||||
source_plugin: {},
|
|
||||||
source_string: {},
|
|
||||||
choices: {
|
|
||||||
label: t`Entries`,
|
|
||||||
description: t`List of entries to choose from`,
|
|
||||||
field_type: 'table',
|
|
||||||
value: [],
|
|
||||||
headers: [
|
|
||||||
{ title: t`Value` },
|
|
||||||
{ title: t`Label` },
|
|
||||||
{ title: t`Description` },
|
|
||||||
{ title: t`Active` }
|
|
||||||
],
|
|
||||||
modelRenderer: (row: TableFieldRowProps) => (
|
|
||||||
<BuildAllocateLineRow props={row} />
|
|
||||||
),
|
|
||||||
addRow: () => {
|
|
||||||
return {
|
|
||||||
value: '',
|
|
||||||
label: '',
|
|
||||||
description: '',
|
|
||||||
active: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -32,6 +32,8 @@ import { PanelGroup } from '../../../../components/panels/PanelGroup';
|
|||||||
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||||
import { Loadable } from '../../../../functions/loading';
|
import { Loadable } from '../../../../functions/loading';
|
||||||
import { useUserState } from '../../../../states/UserState';
|
import { useUserState } from '../../../../states/UserState';
|
||||||
|
import ParameterTemplateTable from '../../../../tables/general/ParameterTemplateTable';
|
||||||
|
import SelectionListTable from '../../../../tables/settings/SelectionListTable';
|
||||||
|
|
||||||
const ReportTemplatePanel = Loadable(
|
const ReportTemplatePanel = Loadable(
|
||||||
lazy(() => import('./ReportTemplatePanel'))
|
lazy(() => import('./ReportTemplatePanel'))
|
||||||
@@ -69,8 +71,6 @@ const MachineManagementPanel = Loadable(
|
|||||||
lazy(() => import('./MachineManagementPanel'))
|
lazy(() => import('./MachineManagementPanel'))
|
||||||
);
|
);
|
||||||
|
|
||||||
const ParameterPanel = Loadable(lazy(() => import('./ParameterPanel')));
|
|
||||||
|
|
||||||
const ErrorReportTable = Loadable(
|
const ErrorReportTable = Loadable(
|
||||||
lazy(() => import('../../../../tables/settings/ErrorTable'))
|
lazy(() => import('../../../../tables/settings/ErrorTable'))
|
||||||
);
|
);
|
||||||
@@ -192,7 +192,14 @@ export default function AdminCenter() {
|
|||||||
name: 'parameters',
|
name: 'parameters',
|
||||||
label: t`Parameters`,
|
label: t`Parameters`,
|
||||||
icon: <IconList />,
|
icon: <IconList />,
|
||||||
content: <ParameterPanel />,
|
content: <ParameterTemplateTable />,
|
||||||
|
hidden: !user.hasViewRole(UserRoles.part)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'selection-lists',
|
||||||
|
label: t`Selection Lists`,
|
||||||
|
icon: <IconList />,
|
||||||
|
content: <SelectionListTable />,
|
||||||
hidden: !user.hasViewRole(UserRoles.part)
|
hidden: !user.hasViewRole(UserRoles.part)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -272,6 +279,7 @@ export default function AdminCenter() {
|
|||||||
id: 'plm',
|
id: 'plm',
|
||||||
label: t`PLM`,
|
label: t`PLM`,
|
||||||
panelIDs: [
|
panelIDs: [
|
||||||
|
'selection-lists',
|
||||||
'parameters',
|
'parameters',
|
||||||
'category-parameters',
|
'category-parameters',
|
||||||
'location-types',
|
'location-types',
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import { Accordion } from '@mantine/core';
|
|
||||||
|
|
||||||
import { StylishText } from '@lib/components/StylishText';
|
|
||||||
import ParameterTemplateTable from '../../../../tables/general/ParameterTemplateTable';
|
|
||||||
import SelectionListTable from '../../../../tables/part/SelectionListTable';
|
|
||||||
|
|
||||||
export default function ParameterPanel() {
|
|
||||||
return (
|
|
||||||
<Accordion multiple defaultValue={['parameter-templates']}>
|
|
||||||
<Accordion.Item value='parameter-templates' key='parameter-templates'>
|
|
||||||
<Accordion.Control>
|
|
||||||
<StylishText size='lg'>{t`Parameter Templates`}</StylishText>
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<ParameterTemplateTable />
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
<Accordion.Item value='selection-lists' key='selection-lists'>
|
|
||||||
<Accordion.Control>
|
|
||||||
<StylishText size='lg'>{t`Selection Lists`}</StylishText>
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<SelectionListTable />
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { Accordion, Alert, LoadingOverlay, Stack } from '@mantine/core';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||||
|
import { RowDeleteAction, RowEditAction } from '@lib/components/RowActions';
|
||||||
|
import { StylishText } from '@lib/components/StylishText';
|
||||||
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
|
import useTable from '@lib/hooks/UseTable';
|
||||||
|
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||||
|
import type { TableColumn } from '@lib/types/Tables';
|
||||||
|
import { IconLock } from '@tabler/icons-react';
|
||||||
|
import { EditApiForm } from '../../components/forms/ApiForm';
|
||||||
|
import {
|
||||||
|
selectionEntryFields,
|
||||||
|
selectionListFields
|
||||||
|
} from '../../forms/CommonForms';
|
||||||
|
import {
|
||||||
|
useCreateApiFormModal,
|
||||||
|
useDeleteApiFormModal,
|
||||||
|
useEditApiFormModal
|
||||||
|
} from '../../hooks/UseForm';
|
||||||
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
|
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
|
||||||
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
|
function SelectionListEntriesTable({
|
||||||
|
id,
|
||||||
|
locked
|
||||||
|
}: Readonly<{ id: string; locked: boolean }>) {
|
||||||
|
const table = useTable('selectionlist-entries');
|
||||||
|
|
||||||
|
const columns: TableColumn[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{ accessor: 'value', sortable: true },
|
||||||
|
{ accessor: 'label', sortable: true },
|
||||||
|
DescriptionColumn({}),
|
||||||
|
BooleanColumn({ accessor: 'active' })
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const entryFields: ApiFormFieldSet = selectionEntryFields();
|
||||||
|
|
||||||
|
const [selectedEntry, setSelectedEntry] = useState<number | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Construct the dynamic URL to edit (or delete) the selected entry
|
||||||
|
const selectedEntryUrl: string = useMemo(() => {
|
||||||
|
let url = apiUrl(ApiEndpoints.selectionentry_list, undefined, { id });
|
||||||
|
|
||||||
|
if (selectedEntry) {
|
||||||
|
url += `${selectedEntry}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}, [id, selectedEntry]);
|
||||||
|
|
||||||
|
const createEntry = useCreateApiFormModal({
|
||||||
|
url: ApiEndpoints.selectionentry_list,
|
||||||
|
pathParams: { id },
|
||||||
|
title: t`Add Selection Entry`,
|
||||||
|
fields: {
|
||||||
|
...entryFields,
|
||||||
|
list: {
|
||||||
|
value: id,
|
||||||
|
hidden: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
table: table
|
||||||
|
});
|
||||||
|
|
||||||
|
const editEntry = useEditApiFormModal({
|
||||||
|
url: selectedEntryUrl,
|
||||||
|
title: t`Edit Selection Entry`,
|
||||||
|
fields: entryFields,
|
||||||
|
table: table
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteEntry = useDeleteApiFormModal({
|
||||||
|
url: selectedEntryUrl,
|
||||||
|
title: t`Delete Selection Entry`,
|
||||||
|
table: table
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableActions = useMemo(() => {
|
||||||
|
if (locked) return [];
|
||||||
|
return [
|
||||||
|
<AddItemButton
|
||||||
|
key='add-entry'
|
||||||
|
onClick={() => createEntry.open()}
|
||||||
|
tooltip={t`Add Entry`}
|
||||||
|
/>
|
||||||
|
];
|
||||||
|
}, [locked, createEntry]);
|
||||||
|
|
||||||
|
const rowActions = useCallback(
|
||||||
|
(record: any) => {
|
||||||
|
if (locked) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
RowEditAction({
|
||||||
|
onClick: () => {
|
||||||
|
console.log('record:', record);
|
||||||
|
setSelectedEntry(record.id);
|
||||||
|
editEntry.open();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
RowDeleteAction({
|
||||||
|
onClick: () => {
|
||||||
|
console.log('record:', record);
|
||||||
|
setSelectedEntry(record.id);
|
||||||
|
deleteEntry.open();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
|
},
|
||||||
|
[editEntry, deleteEntry, locked]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{createEntry.modal}
|
||||||
|
{editEntry.modal}
|
||||||
|
{deleteEntry.modal}
|
||||||
|
<InvenTreeTable
|
||||||
|
url={apiUrl(ApiEndpoints.selectionentry_list, undefined, { id })}
|
||||||
|
tableState={table}
|
||||||
|
columns={columns}
|
||||||
|
props={{
|
||||||
|
enableSearch: true,
|
||||||
|
enableSelection: !locked,
|
||||||
|
enableBulkDelete: !locked,
|
||||||
|
tableActions: tableActions,
|
||||||
|
rowActions: locked ? undefined : rowActions
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SelectionListDrawer({
|
||||||
|
id,
|
||||||
|
refreshTable
|
||||||
|
}: Readonly<{
|
||||||
|
id: string;
|
||||||
|
refreshTable: () => void;
|
||||||
|
}>) {
|
||||||
|
const {
|
||||||
|
instance,
|
||||||
|
refreshInstance,
|
||||||
|
instanceQuery: { isFetching }
|
||||||
|
} = useInstance({
|
||||||
|
endpoint: ApiEndpoints.selectionlist_list,
|
||||||
|
pk: id
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectionFields: ApiFormFieldSet = useMemo(() => {
|
||||||
|
return selectionListFields();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isFetching) {
|
||||||
|
return <LoadingOverlay visible />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
{instance.locked && (
|
||||||
|
<Alert color='red' icon={<IconLock />} title={t`Locked`}>
|
||||||
|
{t`This selection list is locked and cannot be edited.`}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<Accordion defaultValue={['details', 'entries']} multiple>
|
||||||
|
<Accordion.Item key='details' value='details'>
|
||||||
|
<Accordion.Control>
|
||||||
|
<StylishText size='lg'>{t`Selection List Details`}</StylishText>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<fieldset
|
||||||
|
disabled={instance.locked}
|
||||||
|
style={{ border: 'none', padding: 0, margin: 0 }}
|
||||||
|
>
|
||||||
|
<EditApiForm
|
||||||
|
props={{
|
||||||
|
url: ApiEndpoints.selectionlist_list,
|
||||||
|
pk: id,
|
||||||
|
fields: selectionFields,
|
||||||
|
onFormSuccess: () => {
|
||||||
|
refreshTable();
|
||||||
|
refreshInstance();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
id={`selection-list-drawer-${id}`}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item key='entries' value='entries'>
|
||||||
|
<Accordion.Control>
|
||||||
|
<StylishText size='lg'>{t`Selection List Entries`}</StylishText>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
<SelectionListEntriesTable
|
||||||
|
id={id}
|
||||||
|
locked={instance?.locked ?? false}
|
||||||
|
/>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
+20
-15
@@ -1,5 +1,6 @@
|
|||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||||
import {
|
import {
|
||||||
@@ -7,26 +8,28 @@ import {
|
|||||||
RowDeleteAction,
|
RowDeleteAction,
|
||||||
RowEditAction
|
RowEditAction
|
||||||
} from '@lib/components/RowActions';
|
} from '@lib/components/RowActions';
|
||||||
|
import { DetailDrawer } from '@lib/components/nav/DetailDrawer';
|
||||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import { apiUrl } from '@lib/functions/Api';
|
import { apiUrl } from '@lib/functions/Api';
|
||||||
import useTable from '@lib/hooks/UseTable';
|
import useTable from '@lib/hooks/UseTable';
|
||||||
import type { TableColumn } from '@lib/types/Tables';
|
import type { TableColumn } from '@lib/types/Tables';
|
||||||
import { selectionListFields } from '../../forms/selectionListFields';
|
import { selectionListFields } from '../../forms/CommonForms';
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal
|
||||||
useEditApiFormModal
|
|
||||||
} from '../../hooks/UseForm';
|
} from '../../hooks/UseForm';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
|
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
import SelectionListDrawer from './SelectionListDrawer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table for displaying list of selectionlist items
|
* Table for displaying list of selectionlist items
|
||||||
*/
|
*/
|
||||||
export default function SelectionListTable() {
|
export default function SelectionListTable() {
|
||||||
const table = useTable('selectionlist');
|
const table = useTable('selectionlist');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
|
||||||
@@ -70,14 +73,6 @@ export default function SelectionListTable() {
|
|||||||
number | undefined
|
number | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
const editSelectionList = useEditApiFormModal({
|
|
||||||
url: ApiEndpoints.selectionlist_list,
|
|
||||||
pk: selectedSelectionList,
|
|
||||||
title: t`Edit Selection List`,
|
|
||||||
fields: selectionListFields(),
|
|
||||||
table: table
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteSelectionList = useDeleteApiFormModal({
|
const deleteSelectionList = useDeleteApiFormModal({
|
||||||
url: ApiEndpoints.selectionlist_list,
|
url: ApiEndpoints.selectionlist_list,
|
||||||
pk: selectedSelectionList,
|
pk: selectedSelectionList,
|
||||||
@@ -91,8 +86,7 @@ export default function SelectionListTable() {
|
|||||||
RowEditAction({
|
RowEditAction({
|
||||||
hidden: !user.hasChangeRole(UserRoles.admin),
|
hidden: !user.hasChangeRole(UserRoles.admin),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedSelectionList(record.pk);
|
navigate(`${record.pk}/`);
|
||||||
editSelectionList.open();
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
RowDeleteAction({
|
RowDeleteAction({
|
||||||
@@ -120,8 +114,16 @@ export default function SelectionListTable() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{newSelectionList.modal}
|
{newSelectionList.modal}
|
||||||
{editSelectionList.modal}
|
|
||||||
{deleteSelectionList.modal}
|
{deleteSelectionList.modal}
|
||||||
|
<DetailDrawer
|
||||||
|
title={t`Selection List`}
|
||||||
|
size='xl'
|
||||||
|
renderContent={(id) =>
|
||||||
|
id ? (
|
||||||
|
<SelectionListDrawer id={id} refreshTable={table.refreshTable} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
/>
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.selectionlist_list)}
|
url={apiUrl(ApiEndpoints.selectionlist_list)}
|
||||||
tableState={table}
|
tableState={table}
|
||||||
@@ -129,7 +131,10 @@ export default function SelectionListTable() {
|
|||||||
props={{
|
props={{
|
||||||
rowActions: rowActions,
|
rowActions: rowActions,
|
||||||
tableActions: tableActions,
|
tableActions: tableActions,
|
||||||
enableDownload: true
|
enableDownload: true,
|
||||||
|
onRowClick: (record) => {
|
||||||
|
navigate(`${record.pk}/`);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -77,6 +77,7 @@ test('Parts - Image Selection', async ({ browser }) => {
|
|||||||
await page
|
await page
|
||||||
.getByRole('tabpanel', { name: 'Part Details' })
|
.getByRole('tabpanel', { name: 'Part Details' })
|
||||||
.locator('img')
|
.locator('img')
|
||||||
|
.first()
|
||||||
.hover();
|
.hover();
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: 'action-button-select-from-' })
|
.getByRole('button', { name: 'action-button-select-from-' })
|
||||||
@@ -94,6 +95,7 @@ test('Parts - Image Selection', async ({ browser }) => {
|
|||||||
await page
|
await page
|
||||||
.getByRole('tabpanel', { name: 'Part Details' })
|
.getByRole('tabpanel', { name: 'Part Details' })
|
||||||
.locator('img')
|
.locator('img')
|
||||||
|
.first()
|
||||||
.hover();
|
.hover();
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: 'action-button-delete-image' })
|
.getByRole('button', { name: 'action-button-delete-image' })
|
||||||
|
|||||||
@@ -408,29 +408,8 @@ test('Settings - Admin - Parameter', async ({ browser }) => {
|
|||||||
await page.getByRole('button', { name: 'admin' }).click();
|
await page.getByRole('button', { name: 'admin' }).click();
|
||||||
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
|
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
|
||||||
|
|
||||||
await loadTab(page, 'Parameters', true);
|
|
||||||
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
|
|
||||||
// Clean old template data if exists
|
|
||||||
await page
|
|
||||||
.getByRole('cell', { name: 'my custom parameter', exact: true })
|
|
||||||
.waitFor({ timeout: 500 })
|
|
||||||
.then(async (cell) => {
|
|
||||||
await page
|
|
||||||
.getByRole('cell', { name: 'my custom parameter' })
|
|
||||||
.locator('..')
|
|
||||||
.getByLabel('row-action-menu-')
|
|
||||||
.click();
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
// Allow time for the table to load
|
// Allow time for the table to load
|
||||||
await page.getByRole('button', { name: 'Selection Lists' }).click();
|
await loadTab(page, 'Selection Lists');
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
|
|
||||||
// Check for expected entry
|
// Check for expected entry
|
||||||
await page.getByRole('cell', { name: 'Animals', exact: true }).waitFor();
|
await page.getByRole('cell', { name: 'Animals', exact: true }).waitFor();
|
||||||
@@ -456,38 +435,65 @@ 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');
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
// Select the new list to edit entries
|
||||||
|
await page.getByRole('cell', { name: 'some list' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Selection List Entries' }).waitFor();
|
||||||
|
await page.getByRole('button', { name: 'Selection List Details' }).click();
|
||||||
|
|
||||||
// Add an entry to the selection list
|
// Add an entry to the selection list
|
||||||
await page.getByRole('button', { name: 'action-button-add-new-row' }).click();
|
await page.getByRole('button', { name: 'action-button-add-entry' }).click();
|
||||||
await page.getByRole('textbox', { name: 'text-field-value' }).fill('HW');
|
await page.getByRole('textbox', { name: 'text-field-value' }).fill('HW');
|
||||||
await page
|
await page
|
||||||
.getByRole('textbox', { name: 'text-field-label' })
|
.getByRole('textbox', { name: 'text-field-label' })
|
||||||
.fill('Hardwood');
|
.fill('Hardwood');
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('row', { name: 'boolean-field-active action-' })
|
.getByRole('textbox', { name: 'text-field-description' })
|
||||||
.getByLabel('text-field-description')
|
|
||||||
.fill('Hardwood materials');
|
.fill('Hardwood materials');
|
||||||
await page.getByRole('cell', { name: 'boolean-field-active' }).click();
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
await page.getByRole('cell', { name: 'some list' }).waitFor();
|
|
||||||
|
|
||||||
await page.getByLabel('action-button-add-parameter').waitFor();
|
await page.getByRole('cell', { name: 'HW' }).waitFor();
|
||||||
await page.getByLabel('action-button-add-parameter').click();
|
|
||||||
await page.getByLabel('text-field-name').fill('my custom parameter');
|
// Next, navigate to the "Parameters" tab
|
||||||
await page.getByLabel('text-field-description').fill('description');
|
await navigate(page, 'settings/admin/parameters/');
|
||||||
|
|
||||||
|
await loadTab(page, 'Parameters', true);
|
||||||
|
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Clean old template data if exists
|
||||||
await page
|
await page
|
||||||
.locator('div')
|
.getByRole('cell', { name: 'my custom parameter', exact: true })
|
||||||
.filter({ hasText: /^Search\.\.\.$/ })
|
.waitFor({ timeout: 500 })
|
||||||
.nth(2)
|
.then(async (cell) => {
|
||||||
|
await page
|
||||||
|
.getByRole('cell', { name: 'my custom parameter' })
|
||||||
|
.locator('..')
|
||||||
|
.getByLabel('row-action-menu-')
|
||||||
|
.click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
// Create a new parameter, using the "Hardwood" selection list entry as the value
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'action-button-add-parameter-' })
|
||||||
.click();
|
.click();
|
||||||
await page
|
await page
|
||||||
.getByRole('option', { name: 'some list' })
|
.getByRole('textbox', { name: 'text-field-name' })
|
||||||
.locator('div')
|
.fill('my custom parameter');
|
||||||
.first()
|
await page
|
||||||
.click();
|
.getByRole('combobox', { name: 'related-field-selectionlist' })
|
||||||
|
.fill('some');
|
||||||
|
await page.getByRole('option', { name: 'some list' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Submit' }).click();
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
await page.getByRole('cell', { name: 'my custom parameter' }).click();
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
// Fill parameter
|
// Fill parameter
|
||||||
await navigate(page, 'part/104/parameters/');
|
await navigate(page, 'part/104/parameters/');
|
||||||
|
|||||||
Reference in New Issue
Block a user