mirror of
https://github.com/inventree/inventree-app.git
synced 2025-12-03 18:59:50 +00:00
Modern Label printing (#724)
* Basic widget * Redirect label printing action * Refactor label printing code * Construct form elements * Refactor to allow re-use of forms * Basic rendering of label printing form * Remove dead code * Pass custom handler through to form context * Refactoring API forms: - Allow custom pk field name - Add callback when values change * linting * Dynamically rebuild form * Handle nested fields * Handle label printing status * Run dart format * Update release notes * Remove unused var * Enable close icon * Handle initial plugin default value * Store default values: - Selected template (per label type) - Selected printing plugin * Dart format * Fix dart linting * use setter * Just use a public field
This commit is contained in:
@@ -26,23 +26,52 @@ import "package:inventree/widget/fields.dart";
|
||||
import "package:inventree/widget/progress.dart";
|
||||
import "package:inventree/widget/snacks.dart";
|
||||
|
||||
/*
|
||||
* Extract field options from a returned OPTIONS request
|
||||
*/
|
||||
Map<String, dynamic> extractFields(APIResponse response) {
|
||||
if (!response.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var data = response.asMap();
|
||||
|
||||
if (!data.containsKey("actions")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var actions = response.data["actions"] as Map<String, dynamic>;
|
||||
|
||||
dynamic result = actions["POST"] ?? actions["PUT"] ?? actions["PATCH"] ?? {};
|
||||
|
||||
return result as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/*
|
||||
* Class that represents a single "form field",
|
||||
* defined by the InvenTree API
|
||||
*/
|
||||
class APIFormField {
|
||||
// Constructor
|
||||
APIFormField(this.name, this.data);
|
||||
APIFormField(this.name, this.data, {this.formHandler});
|
||||
|
||||
// File to be uploaded for this filed
|
||||
File? attachedfile;
|
||||
|
||||
APIFormWidgetState? formHandler;
|
||||
|
||||
// Name of this field
|
||||
final String name;
|
||||
|
||||
// JSON data which defines the field
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
// Function to update the value of this field
|
||||
void setFieldValue(dynamic val) {
|
||||
data["value"] = val;
|
||||
formHandler?.onValueChanged(name, value);
|
||||
}
|
||||
|
||||
// JSON field definition provided by the server
|
||||
Map<String, dynamic> definition = {};
|
||||
|
||||
@@ -88,6 +117,8 @@ class APIFormField {
|
||||
}
|
||||
}
|
||||
|
||||
String get pk_field => (getParameter("pk_field") ?? "pk") as String;
|
||||
|
||||
// Get the "api_url" associated with a related field
|
||||
String get api_url => (getParameter("api_url") ?? "") as String;
|
||||
|
||||
@@ -244,18 +275,13 @@ class APIFormField {
|
||||
return;
|
||||
}
|
||||
|
||||
int? pk = int.tryParse(value.toString());
|
||||
|
||||
if (pk == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String url = api_url + "/" + pk.toString() + "/";
|
||||
String url = api_url + "/" + value.toString() + "/";
|
||||
|
||||
final APIResponse response = await InvenTreeAPI().get(url, params: filters);
|
||||
|
||||
if (response.successful()) {
|
||||
initial_data = response.data;
|
||||
formHandler?.onValueChanged(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +295,7 @@ class APIFormField {
|
||||
return _constructBoolean();
|
||||
case "related field":
|
||||
return _constructRelatedField();
|
||||
case "integer":
|
||||
case "float":
|
||||
case "decimal":
|
||||
return _constructFloatField();
|
||||
@@ -318,8 +345,7 @@ class APIFormField {
|
||||
onPressed: () async {
|
||||
var handler = UniqueBarcodeHandler((String hash) {
|
||||
controller.text = hash;
|
||||
data["value"] = hash;
|
||||
|
||||
setFieldValue(hash);
|
||||
barcodeSuccess(L10().barcodeAssigned);
|
||||
});
|
||||
|
||||
@@ -347,9 +373,9 @@ class APIFormField {
|
||||
onChanged: (DateTime? time) {
|
||||
// Save the time string
|
||||
if (time == null) {
|
||||
data["value"] = null;
|
||||
setFieldValue(null);
|
||||
} else {
|
||||
data["value"] = time.toString().split(" ").first;
|
||||
setFieldValue(time.toString().split(" ").first);
|
||||
}
|
||||
},
|
||||
onShowPicker: (context, value) async {
|
||||
@@ -432,9 +458,9 @@ class APIFormField {
|
||||
},
|
||||
onSaved: (item) {
|
||||
if (item == null) {
|
||||
data["value"] = null;
|
||||
setFieldValue(null);
|
||||
} else {
|
||||
data["value"] = item["value"];
|
||||
setFieldValue(item["value"]);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -481,7 +507,7 @@ class APIFormField {
|
||||
return null;
|
||||
},
|
||||
onSaved: (val) {
|
||||
data["value"] = val;
|
||||
setFieldValue(val);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -527,7 +553,20 @@ class APIFormField {
|
||||
hintText: helpText,
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
onChanged: (item) {
|
||||
if (item != null) {
|
||||
setFieldValue(item[pk_field]);
|
||||
} else {
|
||||
setFieldValue(null);
|
||||
}
|
||||
},
|
||||
onSaved: (item) {
|
||||
if (item != null) {
|
||||
setFieldValue(item[pk_field]);
|
||||
} else {
|
||||
setFieldValue(null);
|
||||
}
|
||||
},
|
||||
itemAsString: (dynamic item) {
|
||||
Map<String, dynamic> data = item as Map<String, dynamic>;
|
||||
|
||||
@@ -551,13 +590,6 @@ class APIFormField {
|
||||
dropdownBuilder: (context, item) {
|
||||
return _renderRelatedField(name, item, true, false);
|
||||
},
|
||||
onSaved: (item) {
|
||||
if (item != null) {
|
||||
data["value"] = item["pk"];
|
||||
} else {
|
||||
data["value"] = null;
|
||||
}
|
||||
},
|
||||
compareFn: (dynamic item, dynamic selectedItem) {
|
||||
// Comparison is based on the PK value
|
||||
|
||||
@@ -568,7 +600,8 @@ class APIFormField {
|
||||
bool result = false;
|
||||
|
||||
try {
|
||||
result = item["pk"].toString() == selectedItem["pk"].toString();
|
||||
result =
|
||||
item[pk_field].toString() == selectedItem[pk_field].toString();
|
||||
} catch (error) {
|
||||
// Catch any conversion errors
|
||||
result = false;
|
||||
@@ -765,6 +798,18 @@ class APIFormField {
|
||||
so.customer?.thumbnail ?? so.customer?.image ?? "",
|
||||
),
|
||||
);
|
||||
case "labeltemplate":
|
||||
return ListTile(
|
||||
title: Text((data["name"] ?? "").toString()),
|
||||
subtitle: Text((data["description"] ?? "").toString()),
|
||||
);
|
||||
case "pluginconfig":
|
||||
return ListTile(
|
||||
title: Text(
|
||||
(data["meta"]?["human_name"] ?? data["name"] ?? "").toString(),
|
||||
),
|
||||
subtitle: Text((data["meta"]?["description"] ?? "").toString()),
|
||||
);
|
||||
default:
|
||||
return ListTile(
|
||||
title: Text(
|
||||
@@ -810,8 +855,11 @@ class APIFormField {
|
||||
maxLines: multiline ? null : 1,
|
||||
expands: false,
|
||||
initialValue: (value ?? "") as String,
|
||||
onChanged: (val) {
|
||||
setFieldValue(val);
|
||||
},
|
||||
onSaved: (val) {
|
||||
data["value"] = val;
|
||||
setFieldValue(val);
|
||||
},
|
||||
validator: (value) {
|
||||
if (required && (value == null || value.isEmpty)) {
|
||||
@@ -842,7 +890,7 @@ class APIFormField {
|
||||
initial: initial_value,
|
||||
tristate: (getParameter("tristate") ?? false) as bool,
|
||||
onSaved: (val) {
|
||||
data["value"] = val;
|
||||
setFieldValue(val);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -865,27 +913,6 @@ class APIFormField {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Extract field options from a returned OPTIONS request
|
||||
*/
|
||||
Map<String, dynamic> extractFields(APIResponse response) {
|
||||
if (!response.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var data = response.asMap();
|
||||
|
||||
if (!data.containsKey("actions")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var actions = response.data["actions"] as Map<String, dynamic>;
|
||||
|
||||
dynamic result = actions["POST"] ?? actions["PUT"] ?? actions["PATCH"] ?? {};
|
||||
|
||||
return result as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/*
|
||||
* Extract a field definition (map) from the provided JSON data.
|
||||
*
|
||||
@@ -981,6 +1008,7 @@ Future<void> launchApiForm(
|
||||
Function(Map<String, dynamic>)? onSuccess,
|
||||
bool Function(Map<String, dynamic>)? validate,
|
||||
Function? onCancel,
|
||||
APIFormWidgetState? formHandler,
|
||||
IconData icon = TablerIcons.device_floppy,
|
||||
}) async {
|
||||
showLoadingOverlay();
|
||||
@@ -1041,7 +1069,7 @@ Future<void> launchApiForm(
|
||||
field.data["instance_value"] = model_value;
|
||||
|
||||
if (field.data["value"] == null) {
|
||||
field.data["value"] = model_value;
|
||||
field.setFieldValue(model_value);
|
||||
}
|
||||
}
|
||||
formFields.add(field);
|
||||
@@ -1066,6 +1094,7 @@ Future<void> launchApiForm(
|
||||
onSuccess: onSuccess,
|
||||
validate: validate,
|
||||
fileField: fileField,
|
||||
state: formHandler,
|
||||
icon: icon,
|
||||
),
|
||||
),
|
||||
@@ -1079,6 +1108,7 @@ class APIFormWidget extends StatefulWidget {
|
||||
this.fields,
|
||||
this.method, {
|
||||
Key? key,
|
||||
this.state,
|
||||
this.onSuccess,
|
||||
this.validate,
|
||||
this.fileField = "",
|
||||
@@ -1105,12 +1135,15 @@ class APIFormWidget extends StatefulWidget {
|
||||
|
||||
final bool Function(Map<String, dynamic>)? validate;
|
||||
|
||||
final APIFormWidgetState? state;
|
||||
|
||||
// Default form handler is constructed if none is provided
|
||||
@override
|
||||
_APIFormWidgetState createState() => _APIFormWidgetState();
|
||||
APIFormWidgetState createState() => state ?? APIFormWidgetState();
|
||||
}
|
||||
|
||||
class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
_APIFormWidgetState() : super();
|
||||
class APIFormWidgetState extends State<APIFormWidget> {
|
||||
APIFormWidgetState() : super();
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@@ -1118,6 +1151,33 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
|
||||
bool spacerRequired = false;
|
||||
|
||||
// Return a list of all fields used for this form
|
||||
// The default implementation just returns the fields provided to the widget
|
||||
// However, custom form implementations may override this function
|
||||
List<APIFormField> get formFields {
|
||||
final List<APIFormField> fields = widget.fields;
|
||||
|
||||
// Ensure each field has access to this form handler
|
||||
for (var field in fields) {
|
||||
field.formHandler ??= this;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
// Callback for when a field value is changed
|
||||
// Default implementation does nothing,
|
||||
// but custom form implementations may override this function
|
||||
void onValueChanged(String field, dynamic value) {}
|
||||
|
||||
Future<void> handleSuccess(
|
||||
Map<String, dynamic> submittedData,
|
||||
Map<String, dynamic> responseData,
|
||||
) async {
|
||||
widget.onSuccess?.call(responseData);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
List<Widget> _buildForm() {
|
||||
List<Widget> widgets = [];
|
||||
|
||||
@@ -1135,7 +1195,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
widgets.add(Divider(height: 5));
|
||||
}
|
||||
|
||||
for (var field in widget.fields) {
|
||||
for (var field in formFields) {
|
||||
if (field.hidden) {
|
||||
continue;
|
||||
}
|
||||
@@ -1190,7 +1250,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
// Pop the "file" field
|
||||
data.remove(widget.fileField);
|
||||
|
||||
for (var field in widget.fields) {
|
||||
for (var field in formFields) {
|
||||
if (field.name == widget.fileField) {
|
||||
File? file = field.attachedfile;
|
||||
|
||||
@@ -1275,7 +1335,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
match = true;
|
||||
continue;
|
||||
default:
|
||||
for (var field in widget.fields) {
|
||||
for (var field in formFields) {
|
||||
// Hidden fields can't display errors, so we won't match
|
||||
if (field.hidden) {
|
||||
continue;
|
||||
@@ -1327,7 +1387,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
|
||||
// Iterate through and find "simple" top-level fields
|
||||
|
||||
for (var field in widget.fields) {
|
||||
for (var field in formFields) {
|
||||
if (field.readOnly) {
|
||||
continue;
|
||||
}
|
||||
@@ -1366,20 +1426,11 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run custom onSuccess function
|
||||
var successFunc = widget.onSuccess;
|
||||
|
||||
// An "empty" URL means we don't want to submit the form anywhere
|
||||
// Perhaps we just want to process the data?
|
||||
if (widget.url.isEmpty) {
|
||||
// Hide the form
|
||||
Navigator.pop(context);
|
||||
|
||||
if (successFunc != null) {
|
||||
// Return the raw "submitted" data, rather than the server response
|
||||
successFunc(data);
|
||||
}
|
||||
|
||||
handleSuccess(data, {});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1394,29 +1445,24 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
case 200:
|
||||
case 201:
|
||||
// Form was successfully validated by the server
|
||||
// Ensure the response is a valid JSON structure
|
||||
Map<String, dynamic> json = {};
|
||||
|
||||
// Hide this form
|
||||
Navigator.pop(context);
|
||||
var responseData = response.asMap();
|
||||
|
||||
if (successFunc != null) {
|
||||
// Ensure the response is a valid JSON structure
|
||||
Map<String, dynamic> json = {};
|
||||
|
||||
var data = response.asMap();
|
||||
|
||||
for (String key in data.keys) {
|
||||
json[key.toString()] = data[key];
|
||||
}
|
||||
|
||||
successFunc(json);
|
||||
for (String key in responseData.keys) {
|
||||
json[key.toString()] = responseData[key];
|
||||
}
|
||||
|
||||
handleSuccess(data, json);
|
||||
|
||||
return;
|
||||
case 400:
|
||||
// Form submission / validation error
|
||||
showSnackIcon(L10().formError, success: false);
|
||||
|
||||
// Update field errors
|
||||
for (var field in widget.fields) {
|
||||
for (var field in formFields) {
|
||||
field.extractErrorMessages(response);
|
||||
}
|
||||
|
||||
@@ -1444,6 +1490,22 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
// Construct the internal form widget, based on the provided fields
|
||||
Widget buildForm(BuildContext context) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildForm(),
|
||||
),
|
||||
padding: EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -1463,18 +1525,7 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildForm(),
|
||||
),
|
||||
padding: EdgeInsets.all(16),
|
||||
),
|
||||
),
|
||||
body: buildForm(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user