2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-28 13:36:50 +00:00

Many many changes for null-safety support

This commit is contained in:
Oliver 2021-07-09 23:56:38 +10:00
parent 2988716bf3
commit d3eec6a79e
30 changed files with 563 additions and 456 deletions

View File

@ -1,6 +1,12 @@
## InvenTree App Release Notes ## InvenTree App Release Notes
--- ---
### 0.2.6 - July 2021
---
- Major code update with "null safety" features
- Updated translations
### 0.2.5 - June 2021 ### 0.2.5 - June 2021
--- ---

View File

@ -22,24 +22,29 @@ import 'package:one_context/one_context.dart';
*/ */
class InvenTreeFileService extends FileService { class InvenTreeFileService extends FileService {
HttpClient _client; HttpClient? _client = null;
InvenTreeFileService({HttpClient client, bool strictHttps = false}) { InvenTreeFileService({HttpClient? client, bool strictHttps = false}) {
_client = client ?? HttpClient(); _client = client ?? HttpClient();
_client.badCertificateCallback = (cert, host, port) {
print("BAD CERTIFICATE CALLBACK FOR IMAGE REQUEST"); if (_client != null) {
return !strictHttps; _client?.badCertificateCallback = (cert, host, port) {
}; print("BAD CERTIFICATE CALLBACK FOR IMAGE REQUEST");
return !strictHttps;
};
}
} }
@override @override
Future<FileServiceResponse> get(String url, Future<FileServiceResponse> get(String url,
{Map<String, String> headers = const {}}) async { {Map<String, String> headers = const {}}) async {
final Uri resolved = Uri.base.resolve(url); final Uri resolved = Uri.base.resolve(url);
final HttpClientRequest req = await _client.getUrl(resolved); final HttpClientRequest req = await _client!.getUrl(resolved);
headers?.forEach((key, value) {
headers.forEach((key, value) {
req.headers.add(key, value); req.headers.add(key, value);
}); });
final HttpClientResponse httpResponse = await req.close(); final HttpClientResponse httpResponse = await req.close();
final http.StreamedResponse _response = http.StreamedResponse( final http.StreamedResponse _response = http.StreamedResponse(
httpResponse.timeout(Duration(seconds: 60)), httpResponse.statusCode, httpResponse.timeout(Duration(seconds: 60)), httpResponse.statusCode,
@ -101,7 +106,7 @@ class InvenTreeAPI {
String makeUrl(String endpoint) => _makeUrl(endpoint); String makeUrl(String endpoint) => _makeUrl(endpoint);
UserProfile profile; UserProfile? profile = null;
Map<String, dynamic> roles = {}; Map<String, dynamic> roles = {};
@ -171,15 +176,19 @@ class InvenTreeAPI {
* - Request user token from the server * - Request user token from the server
* - Request user roles from the server * - Request user roles from the server
*/ */
Future<bool> _connect(BuildContext context) async { Future<bool> _connect() async {
if (profile == null) return false; if (profile == null) return false;
var ctx = OneContext().context; var ctx = OneContext().context;
String address = profile.server.trim(); String address = profile?.server ?? "";
String username = profile.username.trim(); String username = profile?.username ?? "";
String password = profile.password.trim(); String password = profile?.password ?? "";
address = address.trim();
username = username.trim();
password = password.trim();
if (address.isEmpty || username.isEmpty || password.isEmpty) { if (address.isEmpty || username.isEmpty || password.isEmpty) {
showSnackIcon( showSnackIcon(
@ -202,7 +211,8 @@ class InvenTreeAPI {
print("Connecting to ${apiUrl} -> username=${username}"); print("Connecting to ${apiUrl} -> username=${username}");
HttpClientResponse response; HttpClientResponse? response;
dynamic data; dynamic data;
response = await getResponse(""); response = await getResponse("");
@ -237,7 +247,7 @@ class InvenTreeAPI {
instance = data['instance'] ?? ''; instance = data['instance'] ?? '';
// Default API version is 1 if not provided // Default API version is 1 if not provided
_apiVersion = data['apiVersion'] as int ?? 1; _apiVersion = (data['apiVersion'] ?? 1) as int;
if (_apiVersion < _minApiVersion) { if (_apiVersion < _minApiVersion) {
@ -315,7 +325,7 @@ class InvenTreeAPI {
} }
bool disconnectFromServer() { void disconnectFromServer() {
print("InvenTreeAPI().disconnectFromServer()"); print("InvenTreeAPI().disconnectFromServer()");
_connected = false; _connected = false;
@ -324,7 +334,7 @@ class InvenTreeAPI {
profile = null; profile = null;
} }
Future<bool> connectToServer(BuildContext context) async { Future<bool> connectToServer() async {
// Ensure server is first disconnected // Ensure server is first disconnected
disconnectFromServer(); disconnectFromServer();
@ -345,7 +355,7 @@ class InvenTreeAPI {
_connecting = true; _connecting = true;
_connected = await _connect(context); _connected = await _connect();
print("_connect() returned result: ${_connected}"); print("_connect() returned result: ${_connected}");
@ -411,12 +421,14 @@ class InvenTreeAPI {
// Perform a PATCH request // Perform a PATCH request
Future<dynamic> patch(String url, {Map<String, String> body, int expectedStatusCode=200}) async { Future<dynamic> patch(String url, {Map<String, String>? body, int expectedStatusCode=200}) async {
var _url = makeApiUrl(url); var _url = makeApiUrl(url);
var _body = Map<String, String>(); var _body = Map<String, String>();
// Copy across provided data // Copy across provided data
body.forEach((K, V) => _body[K] = V); if (body != null) {
body.forEach((K, V) => _body[K] = V);
}
print("PATCH: " + _url); print("PATCH: " + _url);
@ -517,7 +529,8 @@ class InvenTreeAPI {
* Upload a file to the given URL * Upload a file to the given URL
*/ */
Future<http.StreamedResponse> uploadFile(String url, File f, Future<http.StreamedResponse> uploadFile(String url, File f,
{String name = "attachment", String method="POST", Map<String, String> fields}) async { {String name = "attachment", String method="POST", Map<String, String>? fields}) async {
var _url = makeApiUrl(url); var _url = makeApiUrl(url);
var request = http.MultipartRequest(method, Uri.parse(_url)); var request = http.MultipartRequest(method, Uri.parse(_url));
@ -543,7 +556,8 @@ class InvenTreeAPI {
* Perform a HTTP POST request * Perform a HTTP POST request
* Returns a json object (or null if unsuccessful) * Returns a json object (or null if unsuccessful)
*/ */
Future<dynamic> post(String url, {Map<String, dynamic> body, int expectedStatusCode=201}) async { Future<dynamic> post(String url, {Map<String, dynamic>? body, int expectedStatusCode=201}) async {
var _url = makeApiUrl(url); var _url = makeApiUrl(url);
print("POST: ${_url} -> ${body.toString()}"); print("POST: ${_url} -> ${body.toString()}");
@ -615,7 +629,7 @@ class InvenTreeAPI {
error.toString() error.toString()
); );
} else if (error is TimeoutException) { } else if (error is TimeoutException) {
showTimeoutError(ctx); showTimeoutError();
} else { } else {
showServerError( showServerError(
L10().serverError, L10().serverError,
@ -674,7 +688,7 @@ class InvenTreeAPI {
* and return the Response object * and return the Response object
* (or null if the request fails) * (or null if the request fails)
*/ */
Future<HttpClientResponse> getResponse(String url, {Map<String, String> params}) async { Future<HttpClientResponse?> getResponse(String url, {Map<String, String>? params}) async {
var _url = makeApiUrl(url); var _url = makeApiUrl(url);
print("GET: ${_url}"); print("GET: ${_url}");
@ -797,7 +811,7 @@ class InvenTreeAPI {
* Perform a HTTP GET request * Perform a HTTP GET request
* Returns a json object (or null if did not complete) * Returns a json object (or null if did not complete)
*/ */
Future<dynamic> get(String url, {Map<String, String> params, int expectedStatusCode=200}) async { Future<dynamic> get(String url, {Map<String, String>? params, int expectedStatusCode=200}) async {
var response = await getResponse(url, params: params); var response = await getResponse(url, params: params);
@ -836,7 +850,7 @@ class InvenTreeAPI {
if (_token.isNotEmpty) { if (_token.isNotEmpty) {
return "Token $_token"; return "Token $_token";
} else if (profile != null) { } else if (profile != null) {
return "Basic " + base64Encode(utf8.encode('${profile.username}:${profile.password}')); return "Basic " + base64Encode(utf8.encode('${profile?.username}:${profile?.password}'));
} else { } else {
return ""; return "";
} }
@ -846,13 +860,11 @@ class InvenTreeAPI {
static String get staticThumb => "/static/img/blank_image.thumbnail.png"; static String get staticThumb => "/static/img/blank_image.thumbnail.png";
/** /**
* Load image from the InvenTree server, * Load image from the InvenTree server,
* or from local cache (if it has been cached!) * or from local cache (if it has been cached!)
*/ */
CachedNetworkImage getImage(String imageUrl, {double height, double width}) { CachedNetworkImage getImage(String imageUrl, {double height = 0, double width = 0}) {
if (imageUrl.isEmpty) { if (imageUrl.isEmpty) {
imageUrl = staticImage; imageUrl = staticImage;
} }

View File

@ -35,8 +35,8 @@ class BarcodeHandler {
BarcodeHandler(); BarcodeHandler();
QRViewController _controller; QRViewController? _controller;
BuildContext _context; BuildContext? _context;
void successTone() async { void successTone() async {
@ -58,12 +58,12 @@ class BarcodeHandler {
} }
} }
Future<void> onBarcodeMatched(Map<String, dynamic> data) { Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
// Called when the server "matches" a barcode // Called when the server "matches" a barcode
// Override this function // Override this function
} }
Future<void> onBarcodeUnknown(Map<String, dynamic> data) { Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
// Called when the server does not know about a barcode // Called when the server does not know about a barcode
// Override this function // Override this function
@ -76,17 +76,17 @@ class BarcodeHandler {
); );
} }
Future<void> onBarcodeUnhandled(Map<String, dynamic> data) { Future<void> onBarcodeUnhandled(Map<String, dynamic> data) async {
failureTone(); failureTone();
// Called when the server returns an unhandled response // Called when the server returns an unhandled response
showServerError(L10().responseUnknown, data.toString()); showServerError(L10().responseUnknown, data.toString());
_controller.resumeCamera(); _controller?.resumeCamera();
} }
Future<void> processBarcode(BuildContext context, QRViewController _controller, String barcode, {String url = "barcode/"}) async { Future<void> processBarcode(BuildContext? context, QRViewController? _controller, String barcode, {String url = "barcode/"}) async {
this._context = context; this._context = context;
this._controller = _controller; this._controller = _controller;
@ -105,13 +105,13 @@ class BarcodeHandler {
} }
if (data.containsKey('error')) { if (data.containsKey('error')) {
_controller.resumeCamera(); _controller?.resumeCamera();
onBarcodeUnknown(data); onBarcodeUnknown(data);
} else if (data.containsKey('success')) { } else if (data.containsKey('success')) {
_controller.resumeCamera(); _controller?.resumeCamera();
onBarcodeMatched(data); onBarcodeMatched(data);
} else { } else {
_controller.resumeCamera(); _controller?.resumeCamera();
onBarcodeUnhandled(data); onBarcodeUnhandled(data);
} }
} }
@ -128,7 +128,7 @@ class BarcodeScanHandler extends BarcodeHandler {
String getOverlayText(BuildContext context) => L10().barcodeScanGeneral; String getOverlayText(BuildContext context) => L10().barcodeScanGeneral;
@override @override
Future<void> onBarcodeUnknown(Map<String, dynamic> data) { Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
failureTone(); failureTone();
@ -140,8 +140,9 @@ class BarcodeScanHandler extends BarcodeHandler {
} }
@override @override
Future<void> onBarcodeMatched(Map<String, dynamic> data) { Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
int pk;
int pk = -1;
print("Handle barcode:"); print("Handle barcode:");
print(data); print(data);
@ -149,16 +150,21 @@ class BarcodeScanHandler extends BarcodeHandler {
// A stocklocation has been passed? // A stocklocation has been passed?
if (data.containsKey('stocklocation')) { if (data.containsKey('stocklocation')) {
pk = data['stocklocation']['pk'] as int ?? null; pk = (data['stocklocation']?['pk'] ?? -1) as int;
if (pk != null) { if (pk > 0) {
successTone(); successTone();
InvenTreeStockLocation().get(_context, pk).then((var loc) { InvenTreeStockLocation().get(pk).then((var loc) {
if (loc is InvenTreeStockLocation) { if (loc is InvenTreeStockLocation) {
Navigator.of(_context).pop();
Navigator.push(_context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); var _ctx = _context;
if (_ctx != null) {
Navigator.of(_ctx).pop();
Navigator.push(_ctx, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
}
} }
}); });
} else { } else {
@ -173,15 +179,24 @@ class BarcodeScanHandler extends BarcodeHandler {
} else if (data.containsKey('stockitem')) { } else if (data.containsKey('stockitem')) {
pk = data['stockitem']['pk'] as int ?? null; pk = (data['stockitem']?['pk'] ?? -1) as int;
if (pk != null) { if (pk > 0) {
successTone(); successTone();
InvenTreeStockItem().get(_context, pk).then((var item) { InvenTreeStockItem().get(pk).then((var item) {
Navigator.of(_context).pop();
Navigator.push(_context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); var _ctx = _context;
if (_ctx != null) {
// Dispose of the barcode scanner
Navigator.of(_ctx).pop();
if (item is InvenTreeStockItem) {
Navigator.push(_ctx, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
}
}
}); });
} else { } else {
@ -194,15 +209,24 @@ class BarcodeScanHandler extends BarcodeHandler {
} }
} else if (data.containsKey('part')) { } else if (data.containsKey('part')) {
pk = data['part']['pk'] as int ?? null; pk = (data['part']?['pk'] ?? -1) as int;
if (pk != null) { if (pk > 0) {
successTone(); successTone();
InvenTreePart().get(_context, pk).then((var part) { InvenTreePart().get(pk).then((var part) {
Navigator.of(_context).pop();
Navigator.push(_context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); var _ctx = _context;
if (_ctx != null) {
// Dismiss the barcode scanner
Navigator.of(_ctx).pop();
if (part is InvenTreePart) {
Navigator.push(_ctx, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
}
}
}); });
} else { } else {
@ -221,8 +245,11 @@ class BarcodeScanHandler extends BarcodeHandler {
L10().barcodeUnknown, L10().barcodeUnknown,
success: false, success: false,
onAction: () { onAction: () {
var _ctx = OneContext().context;
showDialog( showDialog(
context: _context, context: _ctx,
builder: (BuildContext context) => SimpleDialog( builder: (BuildContext context) => SimpleDialog(
title: Text(L10().unknownResponse), title: Text(L10().unknownResponse),
children: <Widget>[ children: <Widget>[
@ -257,7 +284,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
String getOverlayText(BuildContext context) => L10().barcodeScanAssign; String getOverlayText(BuildContext context) => L10().barcodeScanAssign;
@override @override
Future<void> onBarcodeMatched(Map<String, dynamic> data) { Future<void> onBarcodeMatched(Map<String, dynamic> data) async {
failureTone(); failureTone();
@ -270,7 +297,7 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
} }
@override @override
Future<void> onBarcodeUnknown(Map<String, dynamic> data) { Future<void> onBarcodeUnknown(Map<String, dynamic> data) async {
// If the barcode is unknown, we *can* assign it to the stock item! // If the barcode is unknown, we *can* assign it to the stock item!
if (!data.containsKey("hash")) { if (!data.containsKey("hash")) {
@ -282,7 +309,6 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
// Send the 'hash' code as the UID for the stock item // Send the 'hash' code as the UID for the stock item
item.update( item.update(
_context,
values: { values: {
"uid": data['hash'], "uid": data['hash'],
} }
@ -292,8 +318,13 @@ class StockItemBarcodeAssignmentHandler extends BarcodeHandler {
failureTone(); failureTone();
// Close the barcode scanner // Close the barcode scanner
_controller.dispose(); _controller?.dispose();
Navigator.of(_context).pop();
var _ctx = (_context);
if (_ctx != null) {
Navigator.of(_ctx).pop();
}
showSnackIcon( showSnackIcon(
L10().barcodeAssigned, L10().barcodeAssigned,
@ -350,8 +381,13 @@ class StockItemScanIntoLocationHandler extends BarcodeHandler {
successTone(); successTone();
// Close the scanner // Close the scanner
_controller.dispose(); _controller?.dispose();
Navigator.of(_context).pop();
var _ctx = _context;
if (_ctx != null) {
Navigator.of(_ctx).pop();
}
showSnackIcon( showSnackIcon(
L10().barcodeScanIntoLocationSuccess, L10().barcodeScanIntoLocationSuccess,
@ -403,7 +439,7 @@ class StockLocationScanInItemsHandler extends BarcodeHandler {
int item_id = data['stockitem']['pk'] as int; int item_id = data['stockitem']['pk'] as int;
final InvenTreeStockItem item = await InvenTreeStockItem().get(_context, item_id); final InvenTreeStockItem? item = await InvenTreeStockItem().get(item_id) as InvenTreeStockItem;
if (item == null) { if (item == null) {
@ -459,7 +495,7 @@ class InvenTreeQRView extends StatefulWidget {
final BarcodeHandler _handler; final BarcodeHandler _handler;
InvenTreeQRView(this._handler, {Key key}) : super(key: key); InvenTreeQRView(this._handler, {Key? key}) : super(key: key);
@override @override
State<StatefulWidget> createState() => _QRViewState(_handler); State<StatefulWidget> createState() => _QRViewState(_handler);
@ -468,11 +504,11 @@ class InvenTreeQRView extends StatefulWidget {
class _QRViewState extends State<InvenTreeQRView> { class _QRViewState extends State<InvenTreeQRView> {
QRViewController _controller; QRViewController? _controller;
final BarcodeHandler _handler; final BarcodeHandler _handler;
BuildContext context; BuildContext? _context;
// In order to get hot reload to work we need to pause the camera if the platform // In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS. // is android, or resume the camera if the platform is iOS.
@ -480,9 +516,9 @@ class _QRViewState extends State<InvenTreeQRView> {
void reassemble() { void reassemble() {
super.reassemble(); super.reassemble();
if (Platform.isAndroid) { if (Platform.isAndroid) {
_controller.pauseCamera(); _controller?.pauseCamera();
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
_controller.resumeCamera(); _controller?.resumeCamera();
} }
} }
@ -494,7 +530,7 @@ class _QRViewState extends State<InvenTreeQRView> {
_controller = controller; _controller = controller;
controller.scannedDataStream.listen((barcode) { controller.scannedDataStream.listen((barcode) {
_controller?.pauseCamera(); _controller?.pauseCamera();
_handler.processBarcode(context, _controller, barcode.code); _handler.processBarcode(_context, _controller, barcode.code);
}); });
} }
@ -508,7 +544,7 @@ class _QRViewState extends State<InvenTreeQRView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Save the context for later on! // Save the context for later on!
this.context = context; this._context = context;
return Scaffold( return Scaffold(
body: Stack( body: Stack(

View File

@ -33,7 +33,7 @@ class InvenTreePageResponse {
int get count => _count; int get count => _count;
int get length => results?.length ?? 0; int get length => results.length;
List<InvenTreeModel> results = []; List<InvenTreeModel> results = [];
} }
@ -91,16 +91,16 @@ class InvenTreeModel {
} }
int get pk => jsondata['pk'] ?? -1; int get pk => (jsondata['pk'] ?? -1) as int;
// Some common accessors // Some common accessors
String get name => jsondata['name'] ?? ''; String get name => jsondata['name'] ?? '';
String get description => jsondata['description'] ?? ''; String get description => jsondata['description'] ?? '';
String get notes => jsondata['notes'] as String ?? ''; String get notes => jsondata['notes'] ?? '';
int get parentId => jsondata['parent'] as int ?? -1; int get parentId => (jsondata['parent'] ?? -1) as int;
// Legacy API provided external link as "URL", while newer API uses "link" // Legacy API provided external link as "URL", while newer API uses "link"
String get link => jsondata['link'] ?? jsondata['URL'] ?? ''; String get link => jsondata['link'] ?? jsondata['URL'] ?? '';
@ -127,7 +127,7 @@ class InvenTreeModel {
} }
} }
String get keywords => jsondata['keywords'] as String ?? ''; String get keywords => jsondata['keywords'] ?? '';
// Create a new object from JSON data (not a constructor!) // Create a new object from JSON data (not a constructor!)
InvenTreeModel createFromJson(Map<String, dynamic> json) { InvenTreeModel createFromJson(Map<String, dynamic> json) {
@ -142,7 +142,7 @@ class InvenTreeModel {
// Search this Model type in the database // Search this Model type in the database
Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String> filters}) async { Future<List<InvenTreeModel>> search(BuildContext context, String searchTerm, {Map<String, String>? filters}) async {
if (filters == null) { if (filters == null) {
filters = {}; filters = {};
@ -150,7 +150,7 @@ class InvenTreeModel {
filters["search"] = searchTerm; filters["search"] = searchTerm;
final results = list(context, filters: filters); final results = list(filters: filters);
return results; return results;
@ -164,7 +164,7 @@ class InvenTreeModel {
/* /*
* Reload this object, by requesting data from the server * Reload this object, by requesting data from the server
*/ */
Future<bool> reload(BuildContext context) async { Future<bool> reload() async {
var response = await api.get(url, params: defaultGetFilters()); var response = await api.get(url, params: defaultGetFilters());
@ -178,7 +178,7 @@ class InvenTreeModel {
} }
// POST data to update the model // POST data to update the model
Future<bool> update(BuildContext context, {Map<String, String> values}) async { Future<bool> update({Map<String, String>? values}) async {
var addr = path.join(URL, pk.toString()); var addr = path.join(URL, pk.toString());
@ -198,15 +198,15 @@ class InvenTreeModel {
} }
// Return the detail view for the associated pk // Return the detail view for the associated pk
Future<InvenTreeModel> get(BuildContext context, int pk, {Map<String, String> filters}) async { Future<InvenTreeModel?> get(int pk, {Map<String, String>? filters}) async {
// TODO - Add "timeout" // TODO - Add "timeout"
// TODO - Add error catching // TODO - Add error catching
var addr = path.join(URL, pk.toString()); var url = path.join(URL, pk.toString());
if (!addr.endsWith("/")) { if (!url.endsWith("/")) {
addr += "/"; url += "/";
} }
var params = defaultGetFilters(); var params = defaultGetFilters();
@ -214,13 +214,13 @@ class InvenTreeModel {
if (filters != null) { if (filters != null) {
// Override any default values // Override any default values
for (String key in filters.keys) { for (String key in filters.keys) {
params[key] = filters[key]; params[key] = filters[key] ?? '';
} }
} }
print("GET: $addr ${params.toString()}"); print("GET: $url ${params.toString()}");
var response = await api.get(addr, params: params); var response = await api.get(url, params: params);
if (response == null) { if (response == null) {
return null; return null;
@ -229,7 +229,7 @@ class InvenTreeModel {
return createFromJson(response); return createFromJson(response);
} }
Future<InvenTreeModel> create(BuildContext context, Map<String, dynamic> data) async { Future<InvenTreeModel?> create(Map<String, dynamic> data) async {
print("CREATE: ${URL} ${data.toString()}"); print("CREATE: ${URL} ${data.toString()}");
@ -241,8 +241,6 @@ class InvenTreeModel {
data.remove('id'); data.remove('id');
} }
InvenTreeModel _model;
var response = await api.post(URL, body: data); var response = await api.post(URL, body: data);
if (response == null) { if (response == null) {
@ -252,12 +250,12 @@ class InvenTreeModel {
return createFromJson(response); return createFromJson(response);
} }
Future<InvenTreePageResponse> listPaginated(int limit, int offset, {Map<String, String> filters}) async { Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String>? filters = null}) async {
var params = defaultListFilters(); var params = defaultListFilters();
if (filters != null) { if (filters != null) {
for (String key in filters.keys) { for (String key in filters.keys) {
params[key] = filters[key]; params[key] = filters[key] ?? '';
} }
} }
@ -285,14 +283,12 @@ class InvenTreeModel {
return page; return page;
} else { } else {
// Inavlid response
print("Invalid!");
return null; return null;
} }
} }
// Return list of objects from the database, with optional filters // Return list of objects from the database, with optional filters
Future<List<InvenTreeModel>> list(BuildContext context, {Map<String, String> filters}) async { Future<List<InvenTreeModel>> list({Map<String, String>? filters}) async {
if (filters == null) { if (filters == null) {
filters = {}; filters = {};
@ -302,7 +298,7 @@ class InvenTreeModel {
if (filters != null) { if (filters != null) {
for (String key in filters.keys) { for (String key in filters.keys) {
params[key] = filters[key]; params[key] = filters[key] ?? '';
} }
} }
@ -311,7 +307,7 @@ class InvenTreeModel {
var response = await api.get(URL, params: params); var response = await api.get(URL, params: params);
// A list of "InvenTreeModel" items // A list of "InvenTreeModel" items
List<InvenTreeModel> results = new List<InvenTreeModel>(); List<InvenTreeModel> results = new List<InvenTreeModel>.empty();
if (response == null) { if (response == null) {
return results; return results;

View File

@ -100,10 +100,11 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
} }
bool passFailStatus() { bool passFailStatus() {
var result = latestResult(); var result = latestResult();
if (result == null) { if (result == null) {
return null; return false;
} }
return result.result; return result.result;
@ -113,7 +114,7 @@ class InvenTreePartTestTemplate extends InvenTreeModel {
List<InvenTreeStockItemTestResult> results = []; List<InvenTreeStockItemTestResult> results = [];
// Return the most recent test result recorded against this template // Return the most recent test result recorded against this template
InvenTreeStockItemTestResult latestResult() { InvenTreeStockItemTestResult? latestResult() {
if (results.isEmpty) { if (results.isEmpty) {
return null; return null;
} }
@ -143,12 +144,12 @@ class InvenTreePart extends InvenTreeModel {
@override @override
Map<String, String> defaultGetFilters() { Map<String, String> defaultGetFilters() {
return { return {
"category_detail": "1", // Include category detail information "category_detail": "true", // Include category detail information
}; };
} }
// Cached list of stock items // Cached list of stock items
List<InvenTreeStockItem> stockItems = List<InvenTreeStockItem>(); List<InvenTreeStockItem> stockItems = List<InvenTreeStockItem>.empty();
int get stockItemCount => stockItems.length; int get stockItemCount => stockItems.length;
@ -156,7 +157,6 @@ class InvenTreePart extends InvenTreeModel {
Future<void> getStockItems(BuildContext context, {bool showDialog=false}) async { Future<void> getStockItems(BuildContext context, {bool showDialog=false}) async {
await InvenTreeStockItem().list( await InvenTreeStockItem().list(
context,
filters: { filters: {
"part": "${pk}", "part": "${pk}",
"in_stock": "true", "in_stock": "true",
@ -172,18 +172,17 @@ class InvenTreePart extends InvenTreeModel {
}); });
} }
int get supplier_count => jsondata['suppliers'] as int ?? 0; int get supplier_count => (jsondata['suppliers'] ?? 0) as int;
// Cached list of test templates // Cached list of test templates
List<InvenTreePartTestTemplate> testingTemplates = List<InvenTreePartTestTemplate>(); List<InvenTreePartTestTemplate> testingTemplates = List<InvenTreePartTestTemplate>.empty();
int get testTemplateCount => testingTemplates.length; int get testTemplateCount => testingTemplates.length;
// Request test templates from the serve // Request test templates from the serve
Future<void> getTestTemplates(BuildContext context, {bool showDialog=false}) async { Future<void> getTestTemplates({bool showDialog=false}) async {
InvenTreePartTestTemplate().list( InvenTreePartTestTemplate().list(
context,
filters: { filters: {
"part": "${pk}", "part": "${pk}",
}, },
@ -200,10 +199,10 @@ class InvenTreePart extends InvenTreeModel {
} }
// Get the number of stock on order for this Part // Get the number of stock on order for this Part
double get onOrder => double.tryParse(jsondata['ordering'].toString() ?? '0'); double get onOrder => double.tryParse(jsondata['ordering']) ?? 0;
// Get the stock count for this Part // Get the stock count for this Part
double get inStock => double.tryParse(jsondata['in_stock'].toString() ?? '0'); double get inStock => double.tryParse(jsondata['in_stock']) ?? 0;
String get inStockString { String get inStockString {
@ -215,51 +214,55 @@ class InvenTreePart extends InvenTreeModel {
} }
// Get the number of units being build for this Part // Get the number of units being build for this Part
double get building => double.tryParse(jsondata['building'].toString() ?? '0'); double get building => double.tryParse(jsondata['building']) ?? 0;
// Get the number of BOM items in this Part (if it is an assembly) // Get the number of BOM items in this Part (if it is an assembly)
int get bomItemCount => jsondata['bom_items'] as int ?? 0; int get bomItemCount => (jsondata['bom_items'] ?? 0) as int;
// Get the number of BOMs this Part is used in (if it is a component) // Get the number of BOMs this Part is used in (if it is a component)
int get usedInCount => jsondata['used_in'] as int ?? 0; int get usedInCount => (jsondata['used_in'] ?? 0) as int;
bool get isAssembly => jsondata['assembly'] ?? false; bool get isAssembly => (jsondata['assembly'] ?? false) as bool;
bool get isComponent => jsondata['component'] ?? false; bool get isComponent => (jsondata['component'] ?? false) as bool;
bool get isPurchaseable => jsondata['purchaseable'] ?? false; bool get isPurchaseable => (jsondata['purchaseable'] ?? false) as bool;
bool get isSalable => jsondata['salable'] ?? false; bool get isSalable => (jsondata['salable'] ?? false) as bool;
bool get isActive => jsondata['active'] ?? false; bool get isActive => (jsondata['active'] ?? false) as bool;
bool get isVirtual => jsondata['virtual'] ?? false; bool get isVirtual => (jsondata['virtual'] ?? false) as bool;
bool get isTrackable => jsondata['trackable'] ?? false; bool get isTrackable => (jsondata['trackable'] ?? false) as bool;
// Get the IPN (internal part number) for the Part instance // Get the IPN (internal part number) for the Part instance
String get IPN => jsondata['IPN'] as String ?? ''; String get IPN => jsondata['IPN'] ?? '';
// Get the revision string for the Part instance // Get the revision string for the Part instance
String get revision => jsondata['revision'] as String ?? ''; String get revision => jsondata['revision'] ?? '';
// Get the category ID for the Part instance (or 'null' if does not exist) // Get the category ID for the Part instance (or 'null' if does not exist)
int get categoryId => jsondata['category'] as int ?? null; int get categoryId => (jsondata['category'] ?? -1) as int;
// Get the category name for the Part instance // Get the category name for the Part instance
String get categoryName { String get categoryName {
if (categoryId == null) return ''; // Inavlid category ID
if (categoryId <= 0) return '';
if (!jsondata.containsKey('category_detail')) return ''; if (!jsondata.containsKey('category_detail')) return '';
return jsondata['category_detail']['name'] as String ?? ''; return jsondata['category_detail']?['name'] ?? '';
} }
// Get the category description for the Part instance // Get the category description for the Part instance
String get categoryDescription { String get categoryDescription {
if (categoryId == null) return ''; // Invalid category ID
if (categoryId <= 0) return '';
if (!jsondata.containsKey('category_detail')) return ''; if (!jsondata.containsKey('category_detail')) return '';
return jsondata['category_detail']['description'] as String ?? ''; return jsondata['category_detail']?['description'] ?? '';
} }
// Get the image URL for the Part instance // Get the image URL for the Part instance
String get _image => jsondata['image'] ?? ''; String get _image => jsondata['image'] ?? '';
@ -274,7 +277,7 @@ class InvenTreePart extends InvenTreeModel {
if (fn.isNotEmpty) return fn; if (fn.isNotEmpty) return fn;
List<String> elements = List<String>(); List<String> elements = List<String>.empty();
if (IPN.isNotEmpty) elements.add(IPN); if (IPN.isNotEmpty) elements.add(IPN);
@ -324,7 +327,7 @@ class InvenTreePart extends InvenTreeModel {
} }
// Return the "starred" status of this part // Return the "starred" status of this part
bool get starred => jsondata['starred'] as bool ?? false; bool get starred => (jsondata['starred'] ?? false) as bool;
InvenTreePart() : super(); InvenTreePart() : super();

View File

@ -138,14 +138,13 @@ class InvenTreeStockItem extends InvenTreeModel {
// TODO // TODO
} }
List<InvenTreePartTestTemplate> testTemplates = List<InvenTreePartTestTemplate>(); List<InvenTreePartTestTemplate> testTemplates = List<InvenTreePartTestTemplate>.empty();
int get testTemplateCount => testTemplates.length; int get testTemplateCount => testTemplates.length;
// Get all the test templates associated with this StockItem // Get all the test templates associated with this StockItem
Future<void> getTestTemplates(BuildContext context, {bool showDialog=false}) async { Future<void> getTestTemplates({bool showDialog=false}) async {
await InvenTreePartTestTemplate().list( await InvenTreePartTestTemplate().list(
context,
filters: { filters: {
"part": "${partId}", "part": "${partId}",
}, },
@ -160,14 +159,13 @@ class InvenTreeStockItem extends InvenTreeModel {
}); });
} }
List<InvenTreeStockItemTestResult> testResults = List<InvenTreeStockItemTestResult>(); List<InvenTreeStockItemTestResult> testResults = List<InvenTreeStockItemTestResult>.empty();
int get testResultCount => testResults.length; int get testResultCount => testResults.length;
Future<void> getTestResults(BuildContext context) async { Future<void> getTestResults() async {
await InvenTreeStockItemTestResult().list( await InvenTreeStockItemTestResult().list(
context,
filters: { filters: {
"stock_item": "${pk}", "stock_item": "${pk}",
"user_detail": "true", "user_detail": "true",
@ -183,7 +181,7 @@ class InvenTreeStockItem extends InvenTreeModel {
}); });
} }
Future<bool> uploadTestResult(BuildContext context, String testName, bool result, {String value, String notes, File attachment}) async { Future<bool> uploadTestResult(BuildContext context, String testName, bool result, {String? value, String? notes, File? attachment}) async {
Map<String, String> data = { Map<String, String> data = {
"stock_item": pk.toString(), "stock_item": pk.toString(),
@ -204,7 +202,7 @@ class InvenTreeStockItem extends InvenTreeModel {
* TODO: Is there a nice way to refactor this one? * TODO: Is there a nice way to refactor this one?
*/ */
if (attachment == null) { if (attachment == null) {
var _result = await InvenTreeStockItemTestResult().create(context, data); var _result = await InvenTreeStockItemTestResult().create(data);
return (_result != null) && (_result is InvenTreeStockItemTestResult); return (_result != null) && (_result is InvenTreeStockItemTestResult);
} else { } else {
@ -224,12 +222,12 @@ class InvenTreeStockItem extends InvenTreeModel {
int get partId => jsondata['part'] ?? -1; int get partId => jsondata['part'] ?? -1;
int get trackingItemCount => jsondata['tracking_items'] as int ?? 0; int get trackingItemCount => (jsondata['tracking_items'] ?? 0) as int;
// Date of last update // Date of last update
String get updated => jsondata["updated"] ?? ""; String get updated => jsondata["updated"] ?? "";
DateTime get stocktakeDate { DateTime? get stocktakeDate {
if (jsondata.containsKey("stocktake_date")) { if (jsondata.containsKey("stocktake_date")) {
if (jsondata["stocktake_date"] == null) { if (jsondata["stocktake_date"] == null) {
return null; return null;
@ -292,15 +290,18 @@ class InvenTreeStockItem extends InvenTreeModel {
*/ */
String get partThumbnail { String get partThumbnail {
String thumb; String thumb = "";
if (jsondata.containsKey('part_detail')) { thumb = jsondata['part_detail']?['thumbnail'] ?? '';
thumb = jsondata['part_detail']['thumbnail'] as String ?? '';
// Use 'image' as a backup
if (thumb.isEmpty) {
thumb = jsondata['part_detail']?['image'] ?? '';
} }
// Try a different approach // Try a different approach
if (thumb.isEmpty) { if (thumb.isEmpty) {
jsondata['part__thumbnail'] as String ?? ''; thumb = jsondata['part__thumbnail'] ?? '';
} }
// Still no thumbnail? Use the 'no image' image // Still no thumbnail? Use the 'no image' image
@ -309,7 +310,7 @@ class InvenTreeStockItem extends InvenTreeModel {
return thumb; return thumb;
} }
int get supplierPartId => jsondata['supplier_part'] as int ?? -1; int get supplierPartId => (jsondata['supplier_part'] ?? -1) as int;
String get supplierImage { String get supplierImage {
String thumb = ''; String thumb = '';
@ -341,9 +342,9 @@ class InvenTreeStockItem extends InvenTreeModel {
return sku; return sku;
} }
String get serialNumber => jsondata['serial'] as String ?? null; String get serialNumber => jsondata['serial'] ?? "";
double get quantity => double.tryParse(jsondata['quantity'].toString() ?? '0'); double get quantity => double.tryParse(jsondata['quantity']) ?? 0;
String get quantityString { String get quantityString {
@ -354,9 +355,9 @@ class InvenTreeStockItem extends InvenTreeModel {
} }
} }
int get locationId => jsondata['location'] as int ?? -1; int get locationId => (jsondata['location'] ?? -1) as int;
bool isSerialized() => serialNumber != null && quantity.toInt() == 1; bool isSerialized() => serialNumber.isNotEmpty && quantity.toInt() == 1;
String serialOrQuantityDisplay() { String serialOrQuantityDisplay() {
if (isSerialized()) { if (isSerialized()) {
@ -397,7 +398,7 @@ class InvenTreeStockItem extends InvenTreeModel {
String get displayQuantity { String get displayQuantity {
// Display either quantity or serial number! // Display either quantity or serial number!
if (serialNumber != null) { if (serialNumber.isNotEmpty) {
return "SN: $serialNumber"; return "SN: $serialNumber";
} else { } else {
return quantity.toString().trim(); return quantity.toString().trim();
@ -420,7 +421,7 @@ class InvenTreeStockItem extends InvenTreeModel {
* - Remove * - Remove
* - Count * - Count
*/ */
Future<bool> adjustStock(BuildContext context, String endpoint, double q, {String notes}) async { Future<bool> adjustStock(BuildContext context, String endpoint, double q, {String? notes}) async {
// Serialized stock cannot be adjusted // Serialized stock cannot be adjusted
if (isSerialized()) { if (isSerialized()) {
@ -456,30 +457,29 @@ class InvenTreeStockItem extends InvenTreeModel {
return true; return true;
} }
Future<bool> countStock(BuildContext context, double q, {String notes}) async { Future<bool> countStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/count", q, notes: notes); final bool result = await adjustStock(context, "/stock/count", q, notes: notes);
return result; return result;
} }
Future<bool> addStock(BuildContext context, double q, {String notes}) async { Future<bool> addStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/add/", q, notes: notes); final bool result = await adjustStock(context, "/stock/add/", q, notes: notes);
return result; return result;
} }
Future<bool> removeStock(BuildContext context, double q, {String notes}) async { Future<bool> removeStock(BuildContext context, double q, {String? notes}) async {
final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes); final bool result = await adjustStock(context, "/stock/remove/", q, notes: notes);
return result; return result;
} }
Future<bool> transferStock(int location, {double quantity, String notes}) async { Future<bool> transferStock(int location, {double? quantity, String? notes}) async {
if (quantity == null) {} else if ((quantity == null) || (quantity < 0) || (quantity > this.quantity)) {
if ((quantity < 0) || (quantity > this.quantity)) {
quantity = this.quantity; quantity = this.quantity;
} }

View File

@ -5,5 +5,5 @@ import 'package:one_context/one_context.dart';
// Shortcut function to reduce boilerplate! // Shortcut function to reduce boilerplate!
I18N L10() I18N L10()
{ {
return I18N.of(OneContext().context); return I18N.of(OneContext().context)!;
} }

@ -1 +1 @@
Subproject commit 05a5cbf63b4b5479162905def9fdadf21041212e Subproject commit 9cc07cdb0ec0012abcec827fc776d7c5473bb75e

View File

@ -52,7 +52,7 @@ class InvenTreeApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
builder: OneContext().builder, builder: OneContext().builder,
navigatorKey: OneContext().key, navigatorKey: OneContext().key,
onGenerateTitle: (BuildContext context) => I18N.of(context).appTitle, onGenerateTitle: (BuildContext context) => I18N.of(context)!.appTitle,
theme: ThemeData( theme: ThemeData(
primarySwatch: Colors.lightBlue, primarySwatch: Colors.lightBlue,
secondaryHeaderColor: Colors.blueGrey, secondaryHeaderColor: Colors.blueGrey,

View File

@ -15,15 +15,19 @@ class InvenTreePreferencesDB {
InvenTreePreferencesDB._(); InvenTreePreferencesDB._();
Completer<Database> _dbOpenCompleter; Completer<Database> _dbOpenCompleter = Completer();
bool isOpen = false;
Future<Database> get database async { Future<Database> get database async {
// If completer is null, AppDatabaseClass is newly instantiated, so database is not yet opened
if (_dbOpenCompleter == null) { if (!isOpen) {
_dbOpenCompleter = Completer();
// Calling _openDatabase will also complete the completer with database instance // Calling _openDatabase will also complete the completer with database instance
_openDatabase(); _openDatabase();
isOpen = true;
} }
// If the database is already opened, awaiting the future will happen instantly. // If the database is already opened, awaiting the future will happen instantly.
// Otherwise, awaiting the returned future will take some time - until complete() is called // Otherwise, awaiting the returned future will take some time - until complete() is called
// on the Completer in _openDatabase() below. // on the Completer in _openDatabase() below.

View File

@ -26,7 +26,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
final GlobalKey<FormState> _addProfileKey = new GlobalKey<FormState>(); final GlobalKey<FormState> _addProfileKey = new GlobalKey<FormState>();
List<UserProfile> profiles; List<UserProfile> profiles = new List<UserProfile>.empty();
_InvenTreeLoginSettingsState() { _InvenTreeLoginSettingsState() {
_reload(); _reload();
@ -40,14 +40,14 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
}); });
} }
void _editProfile(BuildContext context, {UserProfile userProfile, bool createNew = false}) { void _editProfile(BuildContext context, {UserProfile? userProfile, bool createNew = false}) {
var _name; var _name;
var _server; var _server;
var _username; var _username;
var _password; var _password;
UserProfile profile; UserProfile? profile;
if (userProfile != null) { if (userProfile != null) {
profile = userProfile; profile = userProfile;
@ -69,10 +69,10 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
_addProfile(profile); _addProfile(profile);
} else { } else {
profile.name = _name; profile?.name = _name;
profile.server = _server; profile?.server = _server;
profile.username = _username; profile?.username = _username;
profile.password = _password; profile?.password = _password;
_updateProfile(profile); _updateProfile(profile);
@ -82,28 +82,28 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
StringField( StringField(
label: L10().name, label: L10().name,
hint: "Enter profile name", hint: "Enter profile name",
initial: createNew ? '' : profile.name, initial: createNew ? '' : profile?.name ?? '',
onSaved: (value) => _name = value, onSaved: (value) => _name = value,
validator: _validateProfileName, validator: _validateProfileName,
), ),
StringField( StringField(
label: L10().server, label: L10().server,
hint: "http[s]://<server>:<port>", hint: "http[s]://<server>:<port>",
initial: createNew ? '' : profile.server, initial: createNew ? '' : profile?.server ?? '',
validator: _validateServer, validator: _validateServer,
onSaved: (value) => _server = value, onSaved: (value) => _server = value,
), ),
StringField( StringField(
label: L10().username, label: L10().username,
hint: L10().enterPassword, hint: L10().enterPassword,
initial: createNew ? '' : profile.username, initial: createNew ? '' : profile?.username ?? '',
onSaved: (value) => _username = value, onSaved: (value) => _username = value,
validator: _validateUsername, validator: _validateUsername,
), ),
StringField( StringField(
label: L10().password, label: L10().password,
hint: L10().enterUsername, hint: L10().enterUsername,
initial: createNew ? '' : profile.password, initial: createNew ? '' : profile?.password ?? '',
onSaved: (value) => _password = value, onSaved: (value) => _password = value,
validator: _validatePassword, validator: _validatePassword,
) )
@ -111,7 +111,7 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
); );
} }
String _validateProfileName(String value) { String? _validateProfileName(String value) {
if (value.isEmpty) { if (value.isEmpty) {
return 'Profile name cannot be empty'; return 'Profile name cannot be empty';
@ -122,14 +122,14 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
return null; return null;
} }
String _validateServer(String value) { String? _validateServer(String value) {
if (value.isEmpty) { if (value.isEmpty) {
return 'Server cannot be empty'; return L10().serverEmpty;
} }
if (!value.startsWith("http:") && !value.startsWith("https:")) { if (!value.startsWith("http:") && !value.startsWith("https:")) {
return 'Server must start with http[s]'; return L10().serverStart;
} }
// TODO: URL validator // TODO: URL validator
@ -137,17 +137,17 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
return null; return null;
} }
String _validateUsername(String value) { String? _validateUsername(String value) {
if (value.isEmpty) { if (value.isEmpty) {
return 'Username cannot be empty'; return L10().usernameEmpty;
} }
return null; return null;
} }
String _validatePassword(String value) { String? _validatePassword(String value) {
if (value.isEmpty) { if (value.isEmpty) {
return 'Password cannot be empty'; return L10().passwordEmpty;
} }
return null; return null;
@ -158,12 +158,18 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
// Disconnect InvenTree // Disconnect InvenTree
InvenTreeAPI().disconnectFromServer(); InvenTreeAPI().disconnectFromServer();
await UserProfileDBManager().selectProfile(profile.key); var key = profile.key;
if (key == null) {
return;
}
await UserProfileDBManager().selectProfile(key);
_reload(); _reload();
// Attempt server login (this will load the newly selected profile // Attempt server login (this will load the newly selected profile
InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) { InvenTreeAPI().connectToServer().then((result) {
_reload(); _reload();
}); });
@ -176,21 +182,25 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
_reload(); _reload();
if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) { if (InvenTreeAPI().isConnected() && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
InvenTreeAPI().disconnectFromServer(); InvenTreeAPI().disconnectFromServer();
} }
} }
void _updateProfile(UserProfile profile) async { void _updateProfile(UserProfile? profile) async {
if (profile == null) {
return;
}
await UserProfileDBManager().updateProfile(profile); await UserProfileDBManager().updateProfile(profile);
_reload(); _reload();
if (InvenTreeAPI().isConnected() && profile.key == InvenTreeAPI().profile.key) { if (InvenTreeAPI().isConnected() && InvenTreeAPI().profile != null && profile.key == (InvenTreeAPI().profile?.key ?? '')) {
// Attempt server login (this will load the newly selected profile // Attempt server login (this will load the newly selected profile
InvenTreeAPI().connectToServer(_loginKey.currentContext).then((result) { InvenTreeAPI().connectToServer().then((result) {
_reload(); _reload();
}); });
} }
@ -203,13 +213,13 @@ class _InvenTreeLoginSettingsState extends State<InvenTreeLoginSettingsWidget> {
_reload(); _reload();
} }
Widget _getProfileIcon(UserProfile profile) { Widget? _getProfileIcon(UserProfile profile) {
// Not selected? No icon for you! // Not selected? No icon for you!
if (profile == null || !profile.selected) return null; if (!profile.selected) return null;
// Selected, but (for some reason) not the same as the API... // Selected, but (for some reason) not the same as the API...
if (InvenTreeAPI().profile == null || InvenTreeAPI().profile.key != profile.key) { if ((InvenTreeAPI().profile?.key ?? '') != profile.key) {
return FaIcon( return FaIcon(
FontAwesomeIcons.questionCircle, FontAwesomeIcons.questionCircle,
color: Color.fromRGBO(250, 150, 50, 1) color: Color.fromRGBO(250, 150, 50, 1)

View File

@ -9,32 +9,32 @@ class UserProfile {
UserProfile({ UserProfile({
this.key, this.key,
this.name, this.name = "",
this.server, this.server = "",
this.username, this.username = "",
this.password, this.password = "",
this.selected, this.selected = false,
}); });
// ID of the profile // ID of the profile
int key; int? key;
// Name of the user profile // Name of the user profile
String name; String name = "";
// Base address of the InvenTree server // Base address of the InvenTree server
String server; String server = "";
// Username // Username
String username; String username = "";
// Password // Password
String password; String password = "";
bool selected = false; bool selected = false;
// User ID (will be provided by the server on log-in) // User ID (will be provided by the server on log-in)
int user_id; int user_id = -1;
factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile( factory UserProfile.fromJson(int key, Map<String, dynamic> json, bool isSelected) => UserProfile(
key: key, key: key,
@ -122,7 +122,7 @@ class UserProfileDBManager {
print("Deleted user profile <${profile.key}> - '${profile.name}'"); print("Deleted user profile <${profile.key}> - '${profile.name}'");
} }
Future<UserProfile> getSelectedProfile() async { Future<UserProfile?> getSelectedProfile() async {
/* /*
* Return the currently selected profile. * Return the currently selected profile.
* *
@ -133,7 +133,7 @@ class UserProfileDBManager {
final profiles = await store.find(await _db); final profiles = await store.find(await _db);
List<UserProfile> profileList = new List<UserProfile>(); List<UserProfile> profileList = new List<UserProfile>.empty();
for (int idx = 0; idx < profiles.length; idx++) { for (int idx = 0; idx < profiles.length; idx++) {
@ -158,7 +158,7 @@ class UserProfileDBManager {
final profiles = await store.find(await _db); final profiles = await store.find(await _db);
List<UserProfile> profileList = new List<UserProfile>(); List<UserProfile> profileList = new List<UserProfile>.empty();
for (int idx = 0; idx < profiles.length; idx++) { for (int idx = 0; idx < profiles.length; idx++) {

View File

@ -23,9 +23,9 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class CategoryDisplayWidget extends StatefulWidget { class CategoryDisplayWidget extends StatefulWidget {
CategoryDisplayWidget(this.category, {Key key}) : super(key: key); CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
final InvenTreePartCategory category; final InvenTreePartCategory? category;
@override @override
_CategoryDisplayState createState() => _CategoryDisplayState(category); _CategoryDisplayState createState() => _CategoryDisplayState(category);
@ -81,7 +81,7 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
void _editCategory(Map<String, String> values) async { void _editCategory(Map<String, String> values) async {
final bool result = await category.update(context, values: values); final bool result = await category!.update(values: values);
showSnackIcon( showSnackIcon(
result ? "Category edited" : "Category editing failed", result ? "Category edited" : "Category editing failed",
@ -93,6 +93,11 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
void _editCategoryDialog() { void _editCategoryDialog() {
// Cannot edit top-level category
if (category == null) {
return;
}
var _name; var _name;
var _description; var _description;
@ -108,12 +113,12 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
fields: <Widget>[ fields: <Widget>[
StringField( StringField(
label: L10().name, label: L10().name,
initial: category.name, initial: category?.name,
onSaved: (value) => _name = value onSaved: (value) => _name = value
), ),
StringField( StringField(
label: L10().description, label: L10().description,
initial: category.description, initial: category?.description,
onSaved: (value) => _description = value onSaved: (value) => _description = value
) )
] ]
@ -123,9 +128,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
_CategoryDisplayState(this.category) {} _CategoryDisplayState(this.category) {}
// The local InvenTreePartCategory object // The local InvenTreePartCategory object
final InvenTreePartCategory category; final InvenTreePartCategory? category;
List<InvenTreePartCategory> _subcategories = List<InvenTreePartCategory>(); List<InvenTreePartCategory> _subcategories = List<InvenTreePartCategory>.empty();
@override @override
Future<void> onBuild(BuildContext context) async { Future<void> onBuild(BuildContext context) async {
@ -133,17 +138,17 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
} }
@override @override
Future<void> request(BuildContext context) async { Future<void> request() async {
int pk = category?.pk ?? -1; int pk = category?.pk ?? -1;
// Update the category // Update the category
if (category != null) { if (category != null) {
await category.reload(context); await category!.reload();
} }
// Request a list of sub-categories under this one // Request a list of sub-categories under this one
await InvenTreePartCategory().list(context, filters: {"parent": "$pk"}).then((var cats) { await InvenTreePartCategory().list(filters: {"parent": "$pk"}).then((var cats) {
_subcategories.clear(); _subcategories.clear();
for (var cat in cats) { for (var cat in cats) {
@ -168,10 +173,10 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
List<Widget> children = [ List<Widget> children = [
ListTile( ListTile(
title: Text("${category.name}", title: Text("${category?.name}",
style: TextStyle(fontWeight: FontWeight.bold) style: TextStyle(fontWeight: FontWeight.bold)
), ),
subtitle: Text("${category.description}"), subtitle: Text("${category?.description}"),
), ),
]; ];
@ -179,14 +184,14 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
children.add( children.add(
ListTile( ListTile(
title: Text(L10().parentCategory), title: Text(L10().parentCategory),
subtitle: Text("${category.parentpathstring}"), subtitle: Text("${category?.parentpathstring}"),
leading: FaIcon(FontAwesomeIcons.levelUpAlt), leading: FaIcon(FontAwesomeIcons.levelUpAlt),
onTap: () { onTap: () {
if (category.parentId < 0) { if (category == null || ((category?.parentId ?? 0) < 0)) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null))); Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(null)));
} else { } else {
// TODO - Refactor this code into the InvenTreePart class // TODO - Refactor this code into the InvenTreePart class
InvenTreePartCategory().get(context, category.parentId).then((var cat) { InvenTreePartCategory().get(category?.parentId ?? -1).then((var cat) {
if (cat is InvenTreePartCategory) { if (cat is InvenTreePartCategory) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat)));
} }
@ -290,6 +295,8 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
return ListView( return ListView(
children: actionTiles() children: actionTiles()
); );
default:
return ListView();
} }
} }
} }
@ -306,7 +313,7 @@ class SubcategoryList extends StatelessWidget {
void _openCategory(BuildContext context, int pk) { void _openCategory(BuildContext context, int pk) {
// Attempt to load the sub-category. // Attempt to load the sub-category.
InvenTreePartCategory().get(context, pk).then((var cat) { InvenTreePartCategory().get(pk).then((var cat) {
if (cat is InvenTreePartCategory) { if (cat is InvenTreePartCategory) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat))); Navigator.push(context, MaterialPageRoute(builder: (context) => CategoryDisplayWidget(cat)));
@ -348,7 +355,7 @@ class PaginatedPartList extends StatefulWidget {
final Map<String, String> filters; final Map<String, String> filters;
Function onTotalChanged; Function(int)? onTotalChanged;
PaginatedPartList(this.filters, {this.onTotalChanged}); PaginatedPartList(this.filters, {this.onTotalChanged});
@ -363,7 +370,7 @@ class _PaginatedPartListState extends State<PaginatedPartList> {
String _searchTerm = ""; String _searchTerm = "";
Function onTotalChanged; Function(int)? onTotalChanged;
final Map<String, String> filters; final Map<String, String> filters;
@ -393,21 +400,21 @@ class _PaginatedPartListState extends State<PaginatedPartList> {
Map<String, String> params = filters; Map<String, String> params = filters;
params["search"] = _searchTerm ?? ""; params["search"] = _searchTerm;
final bool cascade = await InvenTreeSettingsManager().getValue("partSubcategory", false); final bool cascade = await InvenTreeSettingsManager().getValue("partSubcategory", false);
params["cascade"] = "${cascade}"; params["cascade"] = "${cascade}";
final page = await InvenTreePart().listPaginated(_pageSize, pageKey, filters: params); final page = await InvenTreePart().listPaginated(_pageSize, pageKey, filters: params);
int pageLength = page.length ?? 0; int pageLength = page?.length ?? 0;
int pageCount = page.count ?? 0; int pageCount = page?.count ?? 0;
final isLastPage = pageLength < _pageSize; final isLastPage = pageLength < _pageSize;
// Construct a list of part objects // Construct a list of part objects
List<InvenTreePart> parts = []; List<InvenTreePart> parts = [];
if (page == null) { if (page != null) {
for (var result in page.results) { for (var result in page.results) {
if (result is InvenTreePart) { if (result is InvenTreePart) {
parts.add(result); parts.add(result);
@ -423,7 +430,7 @@ class _PaginatedPartListState extends State<PaginatedPartList> {
} }
if (onTotalChanged != null) { if (onTotalChanged != null) {
onTotalChanged(pageCount); onTotalChanged!(pageCount);
} }
setState(() { setState(() {
@ -438,7 +445,7 @@ class _PaginatedPartListState extends State<PaginatedPartList> {
void _openPart(BuildContext context, int pk) { void _openPart(BuildContext context, int pk) {
// Attempt to load the part information // Attempt to load the part information
InvenTreePart().get(context, pk).then((var part) { InvenTreePart().get(pk).then((var part) {
if (part is InvenTreePart) { if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));

View File

@ -13,7 +13,7 @@ class CompanyDetailWidget extends StatefulWidget {
final InvenTreeCompany company; final InvenTreeCompany company;
CompanyDetailWidget(this.company, {Key key}) : super(key: key); CompanyDetailWidget(this.company, {Key? key}) : super(key: key);
@override @override
_CompanyDetailState createState() => _CompanyDetailState(company); _CompanyDetailState createState() => _CompanyDetailState(company);
@ -31,8 +31,8 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
String getAppBarTitle(BuildContext context) => L10().company; String getAppBarTitle(BuildContext context) => L10().company;
@override @override
Future<void> request(BuildContext context) async { Future<void> request() async {
await company.reload(context); await company.reload();
} }
_CompanyDetailState(this.company) { _CompanyDetailState(this.company) {
@ -42,7 +42,7 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
void _saveCompany(Map<String, String> values) async { void _saveCompany(Map<String, String> values) async {
Navigator.of(context).pop(); Navigator.of(context).pop();
var response = await company.update(context, values: values); var response = await company.update(values: values);
refresh(); refresh();
} }
@ -66,8 +66,8 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
FlatButton( FlatButton(
child: Text(L10().save), child: Text(L10().save),
onPressed: () { onPressed: () {
if (_editCompanyKey.currentState.validate()) { if (_editCompanyKey.currentState!.validate()) {
_editCompanyKey.currentState.save(); _editCompanyKey.currentState!.save();
_saveCompany({ _saveCompany({
"name": _name, "name": _name,
@ -107,7 +107,7 @@ class _CompanyDetailState extends RefreshableState<CompanyDetailWidget> {
List<Widget> _companyTiles() { List<Widget> _companyTiles() {
var tiles = List<Widget>(); var tiles = List<Widget>.empty();
bool sep = false; bool sep = false;

View File

@ -11,8 +11,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
abstract class CompanyListWidget extends StatefulWidget { abstract class CompanyListWidget extends StatefulWidget {
String title; String title = "";
Map<String, String> filters; Map<String, String> filters = {};
@override @override
_CompanyListState createState() => _CompanyListState(title, filters); _CompanyListState createState() => _CompanyListState(title, filters);
@ -39,9 +39,9 @@ class CustomerListWidget extends CompanyListWidget {
class _CompanyListState extends RefreshableState<CompanyListWidget> { class _CompanyListState extends RefreshableState<CompanyListWidget> {
var _companies = new List<InvenTreeCompany>(); var _companies = new List<InvenTreeCompany>.empty();
var _filteredCompanies = new List<InvenTreeCompany>(); var _filteredCompanies = new List<InvenTreeCompany>.empty();
String _title = "Companies"; String _title = "Companies";
@ -58,9 +58,9 @@ class _CompanyListState extends RefreshableState<CompanyListWidget> {
} }
@override @override
Future<void> request(BuildContext context) async { Future<void> request() async {
await InvenTreeCompany().list(context, filters: _filters).then((var companies) { await InvenTreeCompany().list(filters: _filters).then((var companies) {
_companies.clear(); _companies.clear();
@ -96,8 +96,10 @@ class _CompanyListState extends RefreshableState<CompanyListWidget> {
leading: InvenTreeAPI().getImage(company.image), leading: InvenTreeAPI().getImage(company.image),
onTap: () { onTap: () {
if (company.pk > 0) { if (company.pk > 0) {
InvenTreeCompany().get(context, company.pk).then((var c) { InvenTreeCompany().get(company.pk).then((var c) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyDetailWidget(c))); if (c != null && c is InvenTreeCompany) {
Navigator.push(context, MaterialPageRoute(builder: (context) => CompanyDetailWidget(c)));
}
}); });
} }
}, },

View File

@ -8,15 +8,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:InvenTree/l10.dart'; import 'package:InvenTree/l10.dart';
import 'package:one_context/one_context.dart'; import 'package:one_context/one_context.dart';
Future<void> confirmationDialog(String title, String text, {String acceptText, String rejectText, Function onAccept, Function onReject}) async { Future<void> confirmationDialog(String title, String text, {String? acceptText, String? rejectText, Function? onAccept, Function? onReject}) async {
if (acceptText == null || acceptText.isEmpty) { String _accept = acceptText ?? L10().ok;
acceptText = L10().ok; String _reject = rejectText ?? L10().cancel;
}
if (rejectText == null || rejectText.isEmpty) {
rejectText = L10().cancel;
}
OneContext().showDialog( OneContext().showDialog(
builder: (BuildContext context) { builder: (BuildContext context) {
@ -28,7 +23,7 @@ Future<void> confirmationDialog(String title, String text, {String acceptText, S
content: Text(text), content: Text(text),
actions: [ actions: [
FlatButton( FlatButton(
child: Text(rejectText), child: Text(_reject),
onPressed: () { onPressed: () {
// Close this dialog // Close this dialog
Navigator.pop(context); Navigator.pop(context);
@ -39,7 +34,7 @@ Future<void> confirmationDialog(String title, String text, {String acceptText, S
} }
), ),
FlatButton( FlatButton(
child: Text(acceptText), child: Text(_accept),
onPressed: () { onPressed: () {
// Close this dialog // Close this dialog
Navigator.pop(context); Navigator.pop(context);
@ -56,17 +51,14 @@ Future<void> confirmationDialog(String title, String text, {String acceptText, S
} }
Future<void> showInfoDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.info, String info, Function onDismissed}) async { Future<void> showInfoDialog(String title, String description, {IconData icon = FontAwesomeIcons.info, String? info, Function()? onDismissed}) async {
if (info == null || info.isEmpty) { String _info = info ?? L10().info;
info = L10().info;
}
showDialog( OneContext().showDialog(
context: context,
builder: (BuildContext context) => SimpleDialog( builder: (BuildContext context) => SimpleDialog(
title: ListTile( title: ListTile(
title: Text(info), title: Text(_info),
leading: FaIcon(icon), leading: FaIcon(icon),
), ),
children: <Widget>[ children: <Widget>[
@ -83,16 +75,14 @@ Future<void> showInfoDialog(BuildContext context, String title, String descripti
}); });
} }
Future<void> showErrorDialog(String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String error, Function onDismissed}) async { Future<void> showErrorDialog(String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String? error, Function? onDismissed}) async {
if (error == null || error.isEmpty) { String _error = error ?? L10().error;
error = L10().error;
}
OneContext().showDialog( OneContext().showDialog(
builder: (context) => SimpleDialog( builder: (context) => SimpleDialog(
title: ListTile( title: ListTile(
title: Text(error), title: Text(_error),
leading: FaIcon(icon), leading: FaIcon(icon),
), ),
children: [ children: [
@ -111,7 +101,7 @@ Future<void> showErrorDialog(String title, String description, {IconData icon =
Future<void> showServerError(String title, String description) async { Future<void> showServerError(String title, String description) async {
if (title == null || title.isEmpty) { if (title.isEmpty) {
title = L10().serverError; title = L10().serverError;
} }
@ -140,8 +130,6 @@ Future<void> showServerError(String title, String description) async {
Future<void> showStatusCodeError(int status, {int expected = 200}) async { Future<void> showStatusCodeError(int status, {int expected = 200}) async {
BuildContext ctx = OneContext().context;
String msg = L10().responseInvalid; String msg = L10().responseInvalid;
String extra = "Server responded with status code ${status}"; String extra = "Server responded with status code ${status}";
@ -174,7 +162,7 @@ Future<void> showStatusCodeError(int status, {int expected = 200}) async {
); );
} }
Future<void> showTimeoutError(BuildContext context) async { Future<void> showTimeoutError() async {
// Use OneContext as "sometimes" context is null here? // Use OneContext as "sometimes" context is null here?
var ctx = OneContext().context; var ctx = OneContext().context;
@ -182,42 +170,49 @@ Future<void> showTimeoutError(BuildContext context) async {
await showServerError(L10().timeout, L10().noResponse); await showServerError(L10().timeout, L10().noResponse);
} }
void showFormDialog(String title, {String acceptText, String cancelText, GlobalKey<FormState> key, List<Widget> fields, List<Widget> actions, Function callback}) { void showFormDialog(String title, {String? acceptText, String? cancelText, GlobalKey<FormState>? key, List<Widget>? fields, List<Widget>? actions, Function? callback}) {
BuildContext dialogContext; BuildContext? dialogContext;
var ctx = OneContext().context; var ctx = OneContext().context;
if (acceptText == null) { String _accept = acceptText ?? L10().save;
acceptText = L10().save; String _cancel = cancelText ?? L10().cancel;
}
if (cancelText == null) {
cancelText = L10().cancel;
}
// Undefined actions = OK + Cancel // Undefined actions = OK + Cancel
if (actions == null) { if (actions == null) {
actions = <Widget>[ actions = <Widget>[
FlatButton( FlatButton(
child: Text(cancelText), child: Text(_cancel),
onPressed: () { onPressed: () {
// Close the form // Close the form
Navigator.pop(dialogContext); var _ctx = dialogContext;
if (_ctx != null) {
Navigator.pop(_ctx);
}
} }
), ),
FlatButton( FlatButton(
child: Text(acceptText), child: Text(_accept),
onPressed: () { onPressed: () {
if (key.currentState.validate()) {
key.currentState.save();
// Close the dialog var _key = key;
Navigator.pop(dialogContext);
// Callback if (_key != null && _key.currentState != null) {
if (callback != null) { if (_key.currentState!.validate()) {
callback(); _key.currentState!.save();
// Close the dialog
var _ctx = dialogContext;
if (_ctx != null) {
Navigator.pop(_ctx);
}
// Callback
if (callback != null) {
callback();
}
} }
} }
} }
@ -225,6 +220,8 @@ void showFormDialog(String title, {String acceptText, String cancelText, GlobalK
]; ];
} }
var _fields = fields ?? List<Widget>.empty();
OneContext().showDialog( OneContext().showDialog(
builder: (BuildContext context) { builder: (BuildContext context) {
dialogContext = context; dialogContext = context;
@ -238,7 +235,7 @@ void showFormDialog(String title, {String acceptText, String cancelText, GlobalK
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: fields children: _fields
) )
) )
) )

View File

@ -52,10 +52,10 @@ class ImagePickerField extends FormField<File> {
} }
ImagePickerField(BuildContext context, {String label = "Attach Image", Function onSaved, bool required = false}) : ImagePickerField(BuildContext context, {String? label, Function(File?)? onSaved, bool required = false}) :
super( super(
onSaved: onSaved, onSaved: onSaved,
validator: (File img) { validator: (File? img) {
if (required && (img == null)) { if (required && (img == null)) {
return L10().required; return L10().required;
} }
@ -63,10 +63,13 @@ class ImagePickerField extends FormField<File> {
return null; return null;
}, },
builder: (FormFieldState<File> state) { builder: (FormFieldState<File> state) {
String _label = label ?? L10().attachImage;
return InputDecorator( return InputDecorator(
decoration: InputDecoration( decoration: InputDecoration(
errorText: state.errorText, errorText: state.errorText,
labelText: required ? label + "*" : label, labelText: required ? _label + "*" : _label,
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
@ -92,7 +95,7 @@ class ImagePickerField extends FormField<File> {
class CheckBoxField extends FormField<bool> { class CheckBoxField extends FormField<bool> {
CheckBoxField({String label, String hint, bool initial = false, Function onSaved}) : CheckBoxField({String? label, String? hint, bool initial = false, Function(bool?)? onSaved}) :
super( super(
onSaved: onSaved, onSaved: onSaved,
initialValue: initial, initialValue: initial,
@ -111,7 +114,7 @@ class CheckBoxField extends FormField<bool> {
class StringField extends TextFormField { class StringField extends TextFormField {
StringField({String label, String hint, String initial, Function onSaved, Function validator, bool allowEmpty = false, bool isEnabled = true}) : StringField({String label = "", String? hint, String? initial, Function(String?)? onSaved, Function? validator, bool allowEmpty = false, bool isEnabled = true}) :
super( super(
decoration: InputDecoration( decoration: InputDecoration(
labelText: allowEmpty ? label : label + "*", labelText: allowEmpty ? label : label + "*",
@ -121,7 +124,7 @@ class StringField extends TextFormField {
onSaved: onSaved, onSaved: onSaved,
enabled: isEnabled, enabled: isEnabled,
validator: (value) { validator: (value) {
if (!allowEmpty && value.isEmpty) { if (!allowEmpty && value != null && value.isEmpty) {
return L10().valueCannotBeEmpty; return L10().valueCannotBeEmpty;
} }
@ -140,7 +143,7 @@ class StringField extends TextFormField {
*/ */
class QuantityField extends TextFormField { class QuantityField extends TextFormField {
QuantityField({String label = "", String hint = "", String initial = "", double max = null, TextEditingController controller}) : QuantityField({String label = "", String hint = "", String initial = "", double? max, TextEditingController? controller}) :
super( super(
decoration: InputDecoration( decoration: InputDecoration(
labelText: label, labelText: label,
@ -150,9 +153,9 @@ class QuantityField extends TextFormField {
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true),
validator: (value) { validator: (value) {
if (value.isEmpty) return L10().quantityEmpty; if (value != null && value.isEmpty) return L10().quantityEmpty;
double quantity = double.tryParse(value); double quantity = double.tryParse(value ?? '0') ?? 0;
if (quantity == null) return L10().quantityInvalid; if (quantity == null) return L10().quantityInvalid;
if (quantity <= 0) return L10().quantityPositive; if (quantity <= 0) return L10().quantityPositive;

View File

@ -20,7 +20,7 @@ import 'package:InvenTree/widget/spinner.dart';
import 'package:InvenTree/widget/drawer.dart'; import 'package:InvenTree/widget/drawer.dart';
class InvenTreeHomePage extends StatefulWidget { class InvenTreeHomePage extends StatefulWidget {
InvenTreeHomePage({Key key}) : super(key: key); InvenTreeHomePage({Key? key}) : super(key: key);
@override @override
_InvenTreeHomePageState createState() => _InvenTreeHomePageState(); _InvenTreeHomePageState createState() => _InvenTreeHomePageState();
@ -37,9 +37,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
} }
// Selected user profile // Selected user profile
UserProfile _profile; UserProfile? _profile;
BuildContext _context;
void _searchParts() { void _searchParts() {
if (!InvenTreeAPI().checkConnection(context)) return; if (!InvenTreeAPI().checkConnection(context)) return;
@ -113,7 +111,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) { if (!InvenTreeAPI().isConnected() && !InvenTreeAPI().isConnecting()) {
// Attempt server connection // Attempt server connection
InvenTreeAPI().connectToServer(_homeKey.currentContext).then((result) { InvenTreeAPI().connectToServer().then((result) {
setState(() {}); setState(() {});
}); });
} }
@ -171,7 +169,7 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
} else { } else {
return ListTile( return ListTile(
title: Text(L10().serverCouldNotConnect), title: Text(L10().serverCouldNotConnect),
subtitle: Text("${_profile.server}"), subtitle: Text("${_profile!.server}"),
leading: FaIcon(FontAwesomeIcons.server), leading: FaIcon(FontAwesomeIcons.server),
trailing: FaIcon( trailing: FaIcon(
FontAwesomeIcons.timesCircle, FontAwesomeIcons.timesCircle,
@ -187,8 +185,6 @@ class _InvenTreeHomePageState extends State<InvenTreeHomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_context = context;
// This method is rerun every time setState is called, for instance as done // This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above. // by the _incrementCounter method above.
// //

View File

@ -20,11 +20,11 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class LocationDisplayWidget extends StatefulWidget { class LocationDisplayWidget extends StatefulWidget {
LocationDisplayWidget(this.location, {Key key}) : super(key: key); LocationDisplayWidget(this.location, {Key? key}) : super(key: key);
final InvenTreeStockLocation location; final InvenTreeStockLocation? location;
final String title = "Location"; final String title = L10().stockLocation;
@override @override
_LocationDisplayState createState() => _LocationDisplayState(location); _LocationDisplayState createState() => _LocationDisplayState(location);
@ -32,12 +32,12 @@ class LocationDisplayWidget extends StatefulWidget {
class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> { class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
final InvenTreeStockLocation location; final InvenTreeStockLocation? location;
final _editLocationKey = GlobalKey<FormState>(); final _editLocationKey = GlobalKey<FormState>();
@override @override
String getAppBarTitle(BuildContext context) { return "Stock Location"; } String getAppBarTitle(BuildContext context) { return L10().stockLocation; }
@override @override
List<Widget> getAppBarActions(BuildContext context) { List<Widget> getAppBarActions(BuildContext context) {
@ -80,12 +80,16 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
void _editLocation(Map<String, String> values) async { void _editLocation(Map<String, String> values) async {
final bool result = await location.update(context, values: values); bool result = false;
showSnackIcon( if (location != null) {
result ? "Location edited" : "Location editing failed", result = await location!.update(values: values);
success: result
); showSnackIcon(
result ? "Location edited" : "Location editing failed",
success: result
);
}
refresh(); refresh();
} }
@ -95,6 +99,10 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
var _name; var _name;
var _description; var _description;
if (location == null) {
return;
}
showFormDialog(L10().editLocation, showFormDialog(L10().editLocation,
key: _editLocationKey, key: _editLocationKey,
callback: () { callback: () {
@ -106,12 +114,12 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
fields: <Widget> [ fields: <Widget> [
StringField( StringField(
label: L10().name, label: L10().name,
initial: location.name, initial: location?.name ?? '',
onSaved: (value) => _name = value, onSaved: (value) => _name = value,
), ),
StringField( StringField(
label: L10().description, label: L10().description,
initial: location.description, initial: location?.description ?? '',
onSaved: (value) => _description = value, onSaved: (value) => _description = value,
) )
] ]
@ -120,7 +128,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
_LocationDisplayState(this.location) {} _LocationDisplayState(this.location) {}
List<InvenTreeStockLocation> _sublocations = List<InvenTreeStockLocation>(); List<InvenTreeStockLocation> _sublocations = List<InvenTreeStockLocation>.empty();
String _locationFilter = ''; String _locationFilter = '';
@ -139,17 +147,17 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
} }
@override @override
Future<void> request(BuildContext context) async { Future<void> request() async {
int pk = location?.pk ?? -1; int pk = location?.pk ?? -1;
// Reload location information // Reload location information
if (location != null) { if (location != null) {
await location.reload(context); await location?.reload();
} }
// Request a list of sub-locations under this one // Request a list of sub-locations under this one
await InvenTreeStockLocation().list(context, filters: {"parent": "$pk"}).then((var locs) { await InvenTreeStockLocation().list(filters: {"parent": "$pk"}).then((var locs) {
_sublocations.clear(); _sublocations.clear();
for (var loc in locs) { for (var loc in locs) {
@ -173,8 +181,8 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
List<Widget> children = [ List<Widget> children = [
ListTile( ListTile(
title: Text("${location.name}"), title: Text("${location!.name}"),
subtitle: Text("${location.description}"), subtitle: Text("${location!.description}"),
), ),
]; ];
@ -182,13 +190,17 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
children.add( children.add(
ListTile( ListTile(
title: Text(L10().parentCategory), title: Text(L10().parentCategory),
subtitle: Text("${location.parentpathstring}"), subtitle: Text("${location!.parentpathstring}"),
leading: FaIcon(FontAwesomeIcons.levelUpAlt), leading: FaIcon(FontAwesomeIcons.levelUpAlt),
onTap: () { onTap: () {
if (location.parentId < 0) {
int parent = location?.parentId ?? -1;
if (parent < 0) {
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null))); Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(null)));
} else { } else {
InvenTreeStockLocation().get(context, location.parentId).then((var loc) {
InvenTreeStockLocation().get(parent).then((var loc) {
if (loc is InvenTreeStockLocation) { if (loc is InvenTreeStockLocation) {
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
} }
@ -238,7 +250,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
Map<String, String> filters = {}; Map<String, String> filters = {};
if (location != null) { if (location != null) {
filters["location"] = "${location.pk}"; filters["location"] = "${location!.pk}";
} }
switch (index) { switch (index) {
@ -256,7 +268,7 @@ class _LocationDisplayState extends RefreshableState<LocationDisplayWidget> {
).toList() ).toList()
); );
default: default:
return null; return ListView();
} }
} }
@ -308,14 +320,19 @@ List<Widget> detailTiles() {
leading: FaIcon(FontAwesomeIcons.exchangeAlt), leading: FaIcon(FontAwesomeIcons.exchangeAlt),
trailing: FaIcon(FontAwesomeIcons.qrcode), trailing: FaIcon(FontAwesomeIcons.qrcode),
onTap: () { onTap: () {
Navigator.push(
context, var _loc = location;
MaterialPageRoute(builder: (context) =>
InvenTreeQRView( if (_loc != null) {
StockLocationScanInItemsHandler(location))) Navigator.push(
).then((context) { context,
refresh(); MaterialPageRoute(builder: (context) =>
}); InvenTreeQRView(
StockLocationScanInItemsHandler(_loc)))
).then((context) {
refresh();
});
}
}, },
) )
); );
@ -361,7 +378,7 @@ class SublocationList extends StatelessWidget {
void _openLocation(BuildContext context, int pk) { void _openLocation(BuildContext context, int pk) {
InvenTreeStockLocation().get(context, pk).then((var loc) { InvenTreeStockLocation().get(pk).then((var loc) {
if (loc is InvenTreeStockLocation) { if (loc is InvenTreeStockLocation) {
Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc))); Navigator.push(context, MaterialPageRoute(builder: (context) => LocationDisplayWidget(loc)));
@ -452,26 +469,32 @@ class _PaginatedStockListState extends State<PaginatedStockList> {
params["cascade"] = "${cascade}"; params["cascade"] = "${cascade}";
final page = await InvenTreeStockItem().listPaginated(_pageSize, pageKey, filters: params); final page = await InvenTreeStockItem().listPaginated(_pageSize, pageKey, filters: params);
final isLastPage = page.length < _pageSize;
int pageLength = page?.length ?? 0;
int pageCount = page?.count ?? 0;
final isLastPage = pageLength < _pageSize;
// Construct a list of stock item objects // Construct a list of stock item objects
List<InvenTreeStockItem> items = []; List<InvenTreeStockItem> items = [];
for (var result in page.results) { if (page != null) {
if (result is InvenTreeStockItem) { for (var result in page.results) {
items.add(result); if (result is InvenTreeStockItem) {
items.add(result);
}
} }
} }
if (isLastPage) { if (isLastPage) {
_pagingController.appendLastPage(items); _pagingController.appendLastPage(items);
} else { } else {
final int nextPageKey = pageKey + page.length; final int nextPageKey = pageKey + pageLength;
_pagingController.appendPage(items, nextPageKey); _pagingController.appendPage(items, nextPageKey);
} }
setState(() { setState(() {
resultCount = page.count; resultCount = pageCount;
}); });
} catch (error) { } catch (error) {
@ -480,7 +503,7 @@ class _PaginatedStockListState extends State<PaginatedStockList> {
} }
void _openItem(BuildContext context, int pk) { void _openItem(BuildContext context, int pk) {
InvenTreeStockItem().get(context, pk).then((var item) { InvenTreeStockItem().get(pk).then((var item) {
if (item is InvenTreeStockItem) { if (item is InvenTreeStockItem) {
Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item))); Navigator.push(context, MaterialPageRoute(builder: (context) => StockDetailWidget(item)));
} }

View File

@ -22,7 +22,7 @@ import 'location_display.dart';
class PartDetailWidget extends StatefulWidget { class PartDetailWidget extends StatefulWidget {
PartDetailWidget(this.part, {Key key}) : super(key: key); PartDetailWidget(this.part, {Key? key}) : super(key: key);
final InvenTreePart part; final InvenTreePart part;
@ -87,22 +87,22 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
} }
@override @override
Future<void> request(BuildContext context) async { Future<void> request() async {
await part.reload(context); await part.reload();
await part.getTestTemplates(context); await part.getTestTemplates();
} }
void _toggleStar() async { void _toggleStar() async {
if (InvenTreeAPI().checkPermission('part', 'view')) { if (InvenTreeAPI().checkPermission('part', 'view')) {
await part.update(context, values: {"starred": "${!part.starred}"}); await part.update(values: {"starred": "${!part.starred}"});
refresh(); refresh();
} }
} }
void _savePart(Map<String, String> values) async { void _savePart(Map<String, String> values) async {
final bool result = await part.update(context, values: values); final bool result = await part.update(values: values);
if (result) { if (result) {
showSnackIcon(L10().partEdited, success: true); showSnackIcon(L10().partEdited, success: true);
@ -121,7 +121,11 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
* Upload image for this Part. * Upload image for this Part.
* Show a SnackBar with upload result. * Show a SnackBar with upload result.
*/ */
void _uploadImage(File image) async { void _uploadImage(File? image) async {
if (image == null) {
return;
}
final result = await part.uploadImage(image); final result = await part.uploadImage(image);
@ -143,7 +147,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
void _selectImage() { void _selectImage() {
File _attachment; File? _attachment;
if (!InvenTreeAPI().checkPermission('part', 'change')) { if (!InvenTreeAPI().checkPermission('part', 'change')) {
return; return;
@ -261,7 +265,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
} }
// Category information // Category information
if (part.categoryName != null && part.categoryName.isNotEmpty) { if (part.categoryName.isNotEmpty) {
tiles.add( tiles.add(
ListTile( ListTile(
title: Text(L10().partCategory), title: Text(L10().partCategory),
@ -269,9 +273,12 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
leading: FaIcon(FontAwesomeIcons.sitemap), leading: FaIcon(FontAwesomeIcons.sitemap),
onTap: () { onTap: () {
if (part.categoryId > 0) { if (part.categoryId > 0) {
InvenTreePartCategory().get(context, part.categoryId).then((var cat) { InvenTreePartCategory().get(part.categoryId).then((var cat) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => CategoryDisplayWidget(cat))); if (cat is InvenTreePartCategory) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => CategoryDisplayWidget(cat)));
}
}); });
} }
}, },
@ -499,7 +506,7 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
) )
); );
default: default:
return null; return Center();
} }
} }

View File

@ -9,7 +9,7 @@ class PartNotesWidget extends StatefulWidget {
final InvenTreePart part; final InvenTreePart part;
PartNotesWidget(this.part, {Key key}) : super(key: key); PartNotesWidget(this.part, {Key? key}) : super(key: key);
@override @override
_PartNotesState createState() => _PartNotesState(part); _PartNotesState createState() => _PartNotesState(part);

View File

@ -11,7 +11,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
final refreshableKey = GlobalKey<ScaffoldState>(); final refreshableKey = GlobalKey<ScaffoldState>();
// Storage for context once "Build" is called // Storage for context once "Build" is called
BuildContext context; BuildContext? _context;
// Current tab index (used for widgets which display bottom tabs) // Current tab index (used for widgets which display bottom tabs)
int tabIndex = 0; int tabIndex = 0;
@ -36,7 +36,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => onBuild(context)); WidgetsBinding.instance?.addPostFrameCallback((_) => onBuild(_context!));
} }
// Function called after the widget is first build // Function called after the widget is first build
@ -45,7 +45,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
} }
// Function to request data for this page // Function to request data for this page
Future<void> request(BuildContext context) async { Future<void> request() async {
return; return;
} }
@ -55,7 +55,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
loading = true; loading = true;
}); });
await request(context); await request();
setState(() { setState(() {
loading = false; loading = false;
@ -77,14 +77,16 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
// Function to construct a body (MUST BE PROVIDED) // Function to construct a body (MUST BE PROVIDED)
Widget getBody(BuildContext context) { Widget getBody(BuildContext context) {
// Default return is an empty ListView
return ListView();
}
Widget? getBottomNavBar(BuildContext context) {
return null; return null;
} }
Widget getBottomNavBar(BuildContext context) { Widget? getFab(BuildContext context) {
return null;
}
Widget getFab(BuildContext context) {
return null; return null;
} }
@ -92,7 +94,7 @@ abstract class RefreshableState<T extends StatefulWidget> extends State<T> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Save the context for future use // Save the context for future use
this.context = context; _context = context;
return Scaffold( return Scaffold(
key: refreshableKey, key: refreshableKey,

View File

@ -15,25 +15,21 @@ import '../api.dart';
// TODO - Refactor duplicate code in this file! // TODO - Refactor duplicate code in this file!
class PartSearchDelegate extends SearchDelegate<InvenTreePart> { class PartSearchDelegate extends SearchDelegate<InvenTreePart?> {
final partSearchKey = GlobalKey<ScaffoldState>(); final partSearchKey = GlobalKey<ScaffoldState>();
BuildContext context; BuildContext context;
// What did we search for last time? // What did we search for last time?
String _cachedQuery; String _cachedQuery = "";
bool _searching = false; bool _searching = false;
// Custom filters for the part search // Custom filters for the part search
Map<String, String> filters = {}; Map<String, String> filters = {};
PartSearchDelegate(this.context, {this.filters}) { PartSearchDelegate(this.context, {this.filters = const {}});
if (filters == null) {
filters = {};
}
}
@override @override
String get searchFieldLabel => L10().searchParts; String get searchFieldLabel => L10().searchParts;
@ -71,7 +67,7 @@ class PartSearchDelegate extends SearchDelegate<InvenTreePart> {
for (int idx = 0; idx < results.length; idx++) { for (int idx = 0; idx < results.length; idx++) {
if (results[idx] is InvenTreePart) { if (results[idx] is InvenTreePart) {
partResults.add(results[idx]); partResults.add(results[idx] as InvenTreePart);
} }
} }
@ -132,7 +128,7 @@ class PartSearchDelegate extends SearchDelegate<InvenTreePart> {
), ),
trailing: Text(part.inStockString), trailing: Text(part.inStockString),
onTap: () { onTap: () {
InvenTreePart().get(context, part.pk).then((var prt) { InvenTreePart().get(part.pk).then((var prt) {
if (prt is InvenTreePart) { if (prt is InvenTreePart) {
Navigator.push( Navigator.push(
context, context,
@ -201,18 +197,18 @@ class PartSearchDelegate extends SearchDelegate<InvenTreePart> {
} }
class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem> { class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem?> {
final stockSearchKey = GlobalKey<ScaffoldState>(); final stockSearchKey = GlobalKey<ScaffoldState>();
final BuildContext context; final BuildContext context;
String _cachedQuery; String _cachedQuery = "";
bool _searching = false; bool _searching = false;
// Custom filters for the stock item search // Custom filters for the stock item search
Map<String, String> filters; Map<String, String>? filters;
StockSearchDelegate(this.context, {this.filters}) { StockSearchDelegate(this.context, {this.filters}) {
if (filters == null) { if (filters == null) {
@ -247,7 +243,9 @@ class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem> {
showResults(context); showResults(context);
// Enable cascading part search by default // Enable cascading part search by default
filters["cascade"] = "true"; if (filters != null) {
filters?["cascade"] = "true";
}
final results = await InvenTreeStockItem().search( final results = await InvenTreeStockItem().search(
context, query, filters: filters); context, query, filters: filters);
@ -256,7 +254,7 @@ class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem> {
for (int idx = 0; idx < results.length; idx++) { for (int idx = 0; idx < results.length; idx++) {
if (results[idx] is InvenTreeStockItem) { if (results[idx] is InvenTreeStockItem) {
itemResults.add(results[idx]); itemResults.add(results[idx] as InvenTreeStockItem);
} }
} }
@ -315,7 +313,7 @@ class StockSearchDelegate extends SearchDelegate<InvenTreeStockItem> {
), ),
trailing: Text(item.serialOrQuantityDisplay()), trailing: Text(item.serialOrQuantityDisplay()),
onTap: () { onTap: () {
InvenTreeStockItem().get(context, item.pk).then((var it) { InvenTreeStockItem().get(item.pk).then((var it) {
if (it is InvenTreeStockItem) { if (it is InvenTreeStockItem) {
Navigator.push( Navigator.push(
context, context,

View File

@ -15,14 +15,14 @@ import 'package:one_context/one_context.dart';
import 'package:InvenTree/l10.dart'; import 'package:InvenTree/l10.dart';
void showSnackIcon(String text, {IconData icon, Function onAction, bool success, String actionText}) { void showSnackIcon(String text, {IconData? icon, Function()? onAction, bool? success, String? actionText}) {
OneContext().hideCurrentSnackBar(); OneContext().hideCurrentSnackBar();
Color backgroundColor; Color backgroundColor = Colors.deepOrange;
// Make some selections based on the "success" value // Make some selections based on the "success" value
if (success == true) { if (success != null && success == true) {
backgroundColor = Colors.lightGreen; backgroundColor = Colors.lightGreen;
// Select an icon if we do not have an action // Select an icon if we do not have an action
@ -30,26 +30,21 @@ void showSnackIcon(String text, {IconData icon, Function onAction, bool success,
icon = FontAwesomeIcons.checkCircle; icon = FontAwesomeIcons.checkCircle;
} }
} else if (success == false) { } else if (success != null && success == false) {
backgroundColor = Colors.deepOrange; backgroundColor = Colors.deepOrange;
if (icon == null && onAction == null) { if (icon == null && onAction == null) {
icon = FontAwesomeIcons.exclamationCircle; icon = FontAwesomeIcons.exclamationCircle;
} }
} }
SnackBarAction action; String _action = actionText ?? L10().details;
SnackBarAction? action;
if (onAction != null) { if (onAction != null) {
if (actionText == null) {
// Default action text
actionText = L10().details;
}
action = SnackBarAction( action = SnackBarAction(
label: actionText, label: _action,
onPressed: onAction, onPressed: onAction,
); );
} }

View File

@ -4,13 +4,13 @@ import 'package:flutter/cupertino.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class Spinner extends StatefulWidget { class Spinner extends StatefulWidget {
final IconData icon; final IconData? icon;
final Duration duration; final Duration duration;
final Color color; final Color color;
const Spinner({ const Spinner({
this.color = const Color.fromRGBO(150, 150, 150, 1), this.color = const Color.fromRGBO(150, 150, 150, 1),
Key key, Key? key,
@required this.icon, @required this.icon,
this.duration = const Duration(milliseconds: 1800), this.duration = const Duration(milliseconds: 1800),
}) : super(key: key); }) : super(key: key);
@ -20,8 +20,8 @@ class Spinner extends StatefulWidget {
} }
class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin { class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
AnimationController _controller; AnimationController? _controller;
Widget _child; Widget? _child;
@override @override
void initState() { void initState() {
@ -40,14 +40,14 @@ class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller!.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RotationTransition( return RotationTransition(
turns: _controller, turns: _controller!,
child: _child, child: _child,
); );
} }

View File

@ -14,7 +14,7 @@ import '../api.dart';
class StarredPartWidget extends StatefulWidget { class StarredPartWidget extends StatefulWidget {
StarredPartWidget({Key key}) : super(key: key); StarredPartWidget({Key? key}) : super(key: key);
@override @override
_StarredPartState createState() => _StarredPartState(); _StarredPartState createState() => _StarredPartState();
@ -29,15 +29,17 @@ class _StarredPartState extends RefreshableState<StarredPartWidget> {
String getAppBarTitle(BuildContext context) => L10().partsStarred; String getAppBarTitle(BuildContext context) => L10().partsStarred;
@override @override
Future<void> request(BuildContext context) async { Future<void> request() async {
final parts = await InvenTreePart().list(context, filters: {"starred": "true"}); final parts = await InvenTreePart().list(filters: {"starred": "true"});
starredParts.clear(); starredParts.clear();
for (int idx = 0; idx < parts.length; idx++) { if (parts != null) {
if (parts[idx] is InvenTreePart) { for (int idx = 0; idx < parts.length; idx++) {
starredParts.add(parts[idx]); if (parts[idx] is InvenTreePart) {
starredParts.add(parts[idx] as InvenTreePart);
}
} }
} }
} }
@ -54,7 +56,7 @@ class _StarredPartState extends RefreshableState<StarredPartWidget> {
height: 40 height: 40
), ),
onTap: () { onTap: () {
InvenTreePart().get(context, part.pk).then((var prt) { InvenTreePart().get(part.pk).then((var prt) {
if (prt is InvenTreePart) { if (prt is InvenTreePart) {
Navigator.push( Navigator.push(
context, context,

View File

@ -25,7 +25,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class StockDetailWidget extends StatefulWidget { class StockDetailWidget extends StatefulWidget {
StockDetailWidget(this.item, {Key key}) : super(key: key); StockDetailWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeStockItem item; final InvenTreeStockItem item;
@ -77,7 +77,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
final InvenTreeStockItem item; final InvenTreeStockItem item;
// Part object // Part object
InvenTreePart part; InvenTreePart? part;
@override @override
Future<void> onBuild(BuildContext context) async { Future<void> onBuild(BuildContext context) async {
@ -89,14 +89,14 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
} }
@override @override
Future<void> request(BuildContext context) async { Future<void> request() async {
await item.reload(context); await item.reload();
// Request part information // Request part information
part = await InvenTreePart().get(context, item.partId); part = await InvenTreePart().get(item.partId) as InvenTreePart;
// Request test results... // Request test results...
await item.getTestResults(context); await item.getTestResults();
} }
void _addStock() async { void _addStock() async {
@ -227,7 +227,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
void _unassignBarcode(BuildContext context) async { void _unassignBarcode(BuildContext context) async {
final bool result = await item.update(context, values: {'uid': ''}); final bool result = await item.update(values: {'uid': ''});
if (result) { if (result) {
showSnackIcon( showSnackIcon(
@ -245,7 +245,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
} }
void _transferStock(BuildContext context, InvenTreeStockLocation location) async { void _transferStock(InvenTreeStockLocation location) async {
double quantity = double.tryParse(_quantityController.text) ?? item.quantity; double quantity = double.tryParse(_quantityController.text) ?? item.quantity;
String notes = _notesController.text; String notes = _notesController.text;
@ -264,17 +264,21 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
void _transferStockDialog() async { void _transferStockDialog() async {
var locations = await InvenTreeStockLocation().list(context); var locations = await InvenTreeStockLocation().list();
final _selectedController = TextEditingController(); final _selectedController = TextEditingController();
InvenTreeStockLocation selectedLocation; InvenTreeStockLocation? selectedLocation;
_quantityController.text = "${item.quantityString}"; _quantityController.text = "${item.quantityString}";
showFormDialog(L10().transferStock, showFormDialog(L10().transferStock,
key: _moveStockKey, key: _moveStockKey,
callback: () { callback: () {
_transferStock(context, selectedLocation); var _loc = selectedLocation;
if (_loc != null) {
_transferStock(_loc);
}
}, },
fields: <Widget>[ fields: <Widget>[
QuantityField( QuantityField(
@ -292,7 +296,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
) )
), ),
suggestionsCallback: (pattern) async { suggestionsCallback: (pattern) async {
var suggestions = List<InvenTreeStockLocation>(); var suggestions = List<InvenTreeStockLocation>.empty();
for (var loc in locations) { for (var loc in locations) {
if (loc.matchAgainstString(pattern)) { if (loc.matchAgainstString(pattern)) {
@ -311,7 +315,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
}, },
onSuggestionSelected: (suggestion) { onSuggestionSelected: (suggestion) {
selectedLocation = suggestion as InvenTreeStockLocation; selectedLocation = suggestion as InvenTreeStockLocation;
_selectedController.text = selectedLocation.pathstring; _selectedController.text = selectedLocation!.pathstring;
}, },
onSaved: (value) { onSaved: (value) {
}, },
@ -342,7 +346,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
), ),
onTap: () { onTap: () {
if (item.partId > 0) { if (item.partId > 0) {
InvenTreePart().get(context, item.partId).then((var part) { InvenTreePart().get(item.partId).then((var part) {
if (part is InvenTreePart) { if (part is InvenTreePart) {
Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part))); Navigator.push(context, MaterialPageRoute(builder: (context) => PartDetailWidget(part)));
} }
@ -397,9 +401,12 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
leading: FaIcon(FontAwesomeIcons.mapMarkerAlt), leading: FaIcon(FontAwesomeIcons.mapMarkerAlt),
onTap: () { onTap: () {
if (item.locationId > 0) { if (item.locationId > 0) {
InvenTreeStockLocation().get(context, item.locationId).then((var loc) { InvenTreeStockLocation().get(item.locationId).then((var loc) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LocationDisplayWidget(loc))); if (loc is InvenTreeStockLocation) {
Navigator.push(context, MaterialPageRoute(
builder: (context) => LocationDisplayWidget(loc)));
}
}); });
} }
}, },
@ -442,7 +449,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
); );
} }
if ((item.testResultCount > 0) || (part != null && part.isTrackable)) { if ((item.testResultCount > 0) || (part?.isTrackable ?? false)) {
tiles.add( tiles.add(
ListTile( ListTile(
title: Text(L10().testResults), title: Text(L10().testResults),
@ -641,7 +648,7 @@ class _StockItemDisplayState extends RefreshableState<StockDetailWidget> {
).toList() ).toList()
); );
default: default:
return null; return ListView();
} }
} }

View File

@ -19,7 +19,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class StockItemTestResultsWidget extends StatefulWidget { class StockItemTestResultsWidget extends StatefulWidget {
StockItemTestResultsWidget(this.item, {Key key}) : super(key: key); StockItemTestResultsWidget(this.item, {Key? key}) : super(key: key);
final InvenTreeStockItem item; final InvenTreeStockItem item;
@ -36,16 +36,16 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
String getAppBarTitle(BuildContext context) => L10().testResults; String getAppBarTitle(BuildContext context) => L10().testResults;
@override @override
Future<void> request(BuildContext context) async { Future<void> request() async {
await item.getTestTemplates(context); await item.getTestTemplates();
await item.getTestResults(context); await item.getTestResults();
} }
final InvenTreeStockItem item; final InvenTreeStockItem item;
_StockItemTestResultDisplayState(this.item); _StockItemTestResultDisplayState(this.item);
void uploadTestResult(String name, bool result, String value, String notes, File attachment) async { void uploadTestResult(String name, bool result, String value, String notes, File? attachment) async {
final success = await item.uploadTestResult( final success = await item.uploadTestResult(
context, name, result, context, name, result,
@ -64,11 +64,11 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
void addTestResult({String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async { void addTestResult({String name = '', bool nameIsEditable = true, bool result = false, String value = '', bool valueRequired = false, bool attachmentRequired = false}) async {
String _name; String _name = "";
bool _result; bool _result = false;
String _value; String _value = "";
String _notes; String _notes = "";
File _attachment; File? _attachment;
showFormDialog(L10().testResultAdd, showFormDialog(L10().testResultAdd,
key: _addResultKey, key: _addResultKey,
@ -80,21 +80,21 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
label: L10().testName, label: L10().testName,
initial: name, initial: name,
isEnabled: nameIsEditable, isEnabled: nameIsEditable,
onSaved: (value) => _name = value, onSaved: (value) => _name = value ?? '',
), ),
CheckBoxField( CheckBoxField(
label: L10().result, label: L10().result,
hint: L10().testPassedOrFailed, hint: L10().testPassedOrFailed,
initial: true, initial: true,
onSaved: (value) => _result = value, onSaved: (value) => _result = value ?? false,
), ),
StringField( StringField(
label: L10().value, label: L10().value,
initial: value, initial: value,
allowEmpty: true, allowEmpty: true,
onSaved: (value) => _value = value, onSaved: (value) => _value = value ?? '',
validator: (String value) { validator: (String value) {
if (valueRequired && (value == null || value.isEmpty)) { if (valueRequired && value.isEmpty) {
return L10().valueRequired; return L10().valueRequired;
} }
return null; return null;
@ -109,7 +109,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
StringField( StringField(
allowEmpty: true, allowEmpty: true,
label: L10().notes, label: L10().notes,
onSaved: (value) => _notes = value, onSaved: (value) => _notes = value ?? '',
), ),
] ]
); );
@ -202,10 +202,11 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
for (var item in results) { for (var item in results) {
bool _required = false; bool _required = false;
String _test; String _test = "";
bool _result = null; bool _result = false;
String _value; String _value = "";
String _notes; String _notes = "";
FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: Color.fromRGBO(0, 0, 250, 1)); FaIcon _icon = FaIcon(FontAwesomeIcons.questionCircle, color: Color.fromRGBO(0, 0, 250, 1));
bool _valueRequired = false; bool _valueRequired = false;
bool _attachmentRequired = false; bool _attachmentRequired = false;

View File

@ -10,7 +10,7 @@ class StockNotesWidget extends StatefulWidget {
final InvenTreeStockItem item; final InvenTreeStockItem item;
StockNotesWidget(this.item, {Key key}) : super(key: key); StockNotesWidget(this.item, {Key? key}) : super(key: key);
@override @override
_StockNotesState createState() => _StockNotesState(item); _StockNotesState createState() => _StockNotesState(item);

View File

@ -23,23 +23,23 @@ dependencies:
cupertino_icons: ^0.1.3 cupertino_icons: ^0.1.3
http: ^0.13.0 http: ^0.13.0
cached_network_image: ^2.5.0 cached_network_image: ^2.5.0 # Download and cache remote images
qr_code_scanner: ^0.3.5 # Barcode scanning qr_code_scanner: ^0.3.5 # Barcode scanning
package_info: ^2.0.0 # App information introspection package_info: ^2.0.0 # App information introspection
device_info: ^2.0.0 # Information about the device device_info: ^2.0.0 # Information about the device
font_awesome_flutter: ^8.8.1 # FontAwesome icon set font_awesome_flutter: ^8.8.1 # FontAwesome icon set
flutter_speed_dial: ^1.2.5 # FAB menu elements flutter_speed_dial: ^1.2.5 # FAB menu elements
sentry_flutter: 5.0.0 # Error reporting sentry_flutter: 5.0.0 # Error reporting
flutter_typeahead: ^1.8.1 # Auto-complete input field flutter_typeahead: ^1.8.1 # Auto-complete input field
image_picker: ^0.8.0 # Select or take photos image_picker: ^0.8.0 # Select or take photos
url_launcher: 6.0.0 # Open link in system browser url_launcher: 6.0.0 # Open link in system browser
flutter_markdown: ^0.6.2 # Rendering markdown flutter_markdown: ^0.6.2 # Rendering markdown
camera: # Camera camera: # Camera
path_provider: 2.0.1 #^1.6.28 # Local file storage path_provider: 2.0.1 # Local file storage
sembast: ^2.4.9 # NoSQL data storage sembast: ^2.4.9 # NoSQL data storage
one_context: ^0.5.0 # Dialogs without requiring context one_context: ^0.5.0 # Dialogs without requiring context
infinite_scroll_pagination: ^2.3.0 # Let the server do all the work! infinite_scroll_pagination: ^2.3.0 # Let the server do all the work!
audioplayers: ^0.19.0 audioplayers: ^0.19.0 # Play audio files
path: path:
dev_dependencies: dev_dependencies: