mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
3d8e3f1625
commit
9b4e1743c7
@ -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
|
||||
|
@ -1097,3 +1097,7 @@ a {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.large-treeview-icon {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
@ -113,6 +113,7 @@ class CategoryTree(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'name',
|
||||
'parent',
|
||||
'icon',
|
||||
'structural',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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: {},
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -775,6 +775,7 @@ class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
'name',
|
||||
'parent',
|
||||
'icon',
|
||||
'structural',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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: {},
|
||||
|
@ -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: {},
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user