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:
parent
524c5469f1
commit
665de2bd5a
@ -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)
|
||||||
|
|
||||||
--------
|
--------
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
|
@ -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()})
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user