2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-27 21:16:48 +00:00

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
This commit is contained in:
Oliver 2024-12-14 17:29:35 +11:00 committed by GitHub
parent 524c5469f1
commit 665de2bd5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 168 additions and 105 deletions

View File

@ -7,14 +7,15 @@ Thanks to the following contributors, for their work building this app!
- [GoryMoon](https://github.com/GoryMoon) - [GoryMoon](https://github.com/GoryMoon)
- [simonkuehling](https://github.com/simonkuehling) - [simonkuehling](https://github.com/simonkuehling)
- [Bobbe](https://github.com/30350n) - [Bobbe](https://github.com/30350n)
- [awnz](https://github.com/awnz)
- [joaomnuno](https://github.com/joaomnuno)
-------- --------
## Assets ## Assets
The InvenTree App makes use of the following third party 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) - Sound files have been sourced from [zapsplat](https://www.zapsplat.com)
-------- --------

View File

@ -3,6 +3,7 @@
- Add support for ManufacturerPart model - Add support for ManufacturerPart model
- Support barcode scanning for ManufacturerPart - Support barcode scanning for ManufacturerPart
- Fix bugs in global search view
- Fixes barcode scanning bug which prevents scanning of DataMatrix codes - Fixes barcode scanning bug which prevents scanning of DataMatrix codes
- Display "destination" information in PurchaseOrder detail view - Display "destination" information in PurchaseOrder detail view
- Pre-fill "location" field when receiving items against PurchaseOrder - Pre-fill "location" field when receiving items against PurchaseOrder

View File

@ -29,7 +29,7 @@ Color get COLOR_ACTION {
// Return an "app bar" color based on the current theme // Return an "app bar" color based on the current theme
Color get COLOR_APP_BAR { 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); const Color COLOR_WARNING = Color.fromRGBO(250, 150, 50, 1);

View File

@ -301,13 +301,7 @@ class InvenTreeModel {
/* /*
* Attempt to extract a custom icon for this model. * Attempt to extract a custom icon for this model.
* If icon data is provided, attempt to convert to a FontAwesome icon * If icon data is provided, attempt to convert to a TablerIcon 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
*
*/ */
IconData? get customIcon { IconData? get customIcon {
String icon = (jsondata["icon"] ?? "").toString().trim(); String icon = (jsondata["icon"] ?? "").toString().trim();

View File

@ -1,4 +1,5 @@
import "dart:io"; import "dart:io";
import "dart:math";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
@ -311,19 +312,29 @@ class InvenTreePart extends InvenTreeModel {
String get onOrderString => simpleNumberString(onOrder); 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); String get inStockString => simpleNumberString(inStock);
// Get the 'available stock' for this Part // Get the 'available stock' for this Part
double get unallocatedStock { double get unallocatedStock {
double unallocated = 0;
// Note that the 'available_stock' was not added until API v35 // Note that the 'available_stock' was not added until API v35
if (jsondata.containsKey("unallocated_stock")) { if (jsondata.containsKey("unallocated_stock")) {
return double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0; unallocated = double.tryParse(jsondata["unallocated_stock"].toString()) ?? 0;
} else { } else {
return inStock; unallocated = inStock;
} }
return max(0, unallocated);
} }
String get unallocatedStockString => simpleNumberString(unallocatedStock); String get unallocatedStockString => simpleNumberString(unallocatedStock);

View File

@ -703,12 +703,10 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
@override @override
List<Widget> getTabs(BuildContext context) { List<Widget> getTabs(BuildContext context) {
List<Widget> tabs = [ List<Widget> tabs = [
Center( SingleChildScrollView(
child: ListView( physics: AlwaysScrollableScrollPhysics(),
children: ListTile.divideTiles( child: Column(
context: context, children: partTiles(),
tiles: partTiles()
).toList()
) )
), ),
PaginatedStockItemList({"part": part.pk.toString()}) PaginatedStockItemList({"part": part.pk.toString()})

View File

@ -8,6 +8,8 @@ import "package:inventree/app_colors.dart";
import "package:inventree/l10.dart"; import "package:inventree/l10.dart";
import "package:inventree/inventree/part.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/purchase_order.dart";
import "package:inventree/inventree/stock.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/refreshable_state.dart";
import "package:inventree/widget/stock/stock_list.dart"; import "package:inventree/widget/stock/stock_list.dart";
import "package:inventree/widget/part/category_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/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 // Widget for performing database-wide search
@ -86,12 +90,34 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
// Individual search result count (for legacy search API) // Individual search result count (for legacy search API)
int nPendingSearches = 0; 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 nPurchaseOrderResults = 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 // Callback when the text is being edited
// Incorporates a debounce timer to restrict search frequency // Incorporates a debounce timer to restrict search frequency
@ -104,7 +130,7 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
if (immediate) { if (immediate) {
search(text); search(text);
} else { } else {
debounceTimer = Timer(Duration(milliseconds: 250), () { debounceTimer = Timer(Duration(milliseconds: 300), () {
search(text); search(text);
}); });
} }
@ -142,24 +168,33 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
"search/", "search/",
body: body, body: body,
expectedStatusCode: 200).then((APIResponse response) { expectedStatusCode: 200).then((APIResponse response) {
decrementPendingSearches();
String searchTerm = (body["search"] ?? "").toString();
Map<String, dynamic> results = {}; // Only update if the results correspond to the current search term
if (searchTerm == searchController.text && mounted) {
if (response.data is Map<String, dynamic>) { decrementPendingSearches();
results = response.data as Map<String, dynamic>;
}
if (mounted) { Map<String, dynamic> results = {};
setState(() {
nPartResults = getSearchResultCount(results, "part"); if (response.isValid() && response.data is Map<String, dynamic>) {
nCategoryResults = getSearchResultCount(results, "partcategory"); results = response.data as Map<String, dynamic>;
nStockResults = getSearchResultCount(results, "stockitem");
nLocationResults = getSearchResultCount(results, "stocklocation"); setState(() {
nSupplierResults = 0; //getSearchResultCount(results, "") nPartResults = getSearchResultCount(results, InvenTreePart.MODEL_TYPE);
nPurchaseOrderResults = getSearchResultCount(results, "purchaseorder"); 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<SearchWidget> {
return; return;
} }
setState(() { resetSearchResults();
// Do not search on an empty string
nPartResults = 0;
nCategoryResults = 0;
nStockResults = 0;
nLocationResults = 0;
nSupplierResults = 0;
nPurchaseOrderResults = 0;
nPendingSearches = 0;
});
// Cancel the previous search query (if in progress) // Cancel the previous search query (if in progress)
if (_search_query != null) { if (_search_query != null) {
@ -201,46 +226,34 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
// Consolidated search allows us to perform *all* searches in a single query // Consolidated search allows us to perform *all* searches in a single query
if (api.supportsConsolidatedSearch) { if (api.supportsConsolidatedSearch) {
Map<String, dynamic> body = { Map<String, dynamic> body = {
"limit": 1, "limit": 1,
"search": term, "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) { if (body.isNotEmpty) {
nPendingSearches++;
_search_query = CancelableOperation.fromFuture( if (mounted) {
_perform_search(body), setState(() {
); nPendingSearches = 1;
});
_search_query = CancelableOperation.fromFuture(
_perform_search(body),
);
}
} }
} else { } else {
legacySearch(term); legacySearch(term);
@ -466,31 +479,6 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
); );
} }
// 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 // Purchase orders
if (nPurchaseOrderResults > 0) { if (nPurchaseOrderResults > 0) {
results.add( results.add(
@ -514,6 +502,76 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> {
); );
} }
// 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()) { if (isSearching()) {
tiles.add( tiles.add(
ListTile( ListTile(