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;