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:
@ -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',
|
||||
|
@ -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':
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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':
|
||||
|
Reference in New Issue
Block a user