From 7301243ed631b3f5e374b5eddf664fb828007e3f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 6 Jul 2022 20:24:40 +1000 Subject: [PATCH] Major overhaul of "paginated list" widget class - Simplify implementation - Create mixin class for code reuse - Allow custom app-bar - Allow custom ordering / sorting options - Improve code commenting / readability --- lib/widget/bom_list.dart | 54 +++---------- lib/widget/notifications.dart | 2 +- lib/widget/paginator.dart | 128 ++++++++++++++++++------------ lib/widget/part_detail.dart | 4 +- lib/widget/refreshable_state.dart | 85 +++++++++++--------- lib/widget/search.dart | 4 +- 6 files changed, 143 insertions(+), 134 deletions(-) diff --git a/lib/widget/bom_list.dart b/lib/widget/bom_list.dart index 65ee0131..56d8faa5 100644 --- a/lib/widget/bom_list.dart +++ b/lib/widget/bom_list.dart @@ -17,48 +17,6 @@ import "package:inventree/widget/refreshable_state.dart"; -/* - * Widget for displaying a list of BomItems for the specified 'parent' Part instance - */ -class BomList extends StatefulWidget { - - const BomList(this.parent); - - final InvenTreePart parent; - - @override - _BomListState createState() => _BomListState(parent); - -} - - -class _BomListState extends PaginatedState { - - _BomListState(this.parent); - - final InvenTreePart parent; - - @override - String getAppBarTitle(BuildContext context) => L10().billOfMaterials; - - @override - String get prefix => "bom_"; - - @override - Map get orderingOptions => { - "quantity": L10().quantity, - "sub_part": L10().part, - }; - - @override - Widget getBody(BuildContext context) { - return PaginatedBomList({ - "part": parent.pk.toString(), - }); - } -} - - /* * Create a paginated widget displaying a list of BomItem objects */ @@ -78,13 +36,23 @@ class PaginatedBomList extends StatefulWidget { class _PaginatedBomListState extends PaginatedSearchState { - _PaginatedBomListState(Map filters, this.onTotalChanged) : super(filters); + _PaginatedBomListState(Map filters, this.onTotalChanged) : super(filters, fullscreen: true); Function(int)? onTotalChanged; @override String get prefix => "bom_"; + @override + Map get orderingOptions => { + "quantity": L10().quantity, + "sub_part": L10().part, + }; + + + @override + String getAppBarTitle(BuildContext context) => L10().billOfMaterials; + @override Future requestPage(int limit, int offset, Map params) async { diff --git a/lib/widget/notifications.dart b/lib/widget/notifications.dart index 8c03343b..0af32b8b 100644 --- a/lib/widget/notifications.dart +++ b/lib/widget/notifications.dart @@ -24,7 +24,7 @@ class _NotificationState extends RefreshableState { List notifications = []; @override - AppBar? buildAppBar(BuildContext context) { + AppBar? buildAppBar(BuildContext context, GlobalKey key) { // No app bar for the notification widget return null; } diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart index 4d721d55..a9971ecb 100644 --- a/lib/widget/paginator.dart +++ b/lib/widget/paginator.dart @@ -14,32 +14,32 @@ import "package:inventree/widget/refreshable_state.dart"; /* - * Generic widget class for displaying a "paginated list". - * Provides some basic functionality for adjusting ordering and filtering options + * 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 class PaginatedState extends RefreshableState { +class PaginatedSearchState extends State with BaseWidgetProperties { + + PaginatedSearchState(this.filters, {this.fullscreen = true}); + + final _key = GlobalKey(); + + final Map filters; + + static const _pageSize = 25; + + // Determine if this widget is shown "fullscreen" (i.e. with appbar) + final bool fullscreen; // Prefix for storing and loading pagination options + // Override in implementing class String get prefix => "prefix_"; - // Ordering options for this paginated state (override in implementing class) + // Return a map of sorting options available for this list + // Should be overridden by an implementing subclass Map get orderingOptions => {}; - @override - List getAppBarActions(BuildContext context) { - List actions = []; - - // If ordering options have been provided - if (orderingOptions.isNotEmpty) { - actions.add(IconButton( - icon: FaIcon(FontAwesomeIcons.sort), - onPressed: () => _updateFilters(context), - )); - } - - return actions; - } - // Return the selected ordering "field" for this list widget Future orderingField() async { dynamic field = await InvenTreeSettingsManager().getValue("${prefix}ordering_field", null); @@ -62,9 +62,21 @@ abstract class PaginatedState extends RefreshableState return order == "+" ? "+" : "-"; } - // Update the (configurable) filters for this paginated list - Future _updateFilters(BuildContext context) async { + // Return string for determining 'ordering' of paginated list + Future get orderingString async { + dynamic field = await orderingField(); + dynamic order = await orderingOrder(); + // Return an empty string if no field is provided + if (field.toString().isEmpty) { + return ""; + } + + return "${order}${field}"; + } + + // Update the (configurable) filters for this paginated list + Future _saveOrderingOptions(BuildContext context) async { // Retrieve stored setting dynamic _field = await orderingField(); dynamic _order = await orderingOrder(); @@ -96,12 +108,12 @@ abstract class PaginatedState extends RefreshableState "value": _order, "choices": [ { - "value": "+", - "display_name": "Ascending", + "value": "+", + "display_name": "Ascending", }, { - "value": "-", - "display_name": "Descending", + "value": "-", + "display_name": "Descending", } ] } @@ -123,27 +135,12 @@ abstract class PaginatedState extends RefreshableState await InvenTreeSettingsManager().setValue("${prefix}ordering_field", f); await InvenTreeSettingsManager().setValue("${prefix}ordering_order", o); - // Refresh the widget - setState(() {}); + // Refresh data from the server + _pagingController.refresh(); } ); } -} - - -class PaginatedSearchState extends State { - - PaginatedSearchState(this.filters); - - final Map filters; - - static const _pageSize = 25; - - // Prefix for storing and loading pagination options - // Override in implementing class - String get prefix => "prefix_"; - // Search query term String searchTerm = ""; @@ -176,19 +173,12 @@ class PaginatedSearchState extends State { return null; } - Future get ordering async { - dynamic field = await InvenTreeSettingsManager().getValue("${prefix}ordering_field", ""); - dynamic order = await InvenTreeSettingsManager().getValue("${prefix}ordering_order", "+"); - - return "${order}${field}"; - } - Future _fetchPage(int pageKey) async { try { Map params = filters; params["search"] = "${searchTerm}"; - params["ordering"] = await ordering; + params["ordering"] = await orderingString; final page = await requestPage( _pageSize, @@ -234,6 +224,8 @@ class PaginatedSearchState extends State { _pagingController.refresh(); } + // Function to construct a single paginated item + // Must be overridden in an implementing subclass Widget buildItem(BuildContext context, InvenTreeModel item) { // This method must be overridden by the child class @@ -243,10 +235,31 @@ class PaginatedSearchState extends State { ); } + // Return a string which is displayed when there are no results + // Can be overridden by an implementing subclass String get noResultsText => L10().noResults; @override Widget build (BuildContext context) { + + if (fullscreen) { + return Scaffold( + key: _key, + appBar: buildAppBar(context, _key), + drawer: getDrawer(context), + body: Builder( + builder: (BuildContext ctx) { + return getBody(ctx); + } + ) + ); + } else { + return getBody(context); + } + } + + @override + Widget getBody(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -277,6 +290,21 @@ class PaginatedSearchState extends State { ); } + @override + List getAppBarActions(BuildContext context) { + List actions = []; + + // If ordering options have been provided + if (orderingOptions.isNotEmpty) { + actions.add(IconButton( + icon: FaIcon(FontAwesomeIcons.sort), + onPressed: () => _saveOrderingOptions(context), + )); + } + + return actions; + } + } diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index c7e36b85..abf17423 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -354,7 +354,9 @@ class _PartDisplayState extends RefreshableState { Navigator.push( context, MaterialPageRoute( - builder: (context) => BomList(part) + builder: (context) => PaginatedBomList({ + "part": part.pk.toString(), + }) ) ); } diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart index 35185690..dc29c7de 100644 --- a/lib/widget/refreshable_state.dart +++ b/lib/widget/refreshable_state.dart @@ -3,7 +3,53 @@ import "package:inventree/widget/drawer.dart"; import "package:flutter/material.dart"; -abstract class RefreshableState extends State { +/* + * Simple mixin class which defines simple methods for defining widget properties + */ +mixin BaseWidgetProperties { + + // Return a list of appBar actions (default = None) + List getAppBarActions(BuildContext context) { + return []; + } + + // Return a title for the appBar + String getAppBarTitle(BuildContext context) { return "--- app bar ---"; } + + // Function to construct a drawer (override if needed) + Widget getDrawer(BuildContext context) { + return InvenTreeDrawer(context); + } + + // Function to construct a body (MUST BE PROVIDED) + Widget getBody(BuildContext context) { + + // Default return is an empty ListView + return ListView(); + } + + Widget? getBottomNavBar(BuildContext context) { + return null; + } + + AppBar? buildAppBar(BuildContext context, GlobalKey key) { + return AppBar( + title: Text(getAppBarTitle(context)), + actions: getAppBarActions(context), + leading: backButton(context, key), + ); + } + +} + + +/* + * Abstract base class which provides generic "refresh" functionality. + * + * - Drag down and release to 'refresh' the widget + * - Define some method which runs to 'refresh' the widget state + */ +abstract class RefreshableState extends State with BaseWidgetProperties { final refreshableKey = GlobalKey(); @@ -25,12 +71,6 @@ abstract class RefreshableState extends State { }); } - List getAppBarActions(BuildContext context) { - return []; - } - - String getAppBarTitle(BuildContext context) { return "App Bar Title"; } - @override void initState() { super.initState(); @@ -60,34 +100,6 @@ abstract class RefreshableState extends State { }); } - // Function to construct a drawer (override if needed) - Widget getDrawer(BuildContext context) { - return InvenTreeDrawer(context); - } - - // Function to construct a body (MUST BE PROVIDED) - Widget getBody(BuildContext context) { - - // Default return is an empty ListView - return ListView(); - } - - Widget? getBottomNavBar(BuildContext context) { - return null; - } - - Widget? getFab(BuildContext context) { - return null; - } - - AppBar? buildAppBar(BuildContext context) { - return AppBar( - title: Text(getAppBarTitle(context)), - actions: getAppBarActions(context), - leading: backButton(context, refreshableKey), - ); - } - @override Widget build(BuildContext context) { @@ -96,9 +108,8 @@ abstract class RefreshableState extends State { return Scaffold( key: refreshableKey, - appBar: buildAppBar(context), + appBar: buildAppBar(context, refreshableKey), drawer: getDrawer(context), - floatingActionButton: getFab(context), body: Builder( builder: (BuildContext context) { return RefreshIndicator( diff --git a/lib/widget/search.dart b/lib/widget/search.dart index 49662476..dcbe47e8 100644 --- a/lib/widget/search.dart +++ b/lib/widget/search.dart @@ -53,9 +53,9 @@ class _SearchDisplayState extends RefreshableState { String getAppBarTitle(BuildContext context) => L10().search; @override - AppBar? buildAppBar(BuildContext context) { + AppBar? buildAppBar(BuildContext context, GlobalKey key) { if (hasAppBar) { - return super.buildAppBar(context); + return super.buildAppBar(context, key); } else { return null; }