From b7f9f1c55f85640d1160e72754280fb9fe9d0ba0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 3 Oct 2021 00:40:26 +1000 Subject: [PATCH] Handle form posting with complex "layered" data - Handle data rendering - Handle returned errors --- lib/api.dart | 17 +++--- lib/api_form.dart | 86 +++++++++++++++++++++++++++---- lib/inventree/purchase_order.dart | 2 + 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index 64993608..64a74682 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -485,11 +485,9 @@ class InvenTreeAPI { // Perform a PATCH request - Future patch(String url, {Map body = const {}, int? expectedStatusCode}) async { - Map _body = {}; + Future patch(String url, {Map body = const {}, int? expectedStatusCode}) async { - // Copy across provided data - body.forEach((K, V) => _body[K] = V); + Map _body = body; HttpClientRequest? request = await apiRequest(url, "PATCH"); @@ -599,7 +597,7 @@ class InvenTreeAPI { * Upload a file to the given URL */ Future uploadFile(String url, File f, - {String name = "attachment", String method="POST", Map? fields}) async { + {String name = "attachment", String method="POST", Map? fields}) async { var _url = makeApiUrl(url); var request = http.MultipartRequest(method, Uri.parse(_url)); @@ -607,8 +605,13 @@ class InvenTreeAPI { request.headers.addAll(defaultHeaders()); if (fields != null) { - fields.forEach((String key, String value) { - request.fields[key] = value; + fields.forEach((String key, dynamic value) { + + if (value == null) { + request.fields[key] = ""; + } else { + request.fields[key] = value.toString(); + } }); } diff --git a/lib/api_form.dart b/lib/api_form.dart index f6e7fcf0..4204213b 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -101,13 +101,24 @@ class APIFormField { // Note: This parameter is only defined locally String get parent => (data["parent"] ?? "") as String; + bool get isSimple => !nested && parent.isEmpty; + // Is this field read only? bool get readOnly => (getParameter("read_only") ?? false) as bool; bool get multiline => (getParameter("multiline") ?? false) as bool; // Get the "value" as a string (look for "default" if not available) - dynamic get value => data["value"] ?? data["instance_value"] ?? data["default"]; + dynamic get value => data["value"] ?? data["instance_value"] ?? defaultValue; + + // Render value to string (for form submission) + String renderToString() { + if (value == null) { + return ""; + } else { + return value.toString(); + } + } // Get the "default" as a string dynamic get defaultValue => getParameter("default"); @@ -166,6 +177,28 @@ class APIFormField { bool hasErrors() => errorMessages().isNotEmpty; + // Extract error messages from the server response + void extractErrorMessages(APIResponse response) { + + if (isSimple) { + // Simple fields are easily handled + data["errors"] = response.data[name]; + } else { + if (parent.isNotEmpty) { + dynamic parentElement = response.data[parent]; + + // Extract from list + if (parentElement is List) { + parentElement = parentElement[0]; + } + + if (parentElement is Map) { + data["errors"] = parentElement[name]; + } + } + } + } + // Return the error message associated with this field List errorMessages() { List errors = (data["errors"] ?? []) as List; @@ -902,8 +935,7 @@ class _APIFormWidgetState extends State { return widgets; } - Future _submit(Map data) async { - + Future _submit(Map data) async { // If a file upload is required, we have to handle the submission differently if (fileField.isNotEmpty) { @@ -953,22 +985,50 @@ class _APIFormWidgetState extends State { } + /* + * Submit the form data to the server, and handle the results + */ Future _save(BuildContext context) async { // Package up the form data - Map data = {}; + Map data = {}; + + // Iterate through and find "simple" top-level fields for (var field in fields) { - dynamic value = field.value; - - if (value == null) { - data[field.name] = ""; + if (field.isSimple) { + // Simple top-level field data + data[field.name] = field.renderToString(); } else { - data[field.name] = value.toString(); + // Not so simple... (WHY DID I MAKE THE API SO COMPLEX?) + if (field.parent.isNotEmpty) { + + // TODO: This is a dirty hack, there *must* be a cleaner way?! + + dynamic parent = data[field.parent] ?? {}; + + // In the case of a "nested" object, we need to extract the first item + if (parent is List) { + parent = parent.first; + } + + parent[field.name] = field.renderToString(); + + // Nested fields must be handled as an array! + // For now, we only allow single length nested fields + if (field.nested) { + parent = [parent]; + } + + data[field.parent] = parent; + } } } + print("Submitting form data to server:"); + print(data.toString()); + final response = await _submit(data); if (!response.isValid()) { @@ -976,6 +1036,9 @@ class _APIFormWidgetState extends State { return; } + print("Response: ${response.statusCode}"); + print(response.data.toString()); + switch (response.statusCode) { case 200: case 201: @@ -1010,10 +1073,13 @@ class _APIFormWidgetState extends State { success: false ); + print(response.data); + // Update field errors for (var field in fields) { - field.data["errors"] = response.data[field.name]; + field.extractErrorMessages(response); } + break; case 401: showSnackIcon( diff --git a/lib/inventree/purchase_order.dart b/lib/inventree/purchase_order.dart index ba979a28..6b8595cf 100644 --- a/lib/inventree/purchase_order.dart +++ b/lib/inventree/purchase_order.dart @@ -85,6 +85,8 @@ class InvenTreePurchaseOrder extends InvenTreeModel { bool get isOpen => status == PO_STATUS_PENDING || status == PO_STATUS_PLACED; + bool get isPlaced => status == PO_STATUS_PLACED; + bool get isFailed => status == PO_STATUS_CANCELLED || status == PO_STATUS_LOST || status == PO_STATUS_RETURNED; Future> getLineItems() async {