mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-30 21:05:42 +00:00 
			
		
		
		
	Handle form posting with complex "layered" data
- Handle data rendering - Handle returned errors
This commit is contained in:
		
							
								
								
									
										17
									
								
								lib/api.dart
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								lib/api.dart
									
									
									
									
									
								
							| @@ -485,11 +485,9 @@ class InvenTreeAPI { | |||||||
|  |  | ||||||
|  |  | ||||||
|   // Perform a PATCH request |   // Perform a PATCH request | ||||||
|   Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int? expectedStatusCode}) async { |   Future<APIResponse> patch(String url, {Map<String, dynamic> body = const {}, int? expectedStatusCode}) async { | ||||||
|     Map<String, String> _body = {}; |  | ||||||
|  |  | ||||||
|     // Copy across provided data |     Map<String, dynamic> _body = body; | ||||||
|     body.forEach((K, V) => _body[K] = V); |  | ||||||
|  |  | ||||||
|     HttpClientRequest? request = await apiRequest(url, "PATCH"); |     HttpClientRequest? request = await apiRequest(url, "PATCH"); | ||||||
|  |  | ||||||
| @@ -599,7 +597,7 @@ class InvenTreeAPI { | |||||||
|    * Upload a file to the given URL |    * Upload a file to the given URL | ||||||
|    */ |    */ | ||||||
|   Future<APIResponse> uploadFile(String url, File f, |   Future<APIResponse> uploadFile(String url, File f, | ||||||
|       {String name = "attachment", String method="POST", Map<String, String>? fields}) async { |       {String name = "attachment", String method="POST", Map<String, dynamic>? 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)); | ||||||
| @@ -607,8 +605,13 @@ class InvenTreeAPI { | |||||||
|     request.headers.addAll(defaultHeaders()); |     request.headers.addAll(defaultHeaders()); | ||||||
|  |  | ||||||
|     if (fields != null) { |     if (fields != null) { | ||||||
|       fields.forEach((String key, String value) { |       fields.forEach((String key, dynamic value) { | ||||||
|         request.fields[key] = value; |  | ||||||
|  |         if (value == null) { | ||||||
|  |           request.fields[key] = ""; | ||||||
|  |         } else { | ||||||
|  |           request.fields[key] = value.toString(); | ||||||
|  |         } | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -101,13 +101,24 @@ class APIFormField { | |||||||
|   // Note: This parameter is only defined locally |   // Note: This parameter is only defined locally | ||||||
|   String get parent => (data["parent"] ?? "") as String; |   String get parent => (data["parent"] ?? "") as String; | ||||||
|  |  | ||||||
|  |   bool get isSimple => !nested && parent.isEmpty; | ||||||
|  |  | ||||||
|   // Is this field read only? |   // Is this field read only? | ||||||
|   bool get readOnly => (getParameter("read_only") ?? false) as bool; |   bool get readOnly => (getParameter("read_only") ?? false) as bool; | ||||||
|  |  | ||||||
|   bool get multiline => (getParameter("multiline") ?? false) as bool; |   bool get multiline => (getParameter("multiline") ?? false) as bool; | ||||||
|  |  | ||||||
|   // Get the "value" as a string (look for "default" if not available) |   // 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 |   // Get the "default" as a string | ||||||
|   dynamic get defaultValue => getParameter("default"); |   dynamic get defaultValue => getParameter("default"); | ||||||
| @@ -166,6 +177,28 @@ class APIFormField { | |||||||
|  |  | ||||||
|   bool hasErrors() => errorMessages().isNotEmpty; |   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 |   // Return the error message associated with this field | ||||||
|   List<String> errorMessages() { |   List<String> errorMessages() { | ||||||
|     List<dynamic> errors = (data["errors"] ?? []) as List<dynamic>; |     List<dynamic> errors = (data["errors"] ?? []) as List<dynamic>; | ||||||
| @@ -902,8 +935,7 @@ class _APIFormWidgetState extends State<APIFormWidget> { | |||||||
|     return widgets; |     return widgets; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Future<APIResponse> _submit(Map<String, String> data) async { |   Future<APIResponse> _submit(Map<String, dynamic> data) async { | ||||||
|  |  | ||||||
|  |  | ||||||
|     // If a file upload is required, we have to handle the submission differently |     // If a file upload is required, we have to handle the submission differently | ||||||
|     if (fileField.isNotEmpty) { |     if (fileField.isNotEmpty) { | ||||||
| @@ -953,22 +985,50 @@ class _APIFormWidgetState extends State<APIFormWidget> { | |||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /* | ||||||
|  |    * Submit the form data to the server, and handle the results | ||||||
|  |    */ | ||||||
|   Future<void> _save(BuildContext context) async { |   Future<void> _save(BuildContext context) async { | ||||||
|  |  | ||||||
|     // Package up the form data |     // Package up the form data | ||||||
|     Map<String, String> data = {}; |     Map<String, dynamic> data = {}; | ||||||
|  |  | ||||||
|  |     // Iterate through and find "simple" top-level fields | ||||||
|  |  | ||||||
|     for (var field in fields) { |     for (var field in fields) { | ||||||
|  |  | ||||||
|       dynamic value = field.value; |       if (field.isSimple) { | ||||||
|  |         // Simple top-level field data | ||||||
|       if (value == null) { |         data[field.name] = field.renderToString(); | ||||||
|         data[field.name] = ""; |  | ||||||
|       } else { |       } 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); |     final response = await _submit(data); | ||||||
|  |  | ||||||
|     if (!response.isValid()) { |     if (!response.isValid()) { | ||||||
| @@ -976,6 +1036,9 @@ class _APIFormWidgetState extends State<APIFormWidget> { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     print("Response: ${response.statusCode}"); | ||||||
|  |     print(response.data.toString()); | ||||||
|  |  | ||||||
|     switch (response.statusCode) { |     switch (response.statusCode) { | ||||||
|       case 200: |       case 200: | ||||||
|       case 201: |       case 201: | ||||||
| @@ -1010,10 +1073,13 @@ class _APIFormWidgetState extends State<APIFormWidget> { | |||||||
|           success: false |           success: false | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  |         print(response.data); | ||||||
|  |  | ||||||
|         // Update field errors |         // Update field errors | ||||||
|         for (var field in fields) { |         for (var field in fields) { | ||||||
|           field.data["errors"] = response.data[field.name]; |           field.extractErrorMessages(response); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         break; |         break; | ||||||
|       case 401: |       case 401: | ||||||
|         showSnackIcon( |         showSnackIcon( | ||||||
|   | |||||||
| @@ -85,6 +85,8 @@ class InvenTreePurchaseOrder extends InvenTreeModel { | |||||||
|  |  | ||||||
|   bool get isOpen => status == PO_STATUS_PENDING || status == PO_STATUS_PLACED; |   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; |   bool get isFailed => status == PO_STATUS_CANCELLED || status == PO_STATUS_LOST || status == PO_STATUS_RETURNED; | ||||||
|  |  | ||||||
|   Future<List<InvenTreePOLineItem>> getLineItems() async { |   Future<List<InvenTreePOLineItem>> getLineItems() async { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user