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

Allow file fields for api forms

This commit is contained in:
Oliver 2021-08-16 20:22:05 +10:00
parent e108598557
commit 8bca501fc4
5 changed files with 97 additions and 18 deletions

View File

@ -1,4 +1,5 @@
import 'dart:ui'; import 'dart:ui';
import 'dart:io';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:dropdown_search/dropdown_search.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/api.dart';
import 'package:inventree/app_colors.dart'; import 'package:inventree/app_colors.dart';
import 'package:inventree/inventree/part.dart'; import 'package:inventree/inventree/part.dart';
import 'package:inventree/inventree/sentry.dart';
import 'package:inventree/inventree/stock.dart'; import 'package:inventree/inventree/stock.dart';
import 'package:inventree/widget/dialogs.dart'; import 'package:inventree/widget/dialogs.dart';
import 'package:inventree/widget/fields.dart'; import 'package:inventree/widget/fields.dart';
@ -26,6 +28,9 @@ class APIFormField {
// Constructor // Constructor
APIFormField(this.name, this.data); APIFormField(this.name, this.data);
// File to be uploaded for this filed
File? attachedfile;
// Name of this field // Name of this field
final String name; final String name;
@ -159,6 +164,9 @@ class APIFormField {
return _constructFloatField(); return _constructFloatField();
case "choice": case "choice":
return _constructChoiceField(); return _constructChoiceField();
case "file upload":
case "image upload":
return _constructFileField();
default: default:
return ListTile( return ListTile(
title: Text( 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() { Widget _constructChoiceField() {
dynamic _initial; dynamic _initial;
@ -478,7 +524,7 @@ Map<String, dynamic> extractFields(APIResponse response) {
* @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH) * @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH)
*/ */
Future<void> launchApiForm(BuildContext context, String title, String url, Map<String, dynamic> fields, {Map<String, dynamic> modelData = const {}, String method = "PATCH", Function(Map<String, dynamic>)? onSuccess, Function? onCancel}) async { Future<void> launchApiForm(BuildContext context, String title, String url, Map<String, dynamic> fields, {String fileField = "", Map<String, dynamic> modelData = const {}, String method = "PATCH", Function(Map<String, dynamic>)? onSuccess, Function? onCancel}) async {
var options = await InvenTreeAPI().options(url); var options = await InvenTreeAPI().options(url);
@ -508,6 +554,14 @@ Future<void> launchApiForm(BuildContext context, String title, String url, Map<S
// Check that the field is actually available at the API endpoint // Check that the field is actually available at the API endpoint
if (!availableFields.containsKey(fieldName)) { if (!availableFields.containsKey(fieldName)) {
print("Field '${fieldName}' not available at '${url}'"); print("Field '${fieldName}' not available at '${url}'");
sentryReportMessage(
"API form called with unknown field '${fieldName}'",
context: {
"url": url.toString(),
}
);
continue; continue;
} }
@ -563,6 +617,7 @@ Future<void> launchApiForm(BuildContext context, String title, String url, Map<S
formFields, formFields,
method, method,
onSuccess: onSuccess, onSuccess: onSuccess,
fileField: fileField,
)) ))
); );
} }
@ -579,6 +634,8 @@ class APIFormWidget extends StatefulWidget {
//! API method //! API method
final String method; final String method;
final String fileField;
final List<APIFormField> fields; final List<APIFormField> fields;
Function(Map<String, dynamic>)? onSuccess; Function(Map<String, dynamic>)? onSuccess;
@ -591,11 +648,12 @@ class APIFormWidget extends StatefulWidget {
{ {
Key? key, Key? key,
this.onSuccess, this.onSuccess,
this.fileField = "",
} }
) : super(key: key); ) : super(key: key);
@override @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<APIFormWidget> {
String method; String method;
String fileField;
List<APIFormField> fields; List<APIFormField> fields;
Function(Map<String, dynamic>)? onSuccess; Function(Map<String, dynamic>)? 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; bool spacerRequired = false;
@ -678,6 +738,32 @@ class _APIFormWidgetState extends State<APIFormWidget> {
Future<APIResponse> _submit(Map<String, String> data) async { Future<APIResponse> _submit(Map<String, String> 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") { if (method == "POST") {
return await InvenTreeAPI().post( return await InvenTreeAPI().post(
url, url,

View File

@ -73,7 +73,7 @@ class InvenTreeModel {
return {}; return {};
} }
Future<void> createForm(BuildContext context, String title, {Map<String, dynamic> fields=const{}, Map<String, dynamic> data=const {}, Function(dynamic)? onSuccess}) async { Future<void> createForm(BuildContext context, String title, {String fileField = "", Map<String, dynamic> fields=const{}, Map<String, dynamic> data=const {}, Function(dynamic)? onSuccess}) async {
if (fields.isEmpty) { if (fields.isEmpty) {
fields = formFields(); fields = formFields();
@ -87,6 +87,7 @@ class InvenTreeModel {
modelData: data, modelData: data,
onSuccess: onSuccess, onSuccess: onSuccess,
method: "POST", method: "POST",
fileField: fileField,
); );
} }
@ -511,7 +512,7 @@ class InvenTreeAttachment extends InvenTreeModel {
Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async { Future<bool> uploadAttachment(File attachment, {String comment = "", Map<String, String> fields = const {}}) async {
final http.StreamedResponse response = await InvenTreeAPI().uploadFile( final APIResponse response = await InvenTreeAPI().uploadFile(
URL, URL,
attachment, attachment,
method: 'POST', method: 'POST',
@ -519,11 +520,7 @@ class InvenTreeAttachment extends InvenTreeModel {
fields: fields fields: fields
); );
if (response.statusCode == 200 || response.statusCode == 201) { return response.successful();
return true;
} else {
return false;
}
} }
Future<void> downloadAttachment() async { Future<void> downloadAttachment() async {

View File

@ -356,19 +356,14 @@ class InvenTreePart extends InvenTreeModel {
Future<bool> uploadImage(File image) async { Future<bool> uploadImage(File image) async {
// Upload file against this part // Upload file against this part
final http.StreamedResponse response = await InvenTreeAPI().uploadFile( final APIResponse response = await InvenTreeAPI().uploadFile(
url, url,
image, image,
method: 'PATCH', method: 'PATCH',
name: 'image', name: 'image',
); );
if (response.statusCode != 200) { return response.successful();
print("uploadImage returned ${response.statusCode} at '${url}'");
return false;
}
return true;
} }
// Return the "starred" status of this part // Return the "starred" status of this part

@ -1 +1 @@
Subproject commit fc01d9826d51e15377894d9cb6347c61b22e3be1 Subproject commit da90edf7611618212407d12bb8abcf8a66f05364

View File

@ -244,6 +244,7 @@ class _StockItemTestResultDisplayState extends RefreshableState<StockItemTestRes
trailing: _icon, trailing: _icon,
onLongPress: () { onLongPress: () {
addTestResult( addTestResult(
context,
name: _test, name: _test,
nameIsEditable: !_required, nameIsEditable: !_required,
valueRequired: _valueRequired, valueRequired: _valueRequired,