From 9b4e1743c7ef95b8786d4b4c37fe7d9e692d0c06 Mon Sep 17 00:00:00 2001 From: Lukas <76838159+wolflu05@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:11:49 +0200 Subject: [PATCH] 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 --- InvenTree/InvenTree/api_version.py | 5 +- InvenTree/InvenTree/static/css/inventree.css | 4 + InvenTree/part/serializers.py | 1 + InvenTree/part/templates/part/category.html | 11 ++- InvenTree/part/templates/part/detail.html | 7 +- InvenTree/stock/serializers.py | 1 + .../stock/templates/stock/item_base.html | 4 + InvenTree/stock/templates/stock/location.html | 11 ++- .../InvenTree/settings/settings_staff_js.html | 22 ++++- InvenTree/templates/js/dynamic/nav.js | 96 ++++++++++-------- InvenTree/templates/js/translated/build.js | 16 ++- InvenTree/templates/js/translated/forms.js | 98 ++++++++++++++++++- InvenTree/templates/js/translated/modals.js | 6 ++ InvenTree/templates/js/translated/part.js | 27 ++++- .../templates/js/translated/purchase_order.js | 6 +- .../templates/js/translated/return_order.js | 6 +- InvenTree/templates/js/translated/stock.js | 20 +++- 17 files changed, 281 insertions(+), 60 deletions(-) diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index 2d7aefbc5f..3ccf0ba108 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -2,11 +2,14 @@ # 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 +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 - Adds location path detail to StockLocation and StockItem API endpoints - Adds category path detail to PartCategory and Part API endpoints diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 6a080cba01..912f482243 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -1097,3 +1097,7 @@ a { align-items: center; justify-content: space-between; } + +.large-treeview-icon { + font-size: 1em; +} diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index e3b2876eeb..5beb6562a2 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -113,6 +113,7 @@ class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer): 'name', 'parent', 'icon', + 'structural', ] diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 1faac776f1..0dc61364d0 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -239,8 +239,17 @@ generateStocktakeReport({ category: { {% 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: {}, update_parts: {}, }); diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 32bd4ac3a8..6f9a11147b 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -436,7 +436,12 @@ part: { value: {{ part.pk }} }, - location: {}, + location: { + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, + }, generate_report: { value: false, }, diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 9328c9ce1a..aaca689cc5 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -775,6 +775,7 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer): 'name', 'parent', 'icon', + 'structural', ] diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 4704cca24c..62f9914fa7 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -650,6 +650,10 @@ $("#stock-return-from-customer").click(function() { {% if item.part.default_location %} value: {{ item.part.default_location.pk }}, {% endif %} + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, notes: { icon: 'fa-sticky-note', diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index b9abdb040c..3d9c0854a0 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -238,9 +238,18 @@ {% if stocktake_enable and roles.stocktake.add %} $('#location-stocktake').click(function() { generateStocktakeReport({ - category: {}, + category: { + tree_picker: { + url: '{% url "api-part-category-tree" %}', + default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON, + }, + }, location: { {% if location %}value: {{ location.pk }},{% endif %} + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, generate_report: {}, update_parts: {}, diff --git a/InvenTree/templates/InvenTree/settings/settings_staff_js.html b/InvenTree/templates/InvenTree/settings/settings_staff_js.html index 7d8931d79c..093dacf113 100644 --- a/InvenTree/templates/InvenTree/settings/settings_staff_js.html +++ b/InvenTree/templates/InvenTree/settings/settings_staff_js.html @@ -308,6 +308,10 @@ onPanelLoad('category', function() { parameter_template: {}, category: { icon: 'fa-sitemap', + tree_picker: { + url: '{% url "api-part-category-tree" %}', + default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON, + }, }, default_value: {}, }, @@ -368,6 +372,10 @@ onPanelLoad('category', function() { category: { icon: 'fa-sitemap', value: pk, + tree_picker: { + url: '{% url "api-part-category-tree" %}', + default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON, + }, }, default_value: {}, }, @@ -453,8 +461,18 @@ onPanelLoad('stocktake', function() { $('#btn-generate-stocktake').click(function() { generateStocktakeReport({ part: {}, - category: {}, - location: {}, + category: { + 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: {}, update_parts: {}, }); diff --git a/InvenTree/templates/js/dynamic/nav.js b/InvenTree/templates/js/dynamic/nav.js index 41e3fcd57d..8ce792648b 100644 --- a/InvenTree/templates/js/dynamic/nav.js +++ b/InvenTree/templates/js/dynamic/nav.js @@ -6,6 +6,7 @@ addSidebarHeader, addSidebarItem, addSidebarLink, + generateTreeStructure, enableBreadcrumbTree, enableSidebar, onPanelLoad, @@ -146,6 +147,59 @@ function enableSidebar(label, options={}) { } +/** + * 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 generateTreeStructure(data, options) { + const nodes = {}; + const roots = []; + let node = null; + + for (var i = 0; i < data.length; i++) { + node = data[i]; + nodes[node.pk] = node; + node.selectable = false; + + node.state = { + expanded: node.pk == options.selected, + selected: node.pk == options.selected, + }; + + if (options.processNode) { + node = options.processNode(node); + } + } + + for (var i = 0; i < data.length; i++) { + node = data[i]; + + if (node.parent != null) { + if (nodes[node.parent].nodes) { + nodes[node.parent].nodes.push(node); + } else { + nodes[node.parent].nodes = [node]; + } + + if (node.state.expanded) { + while (node.parent != null) { + nodes[node.parent].state.expanded = true; + node = nodes[node.parent]; + } + } + + } else { + roots.push(node); + } + } + + return roots; +} + /** * Enable support for breadcrumb tree navigation on this page */ @@ -168,47 +222,7 @@ function enableBreadcrumbTree(options) { // 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++) { - node = data[i]; - nodes[node.pk] = node; - node.selectable = false; - - if (options.processNode) { - node = options.processNode(node); - } - - node.state = { - expanded: node.pk == options.selected, - selected: node.pk == options.selected, - }; - } - - for (var i = 0; i < data.length; i++) { - node = data[i]; - - if (node.parent != null) { - if (nodes[node.parent].nodes) { - nodes[node.parent].nodes.push(node); - } else { - nodes[node.parent].nodes = [node]; - } - - if (node.state.expanded) { - while (node.parent != null) { - nodes[node.parent].state.expanded = true; - node = nodes[node.parent]; - } - } - - } else { - roots.push(node); - } - } + const roots = generateTreeStructure(data, options); $('#breadcrumb-tree').treeview({ data: roots, diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 822f30de6b..b37c2fcab9 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -605,6 +605,10 @@ function completeBuildOutputs(build_id, outputs, options={}) { filters: { structural: false, }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, notes: { icon: 'fa-sticky-note', @@ -734,7 +738,11 @@ function scrapBuildOutputs(build_id, outputs, options={}) { location: { filters: { structural: false, - } + }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, notes: {}, discard_allocations: {}, @@ -1926,7 +1934,11 @@ function autoAllocateStockToBuild(build_id, bom_items=[], options={}) { value: options.location, filters: { structural: false, - } + }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, exclude_location: {}, interchangeable: { diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 950fec6a5d..bfa2ef73e8 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -19,6 +19,8 @@ showMessage, showModalSpinner, toBool, + showQuestionDialog, + generateTreeStructure, */ /* exported @@ -2022,6 +2024,94 @@ function initializeRelatedField(field, fields, options={}) { } }); } + + if(field.tree_picker) { + // construct button + const 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 = ` +
+
+ + +
+ +
+
+
+
+
+
+ `; + 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 += `
`; // 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 if (!parameters.required && !parameters.read_only) { @@ -2265,7 +2355,7 @@ function constructField(name, parameters, options={}) { } if (extra) { - html += `
`; + html += `
`; if (parameters.prefix) { html += `${parameters.prefix}`; @@ -2282,9 +2372,9 @@ function constructField(name, parameters, options={}) { if (!parameters.required && !options.hideClearButton) { html += ` - + `; } html += `
`; // input-group diff --git a/InvenTree/templates/js/translated/modals.js b/InvenTree/templates/js/translated/modals.js index 2eff199f54..16d823e593 100644 --- a/InvenTree/templates/js/translated/modals.js +++ b/InvenTree/templates/js/translated/modals.js @@ -44,6 +44,9 @@ function createNewModal(options={}) { if (modal_id >= id) { id = modal_id + 1; } + + // move all other modals behind the backdrops + $(this).css('z-index', 1000); }); var submitClass = options.submitClass || 'primary'; @@ -125,6 +128,9 @@ function createNewModal(options={}) { // Automatically remove the modal when it is deleted! $(modal_name).on('hidden.bs.modal', function() { $(modal_name).remove(); + + // restore all modals before backdrop + $('.inventree-modal').last().css("z-index", 10000); }); // Capture "enter" key input diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 92b7b76a6c..48b8c627a0 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -128,6 +128,10 @@ function partFields(options={}) { filters: { structural: false, }, + tree_picker: { + url: '{% url "api-part-category-tree" %}', + default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON, + }, }, name: {}, IPN: {}, @@ -147,7 +151,11 @@ function partFields(options={}) { icon: 'fa-sitemap', filters: { structural: false, - } + }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, default_supplier: { icon: 'fa-building', @@ -296,6 +304,10 @@ function categoryFields(options={}) { parent: { help_text: '{% trans "Parent part category" %}', required: false, + tree_picker: { + url: '{% url "api-part-category-tree" %}', + default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON, + }, }, name: {}, description: {}, @@ -303,7 +315,11 @@ function categoryFields(options={}) { icon: 'fa-sitemap', filters: { structural: false, - } + }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, default_keywords: { icon: 'fa-key', @@ -2185,7 +2201,12 @@ function setPartCategory(data, options={}) { method: 'POST', preFormContent: html, fields: { - category: {}, + category: { + tree_picker: { + url: '{% url "api-part-category-tree" %}', + default_icon: global_settings.PART_CATEGORY_DEFAULT_ICON, + }, + }, }, processBeforeUpload: function(data) { data.parts = parts; diff --git a/InvenTree/templates/js/translated/purchase_order.js b/InvenTree/templates/js/translated/purchase_order.js index 911f2edea6..67bce86f42 100644 --- a/InvenTree/templates/js/translated/purchase_order.js +++ b/InvenTree/templates/js/translated/purchase_order.js @@ -1306,7 +1306,11 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { location: { filters: { structural: false, - } + }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, }, preFormContent: html, diff --git a/InvenTree/templates/js/translated/return_order.js b/InvenTree/templates/js/translated/return_order.js index 4542a7d6da..260b21ee08 100644 --- a/InvenTree/templates/js/translated/return_order.js +++ b/InvenTree/templates/js/translated/return_order.js @@ -547,7 +547,11 @@ function receiveReturnOrderItems(order_id, line_items, options={}) { location: { filters: { strucutral: false, - } + }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, } }, confirm: true, diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 4ac3afca7b..38f250c189 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -136,6 +136,10 @@ function stockLocationFields(options={}) { parent: { help_text: '{% trans "Parent stock location" %}', required: false, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, name: {}, description: {}, @@ -323,6 +327,10 @@ function stockItemFields(options={}) { filters: { structural: false, }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, quantity: { help_text: '{% trans "Enter initial quantity for this stock item" %}', @@ -878,7 +886,11 @@ function mergeStockItems(items, options={}) { icon: 'fa-sitemap', filters: { structural: false, - } + }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, notes: { icon: 'fa-sticky-note', @@ -3095,7 +3107,11 @@ function uninstallStockItem(installed_item_id, options={}) { icon: 'fa-sitemap', filters: { structural: false, - } + }, + tree_picker: { + url: '{% url "api-location-tree" %}', + default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON, + }, }, note: { icon: 'fa-sticky-note',