InvenTree/InvenTree/templates/js/forms.js

1629 lines
43 KiB
JavaScript
Raw Normal View History

{% load i18n %}
{% load inventree_extras %}
/**
*
* This file contains code for rendering (and managing) HTML forms
* which are served via the django-drf API.
*
* The django DRF library provides an OPTIONS method for each API endpoint,
* which allows us to introspect the available fields at any given endpoint.
*
* The OPTIONS method provides the following information for each available field:
*
* - Field name
* - Field label (translated)
* - Field help text (translated)
* - Field type
* - Read / write status
* - Field required status
* - min_value / max_value
*
*/
/*
* Return true if the OPTIONS specify that the user
* can perform a GET method at the endpoint.
*/
function canView(OPTIONS) {
if ('actions' in OPTIONS) {
return ('GET' in OPTIONS.actions);
} else {
return false;
}
}
/*
* Return true if the OPTIONS specify that the user
* can perform a POST method at the endpoint
*/
function canCreate(OPTIONS) {
if ('actions' in OPTIONS) {
return ('POST' in OPTIONS.actions);
} else {
return false;
}
}
/*
* Return true if the OPTIONS specify that the user
* can perform a PUT or PATCH method at the endpoint
*/
function canChange(OPTIONS) {
if ('actions' in OPTIONS) {
return ('PUT' in OPTIONS.actions || 'PATCH' in OPTIONS.actions);
} else {
return false;
}
}
/*
* Return true if the OPTIONS specify that the user
* can perform a DELETE method at the endpoint
*/
function canDelete(OPTIONS) {
if ('actions' in OPTIONS) {
return ('DELETE' in OPTIONS.actions);
} else {
return false;
}
}
/*
* Get the API endpoint options at the provided URL,
* using a HTTP options request.
*/
2021-06-26 09:11:20 +00:00
function getApiEndpointOptions(url, callback, options) {
// Return the ajax request object
$.ajax({
url: url,
type: 'OPTIONS',
contentType: 'application/json',
dataType: 'json',
accepts: {
json: 'application/json',
},
success: callback,
error: function(request, status, error) {
// TODO: Handle error
console.log(`ERROR in getApiEndpointOptions at '${url}'`);
}
});
}
/*
* Construct a 'creation' (POST) form, to create a new model in the database.
*
* arguments:
* - fields: The 'actions' object provided by the OPTIONS endpoint
*
* options:
* -
*/
2021-06-26 09:11:20 +00:00
function constructCreateForm(fields, options) {
2021-06-27 11:58:22 +00:00
// Check if default values were provided for any fields
for (const name in fields) {
var field = fields[name];
var field_options = options.fields[name] || {};
// If a 'value' is not provided for the field,
if (field.value == null) {
if ('value' in field_options) {
// Client has specified the default value for the field
field.value = field_options.value;
} else if (field.default != null) {
// OPTIONS endpoint provided default value for this field
field.value = field.default;
}
2021-06-27 11:58:22 +00:00
}
}
// We should have enough information to create the form!
2021-06-26 09:11:20 +00:00
constructFormBody(fields, options);
}
/*
* Construct a 'change' (PATCH) form, to create a new model in the database.
*
* arguments:
* - fields: The 'actions' object provided by the OPTIONS endpoint
*
* options:
* -
*/
2021-06-26 09:11:20 +00:00
function constructChangeForm(fields, options) {
// Request existing data from the API endpoint
$.ajax({
2021-06-26 09:11:20 +00:00
url: options.url,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
accepts: {
json: 'application/json',
},
success: function(data) {
// Push existing 'value' to each field
for (const field in data) {
if (field in fields) {
fields[field].value = data[field];
}
}
// Store the entire data object
options.instance = data;
2021-06-26 09:11:20 +00:00
constructFormBody(fields, options);
},
error: function(request, status, error) {
// TODO: Handle error here
2021-06-30 02:03:32 +00:00
console.log(`ERROR in constructChangeForm at '${options.url}'`);
}
2021-06-30 02:03:32 +00:00
});
}
/*
* Construct a 'delete' form, to remove a model instance from the database.
*
* arguments:
* - fields: The 'actions' object provided by the OPTIONS request
* - options: The 'options' object provided by the client
*/
function constructDeleteForm(fields, options) {
// Force the "confirm" property if not set
if (!('confirm' in options)) {
options.confirm = true;
}
2021-06-30 02:03:32 +00:00
// Request existing data from the API endpoint
// This data can be used to render some information on the form
$.ajax({
url: options.url,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
accepts: {
json: 'application/json',
},
success: function(data) {
// Store the instance data
options.instance = data;
constructFormBody(fields, options);
},
error: function(request, status, error) {
// TODO: Handle error here
console.log(`ERROR in constructDeleteForm at '${options.url}`);
}
});
}
/*
* Request API OPTIONS data from the server,
* and construct a modal form based on the response.
*
2021-07-02 09:27:19 +00:00
* url: API URL which defines form data
* options:
2021-07-02 09:27:19 +00:00
* - method: The HTTP method e.g. 'PUT', 'POST', 'DELETE' (default='PATCH')
2021-06-26 04:30:14 +00:00
* - title: The form title
* - submitText: Text for the "submit" button
* - closeText: Text for the "close" button
2021-07-02 09:27:19 +00:00
* - fields: list of fields to display, with the following options
* - filters: API query filters
* - onEdit: callback when field is edited
* - secondary: Define a secondary modal form for this field
* - label: Specify custom label
* - help_text: Specify custom help_text
* - placeholder: Specify custom placeholder text
2021-07-02 09:27:19 +00:00
* - value: Specify initial value
* - hidden: Set to true to hide the field
* - icon: font-awesome icon to display before the field
* - prefix: Custom HTML prefix to display before the field
* - focus: Name of field to focus on when modal is displayed
* - preventClose: Set to true to prevent form from closing on success
* - onSuccess: callback function when form action is successful
* - follow: If a 'url' is provided by the API on success, redirect to it
* - redirect: A URL to redirect to after form success
* - reload: Set to true to reload the current page after form success
2021-07-02 09:27:19 +00:00
* - confirm: Set to true to require a "confirm" button
* - confirmText: Text for confirm button (default = "Confirm")
*
*/
2021-06-26 09:11:20 +00:00
function constructForm(url, options) {
// Save the URL
options.url = url;
2021-06-26 04:30:14 +00:00
// Default HTTP method
options.method = options.method || 'PATCH';
// Request OPTIONS endpoint from the API
getApiEndpointOptions(url, function(OPTIONS) {
/*
* Determine what "type" of form we want to construct,
* based on the requested action.
*
* First we must determine if the user has the correct permissions!
*/
2021-06-26 04:30:14 +00:00
switch (options.method) {
case 'POST':
if (canCreate(OPTIONS)) {
2021-06-26 09:11:20 +00:00
constructCreateForm(OPTIONS.actions.POST, options);
} else {
// User does not have permission to POST to the endpoint
showAlertDialog(
'{% trans "Action Prohibited" %}',
'{% trans "Create operation not allowed" %}'
);
console.log(`'POST action unavailable at ${url}`);
}
break;
case 'PUT':
case 'PATCH':
if (canChange(OPTIONS)) {
2021-06-26 09:11:20 +00:00
constructChangeForm(OPTIONS.actions.PUT, options);
} else {
// User does not have permission to PUT/PATCH to the endpoint
showAlertDialog(
'{% trans "Action Prohibited" %}',
'{% trans "Update operation not allowed" %}'
);
console.log(`${options.method} action unavailable at ${url}`);
}
break;
case 'DELETE':
if (canDelete(OPTIONS)) {
2021-06-30 02:03:32 +00:00
constructDeleteForm(OPTIONS.actions.DELETE, options);
} else {
// User does not have permission to DELETE to the endpoint
showAlertDialog(
'{% trans "Action Prohibited" %}',
'{% trans "Delete operation not allowed" %}'
);
console.log(`DELETE action unavailable at ${url}`);
}
break;
case 'GET':
if (canView(OPTIONS)) {
// TODO?
} else {
// User does not have permission to GET to the endpoint
showAlertDialog(
'{% trans "Action Prohibited" %}',
'{% trans "View operation not allowed" %}'
);
console.log(`GET action unavailable at ${url}`);
}
break;
default:
2021-06-26 07:54:14 +00:00
console.log(`constructForm() called with invalid method '${options.method}'`);
break;
}
});
}
/*
* Construct a modal form based on the provided options
*
* arguments:
* - fields: The endpoint description returned from the OPTIONS request
* - options: form options object provided by the client.
*/
2021-06-26 09:11:20 +00:00
function constructFormBody(fields, options) {
var html = '';
// Client must provide set of fields to be displayed,
// otherwise *all* fields will be displayed
var displayed_fields = options.fields || fields;
2021-06-26 09:49:56 +00:00
// Provide each field object with its own name
for(field in fields) {
fields[field].name = field;
2021-06-28 04:19:05 +00:00
var field_options = displayed_fields[field];
// Copy custom options across to the fields object
if (field_options) {
// Override existing query filters (if provided!)
fields[field].filters = Object.assign(fields[field].filters || {}, field_options.filters);
2021-06-28 05:10:17 +00:00
// Secondary modal options
fields[field].secondary = field_options.secondary;
// Edit callback
fields[field].onEdit = field_options.onEdit;
2021-06-28 11:38:42 +00:00
// Custom help_text
if (field_options.help_text) {
fields[field].help_text = field_options.help_text;
}
2021-06-29 15:07:57 +00:00
// Custom label
if (field_options.label) {
fields[field].label = field_options.label;
}
// Custom placeholder
if (field_options.placeholder) {
fields[field].placeholder = field_options.placeholder;
}
2021-06-28 11:38:42 +00:00
// Field prefix
if (field_options.prefix) {
fields[field].prefix = field_options.prefix;
} else if (field_options.icon) {
// Specify icon like 'fa-user'
fields[field].prefix = `<span class='fas ${field_options.icon}'></span>`;
}
fields[field].hidden = field_options.hidden;
2021-07-02 13:04:53 +00:00
if (field_options.read_only != null) {
fields[field].read_only = field_options.read_only;
}
2021-06-28 04:19:05 +00:00
}
2021-06-26 09:49:56 +00:00
}
// Construct an ordered list of field names
var field_names = [];
for (var name in displayed_fields) {
// Only push names which are actually in the set of fields
if (name in fields) {
field_names.push(name);
} else {
console.log(`WARNING: '${name}' does not match a valid field name.`);
}
}
// Push the ordered field names into the options,
// allowing successive functions to access them.
options.field_names = field_names;
// Render selected fields
2021-06-23 14:13:40 +00:00
for (var idx = 0; idx < field_names.length; idx++) {
var name = field_names[idx];
var field = fields[name];
2021-06-23 14:13:40 +00:00
switch (field.type) {
// Skip field types which are simply not supported
2021-06-23 14:13:40 +00:00
case 'nested object':
continue;
default:
break;
}
var f = constructField(name, field, options);
2021-06-23 12:25:53 +00:00
html += f;
2021-06-23 14:13:40 +00:00
}
// TODO: Dynamically create the modals,
// so that we can have an infinite number of stacks!
2021-06-26 09:11:20 +00:00
2021-07-02 01:06:52 +00:00
// Create a new modal if one does not exists
if (!options.modal) {
options.modal = createNewModal(options);
}
2021-06-26 09:11:20 +00:00
var modal = options.modal;
modalEnable(modal, true);
2021-07-02 01:06:52 +00:00
// Insert generated form content
$(modal).find('.modal-form-content').html(html);
2021-06-30 02:03:32 +00:00
// Clear any existing buttons from the modal
$(modal).find('#modal-footer-buttons').html('');
// Insert "confirm" button (if required)
if (options.confirm) {
insertConfirmButton(options);
}
// Display the modal
$(modal).modal('show');
2021-06-27 11:58:22 +00:00
updateFieldValues(fields, options);
// Setup related fields
initializeRelatedFields(fields, options);
// Attach edit callbacks (if required)
addFieldCallbacks(fields, options);
// Attach clear callbacks (if required)
addClearCallbacks(fields, options);
attachToggle(modal);
2021-06-25 03:23:29 +00:00
$(modal + ' .select2-container').addClass('select-full-width');
$(modal + ' .select2-container').css('width', '100%');
modalShowSubmitButton(modal, true);
2021-06-26 04:30:14 +00:00
2021-06-26 07:54:14 +00:00
$(modal).on('click', '#modal-form-submit', function() {
2021-06-26 09:11:20 +00:00
submitFormData(fields, options);
});
}
2021-06-26 07:54:14 +00:00
2021-06-30 02:03:32 +00:00
// Add a "confirm" checkbox to the modal
// The "submit" button will be disabled unless "confirm" is checked
function insertConfirmButton(options) {
2021-06-30 02:48:14 +00:00
var message = options.confirmMessage || '{% trans "Confirm" %}';
2021-06-30 02:03:32 +00:00
var confirm = `
<span style='float: left;'>
2021-06-30 02:48:14 +00:00
${message}
<input id='modal-confirm' name='confirm' type='checkbox'>
2021-06-30 02:03:32 +00:00
</span>`;
$(options.modal).find('#modal-footer-buttons').append(confirm);
2021-06-30 02:48:14 +00:00
// Disable the 'submit' button
$(options.modal).find('#modal-form-submit').prop('disabled', true);
// Trigger event
$(options.modal).find('#modal-confirm').change(function() {
var enabled = this.checked;
$(options.modal).find('#modal-form-submit').prop('disabled', !enabled);
});
2021-06-30 02:03:32 +00:00
}
2021-06-26 09:11:20 +00:00
/*
* Submit form data to the server.
*
*/
function submitFormData(fields, options) {
2021-06-26 07:54:14 +00:00
// Form data to be uploaded to the server
2021-06-29 15:04:39 +00:00
// Only used if file / image upload is required
var form_data = new FormData();
2021-06-26 09:11:20 +00:00
2021-06-29 15:04:39 +00:00
var data = {};
var has_files = false;
2021-06-26 09:11:20 +00:00
// Extract values for each field
options.field_names.forEach(function(name) {
var field = fields[name] || null;
if (field) {
2021-06-26 09:49:56 +00:00
var value = getFormFieldValue(name, field, options);
2021-06-26 04:30:14 +00:00
// Handle file inputs
if (field.type == 'image upload' || field.type == 'file upload') {
var field_el = $(options.modal).find(`#id_${name}`)[0];
var field_files = field_el.files;
if (field_files.length > 0) {
// One file per field, please!
var file = field_files[0];
form_data.append(name, file);
2021-06-29 15:04:39 +00:00
has_files = true;
}
} else {
// Normal field (not a file or image)
form_data.append(name, value);
2021-06-29 15:04:39 +00:00
data[name] = value;
}
2021-06-26 09:11:20 +00:00
} else {
console.log(`WARNING: Could not find field matching '${name}'`);
}
2021-06-26 07:54:14 +00:00
});
2021-06-26 09:11:20 +00:00
2021-06-29 15:04:39 +00:00
var upload_func = inventreePut;
if (has_files) {
upload_func = inventreeFormDataUpload;
data = form_data;
}
2021-06-26 09:11:20 +00:00
// Submit data
2021-06-29 15:04:39 +00:00
upload_func(
2021-06-26 09:11:20 +00:00
options.url,
2021-06-29 15:04:39 +00:00
data,
2021-06-26 09:11:20 +00:00
{
method: options.method,
success: function(response, status) {
2021-06-26 09:30:57 +00:00
handleFormSuccess(response, options);
2021-06-26 09:11:20 +00:00
},
error: function(xhr, status, thrownError) {
switch (xhr.status) {
2021-07-02 06:53:56 +00:00
case 400: // Bad request
2021-06-26 09:11:20 +00:00
handleFormErrors(xhr.responseJSON, fields, options);
break;
2021-07-02 06:53:56 +00:00
case 0: // No response
$(options.modal).modal('hide');
showAlertDialog(
'{% trans "No Response" %}',
'{% trans "No response from the InvenTree server" %}',
);
break;
case 401: // Not authenticated
$(options.modal).modal('hide');
showAlertDialog(
'{% trans "Error 401: Not Authenticated" %}',
'{% trans "Authentication credentials not supplied" %}',
);
break;
case 403: // Permission denied
$(options.modal).modal('hide');
showAlertDialog(
'{% trans "Error 403: Permission Denied" %}',
'{% trans "You do not have the required permissions to access this function" %}',
);
break;
case 404: // Resource not found
$(options.modal).modal('hide');
showAlertDialog(
'{% trans "Error 404: Resource Not Found" %}',
'{% trans "The requested resource could not be located on the server" %}',
);
break;
case 408: // Timeout
$(options.modal).modal('hide');
showAlertDialog(
'{% trans "Error 408: Timeout" %}',
'{% trans "Connection timeout while requesting data from server" %}',
);
break;
2021-06-26 09:11:20 +00:00
default:
2021-07-02 06:53:56 +00:00
$(options.modal).modal('hide');
showAlertDialog('{% trans "Error requesting form data" %}', renderErrorMessage(xhr));
2021-06-26 09:11:20 +00:00
console.log(`WARNING: Unhandled response code - ${xhr.status}`);
break;
}
}
}
);
}
2021-06-27 11:58:22 +00:00
/*
* Update (set) the field values based on the specified data.
*
* Iterate through each of the displayed fields,
* and set the 'val' attribute of each one.
*
*/
function updateFieldValues(fields, options) {
for (var idx = 0; idx < options.field_names.length; idx++) {
var name = options.field_names[idx];
var field = fields[name] || null;
if (field == null) { continue; }
var value = field.value;
if (value == null) {
value = field.default;
}
2021-06-27 11:58:22 +00:00
if (value == null) { continue; }
updateFieldValue(name, value, field, options);
}
}
2021-06-27 11:58:22 +00:00
function updateFieldValue(name, value, field, options) {
var el = $(options.modal).find(`#id_${name}`);
switch (field.type) {
case 'boolean':
el.prop('checked', value);
break;
case 'related field':
// Clear?
if (value == null && !field.required) {
el.val(null).trigger('change');
}
// TODO - Specify an actual value!
break;
case 'file upload':
case 'image upload':
break;
default:
el.val(value);
break;
2021-06-27 11:58:22 +00:00
}
}
2021-06-26 09:49:56 +00:00
/*
* Extract and field value before sending back to the server
*
* arguments:
* - name: The name of the field
* - field: The field specification provided from the OPTIONS request
* - options: The original options object provided by the client
*/
function getFormFieldValue(name, field, options) {
// Find the HTML element
var el = $(options.modal).find(`#id_${name}`);
var value = null;
2021-06-26 09:49:56 +00:00
switch (field.type) {
case 'boolean':
value = el.is(":checked");
break;
case 'date':
case 'datetime':
value = el.val();
// Ensure empty values are sent as nulls
if (!value || value.length == 0) {
value = null;
}
break;
2021-06-26 09:49:56 +00:00
default:
value = el.val();
break;
2021-06-26 09:49:56 +00:00
}
return value;
2021-06-26 09:49:56 +00:00
}
2021-06-26 09:30:57 +00:00
/*
* Handle successful form posting
*
* arguments:
* - response: The JSON response object from the server
* - options: The original options object provided by the client
*/
function handleFormSuccess(response, options) {
// Close the modal
if (!options.preventClose) {
// TODO: Actually just *delete* the modal,
// rather than hiding it!!
$(options.modal).modal('hide');
}
if (options.onSuccess) {
// Callback function
options.onSuccess(response, options);
2021-06-26 09:30:57 +00:00
}
if (options.follow && response.url) {
// Follow the returned URL
window.location.href = response.url;
} else if (options.reload) {
// Reload the current page
location.reload();
} else if (options.redirect) {
// Redirect to a specified URL
window.location.href = options.redirect;
}
2021-06-26 09:30:57 +00:00
}
2021-06-26 09:11:20 +00:00
/*
* Remove all error text items from the form
*/
function clearFormErrors(options) {
// Remove the individual error messages
$(options.modal).find('.form-error-message').remove();
// Remove the "has error" class
$(options.modal).find('.has-error').removeClass('has-error');
2021-07-02 01:13:24 +00:00
// Hide the 'non field errors'
$(options.modal).find('#non-field-errors').html('');
2021-06-26 09:11:20 +00:00
}
/*
* Display form error messages as returned from the server.
*
* arguments:
* - errors: The JSON error response from the server
* - fields: The form data object
* - options: Form options provided by the client
*/
function handleFormErrors(errors, fields, options) {
// Remove any existing error messages from the form
clearFormErrors(options);
var non_field_errors = $(options.modal).find('#non-field-errors');
non_field_errors.append(
`<div class='alert alert-block alert-danger'>
<b>{% trans "Form errors exist" %}</b>
</div>`
);
// Non-field errors?
if ('non_field_errors' in errors) {
var nfe = errors.non_field_errors;
for (var idx = 0; idx < nfe.length; idx++) {
var err = nfe[idx];
var html = `
<div class='alert alert-block alert-danger'>
${err}
</div>`;
non_field_errors.append(html);
}
}
2021-06-26 09:11:20 +00:00
for (field_name in errors) {
if (field_name in fields) {
// Add the 'has-error' class
$(options.modal).find(`#div_id_${field_name}`).addClass('has-error');
var field_dom = $(options.modal).find(`#errors-${field_name}`); // $(options.modal).find(`#id_${field_name}`);
2021-06-26 09:11:20 +00:00
var field_errors = errors[field_name];
// Add an entry for each returned error message
for (var idx = field_errors.length-1; idx >= 0; idx--) {
var error_text = field_errors[idx];
var html = `
<span id='error_${idx+1}_id_${field_name}' class='help-block form-error-message'>
<strong>${error_text}</strong>
</span>`;
field_dom.append(html);
2021-06-26 09:11:20 +00:00
}
} else {
console.log(`WARNING: handleFormErrors found no match for field '${field_name}'`);
}
}
}
/*
* Attach callbacks to specified fields,
* triggered after the field value is edited.
*
* Callback function is called with arguments (name, field, options)
*/
function addFieldCallbacks(fields, options) {
for (var idx = 0; idx < options.field_names.length; idx++) {
var name = options.field_names[idx];
var field = fields[name];
if (!field || !field.onEdit) continue;
addFieldCallback(name, field, options);
}
}
function addFieldCallback(name, field, options) {
$(options.modal).find(`#id_${name}`).change(function() {
field.onEdit(name, field, options);
});
}
function addClearCallbacks(fields, options) {
for (var idx = 0; idx < options.field_names.length; idx++) {
var name = options.field_names[idx];
var field = fields[name];
if (!field || field.required) continue;
addClearCallback(name, field, options);
}
}
function addClearCallback(name, field, options) {
$(options.modal).find(`#clear_${name}`).click(function() {
updateFieldValue(name, null, field, options);
});
}
2021-06-26 09:11:20 +00:00
function initializeRelatedFields(fields, options) {
2021-06-23 14:16:30 +00:00
var field_names = options.field_names;
2021-06-23 14:16:30 +00:00
for (var idx = 0; idx < field_names.length; idx++) {
var name = field_names[idx];
var field = fields[name] || null;
if (!field || field.hidden) continue;
switch (field.type) {
case 'related field':
initializeRelatedField(name, field, options);
break;
case 'choice':
initializeChoiceField(name, field, options);
break;
}
}
}
2021-06-28 05:10:17 +00:00
/*
* Add a button to launch a secondary modal, to create a new modal instance.
*
* arguments:
* - name: The name of the field
* - field: The field data object
* - options: The options object provided by the client
*/
function addSecondaryModal(name, field, options) {
var secondary = field.secondary;
2021-06-28 05:10:17 +00:00
var html = `
<span style='float: right;'>
<div type='button' class='btn btn-primary btn-secondary' title='${secondary.title || secondary.label}' id='btn-new-${name}'>
${secondary.label || secondary.title}
2021-06-28 05:10:17 +00:00
</div>
</span>`;
$(options.modal).find(`label[for="id_${name}"]`).append(html);
// TODO: Launch a callback
$(options.modal).find(`#btn-new-${name}`).click(function() {
if (secondary.callback) {
// A "custom" callback can be specified for the button
secondary.callback(field, options);
} else if (secondary.api_url) {
// By default, a new modal form is created, with the parameters specified
// The parameters match the "normal" form creation parameters
secondary.onSuccess = function(data, opts) {
2021-07-02 11:17:20 +00:00
setRelatedFieldData(name, data, options);
};
constructForm(secondary.api_url, secondary);
}
});
2021-06-28 05:10:17 +00:00
}
/*
* Initializea single related-field
*
* argument:
* - modal: DOM identifier for the modal window
* - name: name of the field e.g. 'location'
* - field: Field definition from the OPTIONS request
* - options: Original options object provided by the client
*/
function initializeRelatedField(name, field, options) {
if (!field.api_url) {
// TODO: Provide manual api_url option?
console.log(`Related field '${name}' missing 'api_url' parameter.`);
return;
}
// Find the select element and attach a select2 to it
2021-06-26 09:11:20 +00:00
var select = $(options.modal).find(`#id_${name}`);
2021-06-28 05:10:17 +00:00
// Add a button to launch a 'secondary' modal
if (field.secondary != null) {
addSecondaryModal(name, field, options);
}
// TODO: Add 'placeholder' support for entry select2 fields
// limit size for AJAX requests
var pageSize = options.pageSize || 25;
select.select2({
2021-07-02 11:36:41 +00:00
placeholder: '',
dropdownParent: $(options.modal),
dropdownAutoWidth: false,
ajax: {
url: field.api_url,
dataType: 'json',
delay: 250,
cache: true,
data: function(params) {
if (!params.page) {
offset = 0;
} else {
offset = (params.page - 1) * pageSize;
}
2021-06-28 04:19:05 +00:00
// Custom query filters can be specified against each field
var query = field.filters || {};
// Add search and pagination options
query.search = params.term;
query.offset = offset;
query.limit = pageSize;
return query;
2021-06-25 03:47:33 +00:00
},
processResults: function(response) {
// Convert the returned InvenTree data into select2-friendly format
var data = [];
var more = false;
if ('count' in response && 'results' in response) {
// Response is paginated
data = response.results;
// Any more data available?
if (response.next) {
more = true;
}
} else {
// Non-paginated response
data = response;
}
// Each 'row' must have the 'id' attribute
for (var idx = 0; idx < data.length; idx++) {
data[idx].id = data[idx].pk;
}
// Ref: https://select2.org/data-sources/formats
var results = {
results: data,
pagination: {
more: more,
}
};
return results;
},
},
templateResult: function(item, container) {
// Extract 'instance' data passed through from an initial value
// Or, use the raw 'item' data as a backup
var data = item;
if (item.element && item.element.instance) {
data = item.element.instance;
}
2021-06-29 02:48:56 +00:00
if (!data.pk) {
return $(searching());
}
// Custom formatting for the search results
if (field.model) {
// If the 'model' is specified, hand it off to the custom model render
var html = renderModelData(name, field.model, data, field, options);
return $(html);
} else {
// Return a simple renderering
console.log(`WARNING: templateResult() missing 'field.model' for '${name}'`);
return `${name} - ${item.id}`;
}
},
templateSelection: function(item, container) {
// Extract 'instance' data passed through from an initial value
// Or, use the raw 'item' data as a backup
var data = item;
if (item.element && item.element.instance) {
data = item.element.instance;
}
2021-06-29 02:48:56 +00:00
if (!data.pk) {
2021-07-02 11:36:41 +00:00
return field.placeholder || '';
2021-06-29 02:48:56 +00:00
return $(searching());
}
// Custom formatting for selected item
if (field.model) {
// If the 'model' is specified, hand it off to the custom model render
var html = renderModelData(name, field.model, data, field, options);
return $(html);
} else {
// Return a simple renderering
console.log(`WARNING: templateSelection() missing 'field.model' for '${name}'`);
return `${name} - ${item.id}`;
}
}
});
// If a 'value' is already defined, grab the model info from the server
if (field.value) {
var pk = field.value;
var url = `${field.api_url}/${pk}/`.replace('//', '/');
inventreeGet(url, {}, {
success: function(data) {
2021-07-02 11:17:20 +00:00
setRelatedFieldData(name, data, options);
}
});
}
}
/*
* Set the value of a select2 instace for a "related field",
* e.g. with data returned from a secondary modal
*
* arguments:
* - name: The name of the field
* - data: JSON data representing the model instance
* - options: The modal form specifications
*/
function setRelatedFieldData(name, data, options) {
2021-07-02 11:17:20 +00:00
var select = $(options.modal).find(`#id_${name}`);
2021-07-02 11:17:20 +00:00
var option = new Option(name, data.pk, true, true);
2021-07-02 11:17:20 +00:00
// Assign the JSON data to the 'instance' attribute,
// so we can access and render it later
option.instance = data;
2021-07-02 11:17:20 +00:00
select.append(option).trigger('change');
select.trigger({
type: 'select2:select',
params: {
data: data
}
});
2021-06-25 03:23:29 +00:00
}
function initializeChoiceField(name, field, options) {
var select = $(options.modal).find(`#id_${name}`);
select.select2({
dropdownAutoWidth: false,
dropdownParent: $(options.modal),
});
}
2021-06-29 02:48:56 +00:00
// Render a 'no results' element
function searching() {
return `<span>{% trans "Searching" %}...</span>`;
}
2021-06-25 03:23:29 +00:00
/*
* Render a "foreign key" model reference in a select2 instance.
* Allows custom rendering with access to the entire serialized object.
*
* arguments:
* - name: The name of the field e.g. 'location'
2021-06-25 03:23:29 +00:00
* - model: The name of the InvenTree model e.g. 'stockitem'
* - data: The JSON data representation of the modal instance (GET request)
* - parameters: The field definition (OPTIONS) request
* - options: Other options provided at time of modal creation by the client
*/
function renderModelData(name, model, data, parameters, options) {
2021-06-25 05:22:40 +00:00
if (!data) {
2021-07-02 11:36:41 +00:00
return parameters.placeholder || '';
2021-06-25 05:22:40 +00:00
}
// TODO: Implement this function for various models
var html = null;
var renderer = null;
// Find a custom renderer
switch (model) {
case 'company':
renderer = renderCompany;
break;
case 'stockitem':
renderer = renderStockItem;
break;
case 'stocklocation':
renderer = renderStockLocation;
break;
case 'part':
renderer = renderPart;
break;
case 'partcategory':
renderer = renderPartCategory;
break;
2021-07-02 07:11:07 +00:00
case 'supplierpart':
renderer = renderSupplierPart;
break;
2021-07-02 13:04:53 +00:00
case 'owner':
renderer = renderOwner;
default:
break;
}
if (renderer != null) {
html = renderer(name, data, parameters, options);
}
if (html != null) {
return html;
} else {
console.log(`ERROR: Rendering not implemented for model '${model}'`);
// Simple text rendering
2021-06-28 23:26:40 +00:00
return `${model} - ID ${data.id}`;
}
}
/*
* Construct a single form 'field' for rendering in a form.
*
* arguments:
* - name: The 'name' of the field
* - parameters: The field parameters supplied by the DRF OPTIONS method
*
* options:
* -
*
* The function constructs a fieldset which mostly replicates django "crispy" forms:
*
* - Field name
* - Field <input> (depends on specified field type)
* - Field description (help text)
* - Field errors
*/
2021-06-26 09:11:20 +00:00
function constructField(name, parameters, options) {
var field_name = `id_${name}`;
// Hidden inputs are rendered without label / help text / etc
if (parameters.hidden) {
return constructHiddenInput(name, parameters, options);
}
2021-06-23 12:37:45 +00:00
var form_classes = 'form-group';
if (parameters.errors) {
form_classes += ' has-error';
}
var html = `<div id='div_${field_name}' class='${form_classes}'>`;
// Add a label
html += constructLabel(name, parameters);
html += `<div class='controls'>`;
// Does this input deserve "extra" decorators?
var extra = parameters.prefix != null;
2021-06-23 12:25:53 +00:00
// Some fields can have 'clear' inputs associated with them
if (!parameters.required && !parameters.read_only) {
switch (parameters.type) {
case 'string':
case 'url':
case 'email':
case 'integer':
case 'float':
case 'decimal':
case 'related field':
case 'date':
extra = true;
break;
default:
break;
}
}
if (extra) {
html += `<div class='input-group'>`;
if (parameters.prefix) {
html += `<span class='input-group-addon'>${parameters.prefix}</span>`;
}
2021-06-28 11:38:42 +00:00
}
html += constructInput(name, parameters, options);
2021-06-23 12:37:45 +00:00
if (extra) {
if (!parameters.required) {
html += `
<span class='input-group-addon form-clear' id='clear_${name}' title='{% trans "Clear input" %}'>
2021-07-01 11:58:05 +00:00
<span class='icon-red fas fa-backspace'></span>
</span>`;
}
2021-06-28 11:38:42 +00:00
html += `</div>`; // input-group
}
// Div for error messages
html += `<div id='errors-${name}'></div>`;
2021-06-23 12:25:53 +00:00
if (parameters.help_text) {
html += constructHelpText(name, parameters, options);
}
2021-06-23 12:37:45 +00:00
html += `</div>`; // controls
html += `</div>`; // form-group
2021-06-23 12:25:53 +00:00
return html;
}
/*
* Construct a 'label' div
*
* arguments:
* - name: The name of the field
* - required: Is this a required field?
*/
function constructLabel(name, parameters) {
var label_classes = 'control-label';
if (parameters.required) {
label_classes += ' requiredField';
}
2021-06-23 12:25:53 +00:00
var html = `<label class='${label_classes}' for='id_${name}'>`;
2021-06-23 12:25:53 +00:00
if (parameters.label) {
html += `${parameters.label}`;
} else {
html += `${name}`;
}
if (parameters.required) {
2021-06-23 12:25:53 +00:00
html += `<span class='asteriskField'>*</span>`;
}
2021-06-23 12:25:53 +00:00
html += `</label>`;
return html;
}
/*
* Construct a form input based on the field parameters
*
* arguments:
* - name: The name of the field
* - parameters: Field parameters returned by the OPTIONS method
*
*/
2021-06-26 09:11:20 +00:00
function constructInput(name, parameters, options) {
var html = '';
var func = null;
switch (parameters.type) {
case 'boolean':
func = constructCheckboxInput;
break;
case 'string':
case 'url':
case 'email':
2021-06-23 13:27:02 +00:00
func = constructTextInput;
break;
case 'integer':
case 'float':
case 'decimal':
2021-06-23 13:18:36 +00:00
func = constructNumberInput;
break;
case 'choice':
2021-06-23 13:36:38 +00:00
func = constructChoiceInput;
break;
case 'related field':
func = constructRelatedFieldInput;
break;
case 'image upload':
case 'file upload':
func = constructFileUploadInput;
break;
case 'date':
func = constructDateInput;
break;
default:
// Unsupported field type!
break;
}
if (func != null) {
html = func(name, parameters, options);
} else {
console.log(`WARNING: Unhandled form field type: '${parameters.type}'`);
}
2021-06-23 12:25:53 +00:00
return html;
}
2021-06-23 12:25:53 +00:00
2021-06-23 13:18:36 +00:00
// Construct a set of default input options which apply to all input types
function constructInputOptions(name, classes, type, parameters) {
2021-06-23 13:18:36 +00:00
var opts = [];
2021-06-23 13:18:36 +00:00
opts.push(`id='id_${name}'`);
2021-06-23 13:18:36 +00:00
opts.push(`class='${classes}'`);
2021-06-23 13:18:36 +00:00
opts.push(`name='${name}'`);
2021-06-23 13:18:36 +00:00
opts.push(`type='${type}'`);
2021-06-23 14:16:30 +00:00
// Read only?
if (parameters.read_only) {
opts.push(`readonly=''`);
}
if (parameters.value) {
// Existing value?
opts.push(`value='${parameters.value}'`);
} else if (parameters.default) {
// Otherwise, a defualt value?
opts.push(`value='${parameters.default}'`);
}
2021-06-23 13:18:36 +00:00
// Maximum input length
if (parameters.max_length) {
opts.push(`maxlength='${parameters.max_length}'`);
}
2021-06-23 13:18:36 +00:00
// Minimum input length
if (parameters.min_length) {
opts.push(`minlength='${parameters.min_length}'`);
}
2021-06-23 13:18:36 +00:00
// Maximum value
if (parameters.max_value != null) {
opts.push(`max='${parameters.max_value}'`);
}
2021-06-23 13:18:36 +00:00
// Minimum value
if (parameters.min_value != null) {
opts.push(`min='${parameters.min_value}'`);
2021-06-23 12:25:53 +00:00
}
2021-06-23 13:18:36 +00:00
// Field is required?
if (parameters.required) {
2021-06-23 13:18:36 +00:00
opts.push(`required=''`);
}
2021-06-23 12:37:45 +00:00
2021-06-23 13:18:36 +00:00
// Placeholder?
if (parameters.placeholder) {
opts.push(`placeholder='${parameters.placeholder}'`);
}
2021-06-23 13:18:36 +00:00
return `<input ${opts.join(' ')}>`;
}
// Construct a "hidden" input
function constructHiddenInput(name, parameters, options) {
return constructInputOptions(
name,
'hiddeninput',
'hidden',
parameters
);
}
2021-06-23 13:18:36 +00:00
// Construct a "checkbox" input
2021-06-26 09:11:20 +00:00
function constructCheckboxInput(name, parameters, options) {
2021-06-23 13:18:36 +00:00
return constructInputOptions(
name,
'checkboxinput',
'checkbox',
parameters
);
}
// Construct a "text" input
2021-06-26 09:11:20 +00:00
function constructTextInput(name, parameters, options) {
2021-06-23 13:18:36 +00:00
2021-06-23 13:23:28 +00:00
var classes = '';
var type = '';
switch (parameters.type) {
default:
classes = 'textinput textInput form-control';
type = 'text';
break;
case 'url':
classes = 'urlinput form-control';
type = 'url';
break;
2021-06-23 13:27:02 +00:00
case 'email':
classes = 'emailinput form-control';
type = 'email';
break;
2021-06-23 13:23:28 +00:00
}
2021-06-23 13:18:36 +00:00
return constructInputOptions(
name,
2021-06-23 13:23:28 +00:00
classes,
type,
2021-06-23 13:18:36 +00:00
parameters
);
}
// Construct a "number" field
2021-06-26 09:11:20 +00:00
function constructNumberInput(name, parameters, options) {
2021-06-23 13:18:36 +00:00
return constructInputOptions(
name,
'numberinput form-control',
'number',
parameters
);
}
2021-06-23 13:36:38 +00:00
// Construct a "choice" input
2021-06-26 09:11:20 +00:00
function constructChoiceInput(name, parameters, options) {
2021-06-23 13:36:38 +00:00
var html = `<select id='id_${name}' class='select form-control' name='${name}'>`;
var choices = parameters.choices || [];
for (var idx = 0; idx < choices.length; idx++) {
var choice = choices[idx];
var selected = '';
if (parameters.value && parameters.value == choice.value) {
selected = ` selected=''`;
}
html += `<option value='${choice.value}'${selected}>`;
2021-06-23 13:36:38 +00:00
html += `${choice.display_name}`;
html += `</option>`;
}
html += `</select>`;
return html;
}
/*
* Construct a "related field" input.
* This will create a "select" input which will then, (after form is loaded),
* be converted into a select2 input.
* This will then be served custom data from the API (as required)...
*/
2021-06-26 09:11:20 +00:00
function constructRelatedFieldInput(name, parameters, options) {
var html = `<select id='id_${name}' class='select form-control' name='${name}'></select>`;
// Don't load any options - they will be filled via an AJAX request
return html;
}
/*
* Construct a field for file upload
*/
function constructFileUploadInput(name, parameters, options) {
var cls = 'clearablefileinput';
if (parameters.required) {
cls = 'fileinput';
}
return constructInputOptions(
name,
cls,
'file',
parameters
);
}
/*
* Construct a field for a date input
*/
function constructDateInput(name, parameters, options) {
return constructInputOptions(
name,
'dateinput form-control',
'date',
parameters
);
}
/*
* Construct a 'help text' div based on the field parameters
*
* arguments:
* - name: The name of the field
* - parameters: Field parameters returned by the OPTIONS method
*
*/
2021-06-26 09:11:20 +00:00
function constructHelpText(name, parameters, options) {
2021-06-26 09:30:57 +00:00
var html = `<div id='hint_id_${name}' class='help-block'><i>${parameters.help_text}</i></div>`;
return html;
}