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:
parent
e108598557
commit
8bca501fc4
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
2
lib/l10n
2
lib/l10n
@ -1 +1 @@
|
|||||||
Subproject commit fc01d9826d51e15377894d9cb6347c61b22e3be1
|
Subproject commit da90edf7611618212407d12bb8abcf8a66f05364
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user