Feature/Tree picker (#5595)

* Show only the current modal over the backdrop, move others behind

* Added initial draft for tree picker

* Added filters to tree picker

* Added tree picker to more location fields

* Fixed bug with missing input group and filters side effect

* Added tree picker to part category inputs

* Added missing picker for part category parent input

* Fixed disabled items

* Fix js linting errors

* trigger: ci

* Bump api_version.py

* Update api_version.py
This commit is contained in:
Lukas 2023-10-04 15:11:49 +02:00 committed by GitHub
parent 3d8e3f1625
commit 9b4e1743c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 281 additions and 60 deletions

View File

@ -2,11 +2,14 @@
# InvenTree API version # InvenTree API version
INVENTREE_API_VERSION = 135 INVENTREE_API_VERSION = 136
""" """
Increment this API version number whenever there is a significant change to the API that any clients need to know about Increment this API version number whenever there is a significant change to the API that any clients need to know about
v136 -> 2023-09-23 : https://github.com/inventree/InvenTree/pull/5595
- Adds structural to StockLocation and PartCategory tree endpoints
v135 -> 2023-09-19 : https://github.com/inventree/InvenTree/pull/5569 v135 -> 2023-09-19 : https://github.com/inventree/InvenTree/pull/5569
- Adds location path detail to StockLocation and StockItem API endpoints - Adds location path detail to StockLocation and StockItem API endpoints
- Adds category path detail to PartCategory and Part API endpoints - Adds category path detail to PartCategory and Part API endpoints

View File

@ -1097,3 +1097,7 @@ a {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.large-treeview-icon {
font-size: 1em;
}

View File

@ -113,6 +113,7 @@ class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
'name', 'name',
'parent', 'parent',
'icon', 'icon',
'structural',
] ]

View File

@ -239,8 +239,17 @@
generateStocktakeReport({ generateStocktakeReport({
category: { category: {
{% if category %}value: {{ category.pk }},{% endif %} {% if category %}value: {{ category.pk }},{% endif %}
tree_picker: {
url: '{% url "api-part-category-tree" %}',
default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON,
},
},
location: {
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
location: {},
generate_report: {}, generate_report: {},
update_parts: {}, update_parts: {},
}); });

View File

@ -436,7 +436,12 @@
part: { part: {
value: {{ part.pk }} value: {{ part.pk }}
}, },
location: {}, location: {
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
},
generate_report: { generate_report: {
value: false, value: false,
}, },

View File

@ -775,6 +775,7 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
'name', 'name',
'parent', 'parent',
'icon', 'icon',
'structural',
] ]

View File

