mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 13:36:50 +00:00
API form for editing PartCategory
- Custom related field renderer function - Grab related field data from the server
This commit is contained in:
parent
377da3c2fb
commit
bc713dfdcd
@ -5,6 +5,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/widget/fields.dart';
|
import 'package:inventree/widget/fields.dart';
|
||||||
import 'package:inventree/l10.dart';
|
import 'package:inventree/l10.dart';
|
||||||
|
|
||||||
@ -31,9 +32,14 @@ class APIFormField {
|
|||||||
// JSON data which defines the field
|
// JSON data which defines the field
|
||||||
final dynamic data;
|
final dynamic data;
|
||||||
|
|
||||||
|
dynamic initial_data;
|
||||||
|
|
||||||
// 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"] ?? "";
|
String get api_url => data["api_url"] ?? "";
|
||||||
|
|
||||||
|
// Get the "model" associated with a related field
|
||||||
|
String get model => data["model"] ?? "";
|
||||||
|
|
||||||
// Is this field hidden?
|
// Is this field hidden?
|
||||||
bool get hidden => (data['hidden'] ?? false) as bool;
|
bool get hidden => (data['hidden'] ?? false) as bool;
|
||||||
|
|
||||||
@ -104,6 +110,36 @@ class APIFormField {
|
|||||||
|
|
||||||
String get placeholderText => (data['placeholder'] ?? '').toString();
|
String get placeholderText => (data['placeholder'] ?? '').toString();
|
||||||
|
|
||||||
|
Future<void> loadInitialData() async {
|
||||||
|
|
||||||
|
// Only for "related fields"
|
||||||
|
if (type != "related field") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null value? No point!
|
||||||
|
if (value == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? pk = int.tryParse(value.toString());
|
||||||
|
|
||||||
|
if (pk == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = api_url + "/" + pk.toString() + "/";
|
||||||
|
|
||||||
|
final APIResponse response = await InvenTreeAPI().get(
|
||||||
|
url,
|
||||||
|
params: filters,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.isValid()) {
|
||||||
|
initial_data = response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Construct a widget for this input
|
// Construct a widget for this input
|
||||||
Widget constructField() {
|
Widget constructField() {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -132,6 +168,7 @@ class APIFormField {
|
|||||||
return DropdownSearch<dynamic>(
|
return DropdownSearch<dynamic>(
|
||||||
mode: Mode.BOTTOM_SHEET,
|
mode: Mode.BOTTOM_SHEET,
|
||||||
showSelectedItem: true,
|
showSelectedItem: true,
|
||||||
|
selectedItem: initial_data,
|
||||||
onFind: (String filter) async {
|
onFind: (String filter) async {
|
||||||
|
|
||||||
Map<String, String> _filters = {};
|
Map<String, String> _filters = {};
|
||||||
@ -164,32 +201,29 @@ class APIFormField {
|
|||||||
},
|
},
|
||||||
label: label,
|
label: label,
|
||||||
hint: helpText,
|
hint: helpText,
|
||||||
onChanged: print,
|
onChanged: null,
|
||||||
showClearButton: !required,
|
showClearButton: !required,
|
||||||
// popupTitle: Text(
|
|
||||||
// label,
|
|
||||||
// style: _labelStyle(),
|
|
||||||
// ),
|
|
||||||
itemAsString: (dynamic item) {
|
itemAsString: (dynamic item) {
|
||||||
return item['pathstring'];
|
return item['pathstring'];
|
||||||
},
|
},
|
||||||
|
dropdownBuilder: (context, item, itemAsString) {
|
||||||
|
return _renderRelatedField(item, true, false);
|
||||||
|
},
|
||||||
popupItemBuilder: (context, item, isSelected) {
|
popupItemBuilder: (context, item, isSelected) {
|
||||||
return ListTile(
|
return _renderRelatedField(item, isSelected, true);
|
||||||
title: Text(
|
|
||||||
item['pathstring'].toString(),
|
|
||||||
style: TextStyle(fontWeight: isSelected ? FontWeight.bold : FontWeight.normal),
|
|
||||||
),
|
|
||||||
subtitle: Text(item['description'].toString()),
|
|
||||||
trailing: Text(item['pk'].toString()),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onSaved: (item) {
|
onSaved: (item) {
|
||||||
data['value'] = item['pk'].toString();
|
if (item != null) {
|
||||||
|
data['value'] = item['pk'] ?? null;
|
||||||
|
} else {
|
||||||
|
data['value'] = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isFilteredOnline: true,
|
isFilteredOnline: true,
|
||||||
showSearchBox: true,
|
showSearchBox: true,
|
||||||
autoFocusSearchBox: true,
|
autoFocusSearchBox: true,
|
||||||
compareFn: (dynamic item, dynamic selectedItem) {
|
compareFn: (dynamic item, dynamic selectedItem) {
|
||||||
|
// Comparison is based on the PK value
|
||||||
|
|
||||||
if (item == null || selectedItem == null) {
|
if (item == null || selectedItem == null) {
|
||||||
return false;
|
return false;
|
||||||
@ -200,6 +234,48 @@ class APIFormField {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _renderRelatedField(dynamic item, bool selected, bool extended) {
|
||||||
|
// Render a "related field" based on the "model" type
|
||||||
|
|
||||||
|
if (item == null) {
|
||||||
|
return Text(
|
||||||
|
helpText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontStyle: FontStyle.italic
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (model) {
|
||||||
|
case "partcategory":
|
||||||
|
|
||||||
|
var cat = InvenTreePartCategory.fromJson(item);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
cat.pathstring,
|
||||||
|
style: TextStyle(fontWeight: selected && extended ? FontWeight.bold : FontWeight.normal)
|
||||||
|
),
|
||||||
|
subtitle: extended ? Text(
|
||||||
|
cat.description,
|
||||||
|
style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal),
|
||||||
|
) : null,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
"Unsupported model",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: COLOR_DANGER
|
||||||
|
)
|
||||||
|
),
|
||||||
|
subtitle: Text("Model '${model}' rendering not supported"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Construct a string input element
|
// Construct a string input element
|
||||||
Widget _constructString() {
|
Widget _constructString() {
|
||||||
|
|
||||||
@ -341,7 +417,6 @@ Future<void> launchApiForm(BuildContext context, String title, String url, Map<S
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Custom filter updating
|
|
||||||
} else {
|
} else {
|
||||||
remoteField[key] = localField[key];
|
remoteField[key] = localField[key];
|
||||||
}
|
}
|
||||||
@ -360,6 +435,11 @@ Future<void> launchApiForm(BuildContext context, String title, String url, Map<S
|
|||||||
formFields.add(APIFormField(fieldName, remoteField));
|
formFields.add(APIFormField(fieldName, remoteField));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grab existing data for each form field
|
||||||
|
for (var field in formFields) {
|
||||||
|
await field.loadInitialData();
|
||||||
|
}
|
||||||
|
|
||||||
// Now, launch a new widget!
|
// Now, launch a new widget!
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
@ -445,20 +525,6 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add a "Save" button
|
|
||||||
// widgets.add(Spacer());
|
|
||||||
|
|
||||||
/*
|
|
||||||
widgets.add(
|
|
||||||
TextButton(
|
|
||||||
child: Text(
|
|
||||||
L10().save
|
|
||||||
),
|
|
||||||
onPressed: null,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
return widgets;
|
return widgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,7 +534,14 @@ class _APIFormWidgetState extends State<APIFormWidget> {
|
|||||||
Map<String, String> _data = {};
|
Map<String, String> _data = {};
|
||||||
|
|
||||||
for (var field in fields) {
|
for (var field in fields) {
|
||||||
_data[field.name] = field.value.toString();
|
|
||||||
|
dynamic value = field.value;
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
_data[field.name] = "";
|
||||||
|
} else {
|
||||||
|
_data[field.name] = value.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle "POST" forms too!!
|
// TODO: Handle "POST" forms too!!
|
||||||
|
@ -22,6 +22,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
|
|
||||||
|
import '../api_form.dart';
|
||||||
|
|
||||||
class CategoryDisplayWidget extends StatefulWidget {
|
class CategoryDisplayWidget extends StatefulWidget {
|
||||||
|
|
||||||
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
CategoryDisplayWidget(this.category, {Key? key}) : super(key: key);
|
||||||
@ -35,7 +37,6 @@ class CategoryDisplayWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
||||||
|
|
||||||
final _editCategoryKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String getAppBarTitle(BuildContext context) => L10().partCategory;
|
String getAppBarTitle(BuildContext context) => L10().partCategory;
|
||||||
@ -71,7 +72,9 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: FaIcon(FontAwesomeIcons.edit),
|
icon: FaIcon(FontAwesomeIcons.edit),
|
||||||
tooltip: L10().edit,
|
tooltip: L10().edit,
|
||||||
onPressed: _editCategoryDialog,
|
onPressed: () {
|
||||||
|
_editCategoryDialog(context);
|
||||||
|
},
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -80,49 +83,26 @@ class _CategoryDisplayState extends RefreshableState<CategoryDisplayWidget> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _editCategory(Map<String, String> values) async {
|
void _editCategoryDialog(BuildContext context) {
|
||||||
|
|
||||||
final bool result = await category!.update(values: values);
|
final _cat = category;
|
||||||
|
|
||||||
showSnackIcon(
|
|
||||||
result ? "Category edited" : "Category editing failed",
|
|
||||||
success: result
|
|
||||||
);
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _editCategoryDialog() {
|
|
||||||
|
|
||||||
// Cannot edit top-level category
|
// Cannot edit top-level category
|
||||||
if (category == null) {
|
if (_cat == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var _name;
|
launchApiForm(
|
||||||
var _description;
|
context,
|
||||||
|
|
||||||
showFormDialog(
|
|
||||||
L10().editCategory,
|
L10().editCategory,
|
||||||
key: _editCategoryKey,
|
_cat.url,
|
||||||
callback: () {
|
{
|
||||||
_editCategory({
|
"name": {},
|
||||||
"name": _name,
|
"description": {},
|
||||||
"description": _description
|
"parent": {},
|
||||||
});
|
|
||||||
},
|
},
|
||||||
fields: <Widget>[
|
modelData: _cat.jsondata,
|
||||||
StringField(
|
onSuccess: refresh,
|
||||||
label: L10().name,
|
|
||||||
initial: category?.name,
|
|
||||||
onSaved: (value) => _name = value
|
|
||||||
),
|
|
||||||
StringField(
|
|
||||||
label: L10().description,
|
|
||||||
initial: category?.description,
|
|
||||||
onSaved: (value) => _description = value
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,8 +188,6 @@ class _PartDisplayState extends RefreshableState<PartDetailWidget> {
|
|||||||
"link": {},
|
"link": {},
|
||||||
|
|
||||||
"category": {
|
"category": {
|
||||||
"filters": {
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Checkbox fields
|
// Checkbox fields
|
||||||
|
Loading…
x
Reference in New Issue
Block a user