diff --git a/assets/release_notes.md b/assets/release_notes.md index bfb26819..33d045c6 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -6,6 +6,7 @@ - Adds support for proper currency rendering - Fix icon for supplier part detail widget +- Support global search API endpoint ### 0.10.1 - February 2023 --- diff --git a/lib/api.dart b/lib/api.dart index d8b012d6..1617dda9 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -279,6 +279,9 @@ class InvenTreeAPI { // Company attachments require API v95 or newer bool get supportCompanyAttachments => isConnected() && apiVersion >= 95; + // Consolidated search request API v102 or newer + bool get supportsConsolidatedSearch => isConnected() && apiVersion >= 102; + // Are plugins enabled on the server? bool _pluginsEnabled = false; diff --git a/lib/widget/search.dart b/lib/widget/search.dart index 59608c8d..39741430 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -69,27 +69,34 @@ class _SearchDisplayState extends RefreshableState { Timer? debounceTimer; + /* + * Decrement the number of pending / outstanding search queries + */ + void decrementPendingSearches() { + if (nPendingSearches > 0) { + nPendingSearches--; + } + } + + /* + * Determine if the search is still running + */ bool isSearching() { if (searchController.text.isEmpty) { return false; } - return nSearchResults < 5; + return nPendingSearches > 0; } - int nSearchResults = 0; - + // Individual search result count (for legacy search API) + int nPendingSearches = 0; int nPartResults = 0; - int nCategoryResults = 0; - int nStockResults = 0; - int nLocationResults = 0; - int nSupplierResults = 0; - int nPurchaseOrderResults = 0; late FocusNode _focusNode; @@ -114,6 +121,32 @@ class _SearchDisplayState extends RefreshableState { } } + /* + * Return the 'result count' for a particular query from the results map + * e.g. + * { + * "part": { + * "count": 102, + * } + * } + */ + int getSearchResultCount(Map results, String key) { + + dynamic result = results[key]; + + if (result == null || result is! Map) { + return 0; + } + + dynamic count = result["count"]; + + if (count == null || count is! int) { + return 0; + } + + return count; + } + /* * Initiate multiple search requests to the server. * Each request returns at *some point* in the future, @@ -122,7 +155,6 @@ class _SearchDisplayState extends RefreshableState { * So, each request only causes an update *if* the search term is still the same when it completes */ Future search(String term) async { - var api = InvenTreeAPI(); if (!mounted) { @@ -138,21 +170,95 @@ class _SearchDisplayState extends RefreshableState { nSupplierResults = 0; nPurchaseOrderResults = 0; - nSearchResults = 0; + nPendingSearches = 0; }); if (term.isEmpty) { return; } + // Consolidated search allows us to perform *all* searches in a single query + if (api.supportsConsolidatedSearch) { + Map body = { + "limit": 1, + "search": term, + }; + + // Part search + if (api.checkPermission("part", "view")) { + body["part"] = {}; + } + + // PartCategory search + if (api.checkPermission("part_category", "view")) { + body["partcategory"] = {}; + } + + // StockItem search + if (api.checkPermission("stock", "view")) { + body["stockitem"] = { + "in_stock": true, + }; + } + + // StockLocation search + if (api.checkPermission("stock_location", "view")) { + body["stocklocation"] = {}; + } + + // PurchaseOrder search + if (api.checkPermission("purchase_order", "view")) { + body["purchaseorder"] = { + "outstanding": true + }; + } + + if (body.isNotEmpty) { + nPendingSearches++; + + api.post( + "search/", + body: body, + expectedStatusCode: 200).then((APIResponse response) { + decrementPendingSearches(); + + Map results = {}; + + if (response.data is Map) { + results = response.data as Map; + } + + if (mounted) { + setState(() { + nPartResults = getSearchResultCount(results, "part"); + nCategoryResults = getSearchResultCount(results, "partcategory"); + nStockResults = getSearchResultCount(results, "stockitem"); + nLocationResults = getSearchResultCount(results, "stocklocation"); + nSupplierResults = 0; //getSearchResultCount(results, "") + nPurchaseOrderResults = getSearchResultCount(results, "purchaseorder"); + }); + } + }); + } + } else { + legacySearch(term); + } + } + + /* + * Perform "legacy" search (without consolidated search API endpoint + */ + Future legacySearch(String term) async { + // Search parts if (api.checkPermission("part", "view")) { + nPendingSearches++; InvenTreePart().count(searchQuery: term).then((int n) { if (term == searchController.text) { if (mounted) { + decrementPendingSearches(); setState(() { nPartResults = n; - nSearchResults++; }); } } @@ -161,12 +267,13 @@ class _SearchDisplayState extends RefreshableState { // Search part categories if (api.checkPermission("part_category", "view")) { + nPendingSearches++; InvenTreePartCategory().count(searchQuery: term,).then((int n) { if (term == searchController.text) { if (mounted) { + decrementPendingSearches(); setState(() { nCategoryResults = n; - nSearchResults++; }); } } @@ -175,12 +282,13 @@ class _SearchDisplayState extends RefreshableState { // Search stock items if (api.checkPermission("stock", "view")) { + nPendingSearches++; InvenTreeStockItem().count(searchQuery: term).then((int n) { if (term == searchController.text) { if (mounted) { + decrementPendingSearches(); setState(() { nStockResults = n; - nSearchResults++; }); } } @@ -189,35 +297,22 @@ class _SearchDisplayState extends RefreshableState { // Search stock locations if (api.checkPermission("stock_location", "view")) { + nPendingSearches++; InvenTreeStockLocation().count(searchQuery: term).then((int n) { if (term == searchController.text) { if (mounted) { + decrementPendingSearches(); setState(() { nLocationResults = n; - nSearchResults++; }); } } }); } - // TDOO: Re-implement this once display for companies has been fixed - /* - // Search suppliers - InvenTreeCompany().count(searchQuery: term, - filters: { - "is_supplier": "true", - }, - ).then((int n) { - setState(() { - nSupplierResults = n; - nSearchResults++; - }); - }); - */ - // Search purchase orders if (api.checkPermission("purchase_order", "view")) { + nPendingSearches++; InvenTreePurchaseOrder().count( searchQuery: term, filters: { @@ -226,9 +321,9 @@ class _SearchDisplayState extends RefreshableState { ).then((int n) { if (term == searchController.text) { if (mounted) { + decrementPendingSearches(); setState(() { nPurchaseOrderResults = n; - nSearchResults++; }); } }