mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-11-03 23:05:44 +00:00 
			
		
		
		
	Consolidated search (#289)
* Update search widget to support consolidated search API endpoint * Finer control over global search * Update release notes * remove unused variable
This commit is contained in:
		@@ -6,6 +6,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- Adds support for proper currency rendering
 | 
					- Adds support for proper currency rendering
 | 
				
			||||||
- Fix icon for supplier part detail widget
 | 
					- Fix icon for supplier part detail widget
 | 
				
			||||||
 | 
					- Support global search API endpoint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 0.10.1 - February 2023 
 | 
					### 0.10.1 - February 2023 
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -279,6 +279,9 @@ class InvenTreeAPI {
 | 
				
			|||||||
  // Company attachments require API v95 or newer
 | 
					  // Company attachments require API v95 or newer
 | 
				
			||||||
  bool get supportCompanyAttachments => isConnected() && apiVersion >= 95;
 | 
					  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?
 | 
					  // Are plugins enabled on the server?
 | 
				
			||||||
  bool _pluginsEnabled = false;
 | 
					  bool _pluginsEnabled = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -69,27 +69,34 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  Timer? debounceTimer;
 | 
					  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() {
 | 
					  bool isSearching() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (searchController.text.isEmpty) {
 | 
					    if (searchController.text.isEmpty) {
 | 
				
			||||||
      return false;
 | 
					      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 nPartResults = 0;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  int nCategoryResults = 0;
 | 
					  int nCategoryResults = 0;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  int nStockResults = 0;
 | 
					  int nStockResults = 0;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  int nLocationResults = 0;
 | 
					  int nLocationResults = 0;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  int nSupplierResults = 0;
 | 
					  int nSupplierResults = 0;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  int nPurchaseOrderResults = 0;
 | 
					  int nPurchaseOrderResults = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  late FocusNode _focusNode;
 | 
					  late FocusNode _focusNode;
 | 
				
			||||||
@@ -114,6 +121,32 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /*
 | 
				
			||||||
 | 
					   * Return the 'result count' for a particular query from the results map
 | 
				
			||||||
 | 
					   * e.g.
 | 
				
			||||||
 | 
					   * {
 | 
				
			||||||
 | 
					   *     "part": {
 | 
				
			||||||
 | 
					   *         "count": 102,
 | 
				
			||||||
 | 
					   *     }
 | 
				
			||||||
 | 
					   * }
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  int getSearchResultCount(Map <String, dynamic> 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.
 | 
					   * Initiate multiple search requests to the server.
 | 
				
			||||||
   * Each request returns at *some point* in the future,
 | 
					   * Each request returns at *some point* in the future,
 | 
				
			||||||
@@ -122,7 +155,6 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
 | 
				
			|||||||
   * So, each request only causes an update *if* the search term is still the same when it completes
 | 
					   * So, each request only causes an update *if* the search term is still the same when it completes
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  Future<void> search(String term) async {
 | 
					  Future<void> search(String term) async {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    var api = InvenTreeAPI();
 | 
					    var api = InvenTreeAPI();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!mounted) {
 | 
					    if (!mounted) {
 | 
				
			||||||
@@ -138,21 +170,95 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
 | 
				
			|||||||
      nSupplierResults = 0;
 | 
					      nSupplierResults = 0;
 | 
				
			||||||
      nPurchaseOrderResults = 0;
 | 
					      nPurchaseOrderResults = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      nSearchResults = 0;
 | 
					      nPendingSearches = 0;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (term.isEmpty) {
 | 
					    if (term.isEmpty) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Consolidated search allows us to perform *all* searches in a single query
 | 
				
			||||||
 | 
					    if (api.supportsConsolidatedSearch) {
 | 
				
			||||||
 | 
					      Map<String, dynamic> 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<String, dynamic> results = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (response.data is Map<String, dynamic>) {
 | 
				
			||||||
 | 
					            results = response.data as Map<String, dynamic>;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          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<void> legacySearch(String term) async {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Search parts
 | 
					    // Search parts
 | 
				
			||||||
    if (api.checkPermission("part", "view")) {
 | 
					    if (api.checkPermission("part", "view")) {
 | 
				
			||||||
 | 
					      nPendingSearches++;
 | 
				
			||||||
      InvenTreePart().count(searchQuery: term).then((int n) {
 | 
					      InvenTreePart().count(searchQuery: term).then((int n) {
 | 
				
			||||||
        if (term == searchController.text) {
 | 
					        if (term == searchController.text) {
 | 
				
			||||||
          if (mounted) {
 | 
					          if (mounted) {
 | 
				
			||||||
 | 
					            decrementPendingSearches();
 | 
				
			||||||
            setState(() {
 | 
					            setState(() {
 | 
				
			||||||
              nPartResults = n;
 | 
					              nPartResults = n;
 | 
				
			||||||
              nSearchResults++;
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -161,12 +267,13 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Search part categories
 | 
					    // Search part categories
 | 
				
			||||||
    if (api.checkPermission("part_category", "view")) {
 | 
					    if (api.checkPermission("part_category", "view")) {
 | 
				
			||||||
 | 
					      nPendingSearches++;
 | 
				
			||||||
      InvenTreePartCategory().count(searchQuery: term,).then((int n) {
 | 
					      InvenTreePartCategory().count(searchQuery: term,).then((int n) {
 | 
				
			||||||
        if (term == searchController.text) {
 | 
					        if (term == searchController.text) {
 | 
				
			||||||
          if (mounted) {
 | 
					          if (mounted) {
 | 
				
			||||||
 | 
					            decrementPendingSearches();
 | 
				
			||||||
            setState(() {
 | 
					            setState(() {
 | 
				
			||||||
              nCategoryResults = n;
 | 
					              nCategoryResults = n;
 | 
				
			||||||
              nSearchResults++;
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -175,12 +282,13 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Search stock items
 | 
					    // Search stock items
 | 
				
			||||||
    if (api.checkPermission("stock", "view")) {
 | 
					    if (api.checkPermission("stock", "view")) {
 | 
				
			||||||
 | 
					      nPendingSearches++;
 | 
				
			||||||
      InvenTreeStockItem().count(searchQuery: term).then((int n) {
 | 
					      InvenTreeStockItem().count(searchQuery: term).then((int n) {
 | 
				
			||||||
        if (term == searchController.text) {
 | 
					        if (term == searchController.text) {
 | 
				
			||||||
          if (mounted) {
 | 
					          if (mounted) {
 | 
				
			||||||
 | 
					            decrementPendingSearches();
 | 
				
			||||||
            setState(() {
 | 
					            setState(() {
 | 
				
			||||||
              nStockResults = n;
 | 
					              nStockResults = n;
 | 
				
			||||||
              nSearchResults++;
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -189,35 +297,22 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Search stock locations
 | 
					    // Search stock locations
 | 
				
			||||||
    if (api.checkPermission("stock_location", "view")) {
 | 
					    if (api.checkPermission("stock_location", "view")) {
 | 
				
			||||||
 | 
					      nPendingSearches++;
 | 
				
			||||||
      InvenTreeStockLocation().count(searchQuery: term).then((int n) {
 | 
					      InvenTreeStockLocation().count(searchQuery: term).then((int n) {
 | 
				
			||||||
        if (term == searchController.text) {
 | 
					        if (term == searchController.text) {
 | 
				
			||||||
          if (mounted) {
 | 
					          if (mounted) {
 | 
				
			||||||
 | 
					            decrementPendingSearches();
 | 
				
			||||||
            setState(() {
 | 
					            setState(() {
 | 
				
			||||||
              nLocationResults = n;
 | 
					              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
 | 
					    // Search purchase orders
 | 
				
			||||||
    if (api.checkPermission("purchase_order", "view")) {
 | 
					    if (api.checkPermission("purchase_order", "view")) {
 | 
				
			||||||
 | 
					     nPendingSearches++;
 | 
				
			||||||
      InvenTreePurchaseOrder().count(
 | 
					      InvenTreePurchaseOrder().count(
 | 
				
			||||||
          searchQuery: term,
 | 
					          searchQuery: term,
 | 
				
			||||||
          filters: {
 | 
					          filters: {
 | 
				
			||||||
@@ -226,9 +321,9 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
 | 
				
			|||||||
      ).then((int n) {
 | 
					      ).then((int n) {
 | 
				
			||||||
        if (term == searchController.text) {
 | 
					        if (term == searchController.text) {
 | 
				
			||||||
          if (mounted) {
 | 
					          if (mounted) {
 | 
				
			||||||
 | 
					            decrementPendingSearches();
 | 
				
			||||||
            setState(() {
 | 
					            setState(() {
 | 
				
			||||||
              nPurchaseOrderResults = n;
 | 
					              nPurchaseOrderResults = n;
 | 
				
			||||||
              nSearchResults++;
 | 
					 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user