2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-19 05:25:42 +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 647 additions and 540 deletions

View File

@ -9,18 +9,7 @@
global_settings,
modalEnable,
modalShowSubmitButton,
renderBuild,
renderCompany,
renderGroup,
renderManufacturerPart,
renderOwner,
renderPart,
renderPartCategory,
renderPartParameterTemplate,
renderStockItem,
renderStockLocation,
renderSupplierPart,
renderUser,
getModelRenderer,
showAlertOrCache,
showApiError,
*/
@ -1886,7 +1875,7 @@ function initializeRelatedField(field, fields, options={}) {
// Custom formatting for the search results
if (field.model) {
// If the 'model' is specified, hand it off to the custom model render
var html = renderModelData(name, field.model, data, field, options);
var html = renderModelData(name, field.model, data, field);
return $(html);
} else {
// Return a simple renderering
@ -1916,7 +1905,7 @@ function initializeRelatedField(field, fields, options={}) {
// Custom formatting for selected item
if (field.model) {
// If the 'model' is specified, hand it off to the custom model render
var html = renderModelData(name, field.model, data, field, options);
var html = renderModelData(name, field.model, data, field);
return $(html);
} else {
// Return a simple renderering
@ -2027,71 +2016,18 @@ function searching() {
* - parameters: The field definition (OPTIONS) request
* - options: Other options provided at time of modal creation by the client
*/
function renderModelData(name, model, data, parameters, options) {
function renderModelData(name, model, data, parameters) {
if (!data) {
return parameters.placeholder || '';
}
// TODO: Implement this function for various models
var html = null;
var renderer = null;
// Find a custom renderer
switch (model) {
case 'company':
renderer = renderCompany;
break;
case 'stockitem':
renderer = renderStockItem;
break;
case 'stocklocation':
renderer = renderStockLocation;
break;
case 'part':
renderer = renderPart;
break;
case 'partcategory':
renderer = renderPartCategory;
break;
case 'partparametertemplate':
renderer = renderPartParameterTemplate;
break;
case 'purchaseorder':
renderer = renderPurchaseOrder;
break;
case 'salesorder':
renderer = renderSalesOrder;
break;
case 'salesordershipment':
renderer = renderSalesOrderShipment;
break;
case 'manufacturerpart':
renderer = renderManufacturerPart;
break;
case 'supplierpart':
renderer = renderSupplierPart;
break;
case 'build':
renderer = renderBuild;
break;
case 'owner':
renderer = renderOwner;
break;
case 'user':
renderer = renderUser;
break;
case 'group':
renderer = renderGroup;
break;
default:
break;
}
var renderer = getModelRenderer(model);
if (renderer != null) {
html = renderer(name, data, parameters, options);
html = renderer(data, parameters);
}
if (html != null) {

View File

@ -58,7 +58,7 @@ function deleteButton(url, text='{% trans "Delete" %}') {
function shortenString(input_string, options={}) {
// Maximum length can be provided via options argument, or via a user-configurable setting
var max_length = options.max_length || user_settings.TABLE_STRING_MAX_LENGTH;
var max_length = options.max_length || user_settings.TABLE_STRING_MAX_LENGTH || 100;
if (!max_length || !input_string) {
return input_string;

View File

@ -3,9 +3,11 @@
/* globals
blankImage,
select2Thumbnail
shortenString
*/
/* exported
getModelRenderer,
renderBuild,
renderCompany,
renderGroup,
@ -34,91 +36,144 @@
/*
* Trim the supplied string to ensure the string length is limited to the provided value
* Return an appropriate model renderer based on the 'name' of the model
*/
function trim(data, max_length=100) {
if (data.length > max_length) {
data = data.slice(0, max_length - 3) + '...';
}
function getModelRenderer(model) {
return data;
// Find a custom renderer
switch (model) {
case 'company':
return renderCompany;
case 'stockitem':
return renderStockItem;
case 'stocklocation':
return renderStockLocation;
case 'part':
return renderPart;
case 'partcategory':
return renderPartCategory;
case 'partparametertemplate':
return renderPartParameterTemplate;
case 'purchaseorder':
return renderPurchaseOrder;
case 'salesorder':
return renderSalesOrder;
case 'salesordershipment':
return renderSalesOrderShipment;
case 'manufacturerpart':
return renderManufacturerPart;
case 'supplierpart':
return renderSupplierPart;
case 'build':
return renderBuild;
case 'owner':
return renderOwner;
case 'user':
return renderUser;
case 'group':
return renderGroup;
default:
// Un-handled model type
console.error(`Rendering not implemented for model '${model}'`);
return null;
}
}
// Should the ID be rendered for this string
function renderId(title, pk, parameters={}) {
/*
* Generic method for rendering model data in a consistent fashion:
*
* data:
* - image: Render an image (optional)
* - imageSecondary: Render a secondary image (optional)
* - text: Primary text
* - pk: primary key (unique ID) of the model instance
* - textSecondary: Secondary text
* - url: href for link target (is enabled or disabled by showLink option)
* - labels: extra labels to display
*
* options:
* - showImage: Option to create image(s) (default = true)
* - showLink: Option to create link (default = false)
* - showLabels: Option to show or hide extra labels (default = true)
*/
function renderModel(data, options={}) {
// Default = do not render
var render = false;
let showImage = ('showImage' in options) ? options.showImage : true;
let showLink = ('showLink' in options) ? options.showLink : false;
let showLabels = ('showLabels' in options) ? options.showLabels : true;
if ('render_pk' in parameters) {
render = parameters['render_pk'];
let html = '';
if (showImage) {
if (data.image) {
html += select2Thumbnail(data.image);
}
if (data.imageSecondary) {
html += select2Thumbnail(data.imageSecondary);
}
}
if (render) {
return `<span class='float-right'><small>${title}: ${pk}</small></span>`;
} else {
return '';
let text = `<span>${data.text}</span>`;
if (data.textSecondary) {
text += ` - <small><em>${data.textSecondary}</em></small>`;
}
if (showLink && data.url) {
text = renderLink(text, data.url);
}
html += text;
if (showLabels && data.labels) {
html += `<span class='float-right'><small>${data.labels}</small></span>`;
}
return html;
}
// Renderer for "Company" model
// eslint-disable-next-line no-unused-vars
function renderCompany(name, data, parameters={}, options={}) {
function renderCompany(data, parameters={}) {
var html = select2Thumbnail(data.image);
html += `<span><b>${data.name}</b></span> - <i>${trim(data.description)}</i>`;
html += renderId('{% trans "Company ID" %}', data.pk, parameters);
return html;
return renderModel(
{
image: data.image || blankImage(),
text: data.name,
textSecondary: shortenString(data.description),
url: data.url || `/company/${data.pk}/`,
},
parameters
);
}
// Renderer for "StockItem" model
// eslint-disable-next-line no-unused-vars
function renderStockItem(name, data, parameters={}, options={}) {
function renderStockItem(data, parameters={}) {
var image = blankImage();
let part_image = null;
let render_part_detail = ('render_part_detail' in parameters) ? parameters.render_part_detail : true;
let render_location_detail = ('render_location_detail' in parameters) ? parameters.render_location_detail : false;
let render_available_quantity = ('render_available_quantity' in parameters) ? parameters.render_available_quantity : false;
if (data.part_detail) {
image = data.part_detail.thumbnail || data.part_detail.image || blankImage();
if (render_part_detail) {
}
var render_part_detail = true;
if ('render_part_detail' in parameters) {
render_part_detail = parameters['render_part_detail'];
}
var part_detail = '';
let text = '';
let stock_detail = '';
if (render_part_detail && data.part_detail) {
part_detail = `<img src='${image}' class='select2-thumbnail'><span>${data.part_detail.full_name}</span> - `;
part_image = data.part_detail.thumbnail || data.part_detail.image || blankImage();
text += data.part_detail.full_name;
}
var render_location_detail = false;
if ('render_location_detail' in parameters) {
render_location_detail = parameters['render_location_detail'];
}
var location_detail = '';
if (render_location_detail && data.location_detail) {
location_detail = ` <small>- (<em>${data.location_detail.name}</em>)</small>`;
text += ` <small>- (<em>${data.location_detail.name}</em>)</small>`;
}
var render_available_quantity = false;
if ('render_available_quantity' in parameters) {
render_available_quantity = parameters['render_available_quantity'];
}
var stock_detail = '';
if (data.quantity == 0) {
stock_detail = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock"% }</span>`;
} else {
@ -138,313 +193,246 @@ function renderStockItem(name, data, parameters={}, options={}) {
}
}
var html = `
<span>
${part_detail}
${stock_detail}
${location_detail}
${renderId('{% trans "Stock ID" %}', data.pk, parameters)}
</span>
`;
return html;
return renderModel(
{
image: part_image,
text: text,
textSecondary: stock_detail,
url: data.url || `/stock/item/${data.pk}/`,
},
parameters
);
}
// Renderer for "StockLocation" model
// eslint-disable-next-line no-unused-vars
function renderStockLocation(name, data, parameters={}, options={}) {
function renderStockLocation(data, parameters={}) {
var level = '- '.repeat(data.level);
let render_description = ('render_description' in parameters) ? parameters.render_description : true;
let level = '- '.repeat(data.level);
var html = `<span>${level}${data.pathstring}</span>`;
var render_description = true;
if ('render_description' in parameters) {
render_description = parameters['render_description'];
}
if (render_description && data.description) {
html += ` - <i>${trim(data.description)}</i>`;
}
html += renderId('{% trans "Location ID" %}', data.pk, parameters);
return html;
return renderModel(
{
text: `${level}${data.pathstring}`,
textSecondary: render_description ? shortenString(data.description) : '',
url: data.url || `/stock/location/${data.pk}/`,
},
parameters
);
}
// eslint-disable-next-line no-unused-vars
function renderBuild(name, data, parameters={}, options={}) {
function renderBuild(data, parameters={}) {
var image = null;
var image = blankImage();
if (data.part_detail && data.part_detail.thumbnail) {
image = data.part_detail.thumbnail;
image = data.part_detail.thumbnail || data.part_detail.image || blankImage();
}
var html = select2Thumbnail(image);
html += `<span><b>${data.reference}</b> - ${data.quantity} x ${data.part_detail.full_name}</span>`;
html += renderId('{% trans "Build ID" %}', data.pk, parameters);
return html;
return renderModel(
{
image: image,
text: data.reference,
textSecondary: `${data.quantity} x ${data.part_detail.full_name}`,
url: data.url || `/build/${data.pk}/`,
},
parameters
);
}
// Renderer for "Part" model
// eslint-disable-next-line no-unused-vars
function renderPart(name, data, parameters={}, options={}) {
function renderPart(data, parameters={}) {
var html = select2Thumbnail(data.image);
html += ` <span>${data.full_name || data.name}</span>`;
if (data.description) {
html += ` - <i><small>${trim(data.description)}</small></i>`;
}
var stock_data = '';
let labels = '';
if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) {
stock_data = partStockLabel(data);
labels = partStockLabel(data);
if (!data.active) {
labels += `<span class='badge badge-right rounded-pill bg-danger'>{% trans "Inactive" %}</span>`;
}
}
var extra = '';
if (!data.active) {
extra += `<span class='badge badge-right rounded-pill bg-danger'>{% trans "Inactive" %}</span>`;
}
html += `
<span class='float-right'>
<small>
${stock_data}
${extra}
${renderId('{% trans "Part ID" %}', data.pk, parameters)}
</small>
</span>`;
return html;
return renderModel(
{
image: data.image || blankImage(),
text: data.full_name || data.name,
textSecondary: shortenString(data.description),
labels: labels,
url: data.url || `/part/${data.pk}/`,
},
parameters,
);
}
// Renderer for "Group" model
// eslint-disable-next-line no-unused-vars
function renderGroup(name, data, parameters={}, options={}) {
var html = `<span>${data.name}</span>`;
return html;
function renderGroup(data, parameters={}) {
return renderModel(
{
text: data.name,
},
parameters
);
}
// Renderer for "User" model
// eslint-disable-next-line no-unused-vars
function renderUser(name, data, parameters={}, options={}) {
function renderUser(data, parameters={}) {
var html = `<span>${data.username}</span>`;
if (data.first_name && data.last_name) {
html += ` - <i>${data.first_name} ${data.last_name}</i>`;
}
return html;
return renderModel(
{
text: data.username,
textSecondary: `${data.first_name} ${data.last_name}`,
},
parameters
);
}
// Renderer for "Owner" model
// eslint-disable-next-line no-unused-vars
function renderOwner(name, data, parameters={}, options={}) {
function renderOwner(data, parameters={}) {
var html = `<span>${data.name}</span>`;
let label = '';
switch (data.label) {
case 'user':
html += `<span class='float-right fas fa-user'></span>`;
label = `<span class='float-right fas fa-user'></span>`;
break;
case 'group':
html += `<span class='float-right fas fa-users'></span>`;
label = `<span class='float-right fas fa-users'></span>`;
break;
default:
break;
}
return html;
return renderModel(
{
text: data.name,
labels: label,
},
parameters
);
}
// Renderer for "PurchaseOrder" model
// eslint-disable-next-line no-unused-vars
function renderPurchaseOrder(name, data, parameters={}, options={}) {
function renderPurchaseOrder(data, parameters={}) {
var html = '';
let image = blankImage();
if (data.supplier_detail) {
thumbnail = data.supplier_detail.thumbnail || data.supplier_detail.image;
html += select2Thumbnail(thumbnail);
image = data.supplier_detail.thumbnail || data.supplier_detail.image || blankImage();
}
html += `<span>${data.reference}</span>`;
var thumbnail = null;
if (data.supplier_detail) {
html += ` - <span>${data.supplier_detail.name}</span>`;
}
if (data.description) {
html += ` - <em>${trim(data.description)}</em>`;
}
html += renderId('{% trans "Order ID" %}', data.pk, parameters);
return html;
return renderModel(
{
image: image,
text: `${data.reference} - ${data.supplier_detail.name}`,
textSecondary: shortenString(data.description),
url: data.url || `/order/purchase-order/${data.pk}/`,
},
parameters
);
}
// Renderer for "SalesOrder" model
// eslint-disable-next-line no-unused-vars
function renderSalesOrder(name, data, parameters={}, options={}) {
function renderSalesOrder(data, parameters={}) {
var html = `<span>${data.reference}</span>`;
var thumbnail = null;
let image = blankImage();
if (data.customer_detail) {
thumbnail = data.customer_detail.thumbnail || data.customer_detail.image;
html += ' - ' + select2Thumbnail(thumbnail);
html += `<span>${data.customer_detail.name}</span>`;
image = data.customer_detail.thumbnail || data.customer_detail.image || blankImage();
}
if (data.description) {
html += ` - <em>${trim(data.description)}</em>`;
}
html += renderId('{% trans "Order ID" %}', data.pk, parameters);
return html;
return renderModel(
{
image: image,
text: `${data.reference} - ${data.customer_detail.name}`,
textSecondary: shortenString(data.description),
url: data.url || `/order/sales-order/${data.pk}/`,
},
parameters
);
}
// Renderer for "SalesOrderShipment" model
// eslint-disable-next-line no-unused-vars
function renderSalesOrderShipment(name, data, parameters={}, options={}) {
function renderSalesOrderShipment(data, parameters={}) {
var html = `
<span>${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>
<span class='float-right'>
<small>{% trans "Shipment ID" %}: ${data.pk}</small>
</span>
`;
html += renderId('{% trans "Shipment ID" %}', data.pk, parameters);
return html;
return renderModel(
{
text: data.order_detail.reference,
textSecondary: `{% trans "Shipment" %} ${data.reference}`,
},
parameters
);
}
// Renderer for "PartCategory" model
// eslint-disable-next-line no-unused-vars
function renderPartCategory(name, data, parameters={}, options={}) {
function renderPartCategory(data, parameters={}) {
var level = '- '.repeat(data.level);
let level = '- '.repeat(data.level);
var html = `<span>${level}${data.pathstring}</span>`;
if (data.description) {
html += ` - <i>${trim(data.description)}</i>`;
}
html += renderId('{% trans "Category ID" %}', data.pk, parameters);
return html;
return renderModel(
{
text: `${level}${data.pathstring}`,
textSecondary: shortenString(data.description),
url: data.url || `/part/category/${data.pk}/`,
},
parameters
);
}
// eslint-disable-next-line no-unused-vars
function renderPartParameterTemplate(name, data, parameters={}, options={}) {
var units = '';
function renderPartParameterTemplate(data, parameters={}) {
let units = '';
if (data.units) {
units = ` [${data.units}]`;
}
var html = `<span>${data.name}${units}</span>`;
return html;
return renderModel(
{
text: `${data.name}${units}`,
},
parameters
);
}
// Renderer for "ManufacturerPart" model
// eslint-disable-next-line no-unused-vars
function renderManufacturerPart(name, data, parameters={}, options={}) {
function renderManufacturerPart(data, parameters={}) {
var manufacturer_image = null;
var part_image = null;
if (data.manufacturer_detail) {
manufacturer_image = data.manufacturer_detail.image;
}
if (data.part_detail) {
part_image = data.part_detail.thumbnail || data.part_detail.image;
}
var html = '';
html += select2Thumbnail(manufacturer_image);
html += select2Thumbnail(part_image);
if (data.manufacturer_detail) {
html += ` <span><b>${data.manufacturer_detail.name}</b> - ${data.MPN}</span>`;
}
if (data.part_detail) {
html += ` - <i>${data.part_detail.full_name}</i>`;
}
html += renderId('{% trans "Manufacturer Part ID" %}', data.pk, parameters);
return html;
return renderModel(
{
image: data.manufacturer_detail ? data.manufacturer_detail.thumbnail || data.manufacturer_detail.image || blankImage() : null,
imageSecondary: data.part.detail ? data.part_detail.thumbnail || data.part_detail.image || blankImage() : null,
text: `${data.manufacturer_detail.name} - ${data.MPN}`,
textSecondary: data.part_detail.full_name,
url: data.url || `/manufacturer-part/${data.pk}/`,
},
parameters
);
}
// Renderer for "SupplierPart" model
// eslint-disable-next-line no-unused-vars
function renderSupplierPart(name, data, parameters={}, options={}) {
function renderSupplierPart(data, parameters={}) {
var supplier_image = null;
var part_image = null;
if (data.supplier_detail) {
supplier_image = data.supplier_detail.image;
}
if (data.part_detail) {
part_image = data.part_detail.thumbnail || data.part_detail.image;
}
var html = '';
html += select2Thumbnail(supplier_image);
if (data.part_detail) {
html += select2Thumbnail(part_image);
}
if (data.supplier_detail) {
html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`;
}
if (data.part_detail) {
html += ` - <i>${data.part_detail.full_name}</i>`;
}
html += renderId('{% trans "Supplier Part ID" %}', data.pk, parameters);
return html;
return renderModel(
{
image: data.supplier_detail ? data.supplier_detail.thumbnail || data.supplier_detail.image || blankImage() : null,
imageSecondary: data.part_detail ? data.part_detail.thumbnail || data.part_detail.image || blankImage() : null,
text: `${data.supplier_detail.name} - ${data.SKU}`,
textSecondary: data.part_detail.full_name,
url: data.url || `/supplier-part/${data.pk}/`
},
parameters
);
}

View File

@ -640,7 +640,7 @@ function partStockLabel(part, options={}) {
var stock_health = part.unallocated_stock + part.building + part.ordering - part.minimum_stock;
// TODO: Refactor the API to include this information, so we don't have to request it!
if (!options.noDemandInfo) {
if (options.showDemandInfo) {
// Check for demand from unallocated build orders
var required_build_order_quantity = null;

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={}) {
/*
* Add an individual search query, with callback for rendering
*/
function addSearchQuery(key, title, query_params, render_params={}) {
// Include current search term
query_params.search = searchTextCurrent;
searchQuery[key] = query_params;
// 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 the result group to the panel
$('#offcanvas-search').find('#search-results').append(`
<div class='search-result-group-wrapper' id='search-results-wrapper-${key}'></div>
`);
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}'>

View File

@ -12,11 +12,6 @@
<button id='search-clear' class='btn btn-outline-secondary' title='{% trans "Clear search" %}'>
<span class='fas fa-backspace'></span>
</button>
<!--
<button id='search-filter' class="btn btn-outline-secondary" title='{% trans "Filter results" %}'>
<span class='fas fa-filter'></span>
</button>
-->
<button id='search-close' class="btn btn-outline-secondary" data-bs-dismiss='offcanvas' title='{% trans "Close search menu" %}'>
<span class='fas fa-times icon-red'></span>
</button>
@ -25,15 +20,11 @@
</div>
<div class="offcanvas-body">
<div id="search-center">
<p id='search-pending' class='text-muted' display='none'>
<em>{% trans "Searching" %}...</em>
<span class='float-right'>
<span class='fas fa-spinner fa-spin'></span>
</span>
</p>
<p id='search-no-results' class='text-muted'>
<em>{% trans "No search results" %}</em>
</p>
<div id='search-context'>
<div class='alert alert-block alert-info'>
<span class='fas fa-search'></span> <em>{% trans "Enter search query" %}</em>
</div>
</div>
<div id='search-results'>
<!-- Search results go here -->
</div>