mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 13:25:40 +00:00 
			
		
		
		
	Display overlay screen for blocking operations (#186)
* Catch state error in homepage widget * Add flutter_overlay_loader lib - Displays an overlay screen to indicate blocking operation * Wrap blocking widget transitions in a loading overlay - Prevents user from doing other things while loading - Shows the user that something is happening * Linting fixes * Show overlay when uploading attachment file * Show overlay when downloading file also * Show overlay when loading or submitting API forms - Major improvements to usability "feel" * UI improvements for stock item test results widget * Fix API_FORM bug - onSuccess function was not being called
This commit is contained in:
		| @@ -6,6 +6,7 @@ import "package:font_awesome_flutter/font_awesome_flutter.dart"; | ||||
| import "package:inventree/app_colors.dart"; | ||||
| import "package:inventree/inventree/model.dart"; | ||||
| import "package:inventree/widget/fields.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/l10.dart"; | ||||
| @@ -51,8 +52,8 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> { | ||||
|             icon: FaIcon(FontAwesomeIcons.plusCircle), | ||||
|             onPressed: () async { | ||||
|               FilePickerDialog.pickFile( | ||||
|                   onPicked: (File file) { | ||||
|                     upload(file); | ||||
|                   onPicked: (File file) async { | ||||
|                     await upload(context, file); | ||||
|                   } | ||||
|               ); | ||||
|             }, | ||||
| @@ -63,9 +64,11 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> { | ||||
|     return actions; | ||||
|   } | ||||
|  | ||||
|   Future<void> upload(File file) async { | ||||
|   Future<void> upload(BuildContext context, File file) async { | ||||
|  | ||||
|     showLoadingOverlay(context); | ||||
|     final bool result = await widget.attachment.uploadAttachment(file, widget.referenceId); | ||||
|     hideLoadingOverlay(); | ||||
|  | ||||
|     if (result) { | ||||
|       showSnackIcon(L10().uploadSuccess, success: true); | ||||
| @@ -121,7 +124,9 @@ class _AttachmentWidgetState extends RefreshableState<AttachmentWidget> { | ||||
|           subtitle: Text(attachment.comment), | ||||
|           leading: FaIcon(attachment.icon, color: COLOR_CLICK), | ||||
|           onTap: () async { | ||||
|             showLoadingOverlay(context); | ||||
|             await attachment.downloadAttachment(); | ||||
|             hideLoadingOverlay(); | ||||
|           }, | ||||
|         )); | ||||
|       } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import "package:inventree/inventree/part.dart"; | ||||
|  | ||||
| import "package:inventree/widget/paginator.dart"; | ||||
| import "package:inventree/widget/part_detail.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
|  | ||||
|  | ||||
| @@ -125,11 +126,14 @@ class _PaginatedBomListState extends PaginatedSearchState<PaginatedBomList> { | ||||
|         height: 40, | ||||
|       ), | ||||
|       onTap: subPart == null ? null : () async { | ||||
|         InvenTreePart().get(bomItem.subPartId).then((var part) { | ||||
|           if (part is InvenTreePart) { | ||||
|             Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|         showLoadingOverlay(context); | ||||
|         var part = await InvenTreePart().get(bomItem.subPartId); | ||||
|         hideLoadingOverlay(); | ||||
|  | ||||
|         if (part is InvenTreePart) { | ||||
|           Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); | ||||
|         } | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import "package:inventree/inventree/part.dart"; | ||||
|  | ||||
| import "package:inventree/widget/category_list.dart"; | ||||
| import "package:inventree/widget/part_list.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/widget/part_detail.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| @@ -125,16 +126,21 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> { | ||||
|                 FontAwesomeIcons.levelUpAlt, | ||||
|                 color: COLOR_CLICK, | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 if (category == null || ((category?.parentId ?? 0) < 0)) { | ||||
|               onTap: () async { | ||||
|  | ||||
|                 int parentId = category?.parentId ?? -1; | ||||
|  | ||||
|                 if (parentId < 0) { | ||||
|                   Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); | ||||
|                 } else { | ||||
|                   // TODO - Refactor this code into the InvenTreePart class | ||||
|                   InvenTreePartCategory().get(category?.parentId ?? -1).then((var cat) { | ||||
|                     if (cat is InvenTreePartCategory) { | ||||
|                       Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); | ||||
|                     } | ||||
|                   }); | ||||
|  | ||||
|                   showLoadingOverlay(context); | ||||
|                   var cat = await InvenTreePartCategory().get(parentId); | ||||
|                   hideLoadingOverlay(); | ||||
|  | ||||
|                   if (cat is InvenTreePartCategory) { | ||||
|                     Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); | ||||
|                   } | ||||
|                 } | ||||
|               }, | ||||
|             ) | ||||
|   | ||||
| @@ -54,11 +54,13 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { | ||||
|     }); | ||||
|  | ||||
|     InvenTreeAPI().registerCallback(() { | ||||
|       setState(() { | ||||
|         // Reload the widget | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|       if (mounted) { | ||||
|         setState(() { | ||||
|           // Reload the widget | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // Index of bottom navigation bar | ||||
| @@ -192,6 +194,11 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Ignore if the widget is no longer active | ||||
|     if (!mounted) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final notifications = await InvenTreeNotification().list(); | ||||
|  | ||||
|     setState(() { | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import "package:inventree/l10.dart"; | ||||
| import "package:inventree/inventree/stock.dart"; | ||||
|  | ||||
| import "package:inventree/widget/location_list.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:inventree/widget/snacks.dart"; | ||||
| import "package:inventree/widget/stock_detail.dart"; | ||||
| @@ -218,19 +219,21 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { | ||||
|               title: Text(L10().parentLocation), | ||||
|               subtitle: Text("${location!.parentPathString}"), | ||||
|               leading: FaIcon(FontAwesomeIcons.levelUpAlt, color: COLOR_CLICK), | ||||
|               onTap: () { | ||||
|               onTap: () async { | ||||
|  | ||||
|                 int parent = location?.parentId ?? -1; | ||||
|                 int parentId = location?.parentId ?? -1; | ||||
|  | ||||
|                 if (parent < 0) { | ||||
|                 if (parentId < 0) { | ||||
|                   Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); | ||||
|                 } else { | ||||
|  | ||||
|                   InvenTreeStockLocation().get(parent).then((var loc) { | ||||
|                     if (loc is InvenTreeStockLocation) { | ||||
|                       Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); | ||||
|                     } | ||||
|                   }); | ||||
|                   showLoadingOverlay(context); | ||||
|                   var loc = await InvenTreeStockLocation().get(parentId); | ||||
|                   hideLoadingOverlay(); | ||||
|  | ||||
|                   if (loc is InvenTreeStockLocation) { | ||||
|                     Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); | ||||
|                   } | ||||
|                 } | ||||
|               }, | ||||
|             ) | ||||
|   | ||||
| @@ -384,8 +384,8 @@ abstract class PaginatedSearchState<T extends PaginatedSearchWidget> extends Sta | ||||
|               PagedSliverList.separated( | ||||
|                 pagingController: _pagingController, | ||||
|                 builderDelegate: PagedChildBuilderDelegate<InvenTreeModel>( | ||||
|                     itemBuilder: (context, item, index) { | ||||
|                       return buildItem(context, item); | ||||
|                     itemBuilder: (ctx, item, index) { | ||||
|                       return buildItem(ctx, item); | ||||
|                     }, | ||||
|                     noItemsFoundIndicatorBuilder: (context) { | ||||
|                       return NoResultsWidget(noResultsText); | ||||
| @@ -450,9 +450,11 @@ class NoResultsWidget extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|  | ||||
|     return ListTile( | ||||
|       title: Text(L10().noResults), | ||||
|       subtitle: Text(description), | ||||
|       leading: FaIcon(FontAwesomeIcons.exclamationCircle), | ||||
|       title: Text( | ||||
|         description, | ||||
|         style: TextStyle(fontStyle: FontStyle.italic), | ||||
|       ), | ||||
|       leading: FaIcon(FontAwesomeIcons.exclamationCircle, color: COLOR_WARNING), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -253,15 +253,17 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> { | ||||
|             title: Text(L10().partCategory), | ||||
|             subtitle: Text("${part.categoryName}"), | ||||
|             leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK), | ||||
|             onTap: () { | ||||
|             onTap: () async { | ||||
|               if (part.categoryId > 0) { | ||||
|                 InvenTreePartCategory().get(part.categoryId).then((var cat) { | ||||
|  | ||||
|                   if (cat is InvenTreePartCategory) { | ||||
|                     Navigator.push(context, MaterialPageRoute( | ||||
|                         builder: (context) => CategoryDisplayWidget(cat))); | ||||
|                   } | ||||
|                 }); | ||||
|                 showLoadingOverlay(context); | ||||
|                 var cat = await InvenTreePartCategory().get(part.categoryId); | ||||
|                 hideLoadingOverlay(); | ||||
|  | ||||
|                 if (cat is InvenTreePartCategory) { | ||||
|                   Navigator.push(context, MaterialPageRoute( | ||||
|                       builder: (context) => CategoryDisplayWidget(cat))); | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
|           ) | ||||
|   | ||||
| @@ -104,6 +104,10 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> { | ||||
|       "label": L10().filterTemplate, | ||||
|       "help_text": L10().filterTemplateDetail | ||||
|     }, | ||||
|     "trackable": { | ||||
|       "label": L10().filterTrackable, | ||||
|       "help_text": L10().filterTrackableDetail, | ||||
|     }, | ||||
|     "virtual": { | ||||
|       "label": L10().filterVirtual, | ||||
|       "help_text": L10().filterVirtualDetail, | ||||
| @@ -122,15 +126,6 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> { | ||||
|     return page; | ||||
|   } | ||||
|  | ||||
|   void _openPart(BuildContext context, int pk) { | ||||
|     // Attempt to load the part information | ||||
|     InvenTreePart().get(pk).then((var part) { | ||||
|       if (part is InvenTreePart) { | ||||
|  | ||||
|         Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget buildItem(BuildContext context, InvenTreeModel model) { | ||||
| @@ -147,7 +142,7 @@ class _PaginatedPartListState extends PaginatedSearchState<PaginatedPartList> { | ||||
|         height: 40, | ||||
|       ), | ||||
|       onTap: () { | ||||
|         _openPart(context, part.pk); | ||||
|         Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
|  | ||||
|  | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:flutter_overlay_loader/flutter_overlay_loader.dart"; | ||||
|  | ||||
| /* | ||||
|  * Construct a circular progress indicator | ||||
| @@ -10,4 +11,17 @@ Widget progressIndicator() { | ||||
|   return Center( | ||||
|     child: CircularProgressIndicator() | ||||
|   ); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| void showLoadingOverlay(BuildContext context) { | ||||
|   Loader.show( | ||||
|     context, | ||||
|     themeData: Theme.of(context).copyWith(colorScheme: ColorScheme.fromSwatch()) | ||||
|   ); | ||||
| } | ||||
|  | ||||
|  | ||||
| void hideLoadingOverlay() { | ||||
|   Loader.hide(); | ||||
| } | ||||
|   | ||||
| @@ -64,9 +64,12 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with | ||||
|  | ||||
|   // Update current tab selection | ||||
|   void onTabSelectionChanged(int index) { | ||||
|     setState(() { | ||||
|       tabIndex = index; | ||||
|     }); | ||||
|  | ||||
|     if (mounted) { | ||||
|       setState(() { | ||||
|         tabIndex = index; | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -87,6 +90,10 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> with | ||||
|  | ||||
|   Future<void> refresh(BuildContext context) async { | ||||
|  | ||||
|     if (!mounted) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     setState(() { | ||||
|       loading = true; | ||||
|     }); | ||||
|   | ||||
| @@ -1,91 +0,0 @@ | ||||
| import "package:inventree/inventree/part.dart"; | ||||
| import "package:inventree/widget/part_detail.dart"; | ||||
| import "package:inventree/widget/progress.dart"; | ||||
| import "package:inventree/widget/refreshable_state.dart"; | ||||
| import "package:flutter/material.dart"; | ||||
|  | ||||
| import "package:inventree/l10.dart"; | ||||
|  | ||||
| import "package:inventree/api.dart"; | ||||
|  | ||||
|  | ||||
| class StarredPartWidget extends StatefulWidget { | ||||
|  | ||||
|   const StarredPartWidget({Key? key}) : super(key: key); | ||||
|  | ||||
|   @override | ||||
|   _StarredPartState createState() => _StarredPartState(); | ||||
| } | ||||
|  | ||||
|  | ||||
| class _StarredPartState extends RefreshableState<StarredPartWidget> { | ||||
|  | ||||
|   List<InvenTreePart> starredParts = []; | ||||
|  | ||||
|   @override | ||||
|   String getAppBarTitle(BuildContext context) => L10().partsStarred; | ||||
|  | ||||
|   @override | ||||
|   Future<void> request(BuildContext context) async { | ||||
|  | ||||
|     final parts = await InvenTreePart().list(filters: {"starred": "true"}); | ||||
|  | ||||
|     starredParts.clear(); | ||||
|  | ||||
|     for (int idx = 0; idx < parts.length; idx++) { | ||||
|       if (parts[idx] is InvenTreePart) { | ||||
|         starredParts.add(parts[idx] as InvenTreePart); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _partResult(BuildContext context, int index) { | ||||
|     final part = starredParts[index]; | ||||
|  | ||||
|     return ListTile( | ||||
|         title: Text(part.fullname), | ||||
|         subtitle: Text(part.description), | ||||
|         leading: InvenTreeAPI().getImage( | ||||
|             part.thumbnail, | ||||
|             width: 40, | ||||
|             height: 40 | ||||
|         ), | ||||
|         onTap: () { | ||||
|           InvenTreePart().get(part.pk).then((var prt) { | ||||
|             if (prt is InvenTreePart) { | ||||
|               Navigator.push( | ||||
|                   context, | ||||
|                   MaterialPageRoute(builder: (context) => PartDetailWidget(prt)) | ||||
|               ); | ||||
|             } | ||||
|           }); | ||||
|         } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget getBody(BuildContext context) { | ||||
|  | ||||
|     if (loading) { | ||||
|       return progressIndicator(); | ||||
|     } | ||||
|  | ||||
|     if (starredParts.isEmpty) { | ||||
|       return ListView( | ||||
|         children: [ | ||||
|           ListTile( | ||||
|             title: Text(L10().partsNone), | ||||
|             subtitle: Text(L10().partsStarredNone) | ||||
|           ) | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return ListView.separated( | ||||
|       itemCount: starredParts.length, | ||||
|       itemBuilder: _partResult, | ||||
|       separatorBuilder: (_, __) => const Divider(height: 3), | ||||
|       physics: ClampingScrollPhysics(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -475,13 +475,16 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | ||||
|             color: item.statusColor | ||||
|           ) | ||||
|         ), | ||||
|         onTap: () { | ||||
|         onTap: () async { | ||||
|           if (item.partId > 0) { | ||||
|             InvenTreePart().get(item.partId).then((var part) { | ||||
|               if (part is InvenTreePart) { | ||||
|                 Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); | ||||
|               } | ||||
|             }); | ||||
|  | ||||
|             showLoadingOverlay(context); | ||||
|             var part = await InvenTreePart().get(item.partId); | ||||
|             hideLoadingOverlay(); | ||||
|  | ||||
|             if (part is InvenTreePart) { | ||||
|               Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         //trailing: Text(item.serialOrQuantityDisplay()), | ||||
| @@ -533,15 +536,17 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> { | ||||
|               FontAwesomeIcons.mapMarkerAlt, | ||||
|               color: COLOR_CLICK, | ||||
|             ), | ||||
|             onTap: () { | ||||
|             onTap: () async { | ||||
|               if (item.locationId > 0) { | ||||
|                 InvenTreeStockLocation().get(item.locationId).then((var loc) { | ||||
|  | ||||
|                   if (loc is InvenTreeStockLocation) { | ||||
|                     Navigator.push(context, MaterialPageRoute( | ||||
|                         builder: (context) => LocationDisplayWidget(loc))); | ||||
|                   } | ||||
|                 }); | ||||
|                 showLoadingOverlay(context); | ||||
|                 var loc = await InvenTreeStockLocation().get(item.locationId); | ||||
|                 hideLoadingOverlay(); | ||||
|  | ||||
|                 if (loc is InvenTreeStockLocation) { | ||||
|                   Navigator.push(context, MaterialPageRoute( | ||||
|                       builder: (context) => LocationDisplayWidget(loc))); | ||||
|                 } | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|   | ||||
| @@ -156,6 +156,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes | ||||
|       String _test = ""; | ||||
|       bool _result = false; | ||||
|       String _value = ""; | ||||
|       String _notes = ""; | ||||
|  | ||||
|       FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: COLOR_BLUE); | ||||
|       bool _valueRequired = false; | ||||
| @@ -168,11 +169,13 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes | ||||
|         _value = item.latestResult()?.value ?? ""; | ||||
|         _valueRequired = item.requiresValue; | ||||
|         _attachmentRequired = item.requiresAttachment; | ||||
|         _notes = item.latestResult()?.notes ?? ""; | ||||
|       } else if (item is InvenTreeStockItemTestResult) { | ||||
|         _result = item.result; | ||||
|         _test = item.testName; | ||||
|         _required = false; | ||||
|         _value = item.value; | ||||
|         _notes = item.notes; | ||||
|       } | ||||
|  | ||||
|       if (_result == true) { | ||||
| @@ -187,8 +190,9 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes | ||||
|  | ||||
|       tiles.add(ListTile( | ||||
|         title: Text(_test, style: TextStyle(fontWeight: _required ? FontWeight.bold : FontWeight.normal)), | ||||
|         subtitle: Text(_value), | ||||
|         trailing: _icon, | ||||
|         subtitle: Text(_notes), | ||||
|         trailing: Text(_value), | ||||
|         leading: _icon, | ||||
|         onLongPress: () { | ||||
|           addTestResult( | ||||
|               context, | ||||
|   | ||||
| @@ -104,14 +104,6 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt | ||||
|     return page; | ||||
|   } | ||||
|  | ||||
|   void _openItem(BuildContext context, int pk) { | ||||
|     InvenTreeStockItem().get(pk).then((var item) { | ||||
|       if (item is InvenTreeStockItem) { | ||||
|         Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget buildItem(BuildContext context, InvenTreeModel model) { | ||||
|  | ||||
| @@ -132,7 +124,7 @@ class _PaginatedStockItemListState extends PaginatedSearchState<PaginatedStockIt | ||||
|         ), | ||||
|       ), | ||||
|       onTap: () { | ||||
|         _openItem(context, item.pk); | ||||
|         Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user