From 8bca501fc450d8ade5c57abfe7207d5269f16649 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 16 Aug 2021 20:22:05 +1000 Subject: [PATCH] Allow file fields for api forms --- lib/api_form.dart | 92 ++++++++++++++++++++++++- lib/inventree/model.dart | 11 ++- lib/inventree/part.dart | 9 +-- lib/l10n | 2 +- lib/widget/stock_item_test_results.dart | 1 + 5 files changed, 97 insertions(+), 18 deletions(-) diff --git a/lib/api_form.dart b/lib/api_form.dart index 67b52e6d..51366c13 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -1,4 +1,5 @@ import 'dart:ui'; +import 'dart:io'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:dropdown_search/dropdown_search.dart'; @@ -6,6 +7,7 @@ import 'package:dropdown_search/dropdown_search.dart'; import 'package:inventree/api.dart'; import 'package:inventree/app_colors.dart'; import 'package:inventree/inventree/part.dart'; +import 'package:inventree/inventree/sentry.dart'; import 'package:inventree/inventree/stock.dart'; import 'package:inventree/widget/dialogs.dart'; import 'package:inventree/widget/fields.dart'; @@ -26,6 +28,9 @@ class APIFormField { // Constructor APIFormField(this.name, this.data); + // File to be uploaded for this filed + File? attachedfile; + // Name of this field final String name; @@ -159,6 +164,9 @@ class APIFormField { return _constructFloatField(); case "choice": return _constructChoiceField(); + case "file upload": + case "image upload": + return _constructFileField(); default: return ListTile( title: Text( @@ -171,6 +179,44 @@ class APIFormField { } } + // Field for selecting and uploading files + Widget _constructFileField() { + + TextEditingController controller = new TextEditingController(); + + controller.text = (attachedfile?.path ?? L10().attachmentSelect).split("/").last; + + return InputDecorator( + decoration: InputDecoration( + labelText: label, + labelStyle: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + ), + child: ListTile( + title: TextField( + readOnly: true, + controller: controller, + ), + trailing: IconButton( + icon: FaIcon(FontAwesomeIcons.plusCircle), + onPressed: () async { + FilePickerDialog.pickFile( + message: L10().attachmentSelect, + onPicked: (file) { + print("${file.path}"); + // Display the filename + controller.text = file.path.split("/").last; + + // Save the file + attachedfile = file; + } + ); + }, + ) + ) + ); + } + + // Field for selecting from multiple choice options Widget _constructChoiceField() { dynamic _initial; @@ -478,7 +524,7 @@ Map extractFields(APIResponse response) { * @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH) */ -Future launchApiForm(BuildContext context, String title, String url, Map fields, {Map modelData = const {}, String method = "PATCH", Function(Map)? onSuccess, Function? onCancel}) async { +Future launchApiForm(BuildContext context, String title, String url, Map fields, {String fileField = "", Map modelData = const {}, String method = "PATCH", Function(Map)? onSuccess, Function? onCancel}) async { var options = await InvenTreeAPI().options(url); @@ -508,6 +554,14 @@ Future launchApiForm(BuildContext context, String title, String url, Map launchApiForm(BuildContext context, String title, String url, Map fields; Function(Map)? onSuccess; @@ -591,11 +648,12 @@ class APIFormWidget extends StatefulWidget { { Key? key, this.onSuccess, + this.fileField = "", } ) : super(key: key); @override - _APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, method, onSuccess); + _APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, method, onSuccess, fileField); } @@ -610,11 +668,13 @@ class _APIFormWidgetState extends State { String method; + String fileField; + List fields; Function(Map)? onSuccess; - _APIFormWidgetState(this.title, this.url, this.fields, this.method, this.onSuccess) : super(); + _APIFormWidgetState(this.title, this.url, this.fields, this.method, this.onSuccess, this.fileField) : super(); bool spacerRequired = false; @@ -678,6 +738,32 @@ class _APIFormWidgetState extends State { Future _submit(Map data) async { + + // If a file upload is required, we have to handle the submission differently + if (fileField.isNotEmpty) { + + // Pop the "file" field + data.remove(fileField); + + for (var field in fields) { + if (field.name == fileField) { + + File? file = field.attachedfile; + + if (file != null) { + + // A valid file has been supplied + return await InvenTreeAPI().uploadFile( + url, + file, + name: fileField, + fields: data, + ); + } + } + } + } + if (method == "POST") { return await InvenTreeAPI().post( url, diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 8f9f3f5b..50ec330f 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -73,7 +73,7 @@ class InvenTreeModel { return {}; } - Future createForm(BuildContext context, String title, {Map fields=const{}, Map data=const {}, Function(dynamic)? onSuccess}) async { + Future createForm(BuildContext context, String title, {String fileField = "", Map fields=const{}, Map data=const {}, Function(dynamic)? onSuccess}) async { if (fields.isEmpty) { fields = formFields(); @@ -87,6 +87,7 @@ class InvenTreeModel { modelData: data, onSuccess: onSuccess, method: "POST", + fileField: fileField, ); } @@ -511,7 +512,7 @@ class InvenTreeAttachment extends InvenTreeModel { Future uploadAttachment(File attachment, {String comment = "", Map fields = const {}}) async { - final http.StreamedResponse response = await InvenTreeAPI().uploadFile( + final APIResponse response = await InvenTreeAPI().uploadFile( URL, attachment, method: 'POST', @@ -519,11 +520,7 @@ class InvenTreeAttachment extends InvenTreeModel { fields: fields ); - if (response.statusCode == 200 || response.statusCode == 201) { - return true; - } else { - return false; - } + return response.successful(); } Future downloadAttachment() async { diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index dcd18df3..63f11beb 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -356,19 +356,14 @@ class InvenTreePart extends InvenTreeModel { Future uploadImage(File image) async { // Upload file against this part - final http.StreamedResponse response = await InvenTreeAPI().uploadFile( + final APIResponse response = await InvenTreeAPI().uploadFile( url, image, method: 'PATCH', name: 'image', ); - if (response.statusCode != 200) { - print("uploadImage returned ${response.statusCode} at '${url}'"); - return false; - } - - return true; + return response.successful(); } // Return the "starred" status of this part diff --git a/lib/l10n b/lib/l10n index fc01d982..da90edf7 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit fc01d9826d51e15377894d9cb6347c61b22e3be1 +Subproject commit da90edf7611618212407d12bb8abcf8a66f05364 diff --git a/lib/widget/stock_item_test_results.dart b/lib/widget/stock_item_test_results.dart index fe85175e..bdefdc2b 100644 --- a/lib/widget/stock_item_test_results.dart +++ b/lib/widget/stock_item_test_results.dart @@ -244,6 +244,7 @@ class _StockItemTestResultDisplayState extends RefreshableState