From 665de2bd5abb0e398329c870ad0528ccaff9e0d2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 14 Dec 2024 17:29:35 +1100 Subject: [PATCH] Refactor search widget (#578) * Refactor search widget * Cleanup * Fix for unallocated stock count * Fix race condition - Only upate results which match the current search term - Prevents issues with multiple "competing" queries * Fix for stock quantity * Fix icon credits * Tweak app bar color * Cleanup visual stylinh --- assets/credits.md | 5 +- assets/release_notes.md | 1 + lib/app_colors.dart | 2 +- lib/inventree/model.dart | 8 +- lib/inventree/part.dart | 17 ++- lib/widget/part/part_detail.dart | 10 +- lib/widget/search.dart | 230 +++++++++++++++++++------------ 7 files changed, 168 insertions(+), 105 deletions(-) diff --git a/assets/credits.md b/assets/credits.md index abffb6d1..16f28018 100644 --- a/assets/credits.md +++ b/assets/credits.md @@ -7,14 +7,15 @@ Thanks to the following contributors, for their work building this app! - [GoryMoon](https://github.com/GoryMoon) - [simonkuehling](https://github.com/simonkuehling) - [Bobbe](https://github.com/30350n) - +- [awnz](https://github.com/awnz) +- [joaomnuno](https://github.com/joaomnuno) -------- ## Assets The InvenTree App makes use of the following third party assets -- Icons are provided by [fontawesome](https://fontawesome.com) +- Icons are provided by [tabler.io](https://tabler.io/icons) - Sound files have been sourced from [zapsplat](https://www.zapsplat.com) -------- diff --git a/assets/release_notes.md b/assets/release_notes.md index 8b0b61d1..004e1158 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -3,6 +3,7 @@ - Add support for ManufacturerPart model - Support barcode scanning for ManufacturerPart +- Fix bugs in global search view - Fixes barcode scanning bug which prevents scanning of DataMatrix codes - Display "destination" information in PurchaseOrder detail view - Pre-fill "location" field when receiving items against PurchaseOrder diff --git a/lib/app_colors.dart b/lib/app_colors.dart index d9b0da95..5ed9e20a 100644 --- a/lib/app_colors.dart +++ b/lib/app_colors.dart @@ -29,7 +29,7 @@ Color get COLOR_ACTION { // Return an "app bar" color based on the current theme Color get COLOR_APP_BAR { - return Colors.blueGrey; + return Color.fromRGBO(55, 150, 175, 1); } const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1); diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 42ced88e..2078f86d 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -301,13 +301,7 @@ class InvenTreeModel { /* * Attempt to extract a custom icon for this model. - * If icon data is provided, attempt to convert to a FontAwesome icon - * - * Icon data *should* be presented something like "fas fa-boxes" / "fab fa-github" (etc): - * - * - First part specifies the *style* - * - Second part specifies the icon - * + * If icon data is provided, attempt to convert to a TablerIcon icon */ IconData? get customIcon { String icon = (jsondata["icon"] ?? "").toString().trim(); diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index c803c329..29fa99ea 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -1,4 +1,5 @@ import "dart:io"; +import "dart:math"; import "package:flutter/material.dart"; @@ -311,19 +312,29 @@ class InvenTreePart extends InvenTreeModel { String get onOrderString => simpleNumberString(onOrder); - double get inStock => getDouble("in_stock"); + double get inStock { + if (jsondata.containsKey("total_in_stock")) { + return getDouble("total_in_stock"); + } else { + return getDouble("in_stock"); + } + } String get inStockString => simpleNumberString(inStock); // Get the 'available stock' for this Part double get unallocatedStock { + double unallocated = 0; + // Note that the 'available_stock' was not added until API v35 if (jsondata.containsKey("unallocated_stock")) { - return double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0; + unallocated = double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0; } else { - return inStock; + unallocated = inStock; } + + return max(0, unallocated); } String get unallocatedStockString => simpleNumberString(unallocatedStock); diff --git a/lib/widget/part/part_detail.dart b/lib/widget/part/part_detail.dart index a11fe4c2..ee18c558 100644 --- a/lib/widget/part/part_detail.dart +++ b/lib/widget/part/part_detail.dart @@ -703,12 +703,10 @@ class _PartDisplayState extends RefreshableState { @override List getTabs(BuildContext context) { List tabs = [ - Center( - child: ListView( - children: ListTile.divideTiles( - context: context, - tiles: partTiles() - ).toList() + SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Column( + children: partTiles(), ) ), PaginatedStockItemList({"part": part.pk.toString()}) diff --git a/lib/widget/search.dart b/lib/widget/search.dart index dfa0d138..c76924f8 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -8,6 +8,8 @@ import "package:inventree/app_colors.dart"; import "package:inventree/l10.dart"; import "package:inventree/inventree/part.dart"; +import "package:inventree/inventree/company.dart"; +import "package:inventree/inventree/sales_order.dart"; import "package:inventree/inventree/purchase_order.dart"; import "package:inventree/inventree/stock.dart"; @@ -16,8 +18,10 @@ import "package:inventree/widget/order/purchase_order_list.dart"; import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/widget/stock/stock_list.dart"; import "package:inventree/widget/part/category_list.dart"; -import "package:inventree/widget/company/company_list.dart"; import "package:inventree/widget/stock/location_list.dart"; +import "package:inventree/widget/order/sales_order_list.dart"; +import "package:inventree/widget/company/company_list.dart"; +import "package:inventree/widget/company/supplier_part_list.dart"; // Widget for performing database-wide search @@ -86,12 +90,34 @@ class _SearchDisplayState extends RefreshableState { // 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; + int nSalesOrderResults = 0; + int nCompanyResults = 0; + int nSupplierPartResults = 0; + int nManufacturerPartResults = 0; + + void resetSearchResults() { + if (mounted) { + setState(() { + nPendingSearches = 0; + + nPartResults = 0; + nCategoryResults = 0; + nStockResults = 0; + nLocationResults = 0; + nPurchaseOrderResults = 0; + nSalesOrderResults = 0; + nCompanyResults = 0; + nSupplierPartResults = 0; + nManufacturerPartResults = 0; + }); + } + } // Callback when the text is being edited // Incorporates a debounce timer to restrict search frequency @@ -104,7 +130,7 @@ class _SearchDisplayState extends RefreshableState { if (immediate) { search(text); } else { - debounceTimer = Timer(Duration(milliseconds: 250), () { + debounceTimer = Timer(Duration(milliseconds: 300), () { search(text); }); } @@ -142,24 +168,33 @@ class _SearchDisplayState extends RefreshableState { "search/", body: body, expectedStatusCode: 200).then((APIResponse response) { - decrementPendingSearches(); + String searchTerm = (body["search"] ?? "").toString(); - Map results = {}; + // Only update if the results correspond to the current search term + if (searchTerm == searchController.text && mounted) { - if (response.data is Map) { - results = response.data as Map; - } + decrementPendingSearches(); - 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"); - }); + Map results = {}; + + if (response.isValid() && response.data is Map) { + results = response.data as Map; + + setState(() { + nPartResults = getSearchResultCount(results, InvenTreePart.MODEL_TYPE); + nCategoryResults = getSearchResultCount(results, InvenTreePartCategory.MODEL_TYPE); + nStockResults = getSearchResultCount(results, InvenTreeStockItem.MODEL_TYPE); + nLocationResults = getSearchResultCount(results, InvenTreeStockLocation.MODEL_TYPE); + nPurchaseOrderResults = getSearchResultCount(results, InvenTreePurchaseOrder.MODEL_TYPE); + nSalesOrderResults = getSearchResultCount(results, InvenTreeSalesOrder.MODEL_TYPE); + nCompanyResults = getSearchResultCount(results, InvenTreeCompany.MODEL_TYPE); + nSupplierPartResults = getSearchResultCount(results, InvenTreeSupplierPart.MODEL_TYPE); + nManufacturerPartResults = getSearchResultCount(results, InvenTreeManufacturerPart.MODEL_TYPE); + }); + } else { + resetSearchResults(); + } } }); } @@ -174,17 +209,7 @@ class _SearchDisplayState extends RefreshableState { return; } - setState(() { - // Do not search on an empty string - nPartResults = 0; - nCategoryResults = 0; - nStockResults = 0; - nLocationResults = 0; - nSupplierResults = 0; - nPurchaseOrderResults = 0; - - nPendingSearches = 0; - }); + resetSearchResults(); // Cancel the previous search query (if in progress) if (_search_query != null) { @@ -201,46 +226,34 @@ class _SearchDisplayState extends RefreshableState { // Consolidated search allows us to perform *all* searches in a single query if (api.supportsConsolidatedSearch) { + Map body = { "limit": 1, "search": term, + + InvenTreePart.MODEL_TYPE: {}, + InvenTreePartCategory.MODEL_TYPE: {}, + InvenTreeStockItem.MODEL_TYPE: {}, + InvenTreeStockLocation.MODEL_TYPE: {}, + InvenTreePurchaseOrder.MODEL_TYPE: {}, + InvenTreeSalesOrder.MODEL_TYPE: {}, + InvenTreeCompany.MODEL_TYPE: {}, + InvenTreeSupplierPart.MODEL_TYPE: {}, + InvenTreeManufacturerPart.MODEL_TYPE: {}, }; - // Part search - if (InvenTreePart().canView) { - body["part"] = {}; - } - - // PartCategory search - if (InvenTreePartCategory().canView) { - body["partcategory"] = {}; - } - - // StockItem search - if (InvenTreeStockItem().canView) { - body["stockitem"] = { - "in_stock": true, - }; - } - - // StockLocation search - if (InvenTreeStockLocation().canView) { - body["stocklocation"] = {}; - } - - // PurchaseOrder search - if (InvenTreePurchaseOrder().canView) { - body["purchaseorder"] = { - "outstanding": true - }; - } - if (body.isNotEmpty) { - nPendingSearches++; - _search_query = CancelableOperation.fromFuture( - _perform_search(body), - ); + if (mounted) { + setState(() { + nPendingSearches = 1; + }); + + _search_query = CancelableOperation.fromFuture( + _perform_search(body), + ); + } + } } else { legacySearch(term); @@ -466,31 +479,6 @@ class _SearchDisplayState extends RefreshableState { ); } - // Suppliers - if (nSupplierResults > 0) { - results.add( - ListTile( - title: Text(L10().suppliers), - leading: Icon(TablerIcons.building), - trailing: Text("${nSupplierResults}"), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CompanyListWidget( - L10().suppliers, - { - "is_supplier": "true", - "original_search": query - } - ) - ) - ); - }, - ) - ); - } - // Purchase orders if (nPurchaseOrderResults > 0) { results.add( @@ -514,6 +502,76 @@ class _SearchDisplayState extends RefreshableState { ); } + // Sales orders + if (nSalesOrderResults > 0) { + results.add( + ListTile( + title: Text(L10().salesOrders), + leading: Icon(TablerIcons.shopping_cart), + trailing: Text("${nSalesOrderResults}"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SalesOrderListWidget( + filters: { + "original_search": query + } + ) + ) + ); + }, + ) + ); + } + + // Company results + if (nCompanyResults > 0) { + results.add( + ListTile( + title: Text(L10().companies), + leading: Icon(TablerIcons.building), + trailing: Text("${nCompanyResults}"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CompanyListWidget( + L10().companies, + { + "original_search": query + } + ) + ) + ); + }, + ) + ); + } + + // Supplier part results + if (nSupplierPartResults > 0) { + results.add( + ListTile( + title: Text(L10().supplierParts), + leading: Icon(TablerIcons.box), + trailing: Text("${nSupplierPartResults}"), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SupplierPartList( + { + "original_search": query + } + ) + ) + ); + }, + ) + ); + } + if (isSearching()) { tiles.add( ListTile(