@ -650,6 +650,10 @@ $("#stock-return-from-customer").click(function() {
{% if item.part.default_location %} {% if item.part.default_location %}
value: {{ item.part.default_location.pk }}, value: {{ item.part.default_location.pk }},
{% endif %} {% endif %}
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
notes: { notes: {
icon: 'fa-sticky-note', icon: 'fa-sticky-note',

View File

@ -238,9 +238,18 @@
{% if stocktake_enable and roles.stocktake.add %} {% if stocktake_enable and roles.stocktake.add %}
$('#location-stocktake').click(function() { $('#location-stocktake').click(function() {
generateStocktakeReport({ generateStocktakeReport({
category: {}, category: {
tree_picker: {
url: '{% url "api-part-category-tree" %}',
default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON,
},
},
location: { location: {
{% if location %}value: {{ location.pk }},{% endif %} {% if location %}value: {{ location.pk }},{% endif %}
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
generate_report: {}, generate_report: {},
update_parts: {}, update_parts: {},

View File

@ -308,6 +308,10 @@ onPanelLoad('category', function() {
parameter_template: {}, parameter_template: {},
category: { category: {
icon: 'fa-sitemap', icon: 'fa-sitemap',
tree_picker: {
url: '{% url "api-part-category-tree" %}',
default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON,
},
}, },
default_value: {}, default_value: {},
}, },
@ -368,6 +372,10 @@ onPanelLoad('category', function() {
category: { category: {
icon: 'fa-sitemap', icon: 'fa-sitemap',
value: pk, value: pk,
tree_picker: {
url: '{% url "api-part-category-tree" %}',
default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON,
},
}, },
default_value: {}, default_value: {},
}, },
@ -453,8 +461,18 @@ onPanelLoad('stocktake', function() {
$('#btn-generate-stocktake').click(function() { $('#btn-generate-stocktake').click(function() {
generateStocktakeReport({ generateStocktakeReport({
part: {}, part: {},
category: {}, category: {
location: {}, tree_picker: {
url: '{% url "api-part-category-tree" %}',
default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON,
},
},
location: {
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
},
generate_report: {}, generate_report: {},
update_parts: {}, update_parts: {},
}); });

View File

@ -6,6 +6,7 @@
addSidebarHeader, addSidebarHeader,
addSidebarItem, addSidebarItem,
addSidebarLink, addSidebarLink,
generateTreeStructure,
enableBreadcrumbTree, enableBreadcrumbTree,
enableSidebar, enableSidebar,
onPanelLoad, onPanelLoad,
@ -147,45 +148,31 @@ function enableSidebar(label, options={}) {
} }
/** /**
* Enable support for breadcrumb tree navigation on this page * Generate nested tree structure for jquery treeview from flattened list of
* tree nodes with refs to their parents
* @param {Array} data flat tree data as list of objects
* @param {Object} options custom options
* @param {Function} options.processNode Function that can change the treeview node obj
* @param {Number} options.selected pk of the node that should be preselected
*/ */
function enableBreadcrumbTree(options) { function generateTreeStructure(data, options) {
const nodes = {};
var label = options.label; const roots = [];
let node = null;
if (!label) {
console.error('enableBreadcrumbTree called without supplying label');
return;
}
var filters = options.filters || {};
inventreeGet(
options.url,
filters,
{
success: function(data) {
// Data are returned from the InvenTree server as a flattened list;
// We need to convert this into a tree structure
var nodes = {};
var roots = [];
var node = null;
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
node = data[i]; node = data[i];
nodes[node.pk] = node; nodes[node.pk] = node;
node.selectable = false; node.selectable = false;
if (options.processNode) {
node = options.processNode(node);
}
node.state = { node.state = {
expanded: node.pk == options.selected, expanded: node.pk == options.selected,
selected: node.pk == options.selected, selected: node.pk == options.selected,
}; };
if (options.processNode) {
node = options.processNode(node);
}
} }
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
@ -210,6 +197,33 @@ function enableBreadcrumbTree(options) {
} }
} }
return roots;
}
/**
* Enable support for breadcrumb tree navigation on this page
*/
function enableBreadcrumbTree(options) {
var label = options.label;
if (!label) {
console.error('enableBreadcrumbTree called without supplying label');
return;
}
var filters = options.filters || {};
inventreeGet(
options.url,
filters,
{
success: function(data) {
// Data are returned from the InvenTree server as a flattened list;
// We need to convert this into a tree structure
const roots = generateTreeStructure(data, options);
$('#breadcrumb-tree').treeview({ $('#breadcrumb-tree').treeview({
data: roots, data: roots,
showTags: true, showTags: true,

View File

@ -605,6 +605,10 @@ function completeBuildOutputs(build_id, outputs, options={}) {
filters: { filters: {
structural: false, structural: false,
}, },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
notes: { notes: {
icon: 'fa-sticky-note', icon: 'fa-sticky-note',
@ -734,7 +738,11 @@ function scrapBuildOutputs(build_id, outputs, options={}) {
location: { location: {
filters: { filters: {
structural: false, structural: false,
} },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
notes: {}, notes: {},
discard_allocations: {}, discard_allocations: {},
@ -1926,7 +1934,11 @@ function autoAllocateStockToBuild(build_id, bom_items=[], options={}) {
value: options.location, value: options.location,
filters: { filters: {
structural: false, structural: false,
} },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
exclude_location: {}, exclude_location: {},
interchangeable: { interchangeable: {

View File

@ -19,6 +19,8 @@
showMessage, showMessage,
showModalSpinner, showModalSpinner,
toBool, toBool,
showQuestionDialog,
generateTreeStructure,
*/ */
/* exported /* exported
@ -2022,6 +2024,94 @@ function initializeRelatedField(field, fields, options={}) {
} }
}); });
} }
if(field.tree_picker) {
// construct button
const button = $(`<button class="input-group-text px-2"><i class="fas fa-external-link-alt"></i></button>`);
// insert open tree picker button after select
select.parent().find(".select2").after(button);
// save copy of filters, because of possible side effects
const filters = field.filters ? { ...field.filters } : {};
button.on("click", () => {
const tree_id = `${name}_tree`;
const title = '{% trans "Select" %}' + " " + options.actions[name].label;
const content = `
<div class="mb-1">
<div class="input-group mb-2">
<input class="form-control" type="text" id="${name}_tree_search" placeholder="{% trans "Search" %} ${options.actions[name].label}..." />
<button class="input-group-text" id="${name}_tree_search_btn"><i class="fas fa-search"></i></button>
</div>
<div id="${tree_id}" style="height: 65vh; overflow-y: auto;">
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status"></div>
</div>
</div>
</div>
`;
showQuestionDialog(title, content, {
accept_text: '{% trans "Select" %}',
accept: () => {
const selectedNode = $(`#${tree_id}`).treeview('getSelected');
if(selectedNode.length > 0) {
const url = `${field.api_url}/${selectedNode[0].pk}/`.replace('//', '/');
inventreeGet(url, field.filters || {}, {
success: function(data) {
setRelatedFieldData(name, data, options);
}
});
}
}
});
inventreeGet(field.tree_picker.url, {}, {
success: (data) => {
const current_value = getFormFieldValue(name, field, options);
const rootNodes = generateTreeStructure(data, {
selected: current_value,
processNode: (node) => {
node.selectable = true;
node.text = node.name;
// disable this node, if it doesn't match the filter criteria
for (const [k, v] of Object.entries(filters)) {
if (k in node && node[k] !== v) {
node.selectable = false;
node.color = "grey";
break;
}
}
return node;
}
});
$(`#${tree_id}`).treeview({
data: rootNodes,
expandIcon: 'fas fa-plus-square large-treeview-icon',
collapseIcon: 'fa fa-minus-square large-treeview-icon',
nodeIcon: field.tree_picker.defaultIcon,
color: "black",
});
}
});
$(`#${name}_tree_search_btn`).on("click", () => {
const searchValue = $(`#${name}_tree_search`).val();
$(`#${tree_id}`).treeview("search", [searchValue, {
ignoreCase: true,
exactMatch: false,
revealResults: true,
}]);
});
});
}
} }
@ -2244,7 +2334,7 @@ function constructField(name, parameters, options={}) {
html += `<div class='controls'>`; html += `<div class='controls'>`;
// Does this input deserve "extra" decorators? // Does this input deserve "extra" decorators?
var extra = (parameters.icon != null) || (parameters.prefix != null) || (parameters.prefixRaw != null); var extra = (parameters.icon != null) || (parameters.prefix != null) || (parameters.prefixRaw != null) || (parameters.tree_picker != null);
// Some fields can have 'clear' inputs associated with them // Some fields can have 'clear' inputs associated with them
if (!parameters.required && !parameters.read_only) { if (!parameters.required && !parameters.read_only) {
@ -2265,7 +2355,7 @@ function constructField(name, parameters, options={}) {
} }
if (extra) { if (extra) {
html += `<div class='input-group'>`; html += `<div class='input-group flex-nowrap'>`;
if (parameters.prefix) { if (parameters.prefix) {
html += `<span class='input-group-text'>${parameters.prefix}</span>`; html += `<span class='input-group-text'>${parameters.prefix}</span>`;
@ -2282,9 +2372,9 @@ function constructField(name, parameters, options={}) {
if (!parameters.required && !options.hideClearButton) { if (!parameters.required && !options.hideClearButton) {
html += ` html += `
<span class='input-group-text form-clear' id='clear_${field_name}' title='{% trans "Clear input" %}'> <button class='input-group-text form-clear' id='clear_${field_name}' title='{% trans "Clear input" %}'>
<span class='icon-red fas fa-backspace'></span> <span class='icon-red fas fa-backspace'></span>
</span>`; </button>`;
} }
html += `</div>`; // input-group html += `</div>`; // input-group

View File

@ -44,6 +44,9 @@ function createNewModal(options={}) {
if (modal_id >= id) { if (modal_id >= id) {
id = modal_id + 1; id = modal_id + 1;
} }
// move all other modals behind the backdrops
$(this).css('z-index', 1000);
}); });
var submitClass = options.submitClass || 'primary'; var submitClass = options.submitClass || 'primary';
@ -125,6 +128,9 @@ function createNewModal(options={}) {
// Automatically remove the modal when it is deleted! // Automatically remove the modal when it is deleted!
$(modal_name).on('hidden.bs.modal', function() { $(modal_name).on('hidden.bs.modal', function() {
$(modal_name).remove(); $(modal_name).remove();
// restore all modals before backdrop
$('.inventree-modal').last().css("z-index", 10000);
}); });
// Capture "enter" key input // Capture "enter" key input

View File

@ -128,6 +128,10 @@ function partFields(options={}) {
filters: { filters: {
structural: false, structural: false,
}, },
tree_picker: {
url: '{% url "api-part-category-tree" %}',
default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON,
},
}, },
name: {}, name: {},
IPN: {}, IPN: {},
@ -147,7 +151,11 @@ function partFields(options={}) {
icon: 'fa-sitemap', icon: 'fa-sitemap',
filters: { filters: {
structural: false, structural: false,
} },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
default_supplier: { default_supplier: {
icon: 'fa-building', icon: 'fa-building',
@ -296,6 +304,10 @@ function categoryFields(options={}) {
parent: { parent: {
help_text: '{% trans "Parent part category" %}', help_text: '{% trans "Parent part category" %}',
required: false, required: false,
tree_picker: {
url: '{% url "api-part-category-tree" %}',
default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON,
},
}, },
name: {}, name: {},
description: {}, description: {},
@ -303,7 +315,11 @@ function categoryFields(options={}) {
icon: 'fa-sitemap', icon: 'fa-sitemap',
filters: { filters: {
structural: false, structural: false,
} },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
default_keywords: { default_keywords: {
icon: 'fa-key', icon: 'fa-key',
@ -2185,7 +2201,12 @@ function setPartCategory(data, options={}) {
method: 'POST', method: 'POST',
preFormContent: html, preFormContent: html,
fields: { fields: {
category: {}, category: {
tree_picker: {
url: '{% url "api-part-category-tree" %}',
default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON,
},
},
}, },
processBeforeUpload: function(data) { processBeforeUpload: function(data) {
data.parts = parts; data.parts = parts;

View File

@ -1306,7 +1306,11 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) {
location: { location: {
filters: { filters: {
structural: false, structural: false,
} },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
}, },
preFormContent: html, preFormContent: html,

View File

@ -547,7 +547,11 @@ function receiveReturnOrderItems(order_id, line_items, options={}) {
location: { location: {
filters: { filters: {
strucutral: false, strucutral: false,
} },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
} }
}, },
confirm: true, confirm: true,

View File

@ -136,6 +136,10 @@ function stockLocationFields(options={}) {
parent: { parent: {
help_text: '{% trans "Parent stock location" %}', help_text: '{% trans "Parent stock location" %}',
required: false, required: false,
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
name: {}, name: {},
description: {}, description: {},
@ -323,6 +327,10 @@ function stockItemFields(options={}) {
filters: { filters: {
structural: false, structural: false,
}, },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
quantity: { quantity: {
help_text: '{% trans "Enter initial quantity for this stock item" %}', help_text: '{% trans "Enter initial quantity for this stock item" %}',
@ -878,7 +886,11 @@ function mergeStockItems(items, options={}) {
icon: 'fa-sitemap', icon: 'fa-sitemap',
filters: { filters: {
structural: false, structural: false,
} },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
notes: { notes: {
icon: 'fa-sticky-note', icon: 'fa-sticky-note',
@ -3095,7 +3107,11 @@ function uninstallStockItem(installed_item_id, options={}) {
icon: 'fa-sitemap', icon: 'fa-sitemap',
filters: { filters: {
structural: false, structural: false,
} },
tree_picker: {
url: '{% url "api-location-tree" %}',
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
},
}, },
note: { note: {
icon: 'fa-sticky-note', icon: 'fa-sticky-note',