mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 13:36:50 +00:00
Refactoring for APIFormField class
- Class now stores field definitions as returned from the server - Complex lookup for multi-level form structures - Improved / increased error handling
This commit is contained in:
parent
529510bc5c
commit
ce31d968b3
@ -38,53 +38,125 @@ class APIFormField {
|
|||||||
// JSON data which defines the field
|
// JSON data which defines the field
|
||||||
final Map<String, dynamic> data;
|
final Map<String, dynamic> data;
|
||||||
|
|
||||||
|
// JSON field definition provided by the server
|
||||||
|
Map<String, dynamic> definition = {};
|
||||||
|
|
||||||
dynamic initial_data;
|
dynamic initial_data;
|
||||||
|
|
||||||
|
// Return the "lookup path" for this field, within the server data
|
||||||
|
String get lookupPath {
|
||||||
|
|
||||||
|
// Simple top-level case
|
||||||
|
if (parent.isEmpty && !nested) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> path = [];
|
||||||
|
|
||||||
|
if (parent.isNotEmpty) {
|
||||||
|
path.add(parent);
|
||||||
|
path.add("child");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nested) {
|
||||||
|
path.add("children");
|
||||||
|
path.add(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extract a field parameter from the provided field definition.
|
||||||
|
*
|
||||||
|
* - First the user-provided data is checked
|
||||||
|
* - Second, the server-provided definition is checked
|
||||||
|
* - Third, return null
|
||||||
|
*/
|
||||||
|
dynamic getParameter(String key) {
|
||||||
|
|
||||||
|
if (data.containsKey(key)) {
|
||||||
|
return data[key];
|
||||||
|
} else if (definition.containsKey(key)) {
|
||||||
|
return definition[key];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the "api_url" associated with a related field
|
// Get the "api_url" associated with a related field
|
||||||
String get api_url => (data["api_url"] ?? "") as String;
|
String get api_url => (getParameter("api_url") ?? "") as String;
|
||||||
|
|
||||||
// Get the "model" associated with a related field
|
// Get the "model" associated with a related field
|
||||||
String get model => (data["model"] ?? "") as String;
|
String get model => (getParameter("model") ?? "") as String;
|
||||||
|
|
||||||
// Is this field hidden?
|
// Is this field hidden?
|
||||||
bool get hidden => (data["hidden"] ?? false) as bool;
|
bool get hidden => (getParameter("hidden") ?? false) as bool;
|
||||||
|
|
||||||
|
// Is this field nested? (Nested means part of an array)
|
||||||
|
// Note: This parameter is only defined locally
|
||||||
|
bool get nested => (data["nested"] ?? false) as bool;
|
||||||
|
|
||||||
|
// What is the "parent" field of this field?
|
||||||
|
// Note: This parameter is only defined locally
|
||||||
|
String get parent => (data["parent"] ?? "") as String;
|
||||||
|
|
||||||
// Is this field read only?
|
// Is this field read only?
|
||||||
bool get readOnly => (data["read_only"] ?? false) as bool;
|
bool get readOnly => (getParameter("read_only") ?? false) as bool;
|
||||||
|
|
||||||
bool get multiline => (data["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["default"];
|
dynamic get value => getParameter("value") ?? data["default"];
|
||||||
|
|
||||||
// Get the "default" as a string
|
// Get the "default" as a string
|
||||||
dynamic get defaultValue => data["default"];
|
dynamic get defaultValue => getParameter("default");
|
||||||
|
|
||||||
|
// Construct a set of "filters" for this field (e.g. related field)
|
||||||
Map<String, String> get filters {
|
Map<String, String> get filters {
|
||||||
|
|
||||||
Map<String, String> _filters = {};
|
Map<String, String> _filters = {};
|
||||||
|
|
||||||
// Start with the provided "model" filters
|
// Start with the field "definition" (provided by the server)
|
||||||
if (data.containsKey("filters")) {
|
if (definition.containsKey("filters")) {
|
||||||
|
|
||||||
dynamic f = data["filters"];
|
try {
|
||||||
|
var fDef = definition["filters"] as Map<String, dynamic>;
|
||||||
|
|
||||||
if (f is Map) {
|
fDef.forEach((String key, dynamic value) {
|
||||||
f.forEach((key, value) {
|
_filters[key] = value.toString();
|
||||||
_filters[key as String] = value.toString();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// pass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, look at the provided "instance_filters"
|
// Next, look at any "instance_filters" provided by the server
|
||||||
if (data.containsKey("instance_filters")) {
|
if (definition.containsKey("instance_filters")) {
|
||||||
|
|
||||||
dynamic f = data["instance_filters"];
|
try {
|
||||||
|
var fIns = definition["instance_filters"] as Map<String, dynamic>;
|
||||||
|
|
||||||
if (f is Map) {
|
fIns.forEach((String key, dynamic value) {
|
||||||
f.forEach((key, value) {
|
_filters[key] = value.toString();
|
||||||
_filters[key as String] = value.toString();
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, augment or override with any filters provided by the calling function
|
||||||
|
if (data.containsKey("filters")) {
|
||||||
|
try {
|
||||||
|
var fDat = data["filters"] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
fDat.forEach((String key, dynamic value) {
|
||||||
|
_filters[key] = value.toString();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// pass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,17 +180,17 @@ class APIFormField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Is this field required?
|
// Is this field required?
|
||||||
bool get required => (data["required"] ?? false) as bool;
|
bool get required => (getParameter("required") ?? false) as bool;
|
||||||
|
|
||||||
String get type => (data["type"] ?? "").toString();
|
String get type => (getParameter("type") ?? "").toString();
|
||||||
|
|
||||||
String get label => (data["label"] ?? "").toString();
|
String get label => (getParameter("label") ?? "").toString();
|
||||||
|
|
||||||
String get helpText => (data["help_text"] ?? "").toString();
|
String get helpText => (getParameter("help_text") ?? "").toString();
|
||||||
|
|
||||||
String get placeholderText => (data["placeholder"] ?? "").toString();
|
String get placeholderText => (getParameter("placeholder") ?? "").toString();
|
||||||
|
|
||||||
List<dynamic> get choices => (data["choices"] ?? []) as List<dynamic>;
|
List<dynamic> get choices => (getParameter("choices") ?? []) as List<dynamic>;
|
||||||
|
|
||||||
Future<void> loadInitialData() async {
|
Future<void> loadInitialData() async {
|
||||||
|
|
||||||
@ -547,6 +619,72 @@ Map<String, dynamic> extractFields(APIResponse response) {
|
|||||||
return result as Map<String, dynamic>;
|
return result as Map<String, dynamic>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extract a field definition (map) from the provided JSON data.
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - If the field is a top-level item, the provided "path" may be a simple string (e.g. "quantity"),
|
||||||
|
* - If the field is buried in the JSON data, the "path" may use a dotted notation e.g. "items.child.children.quantity"
|
||||||
|
*
|
||||||
|
* The map "tree" is traversed based on the provided lookup string, which can use dotted notation.
|
||||||
|
* This allows complex paths to be used to lookup field information.
|
||||||
|
*/
|
||||||
|
Map<String, dynamic> extractFieldDefinition(Map<String, dynamic> data, String lookup) {
|
||||||
|
|
||||||
|
List<String> path = lookup.split(".");
|
||||||
|
|
||||||
|
// Shadow copy the data for path traversal
|
||||||
|
Map<String, dynamic> _data = data;
|
||||||
|
|
||||||
|
// Iterate through all but the last element of the path
|
||||||
|
for (int ii = 0; ii < (path.length - 1); ii++) {
|
||||||
|
|
||||||
|
String el = path[ii];
|
||||||
|
|
||||||
|
if (!_data.containsKey(el)) {
|
||||||
|
print("Could not find field definition for ${lookup}:");
|
||||||
|
print("- Key ${el} missing at index ${ii}");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_data = _data[el] as Map<String, dynamic>;
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
print("Could not find sub-field element '${el}' for ${lookup}:");
|
||||||
|
print(error.toString());
|
||||||
|
|
||||||
|
// Report the error
|
||||||
|
sentryReportError(error, stackTrace);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String el = path.last;
|
||||||
|
|
||||||
|
if (!_data.containsKey(el)) {
|
||||||
|
print("Could not find field definition for ${lookup}");
|
||||||
|
print("- Final field path ${el} missing from data");
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, dynamic> definition = _data[el] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
return definition;
|
||||||
|
} catch (error, stacktrace) {
|
||||||
|
print("Could not find field definition for ${lookup}");
|
||||||
|
print(error.toString());
|
||||||
|
|
||||||
|
// Report the error
|
||||||
|
sentryReportError(error, stacktrace);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Launch an API-driven form,
|
* Launch an API-driven form,
|
||||||
* which uses the OPTIONS metadata (at the provided URL)
|
* which uses the OPTIONS metadata (at the provided URL)
|
||||||
@ -577,9 +715,10 @@ Future<void> launchApiForm(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var availableFields = extractFields(options);
|
// List of fields defined by the server
|
||||||
|
Map<String, dynamic> serverFields = extractFields(options);
|
||||||
|
|
||||||
if (availableFields.isEmpty) {
|
if (serverFields.isEmpty) {
|
||||||
// User does not have permission to perform this action
|
// User does not have permission to perform this action
|
||||||
showSnackIcon(
|
showSnackIcon(
|
||||||
L10().response403,
|
L10().response403,
|
||||||
@ -592,53 +731,32 @@ Future<void> launchApiForm(
|
|||||||
// Construct a list of APIFormField objects
|
// Construct a list of APIFormField objects
|
||||||
List<APIFormField> formFields = [];
|
List<APIFormField> formFields = [];
|
||||||
|
|
||||||
// Iterate through the provided fields we wish to display
|
APIFormField field;
|
||||||
|
|
||||||
for (String fieldName in fields.keys) {
|
for (String fieldName in fields.keys) {
|
||||||
|
|
||||||
// Check that the field is actually available at the API endpoint
|
dynamic data = fields[fieldName];
|
||||||
if (!availableFields.containsKey(fieldName)) {
|
|
||||||
print("Field '${fieldName}' not available at '${url}'");
|
|
||||||
|
|
||||||
sentryReportMessage(
|
Map<String, dynamic> fieldData = {};
|
||||||
"API form called with unknown field '${fieldName}'",
|
|
||||||
context: {
|
|
||||||
"url": url.toString(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
if (data is Map) {
|
||||||
|
fieldData = Map<String, dynamic>.from(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through the provided fields we wish to display
|
||||||
|
|
||||||
|
field = APIFormField(fieldName, fieldData);
|
||||||
|
|
||||||
|
// Extract the definition of this field from the data received from the server
|
||||||
|
field.definition = extractFieldDefinition(serverFields, field.lookupPath);
|
||||||
|
|
||||||
|
// Skip fields with empty definitions
|
||||||
|
if (field.definition.isEmpty) {
|
||||||
|
print("ERROR: Empty field definition for field '${fieldName}'");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final remoteField = Map<String, dynamic>.from(availableFields[fieldName] as Map);
|
formFields.add(field);
|
||||||
final localField = Map<String, dynamic>.from(fields[fieldName] as Map);
|
|
||||||
|
|
||||||
// Override defined field parameters, if provided
|
|
||||||
for (String key in localField.keys) {
|
|
||||||
// Special consideration must be taken here!
|
|
||||||
if (key == "filters") {
|
|
||||||
|
|
||||||
if (!remoteField.containsKey("filters")) {
|
|
||||||
remoteField["filters"] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
var filters = localField["filters"];
|
|
||||||
|
|
||||||
if (filters is Map) {
|
|
||||||
filters.forEach((key, value) {
|
|
||||||
remoteField["filters"][key] = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
remoteField[key] = localField[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modelData.containsKey(fieldName)) {
|
|
||||||
remoteField["value"] = modelData[fieldName];
|
|
||||||
}
|
|
||||||
|
|
||||||
formFields.add(APIFormField(fieldName, remoteField));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab existing data for each form field
|
// Grab existing data for each form field
|
||||||
|
Loading…
x
Reference in New Issue
Block a user