2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-30 12:36:45 +00:00

Merge pull request #934 from SchrodingersGat/reactive-forms

Reactive forms
This commit is contained in:
Oliver 2020-08-27 00:12:26 +10:00 committed by GitHub
commit ce866c6d30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 308 additions and 95 deletions

View File

@ -1,7 +1,136 @@
function makeOption(id, title) {
function makeOption(text, value, title) {
/* Format an option for a select element
*/
return "<option value='" + id + "'>" + title + "</option>";
var html = `<option value='${value || text}'`;
if (title) {
html += ` title='${title}'`;
}
html += `>${text}</option>`;
return html;
}
function makeOptionsList(elements, textFunc, valueFunc, titleFunc) {
/*
* Programatically generate a list of <option> elements,
* from the (assumed array) of elements.
* For each element, we pass the element to the supplied functions,
* which (in turn) generate display / value / title values.
*
* Args:
* - elements: List of elements
* - textFunc: Function which takes an element and generates the text to be displayed
* - valueFunc: optional function which takes an element and generates the value
* - titleFunc: optional function which takes an element and generates a title
*/
var options = [];
elements.forEach(function(element) {
var text = textFunc(element);
var value = null;
var title = null;
if (valueFunc) {
value = valueFunc(element);
} else {
value = text;
}
if (titleFunc) {
title = titleFunc(element);
}
options.push(makeOption(text, value, title));
});
return options;
}
function setFieldOptions(fieldName, optionList, options={}) {
/* Set the options for a <select> field.
*
* Args:
* - fieldName: The name of the target field
* - Options: List of formatted <option> strings
* - append: If true, options will be appended, otherwise will replace existing options.
*/
var append = options.append || false;
var modal = options.modal || '#modal-form';
var field = getFieldByName(modal, fieldName);
var addEmptyOption = options.addEmptyOption || true;
// If not appending, clear out the field...
if (!append) {
field.find('option').remove();
}
if (addEmptyOption) {
// Add an 'empty' option at the top of the list
field.append(makeOption('---------', '', '---------'));
}
optionList.forEach(function(option) {
field.append(option);
});
}
function reloadFieldOptions(fieldName, options) {
/* Reload the options for a given field,
* using an AJAX request.
*
* Args:
* - fieldName: The name of the field
* - options:
* -- url: Query url
* -- params: Query params
* -- value: A function which takes a returned option and returns the 'value' (if not specified, the `pk` field is used)
* -- text: A function which takes a returned option and returns the 'text'
* -- title: A function which takes a returned option and returns the 'title' (optional!)
*/
inventreeGet(options.url, options.params, {
success: function(response) {
var opts = makeOptionsList(response,
function(item) {
return options.text(item);
},
function(item) {
if (options.value) {
return options.value(item);
} else {
// Fallback is to use the 'pk' field
return item.pk;
}
},
function(item) {
if (options.title) {
return options.title(item);
} else {
return null;
}
}
);
// Update the target field with the new options
setFieldOptions(fieldName, opts);
},
error: function(response) {
console.log("Error GETting field options");
}
});
}
@ -397,6 +526,13 @@ function injectModalForm(modal, form_html) {
}
function getFieldByName(modal, name) {
/* Find the field (with the given name) within the modal */
return $(modal).find(`#id_${name}`);
}
function insertNewItemButton(modal, options) {
/* Insert a button into a modal form, after a field label.
* Looks for a <label> tag inside the form with the attribute "for='id_<field>'"
@ -476,6 +612,39 @@ function attachSecondaries(modal, secondaries) {
}
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);
field.change(function() {
if (callback.action) {
// Run the callback function with the new value of the field!
callback.action(field.val(), field);
} else {
console.log(`Value changed for field ${callback.field} - ${field.val()}`);
}
});
}
function attachCallbacks(modal, callbacks) {
/* Attach a provided list of callback functions */
for (var i = 0; i < callbacks.length; i++) {
attachFieldCallback(modal, callbacks[i]);
}
}
function handleModalForm(url, options) {
/* Update a modal form after data are received from the server.
* Manages POST requests until the form is successfully submitted.
@ -575,6 +744,7 @@ function launchModalForm(url, options = {}) {
* no_post - If true, only display form data, hide submit button, and disallow POST
* after_render - Callback function to run after form is rendered
* secondary - List of secondary modals to attach
* callback - List of callback functions to attach to inputs
*/
var modal = options.modal || '#modal-form';
@ -615,6 +785,10 @@ function launchModalForm(url, options = {}) {
attachSecondaries(modal, options.secondary);
}
if (options.callback) {
attachCallbacks(modal, options.callback);
}
if (options.no_post) {
modalShowSubmitButton(modal, false);
} else {

View File

@ -104,12 +104,41 @@ class SupplierPartList(generics.ListCreateAPIView):
queryset = super().get_queryset()
return queryset
def filter_queryset(self, queryset):
"""
Custom filtering for the queryset.
"""
queryset = super().filter_queryset(queryset)
params = self.request.query_params
# Filter by manufacturer
manufacturer = params.get('manufacturer', None)
if manufacturer is not None:
queryset = queryset.filter(manufacturer=manufacturer)
# Filter by supplier
supplier = params.get('supplier', None)
if supplier is not None:
queryset = queryset.filter(supplier=supplier)
# Filter by EITHER manufacturer or supplier
company = self.request.query_params.get('company', None)
company = params.get('company', None)
if company is not None:
queryset = queryset.filter(Q(manufacturer=company) | Q(supplier=company))
# Filter by parent part?
part = params.get('part', None)
if part is not None:
queryset = queryset.filter(part=part)
return queryset
def get_serializer(self, *args, **kwargs):
@ -130,6 +159,11 @@ class SupplierPartList(generics.ListCreateAPIView):
except AttributeError:
pass
try:
kwargs['pretty'] = str2bool(self.request.query_params.get('pretty', None))
except AttributeError:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
@ -147,9 +181,6 @@ class SupplierPartList(generics.ListCreateAPIView):
]
filter_fields = [
'part',
'supplier',
'manufacturer',
]
search_fields = [

View File

@ -418,6 +418,10 @@ class SupplierPart(models.Model):
return [line.order for line in self.purchase_order_line_items.all().prefetch_related('order')]
@property
def pretty_name(self):
return str(self)
def __str__(self):
s = "{supplier} ({sku})".format(
sku=self.SKU,

View File

@ -80,13 +80,17 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
manufacturer_detail = CompanyBriefSerializer(source='manufacturer', many=False, read_only=True)
pretty_name = serializers.CharField(read_only=True)
def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False)
supplier_detail = kwargs.pop('supplier_detail', False)
manufacturer_detail = kwargs.pop('manufacturer_detail', False)
prettify = kwargs.pop('pretty', False)
super(SupplierPartSerializer, self).__init__(*args, **kwargs)
@ -99,6 +103,9 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
if manufacturer_detail is not True:
self.fields.pop('manufacturer_detail')
if prettify is not True:
self.fields.pop('pretty_name')
supplier = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_supplier=True))
manufacturer = serializers.PrimaryKeyRelatedField(queryset=Company.objects.filter(is_manufacturer=True))
@ -109,6 +116,7 @@ class SupplierPartSerializer(InvenTreeModelSerializer):
'pk',
'part',
'part_detail',
'pretty_name',
'supplier',
'supplier_detail',
'SKU',

View File

@ -44,23 +44,13 @@
});
$("#item-create").click(function() {
launchModalForm("{% url 'stock-item-create' %}", {
reload: true,
createNewStockItem({
data: {
part: {{ part.part.id }},
supplier_part: {{ part.id }},
},
secondary: [
{
field: 'location',
label: '{% trans "New Location" %}',
title: '{% trans "Create New Location" %}',
url: "{% url 'stock-location-create' %}",
}
]
reload: true,
});
return false;
});

View File

@ -25,14 +25,12 @@
{{ block.super }}
$('#add-stock-item').click(function () {
launchModalForm(
"{% url 'stock-item-create' %}",
{
reload: true,
data: {
part: {{ part.id }}
}
});
createNewStockItem({
reload: true,
data: {
part: {{ part.id }},
}
});
});
loadStockTable($("#stock-table"), {
@ -64,37 +62,12 @@
});
$('#item-create').click(function () {
launchModalForm("{% url 'stock-item-create' %}", {
createNewStockItem({
reload: true,
data: {
part: {{ part.id }}
},
secondary: [
{
field: 'part',
label: '{% trans "New Part" %}',
title: '{% trans "Create New Part" %}',
url: "{% url 'part-create' %}",
},
{
field: 'supplier_part',
label: '{% trans "New Supplier Part" %}',
title: '{% trans "Create new Supplier Part" %}',
url: "{% url 'supplier-part-create' %}",
data: {
part: {{ part.id }}
},
},
{
field: 'location',
label: '{% trans "New Location" %}',
title: '{% trans "Create New Location" %}',
url: "{% url 'stock-location-create' %}",
}
]
part: {{ part.id }},
}
});
return false;
});
{% endblock %}

View File

@ -320,15 +320,12 @@ $("#print-label").click(function() {
});
$("#stock-duplicate").click(function() {
launchModalForm(
"{% url 'stock-item-create' %}",
{
follow: true,
data: {
copy: {{ item.id }},
},
createNewStockItem({
follow: true,
data: {
copy: {{ item.id }},
}
);
});
});
$("#stock-edit").click(function () {

View File

@ -204,38 +204,13 @@
{% endif %}
$('#item-create').click(function () {
launchModalForm("{% url 'stock-item-create' %}",
{
follow: true,
data: {
{% if location %}
location: {{ location.id }}
{% endif %}
},
secondary: [
{
field: 'part',
label: 'New Part',
title: 'Create New Part',
url: "{% url 'part-create' %}",
},
{
field: 'supplier_part',
label: 'New Supplier Part',
title: 'Create new Supplier Part',
url: "{% url 'supplier-part-create' %}"
},
{
field: 'location',
label: 'New Location',
title: 'Create New Location',
url: "{% url 'stock-location-create' %}",
}
]
});
return false;
createNewStockItem({
data: {
{% if location %}
location: {{ location.id }}
{% endif %}
}
});
});
loadStockTable($("#stock-table"), {

View File

@ -1240,7 +1240,7 @@ class StockItemCreate(AjaxCreateView):
form.rebuild_layout()
# Hide the 'part' field (as a valid part is selected)
form.fields['part'].widget = HiddenInput()
# form.fields['part'].widget = HiddenInput()
# trackable parts get special consideration
if part.trackable:
@ -1267,6 +1267,11 @@ class StockItemCreate(AjaxCreateView):
# TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate
form.fields['supplier_part'].initial = all_parts[0].id
else:
# No Part has been selected!
# We must not provide *any* options for SupplierPart
form.fields['supplier_part'].queryset = SupplierPart.objects.none()
# Otherwise if the user has selected a SupplierPart, we know what Part they meant!
if form['supplier_part'].value() is not None:
pass

View File

@ -735,3 +735,59 @@ function loadStockTrackingTable(table, options) {
});
});
}
function createNewStockItem(options) {
/* Launch a modal form to create a new stock item.
*
* This is really just a helper function which calls launchModalForm,
* but it does get called a lot, so here we are ...
*/
// Add in some funky options
options.callback = [
{
field: 'part',
action: function(value) {
reloadFieldOptions(
'supplier_part',
{
url: "{% url 'api-supplier-part-list' %}",
params: {
part: value,
pretty: true,
},
text: function(item) {
return item.pretty_name;
}
}
);
}
},
];
options.secondary = [
{
field: 'part',
label: '{% trans "New Part" %}',
title: '{% trans "Create New Part" %}',
url: "{% url 'part-create' %}",
},
{
field: 'supplier_part',
label: '{% trans "New Supplier Part" %}',
title: '{% trans "Create new Supplier Part" %}',
url: "{% url 'supplier-part-create' %}"
},
{
field: 'location',
label: '{% trans "New Location" %}',
title: '{% trans "Create New Location" %}',
url: "{% url 'stock-location-create' %}",
},
];
launchModalForm("{% url 'stock-item-create' %}", options);
}