From: Hamatoma Date: Tue, 27 Oct 2020 09:39:01 +0000 (+0100) Subject: daily work: X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=dfabac54d2554526693f9c5ef38b5154199b33a1;p=flutter_bones.git daily work: * error message on network error * list redraw after create and change --- diff --git a/data/rest/configuration.yaml b/data/rest/configuration.yaml index 2b6816a..33b419a 100644 --- a/data/rest/configuration.yaml +++ b/data/rest/configuration.yaml @@ -27,4 +27,4 @@ modules: - name: list type: list sql: "SELECT * from configuration - WHERE configuration_name like :configuration_name AND configuration_scope like :configuration_scope AND configuration_scope like :configuration_scope;" + WHERE configuration_property like :configuration_property AND configuration_scope like :configuration_scope;" diff --git a/data/rest/user.yaml b/data/rest/user.yaml index 3ac76cd..ab0ff4a 100644 --- a/data/rest/user.yaml +++ b/data/rest/user.yaml @@ -33,4 +33,4 @@ modules: - name: list type: list sql: "SELECT * from user - WHERE user_name like :user_name AND user_role like :user_role AND user_role like :user_role;" + WHERE user_name like :user_name AND (:user_role is null or user_role=:user_role);" diff --git a/lib/app.dart b/lib/app.dart index cb4cf07..1fe35e1 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -2,9 +2,12 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; import 'src/helper/settings.dart'; +import 'src/page/demo_page.dart'; import 'src/page/login_page.dart'; import 'src/page/role/role_create_page.dart'; import 'src/page/role/role_list_page.dart'; +import 'src/page/user/user_create_page.dart'; +import 'src/page/user/user_list_page.dart'; import 'src/private/bsettings.dart'; class BoneApp extends StatefulWidget { @@ -32,7 +35,7 @@ class BoneAppState extends State { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - initialRoute: '/role/list', + initialRoute: '/configuration/list', onGenerateRoute: _getRoute, ); } @@ -42,19 +45,22 @@ Route _getRoute(RouteSettings settings) { MaterialPageRoute route; StatefulWidget page; switch (settings.name) { + case '/demo': + page = DemoPage(BSettings.lastInstance.pageData); + break; case '/role/list': page = RoleListPage(BSettings.lastInstance.pageData); break; case '/role/create': page = RoleCreatePage(BSettings.lastInstance.pageData); break; - /* case '/user/list': page = UserListPage(BSettings.lastInstance.pageData); break; case '/user/create': page = UserCreatePage(BSettings.lastInstance.pageData); break; + /* case '/configuration/list': page = ConfigurationListPage(BSettings.lastInstance.pageData); break; diff --git a/lib/src/helper/string_helper.dart b/lib/src/helper/string_helper.dart index 298f70c..ba9224e 100644 --- a/lib/src/helper/string_helper.dart +++ b/lib/src/helper/string_helper.dart @@ -1,19 +1,46 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:intl/intl.dart'; + import '../model/model_types.dart'; class StringHelper { - static final regExpTrue = RegExp(r'^(true|yes)$', caseSensitive: false); - static final regExpFalse = RegExp(r'^(false|no)$', caseSensitive: false); + static final regExpTrue = RegExp(r'^(true|yes|t)$', caseSensitive: false); + static final regExpFalse = RegExp(r'^(false|no|f)$', caseSensitive: false); static const locale = 'de_DE'; + /// Convert [value] into a string for storing in a database respecting [dataType]. + static String asDatabaseString(dynamic value, DataType dataType) { + if (value == null) { + value = 'NULL'; + } else { + switch (dataType) { + case DataType.dateTime: + value = DateFormat('yyyy-MM-dd HH:mm:ss').format(value); + break; + case DataType.date: + value = DateFormat('yyyy-MM-dd').format(value); + break; + case DataType.bool: + value = value ? 'T' : 'F'; + break; + default: + value = value.toString(); + break; + } + } + return value; + } + /// Converts any [data] to a string. - /// [withSeconds]: only used for DateTime data: true: seconds will be shown - /// [sortable]: only used for DateTime data: true: the date is sortable (year first) + /// [withSeconds]: only used for DateTime data: true: seconds will be shown. + /// [sortable]: only used for DateTime data: true: the date is sortable (year first). + /// [nullString]: is the result if [data] is null static String asString(dynamic data, - {bool withSeconds = true, bool sortable = true}) { + {bool withSeconds = true, bool sortable = true, String nullString}) { String rc; - if (data is String) { + if (data == null) { + rc = nullString; + } else if (data is String) { rc = data; } else if (data is DateTime) { rc = dateTimeToString(data, withSeconds: withSeconds, sortable: sortable); @@ -111,27 +138,4 @@ class StringHelper { } return rc; } - - /// Convert [value] into a string for storing in a database respecting [dataType]. - static String asDatabaseString(dynamic value, DataType dataType) { - if (value == null) { - value = 'NULL'; - } else { - switch (dataType) { - case DataType.dateTime: - value = DateFormat('yyyy-MM-dd HH:mm:ss').format(value); - break; - case DataType.date: - value = DateFormat('yyyy-MM-dd').format(value); - break; - case DataType.bool: - value = value ? 'T' : 'F'; - break; - default: - value = value.toString(); - break; - } - } - return value; - } } diff --git a/lib/src/model/all_db_fields_model.dart b/lib/src/model/all_db_fields_model.dart index 361395d..f1c6eb2 100644 --- a/lib/src/model/all_db_fields_model.dart +++ b/lib/src/model/all_db_fields_model.dart @@ -45,7 +45,7 @@ class AllDbFieldsModel extends WidgetModel { if (col.hasOption('doStore') || (!col.hasOption('hidden') && (!isCreatePage || !col.hasOption('primary')))) { - final field = DbReferenceModel.direct(section, page, col, logger); + final field = DbReferenceModel.fromColumn(section, page, col, logger); page.addField(field); page.widgets.add(field); } diff --git a/lib/src/model/column_model.dart b/lib/src/model/column_model.dart index cf25439..0a119fb 100644 --- a/lib/src/model/column_model.dart +++ b/lib/src/model/column_model.dart @@ -1,45 +1,53 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:meta/meta.dart'; + import 'model_base.dart'; import 'model_types.dart'; import 'table_model.dart'; import 'widget_model.dart'; +import 'combo_base_model.dart'; /// Describes a column of a database table. -class ColumnModel extends WidgetModel { +class ColumnModel extends ComboBaseModel { static final regExprOptions = RegExp( r'^(undef|readonly|disabled|doStore|hidden|null|notnull|password|primary|required|unique)$'); - String name; - String label; - String toolTip; - DataType dataType; + static final regExprListDbOption = + RegExp(r'^\w+\.\w+;\w+ \w+(?:;(:? ?:\w+=[^ ]*?)+)?$'); int size; int rows; - List options; final TableModel table; final Map map; String foreignKey; /// A constructor used in the parser. ColumnModel(this.table, this.map, BaseLogger logger) - : super(null, null, WidgetModelType.column, logger); + : super(null, null, map, WidgetModelType.column, logger); /// A constructor for columns created by the program. - ColumnModel.raw( - {@required this.name, + ColumnModel.direct( + {@required String name, @required this.table, - @required this.label, - @required this.dataType, - @required this.options, + @required String label, + @required DataType dataType, + @required List options, + String toolTip, + String listOption, + ComboboxListType listType, this.size, this.map, + List texts, + List values, BaseLogger logger}) - : super(null, null, WidgetModelType.column, logger); + : super.direct(section: null, page: null, + map: null, + name: name, label: label, toolTip: toolTip, texts: texts, values: values, + listOption: listOption, listType: listType, options: options, + widgetType: WidgetModelType.column, logger: logger); /// Dumps the instance into a [StringBuffer] StringBuffer dump(StringBuffer stringBuffer) { stringBuffer.write( - ' column $name: $dataType "$label" options: ${options.join(' ')}\n'); + ' column $name: $dataType "$label" options: ${options?.join(' ')}\n'); return stringBuffer; } @@ -49,7 +57,7 @@ class ColumnModel extends WidgetModel { /// Tests whether a given [option] is part of [options]. bool hasOption(String option) { - bool rc = options.contains(option); + bool rc = options?.contains(option) ?? false; return rc; } @@ -58,16 +66,14 @@ class ColumnModel extends WidgetModel { name = parseString('column', map, required: true); checkSuperfluousAttributes( map, - 'column dataType foreignKey label options rows size tooTip widgetType' + 'column dataType foreignKey label listOption listType options rows size texts tooTip values widgetType' .split(' ')); + super.parse(); dataType = parseEnum('dataType', map, DataType.values, required: true); - label = parseString('label', map, required: false); - toolTip = parseString('toolTip', map, required: false); size = parseInt('size', map, required: dataType == DataType.string); rows = parseInt('rows', map); - options = parseOptions('options', map); checkOptionsByRegExpr(options, regExprOptions); if (options.contains('primary')) { if (!options.contains('notnull')) { diff --git a/lib/src/model/combo_base_model.dart b/lib/src/model/combo_base_model.dart new file mode 100644 index 0000000..5a11f57 --- /dev/null +++ b/lib/src/model/combo_base_model.dart @@ -0,0 +1,109 @@ +import 'package:dart_bones/dart_bones.dart'; + +import 'field_model.dart'; +import 'model_types.dart'; +import 'page_model.dart'; +import 'section_model.dart'; +import 'widget_model.dart'; + +/// A base class for combobox items like ComboboxModel, ColumnModel and DbReferenceModel. +abstract class ComboBaseModel extends FieldModel { + static final regExprListDbOption = + RegExp(r'^\w+\.\w+;\w+ \w+(?:;(:? ?:\w+=[^ ]*?)+)?$'); + static final regExprListConfiguration = RegExp(r'^scope:\w+$'); + List texts; + List values; + String listOption; + ComboboxListType listType; + + ComboBaseModel(SectionModel section, PageModel page, Map map, + WidgetModelType widgetType, BaseLogger logger) + : super(section, page, map, widgetType, logger); + + ComboBaseModel.direct( + {SectionModel section, + PageModel page, + Map map, + String name, + String label, + String toolTip, + DataType dataType, + List options, + this.texts, + this.values, + this.listOption, + this.listType, + WidgetModelType widgetType, + BaseLogger logger}) + : super.direct(section, page, map, widgetType, name, label, toolTip, + dataType, options, logger); + +/* + SectionModel section, + PageModel page, + this.map, + WidgetModelType fieldModelType, + String name, + String label, + String toolTip, + DataType dataType, + List options, + BaseLogger logger, + + */ + + /// Parses the map and stores the data in the instance. + void parse() { + super.parse(); + texts = parseStringList('texts', map); + listType = + parseEnum('listType', map, ComboboxListType.values); + listOption = parseString('listOption', map) ?? ''; + values = parseValueList('values', map, dataType); + switch (listType) { + case ComboboxListType.undef: + logger.error( + 'wrong value "undef" in attribute "listType" in ${fullName()}'); + break; + case ComboboxListType.explicite: + if (texts.isEmpty) { + logger.error('missing attribute "texts" in ${fullName()}'); + listType = ComboboxListType.undef; + } else if (values.isNotEmpty && texts.length != values.length) { + logger.error( + 'different sizes of "Texts" and "values": ${texts.length}/${values.length}'); + listType = ComboboxListType.undef; + } + break; + case ComboboxListType.configuration: + if (regExprListConfiguration.firstMatch(listOption) == null) { + logger.error( + 'wrong syntax in "listOption". Example: "scope:userstate"'); + listType = ComboboxListType.undef; + } + break; + case ComboboxListType.dbColumn: + if (regExprListDbOption.firstMatch(listOption) == null) { + logger.error( + 'wrong syntax in "listOption". Example: "role.list;role_name role_id;:role_name=%"'); + listType = ComboboxListType.undef; + } + break; + } + } +} + +class ComboboxData { + final List texts; + final List values; + WaitState waitState; + + ComboboxData(this.texts, this.values, [this.waitState = WaitState.undef]); +} + +enum ComboboxListType { + configuration, + dbColumn, + explicite, + undef, +} diff --git a/lib/src/model/combobox_model.dart b/lib/src/model/combobox_model.dart index 5a3dc47..6a27d4c 100644 --- a/lib/src/model/combobox_model.dart +++ b/lib/src/model/combobox_model.dart @@ -1,20 +1,16 @@ import 'package:dart_bones/dart_bones.dart'; -import 'field_model.dart'; +import 'combo_base_model.dart'; import 'page_model.dart'; import 'section_model.dart'; import 'widget_model.dart'; /// Describes a combobox widget. -class ComboboxModel extends FieldModel { +class ComboboxModel extends ComboBaseModel { static final regExprOptions = RegExp(r'^(readonly|disabled|required|undef)$'); - List texts; - List values; - - final Map map; ComboboxModel( - SectionModel section, PageModel page, this.map, BaseLogger logger) + SectionModel section, PageModel page, Map map, BaseLogger logger) : super(section, page, map, WidgetModelType.combobox, logger); /// Dumps the instance into a [StringBuffer] @@ -28,12 +24,9 @@ class ComboboxModel extends FieldModel { void parse() { checkSuperfluousAttributes( map, - 'name label dataType filterType options texts toolTip widgetType values' + 'name label dataType filterType listOption listType options texts toolTip values widgetType' .split(' ')); super.parse(); - texts = parseStringList('texts', map); - values = parseValueList('values', map, dataType); - options = parseOptions('options', map); checkOptionsByRegExpr(options, regExprOptions); } } diff --git a/lib/src/model/db_reference_model.dart b/lib/src/model/db_reference_model.dart index 785e0db..bfa3a36 100644 --- a/lib/src/model/db_reference_model.dart +++ b/lib/src/model/db_reference_model.dart @@ -1,13 +1,13 @@ import 'package:dart_bones/dart_bones.dart'; import 'column_model.dart'; -import 'field_model.dart'; +import 'combo_base_model.dart'; import 'page_model.dart'; import 'section_model.dart'; import 'widget_model.dart'; /// Describes a form text field widget. -class DbReferenceModel extends FieldModel { +class DbReferenceModel extends ComboBaseModel { static final regExprOptions = RegExp(r'^(readonly|disabled|required|password|unique|combobox|undef)$'); int maxSize; @@ -21,23 +21,27 @@ class DbReferenceModel extends FieldModel { : super(section, page, map, WidgetModelType.dbReference, logger); /// A constructor without map parsing. - DbReferenceModel.direct(SectionModel section, PageModel page, + DbReferenceModel.fromColumn(SectionModel section, PageModel page, ColumnModel column, BaseLogger logger) : super.direct( - section, - page, - null, - WidgetModelType.dbReference, - column.name, - column.label, - column.toolTip, - column.dataType, - column.options, - logger) { + section: section, + page: page, + map: null, + widgetType: WidgetModelType.dbReference, + name: column.name, + label: column.label, + toolTip: column.toolTip, + dataType: column.dataType, + options: column.options = [], + texts: column.texts, + values: column.values, + listOption: column.listOption, + listType: column.listType, + logger: logger) { this.column = column; maxSize = column.size; rows = column.rows; - if (column.hasOption('primary') && ! column.hasOption('readonly')){ + if (column.hasOption('primary') && !column.hasOption('readonly')) { options.add('readonly'); } } @@ -54,7 +58,7 @@ class DbReferenceModel extends FieldModel { super.parse(); checkSuperfluousAttributes( map, - 'column label maxSize name options rows toolTip value widgetType' + 'column label maxSize listOption listType name options rows toolTip texts values' .split(' ')); final columnName = parseString('column', map, required: true); column = page.module.getColumn(columnName, this); diff --git a/lib/src/model/field_model.dart b/lib/src/model/field_model.dart index d388646..36ff059 100644 --- a/lib/src/model/field_model.dart +++ b/lib/src/model/field_model.dart @@ -61,17 +61,20 @@ abstract class FieldModel extends WidgetModel { /// Tests whether a given [option] is part of [options]. bool hasOption(String option) { + if (options == null){ + return false; + } bool rc = options.contains(option); return rc; } /// Parses the map and stores the data in the instance. void parse() { - name = parseString('name', map, required: true); + name = parseString(widgetModelType == WidgetModelType.column ? 'column' : 'name', map, required: true); label = parseString('label', map, required: false); toolTip = parseString('toolTip', map, required: false); filterType = parseEnum('filterType', map, FilterType.values); - + options = parseOptions('options', map); dataType = parseEnum('dataType', map, DataType.values); if (dataType == null) { switch (widgetModelType) { diff --git a/lib/src/model/model_base.dart b/lib/src/model/model_base.dart index f0e775a..d70cc28 100644 --- a/lib/src/model/model_base.dart +++ b/lib/src/model/model_base.dart @@ -100,7 +100,7 @@ abstract class ModelBase { if (map.containsKey(key)) { final value = map[key]; if (value.runtimeType == String && value.isNotEmpty) { - rc = value.split(';'); + rc = value.replaceAll(';', ' ').replaceAll(',', ' ').split(' '); } else { logger.error( 'wrong datatype of options ${value.runtimeType} in ${fullName()}'); @@ -151,30 +151,35 @@ abstract class ModelBase { /// This entry is split by the delimiter given at index 0. /// Example: ";a;b" returns ['a', 'b']. /// An error is logged if [required] is true and the map does not contain the key. - List parseValueList( - String key, Map map, DataType dataType, + List parseValueList(String key, Map map, DataType dataType, {bool required = false}) { if (dataType == null) { dataType = DataType.string; } - List rc; - if (ModelBase.isList(map[key])) { - rc = map[key]; - } else { - final strings = parseStringList(key, map, required: required).toList(); - if (strings.isNotEmpty) { - rc = strings.map((item) { - var rc2; - switch (dataType) { - case DataType.int: - rc2 = StringUtils.asInt(item); - break; - default: - logger.error('unknown dataType in parseValueList()'); - rc2 = item; - } - return rc2; - }); + List rc; + if (map.containsKey(key)) { + if (ModelBase.isList(map[key])) { + rc = map[key]; + } else { + List strings = + parseStringList(key, map, required: required).toList(); + if (strings.isNotEmpty) { + rc = strings.map((item) { + var rc2; + if (item.isNotEmpty) { + switch (dataType) { + case DataType.int: + case DataType.reference: + rc2 = StringUtils.asInt(item); + break; + default: + logger.error('unknown dataType in parseValueList()'); + rc2 = item; + } + } + return rc2; + }).toList(); + } } } return rc; diff --git a/lib/src/model/model_types.dart b/lib/src/model/model_types.dart index 4874a31..c7f29ad 100644 --- a/lib/src/model/model_types.dart +++ b/lib/src/model/model_types.dart @@ -17,3 +17,4 @@ enum FilterType { equals, pattern, } +enum WaitState { undef, initial, waiting, ready } diff --git a/lib/src/model/page_model.dart b/lib/src/model/page_model.dart index 6df201e..eeb1416 100644 --- a/lib/src/model/page_model.dart +++ b/lib/src/model/page_model.dart @@ -1,4 +1,5 @@ import 'package:dart_bones/dart_bones.dart'; + import 'button_model.dart'; import 'field_model.dart'; import 'model_base.dart'; @@ -107,8 +108,8 @@ class PageModel extends ModelBase { /// Parses the map and stores the data in the instance. void parse() { name = parseString('page', map, required: true); - checkSuperfluousAttributes( - map, 'options page pageType sections tableColumns tableTitles'.split(' ')); + checkSuperfluousAttributes(map, + 'options page pageType sections tableColumns tableTitles'.split(' ')); pageModelType = parseEnum( 'pageType', map, PageModelType.values, required: true); @@ -127,7 +128,8 @@ class PageModel extends ModelBase { tableTitles = parseStringList('tableTitles', map); tableColumns = parseString('tableColumns', map)?.split(' '); if (pageModelType != PageModelType.list && - (tableTitles != null || tableColumns != null)) { + (tableTitles != null && tableTitles.length > 0 || + tableColumns != null && tableColumns.length > 0)) { logger.error( 'tableTitles and tableColumns are only meaningful in list pages: ${fullName()}'); } diff --git a/lib/src/model/standard/configuration_model.dart b/lib/src/model/standard/configuration_model.dart index 2351d9d..16abd36 100644 --- a/lib/src/model/standard/configuration_model.dart +++ b/lib/src/model/standard/configuration_model.dart @@ -1,4 +1,5 @@ import 'package:dart_bones/dart_bones.dart'; + import '../module_model.dart'; /// @@ -23,7 +24,7 @@ class ConfigurationModel extends ModuleModel { 'dataType': 'string', 'label': 'Bereich', 'size': 64, - 'options': 'unique;notnull', + 'options': 'unique notnull', }, { 'column': 'configuration_property', @@ -89,22 +90,25 @@ class ConfigurationModel extends ModuleModel { { "page": "list", "pageType": "list", - "tableColumns": "configuration_id configuration_scope configuration_property configuration_order configuration_value configuration_type", + "tableColumns": + "configuration_id configuration_scope configuration_property configuration_order configuration_value configuration_type", "tableTitles": ";Id;Bereich;Eigenschaft;Reihe;Wert;Typ", "sections": [ { "sectionType": "filterPanel", "children": [ { - "name": "configuration_name", + "name": "configuration_scope", + "label": "Bereich", "widgetType": "textField", "filterType": "pattern", + //"options": 'undef', }, { - "name": "configuration_scope", - "widgetType": "combobox", - "filterType": "equals", - "options": 'undef', + "name": "configuration_property", + "label": "Eigenschaft", + "widgetType": "textField", + "filterType": "pattern", }, ] } diff --git a/lib/src/model/standard/role_model.dart b/lib/src/model/standard/role_model.dart index 3a1f642..87c0ad3 100644 --- a/lib/src/model/standard/role_model.dart +++ b/lib/src/model/standard/role_model.dart @@ -20,7 +20,7 @@ class RoleModel extends ModuleModel { 'dataType': 'string', 'label': 'Rolle', 'size': 32, - 'options': 'unique;notnull', + 'options': 'unique notnull', }, { 'column': 'role_priority', @@ -67,7 +67,7 @@ class RoleModel extends ModuleModel { { "page": "list", "pageType": "list", - "tableColumns": "role_id role_name role_prio", + "tableColumns": "role_id role_name role_priority", "tableTitles": ";Id;Rolle;Priorität", "sections": [ { diff --git a/lib/src/model/standard/user_model.dart b/lib/src/model/standard/user_model.dart index 3aa5c50..4809366 100644 --- a/lib/src/model/standard/user_model.dart +++ b/lib/src/model/standard/user_model.dart @@ -1,4 +1,5 @@ import 'package:dart_bones/dart_bones.dart'; + import '../module_model.dart'; class UserModel extends ModuleModel { @@ -19,7 +20,7 @@ class UserModel extends ModuleModel { 'dataType': 'string', 'label': 'User', 'size': 64, - 'options': 'unique;notnull', + 'options': 'unique notnull', }, { 'column': 'user_displayname', @@ -38,7 +39,7 @@ class UserModel extends ModuleModel { { 'column': 'user_password', 'dataType': 'string', - 'label': 'User', + 'label': 'Passwort', 'size': 128, 'options': 'password', }, @@ -47,7 +48,11 @@ class UserModel extends ModuleModel { 'dataType': 'reference', 'label': 'Role', 'foreignKey': 'role.role_id', - 'widgetType': 'combobox', + 'listType': 'explicite', + 'texts': ';-;Administrator;Benutzer;Gast;Verwalter', + 'values': ';;1;2;3;4', + //"listType": "dbColumn", + //"listOption": "role.list;role_name role_id;:role_name=%", }, ] }, @@ -84,7 +89,8 @@ class UserModel extends ModuleModel { { "page": "list", "pageType": "list", - "tableColumns": "user_id user_name user_displayname user_email user_role", + "tableColumns": + "user_id user_name user_displayname user_email user_role", "tableTitles": ";Id;Name;Anzeigename;EMail;Rolle", "sections": [ { @@ -94,11 +100,18 @@ class UserModel extends ModuleModel { "widgetType": "textField", "filterType": "pattern", "name": "user_name", + "label": "Name", }, { "widgetType": "combobox", "filterType": "equals", "name": "user_role", + "label": "Rolle", + "listType": "explicite", + "texts": ";Alle Rollen;Administrator;Benutzer;Gast;Verwalter", + "values": ";;1;2;3;4" + //"listType": "dbColumn", + //"listOption": "role.list;role_name role_id;:role_name=%", } ] } diff --git a/lib/src/model/table_model.dart b/lib/src/model/table_model.dart index 1ca2002..c592f8b 100644 --- a/lib/src/model/table_model.dart +++ b/lib/src/model/table_model.dart @@ -30,6 +30,22 @@ class TableModel extends ModelBase { } } + /// Adds a column [name] if it does not exist with [label] and [dataType]. + void addIfMissing( + String name, String label, DataType dataType, List options, + [int size]) { + if (columnByName(name, required: false) == null) { + addColumn(ColumnModel.direct( + name: name, + table: this, + label: label, + dataType: dataType, + options: options ?? [], + size: size, + logger: logger)); + } + } + /// Returns a column by [name] or null if not found. ColumnModel columnByName(String name, {bool required = true}) { ColumnModel rc = columns.firstWhere((element) => element.name == name, @@ -70,35 +86,19 @@ class TableModel extends ModelBase { } } checkOptionsByRegExpr(options, regExprOptions); - _addIfMissing( + addIfMissing( '${name}_createdat', 'Erzeugt', DataType.dateTime, ['hidden', 'null']); - _addIfMissing( + addIfMissing( '${name}_createdby', 'Erzeugt von', DataType.string, ['hidden'], 16); - _addIfMissing( + addIfMissing( '${name}_changedat', 'Geändert', DataType.dateTime, ['hidden', 'null']); - _addIfMissing( + addIfMissing( '${name}_changedby', 'Geändert von', DataType.string, ['hidden'], 16); } @override widgetName() => name; - /// Adds a column [name] if it does not exist with [label] and [dataType]. - void _addIfMissing( - String name, String label, DataType dataType, List options, - [int size]) { - if (columnByName(name, required: false) == null) { - addColumn(ColumnModel.raw( - name: name, - table: this, - label: label, - dataType: dataType, - options: options ?? [], - size: size, - logger: logger)); - } - } - /// Returns a list of tables constructed by the Json like [list]. static void parseList( ModuleModel module, List list, BaseLogger logger) { diff --git a/lib/src/page/application_data.dart b/lib/src/page/application_data.dart index 75b6e58..dd28cc6 100644 --- a/lib/src/page/application_data.dart +++ b/lib/src/page/application_data.dart @@ -15,8 +15,13 @@ class ApplicationData { final Persistence persistence; String currentUser; int currentRoleId; + /// for unittests: dynamic lastModuleState; + + /// : + final _lastErrorMessageMap = {}; + @protected final callerStack = []; @@ -29,20 +34,37 @@ class ApplicationData { currentUser = 'Gast'; currentRoleId = 100; } + /// Enforces a redraw of the caller of the current page. /// [reason] specifies why the redraw is needed and [customString] /// is an additional info for the reason. - void callerRedraw(RedrawReason reason, [String customString]){ - if (callerStack.isNotEmpty && callerStack.last != null){ - callerStack.last.redraw(reason, customString); + void callerRedraw(RedrawReason reason, [String customString]) { + if (callerStack.isNotEmpty && callerStack.last != null) { + callerStack.last.redraw(reason, customString: customString); } } - void pushCaller(PageControllerBones controller){ + + void pushCaller(PageControllerBones controller) { callerStack.add(controller); } - void popCaller(){ - if (callerStack.isNotEmpty){ + + void popCaller() { + if (callerStack.isNotEmpty) { callerStack.removeLast(); } } + + /// Sets the last severe error of the [page]. [message] may be null (no error). + void setLastErrorMessage(String page, String message) { + _lastErrorMessageMap[page] = message; + } + + /// Returns the last severe error of the [page] or null if no error is set. + String lastErrorMessage(String page) { + final rc = _lastErrorMessageMap.containsKey(page) + ? _lastErrorMessageMap[page] + : null; + _lastErrorMessageMap[page] = null; + return rc; + } } diff --git a/lib/src/page/configuration/configuration_change_page.dart b/lib/src/page/configuration/configuration_change_page.dart index e8c0289..2b35b87 100644 --- a/lib/src/page/configuration/configuration_change_page.dart +++ b/lib/src/page/configuration/configuration_change_page.dart @@ -18,7 +18,8 @@ class ConfigurationChangePage extends StatefulWidget { @override ConfigurationChangePageState createState() { - final rc = ConfigurationChangePageState(primaryId, applicationData, initialRow); + final rc = + ConfigurationChangePageState(primaryId, applicationData, initialRow); /// for unittests: applicationData.lastModuleState = rc; @@ -31,24 +32,36 @@ class ConfigurationChangePageState extends State { final int primaryId; final Map initialRow; final GlobalKey _formKey = - GlobalKey(debugLabel: 'configuration_change'); + GlobalKey(debugLabel: 'configuration_change'); ConfigurationController controller; - ConfigurationChangePageState(this.primaryId, this.applicationData, this.initialRow); + ConfigurationChangePageState(this.primaryId, this.applicationData, + this.initialRow); @override Widget build(BuildContext context) { if (controller == null) { controller = ConfigurationController( - _formKey, this, 'change', context, applicationData, - redrawCallback: (RedrawReason reason, String customString) => - setState(() => null)); + _formKey, this, 'change', context, applicationData, redrawCallback: + (RedrawReason reason, + {String customString, + RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); controller.initialize(); } // controller.buildWidgetList() is called in editForm return Scaffold( - appBar: applicationData.appBarBuilder('Rolle ändern'), + appBar: applicationData.appBarBuilder('Konfiguration ändern'), drawer: applicationData.drawerBuilder(context), body: EditForm.editForm( key: _formKey, @@ -58,6 +71,7 @@ class ConfigurationChangePageState extends State { initialRow: initialRow, )); } + void dispose() { controller.dispose(); super.dispose(); diff --git a/lib/src/page/configuration/configuration_controller.dart b/lib/src/page/configuration/configuration_controller.dart index 7b9eb09..76c7e89 100644 --- a/lib/src/page/configuration/configuration_controller.dart +++ b/lib/src/page/configuration/configuration_controller.dart @@ -2,22 +2,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; import '../../model/standard/configuration_model.dart'; -import 'configuration_change_page.dart'; import '../../widget/page_controller_bones.dart'; +import 'configuration_change_page.dart'; class ConfigurationController extends PageControllerBones { /// Controller for a page named [pageName]. - ConfigurationController(GlobalKey formKey, State parent, - String pageName, BuildContext context, ApplicationData applicationData, + ConfigurationController( + GlobalKey formKey, + State parent, + String pageName, + BuildContext context, + ApplicationData applicationData, {Function redrawCallback}) - : super(formKey, parent, ConfigurationModel(Settings().logger), pageName, context, - applicationData, redrawCallback) { + : super(formKey, parent, ConfigurationModel(Settings().logger), pageName, + context, applicationData, redrawCallback) { moduleModel.parse(); } + @override void startChange(int id, Map row) { applicationData.pushCaller(this); - Navigator.push(context, - MaterialPageRoute(builder: (context) => ConfigurationChangePage(id, applicationData, row))); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ConfigurationChangePage(id, applicationData, row))); } } diff --git a/lib/src/page/configuration/configuration_create_page.dart b/lib/src/page/configuration/configuration_create_page.dart index 331a57b..23a7383 100644 --- a/lib/src/page/configuration/configuration_create_page.dart +++ b/lib/src/page/configuration/configuration_create_page.dart @@ -33,13 +33,13 @@ class ConfigurationCreatePageState extends State { @override Widget build(BuildContext context) { if (controller == null) { - controller = - ConfigurationController(_formKey, this, 'create', context, applicationData); + controller = ConfigurationController( + _formKey, this, 'create', context, applicationData); controller.initialize(); } controller.buildWidgetList(); return Scaffold( - appBar: applicationData.appBarBuilder('Neue Rolle'), + appBar: applicationData.appBarBuilder('Neue Konfiguration'), drawer: applicationData.drawerBuilder(context), body: EditForm.editForm( key: _formKey, diff --git a/lib/src/page/configuration/configuration_list_page.dart b/lib/src/page/configuration/configuration_list_page.dart index e42bab4..abb2925 100644 --- a/lib/src/page/configuration/configuration_list_page.dart +++ b/lib/src/page/configuration/configuration_list_page.dart @@ -15,6 +15,7 @@ class ConfigurationListPage extends StatefulWidget { ConfigurationListPageState createState() { // ConfigurationListPageState.setPageData(pageData); final rc = ConfigurationListPageState(applicationData); + /// for unittests: applicationData.lastModuleState = rc; return rc; @@ -36,15 +37,32 @@ class ConfigurationListPageState extends State { Widget build(BuildContext context) { if (controller == null) { controller = ConfigurationController( - _formKey, this, 'list', context, applicationData, - redrawCallback: (RedrawReason reason, String customString) => setState(() => null)); + _formKey, this, 'list', context, applicationData, redrawCallback: + (RedrawReason reason, + {String customString, + RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.fetchList: + controller.buildRows(); + break; + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); controller.initialize(); controller.buildWidgetList(); + controller.buildRows(); + } else { + controller = controller; } filters ??= controller.filterSet(pageName: 'list'); - controller.buildRows(); return Scaffold( - appBar: applicationData.appBarBuilder('Rollen'), + appBar: applicationData.appBarBuilder('Konfigurationen'), drawer: applicationData.drawerBuilder(context), body: ListForm.listForm( key: _formKey, @@ -58,7 +76,7 @@ class ConfigurationListPageState extends State { ButtonBar(alignment: MainAxisAlignment.center, children: [ controller.searchButton(), RaisedButton( - child: Text('Neue Rolle'), + child: Text('Neue Konfiguration'), onPressed: () { Navigator.pushNamed(context, '/configuration/create'); @@ -69,6 +87,8 @@ class ConfigurationListPageState extends State { ]), ], filters: filters, + errorMessage: + applicationData.lastErrorMessage(controller.page.fullName()), ), ); } diff --git a/lib/src/page/demo_page.dart b/lib/src/page/demo_page.dart new file mode 100644 index 0000000..fddb649 --- /dev/null +++ b/lib/src/page/demo_page.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class DemoPage extends StatefulWidget { + final ApplicationData pageData; + + DemoPage(this.pageData, {Key key}) : super(key: key); + + @override + DemoPageState createState() { + // DemoPageState.setPageData(pageData); + final rc = DemoPageState(pageData); + + return rc; + } +} + +class DemoPageState extends State { + DemoPageState(this.pageData); + + final ApplicationData pageData; + + final GlobalKey _formKey = GlobalKey(); + static User currentUser = User(); + + @override + Widget build(BuildContext context) { + final item = DemoItem(); + final texts = ['one', 'two', 'three']; + final items = texts + .map((text) => DropdownMenuItem(value: text, child: Text(text))) + .toList(); + return Scaffold( + appBar: pageData.appBarBuilder('Demo'), + drawer: pageData.drawerBuilder(context), + body: SimpleForm.simpleForm( + key: _formKey, + configuration: pageData.configuration, + fields: [ + TextFormField( + validator: checkNotEmpty, + decoration: InputDecoration(labelText: 'Name'), + onSaved: (input) => item.name = input, + ), + DropdownButtonFormField( + value: 'two', + items: items, + decoration: InputDecoration(labelText: 'Number'), + onChanged: (value) => item.number = value, + ), + ], + buttons: [ + RaisedButton( + onPressed: () => login(context), + child: Text('Anmelden'), + ), + ], + )); + } + + void login(context) async { + if (_formKey.currentState.validate()) { + _formKey.currentState.save(); + //@ToDo: store in database + } + } +} + +class DemoItem { + String name; + String number; +} diff --git a/lib/src/page/role/role_change_page.dart b/lib/src/page/role/role_change_page.dart index 8c65cf3..4a48fda 100644 --- a/lib/src/page/role/role_change_page.dart +++ b/lib/src/page/role/role_change_page.dart @@ -40,10 +40,21 @@ class RoleChangePageState extends State { @override Widget build(BuildContext context) { if (controller == null) { - controller = RoleController( - _formKey, this, 'change', context, applicationData, - redrawCallback: (RedrawReason reason, String customString) => - setState(() => null)); + controller = + RoleController(_formKey, this, 'change', context, applicationData, + redrawCallback: (RedrawReason reason, + {String customString, + RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); controller.initialize(); } // controller.buildWidgetList() is called in editForm @@ -58,6 +69,7 @@ class RoleChangePageState extends State { initialRow: initialRow, )); } + void dispose() { controller.dispose(); super.dispose(); diff --git a/lib/src/page/role/role_controller.dart b/lib/src/page/role/role_controller.dart index 3aff56f..18f7dbf 100644 --- a/lib/src/page/role/role_controller.dart +++ b/lib/src/page/role/role_controller.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; import '../../model/standard/role_model.dart'; -import 'role_change_page.dart'; import '../../widget/page_controller_bones.dart'; +import 'role_change_page.dart'; class RoleController extends PageControllerBones { /// Controller for a page named [pageName]. @@ -17,7 +17,9 @@ class RoleController extends PageControllerBones { @override void startChange(int id, Map row) { applicationData.pushCaller(this); - Navigator.push(context, - MaterialPageRoute(builder: (context) => RoleChangePage(id, applicationData, row))); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RoleChangePage(id, applicationData, row))); } } diff --git a/lib/src/page/role/role_list_page.dart b/lib/src/page/role/role_list_page.dart index 6b85f50..e685ec9 100644 --- a/lib/src/page/role/role_list_page.dart +++ b/lib/src/page/role/role_list_page.dart @@ -38,22 +38,29 @@ class RoleListPageState extends State { if (controller == null) { controller = RoleController(_formKey, this, 'list', context, applicationData, - redrawCallback: (RedrawReason reason, String customString) { - if (reason == RedrawReason.fetchList) { - applicationData.persistence - .list( - module: controller.moduleModel.name, - params: controller.buildSqlParams()) - .then((rows) { - setState(() => null); - }); + redrawCallback: (RedrawReason reason, + {String customString, + RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.fetchList: + controller.buildRows(); + break; + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; } }); controller.initialize(); controller.buildWidgetList(); + controller.buildRows(); + } else { + controller = controller; } filters ??= controller.filterSet(pageName: 'list'); - controller.buildRows(); return Scaffold( appBar: applicationData.appBarBuilder('Rollen'), drawer: applicationData.drawerBuilder(context), @@ -80,6 +87,8 @@ class RoleListPageState extends State { ]), ], filters: filters, + errorMessage: + applicationData.lastErrorMessage(controller.page.fullName()), ), ); } diff --git a/lib/src/page/user/user_change_page.dart b/lib/src/page/user/user_change_page.dart index c548fa1..0e9bf2d 100644 --- a/lib/src/page/user/user_change_page.dart +++ b/lib/src/page/user/user_change_page.dart @@ -40,15 +40,26 @@ class UserChangePageState extends State { @override Widget build(BuildContext context) { if (controller == null) { - controller = UserController( - _formKey, this, 'change', context, applicationData, - redrawCallback: (RedrawReason reason, String customString) => - setState(() => null)); + controller = + UserController(_formKey, this, 'change', context, applicationData, + redrawCallback: (RedrawReason reason, + {String customString, + RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); controller.initialize(); } // controller.buildWidgetList() is called in editForm return Scaffold( - appBar: applicationData.appBarBuilder('Rolle ändern'), + appBar: applicationData.appBarBuilder('Benutzer ändern'), drawer: applicationData.drawerBuilder(context), body: EditForm.editForm( key: _formKey, @@ -58,6 +69,7 @@ class UserChangePageState extends State { initialRow: initialRow, )); } + void dispose() { controller.dispose(); super.dispose(); diff --git a/lib/src/page/user/user_controller.dart b/lib/src/page/user/user_controller.dart index 9358424..6c69c68 100644 --- a/lib/src/page/user/user_controller.dart +++ b/lib/src/page/user/user_controller.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; import '../../model/standard/user_model.dart'; -import 'user_change_page.dart'; import '../../widget/page_controller_bones.dart'; +import 'user_change_page.dart'; class UserController extends PageControllerBones { /// Controller for a page named [pageName]. @@ -17,7 +17,9 @@ class UserController extends PageControllerBones { @override void startChange(int id, Map row) { applicationData.pushCaller(this); - Navigator.push(context, - MaterialPageRoute(builder: (context) => UserChangePage(id, applicationData, row))); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => UserChangePage(id, applicationData, row))); } } diff --git a/lib/src/page/user/user_list_page.dart b/lib/src/page/user/user_list_page.dart index 2715dac..394c87a 100644 --- a/lib/src/page/user/user_list_page.dart +++ b/lib/src/page/user/user_list_page.dart @@ -15,6 +15,7 @@ class UserListPage extends StatefulWidget { UserListPageState createState() { // UserListPageState.setPageData(pageData); final rc = UserListPageState(applicationData); + /// for unittests: applicationData.lastModuleState = rc; return rc; @@ -35,16 +36,33 @@ class UserListPageState extends State { @override Widget build(BuildContext context) { if (controller == null) { - controller = UserController( - _formKey, this, 'list', context, applicationData, - redrawCallback: (RedrawReason reason, String customString) => setState(() => null)); + controller = + UserController(_formKey, this, 'list', context, applicationData, + redrawCallback: (RedrawReason reason, + {String customString, + RedrawCallbackFunctionSimple callback}) { + switch (reason) { + case RedrawReason.fetchList: + controller.buildRows(); + break; + case RedrawReason.callback: + callback(RedrawReason.custom, customString); + setState(() {}); + break; + default: + setState(() {}); + break; + } + }); controller.initialize(); controller.buildWidgetList(); + controller.buildRows(); + } else { + controller = controller; } filters ??= controller.filterSet(pageName: 'list'); - controller.buildRows(); return Scaffold( - appBar: applicationData.appBarBuilder('Rollen'), + appBar: applicationData.appBarBuilder('Benutzer'), drawer: applicationData.drawerBuilder(context), body: ListForm.listForm( key: _formKey, @@ -58,7 +76,7 @@ class UserListPageState extends State { ButtonBar(alignment: MainAxisAlignment.center, children: [ controller.searchButton(), RaisedButton( - child: Text('Neue Rolle'), + child: Text('Neuer Benutzer'), onPressed: () { Navigator.pushNamed(context, '/user/create'); @@ -69,6 +87,8 @@ class UserListPageState extends State { ]), ], filters: filters, + errorMessage: + applicationData.lastErrorMessage(controller.page.fullName()), ), ); } diff --git a/lib/src/private/bdrawer.dart b/lib/src/private/bdrawer.dart index 7fd4833..d25571d 100644 --- a/lib/src/private/bdrawer.dart +++ b/lib/src/private/bdrawer.dart @@ -1,15 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; -import 'package:flutter_bones/src/private/bsettings.dart'; import '../page/configuration/configuration_list_page.dart'; +import '../page/demo_page.dart'; +import '../page/login_page.dart'; import '../page/role/role_list_page.dart'; import '../page/user/user_list_page.dart'; +import 'bsettings.dart'; class MenuItem { final String title; final dynamic page; final IconData icon; + MenuItem(this.title, this.page, this.icon); static List menuItems() { @@ -23,6 +25,8 @@ class MenuItem { Icons.account_box_outlined), MenuItem('Konfiguration', () => ConfigurationListPage(settings.pageData), Icons.account_box_outlined), + MenuItem('Demo', () => DemoPage(settings.pageData), + Icons.account_box_outlined), ]; } } diff --git a/lib/src/widget/callback_controller_bones.dart b/lib/src/widget/callback_controller_bones.dart index 21ca3ad..6d41598 100644 --- a/lib/src/widget/callback_controller_bones.dart +++ b/lib/src/widget/callback_controller_bones.dart @@ -1,21 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; -typedef RedrawCallbackFunction = Function( +import '../model/combobox_model.dart'; +import '../model/combo_base_model.dart'; +import '../model/field_model.dart'; +import '../page/application_data.dart'; + +typedef RedrawCallbackFunctionSimple = Function( RedrawReason reason, String customString); +typedef RedrawCallbackFunction = Function(RedrawReason reason, + {String customString, RedrawCallbackFunctionSimple callback}); /// Interface for a callback controller for flutter_bones specific widgets. /// flutter_bones specific widgets: [CheckboxListTileBone], /// [DropDownButtonFormBone], [RaisedButtonBone], [TextFormFieldBone] abstract class CallbackControllerBones { - /// Prepares the rows shown in the list page + /// Retrieves the rows shown in the list page. void buildRows(); - /// Returns the texts of the [ComboboxModel] named [name]. - List comboboxTexts(String name); - - /// Returns the values of the [ComboboxModel] named [name]. - List comboboxValues(String name); + /// Returns the [ComboboxData] (texts and values) of the [ComboboxModel] named [name]. + ComboboxData comboboxData(String name); /// Frees all resources. void dispose(); @@ -28,9 +31,9 @@ abstract class CallbackControllerBones { /// Returns the [model] named [name]. FieldModel getModel(String name); + ValueChanged getOnChanged( String customString, CallbackControllerBones controller); - ValueChanged getOnChangedCheckbox( String customString, CallbackControllerBones controller); @@ -73,7 +76,8 @@ abstract class CallbackControllerBones { /// Rebuilds the view of the page. /// [reason] and [customString] will be forwarded to the callback function. - void redraw(RedrawReason reason, String customString); + void redraw(RedrawReason reason, + {String customString, RedrawCallbackFunctionSimple callback}); /// Returns a standard search button for the list page. Widget searchButton(); @@ -92,6 +96,9 @@ abstract class CallbackControllerBones { enum RedrawReason { custom, + callback, fetchList, fetchRecord, + redraw, + setError, } diff --git a/lib/src/widget/list_form.dart b/lib/src/widget/list_form.dart index 90d0e44..9a2f754 100644 --- a/lib/src/widget/list_form.dart +++ b/lib/src/widget/list_form.dart @@ -5,6 +5,7 @@ import 'package:flutter_bones/src/widget/page_controller_bones.dart'; import '../helper/string_helper.dart'; import 'filter_set.dart'; import 'page_controller_bones.dart'; +import 'view.dart'; typedef Function OnEditTap(Map row, int index); @@ -43,8 +44,9 @@ class ListForm { TableCallbackController tableCallbackController, String customString, }) { - if (titles.length != columnNames.length){ - controller.moduleModel.logger.error('titles.length != columnNames.length: ${titles.length}/${columnNames.length}'); + if (titles.length != columnNames.length) { + controller.moduleModel.logger.error( + 'titles.length != columnNames.length: ${titles.length}/${columnNames.length}'); } final titles2 = titles.map((item) => DataColumn(label: item)).toList(); if (showEditIcon) { @@ -68,7 +70,7 @@ class ListForm { } for (var key in columnNames) { cells.add(DataCell( - Text(StringHelper.asString(row[key])), + Text(StringHelper.asString(row[key], nullString: '')), )); } return DataRow(cells: cells); @@ -82,6 +84,7 @@ class ListForm { /// [rows] is a list of rows normally delivered from a database query: /// each row is a map with (key, value) pairs. /// If [showEditItems] is true the edit icon is shown in the first column. + /// If [errorMessage] is not null this message will be shown. static Form listForm( {@required Key key, @required FilterSet filters, @@ -90,33 +93,37 @@ class ListForm { @required List columnNames, @required Iterable rows, bool showEditIcon = false, + String errorMessage, PageControllerBones pageController, @required BaseConfiguration configuration, String customString}) { final padding = configuration.asFloat('form.card.padding', defaultValue: 16.0); + final widgets = [ + ...filters.getWidgets(), + SizedBox( + height: configuration.asFloat('form.gap.field_button.height', + defaultValue: 16.0)), + ...buttons + ]; + if (errorMessage != null) { + widgets.add(View().errorMessage(errorMessage)); + } + widgets.add(table( + titles: titles, + columnNames: columnNames, + rows: rows, + showEditIcon: showEditIcon, + controller: pageController, + customString: customString, + )); return Form( key: key, child: Card( margin: EdgeInsets.symmetric(vertical: padding, horizontal: padding), child: Padding( padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0), - child: ListView(children: [ - ...filters.getWidgets(), - SizedBox( - height: configuration.asFloat( - 'form.gap.field_button.height', - defaultValue: 16.0)), - ...buttons, - table( - titles: titles, - columnNames: columnNames, - rows: rows, - showEditIcon: showEditIcon, - controller: pageController, - customString: customString, - ) - ])), + child: ListView(children: widgets)), )); } } diff --git a/lib/src/widget/page_controller_bones.dart b/lib/src/widget/page_controller_bones.dart index c32bbf4..e76fdfc 100644 --- a/lib/src/widget/page_controller_bones.dart +++ b/lib/src/widget/page_controller_bones.dart @@ -1,14 +1,17 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; -import 'package:flutter_bones/src/widget/widget_list.dart'; +import '../helper/string_helper.dart'; +import '../model/model_types.dart'; import '../model/column_model.dart'; +import '../model/combo_base_model.dart'; +import '../model/page_model.dart'; import '../model/module_model.dart'; +import '../model/field_model.dart'; import '../page/application_data.dart'; import 'callback_controller_bones.dart'; import 'filter_set.dart'; import 'view.dart'; +import 'widget_list.dart'; // This interface allows the generic handling of the edit form by a model driven module. class PageControllerBones implements CallbackControllerBones { @@ -24,22 +27,24 @@ class PageControllerBones implements CallbackControllerBones { final BuildContext context; Iterable listRows; final textControllers = {}; + final comboboxDataMap = {}; PageControllerBones(this.globalKey, this.parent, this.moduleModel, this.pageName, this.context, this.applicationData, [this.redrawCallback]); @override - void buildRows() { + buildRows() { final persistence = applicationData.persistence; final params = buildSqlParams() ?? {}; - persistence.list(module: 'role', params: params).then((list) { - if (listRows == null) { - listRows = list; - redraw(RedrawReason.fetchList, null); - } + persistence.list(module: moduleModel.name, params: params).then((list) { + listRows = list; + redraw(RedrawReason.redraw); }, onError: (error) { - applicationData.logger.error('cannot retrieve role list: $error'); + applicationData.logger + .error('cannot retrieve ${moduleModel.name} list: $error'); + redraw(RedrawReason.setError, + customString: 'Keine Verbindung zum Server'); }); } @@ -78,13 +83,60 @@ class PageControllerBones implements CallbackControllerBones { } @override - List comboboxTexts(String name) { - return []; + ComboboxData comboboxData(String name) { + ComboboxData rc = comboboxDataMap.containsKey(name) + ? comboboxDataMap[name] + : ComboboxData([], []); + comboboxDataMap[name] = rc; + if (rc.waitState == WaitState.undef) { + ComboBaseModel model = page.fieldByName(name) as ComboBaseModel; + if (model != null) { + switch (model.listType) { + case ComboboxListType.explicite: + rc = ComboboxData(model.texts, model.values, WaitState.ready); + comboboxDataMap[name] = rc; + break; + case ComboboxListType.undef: + rc = ComboboxData([], [], WaitState.ready); + comboboxDataMap[name] = rc; + break; + case ComboboxListType.dbColumn: + comboboxDataDb(model, rc); + break; + case ComboboxListType.configuration: + break; + } + } + } + return rc; } - @override - List comboboxValues(String name) { - return []; + void comboboxDataDb(ComboBaseModel model, ComboboxData data) { + // example: role.list;role_displayname role_id;:role_name=% :excluded=0' + final parts = model.listOption.split(';'); + final moduleName = parts[0].split('.'); + final cols = parts[1].split(' '); + final params = {}; + if (parts.length > 2 && parts[2].isNotEmpty) { + parts[2].split(' ').forEach((element) { + final keyValue = element.split('='); + params[keyValue[0]] = keyValue[1]; + }); + } + applicationData.persistence + .list(module: moduleName[0], sqlName: moduleName[1], params: params) + .then((rows) { + rows.forEach((row) { + data.texts.add(row[cols[0]]); + data.values.add(row[cols[1]]); + }); + if (model.hasOption('undef')) { + data.texts.insert(0, '-'); + data.values.insert(0, null); + } + data.waitState = WaitState.ready; + redraw(RedrawReason.redraw); + }); } @override @@ -155,7 +207,12 @@ class PageControllerBones implements CallbackControllerBones { @override getOnChangedCombobox( String customString, CallbackControllerBones controller) { - return null; + var rc; + rc = (value) { + final model = page.fieldByName(customString); + model.value = value; + }; + return rc; } @override @@ -202,9 +259,12 @@ class PageControllerBones implements CallbackControllerBones { switch (pageType) { case PageModelType.create: applicationData.persistence - .insert(module: moduleModel.name, data: params); + .insert(module: moduleModel.name, data: params) + .then((id) { + applicationData.callerRedraw(RedrawReason.fetchList); applicationData.popCaller(); - Navigator.pop(controller.getContext()); + Navigator.pop(controller.getContext()); + }); break; case PageModelType.change: applicationData.persistence @@ -297,12 +357,16 @@ class PageControllerBones implements CallbackControllerBones { } @override - void redraw(RedrawReason reason, [String customString]) { + void redraw(RedrawReason reason, + {String customString, RedrawCallbackFunctionSimple callback}) { if (redrawCallback == null) { moduleModel.logger.error( 'not overridden: fetchListRows() in ${moduleModel.widgetName()}.$pageName'); } else { - redrawCallback(reason, customString); + if (reason == RedrawReason.setError) { + applicationData.setLastErrorMessage(page.fullName(), customString); + } + redrawCallback(reason, customString: customString, callback: callback); } } diff --git a/lib/src/widget/view.dart b/lib/src/widget/view.dart index 2fa2000..688fe5e 100644 --- a/lib/src/widget/view.dart +++ b/lib/src/widget/view.dart @@ -1,18 +1,22 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bones/flutter_bones.dart'; -import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; -import 'package:flutter_bones/src/widget/checkbox_list_tile_bone.dart'; import '../helper/settings.dart'; +import '../helper/string_helper.dart'; import '../model/db_reference_model.dart'; +import '../model/model_types.dart'; import '../model/empty_line_model.dart'; import '../model/field_model.dart'; import '../model/section_model.dart'; import '../model/text_model.dart'; import '../model/widget_model.dart'; +import '../model/button_model.dart'; +import '../model/combo_base_model.dart'; +import 'callback_controller_bones.dart'; +import 'checkbox_list_tile_bone.dart'; import 'dropdown_button_form_bone.dart'; import 'raised_button_bone.dart'; +import 'text_form_field_bone.dart'; class View { static View _instance; @@ -21,7 +25,7 @@ class View { final BaseLogger logger; - factory View(BaseLogger logger) { + factory View([BaseLogger logger]) { if (_instance == null) { _instance = View.internal(logger); _instance.settings = Settings(logger: logger); @@ -73,26 +77,30 @@ class View { /// Creates a combobox via the [controller]. Widget combobox( FieldModel model, CallbackControllerBones controller, initialValue) { - final texts = controller.comboboxTexts(model.name); - final values = controller.comboboxValues(model.name) ?? texts; + ComboboxData comboboxData = controller.comboboxData(model.name); final items = >[]; - for (var ix = 0; ix < texts.length; ix++) { + for (var ix = 0; ix < comboboxData.texts.length; ix++) { items.add(DropdownMenuItem( onTap: controller.getOnTap(model.name, controller), - value: values[ix], - child: Text(texts[ix]))); + value: comboboxData.values[ix], + child: Text(comboboxData.texts[ix]))); } - final rc = DropdownButtonFormBone(model.name, controller, - items: items, value: initialValue); + final rc = DropdownButtonFormBone( + model.name, + controller, + items: items, + value: initialValue, + decoration: InputDecoration(labelText: model.label), + ); return rc; } Widget dbReference(DbReferenceModel model, CallbackControllerBones controller, - initialValue) { + dynamic initialValue) { var rc; if (model.dataType == DataType.bool) { - rc = checkbox(model, controller, initialValue); - } else if (model.hasOption('combobox')) { + rc = checkbox(model, controller, StringHelper.fromString(initialValue, DataType.bool)); + } else if (model.listType != null) { rc = combobox(model, controller, initialValue); } else { rc = textField(model, controller, initialValue); @@ -106,6 +114,16 @@ class View { return rc; } + /// Returns a [Text] widget with an error [message]. + Text errorMessage(String message) { + final rc = Text( + message, + style: TextStyle(color: Colors.red, backgroundColor: Colors.yellow), + textAlign: TextAlign.center, + ); + return rc; + } + /// Creates image from the [model]. Widget image(WidgetModel model) { var rc; @@ -147,7 +165,7 @@ class View { rc = checkbox(model, controller, initialValue); break; case WidgetModelType.combobox: - rc = text(model); + rc = combobox(model, controller, initialValue); break; case WidgetModelType.image: rc = image(model); @@ -221,6 +239,7 @@ class View { controller: textController, readOnly: model.hasOption('readonly'), decoration: InputDecoration(labelText: model.label), + obscureText: model.hasOption('password'), ), model); return rc; diff --git a/test/helpers/string_helper_test.dart b/test/helpers/string_helper_test.dart index e20269f..ca7db96 100644 --- a/test/helpers/string_helper_test.dart +++ b/test/helpers/string_helper_test.dart @@ -81,6 +81,8 @@ void main() { }); group('conversion', () { test('asString', () { + expect(StringHelper.asString(null), isNull); + expect(StringHelper.asString(null, nullString: '?'), equals('?')); expect(StringHelper.asString('abc'), equals('abc')); expect(StringHelper.asString(345), equals('345')); expect(StringHelper.asString(34.5), equals('34.5')); diff --git a/test/model/db_model_test.dart b/test/model/db_model_test.dart index eca107c..4436281 100644 --- a/test/model/db_model_test.dart +++ b/test/model/db_model_test.dart @@ -115,7 +115,7 @@ final userModel = { 'name': 'user', 'label': 'User', 'column': 'user_id', - 'options': 'required;unique', + 'options': 'required unique', }, { 'widgetType': 'dbReference', diff --git a/test/model/model_test.dart b/test/model/model_test.dart index b8400a7..a2c5cc3 100644 --- a/test/model/model_test.dart +++ b/test/model/model_test.dart @@ -278,7 +278,7 @@ final userModel = { 'dataType': 'string', 'label': 'User', 'size': 64, - 'options': 'unique;notnull', + 'options': 'unique notnull', }, { 'column': 'user_role', @@ -302,7 +302,7 @@ final userModel = { 'widgetType': 'textField', 'name': 'user', 'label': 'User', - 'options': 'required;unique', + 'options': 'required unique', }, { 'widgetType': 'button',