From: Hamatoma Date: Tue, 3 Nov 2020 08:14:42 +0000 (+0100) Subject: daily work: coverage improved X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=44e36ad7e42443d3200dd9a6d536c02038a89f85;p=flutter_bones.git daily work: coverage improved --- diff --git a/lib/app.dart b/lib/app.dart index ba74153..12883f7 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -37,7 +37,7 @@ class BoneAppState extends State { primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), - initialRoute: '/configuration/list', + initialRoute: '/user/list', onGenerateRoute: _getRoute, ); } diff --git a/lib/flutter_bones.dart b/lib/flutter_bones.dart index 76cf406..1220da3 100644 --- a/lib/flutter_bones.dart +++ b/lib/flutter_bones.dart @@ -5,6 +5,7 @@ export 'src/helper/settings.dart'; export 'src/helper/string_helper.dart'; export 'src/helper/validators.dart'; +export 'src/helper/helper_async.dart'; export 'src/model/button_model.dart'; export 'src/model/checkbox_model.dart'; export 'src/model/combobox_model.dart'; diff --git a/lib/src/helper/helper_async.dart b/lib/src/helper/helper_async.dart new file mode 100644 index 0000000..00df9e2 --- /dev/null +++ b/lib/src/helper/helper_async.dart @@ -0,0 +1,5 @@ +/// Waits for a given amount of [millisec] and call then a [callback] function (if given). +Future wait({int millisec = 50, Function callback}) { + callback ??= () => null; + return Future.delayed(Duration(milliseconds: millisec), callback()); +} diff --git a/lib/src/model/button_model.dart b/lib/src/model/button_model.dart index 84bd202..d2a5a29 100644 --- a/lib/src/model/button_model.dart +++ b/lib/src/model/button_model.dart @@ -44,7 +44,7 @@ class ButtonModel extends WidgetModel { /// Returns the name including the names of the parent @override - String fullName() => '${section.name}.$name'; + String fullName() => '${section?.name}.$name'; /// Parses the map and stores the data in the instance. void parse() { diff --git a/lib/src/model/column_model.dart b/lib/src/model/column_model.dart index 7ab3ba8..b994677 100644 --- a/lib/src/model/column_model.dart +++ b/lib/src/model/column_model.dart @@ -86,7 +86,7 @@ class ColumnModel extends ComboBaseModel { if (foreignKey != null && regExprForeignKey.firstMatch(foreignKey) == null) { logger.error( - 'invalid foreign key: $foreignKey expected: ". " e.g. "role.role_id role_name"'); + 'invalid foreign key in ${fullName()}: $foreignKey expected: "
. " e.g. "role.role_id role_name"'); } checkOptionsByRegExpr(options, regExprOptions); if (options.contains('primary')) { diff --git a/lib/src/model/combo_base_model.dart b/lib/src/model/combo_base_model.dart index 5a11f57..264644a 100644 --- a/lib/src/model/combo_base_model.dart +++ b/lib/src/model/combo_base_model.dart @@ -9,12 +9,13 @@ 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+=[^ ]*?)+)?$'); + RegExp(r'^\w+\.\w+\.\w+;\w+ \w+(?:;( ?:\w+=[^ ])*)?$'); static final regExprListConfiguration = RegExp(r'^scope:\w+$'); List texts; List values; String listOption; ComboboxListType listType; + ComboboxData data; ComboBaseModel(SectionModel section, PageModel page, Map map, WidgetModelType widgetType, BaseLogger logger) @@ -38,20 +39,6 @@ abstract class ComboBaseModel extends FieldModel { : 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(); @@ -69,7 +56,9 @@ abstract class ComboBaseModel extends FieldModel { if (texts.isEmpty) { logger.error('missing attribute "texts" in ${fullName()}'); listType = ComboboxListType.undef; - } else if (values.isNotEmpty && texts.length != values.length) { + } else if (values != null && + values.isNotEmpty && + texts.length != values.length) { logger.error( 'different sizes of "Texts" and "values": ${texts.length}/${values.length}'); listType = ComboboxListType.undef; @@ -85,12 +74,40 @@ abstract class ComboBaseModel extends FieldModel { case ComboboxListType.dbColumn: if (regExprListDbOption.firstMatch(listOption) == null) { logger.error( - 'wrong syntax in "listOption". Example: "role.list;role_name role_id;:role_name=%"'); + 'wrong syntax in "listOption". Example: "all.role.list;role_name role_id;:role_name=%"'); listType = ComboboxListType.undef; } break; } } + + /// Returns a [ComboboxData] related to the [dataType] with given [texts] + /// and [values]. + static ComboboxData createByType( + DataType dataType, List texts, Iterable values) { + ComboboxData rc; + switch (dataType) { + case DataType.bool: + rc = ComboboxData(texts, values); + break; + case DataType.currency: + case DataType.int: + case DataType.reference: + rc = ComboboxData(texts, values); + break; + case DataType.date: + case DataType.dateTime: + rc = ComboboxData(texts, values); + break; + case DataType.float: + rc = ComboboxData(texts, values); + break; + case DataType.string: + rc = ComboboxData(texts, values); + break; + } + return rc; + } } class ComboboxData { diff --git a/lib/src/model/db_reference_model.dart b/lib/src/model/db_reference_model.dart index d138346..aa74170 100644 --- a/lib/src/model/db_reference_model.dart +++ b/lib/src/model/db_reference_model.dart @@ -55,11 +55,11 @@ class DbReferenceModel extends ComboBaseModel { /// Parses the map and stores the data in the instance. void parse() { - super.parse(); checkSuperfluousAttributes( map, - 'column label maxSize listOption listType name options rows toolTip texts values' + 'column filterType label maxSize listOption listType name options rows toolTip texts widgetType values' .split(' ')); + super.parse(); final columnName = parseString('column', map, required: true); column = page.module.getColumn(columnName, this); maxSize = parseInt('maxSize', map, required: false); diff --git a/lib/src/model/standard/user_model.dart b/lib/src/model/standard/user_model.dart index 097e2a0..9fcb1c9 100644 --- a/lib/src/model/standard/user_model.dart +++ b/lib/src/model/standard/user_model.dart @@ -3,7 +3,7 @@ import 'package:dart_bones/dart_bones.dart'; import '../module_model.dart'; class UserModel extends ModuleModel { - static final model = { + static final mapUser = { "module": "user", "tables": [ { @@ -48,11 +48,11 @@ class UserModel extends ModuleModel { 'dataType': 'reference', 'label': 'Role', 'foreignKey': 'role.role_id role_name', - 'listType': 'explicite', - 'texts': ';-;Administrator;Benutzer;Gast;Verwalter', - 'values': ';;1;2;3;4', - //"listType": "dbColumn", - //"listOption": "role.list;role_name role_id;:role_name=%", + //'listType': 'explicite', + //'texts': ';-;Administrator;Benutzer;Gast;Verwalter', + //'values': ';;1;2;3;4', + "listType": "dbColumn", + "listOption": "all.role.list;role_name role_id;:role_name=%", }, ] }, @@ -97,21 +97,20 @@ class UserModel extends ModuleModel { "sectionType": "filterPanel", "children": [ { - "widgetType": "textField", + "widgetType": "dbReference", "filterType": "pattern", "name": "user_name", - "label": "Name", + "column": "user_name", + "toolTip": + "Filter bezüglich des Names der anzuzeigenden Einträge: Joker '*' (beliebiger String) ist erlaubt." }, { - "widgetType": "combobox", - "filterType": "equals", + "widgetType": "dbReference", "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=%", + "filterType": "equals", + "column": "user_role", + "toolTip": + "Filter bezüglich der Rolle der anzuzeigenden Einträge. '-' bedeutet keine Einschränkung" } ] } @@ -120,7 +119,7 @@ class UserModel extends ModuleModel { ] }; - UserModel(BaseLogger logger) : super(model, logger); + UserModel(BaseLogger logger) : super(mapUser, logger); /// Returns the name including the names of the parent @override diff --git a/lib/src/model/text_field_model.dart b/lib/src/model/text_field_model.dart index 01a157a..d893f31 100644 --- a/lib/src/model/text_field_model.dart +++ b/lib/src/model/text_field_model.dart @@ -14,12 +14,23 @@ class TextFieldModel extends FieldModel { int rows; var value; - final Map map; - TextFieldModel( - SectionModel section, PageModel page, this.map, BaseLogger logger) + SectionModel section, PageModel page, Map map, BaseLogger logger) : super(section, page, map, WidgetModelType.textField, logger); + TextFieldModel.direct( + SectionModel section, + PageModel page, + Map map, + String name, + String label, + String toolTip, + DataType dataType, + List options, + BaseLogger logger) + : super.direct(section, page, null, WidgetModelType.textField, name, + label, toolTip, dataType, options, logger); + /// Dumps the internal structure into a [stringBuffer] StringBuffer dump(StringBuffer stringBuffer) { stringBuffer diff --git a/lib/src/page/configuration/configuration_list_page.dart b/lib/src/page/configuration/configuration_list_page.dart index abb2925..12fbb28 100644 --- a/lib/src/page/configuration/configuration_list_page.dart +++ b/lib/src/page/configuration/configuration_list_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; -import '../../widget/filter_set.dart'; +import '../../widget/widget_list.dart'; import '../../widget/list_form.dart'; import '../application_data.dart'; import 'configuration_controller.dart'; @@ -29,7 +29,7 @@ class ConfigurationListPageState extends State { GlobalKey(debugLabel: 'configuration_list'); Iterable rowsDeprecated; ConfigurationController controller; - FilterSet filters; + WidgetList filters; ConfigurationListPageState(this.applicationData); diff --git a/lib/src/page/role/role_list_page.dart b/lib/src/page/role/role_list_page.dart index e685ec9..09e73e0 100644 --- a/lib/src/page/role/role_list_page.dart +++ b/lib/src/page/role/role_list_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; -import '../../widget/filter_set.dart'; +import '../../widget/widget_list.dart'; import '../../widget/list_form.dart'; import '../application_data.dart'; import 'role_controller.dart'; @@ -29,7 +29,7 @@ class RoleListPageState extends State { GlobalKey(debugLabel: 'role_list'); Iterable rowsDeprecated; RoleController controller; - FilterSet filters; + WidgetList filters; RoleListPageState(this.applicationData); diff --git a/lib/src/page/user/user_list_page.dart b/lib/src/page/user/user_list_page.dart index 394c87a..84832de 100644 --- a/lib/src/page/user/user_list_page.dart +++ b/lib/src/page/user/user_list_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bones/src/widget/callback_controller_bones.dart'; -import '../../widget/filter_set.dart'; +import '../../widget/widget_list.dart'; import '../../widget/list_form.dart'; import '../application_data.dart'; import 'user_controller.dart'; @@ -29,38 +29,40 @@ class UserListPageState extends State { GlobalKey(debugLabel: 'user_list'); Iterable rowsDeprecated; UserController controller; - FilterSet filters; + WidgetList filters; UserListPageState(this.applicationData); + @override + void initState() { + super.initState(); + 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(); + controller.widgetList + .waitForCompletion(controller) + .then((result) => setState(() => null)); + } + @override Widget build(BuildContext context) { - if (controller == 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'); return Scaffold( appBar: applicationData.appBarBuilder('Benutzer'), drawer: applicationData.drawerBuilder(context), diff --git a/lib/src/persistence/persistence.dart b/lib/src/persistence/persistence.dart index cd2839e..58f71d7 100644 --- a/lib/src/persistence/persistence.dart +++ b/lib/src/persistence/persistence.dart @@ -29,9 +29,16 @@ abstract class Persistence { /// Returns a record with primary key [id] of the [module] with the /// SQL statement [sqlName]. - Future> record( + Future> recordById( {@required String module, String sqlName, @required int id}); + /// Returns a record specified by one or more [parameters] of the [module] with the + /// SQL statement [sqlName]. + Future> recordByParameter( + {@required String module, + @required String sqlName, + @required Map parameters}); + /// Updates a record of [module] with the [data] using the SQL statement [sqlName]. Future update( {@required String module, diff --git a/lib/src/persistence/persistence_cache.dart b/lib/src/persistence/persistence_cache.dart index d3e299e..b78c0a1 100644 --- a/lib/src/persistence/persistence_cache.dart +++ b/lib/src/persistence/persistence_cache.dart @@ -1,27 +1,40 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter_bones/flutter_bones.dart'; import 'package:flutter_bones/src/model/combo_base_model.dart'; -import 'package:meta/meta.dart'; -import 'persistence.dart'; + +/// Specifies an cache entry. +class CacheEntry { + final String key; + final CacheEntryType entryType; + final List list; + bool ready; + bool oneTime; + CacheEntry(this.key, this.entryType, this.list, + {this.ready = false, this.oneTime = false}); +} + +enum CacheEntryType { + combobox, + record, + list, +} /// A cache for objects coming from a persistence layer. /// Uses the least recently used algorithm to avoid overflow. class PersistenceCache { static PersistenceCache _instance; - //static final regExpCombobox = RegExp(r'^\w\.\w+\.\w+;\w+ \w+;( ?:\w+=\S*)*$'); static final regExpCombobox = RegExp(r'^\w+\.\w+\.\w+;\w+ \w+;( ?:\w+=\S*)*$'); + static final regExpRecord = RegExp(r'^\w+\.\w+\.\w+;( ?:\w+=\S*)+$'); + static int nextRecordNo = 0; final leastReasentlyUsed = []; final map = {}; final maxEntries; - factory PersistenceCache() { - return _instance; - } final ApplicationData application; BaseLogger logger; - PersistenceCache.internal(this.application, this.maxEntries) { - this.logger = this.application.logger; + factory PersistenceCache() { + return _instance; } /// True constructor (of the singleton instance). @@ -34,14 +47,27 @@ class PersistenceCache { return _instance; } + PersistenceCache.internal(this.application, this.maxEntries) { + this.logger = this.application.logger; + } + + /// Returns a unique key for using record(). + String buildRecordKeyPrefix() { + final rc = 'key${++nextRecordNo}'; + return rc; + } + /// Returns the data (text, values) of a combobox specified by [key]. /// If the data are unavailable (e.g. a asynchronous request is running) the /// result is null. - /// [key] has the format: + /// The format of [key]: /// "..; ; ..." /// e.g. "x99.role.list;role_name role_id;:role_name=% :role_active=T" /// If [hasUndef] is true the first combobox list entry is '-' with the value null. - ComboboxData combobox(String key, {bool hasUndef = false}) { + /// If [oneTime] is true the result is only used one time, the LRU algoritm is + /// not used. + ComboboxData combobox(String key, + {bool hasUndef = false, bool oneTime = false}) { ComboboxData rc; if (regExpCombobox.firstMatch(key) == null) { logger.error('wrong key syntax: $key'); @@ -60,10 +86,14 @@ class PersistenceCache { final keyValue = element.split('='); params2[keyValue[0]] = keyValue[1]; }); - final entry = CacheEntry(key, CacheEntryType.combobox, [ - hasUndef ? ['-'] : [], - hasUndef ? [null] : [] - ]); + final entry = CacheEntry( + key, + CacheEntryType.combobox, + [ + hasUndef ? ['-'] : [], + hasUndef ? [null] : [] + ], + oneTime: oneTime); insert(entry); application.persistence .list( @@ -82,38 +112,83 @@ class PersistenceCache { return rc; } + /// Deletes an entry given by [key] from the cache. + /// Returns true on success. + bool deleteEntry(String key) { + bool rc = map.containsKey(key); + if (rc) { + leastReasentlyUsed.remove(map[key]); + map.remove(key); + } + return rc; + } + /// Inserts an [cacheEntry] into the cache. void insert(CacheEntry cacheEntry) { if (leastReasentlyUsed.length >= maxEntries) { map.remove(leastReasentlyUsed[0].key); leastReasentlyUsed.removeAt(0); } - leastReasentlyUsed.add(cacheEntry); + final offset = maxEntries > 50 ? 25 : maxEntries / 2; + if (cacheEntry.oneTime && leastReasentlyUsed.length > offset) { + leastReasentlyUsed.insert(offset, cacheEntry); + } else { + leastReasentlyUsed.add(cacheEntry); + } map[cacheEntry.key] = cacheEntry; } + /// Returns database record specified by [key]. + /// If the data are unavailable (e.g. a asynchronous request is running) the + /// result is null, otherwise the result may be an empty list (record not found). + /// The format of [key]: + /// "..; ..." + /// e.g. "x99.role.by_name:role_name=eve :excluded=22" + /// If [oneTime] is true the result is only used one time, the LRU algoritm is + /// not used. + Map record(String key, {bool oneTime = true}) { + Map rc; + if (regExpRecord.firstMatch(key) == null) { + logger.error('wrong key syntax: $key'); + } else if (map.containsKey(key)) { + final entry = updateLRU(key); + if (entry.ready) { + rc = entry.list[0]; + } + } else { + final parts = key.split(';'); + final idModuleName = parts[0].split('.'); + final params = parts[1].split(' '); + final params2 = {}; + params.forEach((element) { + final keyValue = element.split('='); + params2[keyValue[0]] = keyValue[1]; + }); + final entry = + CacheEntry(key, CacheEntryType.record, [null], oneTime: oneTime); + insert(entry); + application.persistence + .recordByParameter( + module: idModuleName[1], + sqlName: idModuleName[2], + parameters: params2) + .then((row) { + entry.list[0] = row; + entry.ready = true; + }); + } + return rc; + } + /// Moves the entry specified by [key] at the top of the Least Recently Used /// stack. /// Returns the entry specified by [key]. CacheEntry updateLRU(String key) { final entry = map[key]; - leastReasentlyUsed.remove(entry); - leastReasentlyUsed.add(entry); + if (!entry.oneTime) { + leastReasentlyUsed.remove(entry); + leastReasentlyUsed.add(entry); + } return entry; } } - -/// Specifies an cache entry. -class CacheEntry { - final String key; - final CacheEntryType entryType; - final List list; - bool ready; - CacheEntry(this.key, this.entryType, this.list, {this.ready = false}); -} - -enum CacheEntryType { - combobox, - record, - list, -} diff --git a/lib/src/persistence/rest_persistence.dart b/lib/src/persistence/rest_persistence.dart index d6da890..70d5300 100644 --- a/lib/src/persistence/rest_persistence.dart +++ b/lib/src/persistence/rest_persistence.dart @@ -63,9 +63,9 @@ class RestPersistence extends Persistence { @override Future customQuery( - {String module, - String sqlName, - String sqlType, + {@required String module, + @required String sqlName, + @required String sqlType, Map params}) async { var rc; assert(['list', 'record', 'update'].contains(sqlType)); @@ -81,14 +81,17 @@ class RestPersistence extends Persistence { } @override - Future delete({String module, String sqlName, int id}) async { + Future delete( + {@required String module, String sqlName, @required int id}) async { sqlName ??= 'delete'; await runRequest(module, sqlName, 'delete', body: '{":${module}_id":$id}'); } @override Future insert( - {String module, String sqlName, Map data}) async { + {@required String module, + String sqlName, + @required Map data}) async { sqlName ??= 'insert'; var rc = 0; final data2 = data == null ? '{}' : convert.jsonEncode(data); @@ -101,7 +104,8 @@ class RestPersistence extends Persistence { } @override - Future list({String module, String sqlName, Map params}) async { + Future list( + {@required String module, String sqlName, Map params}) async { sqlName ??= 'list'; Iterable rc; final body = params == null ? '{}' : convert.jsonEncode(params); @@ -116,8 +120,8 @@ class RestPersistence extends Persistence { } @override - Future> record( - {String module, String sqlName, int id}) async { + Future> recordById( + {@required String module, String sqlName, @required int id}) async { sqlName ??= 'record'; Map rc; final answer = await runRequest(module, sqlName, 'record', @@ -128,6 +132,21 @@ class RestPersistence extends Persistence { return rc; } + @override + Future> recordByParameter( + {@required String module, + @required String sqlName, + @required Map parameters}) async { + sqlName ??= 'record'; + Map rc; + final answer = await runRequest(module, sqlName, 'record', + body: convert.jsonEncode(parameters)); + if (answer.isNotEmpty) { + rc = convert.jsonDecode(answer); + } + return rc; + } + /// Handles a HTTP request with a single HTTP connection. /// [module]: the module which implies the requested table. /// [sqlName]: the name of the SQL statement. @@ -153,7 +172,9 @@ class RestPersistence extends Persistence { @override Future update( - {String module, String sqlName, Map data}) async { + {@required String module, + String sqlName, + @required Map data}) async { sqlName ??= 'update'; final data2 = data == null ? '{}' : convert.jsonEncode(data); await runRequest(module, sqlName, 'update', body: data2); diff --git a/lib/src/widget/callback_controller_bones.dart b/lib/src/widget/callback_controller_bones.dart index 6d41598..300d0e6 100644 --- a/lib/src/widget/callback_controller_bones.dart +++ b/lib/src/widget/callback_controller_bones.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../model/model_types.dart'; import '../model/combobox_model.dart'; import '../model/combo_base_model.dart'; import '../model/field_model.dart'; @@ -18,7 +19,7 @@ abstract class CallbackControllerBones { void buildRows(); /// Returns the [ComboboxData] (texts and values) of the [ComboboxModel] named [name]. - ComboboxData comboboxData(String name); + ComboboxData comboboxData(String name, DataType dataType); /// Frees all resources. void dispose(); diff --git a/lib/src/widget/filter_set.dart b/lib/src/widget/filter_set.dart index 55851f4..3857700 100644 --- a/lib/src/widget/filter_set.dart +++ b/lib/src/widget/filter_set.dart @@ -2,19 +2,18 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; import 'package:flutter_bones/src/model/combo_base_model.dart'; -import 'package:flutter_bones/src/widget/dropdown_button_form_bone.dart'; import 'package:flutter_bones/src/widget/page_controller_bones.dart'; import 'text_form_field_bone.dart'; typedef FilterPredicate = bool Function(Map row); +@deprecated class FilterItem { final String name; final String label; final FilterType filterType; final String toolTip; - final FilterPredicate filterPredicate; var value; FilterItem({ @@ -22,45 +21,10 @@ class FilterItem { this.filterType, this.toolTip, this.name, - this.filterPredicate, }); - - bool isValid(Map row) { - bool rc = true; - if (filterPredicate != null) { - rc = filterPredicate(row); - } else { - final current = row[name].toString(); - final value2 = value ?? ''; - switch (filterType) { - case FilterType.pattern: - rc = value2 == '' - ? true - : (value2.startsWith('*') - ? current.contains(value2.replaceAll('*', '')) - : current.startsWith(value2)); - break; - case FilterType.dateFrom: - // TODO: Handle this case. - break; - case FilterType.dateTil: - // TODO: Handle this case. - break; - case FilterType.dateTimeFrom: - // TODO: Handle this case. - break; - case FilterType.dateTimeTil: - // TODO: Handle this case. - break; - default: - rc = true; - break; - } - } - return rc; - } } +@deprecated class FilterSet { var filters = []; final BaseLogger logger; @@ -100,17 +64,5 @@ class FilterSet { return rc; } - /// Tests whether the [row] belongs to the result. - bool isValid(Map row) { - var rc = true; - for (var filter in filters) { - if (!filter.isValid(row)) { - rc = false; - break; - } - } - return rc; - } - dynamic valueOf(String name) => byName(name).value; } diff --git a/lib/src/widget/list_form.dart b/lib/src/widget/list_form.dart index 9a2f754..7b9ce30 100644 --- a/lib/src/widget/list_form.dart +++ b/lib/src/widget/list_form.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bones/src/widget/page_controller_bones.dart'; import '../helper/string_helper.dart'; -import 'filter_set.dart'; +import 'widget_list.dart'; import 'page_controller_bones.dart'; import 'view.dart'; @@ -87,7 +87,7 @@ class ListForm { /// If [errorMessage] is not null this message will be shown. static Form listForm( {@required Key key, - @required FilterSet filters, + @required WidgetList filters, @required List buttons, @required List titles, @required List columnNames, @@ -99,8 +99,9 @@ class ListForm { String customString}) { final padding = configuration.asFloat('form.card.padding', defaultValue: 16.0); + final widgets = [ - ...filters.getWidgets(), + ...filters.widgets, SizedBox( height: configuration.asFloat('form.gap.field_button.height', defaultValue: 16.0)), diff --git a/lib/src/widget/page_controller_bones.dart b/lib/src/widget/page_controller_bones.dart index e76fdfc..48a13b2 100644 --- a/lib/src/widget/page_controller_bones.dart +++ b/lib/src/widget/page_controller_bones.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.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 '../model/model_types.dart'; +import '../model/module_model.dart'; +import '../model/page_model.dart'; import '../page/application_data.dart'; import 'callback_controller_bones.dart'; -import 'filter_set.dart'; import 'view.dart'; import 'widget_list.dart'; @@ -77,13 +77,55 @@ class PageControllerBones implements CallbackControllerBones { widgetList.clear(); page.fields.forEach((model) { final value = initialRow == null ? null : initialRow[model.name]; + completeModelByPersistence(model); widgetList.addWidget(model.name, View(moduleModel.logger).modelToWidget(model, this, value)); }); } + /// Completes database based components to the model, e.g. the list for + /// comboboxes. + void completeModelByPersistence(WidgetModel model) { + if (model is ComboBaseModel) { + if (model.data == null) { + if (model.listType == ComboboxListType.explicite) { + model.data = ComboBaseModel.createByType( + model.dataType, model.texts, model.values); + model.data.waitState = WaitState.ready; + } else { + model.data = comboboxData(model.name, model.dataType); + } + } + } + } + @override - ComboboxData comboboxData(String name) { + ComboboxData comboboxData(String name, DataType dataType) { + ComboboxData rc; + switch (dataType) { + case DataType.bool: + rc = comboboxDataByType(name); + break; + case DataType.currency: + case DataType.int: + case DataType.reference: + rc = comboboxDataByType(name); + break; + case DataType.date: + case DataType.dateTime: + rc = comboboxDataByType(name); + break; + case DataType.float: + rc = comboboxDataByType(name); + break; + case DataType.string: + comboboxDataByType(name); + break; + } + return rc; + } + + ComboboxData comboboxDataByType(String name) { ComboboxData rc = comboboxDataMap.containsKey(name) ? comboboxDataMap[name] : ComboboxData([], []); @@ -101,7 +143,7 @@ class PageControllerBones implements CallbackControllerBones { comboboxDataMap[name] = rc; break; case ComboboxListType.dbColumn: - comboboxDataDb(model, rc); + comboboxDataDb(model, rc); break; case ComboboxListType.configuration: break; @@ -111,7 +153,7 @@ class PageControllerBones implements CallbackControllerBones { return rc; } - void comboboxDataDb(ComboBaseModel model, ComboboxData data) { + 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('.'); @@ -148,7 +190,7 @@ class PageControllerBones implements CallbackControllerBones { /// Gets the data from the database using the [primaryId]. fetchData(int primaryId) async { applicationData.persistence - .record(module: moduleModel.name, id: primaryId) + .recordById(module: moduleModel.name, id: primaryId) .then((row) { page.fields.forEach((model) { model.value = row[model.name]; @@ -159,18 +201,19 @@ class PageControllerBones implements CallbackControllerBones { }); } - FilterSet filterSet({@required String pageName}) { - final rc = FilterSet(globalKey, parent, this, moduleModel.logger); + /// Returns a [WidgetList] filled with widgets + WidgetList filterSet({@required String pageName}) { + final rc = WidgetList('${page.fullName()}.widgets', moduleModel.logger); moduleModel .pageByName(pageName) .fields .where((element) => element.filterType != null) .forEach((element) { - rc.add(FilterItem( - label: element.label, - filterType: element.filterType, - toolTip: element.toolTip, - name: element.name)); + Widget widget = View().modelToWidget(element, this); + if (element.widgetModelType == WidgetModelType.combobox) { + rc.waitCandidates.add(element); + } + rc.addWidget(element.name, widget); }); return rc; } @@ -352,7 +395,7 @@ class PageControllerBones implements CallbackControllerBones { ColumnModel primary = moduleModel.mainTable().primary; final id = row[primary.name]; applicationData.persistence - .record(module: moduleModel.name, id: id) + .recordById(module: moduleModel.name, id: id) .then((row) => startChange(id, row)); } diff --git a/lib/src/widget/view.dart b/lib/src/widget/view.dart index aad66b2..bd650d9 100644 --- a/lib/src/widget/view.dart +++ b/lib/src/widget/view.dart @@ -74,10 +74,38 @@ class View { return rc; } - /// Creates a combobox via the [controller]. - Widget combobox( + /// Creates a combobox via the [controller] with an [initialValue]. + Widget combobox( FieldModel model, CallbackControllerBones controller, initialValue) { - ComboboxData comboboxData = controller.comboboxData(model.name); + Widget rc; + switch (model.dataType) { + case DataType.bool: + rc = comboboxByType(model, controller, initialValue); + break; + case DataType.currency: + case DataType.int: + case DataType.reference: + rc = comboboxByType(model, controller, initialValue); + break; + case DataType.date: + case DataType.dateTime: + rc = comboboxByType(model, controller, initialValue); + break; + case DataType.float: + rc = comboboxByType(model, controller, initialValue); + break; + case DataType.string: + comboboxByType(model, controller, initialValue); + break; + } + return rc; + } + + /// Creates a combobox via the [controller] depending on the type + /// with an [initialValue]. + Widget comboboxByType( + FieldModel model, CallbackControllerBones controller, initialValue) { + ComboboxData comboboxData = (model as ComboBaseModel).data; final items = >[]; for (var ix = 0; ix < comboboxData.texts.length; ix++) { items.add(DropdownMenuItem( @@ -95,6 +123,8 @@ class View { return rc; } + /// Creates a widget related to a [model] of type [DbReferenceModel] + /// via the [controller] with an [initialValue]. Widget dbReference(DbReferenceModel model, CallbackControllerBones controller, dynamic initialValue) { var rc; diff --git a/lib/src/widget/widget_list.dart b/lib/src/widget/widget_list.dart index 9fb72af..fd861ee 100644 --- a/lib/src/widget/widget_list.dart +++ b/lib/src/widget/widget_list.dart @@ -1,6 +1,7 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/model/combo_base_model.dart'; import '../helper/string_helper.dart'; import '../model/model_types.dart'; @@ -15,6 +16,9 @@ class WidgetList { final widgetMap = {}; final widgets = []; + /// Data that will be filled asynchronously. + final waitCandidates = []; + WidgetList(this.name, this.logger); /// Tests whether the widget list is empty. @@ -93,4 +97,30 @@ class WidgetList { widgets.clear(); widgetMap.clear(); } + + /// Checks in a loop all open requests for completion. + /// If + Future waitForCompletion(PageControllerBones controller) async { + var again = true; + ComboboxData data; + // 30 sec: + int maxCount = 50 * 30; + while (again && maxCount > 0) { + again = false; + for (var model in waitCandidates) { + if (model is ComboBaseModel) { + data = controller.comboboxData(model.listOption, model.dataType); + if (data != null && data.waitState == WaitState.ready) { + model.data = data; + waitCandidates.remove(model); + } else { + again = true; + } + } + } + if (again) { + await wait(millisec: 20); + } + } + } } diff --git a/test/model/db_model_test.dart b/test/model/db_model_test.dart index 4436281..e90c653 100644 --- a/test/model/db_model_test.dart +++ b/test/model/db_model_test.dart @@ -2,12 +2,14 @@ import 'dart:convert'; import 'package:dart_bones/dart_bones.dart'; import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/model/combo_base_model.dart'; import 'package:test/test.dart'; void main() { final logger = MemoryLogger(LEVEL_FINE); group('module', () { test('module', () { + WidgetModel.lastId = 0; logger.clear(); final module = Demo1(cloneOfMap(userModel), logger); module.parse(); @@ -20,7 +22,7 @@ void main() { expect(table.fullName(), equals('demo1.user')); expect(table.widgetName(), equals('user')); final dump = module.dump(StringBuffer()).toString(); - expect(dump, '''= module demo1: options: + expect(dump, equals('''= module demo1: options: == table user: options: column user_id: DataType.int "Id" options: primary notnull unique column user_name: DataType.string "User" options: unique @@ -42,7 +44,7 @@ void main() { textField role: options: button buttonStore: text: options: null ] # create.simpleForm1 -'''); +''')); final userField = table.columnByName('user_id'); expect(userField, isNotNull); expect(userField.widgetName(), 'user_id'); @@ -53,6 +55,135 @@ void main() { expect(userRef.widgetName(), equals('user')); }); }); + test('combobase-basic', () { + var data = ComboBaseModel.createByType(DataType.bool, ['x'], [true]); + expect(data, isNotNull); + data = ComboBaseModel.createByType(DataType.int, ['x'], [3]); + expect(data, isNotNull); + data = ComboBaseModel.createByType(DataType.float, ['x'], [3.9]); + expect(data, isNotNull); + data = ComboBaseModel.createByType( + DataType.date, ['x'], [DateTime(2020, 1, 1)]); + expect(data, isNotNull); + data = ComboBaseModel.createByType(DataType.string, ['x'], ["Hi"]); + expect(data, isNotNull); + }); + test('combobase', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'tables': [ + { + 'table': 'user', + 'columns': [ + { + 'column': 'user_id', + 'dataType': 'int', + 'label': 'Id', + 'options': 'primary', + 'listType': 'explicite', + 'texts': ';a;b', + 'values': ';22;33' + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final errors = logger.errors; + expect(errors.length, equals(1)); + }); + group('errors', () { + test('errors-combobase', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'tables': [ + { + 'table': 'user', + 'columns': [ + { + 'column': 'user_id', + 'dataType': 'int', + 'label': 'Id', + 'options': 'primary', + 'listType': 'undef', + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final errors = logger.errors; + expect(errors.length, equals(2)); + expect( + errors.contains( + 'wrong value "undef" in attribute "listType" in user.user_id'), + isTrue); + }); + test('errors-missing-texts', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'tables': [ + { + 'table': 'user', + 'columns': [ + { + 'column': 'user_id', + 'dataType': 'int', + 'label': 'Id', + 'listType': 'explicite', + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final errors = logger.errors; + expect(errors.length, equals(2)); + expect( + errors.contains( + 'missing attribute "texts" in user.user_id'), + isTrue); + }); + test('errors-#texts!=#values', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'tables': [ + { + 'table': 'user', + 'columns': [ + { + 'column': 'user_id', + 'dataType': 'int', + 'label': 'Id', + 'listType': 'explicite', + 'texts': ';a', + 'values': ';1;2', + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final errors = logger.errors; + expect(errors.length, equals(2)); + expect( + errors.contains( + 'different sizes of "Texts" and "values": 1/2'), + isTrue); + }); + }); } final userModel = { @@ -78,7 +209,7 @@ final userModel = { 'column': 'user_role', 'dataType': 'reference', 'label': 'Role', - 'foreignKey': 'role.role_id', + 'foreignKey': 'role.role_id role_name', 'widgetType': 'combobox', }, ] diff --git a/test/model/model_test.dart b/test/model/model_test.dart index a2c5cc3..e4fdbd2 100644 --- a/test/model/model_test.dart +++ b/test/model/model_test.dart @@ -8,6 +8,7 @@ void main() { final logger = MemoryLogger(LEVEL_FINE); group('module', () { test('module', () { + WidgetModel.lastId = 0; logger.clear(); final module = Demo1(cloneOfMap(userModel), logger); module.parse(); @@ -17,9 +18,9 @@ void main() { expect(page?.fullName(), equals('demo1.create')); expect(module.fullName(), equals('demo1')); final dump = module.dump(StringBuffer()).toString(); - expect(dump, '''= module demo1: options: + expect(dump, equals('''= module demo1: options: == table user: options: - column user_id: DataType.int "Id" options: primary notnull unique + column user_id: DataType.int "Id" options: primary notnull unique readonly column user_name: DataType.string "User" options: unique notnull column user_role: DataType.reference "Role" options: column user_createdat: DataType.dateTime "Erzeugt" options: hidden null @@ -33,9 +34,9 @@ void main() { ] # create.simpleForm1 == page change: PageModelType.change options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields 12 options: + allDbFields 13 options: ] # change.simpleForm1 -'''); +''')); final userField = page.fieldByName('user'); expect(userField, isNotNull); expect(userField.widgetName(), 'user'); @@ -49,6 +50,204 @@ void main() { expect(button.section, equals(userField.section)); expect(button.fullName(), 'simpleForm1.buttonStore'); expect(button.widgetName(), 'buttonStore'); + final all = module.pageByName('change').getWidgets( + (item) => item.widgetModelType == WidgetModelType.allDbFields); + expect(all.length, equals(1)); + final widget = all[0]; + final name = widget.fullName() + ' ' + widget.widgetName(); + expect(name.contains('allDbFields'), isTrue); + }); + }); + group('page-errors', () { + test('add-button', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'tables': [ + { + 'table': 'user', + 'columns': [ + { + 'column': 'user_id', + 'dataType': 'int', + 'label': 'Id', + 'options': 'primary', + }, + ] + }, + ], + 'pages': [ + { + "page": "list", + "pageType": "list", + "tableColumns": "user_id", + "tableTitles": ";Id;Name", + "sections": [ + { + "sectionType": "filterPanel", + "children": [ + { + "widgetType": "dbReference", + "filterType": "pattern", + "name": "user_name", + "column": "user_name", + }, + ], + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final page = module.pageByName('list'); + page.addButton(ButtonModel.direct( + null, + page, + 'a', + 'b', + ButtonModelType.store, + [], + null, + logger)); + page.addButton(ButtonModel.direct( + null, + page, + 'a', + 'b', + ButtonModelType.store, + [], + null, + logger)); + page.buttonByName('unknown'); + page.addField(TextFieldModel.direct( + null, + page, + null, + 'x', + 'y', + 'z', + DataType.int, + [], + logger)); + page.addField(TextFieldModel.direct( + null, + page, + null, + 'x', + 'y', + 'z', + DataType.int, + [], + logger)); + page.buttonByName('unknown'); + page.fieldByName('nothing'); + final errors = logger.errors; + expect(errors.length, equals(7)); + expect(errors.contains('missing column user_name in table demo1.user'), + isTrue); + expect( + errors.contains('different sizes of tableTitles/tableColumns: 2/1'), + isTrue); + expect(errors.contains('button null.a already defined: null.a'), + isTrue); + expect(errors.contains('missing button unknown in page demo1.list'), + isTrue); + expect(errors.contains('missing field nothing in page demo1.list'), + isTrue); + expect(errors.contains('field list.x already defined: list.x'), + isTrue); + }); + test('missing-section', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'pages': [ + { + "page": "list", + "pageType": "list", + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final page = module.pageByName('list'); + expect(page, isNotNull); + final errors = logger.errors; + expect(errors.length, equals(1)); + expect(errors.contains('missing sections in page demo1.list'), + isTrue); + }); + test('wrong-section', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'pages': [ + { + "page": "list", + "pageType": "list", + "sections": "wrong" + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final page = module.pageByName('list'); + expect(page, isNotNull); + final errors = logger.errors; + expect(errors.length, equals(1)); + expect(errors.contains('"sections" is not an array in demo1.list: wrong'), + isTrue); + }); + test('tableTitles not in list', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'pages': [ + { + "page": "list", + "pageType": "change", + "tableTitles": ";a;b", + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final page = module.pageByName('list'); + final errors = logger.errors; + expect(errors.length, equals(2)); + expect(errors.contains( + 'tableTitles and tableColumns are only meaningful in list pages: demo1.list'), + isTrue); + expect(page.fullName() + page.widgetName(), equals('demo1.listlist')); + }); + test('curious section', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'pages': [ + { + "page": "list", + "pageType": "change", + "sections": [ + [], + ] + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final page = module.pageByName('list'); + final errors = logger.errors; + expect(errors.length, equals(1)); + expect(errors.contains('curious item in section list of demo1.list: []'), + isTrue); + expect(page.fullName() + page.widgetName(), equals('demo1.listlist')); }); }); group('ModelBase', () { @@ -68,12 +267,13 @@ void main() { var errors = logger.errors; expect(errors.length, equals(1)); expect(logger.contains('blub'), isTrue); - logger.clearErrors(); + logger.clear(); field['options'] = 3; module = Demo1(map, logger); + logger.log('wrong option type: 3'); module.parse(); errors = logger.errors; - expect(errors.length, equals(1)); + expect(errors.length, greaterThan(0)); expect(logger.contains('wrong datatype'), isTrue); // === logger.clear(); @@ -83,7 +283,7 @@ void main() { module = Demo1(map, logger); module.parse(); errors = logger.errors; - expect(errors.length, equals(3)); + expect(errors.length, greaterThan(2)); expect(logger.contains('unknown attribute "newfeature"'), isTrue); expect( logger.contains( @@ -94,19 +294,20 @@ void main() { // === logger.clear(); field.removeWhere( - (key, value) => key == 'newfeature' || key == 'dataType'); + (key, value) => key == 'newfeature' || key == 'dataType'); map['pages'][0].remove('pageType'); module = Demo1(map, logger); module.parse(); map['pages'][0]['pageType'] = 'simpleForm'; errors = logger.errors; - expect(errors.length, equals(2)); + expect(errors.length, greaterThan(1)); expect(logger.contains('missing attribute \"pageType\" in demo1.create'), isTrue); }); }); group('CheckboxModel', () { test('basic', () { + WidgetModel.lastId = 0; logger.clear(); final map = cloneOfMap(userModel); final field = { @@ -130,6 +331,7 @@ void main() { expect(checkbox.value, isTrue); }); test('errors', () { + WidgetModel.lastId = 0; logger.clear(); final map = cloneOfMap(userModel); final field = { @@ -149,7 +351,7 @@ void main() { final dump = module.dump(StringBuffer()).toString(); expect(dump, equals('''= module demo1: options: == table user: options: - column user_id: DataType.int "Id" options: primary notnull unique + column user_id: DataType.int "Id" options: primary notnull unique readonly column user_name: DataType.string "User" options: unique notnull column user_role: DataType.reference "Role" options: column user_createdat: DataType.dateTime "Erzeugt" options: hidden null @@ -163,12 +365,13 @@ void main() { ] # create.simpleForm1 == page change: PageModelType.change options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields 102 options: + allDbFields 13 options: ] # change.simpleForm1 ''')); }); }); group('ComboboxModel', () { + WidgetModel.lastId = 0; logger.clear(); test('basic', () { final map = cloneOfMap(userModel); @@ -192,7 +395,7 @@ void main() { final dump = module.dump(StringBuffer()).toString(); expect(dump, equals('''= module demo1: options: == table user: options: - column user_id: DataType.int "Id" options: primary notnull unique + column user_id: DataType.int "Id" options: primary notnull unique readonly column user_name: DataType.string "User" options: unique notnull column user_role: DataType.reference "Role" options: column user_createdat: DataType.dateTime "Erzeugt" options: hidden null @@ -206,13 +409,14 @@ void main() { ] # create.simpleForm1 == page change: PageModelType.change options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields 117 options: + allDbFields 30 options: ] # change.simpleForm1 ''')); }); }); - group('allDbFields', (){ - test('allDbFields', (){ + group('allDbFields', () { + test('allDbFields', () { + WidgetModel.lastId = 0; logger.clear(); final map = cloneOfMap(userModel); var module = Demo1(map, logger); @@ -229,6 +433,7 @@ void main() { }); group('Non field widgets', () { test('basic', () { + WidgetModel.lastId = 0; logger.clear(); final map = cloneOfMap(userModel); final list = [ @@ -252,13 +457,148 @@ void main() { ' ' + element.widgetName() + '/' + element.fullName(); }); expect(names.contains('null'), isFalse); - final nonFieldWidgets = page.getWidgets((item) => [ + final nonFieldWidgets = page.getWidgets((item) => + [ WidgetModelType.text, WidgetModelType.emptyLine ].contains(item.widgetModelType)); expect(nonFieldWidgets.length, equals(2)); }); }); + group('section-errors', () { + test('missing children', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'pages': [ + { + "page": "list", + "pageType": "list", + "sections": [ + { + "sectionType": "filterPanel", + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final errors = logger.errors; + expect(errors.length, equals(1)); + expect(errors.contains('missing children in list.filterPanel1'), + isTrue); + }); + test('wrong children', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'pages': [ + { + "page": "list", + "pageType": "list", + "sections": [ + { + "sectionType": "filterPanel", + "children": "a" + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final errors = logger.errors; + expect(errors.length, equals(1)); + expect(errors.contains('"children" is not a list in list.filterPanel1: a'), + isTrue); + }); + test('not a map in children', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'pages': [ + { + "page": "list", + "pageType": "list", + "sections": [ + { + "sectionType": "filterPanel", + "children": [ + [] + ] + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final errors = logger.errors; + expect(errors.length, equals(1)); + expect(errors.contains('child 1 of "children" is not a map in list.filterPanel1: []'), + isTrue); + }); + test('missing type in child', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'pages': [ + { + "page": "list", + "pageType": "list", + "sections": [ + { + "sectionType": "filterPanel", + "children": [ + {}, + ] + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final errors = logger.errors; + expect(errors.length, equals(1)); + expect(errors.contains('child 1 of "children" does not have "widgetType" in list.filterPanel1: {}'), + isTrue); + }); + test('unknown type in child', () { + WidgetModel.lastId = 0; + logger.clear(); + final map = { + 'module': 'demo1', + 'pages': [ + { + "page": "list", + "pageType": "list", + "sections": [ + { + "sectionType": "filterPanel", + "children": [ + { + "widgetType": "" + }, + ] + }, + ], + }, + ], + }; + final module = ModuleModel(map, logger); + module.parse(); + final errors = logger.errors; + expect(errors.length, equals(1)); + expect(errors.contains('Section: unknown "widgetType" in list.filterPanel1'), + isTrue); + }); + }); } final userModel = { @@ -284,7 +624,7 @@ final userModel = { 'column': 'user_role', 'dataType': 'reference', 'label': 'Role', - 'foreignKey': 'role.role_id', + 'foreignKey': 'role.role_id role_name', 'widgetType': 'combobox', }, ] diff --git a/test/model/standard_test.dart b/test/model/standard_test.dart index e0cd215..7e07669 100644 --- a/test/model/standard_test.dart +++ b/test/model/standard_test.dart @@ -6,6 +6,7 @@ void main() { final logger = MemoryLogger(LEVEL_FINE); group('module', () { test('role', () { + WidgetModel.lastId = 0; logger.clear(); final module = RoleModel(logger); module.parse(); @@ -16,7 +17,7 @@ void main() { final dump = module.dump(StringBuffer()).toString(); expect(dump, '''= module role: options: == table role: options: - column role_id: DataType.int "Id" options: primary notnull unique + column role_id: DataType.int "Id" options: primary notnull unique readonly column role_name: DataType.string "Rolle" options: unique notnull column role_priority: DataType.int "Priorität" options: column role_active: DataType.bool "Aktiv" options: @@ -30,7 +31,7 @@ void main() { ] # create.simpleForm1 == page change: PageModelType.change options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields 13 options: + allDbFields 16 options: ] # change.simpleForm1 == page list: PageModelType.list options: = section filterPanel1: SectionModelType.filterPanel options: [ @@ -39,6 +40,7 @@ void main() { '''); }); test('user', () { + WidgetModel.lastId = 0; logger.clear(); final module = UserModel(logger); module.parse(); @@ -47,13 +49,13 @@ void main() { final errors = logger.errors; expect(errors.length, equals(0)); final dump = module.dump(StringBuffer()).toString(); - expect(dump, '''= module user: options: + expect(dump, equals('''= module user: options: == table user: options: - column user_id: DataType.int "Id" options: primary notnull unique + column user_id: DataType.int "Id" options: primary notnull unique readonly column user_name: DataType.string "User" options: unique notnull column user_displayname: DataType.string "Anzeigename" options: unique column user_email: DataType.string "EMail" options: unique - column user_password: DataType.string "User" options: password + column user_password: DataType.string "Passwort" options: password column user_role: DataType.reference "Role" options: column user_createdat: DataType.dateTime "Erzeugt" options: hidden null column user_createdby: DataType.string "Erzeugt von" options: hidden @@ -61,20 +63,21 @@ void main() { column user_changedby: DataType.string "Geändert von" options: hidden == page create: PageModelType.create options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields 31 options: + allDbFields 12 options: ] # create.simpleForm1 == page change: PageModelType.change options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields 34 options: + allDbFields 20 options: ] # change.simpleForm1 == page list: PageModelType.list options: = section filterPanel1: SectionModelType.filterPanel options: [ textField user_name: options: - combobox user_role: texts: options: + textField user_role: options: ] # list.filterPanel1 -'''); +''')); }); test('configuration', () { + WidgetModel.lastId = 0; logger.clear(); final module = ConfigurationModel(logger); module.parse(); @@ -83,10 +86,10 @@ void main() { final errors = logger.errors; expect(errors.length, equals(0)); final dump = module.dump(StringBuffer()).toString(); - expect(dump, startsWith('''= module configuration: options: + expect(dump, equals('''= module configuration: options: == table configuration: options: - column configuration_id: DataType.int "Id" options: primary notnull unique - column configuration_scope: DataType.string "Bereich" options: unique notnull + column configuration_id: DataType.int "Id" options: primary notnull unique readonly + column configuration_scope: DataType.string "Bereich" options: notnull column configuration_property: DataType.string "Eigenschaft" options: column configuration_order: DataType.int "Reihe" options: column configuration_type: DataType.string "Datentyp" options: @@ -98,17 +101,18 @@ void main() { column configuration_changedby: DataType.string "Geändert von" options: hidden == page create: PageModelType.create options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields''')); - /* - expect(dump.contains('''text: options: + allDbFields 13 options: ] # create.simpleForm1 == page change: PageModelType.change options: = section simpleForm1: SectionModelType.simpleForm options: [ - allDbFields '''), isTrue); - */ - expect(dump, - contains('combobox configuration_scope: texts: options: undef')); - expect(dump, contains('textField configuration_name: options:')); + allDbFields 22 options: + ] # change.simpleForm1 +== page list: PageModelType.list options: + = section filterPanel1: SectionModelType.filterPanel options: [ + textField configuration_scope: options: + textField configuration_property: options: + ] # list.filterPanel1 +''')); }); }); } diff --git a/test/page/application_test.dart b/test/page/application_test.dart new file mode 100644 index 0000000..02bdac1 --- /dev/null +++ b/test/page/application_test.dart @@ -0,0 +1,38 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/widget/page_controller_bones.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + final logger = MemoryLogger(LEVEL_FINE); + RestPersistence persistence; + BaseConfiguration configuration; + PageControllerBones pageControllerBones; + ApplicationData applicationData; + setUpAll(() { + configuration = BaseConfiguration({ + 'client': { + 'host': 'localhost', + 'port': 58011, + 'schema': 'http', + 'application': 'unittest', + 'version': '1.0.0', + } + }, logger); + persistence = RestPersistence.fromConfig(configuration, logger); + pageControllerBones = PageControllerBones( + null, null, UserModel(logger), 'list', null, applicationData); + applicationData = + ApplicationData(configuration, null, null, persistence, logger); + }); + group('Basics', () { + test('basic', () async { + expect(applicationData, isNotNull); + expect(pageControllerBones, isNotNull); + applicationData.pushCaller(pageControllerBones); + applicationData.popCaller(); + applicationData.setLastErrorMessage('list', 'True error'); + expect(applicationData.lastErrorMessage('list'), 'True error'); + }); + }); +} diff --git a/test/persistence_cache_test.dart b/test/persistence_cache_test.dart index 0488167..69e017e 100644 --- a/test/persistence_cache_test.dart +++ b/test/persistence_cache_test.dart @@ -29,6 +29,7 @@ void main() async { }); group('basics', () { test('combobox', () async { + logger.clear(); final cache2 = PersistenceCache(); expect(cache2, isNotNull); const key = 'id1.role.list;role_name role_id;:role_name=%'; @@ -37,9 +38,9 @@ void main() async { var data2 = cache.combobox(key2, hasUndef: false); expect(data, isNull); expect(data2, isNull); - await sleep(200); + await wait(millisec: 200); data = cache.combobox(key); - await sleep(200); + await wait(millisec: 200); data2 = cache.combobox(key2); expect(logger.errors.length, equals(0)); expect(data, isNotNull); @@ -52,9 +53,11 @@ void main() async { expect(cache.leastReasentlyUsed.length, equals(2)); const key3 = 'id3.role.list;role_name role_id;:role_name=%'; var data3 = cache.combobox(key3, hasUndef: false); + expect(data3, isNull); expect(cache.leastReasentlyUsed.length, equals(2)); expect(cache.map.containsKey(key), isFalse); expect(cache.map.containsKey(key2), isTrue); + expect(cache.deleteEntry(key3), isTrue); }); test('error', () { logger.clear(); @@ -63,9 +66,25 @@ void main() async { expect(logger.errors.length, equals(1)); expect(logger.contains('wrong key syntax: id1.role.list;role_name+role_id;:role_name=%'), isTrue); }); + test('record', () async { + logger.clear(); + final cache2 = PersistenceCache(); + expect(cache2, isNotNull); + const key = 'id1.role.by_role_name;:role_name=Administrator :excluded=0'; + const key2 = 'id2.role.by_role_name;:role_name=Admistrator :excluded=1'; + var data = cache.record(key, oneTime: true); + var data2 = cache.record(key2, oneTime: false); + expect(data, isNull); + expect(data2, isNull); + await wait(millisec: 200); + data = cache.record(key); + data2 = cache.record(key2); + await wait(millisec: 200); + expect(logger.errors.length, equals(0)); + expect(data, isNotNull); + expect(data.containsKey('role_id'), isTrue); + expect(data2.isEmpty, isTrue); + }); }); } -Future sleep(int millisec) { - return Future.delayed(Duration(milliseconds: millisec), () => null); -} diff --git a/test/rest_persistence_test.dart b/test/rest_persistence_test.dart index 2e35b4b..7a35beb 100644 --- a/test/rest_persistence_test.dart +++ b/test/rest_persistence_test.dart @@ -40,9 +40,19 @@ void main() async { expect(list.length, greaterThanOrEqualTo(4)); expect(list[0].containsKey('role_id'), isTrue); }); - test('record', () async { + test('recordById', () async { final rest = RestPersistence(); - final record = await rest.record(module: 'role', id: 2); + final record = await rest.recordById(module: 'role', id: 2); + expect(record, isNotNull); + expect(record.length, greaterThan(4)); + expect(record.containsKey('role_id'), isTrue); + }); + test('recordByName', () async { + final rest = RestPersistence(); + final record = await rest.recordByParameter( + module: 'role', + sqlName: 'by_role_name', + parameters: {':role_name': 'Administrator', ':excluded': 0}); expect(record, isNotNull); expect(record.length, greaterThan(4)); expect(record.containsKey('role_id'), isTrue); @@ -73,7 +83,7 @@ void main() async { ':role_active': 'T', ':role_changedby': 'eve' }); - final answer = rest.record(module: 'role', id: id); + final answer = rest.recordById(module: 'role', id: id); logger.log('log: $answer'); await rest.delete(module: 'role', id: id); }); diff --git a/test/tool/tool_test.dart b/test/tool/tool_test.dart index 55254ba..790b331 100644 --- a/test/tool/tool_test.dart +++ b/test/tool/tool_test.dart @@ -61,7 +61,7 @@ modules: sql: "SELECT * from role WHERE role_name=:role_name&&role_id!=:excluded;" - name: list type: list - sql: "SELECT * from role + sql: "SELECT t0.* from role t0 WHERE role_name like :role_name;" ''')); }); diff --git a/test/widget/widget_test.dart b/test/widget/widget_test.dart index d67b471..2c32b0b 100644 --- a/test/widget/widget_test.dart +++ b/test/widget/widget_test.dart @@ -10,14 +10,25 @@ import 'package:flutter/src/foundation/diagnostics.dart'; import 'package:flutter/src/widgets/framework.dart' as x; import 'package:flutter_bones/flutter_bones.dart'; import 'package:flutter_bones/src/widget/widget_helper.dart'; -import 'package:flutter_bones/src/private/bsettings.dart'; import 'package:test/test.dart'; void main() { final logger = MemoryLogger(); final widgetConfiguration = BaseConfiguration({}, logger); Settings(logger: logger, widgetConfiguration: widgetConfiguration); - + Persistence persistence; + setUpAll(() { + final configuration = BaseConfiguration({ + 'client': { + 'host': 'localhost', + 'port': 58011, + 'schema': 'http', + 'application': 'unittest', + 'version': '1.0.0', + } + }, logger); + persistence = RestPersistence.fromConfig(configuration, logger); + }); group('WidgetHelper', () { test('toolTip', () { final widget = Text('Hi'); @@ -29,9 +40,8 @@ void main() { }); group('ModuleController', () { test('basic', () { - ApplicationData appData = ApplicationData( - BaseConfiguration({}, logger), (title) => null, (context) => null, - BSettings.lastInstance.persistence, logger); + ApplicationData appData = ApplicationData(BaseConfiguration({}, logger), + (title) => null, (context) => null, persistence, logger); final role = RoleCreatePage(appData); if (appData.lastModuleState == null) { role.createState();