2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-10 23:14:13 +00:00

Search refactor (#4505)

* Adds 'global search' endpoint which allows for a single API request for multi-model search

* Pass 'search' and 'offset' params through to list methods

* Refactor model renderer function selection

* Refactor existing javascript

* Update API version

* javascript cleanup

* Refactor model rendering code

- Pipe into a single function for consistent display
- Add link rendering support

* Improve UX for searching

* JS linting

* Fix bug caused by typo

* Fix link rendering for company

* Adds unit testing for global search endpoint
This commit is contained in:
Oliver
2023-03-20 07:21:27 +11:00
committed by GitHub
parent 373c8c9cc5
commit 4d8311682c
11 changed files with 656 additions and 549 deletions

View File

@ -60,6 +60,10 @@ function openSearchPanel() {
var panel = $('#offcanvas-search');
let search_input = panel.find('#search-input');
search_input.find('#search-input').val('');
search_input.focus();
clearSearchResults();
// Request user roles if we do not have them
@ -72,7 +76,7 @@ function openSearchPanel() {
}
// Callback for text input changed
panel.find('#search-input').on('keyup change', searchTextChanged);
search_input.on('keyup change', searchTextChanged);
// Callback for "clear search" button
panel.find('#search-clear').click(function(event) {
@ -80,7 +84,7 @@ function openSearchPanel() {
// Prevent this button from actually submitting the form
event.preventDefault();
panel.find('#search-input').val('');
search_input('#search-input').val('');
clearSearchResults();
});
@ -94,7 +98,9 @@ function openSearchPanel() {
var searchInputTimer = null;
var searchText = null;
var searchTextCurrent = null;
var searchQueries = [];
var searchQuery = null;
var searchResultTypes = [];
var searchRequest = null;
function searchTextChanged(event) {
@ -121,41 +127,40 @@ function updateSearch() {
searchTextCurrent = searchText;
// Cancel any previous AJAX requests
searchQueries.forEach(function(query) {
query.abort();
});
searchQueries = [];
// Cancel previous search request
if (searchRequest != null) {
searchRequest.abort();
searchRequest = null;
}
// Show the "searching" text
$('#offcanvas-search').find('#search-pending').show();
searchResultTypes = [];
// Construct base query
searchQuery = {
search: searchTextCurrent,
limit: user_settings.SEARCH_PREVIEW_RESULTS,
offset: 0,
};
// Search for 'part' results
if (checkPermission('part') && user_settings.SEARCH_PREVIEW_SHOW_PARTS) {
var params = {};
let filters = {};
if (user_settings.SEARCH_HIDE_INACTIVE_PARTS) {
// Return *only* active parts
params.active = true;
filters.active = true;
}
// Search for matching parts
addSearchQuery(
'part',
'{% trans "Parts" %}',
'{% url "api-part-list" %}',
params,
renderPart,
{
url: '/part',
}
);
addSearchQuery('part', '{% trans "Parts" %}', filters);
}
if (checkPermission('part') && checkPermission('purchase_order')) {
var params = {
let filters = {
part_detail: true,
supplier_detail: true,
manufacturer_detail: true,
@ -163,54 +168,26 @@ function updateSearch() {
if (user_settings.SEARCH_HIDE_INACTIVE_PARTS) {
// Return *only* active parts
params.active = true;
filters.active = true;
}
if (user_settings.SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS) {
addSearchQuery(
'supplierpart',
'{% trans "Supplier Parts" %}',
'{% url "api-supplier-part-list" %}',
params,
renderSupplierPart,
{
url: '/supplier-part',
}
);
addSearchQuery('supplierpart', '{% trans "Supplier Parts" %}', filters);
}
if (user_settings.SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS) {
addSearchQuery(
'manufacturerpart',
'{% trans "Manufacturer Parts" %}',
'{% url "api-manufacturer-part-list" %}',
params,
renderManufacturerPart,
{
url: '/manufacturer-part',
}
);
addSearchQuery('manufacturerpart', '{% trans "Manufacturer Parts" %}', filters);
}
}
if (checkPermission('part_category') && user_settings.SEARCH_PREVIEW_SHOW_CATEGORIES) {
// Search for matching part categories
addSearchQuery(
'category',
'{% trans "Part Categories" %}',
'{% url "api-part-category-list" %}',
{},
renderPartCategory,
{
url: '/part/category',
},
);
let filters = {};
addSearchQuery('partcategory', '{% trans "Part Categories" %}', filters);
}
if (checkPermission('stock') && user_settings.SEARCH_PREVIEW_SHOW_STOCK) {
// Search for matching stock items
var filters = {
let filters = {
part_detail: true,
location_detail: true,
};
@ -220,61 +197,27 @@ function updateSearch() {
filters.in_stock = true;
}
addSearchQuery(
'stock',
'{% trans "Stock Items" %}',
'{% url "api-stock-list" %}',
filters,
renderStockItem,
{
url: '/stock/item',
render_location_detail: true,
}
);
addSearchQuery('stockitem', '{% trans "Stock Items" %}', filters);
}
if (checkPermission('stock_location') && user_settings.SEARCH_PREVIEW_SHOW_LOCATIONS) {
// Search for matching stock locations
addSearchQuery(
'location',
'{% trans "Stock Locations" %}',
'{% url "api-location-list" %}',
{},
renderStockLocation,
{
url: '/stock/location',
}
);
let filters = {};
addSearchQuery('stocklocation', '{% trans "Stock Locations" %}', filters);
}
if (checkPermission('build') && user_settings.SEARCH_PREVIEW_SHOW_BUILD_ORDERS) {
// Search for matching build orders
addSearchQuery(
'build',
'{% trans "Build Orders" %}',
'{% url "api-build-list" %}',
{
part_detail: true,
},
renderBuild,
{
url: '/build',
}
);
let filters = {
part_detail: true
};
addSearchQuery('build', '{% trans "Build Orders" %}', filters);
}
if ((checkPermission('sales_order') || checkPermission('purchase_order')) && user_settings.SEARCH_PREVIEW_SHOW_COMPANIES) {
// Search for matching companies
addSearchQuery(
'company',
'{% trans "Companies" %}',
'{% url "api-company-list" %}',
{},
renderCompany,
{
url: '/company',
}
);
let filters = {};
addSearchQuery('company', '{% trans "Companies" %}', filters);
}
if (checkPermission('purchase_order') && user_settings.SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS) {
@ -287,17 +230,7 @@ function updateSearch() {
filters.outstanding = true;
}
// Search for matching purchase orders
addSearchQuery(
'purchaseorder',
'{% trans "Purchase Orders" %}',
'{% url "api-po-list" %}',
filters,
renderPurchaseOrder,
{
url: '/order/purchase-order',
}
);
addSearchQuery('purchaseorder', '{% trans "Purchase Orders" %}', filters);
}
if (checkPermission('sales_order') && user_settings.SEARCH_PREVIEW_SHOW_SALES_ORDERS) {
@ -311,23 +244,55 @@ function updateSearch() {
filters.outstanding = true;
}
// Search for matching sales orders
addSearchQuery(
'salesorder',
'{% trans "Sales Orders" %}',
'{% url "api-so-list" %}',
filters,
renderSalesOrder,
{
url: '/order/sales-order',
}
);
addSearchQuery('salesorder', '{% trans "Sales Orders" %}', filters);
}
// Wait until all the pending queries are completed
$.when.apply($, searchQueries).done(function() {
$('#offcanvas-search').find('#search-pending').hide();
});
let ctx = $('#offcanvas-search').find('#search-context');
ctx.html(`
<div class='alert alert-block alert-secondary'>
<span class='fas fa-spinner fa-spin'></span> <em>{% trans "Searching" %}</em>
</div>
`);
// Send off the search query
searchRequest = inventreePut(
'{% url "api-search" %}',
searchQuery,
{
method: 'POST',
success: function(response) {
let any_results = false;
searchResultTypes.forEach(function(resultType) {
if (resultType.key in response) {
let result = response[resultType.key];
if (result.count != null && result.count > 0 && result.results) {
addSearchResults(result.results, resultType);
any_results = true;
}
}
});
if (any_results) {
ctx.html('');
} else {
ctx.html(`
<div class='alert alert-block alert-warning'>
<span class='fas fa-exclamation-circle'></span> <em>{% trans "No results" %}</em>
</div>
`);
}
},
complete: function() {
// Hide the "pending" icon
$('#offcanvas-search').find('#search-pending').hide();
}
}
);
}
@ -335,12 +300,14 @@ function clearSearchResults() {
var panel = $('#offcanvas-search');
// Ensure the 'no results found' element is visible
panel.find('#search-no-results').show();
// Ensure that the 'searching' element is hidden
panel.find('#search-pending').hide();
panel.find('#search-context').html(`
<div class='alert alert-block alert-info'>
<span class='fas fa-search'></span> <em>{% trans "Enter search query" %}</em>
</div>
`);
// Delete any existing search results
panel.find('#search-results').empty();
@ -349,59 +316,47 @@ function clearSearchResults() {
}
function addSearchQuery(key, title, query_url, query_params, render_func, render_params={}) {
// Include current search term
query_params.search = searchTextCurrent;
// How many results to show in each group?
query_params.offset = 0;
query_params.limit = user_settings.SEARCH_PREVIEW_RESULTS;
// Do not display "pk" value for search results
render_params.render_pk = false;
/*
* Add an individual search query, with callback for rendering
*/
function addSearchQuery(key, title, query_params, render_params={}) {
// Add the result group to the panel
$('#offcanvas-search').find('#search-results').append(`
<div class='search-result-group-wrapper' id='search-results-wrapper-${key}'></div>
`);
searchQuery[key] = query_params;
var request = inventreeGet(
query_url,
query_params,
{
success: function(response) {
addSearchResults(
key,
response.results,
title,
render_func,
render_params,
);
}
},
);
// Add the query to the stack
searchQueries.push(request);
render_params.showImage = true;
render_params.showLink = true;
render_params.showLabels = true;
searchResultTypes.push({
key: key,
title: title,
renderer: getModelRenderer(key),
renderParams: render_params,
});
}
// Add a group of results to the list
function addSearchResults(key, results, title, renderFunc, renderParams={}) {
function addSearchResults(results, resultType) {
if (results.length == 0) {
// Do not display this group, as there are no results
return;
}
var panel = $('#offcanvas-search');
let panel = $('#offcanvas-search');
// Ensure the 'no results found' element is hidden
panel.find('#search-no-results').hide();
panel.find(`#search-results-wrapper-${key}`).append(`
let key = resultType.key;
let title = resultType.title;
let renderer = resultType.renderer;
let renderParams = resultType.renderParams;
// Add the result group to the panel
panel.find('#search-results').append(`
<div class='search-result-group-wrapper' id='search-results-wrapper-${key}'>
<div class='search-result-group' id='search-results-${key}'>
<div class='search-result-header' style='display: flex;'>
<h5>${title}</h5>
@ -418,17 +373,14 @@ function addSearchResults(key, results, title, renderFunc, renderParams={}) {
<div class='collapse search-result-list' id='search-result-list-${key}'>
</div>
</div>
</div>
`);
results.forEach(function(result) {
var pk = result.pk || result.id;
var html = renderFunc(key, result, renderParams);
if (renderParams.url) {
html = `<a href='${renderParams.url}/${pk}/'>` + html + `</a>`;
}
var html = renderer(result, renderParams);
var result_html = `
<div class='search-result-entry' id='search-result-${key}-${pk}'>