2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-06-12 02:05:29 +00:00

List refactor (#179)

* Catch paginator bug if widget is disposed before request returns

* Refactoring paginated query widget

- Add option to enable / disable search filters

* Major refactor of paginated search widget

- Learned something new.. a state can access widget.<attribute>
- THIS CHANGES EVERTHING

* Preferences: Add code for tri-state values

- Also improve unit testing for preferences code

* Allow boolean form fields to be optionally tristate

* paginator: Allow custom boolean filters

* Remove outdated filtering preferences

* Refactor filter options

- Allow specification of more detailed options

* Add custom filters for "part" list

* filter tweaks

* Remove legacy "SublocationList" widget

* Add filtering option for locationlist

* Updates for stock location widget

* Refactor category display widget

* More widget refactoring

* Update main search widget

* Fix unit tests

* Improve filtering on BOM display page
This commit is contained in:
Oliver
2022-07-19 23:29:01 +10:00
committed by GitHub
parent e03a8561b9
commit 13ebaf43e1
23 changed files with 667 additions and 403 deletions

View File

@ -15,23 +15,72 @@ import "package:inventree/widget/refreshable_state.dart";
/*
* Generic stateful widget for displaying paginated data retrieved via the API
*
* - Can be displayed as "full screen" (with app-bar and drawer)
* - Can be displayed as a standalone widget
* Abstract base widget class for rendering a PaginatedSearchState
*/
class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseWidgetProperties {
abstract class PaginatedSearchWidget extends StatefulWidget {
PaginatedSearchState(this.filters);
const PaginatedSearchWidget({this.filters = const {}, this.showSearch = false});
final Map<String, String> filters;
final bool showSearch;
}
/*
* Generic stateful widget for displaying paginated data retrieved via the API
*/
abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends State<T> with BaseWidgetProperties {
static const _pageSize = 25;
// Prefix for storing and loading pagination options
// Override in implementing class
String get prefix => "prefix_";
// Return a map of boolean filtering options available for this list
// Should be overridden by an implementing subclass
Map<String, Map<String, dynamic>> get filterOptions => {};
// Return the boolean value of a particular boolean filter
Future<bool?> getBooleanFilterValue(String key) async {
key = "${prefix}bool_${key}";
Map<String, dynamic> opts = filterOptions[key] ?? {};
bool? backup;
dynamic v = opts["default"];
if (v is bool) {
backup = v;
}
final result = await InvenTreeSettingsManager().getTriState(key, backup);
return result;
}
// Set the boolean value of a particular boolean filter
Future<void> setBooleanFilterValue(String key, bool? value) async {
key = "${prefix}bool_${key}";
await InvenTreeSettingsManager().setValue(key, value);
}
// Construct the boolean filter options for this list
Future<Map<String, String>> constructBooleanFilters() async {
Map<String, String> f = {};
for (String k in filterOptions.keys) {
bool? value = await getBooleanFilterValue(k);
if (value is bool) {
f[k] = value ? "true" : "false";
}
}
return f;
}
// Return a map of sorting options available for this list
// Should be overridden by an implementing subclass
Map<String, String> get orderingOptions => {};
@ -115,6 +164,34 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
}
};
// Add in boolean filter options
for (String key in filterOptions.keys) {
Map<String, dynamic> opts = filterOptions[key] ?? {};
// Determine field information
String label = (opts["label"] ?? key) as String;
String? help_text = opts["help_text"] as String?;
bool tristate = (opts["tristate"] ?? true) as bool;
bool? v = await getBooleanFilterValue(key);
// Prevent null value if not tristate
if (!tristate && v == null) {
v = false;
}
// Add in the particular field
fields[key] = {
"type": "boolean",
"display_name": label,
"label": label,
"help_text": help_text,
"value": v,
"tristate": (opts["tristate"] ?? true) as bool,
};
}
// Launch an interactive form for the user to select options
launchApiForm(
context,
@ -132,6 +209,20 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
await InvenTreeSettingsManager().setValue("${prefix}ordering_field", f);
await InvenTreeSettingsManager().setValue("${prefix}ordering_order", o);
// Save boolean fields
for (String key in filterOptions.keys) {
bool? v;
dynamic value = data[key];
if (value is bool) {
v = value;
}
await setBooleanFilterValue(key, v);
}
// Refresh data from the server
_pagingController.refresh();
}
@ -189,10 +280,12 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
*/
Future<void> _fetchPage(int pageKey) async {
try {
Map<String, String> params = filters;
Map<String, String> params = widget.filters;
// Include user search term
params["search"] = "${searchTerm}";
if (searchTerm.isNotEmpty) {
params["search"] = "${searchTerm}";
}
// Use custom query ordering if available
String o = await orderingString;
@ -200,12 +293,24 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
params["ordering"] = o;
}
Map<String, String> f = await constructBooleanFilters();
if (f.isNotEmpty) {
params.addAll(f);
}
final page = await requestPage(
_pageSize,
pageKey,
params
);
// We may have disposed of the widget while the request was in progress
// If this is the case, abort
if (!mounted) {
return;
}
int pageLength = page?.length ?? 0;
int pageCount = page?.count ?? 0;
@ -263,32 +368,39 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW
@override
Widget build (BuildContext context) {
List<Widget> children = [];
if (widget.showSearch) {
children.add(buildSearchInput(context));
}
children.add(
Expanded(
child: CustomScrollView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
slivers: <Widget>[
PagedSliverList.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>(
itemBuilder: (context, item, index) {
return buildItem(context, item);
},
noItemsFoundIndicatorBuilder: (context) {
return NoResultsWidget(noResultsText);
}
),
separatorBuilder: (context, item) => const Divider(height: 1),
)
]
)
)
);
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
buildSearchInput(context),
Expanded(
child: CustomScrollView(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
slivers: <Widget>[
PagedSliverList.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>(
itemBuilder: (context, item, index) {
return buildItem(context, item);
},
noItemsFoundIndicatorBuilder: (context) {
return NoResultsWidget(noResultsText);
}
),
separatorBuilder: (context, item) => const Divider(height: 1),
)
]
)
)
]
children: children,
);
}