mirror of
https://github.com/inventree/inventree-app.git
synced 2025-06-13 10:45:29 +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