{% load i18n %} {% load inventree_extras %} /* globals createNewModal, inventreeFormDataUpload, inventreeGet, inventreePut, modalEnable, modalShowSubmitButton, renderBuild, renderCompany, renderManufacturerPart, renderOwner, renderPart, renderPartCategory, renderPartParameterTemplate, renderStockItem, renderStockLocation, renderSupplierPart, renderUser, showAlertOrCache, showApiError, */ /* exported clearFormInput, disableFormInput, enableFormInput, hideFormInput, setFormGroupVisibility, showFormInput, */ /** * * 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 * */ // Set global default theme for select2 $.fn.select2.defaults.set('theme', 'bootstrap-5'); /* * 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. */ function getApiEndpointOptions(url, callback) { if (!url) { return; } // Return the ajax request object $.ajax({ url: url, type: 'OPTIONS', contentType: 'application/json', dataType: 'json', accepts: { json: 'application/json', }, success: callback, error: function(xhr) { // TODO: Handle error console.log(`ERROR in getApiEndpointOptions at '${url}'`); showApiError(xhr, 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: * - */ function constructCreateForm(fields, options) { // 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; } } } // We should have enough information to create the form! 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: * - */ function constructChangeForm(fields, options) { // Request existing data from the API endpoint $.ajax({ url: options.url, data: options.params || {}, type: 'GET', contentType: 'application/json', dataType: 'json', accepts: { json: 'application/json', }, success: function(data) { // An optional function can be provided to process the returned results, // before they are rendered to the form if (options.processResults) { var processed = options.processResults(data, fields, options); // If the processResults function returns data, it will be stored if (processed) { data = processed; } } // 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; constructFormBody(fields, options); }, error: function(xhr) { // TODO: Handle error here console.log(`ERROR in constructChangeForm at '${options.url}'`); showApiError(xhr, options.url); } }); } /* * 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; } // 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(xhr) { // TODO: Handle error here console.log(`ERROR in constructDeleteForm at '${options.url}`); showApiError(xhr, options.url); } }); } /* * Request API OPTIONS data from the server, * and construct a modal form based on the response. * * url: API URL which defines form data * options: * - method: The HTTP method e.g. 'PUT', 'POST', 'DELETE' (default='PATCH') * - title: The form title * - submitText: Text for the "submit" button * - closeText: Text for the "close" button * - 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 * - 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 * - data: map of data to fill out field values with * - 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 * - confirm: Set to true to require a "confirm" button * - confirmText: Text for confirm button (default = "Confirm") * */ function constructForm(url, options) { // An "empty" form will be defined locally if (url == null) { constructFormBody({}, options); } options.fields = options.fields || {}; // Save the URL options.url = url; // Default HTTP method options.method = options.method || 'PATCH'; // Default "groups" definition options.groups = options.groups || {}; options.current_group = null; // Construct an "empty" data object if not provided if (!options.data) { options.data = {}; } // 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! */ switch (options.method) { case 'POST': if (canCreate(OPTIONS)) { constructCreateForm(OPTIONS.actions.POST, options); } else { // User does not have permission to POST to the endpoint showMessage('{% trans "Action Prohibited" %}', { style: 'danger', details: '{% trans "Create operation not allowed" %}', icon: 'fas fa-user-times', }); console.log(`'POST action unavailable at ${url}`); } break; case 'PUT': case 'PATCH': if (canChange(OPTIONS)) { constructChangeForm(OPTIONS.actions.PUT, options); } else { // User does not have permission to PUT/PATCH to the endpoint showMessage('{% trans "Action Prohibited" %}', { style: 'danger', details: '{% trans "Update operation not allowed" %}', icon: 'fas fa-user-times', }); console.log(`${options.method} action unavailable at ${url}`); } break; case 'DELETE': if (canDelete(OPTIONS)) { constructDeleteForm(OPTIONS.actions.DELETE, options); } else { // User does not have permission to DELETE to the endpoint showMessage('{% trans "Action Prohibited" %}', { style: 'danger', details: '{% trans "Delete operation not allowed" %}', icon: 'fas fa-user-times', }); 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 showMessage('{% trans "Action Prohibited" %}', { style: 'danger', details: '{% trans "View operation not allowed" %}', icon: 'fas fa-user-times', }); console.log(`GET action unavailable at ${url}`); } break; default: 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. */ 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; // Handle initial data overrides if (options.data) { for (const field in options.data) { if (field in fields) { fields[field].value = options.data[field]; } } } // Initialize an "empty" field for each specified field for (field in displayed_fields) { if (!(field in fields)) { fields[field] = {}; } } // Provide each field object with its own name for (field in fields) { fields[field].name = field; // If any "instance_filters" are defined for the endpoint, copy them across (overwrite) if (fields[field].instance_filters) { fields[field].filters = Object.assign(fields[field].filters || {}, fields[field].instance_filters); } 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); for (var opt in field_options) { var val = field_options[opt]; if (opt == 'filters') { // ignore filters (see above) } else if (opt == 'icon') { // Specify custom icon fields[field].prefix = ``; } else { fields[field][opt] = field_options[opt]; } } } } // Construct an ordered list of field names var field_names = []; for (var name in displayed_fields) { field_names.push(name); // Field not specified in the API, but the client wishes to add it! if (!(name in fields)) { fields[name] = displayed_fields[name]; } } // Push the ordered field names into the options, // allowing successive functions to access them. options.field_names = field_names; // Render selected fields for (var idx = 0; idx < field_names.length; idx++) { var field_name = field_names[idx]; var field = fields[field_name]; switch (field.type) { // Skip field types which are simply not supported case 'nested object': continue; default: break; } html += constructField(field_name, field, options); } if (options.current_group) { // Close out the current group html += ``; } // Create a new modal if one does not exists if (!options.modal) { options.modal = createNewModal(options); } var modal = options.modal; modalEnable(modal, true); // Insert generated form content $(modal).find('#form-content').html(html); if (options.preFormContent) { $(modal).find('#pre-form-content').html(options.preFormContent); } if (options.postFormContent) { $(modal).find('#post-form-content').html(options.postFormContent); } // 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'); 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); modalShowSubmitButton(modal, true); $(modal).on('click', '#modal-form-submit', function() { // Immediately disable the "submit" button, // to prevent the form being submitted multiple times! $(options.modal).find('#modal-form-submit').prop('disabled', true); // Run custom code before normal form submission if (options.beforeSubmit) { options.beforeSubmit(fields, options); } // Run custom code instead of normal form submission if (options.onSubmit) { options.onSubmit(fields, options); } else { submitFormData(fields, options); } }); initializeGroups(fields, options); if (options.afterRender) { // Custom callback function after form rendering options.afterRender(fields, options); } // Scroll to the top $(options.modal).find('.modal-form-content-wrapper').scrollTop(0); } // Add a "confirm" checkbox to the modal // The "submit" button will be disabled unless "confirm" is checked function insertConfirmButton(options) { var message = options.confirmMessage || '{% trans "Confirm" %}'; var html = `