2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +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:
Oliver
2023-06-01 07:20:11 +10:00
committed by GitHub
parent 2c05e3e74d
commit e21a5e62b8
16 changed files with 519 additions and 112 deletions

View File

@ -309,11 +309,7 @@ onPanelLoad('part-parameters', function() {
$("#new-param").click(function() {
constructForm('{% url "api-part-parameter-template-list" %}', {
fields: {
name: {},
units: {},
description: {},
},
fields: partParameterTemplateFields(),
method: 'POST',
title: '{% trans "Create Part Parameter Template" %}',
refreshTable: '#param-table',

View File

@ -18,6 +18,7 @@
showApiError,
showMessage,
showModalSpinner,
toBool,
*/
/* exported
@ -990,15 +991,17 @@ function updateFieldValue(name, value, field, options) {
return;
}
if (field.type == null) {
field.type = guessFieldType(el);
}
switch (field.type) {
case 'decimal':
// Strip trailing zeros
el.val(formatDecimal(value));
break;
case 'boolean':
if (value == true || value.toString().toLowerCase() == 'true') {
el.prop('checked');
}
el.prop('checked', toBool(value));
break;
case 'related field':
// 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
*
@ -1088,9 +1119,16 @@ function getFormFieldValue(name, field={}, options={}) {
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) {
case 'boolean':
value = el.is(':checked');
value = toBool(el.prop("checked"));
break;
case 'date':
case 'datetime':

View File

@ -40,15 +40,42 @@
*/
function yesNoLabel(value, options={}) {
var text = '';
var color = '';
/*
* Convert a value (which may be a string) to a boolean value
*
* @param {string} value: Input value
* @returns {boolean} true or false
*/
function toBool(value) {
if (value) {
text = '{% trans "YES" %}';
if (typeof value == 'string') {
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';
} else {
text = '{% trans "NO" %}';
text = options.fail || '{% trans "NO" %}';
color = 'bg-warning';
}

View File

@ -874,8 +874,8 @@ function insertActionButton(modal, options) {
}
}
/* Attach a provided list of buttons */
function attachButtons(modal, buttons) {
/* Attach a provided list of buttons */
for (var i = 0; i < buttons.length; 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) {
/* 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
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) {
/* Attach a provided list of callback functions */
for (var i = 0; i < callbacks.length; 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) {
/* 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';

View File

@ -6,6 +6,7 @@
Chart,
constructForm,
constructFormBody,
constructInput,
convertCurrency,
formatCurrency,
formatDecimal,
@ -14,6 +15,7 @@
getFormFieldValue,
getTableData,
global_settings,
guessFieldType,
handleFormErrors,
handleFormSuccess,
imageHoverIcon,
@ -42,6 +44,7 @@
showMessage,
showModalSpinner,
thumbnailImage,
updateFieldValue,
withTitle,
wrapButtons,
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) {
var params = options.params || {};
@ -1331,6 +1465,15 @@ function loadPartParameterTable(table, options) {
switchable: false,
sortable: true,
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) {
return `<span title='${row.data_numeric} ${row.template_detail.units}'>${row.data}</span>`;
} else {
@ -1368,12 +1511,8 @@ function loadPartParameterTable(table, options) {
$(table).find('.button-parameter-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`{% url "api-part-parameter-list" %}${pk}/`, {
fields: {
data: {},
},
title: '{% trans "Edit Parameter" %}',
refreshTable: table,
editPartParameter(pk, {
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
*/
@ -1410,6 +1567,8 @@ function loadPartParameterTemplateTable(table, options={}) {
url: '{% url "api-part-parameter-template-list" %}',
original: params,
queryParams: filters,
sortable: true,
sidePagination: 'server',
name: 'part-parameter-templates',
formatNoMatches: function() {
return '{% trans "No part parameter templates found" %}';
@ -1438,6 +1597,21 @@ function loadPartParameterTemplateTable(table, options={}) {
sortable: false,
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) {
@ -1459,11 +1633,7 @@ function loadPartParameterTemplateTable(table, options={}) {
constructForm(
`/api/part/parameter/template/${pk}/`,
{
fields: {
name: {},
units: {},
description: {},
},
fields: partParameterTemplateFields(),
title: '{% trans "Edit Part Parameter Template" %}',
refreshTable: table,
}

View File

@ -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
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();
case 'parameters':
return getParametricPartTableFilters();
case 'part-parameters':
return getPartParameterFilters();
case 'part-parameter-templates':
return getPartParameterTemplateFilters();
case 'parts':