diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 3f6fffe19b..4abba23b09 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -1018,4 +1018,33 @@ input[type='number']{ a { text-decoration: none; background-color: transparent; +} + +/* Quicksearch Panel */ + +.search-result-panel { + max-width: 800px; + width: 75% +} + +.search-result-group { + padding: 5px; + padding-left: 10px; + padding-right: 10px; + border: 1px solid var(--border-color); + margin-bottom: 10px; +} + +.search-result-group-buttons > button{ + padding: 2px; + padding-left: 5px; + padding-right: 5px; + font-size: 80%; +} + +.search-result-entry { + border-top: 1px solid var(--border-color); + padding: 3px; + margin-top: 3px; + overflow: hidden; } \ No newline at end of file diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index d1b49aaf4b..efad99c3ec 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -128,81 +128,6 @@ function inventreeDocReady() { attachClipboard('.clip-btn', 'modal-about'); attachClipboard('.clip-btn-version', 'modal-about', 'about-copy-text'); - // Add autocomplete to the search-bar - if ($('#search-bar').exists()) { - $('#search-bar').autocomplete({ - source: function(request, response) { - - var params = { - search: request.term, - limit: user_settings.SEARCH_PREVIEW_RESULTS, - offset: 0, - }; - - if (user_settings.SEARCH_HIDE_INACTIVE_PARTS) { - // Limit to active parts - params.active = true; - } - - $.ajax({ - url: '/api/part/', - data: params, - success: function(data) { - - var transformed = $.map(data.results, function(el) { - return { - label: el.full_name, - id: el.pk, - thumbnail: el.thumbnail, - data: el, - }; - }); - response(transformed); - }, - error: function() { - response([]); - } - }); - }, - create: function() { - $(this).data('ui-autocomplete')._renderItem = function(ul, item) { - - var html = ` -
- - ${item.label} - - - `; - - if (user_settings.SEARCH_SHOW_STOCK_LEVELS) { - html += partStockLabel( - item.data, - { - classes: 'badge-right', - } - ); - } - - html += '
'; - - return $('
  • ').append(html).appendTo(ul); - }; - }, - select: function( event, ui ) { - window.location = '/part/' + ui.item.id + '/'; - }, - minLength: 2, - classes: { - 'ui-autocomplete': 'dropdown-menu search-menu', - }, - position: { - my : "right top", - at: "right bottom" - } - }); - } - // Generate brand-icons $('.brand-icon').each(function(i, obj) { loadBrandIcon($(this), $(this).attr('brand_name')); @@ -231,8 +156,13 @@ function inventreeDocReady() { stopNotificationWatcher(); }); - $('#offcanvasRight').on('show.bs.offcanvas', openNotificationPanel); // listener for opening the notification panel - $('#offcanvasRight').on('hidden.bs.offcanvas', closeNotificationPanel); // listener for closing the notification panel + // Calbacks for search panel + $('#offcanvas-search').on('shown.bs.offcanvas', openSearchPanel); + $('#offcanvas-search').on('hidden.bs.offcanvas', closeSearchPanel); + + // Callbacks for notifications panel + $('#offcanvas-notification').on('show.bs.offcanvas', openNotificationPanel); // listener for opening the notification panel + $('#offcanvas-notification').on('hidden.bs.offcanvas', closeNotificationPanel); // listener for closing the notification panel } diff --git a/InvenTree/InvenTree/test_views.py b/InvenTree/InvenTree/test_views.py index 56d8889984..490e91b03e 100644 --- a/InvenTree/InvenTree/test_views.py +++ b/InvenTree/InvenTree/test_views.py @@ -72,7 +72,7 @@ class ViewTests(TestCase): """ # Change this number as more javascript files are added to the index page - N_SCRIPT_FILES = 37 + N_SCRIPT_FILES = 38 content = self.get_index_page() diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d795b81472..ec8b891f93 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -130,6 +130,7 @@ translated_javascript_urls = [ url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'), url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'), url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'), + url(r'^search.js', DynamicJsView.as_view(template_name='js/translated/search.js'), name='search.js'), url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'), url(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'), url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'), diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 20acd84ce1..bb7de56e99 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -1268,20 +1268,62 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'validator': bool, }, - 'SEARCH_PREVIEW_RESULTS': { - 'name': _('Search Preview Results'), - 'description': _('Number of results to show in search preview window'), - 'default': 10, - 'validator': [int, MinValueValidator(1)] - }, - - 'SEARCH_SHOW_STOCK_LEVELS': { - 'name': _('Search Show Stock'), - 'description': _('Display stock levels in search preview window'), + 'SEARCH_PREVIEW_SHOW_PARTS': { + 'name': _('Search Parts'), + 'description': _('Display parts in search preview window'), 'default': True, 'validator': bool, }, + 'SEARCH_PREVIEW_SHOW_CATEGORIES': { + 'name': _('Search Categories'), + 'description': _('Display part categories in search preview window'), + 'default': False, + 'validator': bool, + }, + + 'SEARCH_PREVIEW_SHOW_STOCK': { + 'name': _('Search Stock'), + 'description': _('Display stock items in search preview window'), + 'default': True, + 'validator': bool, + }, + + 'SEARCH_PREVIEW_SHOW_LOCATIONS': { + 'name': _('Search Locations'), + 'description': _('Display stock locations in search preview window'), + 'default': False, + 'validator': bool, + }, + + 'SEARCH_PREVIEW_SHOW_COMPANIES': { + 'name': _('Search Companies'), + 'description': _('Display companies in search preview window'), + 'default': True, + 'validator': bool, + }, + + 'SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS': { + 'name': _('Search Purchase Orders'), + 'description': _('Display purchase orders in search preview window'), + 'default': True, + 'validator': bool, + }, + + 'SEARCH_PREVIEW_SHOW_SALES_ORDERS': { + 'name': _('Search Sales Orders'), + 'description': _('Display sales orders in search preview window'), + 'default': True, + 'validator': bool, + }, + + 'SEARCH_PREVIEW_RESULTS': { + 'name': _('Search Preview Results'), + 'description': _('Number of results to show in each section of the search preview window'), + 'default': 10, + 'validator': [int, MinValueValidator(1)] + }, + 'SEARCH_HIDE_INACTIVE_PARTS': { 'name': _("Hide Inactive Parts"), 'description': _('Hide inactive parts in search preview window'), diff --git a/InvenTree/templates/InvenTree/settings/user_search.html b/InvenTree/templates/InvenTree/settings/user_search.html index 51df53ee6b..1883110b80 100644 --- a/InvenTree/templates/InvenTree/settings/user_search.html +++ b/InvenTree/templates/InvenTree/settings/user_search.html @@ -14,8 +14,16 @@
    + {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_PARTS" user_setting=True icon='fa-shapes' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_CATEGORIES" user_setting=True icon='fa-sitemap' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_STOCK" user_setting=True icon='fa-boxes' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_LOCATIONS" user_setting=True icon='fa-sitemap' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_COMPANIES" user_setting=True icon='fa-building' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS" user_setting=True icon='fa-shopping-cart' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_SHOW_SALES_ORDERS" user_setting=True icon='fa-truck' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_PREVIEW_RESULTS" user_setting=True icon='fa-search' %} - {% include "InvenTree/settings/setting.html" with key="SEARCH_SHOW_STOCK_LEVELS" user_setting=True icon='fa-boxes' %} + {% include "InvenTree/settings/setting.html" with key="SEARCH_HIDE_INACTIVE_PARTS" user_setting=True icon='fa-eye-slash' %}
    diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 3791d5f059..d3c8ed8a59 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -127,9 +127,11 @@ {% endblock %}
    + {% include 'modals.html' %} {% include 'about.html' %} {% include "notifications.html" %} + {% include "search.html" %} @@ -186,6 +188,7 @@ + diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index e3abe1186f..7be6c954c2 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -10,7 +10,9 @@ renderCompany, renderManufacturerPart, renderOwner, + renderPart, renderPartCategory, + renderStockItem, renderStockLocation, renderSupplierPart, */ @@ -31,7 +33,7 @@ // Renderer for "Company" model // eslint-disable-next-line no-unused-vars -function renderCompany(name, data, parameters, options) { +function renderCompany(name, data, parameters={}, options={}) { var html = select2Thumbnail(data.image); @@ -45,7 +47,7 @@ function renderCompany(name, data, parameters, options) { // Renderer for "StockItem" model // eslint-disable-next-line no-unused-vars -function renderStockItem(name, data, parameters, options) { +function renderStockItem(name, data, parameters={}, options={}) { var image = blankImage(); @@ -111,7 +113,7 @@ function renderStockItem(name, data, parameters, options) { // Renderer for "StockLocation" model // eslint-disable-next-line no-unused-vars -function renderStockLocation(name, data, parameters, options) { +function renderStockLocation(name, data, parameters={}, options={}) { var level = '- '.repeat(data.level); @@ -133,7 +135,7 @@ function renderStockLocation(name, data, parameters, options) { } // eslint-disable-next-line no-unused-vars -function renderBuild(name, data, parameters, options) { +function renderBuild(name, data, parameters={}, options={}) { var image = null; @@ -154,7 +156,7 @@ function renderBuild(name, data, parameters, options) { // Renderer for "Part" model // eslint-disable-next-line no-unused-vars -function renderPart(name, data, parameters, options) { +function renderPart(name, data, parameters={}, options={}) { var html = select2Thumbnail(data.image); @@ -164,13 +166,14 @@ function renderPart(name, data, parameters, options) { html += ` - ${data.description}`; } - var extra = ''; + var stock_data = ''; - // Display available part quantity if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) { - extra += partStockLabel(data); + stock_data = partStockLabel(data); } + var extra = ''; + if (!data.active) { extra += `{% trans "Inactive" %}`; } @@ -178,6 +181,7 @@ function renderPart(name, data, parameters, options) { html += ` + ${stock_data} ${extra} {% trans "Part ID" %}: ${data.pk} @@ -188,7 +192,7 @@ function renderPart(name, data, parameters, options) { // Renderer for "User" model // eslint-disable-next-line no-unused-vars -function renderUser(name, data, parameters, options) { +function renderUser(name, data, parameters={}, options={}) { var html = `${data.username}`; @@ -202,7 +206,7 @@ function renderUser(name, data, parameters, options) { // Renderer for "Owner" model // eslint-disable-next-line no-unused-vars -function renderOwner(name, data, parameters, options) { +function renderOwner(name, data, parameters={}, options={}) { var html = `${data.name}`; @@ -223,15 +227,13 @@ function renderOwner(name, data, parameters, options) { // Renderer for "PurchaseOrder" model // eslint-disable-next-line no-unused-vars -function renderPurchaseOrder(name, data, parameters, options) { - var html = ''; +function renderPurchaseOrder(name, data, parameters={}, options={}) { var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX; + var html = `${prefix}${data.reference}`; var thumbnail = null; - html += `${prefix}${data.reference}`; - if (data.supplier_detail) { thumbnail = data.supplier_detail.thumbnail || data.supplier_detail.image; @@ -257,8 +259,19 @@ function renderPurchaseOrder(name, data, parameters, options) { // Renderer for "SalesOrder" model // eslint-disable-next-line no-unused-vars -function renderSalesOrder(name, data, parameters, options) { - var html = `${data.reference}`; +function renderSalesOrder(name, data, parameters={}, options={}) { + + var prefix = global_settings.SALESORDER_REFERENCE_PREFIX; + var html = `${prefix}${data.reference}`; + + var thumbnail = null; + + if (data.customer_detail) { + thumbnail = data.customer_detail.thumbnail || data.customer_detail.image; + + html += ' - ' + select2Thumbnail(thumbnail); + html += `${data.customer_detail.name}`; + } if (data.description) { html += ` - ${data.description}`; @@ -277,7 +290,7 @@ function renderSalesOrder(name, data, parameters, options) { // Renderer for "SalesOrderShipment" model // eslint-disable-next-line no-unused-vars -function renderSalesOrderShipment(name, data, parameters, options) { +function renderSalesOrderShipment(name, data, parameters={}, options={}) { var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX; @@ -294,7 +307,7 @@ function renderSalesOrderShipment(name, data, parameters, options) { // Renderer for "PartCategory" model // eslint-disable-next-line no-unused-vars -function renderPartCategory(name, data, parameters, options) { +function renderPartCategory(name, data, parameters={}, options={}) { var level = '- '.repeat(data.level); @@ -310,7 +323,7 @@ function renderPartCategory(name, data, parameters, options) { } // eslint-disable-next-line no-unused-vars -function renderPartParameterTemplate(name, data, parameters, options) { +function renderPartParameterTemplate(name, data, parameters={}, options={}) { var units = ''; @@ -326,7 +339,7 @@ function renderPartParameterTemplate(name, data, parameters, options) { // Renderer for "ManufacturerPart" model // eslint-disable-next-line no-unused-vars -function renderManufacturerPart(name, data, parameters, options) { +function renderManufacturerPart(name, data, parameters={}, options={}) { var manufacturer_image = null; var part_image = null; @@ -355,7 +368,7 @@ function renderManufacturerPart(name, data, parameters, options) { // Renderer for "SupplierPart" model // eslint-disable-next-line no-unused-vars -function renderSupplierPart(name, data, parameters, options) { +function renderSupplierPart(name, data, parameters={}, options={}) { var supplier_image = null; var part_image = null; diff --git a/InvenTree/templates/js/translated/search.js b/InvenTree/templates/js/translated/search.js new file mode 100644 index 0000000000..8900007c31 --- /dev/null +++ b/InvenTree/templates/js/translated/search.js @@ -0,0 +1,325 @@ +{% load i18n %} + +/* globals +*/ + +/* exported + closeSearchPanel, + openSearchPanel, + searchTextChanged, +*/ + + +/* + * Callback when the search panel is closed + */ +function closeSearchPanel() { +} + + +/* + * Callback when the search panel is opened. + * Ensure the panel is in a known state + */ +function openSearchPanel() { + + var panel = $('#offcanvas-search'); + + clearSearchResults(); + + panel.find('#search-input').on('keyup change', searchTextChanged); + + // Callback for "clear search" button + panel.find('#search-clear').click(function(event) { + + // Prevent this button from actually submitting the form + event.preventDefault(); + + panel.find('#search-input').val(''); + clearSearchResults(); + }); + + // Callback for the "close search" button + panel.find('#search-close').click(function(event) { + // Prevent this button from actually submitting the form + event.preventDefault(); + }); +} + +var searchInputTimer = null; +var searchText = null; +var searchTextCurrent = null; +var searchQueries = []; + +function searchTextChanged(event) { + + searchText = $('#offcanvas-search').find('#search-input').val(); + + clearTimeout(searchInputTimer); + searchInputTimer = setTimeout(updateSearch, 250); +}; + + +function updateSearch() { + + if (searchText == searchTextCurrent) { + return; + } + + clearSearchResults(); + + if (searchText.length == 0) { + return; + } + + searchTextCurrent = searchText; + + // Cancel any previous AJAX requests + searchQueries.forEach(function(query) { + query.abort(); + }); + + searchQueries = []; + + // Show the "searching" text + $('#offcanvas-search').find('#search-pending').show(); + + if (user_settings.SEARCH_PREVIEW_SHOW_PARTS) { + + var params = {}; + + if (user_settings.SEARCH_HIDE_INACTIVE_PARTS) { + params.active = false; + } + + // Search for matching parts + addSearchQuery( + 'part', + '{% trans "Parts" %}', + '{% url "api-part-list" %}', + params, + renderPart, + { + url: '/part', + } + ); + } + + if (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', + }, + ); + } + + if (user_settings.SEARCH_PREVIEW_SHOW_STOCK) { + // Search for matching stock items + addSearchQuery( + 'stock', + '{% trans "Stock Items" %}', + '{% url "api-stock-list" %}', + { + part_detail: true, + location_detail: true, + }, + renderStockItem, + { + url: '/stock/item', + } + ); + } + + if (user_settings.SEARCH_PREVIEW_SHOW_LOCATIONS) { + // Search for matching stock locations + addSearchQuery( + 'location', + '{% trans "Stock Locations" %}', + '{% url "api-location-list" %}', + {}, + renderStockLocation, + { + url: '/stock/location', + } + ); + } + + if (user_settings.SEARCH_PREVIEW_SHOW_COMPANIES) { + // Search for matching companies + addSearchQuery( + 'company', + '{% trans "Companies" %}', + '{% url "api-company-list" %}', + {}, + renderCompany, + { + url: '/company', + } + ); + } + + if (user_settings.SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS) { + // Search for matching purchase orders + addSearchQuery( + 'purchaseorder', + '{% trans "Purchase Orders" %}', + '{% url "api-po-list" %}', + { + supplier_detail: true, + outstanding: true, + }, + renderPurchaseOrder, + { + url: '/order/purchase-order', + } + ); + } + + if (user_settings.SEARCH_PREVIEW_SHOW_SALES_ORDERS) { + // Search for matching sales orders + addSearchQuery( + 'salesorder', + '{% trans "Sales Orders" %}', + '{% url "api-so-list" %}', + { + customer_detail: true, + outstanding: true, + }, + renderSalesOrder, + { + url: '/order/sales-order', + } + ); + } + + // Wait until all the pending queries are completed + $.when.apply($, searchQueries).done(function() { + $('#offcanvas-search').find('#search-pending').hide(); + }); +} + + +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(); + + // Delete any existing search results + panel.find('#search-results').empty(); + + // Finally, grab keyboard focus in the search bar + panel.find('#search-input').focus(); +} + + +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; + + // Add the result group to the panel + $('#offcanvas-search').find('#search-results').append(` +
    + `); + + 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); + +} + + +// Add a group of results to the list +function addSearchResults(key, results, title, renderFunc, renderParams={}) { + + if (results.length == 0) { + // Do not display this group, as there are no results + return; + } + + var panel = $('#offcanvas-search'); + + // Ensure the 'no results found' element is hidden + panel.find('#search-no-results').hide(); + + panel.find(`#search-results-wrapper-${key}`).append(` +
    +
    +
    ${title}
    + +
    + + +
    +
    +
    +
    +
    + `); + + results.forEach(function(result) { + + var pk = result.pk || result.id; + + var html = renderFunc(key, result, renderParams); + + if (renderParams.url) { + html = `` + html + ``; + } + + var result_html = ` +
    + ${html} +
    + `; + + panel.find(`#search-result-list-${key}`).append(result_html); + }); + + // Expand results panel + panel.find(`#search-result-list-${key}`).toggle(); + + // Add callback for "toggle" button + panel.find(`#hide-results-${key}`).click(function() { + panel.find(`#search-result-list-${key}`).toggle(); + }); + + // Add callback for "remove" button + panel.find(`#remove-results-${key}`).click(function() { + panel.find(`#search-results-${key}`).remove(); + }); +} diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 126376a7dc..d687300eb4 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -87,18 +87,25 @@ {% if demo %} {% include "navbar_demo.html" %} {% endif %} - {% include "search_form.html" %} +