mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Form improvements (#5837)
* Added backend changes to support printing options * Pass printing options seperatly via kwargs for easier api refactor later * Implemented printing options in CUI * Fix js linting * Use translations for printing dialog * Support nested fields in CUI * Added docs * Remove plugin and template fields from send printing options * Fix docs * Added tests * Fix tests * Fix options response and added test for it * Fix tests * Bump api version * Update docs * Apply suggestions from code review * Fix api change date * Added dependent field and improved nested object fields on CUI * Fix: cui js style
This commit is contained in:
parent
17ae1a780d
commit
0d193d8cff
@ -10,6 +10,7 @@ from rest_framework.utils import model_meta
|
|||||||
import InvenTree.permissions
|
import InvenTree.permissions
|
||||||
import users.models
|
import users.models
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool
|
||||||
|
from InvenTree.serializers import DependentField
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
@ -242,6 +243,10 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
|
|
||||||
We take the regular DRF metadata and add our own unique flavor
|
We take the regular DRF metadata and add our own unique flavor
|
||||||
"""
|
"""
|
||||||
|
# Try to add the child property to the dependent field to be used by the super call
|
||||||
|
if self.label_lookup[field] == 'dependent field':
|
||||||
|
field.get_child(raise_exception=True)
|
||||||
|
|
||||||
# Run super method first
|
# Run super method first
|
||||||
field_info = super().get_field_info(field)
|
field_info = super().get_field_info(field)
|
||||||
|
|
||||||
@ -275,4 +280,11 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
else:
|
else:
|
||||||
field_info['api_url'] = model.get_api_url()
|
field_info['api_url'] = model.get_api_url()
|
||||||
|
|
||||||
|
# Add more metadata about dependent fields
|
||||||
|
if field_info['type'] == 'dependent field':
|
||||||
|
field_info['depends_on'] = field.depends_on
|
||||||
|
|
||||||
return field_info
|
return field_info
|
||||||
|
|
||||||
|
|
||||||
|
InvenTreeMetadata.label_lookup[DependentField] = "dependent field"
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from copy import deepcopy
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -94,6 +95,93 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class DependentField(serializers.Field):
|
||||||
|
"""A dependent field can be used to dynamically return child fields based on the value of other fields."""
|
||||||
|
child = None
|
||||||
|
|
||||||
|
def __init__(self, *args, depends_on, field_serializer, **kwargs):
|
||||||
|
"""A dependent field can be used to dynamically return child fields based on the value of other fields.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
This example adds two fields. If the client selects integer, an integer field will be shown, but if he
|
||||||
|
selects char, an char field will be shown. For any other value, nothing will be shown.
|
||||||
|
|
||||||
|
class TestSerializer(serializers.Serializer):
|
||||||
|
select_type = serializers.ChoiceField(choices=[
|
||||||
|
("integer", "Integer"),
|
||||||
|
("char", "Char"),
|
||||||
|
])
|
||||||
|
my_field = DependentField(depends_on=["select_type"], field_serializer="get_my_field")
|
||||||
|
|
||||||
|
def get_my_field(self, fields):
|
||||||
|
if fields["select_type"] == "integer":
|
||||||
|
return serializers.IntegerField()
|
||||||
|
if fields["select_type"] == "char":
|
||||||
|
return serializers.CharField()
|
||||||
|
"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.depends_on = depends_on
|
||||||
|
self.field_serializer = field_serializer
|
||||||
|
|
||||||
|
def get_child(self, raise_exception=False):
|
||||||
|
"""This method tries to extract the child based on the provided data in the request by the client."""
|
||||||
|
data = deepcopy(self.context["request"].data)
|
||||||
|
|
||||||
|
def visit_parent(node):
|
||||||
|
"""Recursively extract the data for the parent field/serializer in reverse."""
|
||||||
|
nonlocal data
|
||||||
|
|
||||||
|
if node.parent:
|
||||||
|
visit_parent(node.parent)
|
||||||
|
|
||||||
|
# only do for composite fields and stop right before the current field
|
||||||
|
if hasattr(node, "child") and node is not self and isinstance(data, dict):
|
||||||
|
data = data.get(node.field_name, None)
|
||||||
|
visit_parent(self)
|
||||||
|
|
||||||
|
# ensure that data is a dictionary and that a parent exists
|
||||||
|
if not isinstance(data, dict) or self.parent is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# check if the request data contains the dependent fields, otherwise skip getting the child
|
||||||
|
for f in self.depends_on:
|
||||||
|
if not data.get(f, None):
|
||||||
|
return
|
||||||
|
|
||||||
|
# partially validate the data for options requests that set raise_exception while calling .get_child(...)
|
||||||
|
if raise_exception:
|
||||||
|
validation_data = {k: v for k, v in data.items() if k in self.depends_on}
|
||||||
|
serializer = self.parent.__class__(context=self.context, data=validation_data, partial=True)
|
||||||
|
serializer.is_valid(raise_exception=raise_exception)
|
||||||
|
|
||||||
|
# try to get the field serializer
|
||||||
|
field_serializer = getattr(self.parent, self.field_serializer)
|
||||||
|
child = field_serializer(data)
|
||||||
|
|
||||||
|
if not child:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.child = child
|
||||||
|
self.child.bind(field_name='', parent=self)
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
"""This method tries to convert the data to an internal representation based on the defined to_internal_value method on the child."""
|
||||||
|
self.get_child()
|
||||||
|
if self.child:
|
||||||
|
return self.child.to_internal_value(data)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
"""This method tries to convert the data to representation based on the defined to_representation method on the child."""
|
||||||
|
self.get_child()
|
||||||
|
if self.child:
|
||||||
|
return self.child.to_representation(value)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeModelSerializer(serializers.ModelSerializer):
|
class InvenTreeModelSerializer(serializers.ModelSerializer):
|
||||||
"""Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""
|
"""Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""
|
||||||
|
|
||||||
|
@ -159,7 +159,8 @@ class LabelPrintMixin(LabelFilterMixin):
|
|||||||
# Check the request to determine if the user has selected a label printing plugin
|
# Check the request to determine if the user has selected a label printing plugin
|
||||||
plugin = self.get_plugin(self.request)
|
plugin = self.get_plugin(self.request)
|
||||||
|
|
||||||
serializer = plugin.get_printing_options_serializer(self.request)
|
kwargs.setdefault('context', self.get_serializer_context())
|
||||||
|
serializer = plugin.get_printing_options_serializer(self.request, *args, **kwargs)
|
||||||
|
|
||||||
# if no serializer is defined, return an empty serializer
|
# if no serializer is defined, return an empty serializer
|
||||||
if not serializer:
|
if not serializer:
|
||||||
@ -226,7 +227,7 @@ class LabelPrintMixin(LabelFilterMixin):
|
|||||||
raise ValidationError('Label has invalid dimensions')
|
raise ValidationError('Label has invalid dimensions')
|
||||||
|
|
||||||
# if the plugin returns a serializer, validate the data
|
# if the plugin returns a serializer, validate the data
|
||||||
if serializer := plugin.get_printing_options_serializer(request, data=request.data):
|
if serializer := plugin.get_printing_options_serializer(request, data=request.data, context=self.get_serializer_context()):
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
# At this point, we offload the label(s) to the selected plugin.
|
# At this point, we offload the label(s) to the selected plugin.
|
||||||
|
@ -298,7 +298,7 @@ function constructDeleteForm(fields, options) {
|
|||||||
* - closeText: Text for the "close" button
|
* - closeText: Text for the "close" button
|
||||||
* - fields: list of fields to display, with the following options
|
* - fields: list of fields to display, with the following options
|
||||||
* - filters: API query filters
|
* - filters: API query filters
|
||||||
* - onEdit: callback when field is edited
|
* - onEdit: callback or array of callbacks which get fired when field is edited
|
||||||
* - secondary: Define a secondary modal form for this field
|
* - secondary: Define a secondary modal form for this field
|
||||||
* - label: Specify custom label
|
* - label: Specify custom label
|
||||||
* - help_text: Specify custom help_text
|
* - help_text: Specify custom help_text
|
||||||
@ -493,6 +493,30 @@ function constructFormBody(fields, options) {
|
|||||||
html += options.header_html;
|
html += options.header_html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// process every field by recursively walking down nested fields
|
||||||
|
const processField = (name, field, optionsField) => {
|
||||||
|
if (field.type === "nested object") {
|
||||||
|
for (const [k, v] of Object.entries(field.children)) {
|
||||||
|
processField(`${name}__${k}`, v, optionsField.children[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === "dependent field") {
|
||||||
|
if(field.child) {
|
||||||
|
// copy child attribute from parameters to options
|
||||||
|
optionsField.child = field.child;
|
||||||
|
|
||||||
|
processField(name, field.child, optionsField.child);
|
||||||
|
} else {
|
||||||
|
delete optionsField.child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [k,v] of Object.entries(fields)) {
|
||||||
|
processField(k, v, options.fields[k]);
|
||||||
|
}
|
||||||
|
|
||||||
// Client must provide set of fields to be displayed,
|
// Client must provide set of fields to be displayed,
|
||||||
// otherwise *all* fields will be displayed
|
// otherwise *all* fields will be displayed
|
||||||
var displayed_fields = options.fields || fields;
|
var displayed_fields = options.fields || fields;
|
||||||
@ -599,14 +623,6 @@ function constructFormBody(fields, options) {
|
|||||||
|
|
||||||
var field = fields[field_name];
|
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);
|
html += constructField(field_name, field, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -810,7 +826,7 @@ function insertSecondaryButtons(options) {
|
|||||||
/*
|
/*
|
||||||
* Extract all specified form values as a single object
|
* Extract all specified form values as a single object
|
||||||
*/
|
*/
|
||||||
function extractFormData(fields, options) {
|
function extractFormData(fields, options, includeLocal = true) {
|
||||||
|
|
||||||
var data = {};
|
var data = {};
|
||||||
|
|
||||||
@ -823,6 +839,7 @@ function extractFormData(fields, options) {
|
|||||||
if (!field) continue;
|
if (!field) continue;
|
||||||
|
|
||||||
if (field.type == 'candy') continue;
|
if (field.type == 'candy') continue;
|
||||||
|
if (!includeLocal && field.localOnly) continue;
|
||||||
|
|
||||||
data[name] = getFormFieldValue(name, field, options);
|
data[name] = getFormFieldValue(name, field, options);
|
||||||
}
|
}
|
||||||
@ -1031,6 +1048,17 @@ function updateFieldValue(name, value, field, options) {
|
|||||||
}
|
}
|
||||||
// TODO - Specify an actual value!
|
// TODO - Specify an actual value!
|
||||||
break;
|
break;
|
||||||
|
case 'nested object':
|
||||||
|
for (const [k, v] of Object.entries(value)) {
|
||||||
|
if (!(k in field.children)) continue;
|
||||||
|
updateFieldValue(`${name}__${k}`, v, field.children[k], options);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'dependent field':
|
||||||
|
if (field.child) {
|
||||||
|
updateFieldValue(name, value, field.child, options);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'file upload':
|
case 'file upload':
|
||||||
case 'image upload':
|
case 'image upload':
|
||||||
break;
|
break;
|
||||||
@ -1165,6 +1193,17 @@ function getFormFieldValue(name, field={}, options={}) {
|
|||||||
case 'email':
|
case 'email':
|
||||||
value = sanitizeInputString(el.val());
|
value = sanitizeInputString(el.val());
|
||||||
break;
|
break;
|
||||||
|
case 'nested object':
|
||||||
|
value = {};
|
||||||
|
for (const [name, subField] of Object.entries(field.children)) {
|
||||||
|
value[name] = getFormFieldValue(subField.name, subField, options);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'dependent field':
|
||||||
|
if(!field.child) return undefined;
|
||||||
|
|
||||||
|
value = getFormFieldValue(name, field.child, options);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
value = el.val();
|
value = el.val();
|
||||||
break;
|
break;
|
||||||
@ -1449,19 +1488,28 @@ function handleFormErrors(errors, fields={}, options={}) {
|
|||||||
var field = fields[field_name] || {};
|
var field = fields[field_name] || {};
|
||||||
var field_errors = errors[field_name];
|
var field_errors = errors[field_name];
|
||||||
|
|
||||||
if ((field.type == 'nested object') && ('children' in field)) {
|
// for nested objects with children and dependent fields with a child defined, extract nested errors
|
||||||
|
if (((field.type == 'nested object') && ('children' in field)) || ((field.type == 'dependent field') && ('child' in field))) {
|
||||||
// Handle multi-level nested errors
|
// Handle multi-level nested errors
|
||||||
|
const handleNestedError = (parent_name, sub_field_errors) => {
|
||||||
|
for (const sub_field in sub_field_errors) {
|
||||||
|
const sub_sub_field_name = `${parent_name}__${sub_field}`;
|
||||||
|
const sub_sub_field_errors = sub_field_errors[sub_field];
|
||||||
|
|
||||||
for (var sub_field in field_errors) {
|
if (!first_error_field && sub_sub_field_errors && isFieldVisible(sub_sub_field_name, options)) {
|
||||||
var sub_field_name = `${field_name}__${sub_field}`;
|
first_error_field = sub_sub_field_name;
|
||||||
var sub_field_errors = field_errors[sub_field];
|
|
||||||
|
|
||||||
if (!first_error_field && sub_field_errors && isFieldVisible(sub_field_name, options)) {
|
|
||||||
first_error_field = sub_field_name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addFieldErrorMessage(sub_field_name, sub_field_errors, options);
|
// if the error is an object, its a nested object, recursively handle the errors
|
||||||
|
if (typeof sub_sub_field_errors === "object" && !Array.isArray(sub_sub_field_errors)) {
|
||||||
|
handleNestedError(sub_sub_field_name, sub_sub_field_errors)
|
||||||
|
} else {
|
||||||
|
addFieldErrorMessage(sub_sub_field_name, sub_sub_field_errors, options);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNestedError(field_name, field_errors);
|
||||||
} else if ((field.type == 'field') && ('child' in field)) {
|
} else if ((field.type == 'field') && ('child' in field)) {
|
||||||
// This is a "nested" array field
|
// This is a "nested" array field
|
||||||
handleNestedArrayErrors(errors, field_name, options);
|
handleNestedArrayErrors(errors, field_name, options);
|
||||||
@ -1556,7 +1604,7 @@ function addFieldCallbacks(fields, options) {
|
|||||||
|
|
||||||
var field = fields[name];
|
var field = fields[name];
|
||||||
|
|
||||||
if (!field || !field.onEdit) continue;
|
if (!field || field.type === "candy") continue;
|
||||||
|
|
||||||
addFieldCallback(name, field, options);
|
addFieldCallback(name, field, options);
|
||||||
}
|
}
|
||||||
@ -1564,17 +1612,36 @@ function addFieldCallbacks(fields, options) {
|
|||||||
|
|
||||||
|
|
||||||
function addFieldCallback(name, field, options) {
|
function addFieldCallback(name, field, options) {
|
||||||
|
const el = getFormFieldElement(name, options);
|
||||||
|
|
||||||
var el = getFormFieldElement(name, options);
|
if (field.onEdit) {
|
||||||
|
|
||||||
el.change(function() {
|
el.change(function() {
|
||||||
|
|
||||||
var value = getFormFieldValue(name, field, options);
|
var value = getFormFieldValue(name, field, options);
|
||||||
|
let onEditHandlers = field.onEdit;
|
||||||
|
|
||||||
field.onEdit(value, name, field, options);
|
if (!Array.isArray(onEditHandlers)) {
|
||||||
|
onEditHandlers = [onEditHandlers];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const onEdit of onEditHandlers) {
|
||||||
|
onEdit(value, name, field, options);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// attach field callback for nested fields
|
||||||
|
if(field.type === "nested object") {
|
||||||
|
for (const [c_name, c_field] of Object.entries(field.children)) {
|
||||||
|
addFieldCallback(`${name}__${c_name}`, c_field, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(field.type === "dependent field" && field.child) {
|
||||||
|
addFieldCallback(name, field.child, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function addClearCallbacks(fields, options) {
|
function addClearCallbacks(fields, options) {
|
||||||
|
|
||||||
@ -1727,6 +1794,11 @@ function initializeRelatedFields(fields, options={}) {
|
|||||||
|
|
||||||
if (!field || field.hidden) continue;
|
if (!field || field.hidden) continue;
|
||||||
|
|
||||||
|
initializeRelatedFieldsRecursively(field, fields, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeRelatedFieldsRecursively(field, fields, options) {
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'related field':
|
case 'related field':
|
||||||
initializeRelatedField(field, fields, options);
|
initializeRelatedField(field, fields, options);
|
||||||
@ -1734,11 +1806,22 @@ function initializeRelatedFields(fields, options={}) {
|
|||||||
case 'choice':
|
case 'choice':
|
||||||
initializeChoiceField(field, fields, options);
|
initializeChoiceField(field, fields, options);
|
||||||
break;
|
break;
|
||||||
|
case 'nested object':
|
||||||
|
for (const [c_name, c_field] of Object.entries(field.children)) {
|
||||||
|
if(!c_field.name) c_field.name = `${field.name}__${c_name}`;
|
||||||
|
initializeRelatedFieldsRecursively(c_field, field.children, options);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'dependent field':
|
||||||
|
if (field.child) {
|
||||||
|
if(!field.child.name) field.child.name = field.name;
|
||||||
|
initializeRelatedFieldsRecursively(field.child, fields, options);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -2346,7 +2429,7 @@ function constructField(name, parameters, options={}) {
|
|||||||
html += `<div id='div_id_${field_name}' class='${form_classes}' ${hover_title} ${css}>`;
|
html += `<div id='div_id_${field_name}' class='${form_classes}' ${hover_title} ${css}>`;
|
||||||
|
|
||||||
// Add a label
|
// Add a label
|
||||||
if (!options.hideLabels) {
|
if (!options.hideLabels && parameters.type !== "nested object" && parameters.type !== "dependent field") {
|
||||||
html += constructLabel(name, parameters);
|
html += constructLabel(name, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2501,6 +2584,12 @@ function constructInput(name, parameters, options={}) {
|
|||||||
case 'raw':
|
case 'raw':
|
||||||
func = constructRawInput;
|
func = constructRawInput;
|
||||||
break;
|
break;
|
||||||
|
case 'nested object':
|
||||||
|
func = constructNestedObject;
|
||||||
|
break;
|
||||||
|
case 'dependent field':
|
||||||
|
func = constructDependentField;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Unsupported field type!
|
// Unsupported field type!
|
||||||
break;
|
break;
|
||||||
@ -2780,6 +2869,129 @@ function constructRawInput(name, parameters) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a nested object input
|
||||||
|
*/
|
||||||
|
function constructNestedObject(name, parameters, options) {
|
||||||
|
let html = `
|
||||||
|
<div id="div_id_${name}" class='panel form-panel' style="margin-bottom: 0; padding-bottom: 0;">
|
||||||
|
<div class='panel-heading form-panel-heading'>
|
||||||
|
<div>
|
||||||
|
<h6 style='display: inline;'>${parameters.label}</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='panel-content form-panel-content' id="id_${name}">
|
||||||
|
`;
|
||||||
|
|
||||||
|
parameters.field_names = [];
|
||||||
|
|
||||||
|
for (const [key, field] of Object.entries(parameters.children)) {
|
||||||
|
const subFieldName = `${name}__${key}`;
|
||||||
|
field.name = subFieldName;
|
||||||
|
parameters.field_names.push(subFieldName);
|
||||||
|
|
||||||
|
html += constructField(subFieldName, field, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
html += "</div></div>";
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldByNestedPath(name, fields) {
|
||||||
|
if (typeof name === "string") {
|
||||||
|
name = name.split("__");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.length === 0) return fields;
|
||||||
|
|
||||||
|
if (fields.type === "nested object") fields = fields.children;
|
||||||
|
|
||||||
|
if (!(name[0] in fields)) return null;
|
||||||
|
let field = fields[name[0]];
|
||||||
|
|
||||||
|
if (field.type === "dependent field" && field.child) {
|
||||||
|
field = field.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFieldByNestedPath(name.slice(1), field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Construct a dependent field input
|
||||||
|
*/
|
||||||
|
function constructDependentField(name, parameters, options) {
|
||||||
|
// add onEdit handler to all fields this dependent field depends on
|
||||||
|
for (let d_field_name of parameters.depends_on) {
|
||||||
|
const d_field = getFieldByNestedPath([...name.split("__").slice(0, -1), d_field_name], options.fields);
|
||||||
|
if (!d_field) continue;
|
||||||
|
|
||||||
|
const onEdit = (value, name, field, options) => {
|
||||||
|
if(value === undefined) return;
|
||||||
|
|
||||||
|
// extract the current form data to include in OPTIONS request
|
||||||
|
const data = extractFormData(options.fields, options, false)
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: options.url,
|
||||||
|
type: "OPTIONS",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: "json",
|
||||||
|
accepts: { json: "application/json" },
|
||||||
|
success: (res) => {
|
||||||
|
const fields = res.actions[options.method];
|
||||||
|
|
||||||
|
// merge already entered values in the newly constructed form
|
||||||
|
options.data = extractFormData(options.fields, options);
|
||||||
|
|
||||||
|
// remove old submit handlers
|
||||||
|
$(options.modal).off('click', '#modal-form-submit');
|
||||||
|
|
||||||
|
if (options.method === "POST") {
|
||||||
|
constructCreateForm(fields, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.method === "PUT" || options.method === "PATCH") {
|
||||||
|
constructChangeForm(fields, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.method === "DELETE") {
|
||||||
|
constructDeleteForm(fields, options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (xhr) => showApiError(xhr, options.url)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// attach on edit handler
|
||||||
|
const originalOnEdit = d_field.onEdit;
|
||||||
|
d_field.onEdit = [onEdit];
|
||||||
|
|
||||||
|
if(typeof originalOnEdit === "function") {
|
||||||
|
d_field.onEdit.push(originalOnEdit);
|
||||||
|
} else if (Array.isArray(originalOnEdit)) {
|
||||||
|
// push old onEdit handlers, but omit the old
|
||||||
|
d_field.onEdit.push(...originalOnEdit.filter(h => h !== d_field._currentDependentFieldOnEdit));
|
||||||
|
}
|
||||||
|
|
||||||
|
// track current onEdit handler function
|
||||||
|
d_field._currentDependentFieldOnEdit = onEdit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// child is not specified already, return a dummy div with id so no errors can happen
|
||||||
|
if (!parameters.child) {
|
||||||
|
return `<div id="id_${name}" hidden></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy label to child if not already provided
|
||||||
|
if(!parameters.child.label) {
|
||||||
|
parameters.child.label = parameters.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// construct the provided child field
|
||||||
|
return constructField(name, parameters.child, options);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Construct a 'help text' div based on the field parameters
|
* Construct a 'help text' div based on the field parameters
|
||||||
|
@ -137,6 +137,11 @@ function printLabels(options) {
|
|||||||
|
|
||||||
// update form
|
// update form
|
||||||
updateForm(formOptions);
|
updateForm(formOptions);
|
||||||
|
|
||||||
|
// workaround to fix a bug where one cannot scroll after changing the plugin
|
||||||
|
// without opening and closing the select box again manually
|
||||||
|
$("#id__plugin").select2("open");
|
||||||
|
$("#id__plugin").select2("close");
|
||||||
}
|
}
|
||||||
|
|
||||||
const printingFormOptions = {
|
const printingFormOptions = {
|
||||||
|
Loading…
Reference in New Issue
Block a user