mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-01 04:56:45 +00:00
Parameter types (#4935)
* Add fields to PartParameterTemplateModel - checkbox: Is the field a 'checkbox' - choices: List of valid options * Update javascript * Adds unit test for PartParameterTemplate - Checkbox cannot have units - Checkbox cannot have choices - Choices must be unique * Improve API filtering - Add "has_choices" filter - Add "has_units" filter * Prune dead code * Update js functions for creating / editing parameters * Update part parameter form - Rebuild the "data" field based on the selected template - Supports "string" / "boolean" / "select" * Adjust data input based on parameter type - Choice displays available options - Checkbox displays boolean switch - Otherwise displays text input - Adds more unit testing - Updates to forms.js for improved functionality * Calculate numeric value for boolean parameters * Update docs * Bump API version
This commit is contained in:
parent
2c05e3e74d
commit
e21a5e62b8
@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 117
|
INVENTREE_API_VERSION = 118
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
|
v118 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4935
|
||||||
|
- Adds extra fields for the PartParameterTemplate model
|
||||||
|
|
||||||
v117 -> 2023-05-22 : https://github.com/inventree/InvenTree/pull/4854
|
v117 -> 2023-05-22 : https://github.com/inventree/InvenTree/pull/4854
|
||||||
- Part.units model now supports physical units (e.g. "kg", "m", "mm", etc)
|
- Part.units model now supports physical units (e.g. "kg", "m", "mm", etc)
|
||||||
- Replaces SupplierPart "pack_size" field with "pack_quantity"
|
- Replaces SupplierPart "pack_size" field with "pack_quantity"
|
||||||
|
@ -1349,8 +1349,35 @@ class PartParameterTemplateFilter(rest_filters.FilterSet):
|
|||||||
# Simple filter fields
|
# Simple filter fields
|
||||||
fields = [
|
fields = [
|
||||||
'units',
|
'units',
|
||||||
|
'checkbox',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
has_choices = rest_filters.BooleanFilter(
|
||||||
|
method='filter_has_choices',
|
||||||
|
label='Has Choice',
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_has_choices(self, queryset, name, value):
|
||||||
|
"""Filter queryset to include only PartParameterTemplates with choices."""
|
||||||
|
|
||||||
|
if str2bool(value):
|
||||||
|
return queryset.exclude(Q(choices=None) | Q(choices=''))
|
||||||
|
else:
|
||||||
|
return queryset.filter(Q(choices=None) | Q(choices=''))
|
||||||
|
|
||||||
|
has_units = rest_filters.BooleanFilter(
|
||||||
|
method='filter_has_units',
|
||||||
|
label='Has Units',
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_has_units(self, queryset, name, value):
|
||||||
|
"""Filter queryset to include only PartParameterTemplates with units."""
|
||||||
|
|
||||||
|
if str2bool(value):
|
||||||
|
return queryset.exclude(Q(units=None) | Q(units=''))
|
||||||
|
else:
|
||||||
|
return queryset.filter(Q(units=None) | Q(units=''))
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTemplateList(ListCreateAPI):
|
class PartParameterTemplateList(ListCreateAPI):
|
||||||
"""API endpoint for accessing a list of PartParameterTemplate objects.
|
"""API endpoint for accessing a list of PartParameterTemplate objects.
|
||||||
@ -1377,6 +1404,7 @@ class PartParameterTemplateList(ListCreateAPI):
|
|||||||
ordering_fields = [
|
ordering_fields = [
|
||||||
'name',
|
'name',
|
||||||
'units',
|
'units',
|
||||||
|
'checkbox',
|
||||||
]
|
]
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
@ -1580,45 +1608,33 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
def filter_available_stock(self, queryset, name, value):
|
def filter_available_stock(self, queryset, name, value):
|
||||||
"""Filter the queryset based on whether each line item has any available stock"""
|
"""Filter the queryset based on whether each line item has any available stock"""
|
||||||
|
|
||||||
value = str2bool(value)
|
if str2bool(value):
|
||||||
|
return queryset.filter(available_stock__gt=0)
|
||||||
if value:
|
|
||||||
queryset = queryset.filter(available_stock__gt=0)
|
|
||||||
else:
|
else:
|
||||||
queryset = queryset.filter(available_stock=0)
|
return queryset.filter(available_stock=0)
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order")
|
on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order")
|
||||||
|
|
||||||
def filter_on_order(self, queryset, name, value):
|
def filter_on_order(self, queryset, name, value):
|
||||||
"""Filter the queryset based on whether each line item has any stock on order"""
|
"""Filter the queryset based on whether each line item has any stock on order"""
|
||||||
|
|
||||||
value = str2bool(value)
|
if str2bool(value):
|
||||||
|
return queryset.filter(on_order__gt=0)
|
||||||
if value:
|
|
||||||
queryset = queryset.filter(on_order__gt=0)
|
|
||||||
else:
|
else:
|
||||||
queryset = queryset.filter(on_order=0)
|
return queryset.filter(on_order=0)
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
has_pricing = rest_filters.BooleanFilter(label="Has Pricing", method="filter_has_pricing")
|
has_pricing = rest_filters.BooleanFilter(label="Has Pricing", method="filter_has_pricing")
|
||||||
|
|
||||||
def filter_has_pricing(self, queryset, name, value):
|
def filter_has_pricing(self, queryset, name, value):
|
||||||
"""Filter the queryset based on whether pricing information is available for the sub_part"""
|
"""Filter the queryset based on whether pricing information is available for the sub_part"""
|
||||||
|
|
||||||
value = str2bool(value)
|
|
||||||
|
|
||||||
q_a = Q(sub_part__pricing_data=None)
|
q_a = Q(sub_part__pricing_data=None)
|
||||||
q_b = Q(sub_part__pricing_data__overall_min=None, sub_part__pricing_data__overall_max=None)
|
q_b = Q(sub_part__pricing_data__overall_min=None, sub_part__pricing_data__overall_max=None)
|
||||||
|
|
||||||
if value:
|
if str2bool(value):
|
||||||
queryset = queryset.exclude(q_a | q_b)
|
return queryset.exclude(q_a | q_b)
|
||||||
else:
|
else:
|
||||||
queryset = queryset.filter(q_a | q_b)
|
return queryset.filter(q_a | q_b)
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
|
|
||||||
class BomMixin:
|
class BomMixin:
|
||||||
|
23
InvenTree/part/migrations/0112_auto_20230531_1205.py
Normal file
23
InvenTree/part/migrations/0112_auto_20230531_1205.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-05-31 12:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0111_auto_20230521_1350'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partparametertemplate',
|
||||||
|
name='checkbox',
|
||||||
|
field=models.BooleanField(default=False, help_text='Is this parameter a checkbox?', verbose_name='Checkbox'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='partparametertemplate',
|
||||||
|
name='choices',
|
||||||
|
field=models.CharField(blank=True, help_text='Valid choices for this parameter (comma-separated)', max_length=5000, verbose_name='Choices'),
|
||||||
|
),
|
||||||
|
]
|
@ -46,7 +46,8 @@ from common.settings import currency_code_default
|
|||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
from InvenTree import helpers, validators
|
from InvenTree import helpers, validators
|
||||||
from InvenTree.fields import InvenTreeURLField
|
from InvenTree.fields import InvenTreeURLField
|
||||||
from InvenTree.helpers import decimal2money, decimal2string, normalize
|
from InvenTree.helpers import (decimal2money, decimal2string, normalize,
|
||||||
|
str2bool)
|
||||||
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
|
from InvenTree.models import (DataImportMixin, InvenTreeAttachment,
|
||||||
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
|
InvenTreeBarcodeMixin, InvenTreeNotesMixin,
|
||||||
InvenTreeTree, MetadataMixin)
|
InvenTreeTree, MetadataMixin)
|
||||||
@ -3307,6 +3308,8 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
|||||||
name: The name (key) of the Parameter [string]
|
name: The name (key) of the Parameter [string]
|
||||||
units: The units of the Parameter [string]
|
units: The units of the Parameter [string]
|
||||||
description: Description of the parameter [string]
|
description: Description of the parameter [string]
|
||||||
|
checkbox: Boolean flag to indicate whether the parameter is a checkbox [bool]
|
||||||
|
choices: List of valid choices for the parameter [string]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -3321,6 +3324,47 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
|||||||
s += " ({units})".format(units=self.units)
|
s += " ({units})".format(units=self.units)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
"""Custom cleaning step for this model:
|
||||||
|
|
||||||
|
- A 'checkbox' field cannot have 'choices' set
|
||||||
|
- A 'checkbox' field cannot have 'units' set
|
||||||
|
"""
|
||||||
|
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Check that checkbox parameters do not have units or choices
|
||||||
|
if self.checkbox:
|
||||||
|
if self.units:
|
||||||
|
raise ValidationError({
|
||||||
|
'units': _('Checkbox parameters cannot have units')
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.choices:
|
||||||
|
raise ValidationError({
|
||||||
|
'choices': _('Checkbox parameters cannot have choices')
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check that 'choices' are in fact valid
|
||||||
|
self.choices = self.choices.strip()
|
||||||
|
|
||||||
|
if self.choices:
|
||||||
|
choice_set = set()
|
||||||
|
|
||||||
|
for choice in self.choices.split(','):
|
||||||
|
choice = choice.strip()
|
||||||
|
|
||||||
|
# Ignore empty choices
|
||||||
|
if not choice:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if choice in choice_set:
|
||||||
|
raise ValidationError({
|
||||||
|
'choices': _('Choices must be unique')
|
||||||
|
})
|
||||||
|
|
||||||
|
choice_set.add(choice)
|
||||||
|
|
||||||
def validate_unique(self, exclude=None):
|
def validate_unique(self, exclude=None):
|
||||||
"""Ensure that PartParameterTemplates cannot be created with the same name.
|
"""Ensure that PartParameterTemplates cannot be created with the same name.
|
||||||
|
|
||||||
@ -3337,6 +3381,14 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
|||||||
except PartParameterTemplate.DoesNotExist:
|
except PartParameterTemplate.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_choices(self):
|
||||||
|
"""Return a list of choices for this parameter template"""
|
||||||
|
|
||||||
|
if not self.choices:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [x.strip() for x in self.choices.split(',') if x.strip()]
|
||||||
|
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
@ -3360,6 +3412,19 @@ class PartParameterTemplate(MetadataMixin, models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
checkbox = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_('Checkbox'),
|
||||||
|
help_text=_('Is this parameter a checkbox?')
|
||||||
|
)
|
||||||
|
|
||||||
|
choices = models.CharField(
|
||||||
|
max_length=5000,
|
||||||
|
verbose_name=_('Choices'),
|
||||||
|
help_text=_('Valid choices for this parameter (comma-separated)'),
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=PartParameterTemplate, dispatch_uid='post_save_part_parameter_template')
|
@receiver(post_save, sender=PartParameterTemplate, dispatch_uid='post_save_part_parameter_template')
|
||||||
def post_save_part_parameter_template(sender, instance, created, **kwargs):
|
def post_save_part_parameter_template(sender, instance, created, **kwargs):
|
||||||
@ -3412,6 +3477,11 @@ class PartParameter(models.Model):
|
|||||||
# Validate the PartParameter before saving
|
# Validate the PartParameter before saving
|
||||||
self.calculate_numeric_value()
|
self.calculate_numeric_value()
|
||||||
|
|
||||||
|
# Convert 'boolean' values to 'True' / 'False'
|
||||||
|
if self.template.checkbox:
|
||||||
|
self.data = str2bool(self.data)
|
||||||
|
self.data_numeric = 1 if self.data else 0
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
@ -3428,6 +3498,13 @@ class PartParameter(models.Model):
|
|||||||
'data': e.message
|
'data': e.message
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Validate the parameter data against the template choices
|
||||||
|
if choices := self.template.get_choices():
|
||||||
|
if self.data not in choices:
|
||||||
|
raise ValidationError({
|
||||||
|
'data': _('Invalid choice for parameter value')
|
||||||
|
})
|
||||||
|
|
||||||
def calculate_numeric_value(self):
|
def calculate_numeric_value(self):
|
||||||
"""Calculate a numeric value for the parameter data.
|
"""Calculate a numeric value for the parameter data.
|
||||||
|
|
||||||
|
@ -216,6 +216,8 @@ class PartParameterTemplateSerializer(InvenTree.serializers.InvenTreeModelSerial
|
|||||||
'name',
|
'name',
|
||||||
'units',
|
'units',
|
||||||
'description',
|
'description',
|
||||||
|
'checkbox',
|
||||||
|
'choices',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -846,47 +846,13 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$('#param-table').inventreeTable({
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if roles.part.add %}
|
{% if roles.part.add %}
|
||||||
$('#param-create').click(function() {
|
$('#param-create').click(function() {
|
||||||
|
createPartParameter({{ part.pk }}, {
|
||||||
constructForm('{% url "api-part-parameter-list" %}', {
|
refreshTable: '#parameter-table'
|
||||||
method: 'POST',
|
|
||||||
fields: {
|
|
||||||
part: {
|
|
||||||
value: {{ part.pk }},
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
filters: {
|
|
||||||
ordering: 'name',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {},
|
|
||||||
},
|
|
||||||
title: '{% trans "Add Parameter" %}',
|
|
||||||
refreshTable: '#parameter-table',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
$('.param-edit').click(function() {
|
|
||||||
var button = $(this);
|
|
||||||
|
|
||||||
launchModalForm(button.attr('url'), {
|
|
||||||
reload: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.param-delete').click(function() {
|
|
||||||
var button = $(this);
|
|
||||||
|
|
||||||
launchModalForm(button.attr('url'), {
|
|
||||||
reload: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onPanelLoad("part-attachments", function() {
|
onPanelLoad("part-attachments", function() {
|
||||||
|
@ -102,6 +102,29 @@ class ParameterTests(TestCase):
|
|||||||
'params'
|
'params'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def test_choice_validation(self):
|
||||||
|
"""Test that parameter choices are correctly validated"""
|
||||||
|
|
||||||
|
template = PartParameterTemplate.objects.create(
|
||||||
|
name='My Template',
|
||||||
|
description='A template with choices',
|
||||||
|
choices='red, blue, green'
|
||||||
|
)
|
||||||
|
|
||||||
|
pass_values = ['red', 'blue', 'green']
|
||||||
|
fail_values = ['rod', 'bleu', 'grene']
|
||||||
|
|
||||||
|
part = Part.objects.all().first()
|
||||||
|
|
||||||
|
for value in pass_values:
|
||||||
|
param = PartParameter(part=part, template=template, data=value)
|
||||||
|
param.full_clean()
|
||||||
|
|
||||||
|
for value in fail_values:
|
||||||
|
param = PartParameter(part=part, template=template, data=value)
|
||||||
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
|
param.full_clean()
|
||||||
|
|
||||||
def test_unit_validation(self):
|
def test_unit_validation(self):
|
||||||
"""Test validation of 'units' field for PartParameterTemplate"""
|
"""Test validation of 'units' field for PartParameterTemplate"""
|
||||||
|
|
||||||
@ -116,7 +139,7 @@ class ParameterTests(TestCase):
|
|||||||
with self.assertRaises(django_exceptions.ValidationError):
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
tmp.full_clean()
|
tmp.full_clean()
|
||||||
|
|
||||||
def test_param_validation(self):
|
def test_param_unit_validation(self):
|
||||||
"""Test that parameters are correctly validated against template units"""
|
"""Test that parameters are correctly validated against template units"""
|
||||||
|
|
||||||
template = PartParameterTemplate.objects.create(
|
template = PartParameterTemplate.objects.create(
|
||||||
@ -137,7 +160,7 @@ class ParameterTests(TestCase):
|
|||||||
with self.assertRaises(django_exceptions.ValidationError):
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
param.full_clean()
|
param.full_clean()
|
||||||
|
|
||||||
def test_param_conversion(self):
|
def test_param_unit_conversion(self):
|
||||||
"""Test that parameters are correctly converted to template units"""
|
"""Test that parameters are correctly converted to template units"""
|
||||||
|
|
||||||
template = PartParameterTemplate.objects.create(
|
template = PartParameterTemplate.objects.create(
|
||||||
@ -202,6 +225,41 @@ class PartParameterTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(response.data), 4)
|
self.assertEqual(len(response.data), 4)
|
||||||
|
|
||||||
|
def test_param_template_validation(self):
|
||||||
|
"""Test that part parameter template validation routines work correctly."""
|
||||||
|
|
||||||
|
# Checkbox parameter cannot have "units" specified
|
||||||
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
|
template = PartParameterTemplate(
|
||||||
|
name='test',
|
||||||
|
description='My description',
|
||||||
|
units='mm',
|
||||||
|
checkbox=True
|
||||||
|
)
|
||||||
|
|
||||||
|
template.clean()
|
||||||
|
|
||||||
|
# Checkbox parameter cannot have "choices" specified
|
||||||
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
|
template = PartParameterTemplate(
|
||||||
|
name='test',
|
||||||
|
description='My description',
|
||||||
|
choices='a,b,c',
|
||||||
|
checkbox=True
|
||||||
|
)
|
||||||
|
|
||||||
|
template.clean()
|
||||||
|
|
||||||
|
# Choices must be 'unique'
|
||||||
|
with self.assertRaises(django_exceptions.ValidationError):
|
||||||
|
template = PartParameterTemplate(
|
||||||
|
name='test',
|
||||||
|
description='My description',
|
||||||
|
choices='a,a,b',
|
||||||
|
)
|
||||||
|
|
||||||
|
template.clean()
|
||||||
|
|
||||||
def test_create_param(self):
|
def test_create_param(self):
|
||||||
"""Test that we can create a param via the API."""
|
"""Test that we can create a param via the API."""
|
||||||
url = reverse('api-part-parameter-list')
|
url = reverse('api-part-parameter-list')
|
||||||
|
@ -309,11 +309,7 @@ onPanelLoad('part-parameters', function() {
|
|||||||
|
|
||||||
$("#new-param").click(function() {
|
$("#new-param").click(function() {
|
||||||
constructForm('{% url "api-part-parameter-template-list" %}', {
|
constructForm('{% url "api-part-parameter-template-list" %}', {
|
||||||
fields: {
|
fields: partParameterTemplateFields(),
|
||||||
name: {},
|
|
||||||
units: {},
|
|
||||||
description: {},
|
|
||||||
},
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
title: '{% trans "Create Part Parameter Template" %}',
|
title: '{% trans "Create Part Parameter Template" %}',
|
||||||
refreshTable: '#param-table',
|
refreshTable: '#param-table',
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
showApiError,
|
showApiError,
|
||||||
showMessage,
|
showMessage,
|
||||||
showModalSpinner,
|
showModalSpinner,
|
||||||
|
toBool,
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
@ -990,15 +991,17 @@ function updateFieldValue(name, value, field, options) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.type == null) {
|
||||||
|
field.type = guessFieldType(el);
|
||||||
|
}
|
||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'decimal':
|
case 'decimal':
|
||||||
// Strip trailing zeros
|
// Strip trailing zeros
|
||||||
el.val(formatDecimal(value));
|
el.val(formatDecimal(value));
|
||||||
break;
|
break;
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
if (value == true || value.toString().toLowerCase() == 'true') {
|
el.prop('checked', toBool(value));
|
||||||
el.prop('checked');
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'related field':
|
case 'related field':
|
||||||
// Clear?
|
// Clear?
|
||||||
@ -1068,6 +1071,34 @@ function validateFormField(name, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Introspect the HTML element to guess the field type
|
||||||
|
*/
|
||||||
|
function guessFieldType(element) {
|
||||||
|
|
||||||
|
if (!element.exists) {
|
||||||
|
console.error(`Could not find element '${element}' for guessFieldType`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (element.attr('type')) {
|
||||||
|
case 'number':
|
||||||
|
return 'decimal';
|
||||||
|
case 'checkbox':
|
||||||
|
return 'boolean';
|
||||||
|
case 'date':
|
||||||
|
return 'date';
|
||||||
|
case 'datetime':
|
||||||
|
return 'datetime';
|
||||||
|
case 'text':
|
||||||
|
return 'string';
|
||||||
|
default:
|
||||||
|
// Unknown field type
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Extract and field value before sending back to the server
|
* Extract and field value before sending back to the server
|
||||||
*
|
*
|
||||||
@ -1088,9 +1119,16 @@ function getFormFieldValue(name, field={}, options={}) {
|
|||||||
|
|
||||||
var value = null;
|
var value = null;
|
||||||
|
|
||||||
|
let guessed_type = guessFieldType(el);
|
||||||
|
|
||||||
|
// If field type is not specified, try to guess it
|
||||||
|
if (field.type == null || guessed_type == 'boolean') {
|
||||||
|
field.type = guessed_type;
|
||||||
|
}
|
||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
value = el.is(':checked');
|
value = toBool(el.prop("checked"));
|
||||||
break;
|
break;
|
||||||
case 'date':
|
case 'date':
|
||||||
case 'datetime':
|
case 'datetime':
|
||||||
|
@ -40,15 +40,42 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
function yesNoLabel(value, options={}) {
|
/*
|
||||||
var text = '';
|
* Convert a value (which may be a string) to a boolean value
|
||||||
var color = '';
|
*
|
||||||
|
* @param {string} value: Input value
|
||||||
|
* @returns {boolean} true or false
|
||||||
|
*/
|
||||||
|
function toBool(value) {
|
||||||
|
|
||||||
if (value) {
|
if (typeof value == 'string') {
|
||||||
text = '{% trans "YES" %}';
|
|
||||||
|
if (value.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value.toLowerCase();
|
||||||
|
|
||||||
|
if (['true', 't', 'yes', 'y', '1', 'on', 'ok'].includes(value)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return value == true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function yesNoLabel(value, options={}) {
|
||||||
|
let text = '';
|
||||||
|
let color = '';
|
||||||
|
|
||||||
|
if (toBool(value)) {
|
||||||
|
text = options.pass || '{% trans "YES" %}';
|
||||||
color = 'bg-success';
|
color = 'bg-success';
|
||||||
} else {
|
} else {
|
||||||
text = '{% trans "NO" %}';
|
text = options.fail || '{% trans "NO" %}';
|
||||||
color = 'bg-warning';
|
color = 'bg-warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -874,8 +874,8 @@ function insertActionButton(modal, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Attach a provided list of buttons */
|
||||||
function attachButtons(modal, buttons) {
|
function attachButtons(modal, buttons) {
|
||||||
/* Attach a provided list of buttons */
|
|
||||||
|
|
||||||
for (var i = 0; i < buttons.length; i++) {
|
for (var i = 0; i < buttons.length; i++) {
|
||||||
insertActionButton(modal, buttons[i]);
|
insertActionButton(modal, buttons[i]);
|
||||||
@ -883,14 +883,14 @@ function attachButtons(modal, buttons) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Attach a 'callback' function to a given field in the modal form.
|
||||||
|
* When the value of that field is changed, the callback function is performed.
|
||||||
|
*
|
||||||
|
* options:
|
||||||
|
* - field: The name of the field to attach to
|
||||||
|
* - action: A function to perform
|
||||||
|
*/
|
||||||
function attachFieldCallback(modal, callback) {
|
function attachFieldCallback(modal, callback) {
|
||||||
/* Attach a 'callback' function to a given field in the modal form.
|
|
||||||
* When the value of that field is changed, the callback function is performed.
|
|
||||||
*
|
|
||||||
* options:
|
|
||||||
* - field: The name of the field to attach to
|
|
||||||
* - action: A function to perform
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Find the field input in the form
|
// Find the field input in the form
|
||||||
var field = getFieldByName(modal, callback.field);
|
var field = getFieldByName(modal, callback.field);
|
||||||
@ -907,8 +907,8 @@ function attachFieldCallback(modal, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Attach a provided list of callback functions */
|
||||||
function attachCallbacks(modal, callbacks) {
|
function attachCallbacks(modal, callbacks) {
|
||||||
/* Attach a provided list of callback functions */
|
|
||||||
|
|
||||||
for (var i = 0; i < callbacks.length; i++) {
|
for (var i = 0; i < callbacks.length; i++) {
|
||||||
attachFieldCallback(modal, callbacks[i]);
|
attachFieldCallback(modal, callbacks[i]);
|
||||||
@ -916,13 +916,13 @@ function attachCallbacks(modal, callbacks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Update a modal form after data are received from the server.
|
||||||
|
* Manages POST requests until the form is successfully submitted.
|
||||||
|
*
|
||||||
|
* The server should respond with a JSON object containing a boolean value 'form_valid'
|
||||||
|
* Form submission repeats (after user interaction) until 'form_valid' = true
|
||||||
|
*/
|
||||||
function handleModalForm(url, options) {
|
function handleModalForm(url, options) {
|
||||||
/* Update a modal form after data are received from the server.
|
|
||||||
* Manages POST requests until the form is successfully submitted.
|
|
||||||
*
|
|
||||||
* The server should respond with a JSON object containing a boolean value 'form_valid'
|
|
||||||
* Form submission repeats (after user interaction) until 'form_valid' = true
|
|
||||||
*/
|
|
||||||
|
|
||||||
var modal = options.modal || '#modal-form';
|
var modal = options.modal || '#modal-form';
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
Chart,
|
Chart,
|
||||||
constructForm,
|
constructForm,
|
||||||
constructFormBody,
|
constructFormBody,
|
||||||
|
constructInput,
|
||||||
convertCurrency,
|
convertCurrency,
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatDecimal,
|
formatDecimal,
|
||||||
@ -14,6 +15,7 @@
|
|||||||
getFormFieldValue,
|
getFormFieldValue,
|
||||||
getTableData,
|
getTableData,
|
||||||
global_settings,
|
global_settings,
|
||||||
|
guessFieldType,
|
||||||
handleFormErrors,
|
handleFormErrors,
|
||||||
handleFormSuccess,
|
handleFormSuccess,
|
||||||
imageHoverIcon,
|
imageHoverIcon,
|
||||||
@ -42,6 +44,7 @@
|
|||||||
showMessage,
|
showMessage,
|
||||||
showModalSpinner,
|
showModalSpinner,
|
||||||
thumbnailImage,
|
thumbnailImage,
|
||||||
|
updateFieldValue,
|
||||||
withTitle,
|
withTitle,
|
||||||
wrapButtons,
|
wrapButtons,
|
||||||
yesNoLabel,
|
yesNoLabel,
|
||||||
@ -1281,6 +1284,137 @@ function loadSimplePartTable(table, url, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a set of fields for the PartParameter model.
|
||||||
|
* Note that the 'data' field changes based on the seleted parameter template
|
||||||
|
*/
|
||||||
|
function partParameterFields(options={}) {
|
||||||
|
|
||||||
|
let fields = {
|
||||||
|
part: {
|
||||||
|
hidden: true, // Part is set by the parent form
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
filters: {
|
||||||
|
ordering: 'name',
|
||||||
|
},
|
||||||
|
onEdit: function(value, name, field, opts) {
|
||||||
|
// Callback function when the parameter template is selected.
|
||||||
|
// We rebuild the 'data' field based on the template selection
|
||||||
|
|
||||||
|
let checkbox = false;
|
||||||
|
let choices = [];
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
// Request the parameter template data
|
||||||
|
inventreeGet(`{% url "api-part-parameter-template-list" %}${value}/`, {}, {
|
||||||
|
async: false,
|
||||||
|
success: function(response) {
|
||||||
|
if (response.checkbox) {
|
||||||
|
// Checkbox input
|
||||||
|
checkbox = true;
|
||||||
|
} else if (response.choices) {
|
||||||
|
// Select input
|
||||||
|
response.choices.split(',').forEach(function(choice) {
|
||||||
|
choice = choice.trim();
|
||||||
|
choices.push({
|
||||||
|
value: choice,
|
||||||
|
display_name: choice,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the current field element
|
||||||
|
let el = $(opts.modal).find('#id_data');
|
||||||
|
|
||||||
|
// Extract the current value from the field
|
||||||
|
let val = getFormFieldValue('data', {}, opts);
|
||||||
|
|
||||||
|
// Rebuild the field
|
||||||
|
let parameters = {};
|
||||||
|
|
||||||
|
if (checkbox) {
|
||||||
|
parameters.type = 'boolean';
|
||||||
|
} else if (choices.length > 0) {
|
||||||
|
parameters.type = 'choice';
|
||||||
|
parameters.choices = choices;
|
||||||
|
} else {
|
||||||
|
parameters.type = 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing_field_type = guessFieldType(el);
|
||||||
|
|
||||||
|
// If the field type has changed, we need to replace the field
|
||||||
|
if (existing_field_type != parameters.type) {
|
||||||
|
// Construct the new field
|
||||||
|
let new_field = constructInput('data', parameters, opts);
|
||||||
|
|
||||||
|
if (guessFieldType(el) == 'boolean') {
|
||||||
|
// Boolean fields are wrapped in a parent element
|
||||||
|
el.parent().replaceWith(new_field);
|
||||||
|
} else {
|
||||||
|
el.replaceWith(new_field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the field parameters in the form options
|
||||||
|
opts.fields.data.type = parameters.type;
|
||||||
|
updateFieldValue('data', val, parameters, opts);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.part) {
|
||||||
|
fields.part.value = options.part;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Launch a modal form for creating a new PartParameter object
|
||||||
|
*/
|
||||||
|
function createPartParameter(part_id, options={}) {
|
||||||
|
|
||||||
|
options.fields = partParameterFields({
|
||||||
|
part: part_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
options.processBeforeUpload = function(data) {
|
||||||
|
// Convert data to string
|
||||||
|
data.data = data.data.toString();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.method = 'POST';
|
||||||
|
options.title = '{% trans "Add Parameter" %}';
|
||||||
|
|
||||||
|
constructForm('{% url "api-part-parameter-list" %}', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Launch a modal form for editing a PartParameter object
|
||||||
|
*/
|
||||||
|
function editPartParameter(param_id, options={}) {
|
||||||
|
options.fields = partParameterFields();
|
||||||
|
options.title = '{% trans "Edit Parameter" %}';
|
||||||
|
|
||||||
|
options.processBeforeUpload = function(data) {
|
||||||
|
// Convert data to string
|
||||||
|
data.data = data.data.toString();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructForm(`{% url "api-part-parameter-list" %}${param_id}/`, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadPartParameterTable(table, options) {
|
function loadPartParameterTable(table, options) {
|
||||||
|
|
||||||
var params = options.params || {};
|
var params = options.params || {};
|
||||||
@ -1331,6 +1465,15 @@ function loadPartParameterTable(table, options) {
|
|||||||
switchable: false,
|
switchable: false,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
|
let template = row.template_detail;
|
||||||
|
|
||||||
|
if (template.checkbox) {
|
||||||
|
return yesNoLabel(value, {
|
||||||
|
pass: '{% trans "True" %}',
|
||||||
|
fail: '{% trans "False" %}',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (row.data_numeric && row.template_detail.units) {
|
if (row.data_numeric && row.template_detail.units) {
|
||||||
return `<span title='${row.data_numeric} ${row.template_detail.units}'>${row.data}</span>`;
|
return `<span title='${row.data_numeric} ${row.template_detail.units}'>${row.data}</span>`;
|
||||||
} else {
|
} else {
|
||||||
@ -1368,12 +1511,8 @@ function loadPartParameterTable(table, options) {
|
|||||||
$(table).find('.button-parameter-edit').click(function() {
|
$(table).find('.button-parameter-edit').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
constructForm(`{% url "api-part-parameter-list" %}${pk}/`, {
|
editPartParameter(pk, {
|
||||||
fields: {
|
refreshTable: table
|
||||||
data: {},
|
|
||||||
},
|
|
||||||
title: '{% trans "Edit Parameter" %}',
|
|
||||||
refreshTable: table,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1391,6 +1530,24 @@ function loadPartParameterTable(table, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return a list of fields for a part parameter template
|
||||||
|
*/
|
||||||
|
function partParameterTemplateFields() {
|
||||||
|
return {
|
||||||
|
name: {},
|
||||||
|
description: {},
|
||||||
|
units: {
|
||||||
|
icon: 'fa-ruler',
|
||||||
|
},
|
||||||
|
choices: {
|
||||||
|
icon: 'fa-th-list',
|
||||||
|
},
|
||||||
|
checkbox: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Construct a table showing a list of part parameter templates
|
* Construct a table showing a list of part parameter templates
|
||||||
*/
|
*/
|
||||||
@ -1410,6 +1567,8 @@ function loadPartParameterTemplateTable(table, options={}) {
|
|||||||
url: '{% url "api-part-parameter-template-list" %}',
|
url: '{% url "api-part-parameter-template-list" %}',
|
||||||
original: params,
|
original: params,
|
||||||
queryParams: filters,
|
queryParams: filters,
|
||||||
|
sortable: true,
|
||||||
|
sidePagination: 'server',
|
||||||
name: 'part-parameter-templates',
|
name: 'part-parameter-templates',
|
||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return '{% trans "No part parameter templates found" %}';
|
return '{% trans "No part parameter templates found" %}';
|
||||||
@ -1438,6 +1597,21 @@ function loadPartParameterTemplateTable(table, options={}) {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
switchable: true,
|
switchable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'checkbox',
|
||||||
|
title: '{% trans "Checkbox" %}',
|
||||||
|
sortable: false,
|
||||||
|
switchable: true,
|
||||||
|
formatter: function(value) {
|
||||||
|
return yesNoLabel(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'choices',
|
||||||
|
title: '{% trans "Choices" %}',
|
||||||
|
sortable: false,
|
||||||
|
switchable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
formatter: function(value, row, index, field) {
|
formatter: function(value, row, index, field) {
|
||||||
|
|
||||||
@ -1459,11 +1633,7 @@ function loadPartParameterTemplateTable(table, options={}) {
|
|||||||
constructForm(
|
constructForm(
|
||||||
`/api/part/parameter/template/${pk}/`,
|
`/api/part/parameter/template/${pk}/`,
|
||||||
{
|
{
|
||||||
fields: {
|
fields: partParameterTemplateFields(),
|
||||||
name: {},
|
|
||||||
units: {},
|
|
||||||
description: {},
|
|
||||||
},
|
|
||||||
title: '{% trans "Edit Part Parameter Template" %}',
|
title: '{% trans "Edit Part Parameter Template" %}',
|
||||||
refreshTable: table,
|
refreshTable: table,
|
||||||
}
|
}
|
||||||
|
@ -709,9 +709,28 @@ function getCompanyFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Return a dictionary of filters for the "PartParameter" table
|
||||||
|
function getPartParameterFilters() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Return a dictionary of filters for the "part parameter template" table
|
// Return a dictionary of filters for the "part parameter template" table
|
||||||
function getPartParameterTemplateFilters() {
|
function getPartParameterTemplateFilters() {
|
||||||
return {};
|
return {
|
||||||
|
checkbox: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Checkbox" %}',
|
||||||
|
},
|
||||||
|
has_choices: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Has Choices" %}',
|
||||||
|
},
|
||||||
|
has_units: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Has Units" %}',
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -747,6 +766,8 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
return getStockLocationFilters();
|
return getStockLocationFilters();
|
||||||
case 'parameters':
|
case 'parameters':
|
||||||
return getParametricPartTableFilters();
|
return getParametricPartTableFilters();
|
||||||
|
case 'part-parameters':
|
||||||
|
return getPartParameterFilters();
|
||||||
case 'part-parameter-templates':
|
case 'part-parameter-templates':
|
||||||
return getPartParameterTemplateFilters();
|
return getPartParameterTemplateFilters();
|
||||||
case 'parts':
|
case 'parts':
|
||||||
|
BIN
docs/docs/assets/images/part/parameter_template_edit.png
Normal file
BIN
docs/docs/assets/images/part/parameter_template_edit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 84 KiB |
@ -10,25 +10,41 @@ Part parameters are located in the "Parameters" tab, on each part detail page.
|
|||||||
There is no limit for the number of part parameters and they are fully customizable through the use of [parameters templates](#parameter-templates).
|
There is no limit for the number of part parameters and they are fully customizable through the use of [parameters templates](#parameter-templates).
|
||||||
|
|
||||||
Here is an example of parameters for a capacitor:
|
Here is an example of parameters for a capacitor:
|
||||||
|
|
||||||
{% with id="part_parameters_example", url="part/part_parameters_example.png", description="Part Parameters Example List" %}
|
{% with id="part_parameters_example", url="part/part_parameters_example.png", description="Part Parameters Example List" %}
|
||||||
{% include 'img.html' %}
|
{% include 'img.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
## Parameter Templates
|
## Parameter Templates
|
||||||
|
|
||||||
Parameter templates are used to define the different types of parameters which are available for use. These are edited via the [settings interface](../settings/global.md).
|
Parameter templates are used to define the different types of parameters which are available for use. The following attributes are defined for a parameter template:
|
||||||
|
|
||||||
|
| Attribute | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| Name | The name of the parameter template (*must be unique*) |
|
||||||
|
| Description | Optional description for the template |
|
||||||
|
| Units | Optional units field (*must be a valid [physical unit](#parameter-units)*) |
|
||||||
|
| Choices | A comma-separated list of valid choices for parameter values linked to this template. |
|
||||||
|
| Checkbox | If set, parameters linked to this template can only be assigned values *true* or *false* |
|
||||||
|
|
||||||
### Create Template
|
### Create Template
|
||||||
|
|
||||||
|
Parameter templates are created and edited via the [settings interface](../settings/global.md).
|
||||||
|
|
||||||
To create a template:
|
To create a template:
|
||||||
|
|
||||||
- Navigate to the "Settings" page
|
- Navigate to the "Settings" page
|
||||||
- Click on the "Parts" tab
|
- Click on the "Part Parameters" tab
|
||||||
- Scroll down to the "Part Parameter Templates" section
|
|
||||||
- Click on the "New Parameter" button
|
- Click on the "New Parameter" button
|
||||||
- Fill out the `Create Part Parameter Template` form: `Name` (required) and `Units` (optional) fields
|
- Fill out the `Create Part Parameter Template` form: `Name` (required) and `Units` (optional) fields
|
||||||
- Click on the "Submit" button.
|
- Click on the "Submit" button.
|
||||||
|
|
||||||
|
An existing template can be edited by clicking on the "Edit" button associated with that template:
|
||||||
|
|
||||||
|
{% with id="part_parameter_template", url="part/parameter_template_edit.png", description="Edit Parameter Template" %}
|
||||||
|
{% include 'img.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
### Create Parameter
|
### Create Parameter
|
||||||
|
|
||||||
After [creating a template](#create-template) or using the existing templates, you can add parameters to any part.
|
After [creating a template](#create-template) or using the existing templates, you can add parameters to any part.
|
||||||
@ -51,12 +67,6 @@ To access a category's parametric table, click on the "Parameters" tab within th
|
|||||||
{% include 'img.html' %}
|
{% include 'img.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
Below is an example of capacitor parametric table filtered with `Package Type = 0402`:
|
|
||||||
|
|
||||||
{% with id="parametric_table_example", url="part/parametric_table_example.png", description="Parametric Table Example" %}
|
|
||||||
{% include 'img.html' %}
|
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
### Sorting by Parameter Value
|
### Sorting by Parameter Value
|
||||||
|
|
||||||
The parametric parts table allows the returned parts to be sorted by particular parameter values. Click on the header of a particular parameter column to sort results by that parameter:
|
The parametric parts table allows the returned parts to be sorted by particular parameter values. Click on the header of a particular parameter column to sort results by that parameter:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user