mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 21:35:42 +00:00 
			
		
		
		
	List refactor (#179)
* Catch paginator bug if widget is disposed before request returns * Refactoring paginated query widget - Add option to enable / disable search filters * Major refactor of paginated search widget - Learned something new.. a state can access widget.<attribute> - THIS CHANGES EVERTHING * Preferences: Add code for tri-state values - Also improve unit testing for preferences code * Allow boolean form fields to be optionally tristate * paginator: Allow custom boolean filters * Remove outdated filtering preferences * Refactor filter options - Allow specification of more detailed options * Add custom filters for "part" list * filter tweaks * Remove legacy "SublocationList" widget * Add filtering option for locationlist * Updates for stock location widget * Refactor category display widget * More widget refactoring * Update main search widget * Fix unit tests * Improve filtering on BOM display page
This commit is contained in:
		| @@ -704,7 +704,8 @@ class APIFormField { | ||||
|       labelStyle: _labelStyle(), | ||||
|       helperText: helpText, | ||||
|       helperStyle: _helperStyle(), | ||||
|       initial: value as bool, | ||||
|       initial: value as bool?, | ||||
|       tristate: (getParameter("tristate") ?? false) as bool, | ||||
|       onSaved: (val) { | ||||
|         data["value"] = val; | ||||
|       }, | ||||
|   | ||||
| @@ -32,15 +32,6 @@ class InvenTreePartCategory extends InvenTreeModel { | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Map<String, String> defaultListFilters() { | ||||
|  | ||||
|     return { | ||||
|       "active": "true", | ||||
|       "cascade": "false" | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   String get pathstring => (jsondata["pathstring"] ?? "") as String; | ||||
|  | ||||
|   String get parentPathString { | ||||
| @@ -171,8 +162,7 @@ class InvenTreePart extends InvenTreeModel { | ||||
|   @override | ||||
|   Map<String, String> defaultListFilters() { | ||||
|     return { | ||||
|       "cascade": "false", | ||||
|       "active": "true", | ||||
|       "location_detail": "true", | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -224,7 +224,6 @@ class InvenTreeStockItem extends InvenTreeModel { | ||||
|       "part_detail": "true", | ||||
|       "location_detail": "true", | ||||
|       "supplier_detail": "true", | ||||
|       "cascade": "false", | ||||
|       "in_stock": "true", | ||||
|     }; | ||||
|   } | ||||
|   | ||||
| @@ -297,6 +297,48 @@ | ||||
|   "feedbackSuccess": "Feedback submitted", | ||||
|   "@feedbackSuccess": {}, | ||||
|  | ||||
|   "filterActive": "Active", | ||||
|   "@filterActive": {}, | ||||
|  | ||||
|   "filterActiveDetail": "Show active parts", | ||||
|   "@filterActiveDetail": {}, | ||||
|  | ||||
|   "filterAssembly": "Assembled", | ||||
|   "@filterAssembly": {}, | ||||
|  | ||||
|   "filterAssemblyDetail": "Show assembled parts", | ||||
|   "@filterAssemblyDetail": {}, | ||||
|  | ||||
|   "filterComponent": "Component", | ||||
|   "@filterComponent": {}, | ||||
|  | ||||
|   "filterComponentDetail": "Show component parts", | ||||
|   "@filterComponentDetail": {}, | ||||
|  | ||||
|   "filterInStock": "In Stock", | ||||
|   "@filterInStock": {}, | ||||
|  | ||||
|   "filterInStockDetail": "Show parts which have stock", | ||||
|   "@filterInStockDetail": {}, | ||||
|  | ||||
|   "filterSerialized": "Serialized", | ||||
|   "@filterSerialized": {}, | ||||
|  | ||||
|   "filterSerializedDetail": "Show serialized stock items", | ||||
|   "@filterSerializedDetail": {}, | ||||
|  | ||||
|   "filterTemplate": "Template", | ||||
|   "@filterTemplate": {}, | ||||
|  | ||||
|   "filterTemplateDetail": "Show template parts", | ||||
|   "@filterTemplateDetail": {}, | ||||
|  | ||||
|   "filterVirtual": "Virtual", | ||||
|   "@filterVirtual": {}, | ||||
|  | ||||
|   "filterVirtualDetail": "Show virtual parts", | ||||
|   "@filterVirtualDetail": {}, | ||||
|  | ||||
|   "filteringOptions": "Filtering Options", | ||||
|   "@filteringOptions": {}, | ||||
|  | ||||
| @@ -368,13 +410,13 @@ | ||||
|   "includeSubcategories": "Include Subcategories", | ||||
|   "@includeSubcategories": {}, | ||||
|  | ||||
|   "includeSubcategoriesDetail": "Display subcategory parts in list view", | ||||
|   "includeSubcategoriesDetail": "Show results from subcategories", | ||||
|   "@includeSubcategoriesDetail": {}, | ||||
|  | ||||
|   "includeSublocations": "Include Sublocations", | ||||
|   "@includeSublocations": {}, | ||||
|  | ||||
|   "includeSublocationsDetail": "Display sublocation items in list view", | ||||
|   "includeSublocationsDetail": "Show results from sublocations", | ||||
|   "@includeSublocationsDetail": {}, | ||||
|  | ||||
|   "incompleteDetails": "Incomplete profile details", | ||||
|   | ||||
| @@ -16,9 +16,6 @@ const String INV_HOME_SHOW_SUPPLIERS = "homeShowSuppliers"; | ||||
| const String INV_SOUNDS_BARCODE = "barcodeSounds"; | ||||
| const String INV_SOUNDS_SERVER = "serverSounds"; | ||||
|  | ||||
| const String INV_PART_SUBCATEGORY = "partSubcategory"; | ||||
|  | ||||
| const String INV_STOCK_SUBLOCATION = "stockSublocation"; | ||||
| const String INV_STOCK_SHOW_HISTORY = "stockShowHistory"; | ||||
|  | ||||
| const String INV_REPORT_ERRORS = "reportErrors"; | ||||
| @@ -86,6 +83,11 @@ class InvenTreeSettingsManager { | ||||
|  | ||||
|   Future<Database> get _db async => InvenTreePreferencesDB.instance.database; | ||||
|  | ||||
|  | ||||
|   Future<void> removeValue(String key) async { | ||||
|     await store.record(key).delete(await _db); | ||||
|   } | ||||
|  | ||||
|   Future<dynamic> getValue(String key, dynamic backup) async { | ||||
|  | ||||
|     final value = await store.record(key).get(await _db); | ||||
| @@ -103,13 +105,40 @@ class InvenTreeSettingsManager { | ||||
|  | ||||
|     if (value is bool) { | ||||
|       return value; | ||||
|     } else if (value is String) { | ||||
|       return value.toLowerCase().contains("t"); | ||||
|     } else { | ||||
|       return backup; | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Load a tristate (true / false / null) setting | ||||
|   Future<bool?> getTriState(String key, dynamic backup) async { | ||||
|     final dynamic value = await getValue(key, backup); | ||||
|  | ||||
|     if (value == null) { | ||||
|       return null; | ||||
|     } else if (value is bool) { | ||||
|       return value; | ||||
|     } else { | ||||
|       String s = value.toString().toLowerCase(); | ||||
|  | ||||
|       if (s.contains("t")) { | ||||
|         return true; | ||||
|       } else if (s.contains("f")) { | ||||
|         return false; | ||||
|       } else { | ||||
|         return null; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Store a key:value pair in the database | ||||
|   Future<void> setValue(String key, dynamic value) async { | ||||
|  | ||||
|     // Encode null values as strings | ||||
|     value ??= "null"; | ||||
|  | ||||
|     await store.record(key).put(await _db, value); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -21,11 +21,7 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> { | ||||
|   bool barcodeSounds = true; | ||||
|   bool serverSounds = true; | ||||
|  | ||||
|   // Part settings | ||||
|   bool partSubcategory = false; | ||||
|  | ||||
|   // Stock settings | ||||
|   bool stockSublocation = false; | ||||
|   bool stockShowHistory = false; | ||||
|  | ||||
|   bool reportErrors = true; | ||||
| @@ -45,9 +41,6 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> { | ||||
|     barcodeSounds = await InvenTreeSettingsManager().getValue(INV_SOUNDS_BARCODE, true) as bool; | ||||
|     serverSounds = await InvenTreeSettingsManager().getValue(INV_SOUNDS_SERVER, true) as bool; | ||||
|  | ||||
|     partSubcategory = await InvenTreeSettingsManager().getValue(INV_PART_SUBCATEGORY, true) as bool; | ||||
|  | ||||
|     stockSublocation = await InvenTreeSettingsManager().getValue(INV_STOCK_SUBLOCATION, true) as bool; | ||||
|     stockShowHistory = await InvenTreeSettingsManager().getValue(INV_STOCK_SHOW_HISTORY, false) as bool; | ||||
|  | ||||
|     reportErrors = await InvenTreeSettingsManager().getValue(INV_REPORT_ERRORS, true) as bool; | ||||
| @@ -68,49 +61,13 @@ class _InvenTreeAppSettingsState extends State<InvenTreeAppSettingsWidget> { | ||||
|       body: Container( | ||||
|         child: ListView( | ||||
|           children: [ | ||||
|             ListTile( | ||||
|               title: Text( | ||||
|                 L10().parts, | ||||
|                 style: TextStyle(fontWeight: FontWeight.bold), | ||||
|               ), | ||||
|               leading: FaIcon(FontAwesomeIcons.shapes), | ||||
|             ), | ||||
|             ListTile( | ||||
|               title: Text(L10().includeSubcategories), | ||||
|               subtitle: Text(L10().includeSubcategoriesDetail), | ||||
|               leading: FaIcon(FontAwesomeIcons.sitemap), | ||||
|               trailing: Switch( | ||||
|                 value: partSubcategory, | ||||
|                 onChanged: (bool value) { | ||||
|                   InvenTreeSettingsManager().setValue(INV_PART_SUBCATEGORY, value); | ||||
|                   setState(() { | ||||
|                     partSubcategory = value; | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|             /* Stock Settings */ | ||||
|             Divider(height: 3), | ||||
|             ListTile( | ||||
|               title: Text(L10().stock, | ||||
|                 style: TextStyle(fontWeight: FontWeight.bold), | ||||
|               ), | ||||
|               leading: FaIcon(FontAwesomeIcons.boxes), | ||||
|             ), | ||||
|             ListTile( | ||||
|               title: Text(L10().includeSublocations), | ||||
|               subtitle: Text(L10().includeSublocationsDetail), | ||||
|               leading: FaIcon(FontAwesomeIcons.sitemap), | ||||
|               trailing: Switch( | ||||
|                 value: stockSublocation, | ||||
|                 onChanged: (bool value) { | ||||
|                   InvenTreeSettingsManager().setValue(INV_STOCK_SUBLOCATION, value); | ||||
|                   setState(() { | ||||
|                     stockSublocation = value; | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|             ListTile( | ||||
|               title: Text(L10().stockItemHistory), | ||||
|               subtitle: Text(L10().stockItemHistoryDetail), | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
|  | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/helpers.dart"; | ||||
| @@ -32,14 +33,31 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> { | ||||
|  | ||||
|   final InvenTreePart part; | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().billOfMaterials; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|         setState(() { | ||||
|           showFilterOptions = !showFilterOptions; | ||||
|         }); | ||||
|       }, | ||||
|     ) | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return PaginatedBomList({ | ||||
|       "part": part.pk.toString(), | ||||
|     }); | ||||
|     return PaginatedBomList( | ||||
|       { | ||||
|         "part": part.pk.toString(), | ||||
|       }, | ||||
|       showFilterOptions, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -47,26 +65,18 @@ class _BillOfMaterialsState extends RefreshableState<BillOfMaterialsWidget> { | ||||
| /* | ||||
|  * Create a paginated widget displaying a list of BomItem objects | ||||
|  */ | ||||
| class PaginatedBomList extends StatefulWidget { | ||||
| class PaginatedBomList extends PaginatedSearchWidget { | ||||
|  | ||||
|   const PaginatedBomList(this.filters, {this.onTotalChanged}); | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   final Function(int)? onTotalChanged; | ||||
|   const PaginatedBomList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch); | ||||
|  | ||||
|   @override | ||||
|   _PaginatedBomListState createState() => _PaginatedBomListState(filters, onTotalChanged); | ||||
|  | ||||
|  | ||||
|   _PaginatedBomListState createState() => _PaginatedBomListState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { | ||||
|  | ||||
|   _PaginatedBomListState(Map<String, String> filters, this.onTotalChanged) : super(filters); | ||||
|  | ||||
|   Function(int)? onTotalChanged; | ||||
|   _PaginatedBomListState() : super(); | ||||
|  | ||||
|   @override | ||||
|   String get prefix => "bom_"; | ||||
| @@ -77,6 +87,14 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { | ||||
|     "sub_part": L10().part, | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     "sub_part_assembly": { | ||||
|       "label": L10().filterAssembly, | ||||
|       "help_text": L10().filterAssemblyDetail, | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
|  | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| import "package:flutter/material.dart"; | ||||
|  | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/inventree/part.dart"; | ||||
| import "package:inventree/widget/part_list.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/part.dart"; | ||||
|  | ||||
| import "package:inventree/widget/category_list.dart"; | ||||
| import "package:inventree/widget/part_list.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/widget/part_detail.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
|  | ||||
| @@ -28,6 +29,8 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|  | ||||
|   _CategoryDisplayState(this.category); | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().partCategory; | ||||
|  | ||||
| @@ -73,8 +76,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|   // The local InvenTreePartCategory object | ||||
|   final InvenTreePartCategory? category; | ||||
|  | ||||
|   List<InvenTreePartCategory> _subcategories = []; | ||||
|  | ||||
|   @override | ||||
|   Future<void> onBuild(BuildContext context) async { | ||||
|     refresh(context); | ||||
| @@ -83,8 +84,6 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
|  | ||||
|     int pk = category?.pk ?? -1; | ||||
|  | ||||
|     // Update the category | ||||
|     if (category != null) { | ||||
|       final bool result = await category?.reload() ?? false; | ||||
| @@ -93,27 +92,17 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|         Navigator.of(context).pop(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Request a list of sub-categories under this one | ||||
|     await InvenTreePartCategory().list(filters: {"parent": "$pk"}).then((var cats) { | ||||
|       _subcategories.clear(); | ||||
|  | ||||
|       for (var cat in cats) { | ||||
|         if (cat is InvenTreePartCategory) { | ||||
|           _subcategories.add(cat); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Update state | ||||
|       setState(() {}); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Widget getCategoryDescriptionCard({bool extra = true}) { | ||||
|     if (category == null) { | ||||
|       return Card( | ||||
|         child: ListTile( | ||||
|           title: Text(L10().partCategoryTopLevel) | ||||
|           leading: FaIcon(FontAwesomeIcons.shapes), | ||||
|           title: Text( | ||||
|             L10().partCategoryTopLevel, | ||||
|             style: TextStyle(fontStyle: FontStyle.italic), | ||||
|           ) | ||||
|         ) | ||||
|       ); | ||||
|     } else { | ||||
| @@ -182,7 +171,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Construct the "details" panel | ||||
|   List<Widget> detailTiles() { | ||||
|  | ||||
|     List<Widget> tiles = <Widget>[ | ||||
|       getCategoryDescriptionCard(), | ||||
|       ListTile( | ||||
| @@ -190,27 +181,62 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|           L10().subcategories, | ||||
|           style: TextStyle(fontWeight: FontWeight.bold) | ||||
|         ), | ||||
|         trailing: _subcategories.isNotEmpty ? Text("${_subcategories.length}") : null, | ||||
|         trailing: GestureDetector( | ||||
|           child: FaIcon(FontAwesomeIcons.filter), | ||||
|           onTap: () async { | ||||
|             setState(() { | ||||
|               showFilterOptions = !showFilterOptions; | ||||
|             }); | ||||
|           }, | ||||
|         ) | ||||
|       ), | ||||
|       Expanded( | ||||
|         child: PaginatedPartCategoryList( | ||||
|             { | ||||
|               "parent": category?.pk.toString() ?? "null" | ||||
|             }, | ||||
|             showFilterOptions, | ||||
|         ), | ||||
|         flex: 10, | ||||
|       ) | ||||
|     ]; | ||||
|  | ||||
|     if (loading) { | ||||
|       tiles.add(progressIndicator()); | ||||
|     } else if (_subcategories.isEmpty) { | ||||
|       tiles.add(ListTile( | ||||
|         title: Text(L10().noSubcategories), | ||||
|         subtitle: Text( | ||||
|             L10().noSubcategoriesAvailable, | ||||
|             style: TextStyle(fontStyle: FontStyle.italic) | ||||
|         ) | ||||
|       )); | ||||
|     } else { | ||||
|       tiles.add(SubcategoryList(_subcategories)); | ||||
|     } | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   // Construct the "parts" panel | ||||
|   List<Widget> partsTiles() { | ||||
|  | ||||
|     Map<String, String> filters = { | ||||
|       "category": category?.pk.toString() ?? "null", | ||||
|     }; | ||||
|  | ||||
|     return [ | ||||
|       getCategoryDescriptionCard(extra: false), | ||||
|       ListTile( | ||||
|         title: Text( | ||||
|           L10().parts, | ||||
|           style: TextStyle(fontWeight: FontWeight.bold), | ||||
|         ), | ||||
|         trailing: GestureDetector( | ||||
|           child: FaIcon(FontAwesomeIcons.filter), | ||||
|           onTap: () async { | ||||
|             setState(() { | ||||
|               showFilterOptions = !showFilterOptions; | ||||
|             }); | ||||
|           }, | ||||
|         ), | ||||
|       ), | ||||
|       Expanded( | ||||
|         child: PaginatedPartList( | ||||
|           filters, | ||||
|           showFilterOptions, | ||||
|         ), | ||||
|         flex: 10, | ||||
|       ) | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   Future<void> _newCategory(BuildContext context) async { | ||||
|  | ||||
|     int pk = category?.pk ?? -1; | ||||
| @@ -323,14 +349,12 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|  | ||||
|     switch (tabIndex) { | ||||
|       case 0: | ||||
|         return ListView( | ||||
|           children: detailTiles() | ||||
|         return Column( | ||||
|             children: detailTiles() | ||||
|         ); | ||||
|       case 1: | ||||
|         return PaginatedPartList( | ||||
|           { | ||||
|             "category": "${category?.pk ?? 'null'}" | ||||
|           }, | ||||
|         return Column( | ||||
|           children: partsTiles() | ||||
|         ); | ||||
|       case 2: | ||||
|         return ListView( | ||||
| @@ -341,47 +365,3 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Builder for displaying a list of PartCategory objects | ||||
|  */ | ||||
| class SubcategoryList extends StatelessWidget { | ||||
|  | ||||
|   const SubcategoryList(this._categories); | ||||
|  | ||||
|   final List<InvenTreePartCategory> _categories; | ||||
|  | ||||
|   void _openCategory(BuildContext context, int pk) { | ||||
|  | ||||
|     // Attempt to load the sub-category. | ||||
|     InvenTreePartCategory().get(pk).then((var cat) { | ||||
|       if (cat is InvenTreePartCategory) { | ||||
|  | ||||
|         Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Widget _build(BuildContext context, int index) { | ||||
|     InvenTreePartCategory cat = _categories[index]; | ||||
|  | ||||
|     return ListTile( | ||||
|       title: Text("${cat.name}"), | ||||
|       subtitle: Text("${cat.description}"), | ||||
|       trailing: Text("${cat.partcount}"), | ||||
|       onTap: () { | ||||
|         _openCategory(context, cat.pk); | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListView.separated( | ||||
|         shrinkWrap: true, | ||||
|         physics: ClampingScrollPhysics(), | ||||
|         separatorBuilder: (_, __) => const Divider(height: 3), | ||||
|         itemBuilder: _build, itemCount: _categories.length); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/model.dart"; | ||||
| import "package:inventree/inventree/part.dart"; | ||||
| @@ -25,34 +26,55 @@ class _PartCategoryListState extends RefreshableState<PartCategoryList> { | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|         setState(() { | ||||
|           showFilterOptions = !showFilterOptions; | ||||
|         }); | ||||
|       }, | ||||
|     ) | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().partCategories; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return PaginatedPartCategoryList(filters); | ||||
|     return PaginatedPartCategoryList(filters, showFilterOptions); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PaginatedPartCategoryList extends PaginatedSearchWidget { | ||||
|  | ||||
| class PaginatedPartCategoryList extends StatefulWidget { | ||||
|  | ||||
|   const PaginatedPartCategoryList(this.filters); | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|   const PaginatedPartCategoryList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch); | ||||
|  | ||||
|   @override | ||||
|   _PaginatedPartCategoryListState createState() => _PaginatedPartCategoryListState(filters); | ||||
|   _PaginatedPartCategoryListState createState() => _PaginatedPartCategoryListState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedPartCategoryListState extends PaginatedSearchState<PaginatedPartCategoryList> { | ||||
|  | ||||
|   _PaginatedPartCategoryListState(Map<String, String> filters) : super(filters); | ||||
|   // _PaginatedPartCategoryListState(Map<String, String> filters, bool searchEnabled) : super(filters, searchEnabled); | ||||
|  | ||||
|   @override | ||||
|   String get prefix => "category_"; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     "cascade": { | ||||
|       "default": false, | ||||
|       "label": L10().includeSubcategories, | ||||
|       "help_text": L10().includeSubcategoriesDetail, | ||||
|       "tristate": false, | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, String> get orderingOptions => { | ||||
|     "name": L10().name, | ||||
|   | ||||
| @@ -35,29 +35,22 @@ class _CompanyListWidgetState extends RefreshableState<CompanyListWidget> { | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|  | ||||
|     return PaginatedCompanyList(filters); | ||||
|  | ||||
|     return PaginatedCompanyList(filters, true); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class PaginatedCompanyList extends PaginatedSearchWidget { | ||||
|  | ||||
| class PaginatedCompanyList extends StatefulWidget { | ||||
|  | ||||
|   const PaginatedCompanyList(this.filters, {this.onTotalChanged}); | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   final Function(int)? onTotalChanged; | ||||
|   const PaginatedCompanyList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch); | ||||
|  | ||||
|   @override | ||||
|   _CompanyListState createState() => _CompanyListState(filters); | ||||
|   _CompanyListState createState() => _CompanyListState(); | ||||
| } | ||||
|  | ||||
| class _CompanyListState extends PaginatedSearchState<PaginatedCompanyList> { | ||||
|  | ||||
|   _CompanyListState(Map<String, String> filters) : super(filters); | ||||
|   _CompanyListState() : super(); | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
|   | ||||
| @@ -142,7 +142,10 @@ class FilePickerDialog { | ||||
|  | ||||
| class CheckBoxField extends FormField<bool> { | ||||
|   CheckBoxField({ | ||||
|       String? label, bool initial = false, Function(bool?)? onSaved, | ||||
|       String? label, | ||||
|       bool? initial = false, | ||||
|       bool tristate = false, | ||||
|       Function(bool?)? onSaved, | ||||
|       TextStyle? labelStyle, | ||||
|       String? helperText, | ||||
|       TextStyle? helperStyle, | ||||
| @@ -155,6 +158,7 @@ class CheckBoxField extends FormField<bool> { | ||||
|             //dense: state.hasError, | ||||
|             title: label != null ? Text(label, style: labelStyle) : null, | ||||
|             value: state.value, | ||||
|             tristate: tristate, | ||||
|             onChanged: state.didChange, | ||||
|             subtitle: helperText != null ? Text(helperText, style: helperStyle) : null, | ||||
|             contentPadding: EdgeInsets.zero, | ||||
|   | ||||
| @@ -5,15 +5,20 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/barcode.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/stock.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
|  | ||||
| import "package:inventree/widget/location_list.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/widget/stock_detail.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/widget/stock_list.dart"; | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Widget for displaying detail view for a single StockLocation instance | ||||
|  */ | ||||
| class LocationDisplayWidget extends StatefulWidget { | ||||
|  | ||||
|   LocationDisplayWidget(this.location, {Key? key}) : super(key: key); | ||||
| @@ -32,6 +37,8 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|  | ||||
|   final InvenTreeStockLocation? location; | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) { return L10().stockLocation; } | ||||
|  | ||||
| @@ -103,19 +110,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   List<InvenTreeStockLocation> _sublocations = []; | ||||
|  | ||||
|   String _locationFilter = ""; | ||||
|  | ||||
|   List<InvenTreeStockLocation> get sublocations { | ||||
|      | ||||
|     if (_locationFilter.isEmpty || _sublocations.isEmpty) { | ||||
|       return _sublocations; | ||||
|     } else { | ||||
|       return _sublocations.where((loc) => loc.filter(_locationFilter)).toList(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> onBuild(BuildContext context) async { | ||||
|     refresh(context); | ||||
| @@ -124,8 +118,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
|  | ||||
|     int pk = location?.pk ?? -1; | ||||
|  | ||||
|     // Reload location information | ||||
|     if (location != null) { | ||||
|       final bool result = await location!.reload(); | ||||
| @@ -135,17 +127,6 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Request a list of sub-locations under this one | ||||
|     await InvenTreeStockLocation().list(filters: {"parent": "$pk"}).then((var locs) { | ||||
|       _sublocations.clear(); | ||||
|  | ||||
|       for (var loc in locs) { | ||||
|         if (loc is InvenTreeStockLocation) { | ||||
|           _sublocations.add(loc); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
| @@ -214,7 +195,11 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|     if (location == null) { | ||||
|       return Card( | ||||
|         child: ListTile( | ||||
|           title: Text(L10().stockTopLevel), | ||||
|           title: Text( | ||||
|             L10().stockTopLevel, | ||||
|             style: TextStyle(fontStyle: FontStyle.italic) | ||||
|           ), | ||||
|           leading: FaIcon(FontAwesomeIcons.boxes), | ||||
|         ) | ||||
|       ); | ||||
|     } else { | ||||
| @@ -223,7 +208,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|         ListTile( | ||||
|           title: Text("${location!.name}"), | ||||
|           subtitle: Text("${location!.description}"), | ||||
|           trailing: Text("${location!.itemcount}"), | ||||
|           leading: FaIcon(FontAwesomeIcons.boxes), | ||||
|         ), | ||||
|       ]; | ||||
|  | ||||
| @@ -286,20 +271,15 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|  | ||||
|   Widget getSelectedWidget(int index) { | ||||
|  | ||||
|     // Construct filters for paginated stock list | ||||
|     Map<String, String> filters = {}; | ||||
|  | ||||
|     if (location != null) { | ||||
|       filters["location"] = "${location!.pk}"; | ||||
|     } | ||||
|  | ||||
|     switch (index) { | ||||
|       case 0: | ||||
|         return ListView( | ||||
|         return Column( | ||||
|           children: detailTiles(), | ||||
|         ); | ||||
|       case 1: | ||||
|         return PaginatedStockItemList(filters); | ||||
|         return Column( | ||||
|           children: stockTiles(), | ||||
|         ); | ||||
|       case 2: | ||||
|         return ListView( | ||||
|           children: ListTile.divideTiles( | ||||
| @@ -317,8 +297,8 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|     return getSelectedWidget(tabIndex); | ||||
|   } | ||||
|  | ||||
|  | ||||
| List<Widget> detailTiles() { | ||||
|   // Construct the "details" panel | ||||
|   List<Widget> detailTiles() { | ||||
|     List<Widget> tiles = [ | ||||
|       locationDescriptionCard(), | ||||
|       ListTile( | ||||
| @@ -326,27 +306,61 @@ List<Widget> detailTiles() { | ||||
|           L10().sublocations, | ||||
|           style: TextStyle(fontWeight: FontWeight.bold), | ||||
|         ), | ||||
|         trailing: sublocations.isNotEmpty ? Text("${sublocations.length}") : null, | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     if (loading) { | ||||
|       tiles.add(progressIndicator()); | ||||
|     } else if (_sublocations.isNotEmpty) { | ||||
|       tiles.add(SublocationList(_sublocations)); | ||||
|     } else { | ||||
|       tiles.add(ListTile( | ||||
|         title: Text(L10().sublocationNone), | ||||
|         subtitle: Text( | ||||
|             L10().sublocationNoneDetail, | ||||
|             style: TextStyle(fontStyle: FontStyle.italic) | ||||
|         trailing: GestureDetector( | ||||
|           child: FaIcon(FontAwesomeIcons.filter), | ||||
|           onTap: () async { | ||||
|             setState(() { | ||||
|               showFilterOptions = !showFilterOptions; | ||||
|             }); | ||||
|           }, | ||||
|         ) | ||||
|       )); | ||||
|     } | ||||
|       ), | ||||
|       Expanded( | ||||
|         child: PaginatedStockLocationList( | ||||
|           { | ||||
|             "parent": location?.pk.toString() ?? "null", | ||||
|           }, | ||||
|           showFilterOptions, | ||||
|         ), | ||||
|         flex: 10, | ||||
|       ) | ||||
|     ]; | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
|   // Construct the "stock" panel | ||||
|   List<Widget> stockTiles() { | ||||
|  | ||||
|     Map<String, String> filters = { | ||||
|       "location": location?.pk.toString() ?? "null", | ||||
|     }; | ||||
|  | ||||
|     return [ | ||||
|       locationDescriptionCard(includeActions: false), | ||||
|       ListTile( | ||||
|         title: Text( | ||||
|           L10().stock, | ||||
|           style: TextStyle(fontWeight: FontWeight.bold), | ||||
|         ), | ||||
|         trailing: GestureDetector( | ||||
|           child: FaIcon(FontAwesomeIcons.filter), | ||||
|           onTap: () async { | ||||
|             setState(() { | ||||
|               showFilterOptions = !showFilterOptions; | ||||
|             }); | ||||
|           }, | ||||
|         ), | ||||
|       ), | ||||
|       Expanded( | ||||
|         child: PaginatedStockItemList( | ||||
|           filters, | ||||
|           showFilterOptions, | ||||
|         ), | ||||
|         flex: 10, | ||||
|       ) | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   List<Widget> actionTiles() { | ||||
|     List<Widget> tiles = []; | ||||
| @@ -452,48 +466,4 @@ List<Widget> detailTiles() { | ||||
|  | ||||
|     return tiles; | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| class SublocationList extends StatelessWidget { | ||||
|  | ||||
|   const SublocationList(this._locations); | ||||
|  | ||||
|   final List<InvenTreeStockLocation> _locations; | ||||
|  | ||||
|   void _openLocation(BuildContext context, int pk) { | ||||
|  | ||||
|     InvenTreeStockLocation().get(pk).then((var loc) { | ||||
|       if (loc is InvenTreeStockLocation) { | ||||
|  | ||||
|         Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Widget _build(BuildContext context, int index) { | ||||
|     InvenTreeStockLocation loc = _locations[index]; | ||||
|  | ||||
|     return ListTile( | ||||
|       title: Text("${loc.name}"), | ||||
|       subtitle: Text("${loc.description}"), | ||||
|       trailing: Text("${loc.itemcount}"), | ||||
|       onTap: () { | ||||
|         _openLocation(context, loc.pk); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListView.separated( | ||||
|         shrinkWrap: true, | ||||
|         physics: ClampingScrollPhysics(), | ||||
|         itemBuilder: _build, | ||||
|         separatorBuilder: (_, __) => const Divider(height: 3), | ||||
|         itemCount: _locations.length | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/model.dart"; | ||||
| import "package:inventree/inventree/stock.dart"; | ||||
| @@ -26,30 +27,57 @@ class _StockLocationListState extends RefreshableState<StockLocationList> { | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|         setState(() { | ||||
|           showFilterOptions = !showFilterOptions; | ||||
|         }); | ||||
|       }, | ||||
|     ) | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().stockLocations; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return PaginatedStockLocationList(filters); | ||||
|     return PaginatedStockLocationList(filters, showFilterOptions); | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| class PaginatedStockLocationList extends StatefulWidget { | ||||
| class PaginatedStockLocationList extends PaginatedSearchWidget { | ||||
|  | ||||
|   const PaginatedStockLocationList(this.filters); | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|   const PaginatedStockLocationList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch); | ||||
|  | ||||
|   @override | ||||
|   _PaginatedStockLocationListState createState() => _PaginatedStockLocationListState(filters); | ||||
|   _PaginatedStockLocationListState createState() => _PaginatedStockLocationListState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedStockLocationListState extends PaginatedSearchState<PaginatedStockLocationList> { | ||||
|  | ||||
|   _PaginatedStockLocationListState(Map<String, String> filters) : super(filters); | ||||
|   _PaginatedStockLocationListState() : super(); | ||||
|  | ||||
|   @override | ||||
|   Map<String, String> get orderingOptions => { | ||||
|     "name": L10().name, | ||||
|     "level": L10().level, | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     "cascade": { | ||||
|       "label": L10().includeSublocations, | ||||
|       "help_text": L10().includeSublocationsDetail, | ||||
|       "tristate": false, | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
|   | ||||
| @@ -15,23 +15,72 @@ import "package:inventree/widget/refreshable_state.dart"; | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Generic stateful widget for displaying paginated data retrieved via the API | ||||
|  * | ||||
|  * - Can be displayed as "full screen" (with app-bar and drawer) | ||||
|  * - Can be displayed as a standalone widget | ||||
|  * Abstract base widget class for rendering a PaginatedSearchState | ||||
|  */ | ||||
| class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseWidgetProperties { | ||||
| abstract class PaginatedSearchWidget extends StatefulWidget { | ||||
|  | ||||
|   PaginatedSearchState(this.filters); | ||||
|   const PaginatedSearchWidget({this.filters = const {}, this.showSearch = false}); | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   final bool showSearch; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Generic stateful widget for displaying paginated data retrieved via the API | ||||
|  */ | ||||
| abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends State<T> with BaseWidgetProperties { | ||||
|  | ||||
|   static const _pageSize = 25; | ||||
|  | ||||
|   // Prefix for storing and loading pagination options | ||||
|   // Override in implementing class | ||||
|   String get prefix => "prefix_"; | ||||
|  | ||||
|   // Return a map of boolean filtering options available for this list | ||||
|   // Should be overridden by an implementing subclass | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => {}; | ||||
|  | ||||
|   // Return the boolean value of a particular boolean filter | ||||
|   Future<bool?> getBooleanFilterValue(String key) async { | ||||
|     key = "${prefix}bool_${key}"; | ||||
|  | ||||
|     Map<String, dynamic> opts = filterOptions[key] ?? {}; | ||||
|  | ||||
|     bool? backup; | ||||
|     dynamic v = opts["default"]; | ||||
|  | ||||
|     if (v is bool) { | ||||
|       backup = v; | ||||
|     } | ||||
|  | ||||
|     final result = await InvenTreeSettingsManager().getTriState(key, backup); | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   // Set the boolean value of a particular boolean filter | ||||
|   Future<void> setBooleanFilterValue(String key, bool? value) async { | ||||
|     key = "${prefix}bool_${key}"; | ||||
|     await InvenTreeSettingsManager().setValue(key, value); | ||||
|   } | ||||
|  | ||||
|   // Construct the boolean filter options for this list | ||||
|   Future<Map<String, String>> constructBooleanFilters() async { | ||||
|  | ||||
|     Map<String, String> f = {}; | ||||
|  | ||||
|     for (String k in filterOptions.keys) { | ||||
|       bool? value = await getBooleanFilterValue(k); | ||||
|  | ||||
|       if (value is bool) { | ||||
|         f[k] = value ? "true" : "false"; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return f; | ||||
|   } | ||||
|  | ||||
|   // Return a map of sorting options available for this list | ||||
|   // Should be overridden by an implementing subclass | ||||
|   Map<String, String> get orderingOptions => {}; | ||||
| @@ -115,6 +164,34 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     // Add in boolean filter options | ||||
|     for (String key in filterOptions.keys) { | ||||
|       Map<String, dynamic> opts = filterOptions[key] ?? {}; | ||||
|  | ||||
|       // Determine field information | ||||
|       String label = (opts["label"] ?? key) as String; | ||||
|       String? help_text = opts["help_text"] as String?; | ||||
|  | ||||
|       bool tristate = (opts["tristate"] ?? true) as bool; | ||||
|  | ||||
|       bool? v = await getBooleanFilterValue(key); | ||||
|  | ||||
|       // Prevent null value if not tristate | ||||
|       if (!tristate && v == null) { | ||||
|         v = false; | ||||
|       } | ||||
|  | ||||
|       // Add in the particular field | ||||
|       fields[key] = { | ||||
|         "type": "boolean", | ||||
|         "display_name": label, | ||||
|         "label": label, | ||||
|         "help_text": help_text, | ||||
|         "value": v, | ||||
|         "tristate": (opts["tristate"] ?? true) as bool, | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|     // Launch an interactive form for the user to select options | ||||
|     launchApiForm( | ||||
|       context, | ||||
| @@ -132,6 +209,20 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW | ||||
|         await InvenTreeSettingsManager().setValue("${prefix}ordering_field", f); | ||||
|         await InvenTreeSettingsManager().setValue("${prefix}ordering_order", o); | ||||
|  | ||||
|         // Save boolean fields | ||||
|         for (String key in filterOptions.keys) { | ||||
|  | ||||
|           bool? v; | ||||
|  | ||||
|           dynamic value = data[key]; | ||||
|  | ||||
|           if (value is bool) { | ||||
|             v = value; | ||||
|           } | ||||
|  | ||||
|           await setBooleanFilterValue(key, v); | ||||
|         } | ||||
|  | ||||
|         // Refresh data from the server | ||||
|         _pagingController.refresh(); | ||||
|       } | ||||
| @@ -189,10 +280,12 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW | ||||
|    */ | ||||
|   Future<void> _fetchPage(int pageKey) async { | ||||
|     try { | ||||
|       Map<String, String> params = filters; | ||||
|       Map<String, String> params = widget.filters; | ||||
|  | ||||
|       // Include user search term | ||||
|       params["search"] = "${searchTerm}"; | ||||
|       if (searchTerm.isNotEmpty) { | ||||
|         params["search"] = "${searchTerm}"; | ||||
|       } | ||||
|  | ||||
|       // Use custom query ordering if available | ||||
|       String o = await orderingString; | ||||
| @@ -200,12 +293,24 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW | ||||
|         params["ordering"] = o; | ||||
|       } | ||||
|  | ||||
|       Map<String, String> f = await constructBooleanFilters(); | ||||
|  | ||||
|       if (f.isNotEmpty) { | ||||
|         params.addAll(f); | ||||
|       } | ||||
|  | ||||
|       final page = await requestPage( | ||||
|         _pageSize, | ||||
|         pageKey, | ||||
|         params | ||||
|       ); | ||||
|  | ||||
|       // We may have disposed of the widget while the request was in progress | ||||
|       // If this is the case, abort | ||||
|       if (!mounted) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       int pageLength = page?.length ?? 0; | ||||
|       int pageCount = page?.count ?? 0; | ||||
|  | ||||
| @@ -263,32 +368,39 @@ class PaginatedSearchState<T extends StatefulWidget> extends State<T> with BaseW | ||||
|   @override | ||||
|   Widget build (BuildContext context) { | ||||
|  | ||||
|     List<Widget> children = []; | ||||
|  | ||||
|     if (widget.showSearch) { | ||||
|       children.add(buildSearchInput(context)); | ||||
|     } | ||||
|  | ||||
|     children.add( | ||||
|       Expanded( | ||||
|         child: CustomScrollView( | ||||
|             shrinkWrap: true, | ||||
|             physics: ClampingScrollPhysics(), | ||||
|             scrollDirection: Axis.vertical, | ||||
|             slivers: <Widget>[ | ||||
|               PagedSliverList.separated( | ||||
|                 pagingController: _pagingController, | ||||
|                 builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>( | ||||
|                     itemBuilder: (context, item, index) { | ||||
|                       return buildItem(context, item); | ||||
|                     }, | ||||
|                     noItemsFoundIndicatorBuilder: (context) { | ||||
|                       return NoResultsWidget(noResultsText); | ||||
|                     } | ||||
|                 ), | ||||
|                 separatorBuilder: (context, item) => const Divider(height: 1), | ||||
|               ) | ||||
|             ] | ||||
|         ) | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     return Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.start, | ||||
|         children: [ | ||||
|           buildSearchInput(context), | ||||
|           Expanded( | ||||
|               child: CustomScrollView( | ||||
|                   shrinkWrap: true, | ||||
|                   physics: ClampingScrollPhysics(), | ||||
|                   scrollDirection: Axis.vertical, | ||||
|                   slivers: <Widget>[ | ||||
|                     PagedSliverList.separated( | ||||
|                       pagingController: _pagingController, | ||||
|                       builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>( | ||||
|                           itemBuilder: (context, item, index) { | ||||
|                             return buildItem(context, item); | ||||
|                           }, | ||||
|                           noItemsFoundIndicatorBuilder: (context) { | ||||
|                             return NoResultsWidget(noResultsText); | ||||
|                           } | ||||
|                       ), | ||||
|                       separatorBuilder: (context, item) => const Divider(height: 1), | ||||
|                     ) | ||||
|                   ] | ||||
|               ) | ||||
|           ) | ||||
|         ] | ||||
|         children: children, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -606,7 +606,8 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|       ); | ||||
|       case 1: | ||||
|         return PaginatedStockItemList( | ||||
|           {"part": "${part.pk}"} | ||||
|           {"part": "${part.pk}"}, | ||||
|           true, | ||||
|         ); | ||||
|       case 2: | ||||
|         return Center( | ||||
|   | ||||
| @@ -1,13 +1,15 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/model.dart"; | ||||
| import "package:inventree/inventree/part.dart"; | ||||
|  | ||||
| import "package:inventree/widget/paginator.dart"; | ||||
| import "package:inventree/widget/part_detail.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/preferences.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
|  | ||||
| class PartList extends StatefulWidget { | ||||
| @@ -31,35 +33,43 @@ class _PartListState extends RefreshableState<PartList> { | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => title.isNotEmpty ? title : L10().parts; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|         setState(() { | ||||
|           showFilterOptions = !showFilterOptions; | ||||
|         }); | ||||
|       }, | ||||
|     ) | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return PaginatedPartList(filters); | ||||
|     return PaginatedPartList(filters, showFilterOptions); | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| class PaginatedPartList extends StatefulWidget { | ||||
| class PaginatedPartList extends PaginatedSearchWidget { | ||||
|  | ||||
|   const PaginatedPartList(this.filters, {this.onTotalChanged}); | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   final Function(int)? onTotalChanged; | ||||
|   const PaginatedPartList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch); | ||||
|  | ||||
|   @override | ||||
|   _PaginatedPartListState createState() => _PaginatedPartListState(filters, onTotalChanged); | ||||
|   _PaginatedPartListState createState() => _PaginatedPartListState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> { | ||||
|  | ||||
|   _PaginatedPartListState(Map<String, String> filters, this.onTotalChanged) : super(filters); | ||||
|  | ||||
|   Function(int)? onTotalChanged; | ||||
|   _PaginatedPartListState() : super(); | ||||
|  | ||||
|   @override | ||||
|   String get prefix => "part_"; | ||||
| @@ -71,13 +81,42 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> { | ||||
|     "IPN": L10().internalPartNumber, | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     "cascade": { | ||||
|       "default": true, | ||||
|       "label": L10().includeSubcategories, | ||||
|       "help_text": L10().includeSubcategoriesDetail, | ||||
|     }, | ||||
|     "active": { | ||||
|       "label": L10().filterActive, | ||||
|       "help_text": L10().filterActiveDetail, | ||||
|     }, | ||||
|     "assembly": { | ||||
|       "label": L10().filterAssembly, | ||||
|       "help_text": L10().filterAssemblyDetail | ||||
|     }, | ||||
|     "component": { | ||||
|       "label": L10().filterComponent, | ||||
|       "help_text": L10().filterComponentDetail, | ||||
|     }, | ||||
|     "is_template": { | ||||
|       "label": L10().filterTemplate, | ||||
|       "help_text": L10().filterTemplateDetail | ||||
|     }, | ||||
|     "virtual": { | ||||
|       "label": L10().filterVirtual, | ||||
|       "help_text": L10().filterVirtualDetail, | ||||
|     }, | ||||
|     "has_stock": { | ||||
|       "label": L10().filterInStock, | ||||
|       "help_text": L10().filterInStockDetail, | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
|  | ||||
|     final bool cascade = await InvenTreeSettingsManager().getBool(INV_PART_SUBCATEGORY, true); | ||||
|  | ||||
|     params["cascade"] = "${cascade}"; | ||||
|  | ||||
|     final page = await InvenTreePart().listPaginated(limit, offset, filters: params); | ||||
|  | ||||
|     return page; | ||||
|   | ||||
| @@ -391,7 +391,7 @@ class _PurchaseOrderDetailState extends RefreshableState<PurchaseOrderDetailWidg | ||||
|           "purchase_order": "${order.pk}" | ||||
|         }; | ||||
|  | ||||
|         return PaginatedStockItemList(filters); | ||||
|         return PaginatedStockItemList(filters, true); | ||||
|  | ||||
|       default: | ||||
|         return ListView(); | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/company.dart"; | ||||
| import "package:inventree/inventree/model.dart"; | ||||
| @@ -29,32 +30,43 @@ class _PurchaseOrderListWidgetState extends RefreshableState<PurchaseOrderListWi | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().purchaseOrders; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|         setState(() { | ||||
|           showFilterOptions = !showFilterOptions; | ||||
|         }); | ||||
|       }, | ||||
|     ) | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return PaginatedPurchaseOrderList(filters); | ||||
|     return PaginatedPurchaseOrderList(filters, showFilterOptions); | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| class PaginatedPurchaseOrderList extends StatefulWidget { | ||||
| class PaginatedPurchaseOrderList extends PaginatedSearchWidget { | ||||
|  | ||||
|   const PaginatedPurchaseOrderList(this.filters); | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|   const PaginatedPurchaseOrderList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch); | ||||
|  | ||||
|   @override | ||||
|   _PaginatedPurchaseOrderListState createState() => _PaginatedPurchaseOrderListState(filters); | ||||
|   _PaginatedPurchaseOrderListState createState() => _PaginatedPurchaseOrderListState(); | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedPurchaseOrderListState extends PaginatedSearchState<PaginatedPurchaseOrderList> { | ||||
|  | ||||
|   _PaginatedPurchaseOrderListState(Map<String, String> filters) : super(filters); | ||||
|  | ||||
|   _PaginatedPurchaseOrderListState() : super(); | ||||
|   // Purchase order prefix | ||||
|   String _poPrefix = ""; | ||||
|  | ||||
|   | ||||
| @@ -9,9 +9,7 @@ import "package:flutter/material.dart"; | ||||
| mixin BaseWidgetProperties { | ||||
|  | ||||
|   // Return a list of appBar actions (default = None) | ||||
|   List<Widget> getAppBarActions(BuildContext context) { | ||||
|     return []; | ||||
|   } | ||||
|   List<Widget> getAppBarActions(BuildContext context) => []; | ||||
|  | ||||
|   // Return a title for the appBar | ||||
|   String getAppBarTitle(BuildContext context) { return "--- app bar ---"; } | ||||
|   | ||||
| @@ -1,17 +1,19 @@ | ||||
| import "dart:async"; | ||||
|  | ||||
| import "package:flutter/material.dart"; | ||||
|  | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/part.dart"; | ||||
| import "package:inventree/inventree/purchase_order.dart"; | ||||
| import "package:inventree/inventree/stock.dart"; | ||||
|  | ||||
| import "package:inventree/widget/part_list.dart"; | ||||
| import "package:inventree/widget/purchase_order_list.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/api.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/inventree/part.dart"; | ||||
| import "package:inventree/inventree/stock.dart"; | ||||
| import "package:inventree/widget/stock_list.dart"; | ||||
| import "package:inventree/widget/category_list.dart"; | ||||
| import "package:inventree/widget/company_list.dart"; | ||||
| @@ -222,31 +224,32 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> { | ||||
|  | ||||
|     // Search input | ||||
|     tiles.add( | ||||
|       TextFormField( | ||||
|         decoration: InputDecoration( | ||||
|           hintText: L10().queryEmpty, | ||||
|           prefixIcon: IconButton( | ||||
|             icon: FaIcon(FontAwesomeIcons.search), | ||||
|             onPressed: null, | ||||
|           ), | ||||
|           suffixIcon: IconButton( | ||||
|             icon: FaIcon(FontAwesomeIcons.backspace, color: Colors.red), | ||||
|             onPressed: () { | ||||
|               searchController.clear(); | ||||
|               onSearchTextChanged("", immediate: true); | ||||
|               _focusNode.requestFocus(); | ||||
|             } | ||||
|       ListTile( | ||||
|         title: TextFormField( | ||||
|           decoration: InputDecoration( | ||||
|             hintText: L10().queryEmpty, | ||||
|           ), | ||||
|           readOnly: false, | ||||
|           autofocus: true, | ||||
|           autocorrect: false, | ||||
|           focusNode: _focusNode, | ||||
|           controller: searchController, | ||||
|           onChanged: (String text) { | ||||
|             onSearchTextChanged(text); | ||||
|           }, | ||||
|         ), | ||||
|         readOnly: false, | ||||
|         autofocus: true, | ||||
|         autocorrect: false, | ||||
|         focusNode: _focusNode, | ||||
|         controller: searchController, | ||||
|         onChanged: (String text) { | ||||
|           onSearchTextChanged(text); | ||||
|         }, | ||||
|       ), | ||||
|         trailing: GestureDetector( | ||||
|           child: FaIcon( | ||||
|             searchController.text.isEmpty ? FontAwesomeIcons.search : FontAwesomeIcons.backspace, | ||||
|             color: searchController.text.isEmpty ? COLOR_CLICK : COLOR_DANGER, | ||||
|           ), | ||||
|           onTap: () { | ||||
|             searchController.clear(); | ||||
|             onSearchTextChanged("", immediate: true); | ||||
|           }, | ||||
|         ), | ||||
|       ) | ||||
|  | ||||
|     ); | ||||
|  | ||||
|     String query = searchController.text; | ||||
| @@ -406,8 +409,11 @@ class _SearchDisplayState extends RefreshableState<SearchWidget> { | ||||
|     if (!isSearching() && results.isEmpty && searchController.text.isNotEmpty) { | ||||
|       tiles.add( | ||||
|         ListTile( | ||||
|           title: Text(L10().queryNoResults), | ||||
|           leading: FaIcon(FontAwesomeIcons.search), | ||||
|           title: Text( | ||||
|             L10().queryNoResults, | ||||
|             style: TextStyle(fontStyle: FontStyle.italic), | ||||
|           ), | ||||
|           leading: FaIcon(FontAwesomeIcons.searchMinus), | ||||
|         ) | ||||
|       ); | ||||
|     } else { | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
|  | ||||
| import "package:inventree/inventree/model.dart"; | ||||
| import "package:inventree/inventree/stock.dart"; | ||||
| import "package:inventree/widget/paginator.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| import "package:inventree/preferences.dart"; | ||||
| import "package:inventree/widget/stock_detail.dart"; | ||||
| import "package:inventree/api.dart"; | ||||
|  | ||||
| @@ -27,30 +27,42 @@ class _StockListState extends RefreshableState<StockItemList> { | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|  | ||||
|   bool showFilterOptions = false; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().stockItems; | ||||
|  | ||||
|   @override | ||||
|   List<Widget> getAppBarActions(BuildContext context) => [ | ||||
|     IconButton( | ||||
|       icon: FaIcon(FontAwesomeIcons.filter), | ||||
|       onPressed: () async { | ||||
|         setState(() { | ||||
|           showFilterOptions = !showFilterOptions; | ||||
|         }); | ||||
|       }, | ||||
|     ) | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|     return PaginatedStockItemList(filters); | ||||
|     return PaginatedStockItemList(filters, showFilterOptions); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PaginatedStockItemList extends StatefulWidget { | ||||
| class PaginatedStockItemList extends PaginatedSearchWidget { | ||||
|  | ||||
|   const PaginatedStockItemList(this.filters); | ||||
|  | ||||
|   final Map<String, String> filters; | ||||
|   const PaginatedStockItemList(Map<String, String> filters, bool showSearch) : super(filters: filters, showSearch: showSearch); | ||||
|  | ||||
|   @override | ||||
|   _PaginatedStockItemListState createState() => _PaginatedStockItemListState(filters); | ||||
|   _PaginatedStockItemListState createState() => _PaginatedStockItemListState(); | ||||
|    | ||||
| } | ||||
|  | ||||
|  | ||||
| class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockItemList> { | ||||
|  | ||||
|   _PaginatedStockItemListState(Map<String, String> filters) : super(filters); | ||||
|   _PaginatedStockItemListState() : super(); | ||||
|  | ||||
|   @override | ||||
|   String get prefix => "stock_"; | ||||
| @@ -66,14 +78,23 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt | ||||
|     "stocktake_date": L10().lastStocktake, | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Map<String, Map<String, dynamic>> get filterOptions => { | ||||
|     "cascade": { | ||||
|       "default": false, | ||||
|       "label": L10().includeSublocations, | ||||
|       "help_text": L10().includeSublocationsDetail, | ||||
|       "tristate": false, | ||||
|     }, | ||||
|     "serialized": { | ||||
|       "label": L10().filterSerialized, | ||||
|       "help_text": L10().filterSerializedDetail, | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   @override | ||||
|   Future<InvenTreePageResponse?> requestPage(int limit, int offset, Map<String, String> params) async { | ||||
|  | ||||
|     // Do we include stock items from sub-locations? | ||||
|     final bool cascade = await InvenTreeSettingsManager().getBool(INV_STOCK_SUBLOCATION, true); | ||||
|  | ||||
|     params["cascade"] = "${cascade}"; | ||||
|  | ||||
|     final page = await InvenTreeStockItem().listPaginated( | ||||
|       limit, | ||||
|       offset, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user