From 18ef039f4a068c4ccdbbebec11759d263883c53e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 21:06:21 +1000 Subject: [PATCH 01/18] POST barcode data to server, and interpret the response --- lib/barcode.dart | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/lib/barcode.dart b/lib/barcode.dart index 975fd088..740e5bb3 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -5,6 +5,8 @@ import 'package:qr_utils/qr_utils.dart'; import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/inventree/part.dart'; +import 'package:InvenTree/api.dart'; + import 'package:InvenTree/widget/location_display.dart'; import 'package:InvenTree/widget/part_detail.dart'; import 'package:InvenTree/widget/category_display.dart'; @@ -12,7 +14,7 @@ import 'package:InvenTree/widget/stock_detail.dart'; import 'dart:convert'; -void scanQrCode(BuildContext context) async { +Future scanQrCode(BuildContext context) async { QrUtils.scanQR.then((String result) { @@ -21,6 +23,47 @@ void scanQrCode(BuildContext context) async { // Look for JSON data in the result... final data = json.decode(result); + InvenTreeAPI().post("barcode/", body: data).then((var response) { + + if (response.statusCode != 200) { + showDialog( + context: context, + child: new SimpleDialog( + title: Text("Server Error"), + children: [ + ListTile( + title: Text("Error ${response.statusCode}"), + subtitle: Text("${response.body.toString().split("\n").first}"), + ) + ], + ), + ); + + return; + } + + final Map body = json.decode(response.body); + + if (body.containsKey('error')) { + showDialog( + context: context, + child: new SimpleDialog( + title: Text("Barcode Error"), + children: [ + ListTile( + title: Text("${body['error']}"), + subtitle: Text("Plugin: ${body['plugin'] ?? ''}"), + ) + ], + ) + ); + } + + print("body: ${body.toString()}"); + + }); + + /* // Look for an 'InvenTree' style barcode if ((data['tool'] ?? '').toString().toLowerCase() == 'inventree') { _handleInvenTreeBarcode(context, data); @@ -39,6 +82,8 @@ void scanQrCode(BuildContext context) async { ); } + */ + }); } From 83cc92c4225cb10a2601727f9fba66d26b7a2aed Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 21:59:58 +1000 Subject: [PATCH 02/18] Handle barcode data passed back from the server --- lib/barcode.dart | 103 ++++++++++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/lib/barcode.dart b/lib/barcode.dart index 740e5bb3..1fb9df39 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -57,59 +57,80 @@ Future scanQrCode(BuildContext context) async { ], ) ); + } else if (body.containsKey('success')) { + // Decode the barcode! + // Ideally, the server has returned unto us something sensible... + _handleBarcode(context, body); + } else { + showDialog( + context: context, + child: new SimpleDialog( + title: Text("Unknown response"), + children: [ + ListTile( + title: Text("Response data"), + subtitle: Text("${body.toString()}"), + ) + ], + ) + ); } print("body: ${body.toString()}"); }); - - /* - // Look for an 'InvenTree' style barcode - if ((data['tool'] ?? '').toString().toLowerCase() == 'inventree') { - _handleInvenTreeBarcode(context, data); - } - - // Unknown barcode style! - else { - showDialog( - context: context, - child: new SimpleDialog( - title: new Text("Unknown barcode"), - children: [ - Text("Data: $result"), - ] - ) - ); - } - - */ - }); } -void _handleInvenTreeBarcode(BuildContext context, Map data) { +void _handleBarcode(BuildContext context, Map data) { - final String codeType = (data['type'] ?? '').toString().toLowerCase(); + int id; - final int pk = (data['id'] ?? -1) as int; + // A stocklocation has been passed? + if (data.containsKey('stocklocation')) { - if (codeType == 'stocklocation') { + id = data['stocklocation']['id'] ?? null; - // Try to open a stock location... - InvenTreeStockLocation().get(pk).then((var loc) { - if (loc is InvenTreeStockLocation) { - Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); - } - }); + if (id != null) { + // Try to open a stock location... + InvenTreeStockLocation().get(id).then((var loc) { + if (loc is InvenTreeStockLocation) { + Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); + } + }); + } - } else if (codeType == 'stockitem') { - InvenTreeStockItem().get(pk).then((var item) { - Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); - }); - } else if (codeType == 'part') { - InvenTreePart().get(pk).then((var part) { - Navigator.push(context, - MaterialPageRoute(builder: (context) => PartDetailWidget(part))); - }); + } else if (data.containsKey('stockitem')) { + + id = data['stockitem']['id'] ?? null; + + if (id != null) { + InvenTreeStockItem().get(id).then((var item) { + Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); + }); + } + } else if (data.containsKey('part')) { + + id = data['part']['id'] ?? null; + + if (id != null) { + InvenTreePart().get(id).then((var part) { + Navigator.push(context, + MaterialPageRoute(builder: (context) => PartDetailWidget(part))); + }); + } + } else { + showDialog( + context: context, + child: SimpleDialog( + title: Text("Unknown response"), + children: [ + ListTile( + title: Text("Response data"), + subtitle: Text(data.toString()), + ) + ], + ) + ); } } \ No newline at end of file From ca7505796d38b185d3e2bc89b395a7199aeb9bb9 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 22:11:38 +1000 Subject: [PATCH 03/18] Show progress dialog when requesting info from server --- lib/barcode.dart | 24 ++++++++++++++++++++++++ lib/widget/dialogs.dart | 21 +++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 lib/widget/dialogs.dart diff --git a/lib/barcode.dart b/lib/barcode.dart index 1fb9df39..9f608a33 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -1,3 +1,4 @@ +import 'package:InvenTree/widget/dialogs.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:qr_utils/qr_utils.dart'; @@ -23,8 +24,13 @@ Future scanQrCode(BuildContext context) async { // Look for JSON data in the result... final data = json.decode(result); + showProgressDialog(context, "Querying Server", "Sending barcode data to server"); + InvenTreeAPI().post("barcode/", body: data).then((var response) { + // Close the progress dialog + Navigator.pop(context); + if (response.statusCode != 200) { showDialog( context: context, @@ -93,7 +99,13 @@ void _handleBarcode(BuildContext context, Map data) { if (id != null) { // Try to open a stock location... + + showProgressDialog(context, "Loading data", "Requesting stock location information from server"); + InvenTreeStockLocation().get(id).then((var loc) { + + hideProgressDialog(context); + if (loc is InvenTreeStockLocation) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); } @@ -105,7 +117,13 @@ void _handleBarcode(BuildContext context, Map data) { id = data['stockitem']['id'] ?? null; if (id != null) { + + showProgressDialog(context, "Loading data", "Requesting stock item information from server"); + InvenTreeStockItem().get(id).then((var item) { + + hideProgressDialog(context); + Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); }); } @@ -114,7 +132,13 @@ void _handleBarcode(BuildContext context, Map data) { id = data['part']['id'] ?? null; if (id != null) { + + showProgressDialog(context, "Loading data", "Requesting part information from server"); + InvenTreePart().get(id).then((var part) { + + hideProgressDialog(context); + Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); }); diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart new file mode 100644 index 00000000..5daa7092 --- /dev/null +++ b/lib/widget/dialogs.dart @@ -0,0 +1,21 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +void showProgressDialog(BuildContext context, String title, String description) { + showDialog( + context: context, + barrierDismissible: false, + child: SimpleDialog( + title: Text(title), + children: [ + CircularProgressIndicator(), + Text(description), + ], + ) + ); +} + +void hideProgressDialog(BuildContext context) { + Navigator.pop(context); +} \ No newline at end of file From 8bd022bccdaf237b729d119807ba564bcdedc336 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 22:18:05 +1000 Subject: [PATCH 04/18] Progress dialog is now a part of the model GET request --- lib/barcode.dart | 25 +++---------------------- lib/inventree/company.dart | 4 ++++ lib/inventree/model.dart | 10 +++++++++- lib/inventree/part.dart | 7 +++++++ lib/inventree/stock.dart | 8 ++++++++ lib/widget/category_display.dart | 6 +++--- lib/widget/company_list.dart | 2 +- lib/widget/location_display.dart | 6 +++--- lib/widget/part_detail.dart | 2 +- lib/widget/stock_detail.dart | 4 ++-- 10 files changed, 41 insertions(+), 33 deletions(-) diff --git a/lib/barcode.dart b/lib/barcode.dart index 9f608a33..fc1d02c1 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -98,14 +98,7 @@ void _handleBarcode(BuildContext context, Map data) { id = data['stocklocation']['id'] ?? null; if (id != null) { - // Try to open a stock location... - - showProgressDialog(context, "Loading data", "Requesting stock location information from server"); - - InvenTreeStockLocation().get(id).then((var loc) { - - hideProgressDialog(context); - + InvenTreeStockLocation().get(context, id).then((var loc) { if (loc is InvenTreeStockLocation) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); } @@ -117,13 +110,7 @@ void _handleBarcode(BuildContext context, Map data) { id = data['stockitem']['id'] ?? null; if (id != null) { - - showProgressDialog(context, "Loading data", "Requesting stock item information from server"); - - InvenTreeStockItem().get(id).then((var item) { - - hideProgressDialog(context); - + InvenTreeStockItem().get(context, id).then((var item) { Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); }); } @@ -132,13 +119,7 @@ void _handleBarcode(BuildContext context, Map data) { id = data['part']['id'] ?? null; if (id != null) { - - showProgressDialog(context, "Loading data", "Requesting part information from server"); - - InvenTreePart().get(id).then((var part) { - - hideProgressDialog(context); - + InvenTreePart().get(context, id).then((var part) { Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); }); diff --git a/lib/inventree/company.dart b/lib/inventree/company.dart index 74ff87c8..7958d7f3 100644 --- a/lib/inventree/company.dart +++ b/lib/inventree/company.dart @@ -5,6 +5,10 @@ import 'model.dart'; * The InvenTreeCompany class repreents the Company model in the InvenTree database. */ class InvenTreeCompany extends InvenTreeModel { + + @override + String NAME = "Company"; + @override String URL = "company/"; diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index a501ce7a..47b4bf6d 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -1,4 +1,6 @@ import 'package:InvenTree/api.dart'; +import 'package:InvenTree/widget/dialogs.dart'; +import 'package:flutter/cupertino.dart'; import 'dart:convert'; @@ -15,6 +17,8 @@ class InvenTreeModel { // Override the endpoint URL for each subclass String URL = ""; + String NAME = "Model"; + // JSON data which defines this object Map jsondata = {}; @@ -100,7 +104,7 @@ class InvenTreeModel { } // Return the detail view for the associated pk - Future get(int pk, {Map filters}) async { + Future get(BuildContext context, int pk, {Map filters}) async { // TODO - Add "timeout" // TODO - Add error catching @@ -122,8 +126,12 @@ class InvenTreeModel { print("GET: $addr ${params.toString()}"); + showProgressDialog(context, "Requesting Data", "Requesting ${NAME} data from server"); + var response = await api.get(addr, params: params); + hideProgressDialog(context); + if (response.statusCode != 200) { print("Error retrieving data"); return null; diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 3283a19c..4732fa2a 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -9,6 +9,10 @@ import 'package:path/path.dart' as path; import 'package:http/http.dart' as http; class InvenTreePartCategory extends InvenTreeModel { + + @override + String NAME = "PartCategory"; + @override String URL = "part/category/"; @@ -61,6 +65,9 @@ class InvenTreePartCategory extends InvenTreeModel { class InvenTreePart extends InvenTreeModel { + @override + String Name = "Part"; + @override String URL = "part/"; diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index e8cfcf22..ebcef711 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -6,6 +6,10 @@ import 'model.dart'; import 'package:InvenTree/api.dart'; class InvenTreeStockItem extends InvenTreeModel { + + @override + String NAME = "StockItem"; + @override String URL = "stock/"; @@ -208,6 +212,10 @@ class InvenTreeStockItem extends InvenTreeModel { class InvenTreeStockLocation extends InvenTreeModel { + + @override + String NAME = "StockLocation"; + @override String URL = "stock/location/"; diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 7f7f47d2..4de2ac42 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -112,7 +112,7 @@ class _CategoryDisplayState extends State { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); } else { // TODO - Refactor this code into the InvenTreePart class - InvenTreePartCategory().get(category.parentId).then((var cat) { + InvenTreePartCategory().get(context, category.parentId).then((var cat) { if (cat is InvenTreePartCategory) { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); } @@ -211,7 +211,7 @@ class SubcategoryList extends StatelessWidget { void _openCategory(BuildContext context, int pk) { // Attempt to load the sub-category. - InvenTreePartCategory().get(pk).then((var cat) { + InvenTreePartCategory().get(context, pk).then((var cat) { if (cat is InvenTreePartCategory) { Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); @@ -252,7 +252,7 @@ class PartList extends StatelessWidget { void _openPart(BuildContext context, int pk) { // Attempt to load the part information - InvenTreePart().get(pk).then((var part) { + InvenTreePart().get(context, pk).then((var part) { if (part is InvenTreePart) { Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); diff --git a/lib/widget/company_list.dart b/lib/widget/company_list.dart index 512c198f..760a9b8c 100644 --- a/lib/widget/company_list.dart +++ b/lib/widget/company_list.dart @@ -85,7 +85,7 @@ class _CompanyListState extends State { ), onTap: () { if (company.pk > 0) { - InvenTreeCompany().get(company.pk).then((var c) { + InvenTreeCompany().get(context, company.pk).then((var c) { Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyDetailWidget(c))); }); } diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index fa1456ac..15b970f1 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -119,7 +119,7 @@ class _LocationDisplayState extends State { if (location.parentId < 0) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); } else { - InvenTreeStockLocation().get(location.parentId).then((var loc) { + InvenTreeStockLocation().get(context, location.parentId).then((var loc) { if (loc is InvenTreeStockLocation) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); } @@ -208,7 +208,7 @@ class SublocationList extends StatelessWidget { void _openLocation(BuildContext context, int pk) { - InvenTreeStockLocation().get(pk).then((var loc) { + InvenTreeStockLocation().get(context, pk).then((var loc) { if (loc is InvenTreeStockLocation) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); @@ -244,7 +244,7 @@ class StockList extends StatelessWidget { StockList(this._items); void _openItem(BuildContext context, int pk) { - InvenTreeStockItem().get(pk).then((var item) { + InvenTreeStockItem().get(context, pk).then((var item) { if (item is InvenTreeStockItem) { Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); } diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index e207db40..3c441a1b 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -62,7 +62,7 @@ class _PartDisplayState extends State { leading: FaIcon(FontAwesomeIcons.stream), onTap: () { if (part.categoryId > 0) { - InvenTreePartCategory().get(part.categoryId).then((var cat) { + InvenTreePartCategory().get(context, part.categoryId).then((var cat) { Navigator.push(context, MaterialPageRoute( builder: (context) => CategoryDisplayWidget(cat))); }); diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index a76873e5..175db487 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -322,7 +322,7 @@ class _StockItemDisplayState extends State { leading: FaIcon(FontAwesomeIcons.shapes), onTap: () { if (item.partId > 0) { - InvenTreePart().get(item.partId).then((var part) { + InvenTreePart().get(context, item.partId).then((var part) { if (part is InvenTreePart) { Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); } @@ -362,7 +362,7 @@ class _StockItemDisplayState extends State { leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), onTap: () { if (item.locationId > 0) { - InvenTreeStockLocation().get(item.locationId).then((var loc) { + InvenTreeStockLocation().get(context, item.locationId).then((var loc) { Navigator.push(context, MaterialPageRoute( builder: (context) => LocationDisplayWidget(loc))); }); From 200d2edc9a6c9e6cfb58b8e4afe0e8e57dd8d324 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 22:57:58 +1000 Subject: [PATCH 05/18] Add some docs --- lib/barcode.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/barcode.dart b/lib/barcode.dart index fc1d02c1..243e3497 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -26,6 +26,11 @@ Future scanQrCode(BuildContext context) async { showProgressDialog(context, "Querying Server", "Sending barcode data to server"); + /* + * POST the scanned barcode data to the server. + * It is the responsibility of the server to validate and sanitize the barcode data, + * and return a "common" response that we know how to deal with. + */ InvenTreeAPI().post("barcode/", body: data).then((var response) { // Close the progress dialog From d0f1a4d4ced55827c301470c5be1145f08cbf0e5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 23:11:30 +1000 Subject: [PATCH 06/18] error dialog if the barcode cannot be converted to JSON! --- lib/barcode.dart | 10 +++++++++- lib/widget/dialogs.dart | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/barcode.dart b/lib/barcode.dart index 243e3497..8bc8bda3 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -21,8 +21,16 @@ Future scanQrCode(BuildContext context) async { print("Scanned: $result"); + Map data; + // Look for JSON data in the result... - final data = json.decode(result); + try { + data = json.decode(result); + } on FormatException { + showErrorDialog(context, "Bad barcode data", result); + + return; + } showProgressDialog(context, "Querying Server", "Sending barcode data to server"); diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 5daa7092..db6a0eae 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -1,6 +1,25 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +void showErrorDialog(BuildContext context, String title, String description) { + showDialog( + context: context, + child: SimpleDialog( + title: ListTile( + title: Text("Error"), + leading: FaIcon(FontAwesomeIcons.exclamationCircle), + ), + children: [ + ListTile( + title: Text(title), + subtitle: Text(description) + ) + ] + ) + ); +} void showProgressDialog(BuildContext context, String title, String description) { showDialog( From cd864abfbefea0686bd35e821f38f586d4592081 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 14 Apr 2020 23:23:24 +1000 Subject: [PATCH 07/18] Don't force barcode data to be a JSON object. Just send the scanned data to the server, and let it handle it... --- lib/barcode.dart | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/barcode.dart b/lib/barcode.dart index 8bc8bda3..07500d98 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -17,20 +17,9 @@ import 'dart:convert'; Future scanQrCode(BuildContext context) async { - QrUtils.scanQR.then((String result) { + QrUtils.scanQR.then((String barcode) { - print("Scanned: $result"); - - Map data; - - // Look for JSON data in the result... - try { - data = json.decode(result); - } on FormatException { - showErrorDialog(context, "Bad barcode data", result); - - return; - } + print("Scanned: $barcode"); showProgressDialog(context, "Querying Server", "Sending barcode data to server"); @@ -39,7 +28,7 @@ Future scanQrCode(BuildContext context) async { * It is the responsibility of the server to validate and sanitize the barcode data, * and return a "common" response that we know how to deal with. */ - InvenTreeAPI().post("barcode/", body: data).then((var response) { + InvenTreeAPI().post("barcode/", body: {"barcode": barcode}).then((var response) { // Close the progress dialog Navigator.pop(context); From 5ed13e69aa678e9d0481d3cf71883fd8bceecbbd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 09:28:35 +1000 Subject: [PATCH 08/18] Add timeout funtionality to GET and LIST requests --- lib/barcode.dart | 3 +-- lib/inventree/model.dart | 43 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/barcode.dart b/lib/barcode.dart index 07500d98..d145a58d 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -30,8 +30,7 @@ Future scanQrCode(BuildContext context) async { */ InvenTreeAPI().post("barcode/", body: {"barcode": barcode}).then((var response) { - // Close the progress dialog - Navigator.pop(context); + hideProgressDialog(context); if (response.statusCode != 200) { showDialog( diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 47b4bf6d..5f541361 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:InvenTree/api.dart'; import 'package:InvenTree/widget/dialogs.dart'; import 'package:flutter/cupertino.dart'; @@ -128,7 +130,23 @@ class InvenTreeModel { showProgressDialog(context, "Requesting Data", "Requesting ${NAME} data from server"); - var response = await api.get(addr, params: params); + var response = await api.get(addr, params: params) + .timeout(Duration(seconds: 10)) + .catchError((e) { + + hideProgressDialog(context); + + if (e is TimeoutException) { + showErrorDialog(context, "Timeout", "No response from server"); + } else { + showErrorDialog(context, "Error", e.toString()); + } + return null; + }); + + if (response == null) { + return null; + } hideProgressDialog(context); @@ -143,7 +161,7 @@ class InvenTreeModel { } // Return list of objects from the database, with optional filters - Future> list({Map filters}) async { + Future> list(BuildContext context, {Map filters}) async { if (filters == null) { filters = {}; @@ -162,7 +180,26 @@ class InvenTreeModel { // TODO - Add "timeout" // TODO - Add error catching - var response = await api.get(URL, params:params); + showProgressDialog(context, "Requesting Data", "Requesting ${NAME} data from server"); + + var response = await api.get(URL, params:params) + .timeout(Duration(seconds: 10)) + .catchError((e) { + + hideProgressDialog(context); + + if (e is TimeoutException) { + showErrorDialog(context, "Timeout", "No response from server"); + } else { + showErrorDialog(context, "Error", e.toString()); + } + + return null; + }); + + if (response == null) { + return null; + } // A list of "InvenTreeModel" items List results = new List(); From 3fead77f6d169210af3d1cf0e3daaea38275806f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 10:33:10 +1000 Subject: [PATCH 09/18] Capture context data in the LocationDisplay widget --- lib/inventree/model.dart | 2 ++ lib/inventree/part.dart | 2 +- lib/main.dart | 46 +++++--------------------------- lib/widget/category_display.dart | 4 +-- lib/widget/company_list.dart | 2 +- lib/widget/dialogs.dart | 1 + lib/widget/location_display.dart | 41 +++++++++++++++++++--------- 7 files changed, 42 insertions(+), 56 deletions(-) diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 5f541361..05750586 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -201,6 +201,8 @@ class InvenTreeModel { return null; } + hideProgressDialog(context); + // A list of "InvenTreeModel" items List results = new List(); diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 4732fa2a..d3b3e269 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -66,7 +66,7 @@ class InvenTreePartCategory extends InvenTreeModel { class InvenTreePart extends InvenTreeModel { @override - String Name = "Part"; + String NAME = "Part"; @override String URL = "part/"; diff --git a/lib/main.dart b/lib/main.dart index 4dd424d9..a15fb541 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,6 +22,8 @@ import 'preferences.dart'; import 'package:InvenTree/inventree/part.dart'; + + void main() async { // await PrefService.init(prefix: "inventree_"); @@ -31,10 +33,10 @@ void main() async { // Load login details InvenTreePreferences().loadLoginDetails(); - runApp(MyApp()); + runApp(InvenTreeApp()); } -class MyApp extends StatelessWidget { +class InvenTreeApp extends StatelessWidget { // This widget is the root of your application. @override @@ -42,16 +44,8 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'InvenTree', theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. - primarySwatch: Colors.lightGreen, + primarySwatch: Colors.lightBlue, + secondaryHeaderColor: Colors.blueGrey, ), home: MyHomePage(title: 'InvenTree'), ); @@ -59,34 +53,6 @@ class MyApp extends StatelessWidget { } -class ProductList extends StatelessWidget { - final List _parts; - - ProductList(this._parts); - - Widget _buildPart(BuildContext context, int index) { - InvenTreePart part; - - if (index < _parts.length) { - part = _parts[index]; - } - - return Card( - child: Column( - children: [ - Text('${part.name} - ${part.description}'), - ] - ) - ); - } - - @override - Widget build(BuildContext context) { - return ListView.builder(itemBuilder: _buildPart, itemCount: _parts.length); - } -} - - class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 4de2ac42..cbda3091 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -56,7 +56,7 @@ class _CategoryDisplayState extends State { int pk = category?.pk ?? -1; // Request a list of sub-categories under this one - InvenTreePartCategory().list(filters: {"parent": "$pk"}).then((var cats) { + InvenTreePartCategory().list(context, filters: {"parent": "$pk"}).then((var cats) { _subcategories.clear(); for (var cat in cats) { @@ -70,7 +70,7 @@ class _CategoryDisplayState extends State { }); // Request a list of parts under this category - InvenTreePart().list(filters: {"category": "$pk"}).then((var parts) { + InvenTreePart().list(context, filters: {"category": "$pk"}).then((var parts) { _parts.clear(); for (var part in parts) { diff --git a/lib/widget/company_list.dart b/lib/widget/company_list.dart index 760a9b8c..8402b3aa 100644 --- a/lib/widget/company_list.dart +++ b/lib/widget/company_list.dart @@ -46,7 +46,7 @@ class _CompanyListState extends State { void _requestData() { - InvenTreeCompany().list(filters: _filters).then((var companies) { + InvenTreeCompany().list(context, filters: _filters).then((var companies) { _companies.clear(); diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index db6a0eae..ceb94158 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -22,6 +22,7 @@ void showErrorDialog(BuildContext context, String title, String description) { } void showProgressDialog(BuildContext context, String title, String description) { + showDialog( context: context, barrierDismissible: false, diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 15b970f1..29d91b15 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -10,6 +10,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; class LocationDisplayWidget extends StatefulWidget { + + LocationDisplayWidget(this.location, {Key key}) : super(key: key); final InvenTreeStockLocation location; @@ -23,8 +25,15 @@ class LocationDisplayWidget extends StatefulWidget { class _LocationDisplayState extends State { + BuildContext context; + _LocationDisplayState(this.location) { - _requestData(); + + } + + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _requestData(context)); } final InvenTreeStockLocation location; @@ -60,12 +69,14 @@ class _LocationDisplayState extends State { * - List of sublocations under this one * - List of stock items at this location */ - void _requestData() { + void _requestData(BuildContext context) { + + print("Requesting data!"); int pk = location?.pk ?? -1; // Request a list of sub-locations under this one - InvenTreeStockLocation().list(filters: {"parent": "$pk"}).then((var locs) { + InvenTreeStockLocation().list(context, filters: {"parent": "$pk"}).then((var locs) { _sublocations.clear(); for (var loc in locs) { @@ -76,18 +87,18 @@ class _LocationDisplayState extends State { setState(() {}); - // Request a list of stock-items under this one - InvenTreeStockItem().list(filters: {"location": "$pk"}).then((var items) { - _items.clear(); + // Request a list of stock-items under this one + InvenTreeStockItem().list(context, filters: {"location": "$pk"}).then((var items) { + _items.clear(); - for (var item in items) { - if (item is InvenTreeStockItem) { - _items.add(item); + for (var item in items) { + if (item is InvenTreeStockItem) { + _items.add(item); + } } - } - setState(() {}); - }); + setState(() {}); + }); }); } @@ -135,6 +146,12 @@ class _LocationDisplayState extends State { @override Widget build(BuildContext context) { + + // Save the context + this.context = context; + + print("Saved context!"); + return Scaffold( appBar: AppBar( title: Text(_title), From 5cc80ee3f4feb8a2c055d3362aae232895de2005 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 11:04:27 +1000 Subject: [PATCH 10/18] Add "pull to refresh" for location_display widget --- lib/widget/location_display.dart | 122 ++++++++++++++++--------------- 1 file changed, 62 insertions(+), 60 deletions(-) diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 29d91b15..6da793a8 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -28,7 +28,6 @@ class _LocationDisplayState extends State { BuildContext context; _LocationDisplayState(this.location) { - } void initState() { @@ -62,6 +61,10 @@ class _LocationDisplayState extends State { } } + Future _refresh() async { + await _requestData(context); + } + /* * Request data from the server. * It will be displayed once loaded @@ -69,9 +72,7 @@ class _LocationDisplayState extends State { * - List of sublocations under this one * - List of stock items at this location */ - void _requestData(BuildContext context) { - - print("Requesting data!"); + Future _requestData(BuildContext context) async { int pk = location?.pk ?? -1; @@ -150,68 +151,69 @@ class _LocationDisplayState extends State { // Save the context this.context = context; - print("Saved context!"); - return Scaffold( appBar: AppBar( title: Text(_title), ), drawer: new InvenTreeDrawer(context), - body: ListView( - children: [ - locationDescriptionCard(), - ExpansionPanelList( - expansionCallback: (int index, bool isExpanded) { - setState(() { - switch (index) { - case 0: - InvenTreePreferences().expandLocationList = !isExpanded; - break; - case 1: - InvenTreePreferences().expandStockList = !isExpanded; - break; - default: - break; - } - }); + body: new RefreshIndicator( + onRefresh: _refresh, + child: ListView( + children: [ + locationDescriptionCard(), + ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + setState(() { + switch (index) { + case 0: + InvenTreePreferences().expandLocationList = !isExpanded; + break; + case 1: + InvenTreePreferences().expandStockList = !isExpanded; + break; + default: + break; + } + }); - }, - children: [ - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Sublocations"), - leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), - trailing: Text("${_sublocations.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandLocationList = !InvenTreePreferences().expandLocationList; - }); - }, - ); - }, - body: SublocationList(_sublocations), - isExpanded: InvenTreePreferences().expandLocationList && _sublocations.length > 0, - ), - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Stock Items"), - leading: FaIcon(FontAwesomeIcons.boxes), - trailing: Text("${_items.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandStockList = !InvenTreePreferences().expandStockList; - }); - }, - ); - }, - body: StockList(_items), - isExpanded: InvenTreePreferences().expandStockList && _items.length > 0, - ) - ] - ), - ] + }, + children: [ + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Sublocations"), + leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), + trailing: Text("${_sublocations.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandLocationList = !InvenTreePreferences().expandLocationList; + }); + }, + ); + }, + body: SublocationList(_sublocations), + isExpanded: InvenTreePreferences().expandLocationList && _sublocations.length > 0, + ), + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Stock Items"), + leading: FaIcon(FontAwesomeIcons.boxes), + trailing: Text("${_items.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandStockList = !InvenTreePreferences().expandStockList; + }); + }, + ); + }, + body: StockList(_items), + isExpanded: InvenTreePreferences().expandStockList && _items.length > 0, + ) + ] + ), + ] + ) ) ); } From 58f6fd0f15f93034140e8556038a0266d05133da Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 11:39:29 +1000 Subject: [PATCH 11/18] Make a 'refreshable_state' class which makes API requests easier --- lib/widget/location_display.dart | 161 ++++++++++++------------------ lib/widget/refreshable_state.dart | 61 +++++++++++ 2 files changed, 123 insertions(+), 99 deletions(-) create mode 100644 lib/widget/refreshable_state.dart diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 6da793a8..7bb71d7c 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -8,10 +8,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; + class LocationDisplayWidget extends StatefulWidget { - - LocationDisplayWidget(this.location, {Key key}) : super(key: key); final InvenTreeStockLocation location; @@ -22,21 +22,15 @@ class LocationDisplayWidget extends StatefulWidget { _LocationDisplayState createState() => _LocationDisplayState(location); } - -class _LocationDisplayState extends State { - - BuildContext context; - - _LocationDisplayState(this.location) { - } - - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _requestData(context)); - } +class _LocationDisplayState extends RefreshableState { final InvenTreeStockLocation location; + @override + String app_bar_title = "Stock Location"; + + _LocationDisplayState(this.location) {} + List _sublocations = List(); String _locationFilter = ''; @@ -52,27 +46,8 @@ class _LocationDisplayState extends State { List _items = List(); - String get _title { - - if (location == null) { - return "Stock Locations"; - } else { - return "Stock Location - ${location.name}"; - } - } - - Future _refresh() async { - await _requestData(context); - } - - /* - * Request data from the server. - * It will be displayed once loaded - * - * - List of sublocations under this one - * - List of stock items at this location - */ - Future _requestData(BuildContext context) async { + @override + Future request(BuildContext context) async { int pk = location?.pk ?? -1; @@ -146,75 +121,63 @@ class _LocationDisplayState extends State { } @override - Widget build(BuildContext context) { + Widget getBody(BuildContext context) { - // Save the context - this.context = context; - - return Scaffold( - appBar: AppBar( - title: Text(_title), - ), - drawer: new InvenTreeDrawer(context), - body: new RefreshIndicator( - onRefresh: _refresh, - child: ListView( - children: [ - locationDescriptionCard(), - ExpansionPanelList( - expansionCallback: (int index, bool isExpanded) { - setState(() { - switch (index) { - case 0: - InvenTreePreferences().expandLocationList = !isExpanded; - break; - case 1: - InvenTreePreferences().expandStockList = !isExpanded; - break; - default: - break; - } - }); + return ListView( + children: [ + locationDescriptionCard(), + ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + setState(() { + switch (index) { + case 0: + InvenTreePreferences().expandLocationList = !isExpanded; + break; + case 1: + InvenTreePreferences().expandStockList = !isExpanded; + break; + default: + break; + } + }); + }, + children: [ + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Sublocations"), + leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), + trailing: Text("${_sublocations.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandLocationList = !InvenTreePreferences().expandLocationList; + }); + }, + ); }, - children: [ - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Sublocations"), - leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), - trailing: Text("${_sublocations.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandLocationList = !InvenTreePreferences().expandLocationList; - }); - }, - ); - }, - body: SublocationList(_sublocations), - isExpanded: InvenTreePreferences().expandLocationList && _sublocations.length > 0, - ), - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Stock Items"), - leading: FaIcon(FontAwesomeIcons.boxes), - trailing: Text("${_items.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandStockList = !InvenTreePreferences().expandStockList; - }); - }, - ); - }, - body: StockList(_items), - isExpanded: InvenTreePreferences().expandStockList && _items.length > 0, - ) - ] + body: SublocationList(_sublocations), + isExpanded: InvenTreePreferences().expandLocationList && _sublocations.length > 0, ), + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Stock Items"), + leading: FaIcon(FontAwesomeIcons.boxes), + trailing: Text("${_items.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandStockList = !InvenTreePreferences().expandStockList; + }); + }, + ); + }, + body: StockList(_items), + isExpanded: InvenTreePreferences().expandStockList && _items.length > 0, + ) ] - ) - ) + ), + ] ); } } diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart new file mode 100644 index 00000000..426254d6 --- /dev/null +++ b/lib/widget/refreshable_state.dart @@ -0,0 +1,61 @@ +import 'package:InvenTree/widget/drawer.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:InvenTree/widget/drawer.dart'; + + +abstract class RefreshableState extends State { + + // Storage for context once "Build" is called + BuildContext context; + + String app_bar_title = "App Bar Title"; + + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => request(context)); + } + + // Function to request data for this page + Future request(BuildContext context) async { + return; + } + + Future refresh() async { + await request(context); + setState(() {}); + } + + // Function to construct an appbar (override if needed) + AppBar getAppBar(BuildContext context) { + return AppBar( + title: Text(app_bar_title) + ); + } + + // Function to construct a drawer (override if needed) + Widget getDrawer(BuildContext context) { + return InvenTreeDrawer(context); + } + + // Function to construct a body (MUST BE PROVIDED) + Widget getBody(BuildContext context); + + @override + Widget build(BuildContext context) { + + // Save the context for future use + this.context = context; + + return Scaffold( + appBar: getAppBar(context), + drawer: getDrawer(context), + body: RefreshIndicator( + onRefresh: refresh, + child: getBody(context) + ) + ); + } +} \ No newline at end of file From 26c47b5fff044259b1f762880d0802a04f5b13a4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 11:50:04 +1000 Subject: [PATCH 12/18] the stock detail view is now a refreshable state --- lib/widget/stock_detail.dart | 48 ++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 175db487..f86d5b77 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -4,12 +4,15 @@ import 'package:InvenTree/inventree/stock.dart'; import 'package:InvenTree/inventree/part.dart'; import 'package:InvenTree/widget/location_display.dart'; import 'package:InvenTree/widget/part_detail.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:InvenTree/api.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; + import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; @@ -25,7 +28,10 @@ class StockDetailWidget extends StatefulWidget { } -class _StockItemDisplayState extends State { +class _StockItemDisplayState extends RefreshableState { + + @override + String app_bar_title = "Stock Item"; final TextEditingController _quantityController = TextEditingController(); final TextEditingController _notesController = TextEditingController(); @@ -42,13 +48,9 @@ class _StockItemDisplayState extends State { final InvenTreeStockItem item; - /** - * Function to reload the page data - */ - Future _refresh() async { - + @override + Future request(BuildContext context) async { await item.reload(); - setState(() {}); } void _editStockItem() { @@ -92,7 +94,7 @@ class _StockItemDisplayState extends State { _notesController.clear(); // TODO - Handle error cases - _refresh(); + refresh(); } void _addStockDialog() async { @@ -158,7 +160,7 @@ class _StockItemDisplayState extends State { // TODO - Handle error cases - _refresh(); + refresh(); } void _removeStockDialog() { @@ -227,7 +229,7 @@ class _StockItemDisplayState extends State { // TODO - Handle error cases - _refresh(); + refresh(); } void _countStockDialog() async { @@ -464,26 +466,30 @@ class _StockItemDisplayState extends State { return buttons; } + @override + Widget getBody(BuildContext context) { + return ListView( + children: stockTiles() + ); + } + @override Widget build(BuildContext context) { + + this.context = context; + return Scaffold( - appBar: AppBar( - title: Text("Stock Item"), - ), - drawer: new InvenTreeDrawer(context), + appBar: getAppBar(context), + drawer: getDrawer(context), floatingActionButton: SpeedDial( visible: true, animatedIcon: AnimatedIcons.menu_close, heroTag: 'stock-item-fab', children: actionButtons(), ), - body: Center( - child: new RefreshIndicator( - onRefresh: _refresh, - child: ListView( - children: stockTiles(), - ) - ) + body: RefreshIndicator( + onRefresh: refresh, + child: getBody(context) ) ); } From 9b144832730cfef7470e81a759689aecf1d8fab7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 11:54:46 +1000 Subject: [PATCH 13/18] part category display is now refreshable --- lib/widget/category_display.dart | 146 ++++++++++++++----------------- 1 file changed, 67 insertions(+), 79 deletions(-) diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index cbda3091..a1328b18 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -5,6 +5,7 @@ import 'package:InvenTree/preferences.dart'; import 'package:InvenTree/widget/part_detail.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -26,11 +27,12 @@ class CategoryDisplayWidget extends StatefulWidget { } -class _CategoryDisplayState extends State { +class _CategoryDisplayState extends RefreshableState { - _CategoryDisplayState(this.category) { - _requestData(); - } + @override + String app_bar_title = "Part Category"; + + _CategoryDisplayState(this.category) {} // The local InvenTreePartCategory object final InvenTreePartCategory category; @@ -39,19 +41,11 @@ class _CategoryDisplayState extends State { List _parts = List(); - String get _titleString { - - if (category == null) { - return "Part Categories"; - } else { - return "Part Category - ${category.name}"; - } - } - /* * Request data from the server */ - void _requestData() { + @override + Future request(BuildContext context) async { int pk = category?.pk ?? -1; @@ -127,74 +121,68 @@ class _CategoryDisplayState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(_titleString), - ), - drawer: new InvenTreeDrawer(context), - body: ListView( - children: [ - getCategoryDescriptionCard(), - ExpansionPanelList( - expansionCallback: (int index, bool isExpanded) { - setState(() { + Widget getBody(BuildContext context) { + return ListView( + children: [ + getCategoryDescriptionCard(), + ExpansionPanelList( + expansionCallback: (int index, bool isExpanded) { + setState(() { - switch (index) { - case 0: - InvenTreePreferences().expandCategoryList = !isExpanded; - break; - case 1: - InvenTreePreferences().expandPartList = !isExpanded; - break; - default: - break; - } - }); + switch (index) { + case 0: + InvenTreePreferences().expandCategoryList = !isExpanded; + break; + case 1: + InvenTreePreferences().expandPartList = !isExpanded; + break; + default: + break; + } + }); + }, + children: [ + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Subcategories"), + leading: FaIcon(FontAwesomeIcons.stream), + trailing: Text("${_subcategories.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandCategoryList = !InvenTreePreferences().expandCategoryList; + }); + }, + onLongPress: () { + // TODO - Context menu for e.g. creating a new PartCategory + }, + ); }, - children: [ - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Subcategories"), - leading: FaIcon(FontAwesomeIcons.stream), - trailing: Text("${_subcategories.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandCategoryList = !InvenTreePreferences().expandCategoryList; - }); - }, - onLongPress: () { - // TODO - Context menu for e.g. creating a new PartCategory - }, - ); - }, - body: SubcategoryList(_subcategories), - isExpanded: InvenTreePreferences().expandCategoryList && _subcategories.length > 0, - ), - ExpansionPanel( - headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile( - title: Text("Parts"), - leading: FaIcon(FontAwesomeIcons.shapes), - trailing: Text("${_parts.length}"), - onTap: () { - setState(() { - InvenTreePreferences().expandPartList = !InvenTreePreferences().expandPartList; - }); - }, - onLongPress: () { - // TODO - Context menu for e.g. creating a new Part - }, - ); - }, - body: PartList(_parts), - isExpanded: InvenTreePreferences().expandPartList && _parts.length > 0, - ) - ], + body: SubcategoryList(_subcategories), + isExpanded: InvenTreePreferences().expandCategoryList && _subcategories.length > 0, ), - ] - ) + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return ListTile( + title: Text("Parts"), + leading: FaIcon(FontAwesomeIcons.shapes), + trailing: Text("${_parts.length}"), + onTap: () { + setState(() { + InvenTreePreferences().expandPartList = !InvenTreePreferences().expandPartList; + }); + }, + onLongPress: () { + // TODO - Context menu for e.g. creating a new Part + }, + ); + }, + body: PartList(_parts), + isExpanded: InvenTreePreferences().expandPartList && _parts.length > 0, + ) + ], + ), + ] ); } } From 578d54367ac97c6ccb4ac233640cedacb91de0c2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 12:00:26 +1000 Subject: [PATCH 14/18] Part detail view is now refreshabel - lso add a progress dialog when reloading a model object --- lib/inventree/model.dart | 25 +++++++++++++++++++++++-- lib/widget/part_detail.dart | 30 ++++++++++++++---------------- lib/widget/stock_detail.dart | 2 +- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 05750586..96847ee6 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -89,9 +89,30 @@ class InvenTreeModel { /* * Reload this object, by requesting data from the server */ - Future reload() async { + Future reload(BuildContext context) async { - var response = await api.get(url, params: defaultGetFilters()); + showProgressDialog(context, "Refreshing data", "Refreshing data for ${NAME}"); + + var response = await api.get(url, params: defaultGetFilters()) + .timeout(Duration(seconds: 10)) + .catchError((e) { + + hideProgressDialog(context); + + if (e is TimeoutException) { + showErrorDialog(context, "Timeout", "No response from server"); + } else { + showErrorDialog(context, "Error", e.toString()); + } + + return null; + }); + + if (response == null) { + return false; + } + + hideProgressDialog(context); if (response.statusCode != 200) { print("Error retrieving data"); diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 3c441a1b..41211f86 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:InvenTree/api.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:InvenTree/widget/drawer.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -21,7 +22,10 @@ class PartDetailWidget extends StatefulWidget { } -class _PartDisplayState extends State { +class _PartDisplayState extends RefreshableState { + + @override + String app_bar_title = "Part"; _PartDisplayState(this.part) { // TODO @@ -29,6 +33,11 @@ class _PartDisplayState extends State { InvenTreePart part; + @override + Future request(BuildContext context) async { + await part.reload(context); + } + /* * Build a list of tiles to display under the part description */ @@ -154,22 +163,11 @@ class _PartDisplayState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("Part Details"), + Widget getBody(BuildContext context) { + return Center( + child: ListView( + children: partTiles(), ), - drawer: new InvenTreeDrawer(context), - floatingActionButton: FloatingActionButton( - child: FaIcon(FontAwesomeIcons.ellipsisH), - // TODO - Add pop-up icons - // Ref: https://stackoverflow.com/questions/46480221/flutter-floating-action-button-with-speed-dial#46480722 - ), - body: Center( - child: ListView( - children: partTiles(), - ), - ) ); } } \ No newline at end of file diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index f86d5b77..c180bba9 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -50,7 +50,7 @@ class _StockItemDisplayState extends RefreshableState { @override Future request(BuildContext context) async { - await item.reload(); + await item.reload(context); } void _editStockItem() { From 90a39ae3de2359c4b98336c239d6d1fa98e87505 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 12:07:36 +1000 Subject: [PATCH 15/18] Company list is now refreshable --- lib/widget/category_display.dart | 2 +- lib/widget/company_list.dart | 63 +++++++++++++------------------ lib/widget/location_display.dart | 2 +- lib/widget/part_detail.dart | 2 +- lib/widget/refreshable_state.dart | 4 +- lib/widget/stock_detail.dart | 2 +- 6 files changed, 32 insertions(+), 43 deletions(-) diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index a1328b18..46a04bde 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -30,7 +30,7 @@ class CategoryDisplayWidget extends StatefulWidget { class _CategoryDisplayState extends RefreshableState { @override - String app_bar_title = "Part Category"; + String getAppBarTitle(BuildContext context) { return "Part Category"; } _CategoryDisplayState(this.category) {} diff --git a/lib/widget/company_list.dart b/lib/widget/company_list.dart index 8402b3aa..767b8a79 100644 --- a/lib/widget/company_list.dart +++ b/lib/widget/company_list.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:InvenTree/api.dart'; import 'package:InvenTree/inventree/company.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; abstract class CompanyListWidget extends StatefulWidget { @@ -30,21 +31,23 @@ class CustomerListWidget extends CompanyListWidget { } -class _CompanyListState extends State { +class _CompanyListState extends RefreshableState { var _companies = new List(); var _filteredCompanies = new List(); - var _title = "Companies"; + String _title = "Companies"; + + @override + String getAppBarTitle(BuildContext context) { return _title; } Map _filters = Map(); - _CompanyListState(this._title, this._filters) { - _requestData(); - } + _CompanyListState(this._title, this._filters) {} - void _requestData() { + @override + Future request(BuildContext context) async { InvenTreeCompany().list(context, filters: _filters).then((var companies) { @@ -94,38 +97,24 @@ class _CompanyListState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("$_title"), - actions: [ - IconButton( - icon: FaIcon(FontAwesomeIcons.plus), - tooltip: 'New', - onPressed: null, - ) - ], + Widget getBody(BuildContext context) { + return ListView( + children: [ + TextField( + decoration: InputDecoration( + hintText: 'Filter results', + ), + onChanged: (String text) { + setState(() { + _filterResults(text); + }); + }, ), - drawer: new InvenTreeDrawer(context), - body: ListView( - children: [ - TextField( - decoration: InputDecoration( - hintText: 'Filter results', - ), - onChanged: (String text) { - setState(() { - _filterResults(text); - }); - }, - ), - ListView.builder( - shrinkWrap: true, - physics: ClampingScrollPhysics(), - itemBuilder: _showCompany, itemCount: _filteredCompanies.length) - ], - ) + ListView.builder( + shrinkWrap: true, + physics: ClampingScrollPhysics(), + itemBuilder: _showCompany, itemCount: _filteredCompanies.length) + ], ); } - } \ No newline at end of file diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 7bb71d7c..84362689 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -27,7 +27,7 @@ class _LocationDisplayState extends RefreshableState { final InvenTreeStockLocation location; @override - String app_bar_title = "Stock Location"; + String getAppBarTitle(BuildContext context) { return "Stock Location"; } _LocationDisplayState(this.location) {} diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 41211f86..4efc7a69 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -25,7 +25,7 @@ class PartDetailWidget extends StatefulWidget { class _PartDisplayState extends RefreshableState { @override - String app_bar_title = "Part"; + String getAppBarTitle(BuildContext context) { return "Part"; } _PartDisplayState(this.part) { // TODO diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart index 426254d6..bfb061ef 100644 --- a/lib/widget/refreshable_state.dart +++ b/lib/widget/refreshable_state.dart @@ -11,7 +11,7 @@ abstract class RefreshableState extends State { // Storage for context once "Build" is called BuildContext context; - String app_bar_title = "App Bar Title"; + String getAppBarTitle(BuildContext context) { return "App Bar Title"; } void initState() { super.initState(); @@ -31,7 +31,7 @@ abstract class RefreshableState extends State { // Function to construct an appbar (override if needed) AppBar getAppBar(BuildContext context) { return AppBar( - title: Text(app_bar_title) + title: Text(getAppBarTitle(context)) ); } diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index c180bba9..e77e6cc8 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -31,7 +31,7 @@ class StockDetailWidget extends StatefulWidget { class _StockItemDisplayState extends RefreshableState { @override - String app_bar_title = "Stock Item"; + String getAppBarTitle(BuildContext context) { return "Stock Item"; } final TextEditingController _quantityController = TextEditingController(); final TextEditingController _notesController = TextEditingController(); From 93630ea91090f3d7a693a2a7e6d49fb9f533f2ca Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 15 Apr 2020 12:47:17 +1000 Subject: [PATCH 16/18] Separate onBuild and refresh callbacks --- lib/widget/category_display.dart | 8 +++++--- lib/widget/company_detail.dart | 25 ++++++++++++++----------- lib/widget/company_list.dart | 5 +++++ lib/widget/location_display.dart | 5 +++++ lib/widget/refreshable_state.dart | 7 ++++++- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index 46a04bde..6832b212 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -41,9 +41,11 @@ class _CategoryDisplayState extends RefreshableState { List _parts = List(); - /* - * Request data from the server - */ + @override + Future onBuild(BuildContext context) async { + refresh(); + } + @override Future request(BuildContext context) async { diff --git a/lib/widget/company_detail.dart b/lib/widget/company_detail.dart index 56d16d7e..16201d5a 100644 --- a/lib/widget/company_detail.dart +++ b/lib/widget/company_detail.dart @@ -2,6 +2,7 @@ import 'package:InvenTree/api.dart'; import 'package:InvenTree/inventree/company.dart'; import 'package:InvenTree/widget/drawer.dart'; +import 'package:InvenTree/widget/refreshable_state.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -18,10 +19,18 @@ class CompanyDetailWidget extends StatefulWidget { } -class _CompanyDetailState extends State { +class _CompanyDetailState extends RefreshableState { final InvenTreeCompany company; + @override + String getAppBarTitle(BuildContext context) { return "Company"; } + + @override + Future request(BuildContext context) async { + await company.reload(context); + } + _CompanyDetailState(this.company) { // TODO } @@ -122,17 +131,11 @@ class _CompanyDetailState extends State { } @override - Widget build(BuildContext context) { + Widget getBody(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text("${company.name}"), - ), - drawer: new InvenTreeDrawer(context), - body: Center( - child: ListView( - children: _companyTiles(), - ) + return Center( + child: ListView( + children: _companyTiles(), ) ); } diff --git a/lib/widget/company_list.dart b/lib/widget/company_list.dart index 767b8a79..a627ee74 100644 --- a/lib/widget/company_list.dart +++ b/lib/widget/company_list.dart @@ -46,6 +46,11 @@ class _CompanyListState extends RefreshableState { _CompanyListState(this._title, this._filters) {} + @override + Future onBuild(BuildContext context) async { + refresh(); + } + @override Future request(BuildContext context) async { diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index 84362689..f8ab0c8d 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -46,6 +46,11 @@ class _LocationDisplayState extends RefreshableState { List _items = List(); + @override + Future onBuild(BuildContext context) async { + refresh(); + } + @override Future request(BuildContext context) async { diff --git a/lib/widget/refreshable_state.dart b/lib/widget/refreshable_state.dart index bfb061ef..b0b9ad3c 100644 --- a/lib/widget/refreshable_state.dart +++ b/lib/widget/refreshable_state.dart @@ -15,7 +15,12 @@ abstract class RefreshableState extends State { void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => request(context)); + WidgetsBinding.instance.addPostFrameCallback((_) => onBuild(context)); + } + + // Function called after the widget is first build + Future onBuild(BuildContext context) async { + return; } // Function to request data for this page From 0f472d448b0de7539862826f75b2931001f9573c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 16 Apr 2020 10:21:44 +1000 Subject: [PATCH 17/18] Udpate barcode decoding to match API changes --- lib/barcode.dart | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/barcode.dart b/lib/barcode.dart index d145a58d..44c173b9 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -51,6 +51,11 @@ Future scanQrCode(BuildContext context) async { final Map body = json.decode(response.body); + // TODO - Handle potential error decoding response + + print("Barcode response:"); + print(body.toString()); + if (body.containsKey('error')) { showDialog( context: context, @@ -91,39 +96,45 @@ Future scanQrCode(BuildContext context) async { void _handleBarcode(BuildContext context, Map data) { - int id; + int pk; // A stocklocation has been passed? if (data.containsKey('stocklocation')) { - id = data['stocklocation']['id'] ?? null; + pk = data['stocklocation']['pk'] as int ?? null; - if (id != null) { - InvenTreeStockLocation().get(context, id).then((var loc) { + if (pk != null) { + InvenTreeStockLocation().get(context, pk).then((var loc) { if (loc is InvenTreeStockLocation) { Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); } }); + } else { + // TODO - Show an error here! } } else if (data.containsKey('stockitem')) { - id = data['stockitem']['id'] ?? null; + pk = data['stockitem']['pk'] as int ?? null; - if (id != null) { - InvenTreeStockItem().get(context, id).then((var item) { + if (pk != null) { + InvenTreeStockItem().get(context, pk).then((var item) { Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); }); + } else { + // TODO - Show an error here! } } else if (data.containsKey('part')) { - id = data['part']['id'] ?? null; + pk = data['part']['pk'] as int ?? null; - if (id != null) { - InvenTreePart().get(context, id).then((var part) { + if (pk != null) { + InvenTreePart().get(context, pk).then((var part) { Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); }); + } else { + // TODO - Show an error here! } } else { showDialog( From e04dadcd2f0519e73726e371799f979f48cf5e77 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 16 Apr 2020 17:43:49 +1000 Subject: [PATCH 18/18] Add better error handling for API --- lib/api.dart | 13 ++++++++++--- lib/main.dart | 16 ---------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 6d272520..b2d8eaeb 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -182,15 +182,22 @@ class InvenTreeAPI { var response = await get("").timeout(Duration(seconds: 10)).catchError((error) { if (error is SocketException) { - errorMessage = "Could not connect to server."; - print(errorMessage); - throw errorMessage; + print("Could not connect to server"); + return null; + } else if (error is TimeoutException) { + print("Server timeout"); + return null; } else { // Unknown error type, re-throw error + print("Unknown error: ${error.toString()}"); throw error; } }); + if (response == null) { + return false; + } + if (response.statusCode != 200) { print("Invalid status code: " + response.statusCode.toString()); return false; diff --git a/lib/main.dart b/lib/main.dart index a15fb541..991d2452 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -134,22 +134,6 @@ class _MyHomePageState extends State { onConnectFailure("Could not connect to server"); } - }).catchError((e) { - - String fault = "Connection error"; - - _serverConnection = false; - _serverStatusColor = Color.fromARGB(255, 250, 50, 50); - - _serverStatus = "Error connecting to $_serverAddress"; - - if (e is TimeoutException) { - fault = "Timeout: No response from server"; - } else { - fault = e.toString(); - } - - onConnectFailure(fault); }); // Update widget state