mirror of
				https://github.com/inventree/inventree-app.git
				synced 2025-10-31 05:15:42 +00:00 
			
		
		
		
	API form for editing PartCategory
- Custom related field renderer function - Grab related field data from the server
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user