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 = 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

View File

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

View File

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

View File

@ -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: {},
});

View File

@ -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,
},

View File

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

View File

@ -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',

View File

@ -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: {},

View File

@ -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: {},
});

View File

@ -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,

View File

@ -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: {

View File

@ -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 = $(`<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'>`;
// 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 += `<div class='input-group'>`;
html += `<div class='input-group flex-nowrap'>`;
if (parameters.prefix) {
html += `<span class='input-group-text'>${parameters.prefix}</span>`;
@ -2282,9 +2372,9 @@ function constructField(name, parameters, options={}) {
if (!parameters.required && !options.hideClearButton) {
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>`;
</button>`;
}
html += `</div>`; // input-group

View File

@ -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

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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',