diff --git a/assets/release_notes.md b/assets/release_notes.md index 6460a48a..827b32ca 100644 --- a/assets/release_notes.md +++ b/assets/release_notes.md @@ -4,9 +4,13 @@ ### 0.4.4 - August 2021 --- +- Add ability to create new Part Categories +- Add ability to create new Parts +- Add ability to create new Stock Locations +- Add ability to create new Stock Items - App bar now always displays "back" button - Display "batch code" information for stock item -- Display "packagin" information for stock item +- Display "packaging" information for stock item ### 0.4.3 - August 2021 --- diff --git a/lib/api.dart b/lib/api.dart index f47feccb..62d49168 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -540,7 +540,7 @@ class InvenTreeAPI { * Perform a HTTP POST request * Returns a json object (or null if unsuccessful) */ - Future post(String url, {Map body = const {}, int expectedStatusCode=201}) async { + Future post(String url, {Map body = const {}, int? expectedStatusCode=201}) async { HttpClientRequest? request = await apiRequest(url, "POST"); @@ -763,7 +763,7 @@ class InvenTreeAPI { * Perform a HTTP GET request * Returns a json object (or null if did not complete) */ - Future get(String url, {Map params = const {}, int expectedStatusCode=200}) async { + Future get(String url, {Map params = const {}, int? expectedStatusCode=200}) async { HttpClientRequest? request = await apiRequest( url, diff --git a/lib/api_form.dart b/lib/api_form.dart index 1b18b841..14df198b 100644 --- a/lib/api_form.dart +++ b/lib/api_form.dart @@ -7,6 +7,7 @@ import 'package:inventree/api.dart'; import 'package:inventree/app_colors.dart'; import 'package:inventree/inventree/part.dart'; import 'package:inventree/inventree/stock.dart'; +import 'package:inventree/widget/dialogs.dart'; import 'package:inventree/widget/fields.dart'; import 'package:inventree/l10.dart'; @@ -153,6 +154,9 @@ class APIFormField { return _constructBoolean(); case "related field": return _constructRelatedField(); + case "float": + case "decimal": + return _constructFloatField(); case "choice": return _constructChoiceField(); default: @@ -202,6 +206,34 @@ class APIFormField { ); } + // Construct a floating point numerical input field + Widget _constructFloatField() { + + return TextFormField( + decoration: InputDecoration( + labelText: required ? label + "*" : label, + labelStyle: _labelStyle(), + helperText: helpText, + helperStyle: _helperStyle(), + hintText: placeholderText, + ), + initialValue: (value ?? 0).toString(), + keyboardType: TextInputType.numberWithOptions(signed: true, decimal: true), + validator: (value) { + + double? quantity = double.tryParse(value.toString()) ?? null; + + if (quantity == null) { + return L10().numberInvalid; + } + }, + onSaved: (val) { + data["value"] = val; + }, + ); + + } + // Construct an input for a related field Widget _constructRelatedField() { @@ -244,7 +276,16 @@ class APIFormField { onChanged: null, showClearButton: !required, itemAsString: (dynamic item) { - return item['pathstring']; + switch (model) { + case "part": + return InvenTreePart.fromJson(item).fullname; + case "partcategory": + return InvenTreePartCategory.fromJson(item).pathstring; + case "stocklocation": + return InvenTreeStockLocation.fromJson(item).pathstring; + default: + return "itemAsString not implemented for '${model}'"; + } }, dropdownBuilder: (context, item, itemAsString) { return _renderRelatedField(item, true, false); @@ -287,6 +328,22 @@ class APIFormField { } switch (model) { + case "part": + + var part = InvenTreePart.fromJson(item); + + return ListTile( + title: Text( + part.fullname, + style: TextStyle(fontWeight: selected && extended ? FontWeight.bold : FontWeight.normal) + ), + subtitle: extended ? Text( + part.description, + style: TextStyle(fontWeight: selected ? FontWeight.bold : FontWeight.normal), + ) : null, + leading: extended ? InvenTreeAPI().getImage(part.thumbnail, width: 40, height: 40) : null, + ); + case "partcategory": var cat = InvenTreePartCategory.fromJson(item); @@ -420,7 +477,7 @@ Map extractFields(APIResponse response) { * @param method is the HTTP method to use to send the form data to the server (e.g. POST / PATCH) */ -Future launchApiForm(BuildContext context, String title, String url, Map fields, {Map modelData = const {}, String method = "PATCH", Function? onSuccess, Function? onCancel}) async { +Future launchApiForm(BuildContext context, String title, String url, Map fields, {Map modelData = const {}, String method = "PATCH", Function(Map)? onSuccess, Function? onCancel}) async { var options = await InvenTreeAPI().options(url); @@ -503,6 +560,7 @@ Future launchApiForm(BuildContext context, String title, String url, Map fields; - Function? onSuccess; + Function(Map)? onSuccess; APIFormWidget( this.title, this.url, this.fields, + this.method, { Key? key, this.onSuccess, @@ -532,7 +594,7 @@ class APIFormWidget extends StatefulWidget { ) : super(key: key); @override - _APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, onSuccess); + _APIFormWidgetState createState() => _APIFormWidgetState(title, url, fields, method, onSuccess); } @@ -545,11 +607,13 @@ class _APIFormWidgetState extends State { String url; + String method; + List fields; - Function? onSuccess; + Function(Map)? onSuccess; - _APIFormWidgetState(this.title, this.url, this.fields, this.onSuccess) : super(); + _APIFormWidgetState(this.title, this.url, this.fields, this.method, this.onSuccess) : super(); List _buildForm() { @@ -579,35 +643,60 @@ class _APIFormWidgetState extends State { ); } } + + // Add divider after some widgets + switch (field.type) { + case "related field": + case "choice": + widgets.add(Divider(height: 10)); + break; + default: + break; + } + } return widgets; } + Future _submit(Map data) async { + + if (method == "POST") { + return await InvenTreeAPI().post( + url, + body: data, + expectedStatusCode: null + ); + } else { + return await InvenTreeAPI().patch( + url, + body: data, + expectedStatusCode: null + ); + } + + } + Future _save(BuildContext context) async { // Package up the form data - Map _data = {}; + Map data = {}; for (var field in fields) { dynamic value = field.value; if (value == null) { - _data[field.name] = ""; + data[field.name] = ""; } else { - _data[field.name] = value.toString(); + data[field.name] = value.toString(); } } - // TODO: Handle "POST" forms too!! - final response = await InvenTreeAPI().patch( - url, - body: _data, - ); + final response = await _submit(data); if (!response.isValid()) { - // TODO: Display an error message! + showServerError(L10().serverError, L10().responseInvalid); return; } @@ -625,11 +714,25 @@ class _APIFormWidgetState extends State { var successFunc = onSuccess; if (successFunc != null) { - successFunc(); + + // Ensure the response is a valid JSON structure + Map json = {}; + + if (response.data != null && response.data is Map) { + for (dynamic key in response.data.keys) { + json[key.toString()] = response.data[key]; + } + } + + successFunc(json); } return; case 400: // Form submission / validation error + showSnackIcon( + L10().error, + success: false + ); // Update field errors for (var field in fields) { diff --git a/lib/barcode.dart b/lib/barcode.dart index 0e305c6f..de3af32b 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -515,7 +515,7 @@ class _QRViewState extends State { return Scaffold( appBar: AppBar( - title: Text(_handler.getOverlayText(context)), + title: Text(L10().scanBarcode), ), body: Stack( children: [ diff --git a/lib/inventree/company.dart b/lib/inventree/company.dart index ebfad90d..a5d65b25 100644 --- a/lib/inventree/company.dart +++ b/lib/inventree/company.dart @@ -13,7 +13,20 @@ class InvenTreeCompany extends InvenTreeModel { String NAME = "Company"; @override - String URL = "company/"; + String get URL => "company/"; + + @override + Map formFields() { + return { + "name": {}, + "description": {}, + "website": {}, + "is_supplier": {}, + "is_manufacturer": {}, + "is_customer": {}, + "currency": {}, + }; + } InvenTreeCompany() : super(); @@ -49,7 +62,7 @@ class InvenTreeCompany extends InvenTreeModel { */ class InvenTreeSupplierPart extends InvenTreeModel { @override - String URL = "company/part/"; + String get URL => "company/part/"; Map _filters() { return { diff --git a/lib/inventree/model.dart b/lib/inventree/model.dart index 10eff329..2d18db59 100644 --- a/lib/inventree/model.dart +++ b/lib/inventree/model.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:path/path.dart' as path; import '../l10.dart'; +import '../api_form.dart'; // Paginated response object @@ -37,7 +38,7 @@ class InvenTreePageResponse { class InvenTreeModel { // Override the endpoint URL for each subclass - String URL = ""; + String get URL => ""; // Override the web URL for each subclass // Note: If the WEB_URL is the same (except for /api/) as URL then just leave blank @@ -64,6 +65,49 @@ class InvenTreeModel { } + // Fields for editing / creating this model + // Override per-model + Map formFields() { + + return {}; + } + + Future createForm(BuildContext context, String title, {Map fields=const{}, Map data=const {}, Function(dynamic)? onSuccess}) async { + + if (fields.isEmpty) { + fields = formFields(); + } + + launchApiForm( + context, + title, + URL, + fields, + modelData: data, + onSuccess: onSuccess, + method: "POST", + ); + + } + + Future editForm(BuildContext context, String title, {Map fields=const {}, Function(dynamic)? onSuccess}) async { + + if (fields.isEmpty) { + fields = formFields(); + } + + launchApiForm( + context, + title, + url, + fields, + modelData: jsondata, + onSuccess: onSuccess, + method: "PATCH" + ); + + } + // JSON data which defines this object Map jsondata = {}; @@ -155,17 +199,19 @@ class InvenTreeModel { if (!response.isValid() || response.data == null || !(response.data is Map)) { // Report error - await sentryReportMessage( - "InvenTreeModel.reload() returned invalid response", - context: { - "url": url, - "statusCode": response.statusCode.toString(), - "data": response.data?.toString() ?? "null", - "valid": response.isValid().toString(), - "error": response.error, - "errorDetail": response.errorDetail, - } - ); + if (response.statusCode > 0) { + await sentryReportMessage( + "InvenTreeModel.reload() returned invalid response", + context: { + "url": url, + "statusCode": response.statusCode.toString(), + "data": response.data?.toString() ?? "null", + "valid": response.isValid().toString(), + "error": response.error, + "errorDetail": response.errorDetail, + } + ); + } showServerError( L10().serverError, @@ -226,17 +272,19 @@ class InvenTreeModel { if (!response.isValid() || response.data == null || !(response.data is Map)) { - await sentryReportMessage( - "InvenTreeModel.get() returned invalid response", - context: { - "url": url, - "statusCode": response.statusCode.toString(), - "data": response.data?.toString() ?? "null", - "valid": response.isValid().toString(), - "error": response.error, - "errorDetail": response.errorDetail, - } - ); + if (response.statusCode > 0) { + await sentryReportMessage( + "InvenTreeModel.get() returned invalid response", + context: { + "url": url, + "statusCode": response.statusCode.toString(), + "data": response.data?.toString() ?? "null", + "valid": response.isValid().toString(), + "error": response.error, + "errorDetail": response.errorDetail, + } + ); + } showServerError( L10().serverError, @@ -267,17 +315,19 @@ class InvenTreeModel { // Invalid response returned from server if (!response.isValid() || response.data == null || !(response.data is Map)) { - await sentryReportMessage( - "InvenTreeModel.create() returned invalid response", - context: { - "url": url, - "statusCode": response.statusCode.toString(), - "data": response.data?.toString() ?? "null", - "valid": response.isValid().toString(), - "error": response.error, - "errorDetail": response.errorDetail, - } - ); + if (response.statusCode > 0) { + await sentryReportMessage( + "InvenTreeModel.create() returned invalid response", + context: { + "url": url, + "statusCode": response.statusCode.toString(), + "data": response.data?.toString() ?? "null", + "valid": response.isValid().toString(), + "error": response.error, + "errorDetail": response.errorDetail, + } + ); + } showServerError( L10().serverError, diff --git a/lib/inventree/part.dart b/lib/inventree/part.dart index 60f12996..57678467 100644 --- a/lib/inventree/part.dart +++ b/lib/inventree/part.dart @@ -14,7 +14,17 @@ class InvenTreePartCategory extends InvenTreeModel { String NAME = "PartCategory"; @override - String URL = "part/category/"; + String get URL => "part/category/"; + + @override + Map formFields() { + + return { + "name": {}, + "description": {}, + "parent": {} + }; + } @override Map defaultListFilters() { @@ -68,7 +78,7 @@ class InvenTreePartTestTemplate extends InvenTreeModel { String NAME = "PartTestTemplate"; @override - String URL = "part/test-template/"; + String get URL => "part/test-template/"; String get key => jsondata['key'] ?? ''; @@ -125,7 +135,33 @@ class InvenTreePart extends InvenTreeModel { String NAME = "Part"; @override - String URL = "part/"; + String get URL => "part/"; + + @override + Map formFields() { + return { + "name": {}, + "description": {}, + "IPN": {}, + "revision": {}, + "keywords": {}, + "link": {}, + + // Parent category + "category": { + }, + + // Checkbox fields + "active": {}, + "assembly": {}, + "component": {}, + "purchaseable": {}, + "salable": {}, + "trackable": {}, + "is_template": {}, + "virtual": {}, + }; + } @override Map defaultListFilters() { diff --git a/lib/inventree/stock.dart b/lib/inventree/stock.dart index fa7c4e8b..ac03a1fa 100644 --- a/lib/inventree/stock.dart +++ b/lib/inventree/stock.dart @@ -18,7 +18,7 @@ class InvenTreeStockItemTestResult extends InvenTreeModel { String NAME = "StockItemTestResult"; @override - String URL = "stock/test/"; + String get URL => "stock/test/"; String get key => jsondata['key'] ?? ''; @@ -103,11 +103,24 @@ class InvenTreeStockItem extends InvenTreeModel { String NAME = "StockItem"; @override - String URL = "stock/"; + String get URL => "stock/"; @override String WEB_URL = "stock/item/"; + @override + Map formFields() { + return { + "part": {}, + "location": {}, + "quantity": {}, + "status": {}, + "batch": {}, + "packaging": {}, + "link": {}, + }; + } + @override Map defaultGetFilters() { @@ -544,10 +557,19 @@ class InvenTreeStockLocation extends InvenTreeModel { String NAME = "StockLocation"; @override - String URL = "stock/location/"; + String get URL => "stock/location/"; String get pathstring => jsondata['pathstring'] ?? ''; + @override + Map formFields() { + return { + "name": {}, + "description": {}, + "parent": {}, + }; + } + String get parentpathstring { // TODO - Drive the refactor tractor through this List psplit = pathstring.split('/'); diff --git a/lib/l10n b/lib/l10n index c81c1c79..f4f7b95c 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit c81c1c79d18a7304761a30adb15090017a613157 +Subproject commit f4f7b95c28f82bfd4e398ec5bb5e35823102323c diff --git a/lib/widget/category_display.dart b/lib/widget/category_display.dart index e309b535..a86981e3 100644 --- a/lib/widget/category_display.dart +++ b/lib/widget/category_display.dart @@ -19,8 +19,6 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import '../api_form.dart'; - class CategoryDisplayWidget extends StatefulWidget { CategoryDisplayWidget(this.category, {Key? key}) : super(key: key); @@ -43,27 +41,6 @@ class _CategoryDisplayState extends RefreshableState { List actions = []; - /* - actions.add( - IconButton( - icon: FaIcon(FontAwesomeIcons.search), - onPressed: () { - - Map filters = {}; - - if (category != null) { - filters["category"] = "${category.pk}"; - } - - showSearch( - context: context, - delegate: PartSearchDelegate(context, filters: filters) - ); - } - ) - ); - */ - if ((category != null) && InvenTreeAPI().checkPermission('part_category', 'change')) { actions.add( IconButton( @@ -81,7 +58,6 @@ class _CategoryDisplayState extends RefreshableState { } void _editCategoryDialog(BuildContext context) { - final _cat = category; // Cannot edit top-level category @@ -89,17 +65,12 @@ class _CategoryDisplayState extends RefreshableState { return; } - launchApiForm( - context, - L10().editCategory, - _cat.url, - { - "name": {}, - "description": {}, - "parent": {}, - }, - modelData: _cat.jsondata, - onSuccess: refresh, + _cat.editForm( + context, + L10().editCategory, + onSuccess: (data) async { + refresh(); + } ); } @@ -206,12 +177,10 @@ class _CategoryDisplayState extends RefreshableState { label: L10().parts, ), // TODO - Add the "actions" item back in - /* BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.wrench), label: L10().actions ), - */ ] ); } @@ -242,18 +211,103 @@ class _CategoryDisplayState extends RefreshableState { return tiles; } - List actionTiles() { + Future _newCategory(BuildContext context) async { + + int pk = category?.pk ?? -1; + + InvenTreePartCategory().createForm( + context, + L10().categoryCreate, + data: { + "parent": (pk > 0) ? pk : null, + }, + onSuccess: (data) async { + + if (data.containsKey("pk")) { + var cat = InvenTreePartCategory.fromJson(data); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CategoryDisplayWidget(cat) + ) + ); + } else { + refresh(); + } + } + ); + } + + Future _newPart() async { + + int pk = category?.pk ?? -1; + + InvenTreePart().createForm( + context, + L10().partCreate, + data: { + "category": (pk > 0) ? pk : null + }, + onSuccess: (data) async { + + if (data.containsKey("pk")) { + var part = InvenTreePart.fromJson(data); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PartDetailWidget(part) + ) + ); + } + } + ); + } + + List actionTiles(BuildContext context) { List tiles = [ getCategoryDescriptionCard(extra: false), - ListTile( - title: Text(L10().actions, - style: TextStyle(fontWeight: FontWeight.bold) - ) - ) ]; - // TODO - Actions! + if (InvenTreeAPI().checkPermission('part', 'add')) { + tiles.add( + ListTile( + title: Text(L10().categoryCreate), + subtitle: Text(L10().categoryCreateDetail), + leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK), + onTap: () async { + _newCategory(context); + }, + ) + ); + + if (category != null) { + tiles.add( + ListTile( + title: Text(L10().partCreate), + subtitle: Text(L10().partCreateDetail), + leading: FaIcon(FontAwesomeIcons.shapes, color: COLOR_CLICK), + onTap: _newPart, + ) + ); + } + } + + if (tiles.length == 0) { + tiles.add( + ListTile( + title: Text( + L10().actionsNone + ), + subtitle: Text( + L10().permissionAccountDenied, + ), + leading: FaIcon(FontAwesomeIcons.userTimes), + ) + ); + } return tiles; } @@ -274,7 +328,7 @@ class _CategoryDisplayState extends RefreshableState { ); case 2: return ListView( - children: actionTiles() + children: actionTiles(context) ); default: return ListView(); diff --git a/lib/widget/company_detail.dart b/lib/widget/company_detail.dart index bccfaa1a..bc87322e 100644 --- a/lib/widget/company_detail.dart +++ b/lib/widget/company_detail.dart @@ -65,21 +65,12 @@ class _CompanyDetailState extends RefreshableState { void editCompany(BuildContext context) async { - launchApiForm( + company.editForm( context, L10().companyEdit, - company.url, - { - "name": {}, - "description": {}, - "website": {}, - "is_supplier": {}, - "is_manufacturer": {}, - "is_customer": {}, - "currency": {}, - }, - modelData: company.jsondata, - onSuccess: refresh + onSuccess: (data) async { + refresh(); + } ); } diff --git a/lib/widget/location_display.dart b/lib/widget/location_display.dart index cab65a1f..c26d8999 100644 --- a/lib/widget/location_display.dart +++ b/lib/widget/location_display.dart @@ -1,5 +1,4 @@ import 'package:inventree/api.dart'; -import 'package:inventree/api_form.dart'; import 'package:inventree/app_colors.dart'; import 'package:inventree/app_settings.dart'; import 'package:inventree/barcode.dart'; @@ -84,17 +83,12 @@ class _LocationDisplayState extends RefreshableState { return; } - launchApiForm( + _loc.editForm( context, L10().editLocation, - _loc.url, - { - "name": {}, - "description": {}, - "parent": {}, - }, - modelData: _loc.jsondata, - onSuccess: refresh + onSuccess: (data) async { + refresh(); + } ); } @@ -142,6 +136,61 @@ class _LocationDisplayState extends RefreshableState { setState(() {}); } + Future _newLocation(BuildContext context) async { + + int pk = location?.pk ?? -1; + + InvenTreeStockLocation().createForm( + context, + L10().locationCreate, + data: { + "parent": (pk > 0) ? pk : null, + }, + onSuccess: (data) async { + if (data.containsKey("pk")) { + var loc = InvenTreeStockLocation.fromJson(data); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LocationDisplayWidget(loc) + ) + ); + } + } + ); + } + + Future _newStockItem(BuildContext context) async { + + int pk = location?.pk ?? -1; + + if (pk <= 0) { + return; + } + + InvenTreeStockItem().createForm( + context, + L10().stockItemCreate, + data: { + "location": pk, + }, + onSuccess: (data) async { + if (data.containsKey("pk")) { + var item = InvenTreeStockItem.fromJson(data); + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StockDetailWidget(item) + ) + ); + } + } + ); + + } + Widget locationDescriptionCard({bool includeActions = true}) { if (location == null) { return Card( @@ -206,7 +255,6 @@ class _LocationDisplayState extends RefreshableState { icon: FaIcon(FontAwesomeIcons.boxes), label: L10().stock, ), - // TODO - Add in actions when they are written... BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.wrench), label: L10().actions, @@ -283,6 +331,32 @@ List detailTiles() { tiles.add(locationDescriptionCard(includeActions: false)); + if (InvenTreeAPI().checkPermission('stock', 'add')) { + + tiles.add( + ListTile( + title: Text(L10().locationCreate), + subtitle: Text(L10().locationCreateDetail), + leading: FaIcon(FontAwesomeIcons.sitemap, color: COLOR_CLICK), + onTap: () async { + _newLocation(context); + }, + ) + ); + + tiles.add( + ListTile( + title: Text(L10().stockItemCreate), + subtitle: Text(L10().stockItemCreateDetail), + leading: FaIcon(FontAwesomeIcons.boxes, color: COLOR_CLICK), + onTap: () async { + _newStockItem(context); + }, + ) + ); + + } + if (location != null) { // Stock adjustment actions if (InvenTreeAPI().checkPermission('stock', 'change')) { diff --git a/lib/widget/part_detail.dart b/lib/widget/part_detail.dart index 131cf921..ed2759f5 100644 --- a/lib/widget/part_detail.dart +++ b/lib/widget/part_detail.dart @@ -6,12 +6,10 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:inventree/app_colors.dart'; import 'package:inventree/l10.dart'; -import 'package:inventree/api_form.dart'; import 'package:inventree/widget/part_notes.dart'; import 'package:inventree/widget/progress.dart'; import 'package:inventree/inventree/part.dart'; import 'package:inventree/widget/category_display.dart'; -import 'package:inventree/widget/part_suppliers.dart'; import 'package:inventree/api.dart'; import 'package:inventree/widget/refreshable_state.dart'; import 'package:inventree/widget/part_image_widget.dart'; @@ -100,33 +98,12 @@ class _PartDisplayState extends RefreshableState { void _editPartDialog(BuildContext context) { - launchApiForm( - context, - L10().editPart, - part.url, - { - "name": {}, - "description": {}, - "IPN": {}, - "revision": {}, - "keywords": {}, - "link": {}, - - "category": { - }, - - // Checkbox fields - "active": {}, - "assembly": {}, - "component": {}, - "purchaseable": {}, - "salable": {}, - "trackable": {}, - "is_template": {}, - "virtual": {}, - }, - modelData: part.jsondata, - onSuccess: refresh, + part.editForm( + context, + L10().editPart, + onSuccess: (data) async { + refresh(); + } ); } @@ -305,16 +282,18 @@ class _PartDisplayState extends RefreshableState { // Tiles for an "assembly" part if (part.isAssembly) { - tiles.add( - ListTile( - title: Text(L10().billOfMaterials), - leading: FaIcon(FontAwesomeIcons.thList), - trailing: Text("${part.bomItemCount}"), - onTap: () { - // TODO - } - ) - ); + if (part.bomItemCount > 0) { + tiles.add( + ListTile( + title: Text(L10().billOfMaterials), + leading: FaIcon(FontAwesomeIcons.thList), + trailing: Text("${part.bomItemCount}"), + onTap: () { + // TODO + } + ) + ); + } if (part.building > 0) { tiles.add( @@ -331,7 +310,7 @@ class _PartDisplayState extends RefreshableState { } // Tiles for "component" part - if (part.isComponent) { + if (part.isComponent && part.usedInCount > 0) { tiles.add( ListTile( @@ -421,6 +400,19 @@ class _PartDisplayState extends RefreshableState { }, ), ); + + if (false && !part.isActive && InvenTreeAPI().checkPermission('part', 'delete')) { + tiles.add( + ListTile( + title: Text(L10().deletePart), + subtitle: Text(L10().deletePartDetail), + leading: FaIcon(FontAwesomeIcons.trashAlt, color: COLOR_DANGER), + onTap: () { + // TODO + }, + ) + ); + } return tiles; } @@ -469,12 +461,10 @@ class _PartDisplayState extends RefreshableState { label: L10().stock ), // TODO - Add part actions - /* BottomNavigationBarItem( icon: FaIcon(FontAwesomeIcons.wrench), label: L10().actions, ), - */ ] ); } diff --git a/lib/widget/part_notes.dart b/lib/widget/part_notes.dart index 1abea016..484627a0 100644 --- a/lib/widget/part_notes.dart +++ b/lib/widget/part_notes.dart @@ -7,8 +7,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:inventree/l10.dart'; -import '../api_form.dart'; - class PartNotesWidget extends StatefulWidget { @@ -46,17 +44,15 @@ class _PartNotesState extends RefreshableState { icon: FaIcon(FontAwesomeIcons.edit), tooltip: L10().edit, onPressed: () { - launchApiForm( + part.editForm( context, L10().editNotes, - part.url, - { + fields: { "notes": { "multiline": true, } }, - modelData: part.jsondata, - onSuccess: () async { + onSuccess: (data) async { refresh(); } ); diff --git a/lib/widget/stock_detail.dart b/lib/widget/stock_detail.dart index 2f7e6103..dd062cd6 100644 --- a/lib/widget/stock_detail.dart +++ b/lib/widget/stock_detail.dart @@ -22,8 +22,6 @@ import 'package:inventree/api.dart'; import 'package:dropdown_search/dropdown_search.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import '../api_form.dart'; - class StockDetailWidget extends StatefulWidget { StockDetailWidget(this.item, {Key? key}) : super(key: key); @@ -109,18 +107,20 @@ class _StockItemDisplayState extends RefreshableState { void _editStockItem(BuildContext context) async { - launchApiForm( + var fields = InvenTreeStockItem().formFields(); + + // Some fields we don't want to edit! + fields.remove("part"); + fields.remove("quantity"); + fields.remove("location"); + + item.editForm( context, L10().editItem, - item.url, - { - "status": {}, - "batch": {}, - "packaging": {}, - "link": {}, - }, - modelData: item.jsondata, - onSuccess: refresh + fields: fields, + onSuccess: (data) async { + refresh(); + } ); } diff --git a/lib/widget/stock_notes.dart b/lib/widget/stock_notes.dart index 1034605e..130f863c 100644 --- a/lib/widget/stock_notes.dart +++ b/lib/widget/stock_notes.dart @@ -8,7 +8,6 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:inventree/l10.dart'; import '../api.dart'; -import '../api_form.dart'; class StockNotesWidget extends StatefulWidget { @@ -46,19 +45,17 @@ class _StockNotesState extends RefreshableState { icon: FaIcon(FontAwesomeIcons.edit), tooltip: L10().edit, onPressed: () { - launchApiForm( - context, - L10().editNotes, - item.url, - { - "notes": { - "multiline": true, - } - }, - modelData: item.jsondata, - onSuccess: () { - refresh(); + item.editForm( + context, + L10().editNotes, + fields: { + "notes": { + "multiline": true, } + }, + onSuccess: (data) async { + refresh(); + } ); } ) diff --git a/lib/widget/submit_feedback.dart b/lib/widget/submit_feedback.dart index a4f60787..b335d6f0 100644 --- a/lib/widget/submit_feedback.dart +++ b/lib/widget/submit_feedback.dart @@ -1,6 +1,5 @@ -import 'package:email_validator/email_validator.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';