From c878f37ec2326f8cfaeeda78f8081e6148882543 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Tue, 5 Jul 2022 21:42:55 +1000 Subject: [PATCH 01/12] Implementing a generic "ordering" option configuration for paginated list widget --- lib/widget/bom_list.dart | 15 ++++- lib/widget/paginator.dart | 126 +++++++++++++++++++++++++++++++++++++- pubspec.yaml | 12 ++-- 3 files changed, 144 insertions(+), 9 deletions(-) diff --git a/lib/widget/bom_list.dart b/lib/widget/bom_list.dart index 5e51feeb..e2b6f136 100644 --- a/lib/widget/bom_list.dart +++ b/lib/widget/bom_list.dart @@ -1,8 +1,9 @@ - import "package:flutter/material.dart"; +import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/api.dart"; +import "package:inventree/api_form.dart"; import "package:inventree/helpers.dart"; import "package:inventree/inventree/bom.dart"; import "package:inventree/l10.dart"; @@ -15,6 +16,7 @@ import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/refreshable_state.dart"; + /* * Widget for displaying a list of BomItems for the specified 'parent' Part instance */ @@ -30,7 +32,7 @@ class BomList extends StatefulWidget { } -class _BomListState extends RefreshableState<BomList> { +class _BomListState extends PaginatedState<BomList> { _BomListState(this.parent); @@ -39,6 +41,15 @@ class _BomListState extends RefreshableState<BomList> { @override String getAppBarTitle(BuildContext context) => L10().billOfMaterials; + @override + String get prefix => "bom_"; + + @override + Map<String, String> get orderingOptions => { + "quantity": L10().quantity, + "part": L10().part, + }; + @override Widget getBody(BuildContext context) { return PaginatedBomList({ diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart index aa62647d..26bae40a 100644 --- a/lib/widget/paginator.dart +++ b/lib/widget/paginator.dart @@ -3,9 +3,129 @@ import "package:flutter/material.dart"; import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:infinite_scroll_pagination/infinite_scroll_pagination.dart"; +import "package:inventree/api_form.dart"; +import "package:inventree/l10.dart"; + import "package:inventree/inventree/model.dart"; import "package:inventree/inventree/sentry.dart"; -import "package:inventree/l10.dart"; +import "package:inventree/preferences.dart"; + +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 + */ +abstract class PaginatedState<T extends StatefulWidget> extends RefreshableState<T> { + + // Prefix for storing and loading pagination options + String get prefix => "prefix_"; + + // Ordering options for this paginated state (override in implementing class) + Map<String, String> get orderingOptions => {}; + + @override + List<Widget> getAppBarActions(BuildContext context) { + List<Widget> 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<String> orderingField() async { + dynamic field = await InvenTreeSettingsManager().getValue("${prefix}ordering_field", null); + + if (field != null) { + return field.toString(); + } else if (orderingOptions.isNotEmpty) { + // By default, return the first specified key + return orderingOptions.keys.first; + } else { + return ""; + } + } + + // Return the selected ordering "order" ("+" or "-") for this list widget + Future<String> orderingOrder() async { + dynamic order = await InvenTreeSettingsManager().getValue("${prefix}ordering_order", "+"); + + return order == "+" ? "+" : "-"; + } + + // Update the (configurable) filters for this paginated list + Future<void> _updateFilters(BuildContext context) async { + + // Retrieve stored setting + dynamic _field = await orderingField(); + dynamic _order = await orderingOrder(); + + // Construct the 'ordering' options + List<Map<String, dynamic>> _opts = []; + + orderingOptions.forEach((k, v) => _opts.add({ + "value": k.toString(), + "display_name": v.toString() + })); + + if (_field == null && _opts.isNotEmpty) { + _field = _opts.first["value"]; + } + + Map<String, dynamic> fields = { + "ordering_field": { + "type": "choice", + "label": "Ordering Field", + "required": true, + "choices": _opts, + "value": _field, + }, + "ordering_order": { + "type": "choice", + "label": "Ordering Direction", + "required": true, + "value": _order, + "choices": [ + { + "value": "+", + "display_name": "Ascending", + }, + { + "value": "-", + "display_name": "Descending", + } + ] + } + }; + + launchApiForm( + context, + "...filtering...", + "", + fields, + icon: FontAwesomeIcons.checkCircle, + onSuccess: (Map<String, dynamic> data) async { + + // Extract data from the processed form + String f = (data["ordering_field"] ?? _field) as String; + String o = (data["ordering_order"] ?? _order) as String; + + // Save values to settings + await InvenTreeSettingsManager().setValue("${prefix}ordering_field", f); + await InvenTreeSettingsManager().setValue("${prefix}ordering_order", o); + } + ); + } + +} class PaginatedSearchState<T extends StatefulWidget> extends State<T> { @@ -21,6 +141,10 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> { int resultCount = 0; + // List of variables by which the list can be "ordered". + // Override in any implementing sub-class + List<String> orderingFilters = []; + // Text controller final TextEditingController searchController = TextEditingController(); diff --git a/pubspec.yaml b/pubspec.yaml index 16a35064..6528ab40 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,11 +10,11 @@ dependencies: audioplayers: ^0.20.1 # Play audio files cached_network_image: ^3.2.0 # Download and cache remote images - camera: ^0.9.4 # Camera + camera: ^0.9.4 # Camera cupertino_icons: ^1.0.3 datetime_picker_formfield: ^2.0.0 # Date / time picker device_info_plus: ^3.2.2 # Information about the device - dropdown_search: ^0.6.3 # Dropdown autocomplete form fields + dropdown_search: ^0.6.3 # Dropdown autocomplete form fields file_picker: ^4.5.1 # Select files from the device flutter: sdk: flutter @@ -28,14 +28,14 @@ dependencies: infinite_scroll_pagination: ^3.1.0 # Let the server do all the work! intl: ^0.17.0 one_context: ^1.1.0 # Dialogs without requiring context - open_file: ^3.2.1 # Open local files + open_file: ^3.2.1 # Open local files package_info_plus: ^1.0.4 # App information introspection path: ^1.8.0 - path_provider: ^2.0.2 # Local file storage + path_provider: ^2.0.2 # Local file storage qr_code_scanner: ^0.7.0 # Barcode scanning sembast: ^3.1.0+2 # NoSQL data storage - sentry_flutter: ^6.4.0 # Error reporting - url_launcher: ^6.0.9 # Open link in system browser + sentry_flutter: ^6.4.0 # Error reporting + url_launcher: ^6.0.9 # Open link in system browser dev_dependencies: flutter_launcher_icons: ^0.9.0 From 6c1099356f466aa58b0565d13e367201477154e0 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 09:49:40 +1000 Subject: [PATCH 02/12] Enable basic ordering for BOM list --- lib/inventree/bom.dart | 3 +++ lib/widget/bom_list.dart | 7 +++++-- lib/widget/paginator.dart | 27 +++++++++++++++++++++------ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/inventree/bom.dart b/lib/inventree/bom.dart index 52ad5782..20487bcd 100644 --- a/lib/inventree/bom.dart +++ b/lib/inventree/bom.dart @@ -34,6 +34,9 @@ class InvenTreeBomItem extends InvenTreeModel { }; } + // Extract the 'reference' value associated with this BomItem + String get reference => (jsondata["reference"] ?? "") as String; + // Extract the 'quantity' value associated with this BomItem double get quantity => double.tryParse(jsondata["quantity"].toString()) ?? 0; diff --git a/lib/widget/bom_list.dart b/lib/widget/bom_list.dart index e2b6f136..65ee0131 100644 --- a/lib/widget/bom_list.dart +++ b/lib/widget/bom_list.dart @@ -47,7 +47,7 @@ class _BomListState extends PaginatedState<BomList> { @override Map<String, String> get orderingOptions => { "quantity": L10().quantity, - "part": L10().part, + "sub_part": L10().part, }; @override @@ -82,6 +82,9 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { Function(int)? onTotalChanged; + @override + String get prefix => "bom_"; + @override Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { @@ -102,7 +105,7 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { return ListTile( title: Text(title), - subtitle: Text(description), + subtitle: Text(bomItem.reference), trailing: Text( simpleNumberString(bomItem.quantity), style: TextStyle(fontWeight: FontWeight.bold), diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart index 26bae40a..4d721d55 100644 --- a/lib/widget/paginator.dart +++ b/lib/widget/paginator.dart @@ -44,7 +44,8 @@ abstract class PaginatedState<T extends StatefulWidget> extends RefreshableState Future<String> orderingField() async { dynamic field = await InvenTreeSettingsManager().getValue("${prefix}ordering_field", null); - if (field != null) { + if (field != null && orderingOptions.containsKey(field.toString())) { + // A valid ordering field has been found return field.toString(); } else if (orderingOptions.isNotEmpty) { // By default, return the first specified key @@ -121,6 +122,9 @@ abstract class PaginatedState<T extends StatefulWidget> extends RefreshableState // Save values to settings await InvenTreeSettingsManager().setValue("${prefix}ordering_field", f); await InvenTreeSettingsManager().setValue("${prefix}ordering_order", o); + + // Refresh the widget + setState(() {}); } ); } @@ -136,15 +140,15 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> { static const _pageSize = 25; + // Prefix for storing and loading pagination options + // Override in implementing class + String get prefix => "prefix_"; + // Search query term String searchTerm = ""; int resultCount = 0; - // List of variables by which the list can be "ordered". - // Override in any implementing sub-class - List<String> orderingFilters = []; - // Text controller final TextEditingController searchController = TextEditingController(); @@ -172,11 +176,19 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> { return null; } + Future<String> get ordering async { + dynamic field = await InvenTreeSettingsManager().getValue("${prefix}ordering_field", ""); + dynamic order = await InvenTreeSettingsManager().getValue("${prefix}ordering_order", "+"); + + return "${order}${field}"; + } + Future<void> _fetchPage(int pageKey) async { try { Map<String, String> params = filters; params["search"] = "${searchTerm}"; + params["ordering"] = await ordering; final page = await requestPage( _pageSize, @@ -299,7 +311,10 @@ class PaginatedSearchWidget extends StatelessWidget { ), trailing: Text( "${results}", - style: TextStyle(fontWeight: FontWeight.bold), + style: TextStyle( + fontWeight: FontWeight.bold, + fontStyle: FontStyle.italic + ), ), ); } From 979f9501291c69a1f7796da17e92eee41b098621 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 09:51:05 +1000 Subject: [PATCH 03/12] Update release notes --- assets/release_notes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/assets/release_notes.md b/assets/release_notes.md index fe7cd9b7..bf6e7837 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -1,6 +1,14 @@ ## InvenTree App Release Notes --- +### 0.8.0 - July 2022 +--- + +- Display part variants in the part detail view +- Display Bill of Materials in the part detail view +- Indicate available quantity in stock detail view +- Adds configurable filtering to various list views + ### 0.7.3 - June 2022 --- From 7301243ed631b3f5e374b5eddf664fb828007e3f Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 20:24:40 +1000 Subject: [PATCH 04/12] 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<BomList> { - - _BomListState(this.parent); - - final InvenTreePart parent; - - @override - String getAppBarTitle(BuildContext context) => L10().billOfMaterials; - - @override - String get prefix => "bom_"; - - @override - Map<String, String> 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<PaginatedBomList> { - _PaginatedBomListState(Map<String, String> filters, this.onTotalChanged) : super(filters); + _PaginatedBomListState(Map<String, String> filters, this.onTotalChanged) : super(filters, fullscreen: true); Function(int)? onTotalChanged; @override String get prefix => "bom_"; + @override + Map<String, String> get orderingOptions => { + "quantity": L10().quantity, + "sub_part": L10().part, + }; + + + @override + String getAppBarTitle(BuildContext context) => L10().billOfMaterials; + @override Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> 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<NotificationWidget> { List<InvenTreeNotification> notifications = []; @override - AppBar? buildAppBar(BuildContext context) { + AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> 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<T extends StatefulWidget> extends RefreshableState<T> { +class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseWidgetProperties { + + PaginatedSearchState(this.filters, {this.fullscreen = true}); + + final _key = GlobalKey<ScaffoldState>(); + + final Map<String, String> 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<String, String> get orderingOptions => {}; - @override - List<Widget> getAppBarActions(BuildContext context) { - List<Widget> 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<String> orderingField() async { dynamic field = await InvenTreeSettingsManager().getValue("${prefix}ordering_field", null); @@ -62,9 +62,21 @@ abstract class PaginatedState<T extends StatefulWidget> extends RefreshableState return order == "+" ? "+" : "-"; } - // Update the (configurable) filters for this paginated list - Future<void> _updateFilters(BuildContext context) async { + // Return string for determining 'ordering' of paginated list + Future<String> 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<void> _saveOrderingOptions(BuildContext context) async { // Retrieve stored setting dynamic _field = await orderingField(); dynamic _order = await orderingOrder(); @@ -96,12 +108,12 @@ abstract class PaginatedState<T extends StatefulWidget> 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<T extends StatefulWidget> 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<T extends StatefulWidget> extends State<T> { - - PaginatedSearchState(this.filters); - - final Map<String, String> 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<T extends StatefulWidget> extends State<T> { return null; } - Future<String> get ordering async { - dynamic field = await InvenTreeSettingsManager().getValue("${prefix}ordering_field", ""); - dynamic order = await InvenTreeSettingsManager().getValue("${prefix}ordering_order", "+"); - - return "${order}${field}"; - } - Future<void> _fetchPage(int pageKey) async { try { Map<String, String> params = filters; params["search"] = "${searchTerm}"; - params["ordering"] = await ordering; + params["ordering"] = await orderingString; final page = await requestPage( _pageSize, @@ -234,6 +224,8 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> { _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<T extends StatefulWidget> extends State<T> { ); } + // 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<T extends StatefulWidget> extends State<T> { ); } + @override + List<Widget> getAppBarActions(BuildContext context) { + List<Widget> 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<PartDetailWidget> { 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<T extends StatefulWidget> extends State<T> { +/* + * Simple mixin class which defines simple methods for defining widget properties + */ +mixin BaseWidgetProperties { + + // Return a list of appBar actions (default = None) + List<Widget> 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<ScaffoldState> 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<T extends StatefulWidget> extends State<T> with BaseWidgetProperties { final refreshableKey = GlobalKey<ScaffoldState>(); @@ -25,12 +71,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> { }); } - List<Widget> getAppBarActions(BuildContext context) { - return []; - } - - String getAppBarTitle(BuildContext context) { return "App Bar Title"; } - @override void initState() { super.initState(); @@ -60,34 +100,6 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> { }); } - // 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<T extends StatefulWidget> extends State<T> { 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<SearchWidget> { String getAppBarTitle(BuildContext context) => L10().search; @override - AppBar? buildAppBar(BuildContext context) { + AppBar? buildAppBar(BuildContext context, GlobalKey<ScaffoldState> key) { if (hasAppBar) { - return super.buildAppBar(context); + return super.buildAppBar(context, key); } else { return null; } From 6d247f426cacb81f9bb38d3abd37f7a717a69d94 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 20:54:17 +1000 Subject: [PATCH 05/12] Further refactoring --- lib/widget/bom_list.dart | 36 ++++++++++++--- lib/widget/paginator.dart | 89 +++++++++---------------------------- lib/widget/part_detail.dart | 4 +- 3 files changed, 53 insertions(+), 76 deletions(-) diff --git a/lib/widget/bom_list.dart b/lib/widget/bom_list.dart index 56d8faa5..14cd4906 100644 --- a/lib/widget/bom_list.dart +++ b/lib/widget/bom_list.dart @@ -16,6 +16,35 @@ import "package:inventree/widget/part_detail.dart"; import "package:inventree/widget/refreshable_state.dart"; +/* + * Widget for displaying a Bill of Materials for a specified Part instance + */ +class BillOfMaterialsWidget extends StatefulWidget { + + const BillOfMaterialsWidget(this.part, {Key? key}) : super(key: key); + + final InvenTreePart part; + + @override + _BillOfMaterialsState createState() => _BillOfMaterialsState(part); +} + +class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> { + _BillOfMaterialsState(this.part); + + final InvenTreePart part; + + @override + String getAppBarTitle(BuildContext context) => L10().billOfMaterials; + + @override + Widget getBody(BuildContext context) { + return PaginatedBomList({ + "part": part.pk.toString(), + }); + } +} + /* * Create a paginated widget displaying a list of BomItem objects @@ -31,12 +60,13 @@ class PaginatedBomList extends StatefulWidget { @override _PaginatedBomListState createState() => _PaginatedBomListState(filters, onTotalChanged); + } class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { - _PaginatedBomListState(Map<String, String> filters, this.onTotalChanged) : super(filters, fullscreen: true); + _PaginatedBomListState(Map<String, String> filters, this.onTotalChanged) : super(filters); Function(int)? onTotalChanged; @@ -49,10 +79,6 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { "sub_part": L10().part, }; - - @override - String getAppBarTitle(BuildContext context) => L10().billOfMaterials; - @override Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart index a9971ecb..7781a3db 100644 --- a/lib/widget/paginator.dart +++ b/lib/widget/paginator.dart @@ -21,7 +21,7 @@ import "package:inventree/widget/refreshable_state.dart"; */ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseWidgetProperties { - PaginatedSearchState(this.filters, {this.fullscreen = true}); + PaginatedSearchState(this.filters); final _key = GlobalKey<ScaffoldState>(); @@ -29,9 +29,6 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW 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_"; @@ -242,35 +239,16 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW @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: [ - PaginatedSearchWidget(searchController, updateSearchTerm, resultCount), + buildSearchInput(context), Expanded( child: CustomScrollView( shrinkWrap: true, physics: ClampingScrollPhysics(), scrollDirection: Axis.vertical, slivers: <Widget>[ - // TODO - Search input PagedSliverList.separated( pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>( @@ -290,64 +268,39 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW ); } - @override - List<Widget> getAppBarActions(BuildContext context) { - List<Widget> actions = []; - - // If ordering options have been provided - if (orderingOptions.isNotEmpty) { - actions.add(IconButton( - icon: FaIcon(FontAwesomeIcons.sort), - onPressed: () => _saveOrderingOptions(context), - )); - } - - return actions; - } - -} - - -class PaginatedSearchWidget extends StatelessWidget { - - const PaginatedSearchWidget(this.controller, this.onChanged, this.results); - - final Function onChanged; - - final int results; - - final TextEditingController controller; - - @override - Widget build(BuildContext context) { + /* + * Construct a search input text field for the user to enter a search term + */ + Widget buildSearchInput(BuildContext context) { return ListTile( - leading: GestureDetector( - child: FaIcon(controller.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace), + leading: orderingOptions.isEmpty ? null : GestureDetector( + child: FaIcon(FontAwesomeIcons.sort), + onTap: () async { + _saveOrderingOptions(context); + }, + ), + trailing: GestureDetector( + child: FaIcon(searchController.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace), onTap: () { - controller.clear(); - onChanged(); + searchController.clear(); + updateSearchTerm(); }, ), title: TextFormField( - controller: controller, + controller: searchController, onChanged: (value) { - onChanged(); + updateSearchTerm(); }, decoration: InputDecoration( hintText: L10().search, + helperText: resultCount.toString(), ), - ), - trailing: Text( - "${results}", - style: TextStyle( - fontWeight: FontWeight.bold, - fontStyle: FontStyle.italic - ), - ), + ) ); } } + class NoResultsWidget extends StatelessWidget { const NoResultsWidget(this.description); diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index abf17423..8dc162fd 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -354,9 +354,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { Navigator.push( context, MaterialPageRoute( - builder: (context) => PaginatedBomList({ - "part": part.pk.toString(), - }) + builder: (context) => BillOfMaterialsWidget(part) ) ); } From 61929323223da7350d24b1aaca5507b3f0b0ba06 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 20:59:38 +1000 Subject: [PATCH 06/12] Add ordering options for "Part" list --- lib/widget/part_list.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/widget/part_list.dart b/lib/widget/part_list.dart index 2be5e557..fcec2c00 100644 --- a/lib/widget/part_list.dart +++ b/lib/widget/part_list.dart @@ -61,6 +61,16 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> { Function(int)? onTotalChanged; + @override + String get prefix => "part_"; + + @override + Map<String, String> get orderingOptions => { + "name": L10().name, + "in_stock": L10().stock, + "IPN": L10().internalPartNumber, + }; + @override Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { From a450154bacf60764fb4a7d28e64e5de6fa9cdafd Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 21:07:19 +1000 Subject: [PATCH 07/12] Add sorting options for the StockItem list --- lib/l10n/app_en.arb | 3 +++ lib/widget/category_list.dart | 9 +++++++++ lib/widget/stock_list.dart | 14 ++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8183af6b..449fa45d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -425,6 +425,9 @@ "lastUpdated": "Last Updated", "@lastUpdated": {}, + "level": "Level", + "@level": {}, + "lineItem": "Line Item", "@lineItem": {}, diff --git a/lib/widget/category_list.dart b/lib/widget/category_list.dart index 8bb24653..0a6ca607 100644 --- a/lib/widget/category_list.dart +++ b/lib/widget/category_list.dart @@ -50,6 +50,15 @@ class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPart _PaginatedPartCategoryListState(Map<String, String> filters) : super(filters); + @override + String get prefix => "category_"; + + @override + Map<String, String> get orderingOptions => { + "name": L10().name, + "level": L10().level, + }; + @override Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { diff --git a/lib/widget/stock_list.dart b/lib/widget/stock_list.dart index 90481c78..19edcd54 100644 --- a/lib/widget/stock_list.dart +++ b/lib/widget/stock_list.dart @@ -52,6 +52,20 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt _PaginatedStockItemListState(Map<String, String> filters) : super(filters); + @override + String get prefix => "stock_"; + + @override + Map<String, String> get orderingOptions => { + "part__name": L10().name, + "part__IPN": L10().internalPartNumber, + "quantity": L10().quantity, + "status": L10().status, + "batch": L10().batchCode, + "updated": L10().lastUpdated, + "stocktake_date": L10().lastStocktake, + }; + @override Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { From c3e6d3f9026d9b937bed4458526d89c77d0e136b Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 21:13:07 +1000 Subject: [PATCH 08/12] Widget cleanup --- lib/l10n/app_en.arb | 3 +++ lib/widget/paginator.dart | 21 +++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 449fa45d..558418f8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -294,6 +294,9 @@ "feedbackSuccess": "Feedback submitted", "@feedbackSuccess": {}, + "filteringOptions": "Filtering Options", + "@filteringOptions": {}, + "formatException": "Format Exception", "@formatException": {}, diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart index 7781a3db..1acf0687 100644 --- a/lib/widget/paginator.dart +++ b/lib/widget/paginator.dart @@ -4,6 +4,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:infinite_scroll_pagination/infinite_scroll_pagination.dart"; import "package:inventree/api_form.dart"; +import 'package:inventree/app_colors.dart'; import "package:inventree/l10.dart"; import "package:inventree/inventree/model.dart"; @@ -118,7 +119,7 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW launchApiForm( context, - "...filtering...", + L10().filteringOptions, "", fields, icon: FontAwesomeIcons.checkCircle, @@ -143,6 +144,15 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW int resultCount = 0; + String resultsString() { + + if (resultCount <= 0) { + return noResultsText; + } else { + return "${resultCount} ${L10().results}"; + } + } + // Text controller final TextEditingController searchController = TextEditingController(); @@ -274,13 +284,16 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW Widget buildSearchInput(BuildContext context) { return ListTile( leading: orderingOptions.isEmpty ? null : GestureDetector( - child: FaIcon(FontAwesomeIcons.sort), + child: FaIcon(FontAwesomeIcons.sort, color: COLOR_CLICK), onTap: () async { _saveOrderingOptions(context); }, ), trailing: GestureDetector( - child: FaIcon(searchController.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace), + child: FaIcon( + searchController.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace, + color: searchController.text.isNotEmpty ? COLOR_DANGER : COLOR_CLICK, + ), onTap: () { searchController.clear(); updateSearchTerm(); @@ -293,7 +306,7 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW }, decoration: InputDecoration( hintText: L10().search, - helperText: resultCount.toString(), + helperText: resultsString(), ), ) ); From bb73fb74005b611113c622b736bfbd000df70f5d Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 21:17:52 +1000 Subject: [PATCH 09/12] Cleanup --- lib/widget/paginator.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart index 1acf0687..7b6f8034 100644 --- a/lib/widget/paginator.dart +++ b/lib/widget/paginator.dart @@ -4,7 +4,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:infinite_scroll_pagination/infinite_scroll_pagination.dart"; import "package:inventree/api_form.dart"; -import 'package:inventree/app_colors.dart'; +import "package:inventree/app_colors.dart"; import "package:inventree/l10.dart"; import "package:inventree/inventree/model.dart"; @@ -117,6 +117,7 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW } }; + // Launch an interactive form for the user to select options launchApiForm( context, L10().filteringOptions, @@ -174,18 +175,32 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW super.dispose(); } + /* + * Custom function to request a single page of results from the server. + * Each implementing class must override this function, + * and return an InvenTreePageResponse object with the correct data format + */ Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { // Default implementation returns null - must be overridden return null; } + /* + * Request a single page of results from the server + */ Future<void> _fetchPage(int pageKey) async { try { Map<String, String> params = filters; + // Include user search term params["search"] = "${searchTerm}"; - params["ordering"] = await orderingString; + + // Use custom query ordering if available + String o = await orderingString; + if (o.isNotEmpty) { + params["ordering"] = o; + } final page = await requestPage( _pageSize, @@ -226,6 +241,7 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW } } + // Callback function when the search term is updated void updateSearchTerm() { searchTerm = searchController.text; _pagingController.refresh(); From 2e7abf8a1ee824c6d0b30097039c252e84d14a64 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 21:21:19 +1000 Subject: [PATCH 10/12] Allow "trailing" widget to be displayed on home screen entries --- lib/widget/home.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 19f1c953..0870b02c 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -192,7 +192,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { } - Widget _listTile(BuildContext context, String label, IconData icon, {Function()? callback, String role = "", String permission = ""}) { + Widget _listTile(BuildContext context, String label, IconData icon, {Function()? callback, String role = "", String permission = "", Widget? trailing}) { bool connected = InvenTreeAPI().isConnected(); @@ -211,6 +211,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { child: ListTile( leading: FaIcon(icon, color: connected && allowed ? COLOR_CLICK : Colors.grey), title: Text(label), + trailing: trailing, ), ), onTap: () { @@ -257,7 +258,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { FontAwesomeIcons.shapes, callback: () { _showParts(context); - } + }, )); // Starred parts From 847fda7652d07bc42ecc8037e705cca130d93a8e Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 21:24:26 +1000 Subject: [PATCH 11/12] Enable ordering for purchase order list --- lib/l10n/app_en.arb | 3 +++ lib/widget/purchase_order_list.dart | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 558418f8..4c00ca11 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -691,6 +691,9 @@ "receivedItem": "Received Stock Item", "@receivedItem": {}, + "reference": "Reference", + "@reference": {}, + "refresh": "Refresh", "@refresh": {}, diff --git a/lib/widget/purchase_order_list.dart b/lib/widget/purchase_order_list.dart index ec3b1a0e..ef3c6a7d 100644 --- a/lib/widget/purchase_order_list.dart +++ b/lib/widget/purchase_order_list.dart @@ -58,6 +58,17 @@ class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPur // Purchase order prefix String _poPrefix = ""; + @override + String get prefix => "po_"; + + @override + Map<String, String> get orderingOptions => { + "reference": L10().reference, + "supplier__name": L10().supplier, + "status": L10().status, + "target_date": L10().targetDate, + }; + @override Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { From ed2523c3c57b4430482638cf04f366a74f7707b5 Mon Sep 17 00:00:00 2001 From: Oliver Walters <oliver.henry.walters@gmail.com> Date: Wed, 6 Jul 2022 21:29:26 +1000 Subject: [PATCH 12/12] Linting fixes --- lib/widget/attachment_widget.dart | 12 ++++++------ lib/widget/back.dart | 7 +++---- lib/widget/bom_list.dart | 3 --- lib/widget/paginator.dart | 2 -- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/widget/attachment_widget.dart b/lib/widget/attachment_widget.dart index 8437ae7a..0a95d0fb 100644 --- a/lib/widget/attachment_widget.dart +++ b/lib/widget/attachment_widget.dart @@ -1,9 +1,3 @@ -/* - * A generic widget for displaying a list of attachments. - * - * To allow use with different "types" of attachments, - * we pass a subclassed instance of the InvenTreeAttachment model. - */ import "dart:io"; @@ -17,6 +11,12 @@ import "package:inventree/widget/refreshable_state.dart"; import "package:inventree/l10.dart"; import "package:url_launcher/url_launcher.dart"; +/* + * A generic widget for displaying a list of attachments. + * + * To allow use with different "types" of attachments, + * we pass a subclassed instance of the InvenTreeAttachment model. + */ class AttachmentWidget extends StatefulWidget { const AttachmentWidget(this.attachment, this.referenceId, this.hasUploadPermission) : super(); diff --git a/lib/widget/back.dart b/lib/widget/back.dart index 9797e4b7..27a9c23a 100644 --- a/lib/widget/back.dart +++ b/lib/widget/back.dart @@ -1,11 +1,10 @@ +import "package:flutter/material.dart"; + /* - * A custom implementation of a "Back" button for display in the app drawer + * Construct a custom back button with special feature! * * Long-pressing on this will return the user to the home screen */ - -import "package:flutter/material.dart"; - Widget backButton(BuildContext context, GlobalKey<ScaffoldState> key) { return GestureDetector( diff --git a/lib/widget/bom_list.dart b/lib/widget/bom_list.dart index 14cd4906..3014fb02 100644 --- a/lib/widget/bom_list.dart +++ b/lib/widget/bom_list.dart @@ -1,9 +1,7 @@ import "package:flutter/material.dart"; -import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:inventree/api.dart"; -import "package:inventree/api_form.dart"; import "package:inventree/helpers.dart"; import "package:inventree/inventree/bom.dart"; import "package:inventree/l10.dart"; @@ -95,7 +93,6 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { InvenTreePart? subPart = bomItem.subPart; String title = subPart?.fullname ?? "error - no name"; - String description = subPart?.description ?? "error - no description"; return ListTile( title: Text(title), diff --git a/lib/widget/paginator.dart b/lib/widget/paginator.dart index 7b6f8034..91ba215e 100644 --- a/lib/widget/paginator.dart +++ b/lib/widget/paginator.dart @@ -24,8 +24,6 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW PaginatedSearchState(this.filters); - final _key = GlobalKey<ScaffoldState>(); - final Map<String, String> filters; static const _pageSize = 25;