From: Hamatoma Date: Mon, 19 Oct 2020 06:05:46 +0000 (+0200) Subject: daily work: persistence works X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=3db96c4d487511259762c60dc5e2dcbec8b93321;p=flutter_bones.git daily work: persistence works --- diff --git a/data/ddl/configuration.sql b/data/ddl/configuration.sql new file mode 100644 index 0000000..dd4958d --- /dev/null +++ b/data/ddl/configuration.sql @@ -0,0 +1,15 @@ +DROP TABLE IF EXISTS configuration; +CREATE TABLE configuration ( + configuration_id INT(10) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT, + configuration_scope VARCHAR(64) UNIQUE NOT NULL, + configuration_property VARCHAR(64), + configuration_order INT(10), + configuration_type VARCHAR(16), + configuration_value VARCHAR(255), + configuration_description VARCHAR(255), + configuration_createdat TIMESTAMP NULL, + configuration_createdby VARCHAR(16), + configuration_changedat TIMESTAMP NULL, + configuration_changedby VARCHAR(16), + PRIMARY KEY(configuration_id) +); diff --git a/data/ddl/role.sql b/data/ddl/role.sql new file mode 100644 index 0000000..cba827e --- /dev/null +++ b/data/ddl/role.sql @@ -0,0 +1,12 @@ +DROP TABLE IF EXISTS role; +CREATE TABLE role ( + role_id INT(10) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT, + role_name VARCHAR(32) UNIQUE NOT NULL, + role_priority INT(10), + role_active CHAR(1), + role_createdat TIMESTAMP NULL, + role_createdby VARCHAR(16), + role_changedat TIMESTAMP NULL, + role_changedby VARCHAR(16), + PRIMARY KEY(role_id) +); diff --git a/data/ddl/user.sql b/data/ddl/user.sql new file mode 100644 index 0000000..84441b6 --- /dev/null +++ b/data/ddl/user.sql @@ -0,0 +1,14 @@ +DROP TABLE IF EXISTS user; +CREATE TABLE user ( + user_id INT(10) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT, + user_name VARCHAR(64) UNIQUE NOT NULL, + user_displayname VARCHAR(32) UNIQUE, + user_email VARCHAR(128) UNIQUE, + user_password VARCHAR(128), + user_role INT(10) UNSIGNED, + user_createdat TIMESTAMP NULL, + user_createdby VARCHAR(16), + user_changedat TIMESTAMP NULL, + user_changedby VARCHAR(16), + PRIMARY KEY(user_id) +); diff --git a/data/rest/role.yaml b/data/rest/role.yaml new file mode 100644 index 0000000..9d7cb3c --- /dev/null +++ b/data/rest/role.yaml @@ -0,0 +1,30 @@ +--- +# configuration of the bones backend for role: +created: 2020.10.20 15:25:01 +author: flutter_bones.module_model.exportSqlBackend() +version: 1.0.0 +modules: + - module: role + list: + - name: insert + type: insert + sql: "INSERT INTO role(role_name,role_priority,role_active,role_createdat,role_createdby) + VALUES(:role_name,:role_priority,:role_active,NOW(),:role_createdby);" + - name: update + type: update + sql: "UPDATE role SET + role_name=:role_name,role_priority=:role_priority,role_active=:role_active,role_changedat=NOW(),role_changedby=:role_changedby + WHERE role_id=:role_id;" + - name: delete + type: delete + sql: "DELETE from role WHERE role_id=:role_id;" + - name: record + type: record + sql: "SELECT * from role WHERE role_id=:role_id;" + - name: by_role_name + type: record + sql: "SELECT * from role WHERE role_name=:role_name&&role_id!=:excluded;" + - name: list + type: list + sql: "SELECT * from role + WHERE role_name like :role_name;" diff --git a/data/rest/user.yaml b/data/rest/user.yaml new file mode 100644 index 0000000..3a995c6 --- /dev/null +++ b/data/rest/user.yaml @@ -0,0 +1,36 @@ +--- +# configuration of the bones backend for user: +created: 2020.10.20 15:25:01 +author: flutter_bones.module_model.exportSqlBackend() +version: 1.0.0 +modules: + - module: user + list: + - name: insert + type: insert + sql: "INSERT INTO user(user_name,user_displayname,user_email,user_password,user_role,user_createdat,user_createdby) + VALUES(:user_name,:user_displayname,:user_email,:user_password,:user_role,NOW(),:user_createdby);" + - name: update + type: update + sql: "UPDATE user SET + user_name=:user_name,user_displayname=:user_displayname,user_email=:user_email,user_password=:user_password,user_role=:user_role,user_changedat=NOW(),user_changedby=:user_changedby + WHERE user_id=:user_id;" + - name: delete + type: delete + sql: "DELETE from user WHERE user_id=:user_id;" + - name: record + type: record + sql: "SELECT * from user WHERE user_id=:user_id;" + - name: by_user_name + type: record + sql: "SELECT * from user WHERE user_name=:user_name&&user_id!=:excluded;" + - name: by_user_displayname + type: record + sql: "SELECT * from user WHERE user_displayname=:user_displayname&&user_id!=:excluded;" + - name: by_user_email + type: record + sql: "SELECT * from user WHERE user_email=:user_email&&user_id!=:excluded;" + - 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;" diff --git a/lib/db_tool.dart b/lib/db_tool.dart index 7c4c080..48299f4 100644 --- a/lib/db_tool.dart +++ b/lib/db_tool.dart @@ -44,7 +44,7 @@ class DbHelper { FileSync.ensureDirectory(dirDDL); FileSync.ensureDirectory(dirREST); if (args.isEmpty) { - args = ['user', 'role']; + args = ['user', 'role', 'configuration']; } while (args.isNotEmpty) { final name = args[0]; @@ -57,6 +57,9 @@ class DbHelper { case 'role': module = RoleModel(logger); break; + case 'configuration': + module = ConfigurationModel(logger); + break; default: logger.error('unknown table'); break; diff --git a/lib/flutter_bones.dart b/lib/flutter_bones.dart index 0d8fa51..3dd8821 100644 --- a/lib/flutter_bones.dart +++ b/lib/flutter_bones.dart @@ -15,7 +15,9 @@ export 'src/model/model_types.dart'; export 'src/model/module_model.dart'; export 'src/model/page_model.dart'; export 'src/model/section_model.dart'; +export 'src/model/standard/configuration_model.dart'; export 'src/model/standard/role_model.dart'; +export 'src/model/standard/standard_modules.dart'; export 'src/model/standard/user_model.dart'; export 'src/model/text_field_model.dart'; export 'src/model/text_model.dart'; @@ -24,6 +26,8 @@ export 'src/page/login_page.dart'; export 'src/page/page_data.dart'; export 'src/page/role/role_create_page.dart'; export 'src/page/user_page.dart'; +export 'src/persistence/persistence.dart'; +export 'src/persistence/rest_persistence.dart'; export 'src/widget/raised_button_bone.dart'; export 'src/widget/simple_form.dart'; export 'src/widget/text_form_field_bone.dart'; diff --git a/lib/src/controller/base_controller.dart b/lib/src/controller/base_controller.dart new file mode 100644 index 0000000..46b9533 --- /dev/null +++ b/lib/src/controller/base_controller.dart @@ -0,0 +1,11 @@ +import 'package:flutter_bones/flutter_bones.dart'; + +/// Base class of all controller classes in this package. +abstract class BaseController { + /// Returns the assigned model. + WidgetModel getModel(); + + /// Returns a unique name of the controller inside the module, mostly the + /// widget name of the assigned model. + String getName(); +} diff --git a/lib/src/controller/button_controller.dart b/lib/src/controller/button_controller.dart new file mode 100644 index 0000000..7d5347c --- /dev/null +++ b/lib/src/controller/button_controller.dart @@ -0,0 +1,48 @@ +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/controller/base_controller.dart'; + +import '../widget/raised_button_bone.dart'; + +class ButtonController extends BaseController + implements ButtonCallbackController { + ButtonModel model; + + ButtonController(this.model); + + @override + WidgetModel getModel() { + return model; + } + + @override + String getName() { + return model.widgetName(); + } + + @override + getOnHighlightChanged( + String customString, ButtonCallbackController controller) { + return null; + } + + @override + getOnLongPressed(String customString, ButtonCallbackController controller) { + return null; + } + + @override + getOnPressed(String customString, ButtonCallbackController controller) { + var rc; + switch (model.buttonModelType) { + case ButtonModelType.cancel: + break; + case ButtonModelType.custom: + break; + case ButtonModelType.search: + break; + case ButtonModelType.store: + break; + } + return rc; + } +} diff --git a/lib/src/controller/combobox_controller.dart b/lib/src/controller/combobox_controller.dart new file mode 100644 index 0000000..322f642 --- /dev/null +++ b/lib/src/controller/combobox_controller.dart @@ -0,0 +1,57 @@ +import 'package:flutter_bones/flutter_bones.dart'; + +import '../widget/dropdown_button_form_bone.dart'; +import 'base_controller.dart'; + +class ComboboxController extends BaseController + implements ComboboxCallbackController { + ComboboxModel model; + + ComboboxController(this.model); + + @override + WidgetModel getModel() { + return model; + } + + @override + String getName() { + return model.widgetName(); + } + + @override + getOnChanged(String customString, ComboboxCallbackController controller) { + return null; + } + + @override + getOnSaved(String customString, ComboboxCallbackController controller) { + return null; + } + + @override + getOnSelectedItemBuilder( + String customString, ComboboxCallbackController controller) { + return null; + } + + @override + getOnTap(String customString, ComboboxCallbackController controller) { + return null; + } + + @override + getOnValidator(String customString, ComboboxCallbackController controller) { + return null; + } + + @override + List texts() { + return model.texts; + } + + @override + List values() { + return model.values; + } +} diff --git a/lib/src/controller/text_form_controller.dart b/lib/src/controller/text_form_controller.dart new file mode 100644 index 0000000..d7cd775 --- /dev/null +++ b/lib/src/controller/text_form_controller.dart @@ -0,0 +1,53 @@ +import 'package:flutter_bones/flutter_bones.dart'; + +import '../widget/text_form_field_bone.dart'; +import 'base_controller.dart'; + +class TextFormController extends BaseController + implements TextFormCallbackController { + ComboboxModel model; + + TextFormController(this.model); + + @override + WidgetModel getModel() { + return model; + } + + @override + String getName() { + return model.widgetName(); + } + + @override + getOnChanged(String customString, TextFormCallbackController controller) { + return null; + } + + @override + getOnEditingComplete( + String customString, TextFormCallbackController controller) { + return null; + } + + @override + getOnFieldSubmitted( + String customString, TextFormCallbackController controller) { + return null; + } + + @override + getOnSaved(String customString, TextFormCallbackController controller) { + return null; + } + + @override + getOnTap(String customString, TextFormCallbackController controller) { + return null; + } + + @override + getValidator(String customString, TextFormCallbackController controller) { + return null; + } +} diff --git a/lib/src/helper/settings.dart b/lib/src/helper/settings.dart index 2836e36..a3dfb5a 100644 --- a/lib/src/helper/settings.dart +++ b/lib/src/helper/settings.dart @@ -1,39 +1,36 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; -class Settings { +class BaseSettings { static Locale locale = Locale('US', 'en'); - static final mapWidgetData = { - 'form.card.padding': '16.0', - 'form.gap.field_button.height': '16.0', - }; - static Settings _instance; + static get language => locale.languageCode; - final BaseConfiguration widgetConfiguration; + final BaseConfiguration configuration; final BaseLogger logger; - factory Settings({BaseLogger logger, BaseConfiguration widgetConfiguration}) { - if (_instance == null) { - _instance = Settings.internal( - widgetConfiguration == null - ? BaseConfiguration(mapWidgetData, logger) - : widgetConfiguration, - logger); - } - return _instance; - } - - Settings.internal(this.widgetConfiguration, this.logger); + BaseSettings(this.configuration, this.logger); /// Sets the locale code. /// [locale] the info about localisation /// Finding [locale] of an app: @see https://github.com/flutter/website/blob/master/examples/internationalization/minimal/lib/main.dart - static setLocale(locale) => Settings.locale = locale; + static setLocale(locale) { + BaseSettings.locale = locale; + Intl.defaultLocale = "${locale.languageCode}_$locale.countryCode"; + initializeDateFormatting(); + } - static setLocaleByNames({String country = 'US', String language = 'en'}) => - Settings.locale = Locale(country, language); + /// Sets the locales with [language] and [country]. + static setLocaleByNames({String country, @required String language}) { + country ??= language; + country = country.toUpperCase(); + country = country == 'EN' ? 'US' : country; + language = language.toLowerCase(); + setLocale(Locale(language, country)); + } /// Translates a [text] with a given translation [map]. /// Structure of the [map]: { : { : translation } } @@ -52,3 +49,25 @@ class Settings { return rc; } } + +class Settings extends BaseSettings { + static final mapWidgetData = { + 'form.card.padding': '16.0', + 'form.gap.field_button.height': '16.0', + }; + static Settings _instance; + + factory Settings({BaseLogger logger, BaseConfiguration widgetConfiguration}) { + if (_instance == null) { + _instance = Settings.internal( + widgetConfiguration == null + ? BaseConfiguration(mapWidgetData, logger) + : widgetConfiguration, + logger); + } + return _instance; + } + + Settings.internal(BaseConfiguration configuration, BaseLogger logger) + : super(configuration, logger); +} diff --git a/lib/src/helper/validators.dart b/lib/src/helper/validators.dart index 7bb567b..351bf22 100644 --- a/lib/src/helper/validators.dart +++ b/lib/src/helper/validators.dart @@ -46,7 +46,7 @@ String checkPhoneNumber(String input) => Validation.isPhoneNumber(input) {'0': input}); String _vt(String key, [Map placeholders]) => - Settings.translate(key, ValidatorTranslations.translations, + BaseSettings.translate(key, ValidatorTranslations.translations, placeholders: placeholders); @protected @@ -63,7 +63,7 @@ class ValidatorTranslations { }, 'Not a phone number: %{0} Examples: "089-123452 "+49-89-12345"': { 'de': - 'Keine gültige Telefonnummer: %{0} Beispiele: "089-123452 "+49-89-12345"', + 'Keine gültige Telefonnummer: %{0} Beispiele: "089-123452 "+49-89-12345"', }, }; } diff --git a/lib/src/model/button_model.dart b/lib/src/model/button_model.dart index eb5721c..3fba73b 100644 --- a/lib/src/model/button_model.dart +++ b/lib/src/model/button_model.dart @@ -5,7 +5,7 @@ import 'package:flutter_bones/flutter_bones.dart'; typedef ButtonOnPressed = void Function(String name); /// Describes a button widget. -class ButtonModel extends WidgetModel implements ButtonCallbackController { +class ButtonModel extends WidgetModel { static final regExprOptions = RegExp(r'^(undef)$'); String text; String name; @@ -48,22 +48,6 @@ class ButtonModel extends WidgetModel implements ButtonCallbackController { @override String widgetName() => name; - - @override - getOnHighlightChanged( - String customString, ButtonCallbackController controller) { - return null; - } - - @override - getOnLongPressed(String customString, ButtonCallbackController controller) { - return null; - } - - @override - getOnPressed(String customString, ButtonCallbackController controller) { - return onPressed; - } } enum ButtonModelType { diff --git a/lib/src/model/checkbox_model.dart b/lib/src/model/checkbox_model.dart index 78990d1..79c4c16 100644 --- a/lib/src/model/checkbox_model.dart +++ b/lib/src/model/checkbox_model.dart @@ -26,8 +26,8 @@ class CheckboxModel extends FieldModel { /// Dumps the instance into a [StringBuffer] StringBuffer dump(StringBuffer stringBuffer) { - stringBuffer.write( - ' checkbox $name: text: options: ${listToString(options)}\n'); + stringBuffer + .write(' checkbox $name: text: options: ${listToString(options)}\n'); return stringBuffer; } } diff --git a/lib/src/model/combobox_model.dart b/lib/src/model/combobox_model.dart index 395c660..695a47d 100644 --- a/lib/src/model/combobox_model.dart +++ b/lib/src/model/combobox_model.dart @@ -32,7 +32,7 @@ class ComboboxModel extends FieldModel { super.parse(); checkSuperfluousAttributes( map, - 'name label dataType options texts toolTip widgetType values' + 'name label dataType filterType options texts toolTip widgetType values' .split(' ')); texts = parseStringList('texts', map); values = parseValueList('values', map, dataType); diff --git a/lib/src/model/field_model.dart b/lib/src/model/field_model.dart index d9185fb..5d5b0c8 100644 --- a/lib/src/model/field_model.dart +++ b/lib/src/model/field_model.dart @@ -9,8 +9,6 @@ import 'widget_model.dart'; /// Base class of widgets with user interaction like text field, combobox, checkbox. abstract class FieldModel extends WidgetModel { - static final regExprOptions = - RegExp(r'^(undef|readonly|disabled|password|required)$'); String name; String label; String toolTip; diff --git a/lib/src/model/module_model.dart b/lib/src/model/module_model.dart index 2d781e2..039affe 100644 --- a/lib/src/model/module_model.dart +++ b/lib/src/model/module_model.dart @@ -68,8 +68,8 @@ class ModuleModel extends ModelBase { buffer.write(''' sql: "INSERT INTO ${table.name}('''); final columns = table.columns .where((col) => - col.hasOption('doStore') || - (!col.hasOption('primary') && (!col.hasOption('hidden')))) + col.hasOption('doStore') || + (!col.hasOption('primary') && (!col.hasOption('hidden')))) .toList(); addColumnIfMissing(columns, table.columns, '${table.name}_createdat'); addColumnIfMissing(columns, table.columns, '${table.name}_createdby'); @@ -91,8 +91,8 @@ class ModuleModel extends ModelBase { /// Writes the list SQL part into the [buffer]. void exportList(StringBuffer buffer, TableModel table) { - final page = pages - .firstWhere((element) => element.pageModelType == PageModelType.list, + final page = pages.firstWhere( + (element) => element.pageModelType == PageModelType.list, orElse: () => null); if (page != null) { buffer.write(''' - name: list @@ -119,6 +119,9 @@ class ModuleModel extends ModelBase { case FilterType.dateTimeTil: buffer.write('${field.name}<=:${field.name}'); break; + case FilterType.equals: + buffer.write('${field.name} like :${field.name}'); + break; case FilterType.pattern: buffer.write('${field.name} like :${field.name}'); break; @@ -130,6 +133,29 @@ class ModuleModel extends ModelBase { } } + /// Writes the record SQL part into the [buffer]. + void exportRecord(StringBuffer buffer, TableModel table) { + exportRecordOne( + 'record', '${table.name}_id=:${table.name}_id', buffer, table); + table.columns + .where((col) => col.hasOption('unique') && !col.hasOption('primary')) + .forEach((col) { + exportRecordOne( + 'by_${col.name}', + '${col.name}=:${col.name}&&${table.name}_id!=:excluded', buffer, + table); + }); + } + + /// Writes the record SQL part into the [buffer]. + void exportRecordOne(String sqlName, String condition, StringBuffer buffer, + TableModel table) { + buffer.write(' - name: $sqlName\n'); + buffer.write(' type: record\n'); + buffer.write( + ' sql: "SELECT * from ${table.name} WHERE $condition;"\n'); + } + /// Exports a YAML file (as String) describing the SQL statements for insert, update ... /// testDate: the fix date 2020.01.01 will be used (for unit tests) String exportSqlBackend({bool testDate: false}) { @@ -158,10 +184,8 @@ modules: buffer.write(''' - name: delete type: delete sql: "DELETE from ${table.name} WHERE ${table.name}_id=:${table.name}_id;" - - name: record - type: record - sql: "SELECT * from ${table.name} WHERE ${table.name}_id=:${table.name}_id;" '''); + exportRecord(buffer, table); exportList(buffer, table); } return buffer.toString(); @@ -192,7 +216,7 @@ modules: type = 'DOUBLE'; break; case DataType.int: - type = 'INT(10)'; + type = column.hasOption('primary') ? 'INT(10) UNSIGNED' : 'INT(10)'; break; case DataType.reference: type = 'INT(10) UNSIGNED'; @@ -211,10 +235,15 @@ modules: } String options = ''; for (var option in column.options) { - if ('notnull null unique'.contains(option)) { - options += ' ' + option; + if (option == 'notnull') { + options += ' NOT NULL'; + } else if ('null unique'.contains(option)) { + options += ' ' + option.toUpperCase(); } } + if (column.hasOption('primary')) { + options += ' AUTO_INCREMENT'; + } buffer.write(' ${column.name} $type$options,\n'); } buffer.write(' PRIMARY KEY(${table.name}_id)\n'); @@ -236,13 +265,13 @@ modules: addColumnIfMissing(columns, table.columns, '${table.name}_changedby'); buffer.write(columns.fold( '', - (prev, col) => - (prev as String) + + (prev, col) => + (prev as String) + ((prev as String).isEmpty ? '' : ',') + col.name + '=' + ((col.name.endsWith('changedat') ? 'NOW()' : ':' + col.name)))); - buffer.write('\n WHERE ${table.name}_id=:${table.name}_id";\n'); + buffer.write('\n WHERE ${table.name}_id=:${table.name}_id;"\n'); } /// Returns the name including the names of the parent @@ -279,8 +308,8 @@ modules: /// Returns a child page given by [name], null otherwise. PageModel pageByName(String name) { - final found = pages.firstWhere((element) => element.name == name, - orElse: () => null); + final found = + pages.firstWhere((element) => element.name == name, orElse: () => null); return found; } diff --git a/lib/src/model/page_model.dart b/lib/src/model/page_model.dart index eca7fdb..cc1fc91 100644 --- a/lib/src/model/page_model.dart +++ b/lib/src/model/page_model.dart @@ -1,5 +1,4 @@ import 'package:dart_bones/dart_bones.dart'; -import 'package:flutter/foundation.dart'; import 'button_model.dart'; import 'field_model.dart'; @@ -19,9 +18,7 @@ class PageModel extends ModelBase { final List sections = []; PageModelType pageModelType; List options; - @protected final fields = {}; - @protected final buttons = {}; final widgets = []; diff --git a/lib/src/model/section_model.dart b/lib/src/model/section_model.dart index cce657e..c1955be 100644 --- a/lib/src/model/section_model.dart +++ b/lib/src/model/section_model.dart @@ -30,8 +30,7 @@ class SectionModel extends WidgetModel { /// Dumps the internal structure into a [stringBuffer] StringBuffer dump(StringBuffer stringBuffer) { stringBuffer.write( - ' = section $name: $sectionModelType options: ${options.join( - ' ')} [\n'); + ' = section $name: $sectionModelType options: ${options.join(' ')} [\n'); for (var child in children) { child.dump(stringBuffer); } diff --git a/lib/src/model/standard/configuration_model.dart b/lib/src/model/standard/configuration_model.dart new file mode 100644 index 0000000..fcc66a6 --- /dev/null +++ b/lib/src/model/standard/configuration_model.dart @@ -0,0 +1,122 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +/// +class ConfigurationModel extends ModuleModel { + static final yamlMap = { + // + "module": "configuration", + "tables": [ + { + // configuration_id | configuration_scope | configuration_property | configuration_order | configuration_type + // | configuration_value | configuration_description + 'table': 'configuration', + 'columns': [ + { + 'column': 'configuration_id', + 'dataType': 'int', + 'label': 'Id', + 'options': 'primary', + }, + { + 'column': 'configuration_scope', + 'dataType': 'string', + 'label': 'Bereich', + 'size': 64, + 'options': 'unique;notnull', + }, + { + 'column': 'configuration_property', + 'dataType': 'string', + 'size': 64, + 'label': 'Eigenschaft', + }, + { + 'column': 'configuration_order', + 'dataType': 'int', + 'label': 'Reihe', + }, + { + 'column': 'configuration_type', + 'dataType': 'string', + 'size': 16, + 'label': 'Datentyp', + }, + { + 'column': 'configuration_value', + 'dataType': 'string', + 'size': 255, + 'label': 'Wert', + }, + { + 'column': 'configuration_description', + 'dataType': 'string', + 'size': 255, + 'label': 'Beschreibung', + }, + ] + }, + ], + 'pages': [ + { + "page": "create", + "pageType": "create", + "sections": [ + { + "sectionType": "simpleForm", + "children": [ + { + "widgetType": "allDbFields", + } + ] + } + ] + }, + { + "page": "change", + "pageType": "change", + "sections": [ + { + "sectionType": "simpleForm", + "children": [ + { + "widgetType": "allDbFields", + } + ] + } + ] + }, + { + "page": "list", + "pageType": "list", + "sections": [ + { + "sectionType": "filterPanel", + "children": [ + { + "name": "configuration_name", + "widgetType": "textField", + "filterType": "pattern", + }, + { + "name": "configuration_scope", + "widgetType": "combobox", + "filterType": "equals", + "options": 'undef', + }, + ] + } + ] + }, + ], + }; + + ConfigurationModel(BaseLogger logger) : super(yamlMap, logger); + + /// Returns the name including the names of the parent + @override + String fullName() => name; + + @override + String widgetName() => name; +} diff --git a/lib/src/model/standard/standard_modules.dart b/lib/src/model/standard/standard_modules.dart new file mode 100644 index 0000000..7eda53f --- /dev/null +++ b/lib/src/model/standard/standard_modules.dart @@ -0,0 +1,25 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +/// Returns an instance of a standard module given by [name]. +ModuleModel standardModule(String name, BaseLogger logger) { + var rc; + switch (name) { + case 'role': + rc = RoleModel(logger); + break; + case 'user': + rc = UserModel(logger); + break; + case 'configuration': + rc = ConfigurationModel(logger); + break; + default: + logger.error('unknown standard module: $name'); + break; + } + return rc; +} + +/// Returns the names of the standard modules. +List standardModules() => ['configuration', 'role', 'user']; diff --git a/lib/src/model/table_model.dart b/lib/src/model/table_model.dart index 3deafec..3be6eff 100644 --- a/lib/src/model/table_model.dart +++ b/lib/src/model/table_model.dart @@ -75,14 +75,14 @@ class TableModel extends ModelBase { } } checkOptionsByRegExpr(options, regExprOptions); - _addIfMissing('${name}_createdat', 'Erzeugt', DataType.dateTime, - ['hidden', 'null', 'doStore']); - _addIfMissing('${name}_createdby', 'Erzeugt von', DataType.string, - ['hidden', 'doStore'], 16); - _addIfMissing('${name}_changedat', 'Geändert', DataType.dateTime, - ['hidden', 'null', 'doStore']); - _addIfMissing('${name}_changedby', 'Geändert von', DataType.string, - ['hidden', 'doStore'], 16); + _addIfMissing( + '${name}_createdat', 'Erzeugt', DataType.dateTime, ['hidden', 'null']); + _addIfMissing( + '${name}_createdby', 'Erzeugt von', DataType.string, ['hidden'], 16); + _addIfMissing( + '${name}_changedat', 'Geändert', DataType.dateTime, ['hidden', 'null']); + _addIfMissing( + '${name}_changedby', 'Geändert von', DataType.string, ['hidden'], 16); } @override diff --git a/lib/src/model/widget_model.dart b/lib/src/model/widget_model.dart index e7f75cc..269964b 100644 --- a/lib/src/model/widget_model.dart +++ b/lib/src/model/widget_model.dart @@ -16,6 +16,7 @@ abstract class WidgetModel extends ModelBase { : super(logger) { this.id = ++lastId; } + /// Dumps the internal structure into a [stringBuffer] StringBuffer dump(StringBuffer stringBuffer); diff --git a/lib/src/page/login_page.dart b/lib/src/page/login_page.dart index d03a477..a8282df 100644 --- a/lib/src/page/login_page.dart +++ b/lib/src/page/login_page.dart @@ -4,7 +4,8 @@ import 'package:flutter_bones/flutter_bones.dart'; class LoginPage extends StatefulWidget { final PageData pageData; - LoginPage(this.pageData, { Key key }) : super(key: key); + LoginPage(this.pageData, {Key key}) : super(key: key); + @override LoginPageState createState() { // LoginPageState.setPageData(pageData); @@ -14,8 +15,9 @@ class LoginPage extends StatefulWidget { } } -class LoginPageState extends State{ +class LoginPageState extends State { LoginPageState(this.pageData); + final PageData pageData; final GlobalKey _formKey = GlobalKey(); @@ -25,7 +27,7 @@ class LoginPageState extends State{ Widget build(BuildContext context) { final user = LoginUser(); return Scaffold( - appBar: pageData.appBarBuilder('login'), + appBar: pageData.appBarBuilder('login'), drawer: pageData.drawerBuilder(context), body: SimpleForm.simpleForm( key: _formKey, @@ -38,7 +40,10 @@ class LoginPageState extends State{ onSaved: (input) => user.name = input, ), TextFormField( - validator: (input) => Validation.isEmail(input) || Validation.isPhoneNumber(input) ? null : 'keine Emailadresse und keine Telefonnummer: $input', + validator: (input) => + Validation.isEmail(input) || Validation.isPhoneNumber(input) + ? null + : 'keine Emailadresse und keine Telefonnummer: $input', decoration: InputDecoration(labelText: 'Passwort'), onSaved: (input) => user.password = input, obscureText: true, @@ -64,4 +69,4 @@ class LoginPageState extends State{ class LoginUser { String name; String password; -} \ No newline at end of file +} diff --git a/lib/src/page/page_data.dart b/lib/src/page/page_data.dart index 70ad597..646e317 100644 --- a/lib/src/page/page_data.dart +++ b/lib/src/page/page_data.dart @@ -4,12 +4,14 @@ import 'package:flutter/material.dart'; /// Data class for storing parameter to build a page. class PageData { BaseConfiguration configuration; + /// Signature: AppBar func(String title) AppBar Function(String title) appBarBuilder; Drawer Function(dynamic context) drawerBuilder; + /// Constructor. /// [configuration] is a map with the widget data (e.g. padding) /// [appBarBuilder] is a factory of a function returning a outside designed AppBar /// [drawerBuilder] is a factory of a function returning a Drawer which handles the "Hamburger menu" PageData(this.configuration, this.appBarBuilder, this.drawerBuilder); -} \ No newline at end of file +} diff --git a/lib/src/page/role/role_list_page.dart b/lib/src/page/role/role_list_page.dart index f8aa522..5d26db0 100644 --- a/lib/src/page/role/role_list_page.dart +++ b/lib/src/page/role/role_list_page.dart @@ -85,7 +85,8 @@ class RoleListPageState extends State { showEditIcon: true, buttons: [ RaisedButtonBone( - 'search', filters, + 'search', + filters, child: Text('Suchen'), ), ], diff --git a/lib/src/page/user_page.dart b/lib/src/page/user_page.dart index 85166d4..127a3cd 100644 --- a/lib/src/page/user_page.dart +++ b/lib/src/page/user_page.dart @@ -4,7 +4,8 @@ import 'package:flutter_bones/flutter_bones.dart'; class UserPage extends StatefulWidget { final PageData pageData; - UserPage(this.pageData, { Key key }) : super(key: key); + UserPage(this.pageData, {Key key}) : super(key: key); + @override UserPageState createState() { // UserPageState.setPageData(pageData); @@ -14,8 +15,9 @@ class UserPage extends StatefulWidget { } } -class UserPageState extends State{ +class UserPageState extends State { UserPageState(this.pageData); + final PageData pageData; final GlobalKey _formKey = GlobalKey(); @@ -25,7 +27,7 @@ class UserPageState extends State{ Widget build(BuildContext context) { final user = User(); return Scaffold( - appBar: pageData.appBarBuilder('Benutzer'), + appBar: pageData.appBarBuilder('Benutzer'), drawer: pageData.drawerBuilder(context), body: SimpleForm.simpleForm( key: _formKey, @@ -37,7 +39,10 @@ class UserPageState extends State{ onSaved: (input) => user.name = input, ), TextFormField( - validator: (input) => Validation.isEmail(input) || Validation.isPhoneNumber(input) ? null : 'keine Emailadresse und keine Telefonnummer: $input', + validator: (input) => + Validation.isEmail(input) || Validation.isPhoneNumber(input) + ? null + : 'keine Emailadresse und keine Telefonnummer: $input', decoration: InputDecoration(labelText: 'Passwort'), onSaved: (input) => user.password = input, obscureText: true, @@ -66,4 +71,4 @@ class User { String displayName; String password; int role; -} \ No newline at end of file +} diff --git a/lib/src/persistence/persistence.dart b/lib/src/persistence/persistence.dart new file mode 100644 index 0000000..f548a49 --- /dev/null +++ b/lib/src/persistence/persistence.dart @@ -0,0 +1,38 @@ +import 'package:meta/meta.dart'; + +abstract class Persistence { + /// Makes a customized query for [module] with a SQL statement named [sqlName] + /// and the [sqlType] using some [params]. + /// Returns null or a record (as Map) (if sqlType == 'record') + /// or a list of records (List). + Future customQuery( + {@required String module, + @required String sqlName, + @required String sqlType, + Map params}); + + /// Deletes a record with primary key [id] of the [module] with the + /// SQL statement [sqlName]. + Future delete({@required String module, String sqlName, @required int id}); + + /// Inserts a record of the [module] with the SQL statement [sqlName] + /// and returns the primary key. + Future insert( + {@required String module, + String sqlName, + @required Map data}); + + /// Returns all records of the [module] specified with [params] using + /// a SQL statement named [sqlName]. + Future list( + {@required String module, String sqlName, Map params}); + + /// Returns a record with primary key [id] of the [module] with the + /// SQL statement [sqlName]. + Future> record({@required String module, String sqlName, @required int id}); + + /// Updates a record of [module] with the [data] using the SQL statement [sqlName]. + Future update({@required String module, + String sqlName, + @required Map data}); +} diff --git a/lib/src/persistence/rest_persistence.dart b/lib/src/persistence/rest_persistence.dart new file mode 100644 index 0000000..68896af --- /dev/null +++ b/lib/src/persistence/rest_persistence.dart @@ -0,0 +1,160 @@ +import 'dart:convert' as convert; + +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/src/persistence/persistence.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; + +/// A persistence layer using a REST interface to store/fetch the data. +class RestPersistence extends Persistence { + static RestPersistence _instance; + final String application; + final String version; + final BaseLogger logger; + final String host; + int sqlTraceLimit = 80; + + final String scheme; + int port; + int sessionTimeout; + String uriPrefix; + + Map headers; + + factory RestPersistence( + {String application, + String version, + int port, + String host, + BaseLogger logger}) { + _instance ??= RestPersistence.internal( + application: application, + host: host, + port: port, + version: version, + logger: logger); + return _instance; + } + + factory RestPersistence.fromConfig( + BaseConfiguration configuration, BaseLogger logger) { + final section = 'client'; + _instance = RestPersistence.internal( + application: configuration.asString('application', section: section), + version: configuration.asString('version', section: section), + host: configuration.asString('host', section: section), + port: configuration.asInt('port', section: section), + scheme: configuration.asString('schema', + section: section, defaultValue: 'https'), + logger: logger); + + return _instance; + } + + RestPersistence.internal({this.application, + @required this.version, + @required this.host, + @required this.port, + this.scheme = 'http', + @required this.logger}) { + uriPrefix = '$scheme://$host:$port'; + } + + @override + Future customQuery({String module, + String sqlName, + String sqlType, + Map params}) async { + var rc; + assert(['list', 'record', 'update'].contains(sqlType)); + final params2 = params == null ? '{}' : convert.jsonEncode(params); + final answer = await runRequest(module, sqlName, sqlType, body: params2); + if (answer.isNotEmpty) { + rc = convert.jsonDecode(answer); + } + if (logger.logLevel >= LEVEL_FINE) { + logger.log('answer: ${StringUtils.limitString(answer, sqlTraceLimit)}'); + } + return rc; + } + + @override + Future delete({String module, String sqlName, int id}) async { + sqlName ??= 'delete'; + await runRequest(module, sqlName, 'delete', body: '{":${module}_id":$id}'); + } + + @override + Future insert( + {String module, String sqlName, Map data}) async { + sqlName ??= 'insert'; + var rc = 0; + final data2 = data == null ? '{}' : convert.jsonEncode(data); + final answer = await runRequest(module, sqlName, 'insert', body: data2); + if (answer.isNotEmpty) { + final map = convert.jsonDecode(answer); + rc = map['id']; + } + return rc; + } + + @override + Future list( + {String module, String sqlName, Map params}) async { + sqlName ??= 'list'; + List rc; + final answer = await runRequest(module, sqlName, 'list', + body: params == null ? '{}' : convert.jsonEncode(params)); + if (answer.isNotEmpty) { + rc = convert.jsonDecode(answer); + } + if (logger.logLevel >= LEVEL_FINE) { + logger.log('answer: ${StringUtils.limitString(answer, sqlTraceLimit)}'); + } + return rc; + } + + @override + Future> record( + {String module, String sqlName, int id}) async { + sqlName ??= 'record'; + Map rc; + final answer = await runRequest(module, sqlName, 'record', + body: '{":${module}_id":$id}'); + 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. + /// [sqlType]: 'insert', 'record', 'update', 'list', 'delete' + /// [body]: null or the body of the POST request, e.g. '{ 'role_id' + /// [headers]: the optional HTTP headers. + /// [result]: the body of the response or '' + Future runRequest(String module, String sqlName, String sqlType, + {String body, Map headers}) async { + var rc = ''; + final uri = '$uriPrefix/$application/$module/$sqlType/$sqlName/$version'; + http.Response response; + logger.log('request: POST $module $sqlName', LEVEL_LOOP); + response = await http.post(uri, body: body, headers: headers); + logger.log('status: ${response.statusCode}', LEVEL_LOOP); + if (response.statusCode != 200) { + logger.error('$uri: status: ${response.statusCode}'); + } else { + rc = response.body ?? ''; + } + return rc; + } + + @override + Future update( + {String module, String sqlName, Map data}) async { + sqlName ??= 'update'; + final data2 = data == null ? '{}' : convert.jsonEncode(data); + await runRequest(module, sqlName, 'update', body: data2); + } +} diff --git a/lib/src/private/bdrawer.dart b/lib/src/private/bdrawer.dart index 544dd48..58444f1 100644 --- a/lib/src/private/bdrawer.dart +++ b/lib/src/private/bdrawer.dart @@ -53,6 +53,7 @@ class VFrageDrawer extends Drawer { )); return rc; } + static Widget buildListView(context) { final list = MenuItem.menuItems(); final rc = Card( diff --git a/lib/src/widget/dropdown_button_form_bone.dart b/lib/src/widget/dropdown_button_form_bone.dart new file mode 100644 index 0000000..3bbedf6 --- /dev/null +++ b/lib/src/widget/dropdown_button_form_bone.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +/// Implements a [DropdownButtonFormField] with "outsourced" callbacks: +/// [customString] a string mostly used for a name needed in the [customController] +/// [callbackController] handles the callback methods. +class DropdownButtonFormBone extends DropdownButtonFormField { + final String customString; + final ComboboxCallbackController callbackController; + + DropdownButtonFormBone( + this.customString, + this.callbackController, { + Key key, + @required List> items, + T value, + Widget hint, + Widget disabledHint, + int elevation: 8, + TextStyle style, + Widget icon, + Color iconDisabledColor, + Color iconEnabledColor, + double iconSize: 24.0, + bool isDense: true, + bool isExpanded: false, + double itemHeight, + Color focusColor, + FocusNode focusNode, + bool autofocus: false, + Color dropdownColor, + InputDecoration decoration, + FormFieldSetter onSaved, + FormFieldValidator validator, + AutovalidateMode autovalidateMode, + }) : super( + key: key, + items: items, + selectedItemBuilder: callbackController.getOnSelectedItemBuilder( + customString, callbackController), + value: value, + hint: hint, + disabledHint: disabledHint, + onChanged: + callbackController.getOnChanged(customString, callbackController), + onTap: callbackController.getOnTap(customString, callbackController), + elevation: elevation, + style: style, + icon: icon, + iconDisabledColor: iconDisabledColor, + iconEnabledColor: iconEnabledColor, + iconSize: iconSize, + isDense: isDense, + isExpanded: isExpanded, + itemHeight: itemHeight, + focusColor: focusColor, + focusNode: focusNode, + autofocus: autofocus, + dropdownColor: dropdownColor, + decoration: decoration, + autovalidateMode: autovalidateMode, + ); +} + +/// Interface for a [DropdownButtonFormBone] specific callback controller. +abstract class ComboboxCallbackController { + ValueChanged getOnChanged( + String customString, ComboboxCallbackController controller); + + FormFieldSetter getOnSaved( + String customString, ComboboxCallbackController controller); + + DropdownButtonBuilder getOnSelectedItemBuilder( + String customString, ComboboxCallbackController controller); + + VoidCallback getOnTap( + String customString, ComboboxCallbackController controller); + + FormFieldValidator getOnValidator( + String customString, ComboboxCallbackController controller); + + String getName(); + + List texts(); + + List values(); +} diff --git a/lib/src/widget/filters.dart b/lib/src/widget/filters.dart index 99e290f..1c779bb 100644 --- a/lib/src/widget/filters.dart +++ b/lib/src/widget/filters.dart @@ -61,7 +61,7 @@ class FilterItem { class Filters implements - TextCallbackController, + TextFormCallbackController, ButtonCallbackController, TableCallbackController { var filters = []; @@ -80,12 +80,13 @@ class Filters filters.firstWhere((element) => element.name == name, orElse: () => null); @override - getOnChanged(String customString, TextCallbackController controller) { + getOnChanged(String customString, TextFormCallbackController controller) { return null; } @override - getOnEditingComplete(String customString, TextCallbackController controller) { + getOnEditingComplete( + String customString, TextFormCallbackController controller) { return null; } @@ -96,7 +97,8 @@ class Filters } @override - getOnFieldSubmitted(String customString, TextCallbackController controller) { + getOnFieldSubmitted( + String customString, TextFormCallbackController controller) { return null; } @@ -126,19 +128,19 @@ class Filters } @override - getOnSaved(String customString, TextCallbackController controller) { + getOnSaved(String customString, TextFormCallbackController controller) { return (input) { byName(customString).value = input; }; } @override - getOnTap(String customString, TextCallbackController controller) { + getOnTap(String customString, TextFormCallbackController controller) { return null; } @override - getValidator(String customString, TextCallbackController controller) { + getValidator(String customString, TextFormCallbackController controller) { return null; } diff --git a/lib/src/widget/list_form.dart b/lib/src/widget/list_form.dart index d3db052..994a8cd 100644 --- a/lib/src/widget/list_form.dart +++ b/lib/src/widget/list_form.dart @@ -62,9 +62,8 @@ class ListForm { for (var key in columnNames) { cells.add(DataCell( Text(row[key]), - onTap: () => - tableCallbackController.getOnEditTap( - customString, tableCallbackController, row), + onTap: () => tableCallbackController.getOnEditTap( + customString, tableCallbackController, row), )); } return DataRow(cells: cells); diff --git a/lib/src/widget/module_controller.dart b/lib/src/widget/module_controller.dart index 49c10c8..b27c639 100644 --- a/lib/src/widget/module_controller.dart +++ b/lib/src/widget/module_controller.dart @@ -11,7 +11,7 @@ import 'widget_helper.dart'; // This interface allows the generic handling of the edit form by a model driven module. class ModuleController - implements TextCallbackController, ButtonCallbackController { + implements TextFormCallbackController, ButtonCallbackController { final ModuleModel moduleModel; String primaryColumn; WidgetList createWidgets; @@ -33,17 +33,19 @@ class ModuleController ModuleModel getModuleModel() => moduleModel; @override - getOnChanged(String customString, TextCallbackController controller) { + getOnChanged(String customString, TextFormCallbackController controller) { return null; } @override - getOnEditingComplete(String customString, TextCallbackController controller) { + getOnEditingComplete( + String customString, TextFormCallbackController controller) { return null; } @override - getOnFieldSubmitted(String customString, TextCallbackController controller) { + getOnFieldSubmitted( + String customString, TextFormCallbackController controller) { return null; } @@ -73,7 +75,7 @@ class ModuleController } @override - getOnSaved(String customString, TextCallbackController controller) { + getOnSaved(String customString, TextFormCallbackController controller) { final rc = (input) { values[customString] = StringHelper.fromString(input, dataTypes[customString]); @@ -82,12 +84,12 @@ class ModuleController } @override - getOnTap(String customString, TextCallbackController controller) { + getOnTap(String customString, TextFormCallbackController controller) { return null; } @override - getValidator(String customString, TextCallbackController controller) { + getValidator(String customString, TextFormCallbackController controller) { return null; } diff --git a/lib/src/widget/raised_button_bone.dart b/lib/src/widget/raised_button_bone.dart index 402c611..64bd8e8 100644 --- a/lib/src/widget/raised_button_bone.dart +++ b/lib/src/widget/raised_button_bone.dart @@ -45,22 +45,22 @@ class RaisedButtonBone extends RaisedButton { Duration animationDuration, Widget child}) : super( - onPressed: callbackController.getOnPressed( - customString, callbackController), - onLongPress: callbackController.getOnLongPressed( - customString, callbackController), - onHighlightChanged: callbackController.getOnHighlightChanged( - customString, callbackController), - textTheme: textTheme, - textColor: textColor, - disabledTextColor: disabledTextColor, - color: color, - disabledColor: disabledColor, - focusColor: focusColor, - hoverColor: hoverColor, - highlightColor: highlightColor, - splashColor: splashColor, - colorBrightness: colorBrightness, + onPressed: callbackController.getOnPressed( + customString, callbackController), + onLongPress: callbackController.getOnLongPressed( + customString, callbackController), + onHighlightChanged: callbackController.getOnHighlightChanged( + customString, callbackController), + textTheme: textTheme, + textColor: textColor, + disabledTextColor: disabledTextColor, + color: color, + disabledColor: disabledColor, + focusColor: focusColor, + hoverColor: hoverColor, + highlightColor: highlightColor, + splashColor: splashColor, + colorBrightness: colorBrightness, elevation: elevation, focusElevation: focusElevation, hoverElevation: hoverElevation, diff --git a/lib/src/widget/text_form_field_bone.dart b/lib/src/widget/text_form_field_bone.dart index e8c01de..09b2289 100644 --- a/lib/src/widget/text_form_field_bone.dart +++ b/lib/src/widget/text_form_field_bone.dart @@ -2,24 +2,24 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// Interface for a [TextFormField] specific callback controller. -abstract class TextCallbackController { +abstract class TextFormCallbackController { ValueChanged getOnChanged( - String customString, TextCallbackController controller); + String customString, TextFormCallbackController controller); VoidCallback getOnEditingComplete( - String customString, TextCallbackController controller); + String customString, TextFormCallbackController controller); ValueChanged getOnFieldSubmitted( - String customString, TextCallbackController controller); + String customString, TextFormCallbackController controller); FormFieldSetter getOnSaved( - String customString, TextCallbackController controller); + String customString, TextFormCallbackController controller); GestureTapCallback getOnTap( - String customString, TextCallbackController controller); + String customString, TextFormCallbackController controller); FormFieldValidator getValidator( - String customString, TextCallbackController controller); + String customString, TextFormCallbackController controller); } /// Implements a [TextFormField] with "outsourced" callbacks: @@ -27,27 +27,26 @@ abstract class TextCallbackController { /// [callbackController] handles the callback methods. class TextFormFieldBone extends TextFormField { final String customString; - final TextCallbackController callbackController; + final TextFormCallbackController callbackController; - TextFormFieldBone( - this.customString, - this.callbackController, { - Key key, - TextEditingController controller, - String initialValue, - FocusNode focusNode, - InputDecoration decoration = const InputDecoration(), - TextInputType keyboardType, - TextCapitalization textCapitalization = TextCapitalization.none, - TextInputAction textInputAction, - TextStyle style, - StrutStyle strutStyle, - TextDirection textDirection, - TextAlign textAlign = TextAlign.start, - TextAlignVertical textAlignVertical, - bool autofocus = false, - bool readOnly = false, - ToolbarOptions toolbarOptions, + TextFormFieldBone(this.customString, + this.callbackController, { + Key key, + TextEditingController controller, + String initialValue, + FocusNode focusNode, + InputDecoration decoration = const InputDecoration(), + TextInputType keyboardType, + TextCapitalization textCapitalization = TextCapitalization.none, + TextInputAction textInputAction, + TextStyle style, + StrutStyle strutStyle, + TextDirection textDirection, + TextAlign textAlign = TextAlign.start, + TextAlignVertical textAlignVertical, + bool autofocus = false, + bool readOnly = false, + ToolbarOptions toolbarOptions, bool showCursor, String obscuringCharacter = '•', bool obscureText = false, diff --git a/lib/src/widget/view.dart b/lib/src/widget/view.dart index d820d8f..989279c 100644 --- a/lib/src/widget/view.dart +++ b/lib/src/widget/view.dart @@ -1,14 +1,16 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bones/src/controller/button_controller.dart'; +import '../controller/combobox_controller.dart'; import '../helper/settings.dart'; -import '../model/button_model.dart'; import '../model/empty_line_model.dart'; import '../model/field_model.dart'; import '../model/section_model.dart'; import '../model/text_field_model.dart'; import '../model/text_model.dart'; import '../model/widget_model.dart'; +import 'dropdown_button_form_bone.dart'; import 'raised_button_bone.dart'; class View { @@ -22,32 +24,46 @@ class View { if (_instance == null) { _instance = View.internal(logger); _instance.settings = Settings(logger: logger); - _instance.widgetConfiguration = _instance.settings.widgetConfiguration; + _instance.widgetConfiguration = _instance.settings.configuration; } return _instance; } View.internal(this.logger); - /// Creates a button from the [model]. - Widget button(ButtonModel model) { + /// Creates a button from the [controller]. + Widget button(ButtonController controller) { final rc = RaisedButtonBone( - model.widgetName(), - model, - child: Text(model.text), + controller.getName(), + controller, + child: Text(controller.model.text), ); return rc; } - /// Creates a list of buttons from a list of [models]. - List buttonList(List models) { + /// Creates a list of buttons from a list of [controllers]. + List buttonList(List controllers) { final rc = []; - for (var item in models) { + for (var item in controllers) { rc.add(button(item)); } return rc; } + /// Creates a combobox via the [controller]. + Widget combobox(ComboboxController controller, onTap) { + final texts = controller.texts(); + final values = controller.values() ?? texts; + final items = >[]; + for (var ix = 0; ix < texts.length; ix++) { + items.add(DropdownMenuItem( + onTap: onTap, value: values[ix], child: Text(texts[ix]))); + } + final rc = + DropdownButtonFormBone(controller.getName(), controller, items: items); + return rc; + } + /// Creates a checkbox from the [model]. Widget checkbox(WidgetModel model) { var rc; @@ -84,7 +100,8 @@ class View { final padding = widgetConfiguration.asFloat('form.card.padding', defaultValue: 16.0); final children = widgetsOfSection(model.children); - final buttons = buttonList(model.buttons); + final controllers = model.buttons.map((button) => ButtonController(button)); + final buttons = buttonList(controllers); final rc = Form( key: formKey, child: Card( @@ -142,7 +159,7 @@ class View { rc.add(textField(child)); break; case WidgetModelType.button: - rc.add(button(child)); + rc.add(button(ButtonController(child))); break; case WidgetModelType.emptyLine: rc.add(emptyLine(child)); diff --git a/pubspec.yaml b/pubspec.yaml index ffc1db6..0cd45ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - dart_bones: "^0.4.3" + dart_bones: "^0.4.5" # The following adds the Cupertino Icons font to your application. diff --git a/test/helpers/settings_test.dart b/test/helpers/settings_test.dart index 5413fd6..dda85c8 100644 --- a/test/helpers/settings_test.dart +++ b/test/helpers/settings_test.dart @@ -1,37 +1,47 @@ +import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; +import 'package:intl/intl.dart'; import 'package:test/test.dart'; void main() { + Intl.defaultLocale = "de_DE"; + final logger = MemoryLogger(LEVEL_FINE); final map = { 'help': {'de': 'Hilfe'}, - 'wrong name %{0}': {'de': 'falscher name %{0}'} + 'wrong name %{0}': {'de': 'falscher Name %{0}'} }; - setUpAll(() => Settings.setLocaleByNames(language: 'en')); + setUpAll(() => BaseSettings.setLocaleByNames(language: 'en')); group('Settings', () { test('basic', () { - Settings.setLocale(Locale('de', 'de')); - expect(Settings.translate('abc', map), equals('abc')); - expect(Settings.translate('help', map), equals('help')); + BaseSettings.setLocale(Locale('de', 'DE')); + expect(BaseSettings.language, 'de'); + expect(BaseSettings.translate('abc', map), equals('abc')); + expect(BaseSettings.translate('help', map), equals('Hilfe')); expect( - Settings.translate('wrong name %{0}', map, + BaseSettings.translate('wrong name %{0}', map, placeholders: {'0': 'Joe'}), - equals('wrong name Joe')); + equals('falscher Name Joe')); + expect(Settings(logger: logger), isNotNull); }); test('translate-en', () { - Settings.setLocaleByNames(language: 'en'); - expect(Settings.translate('abc', map), equals('abc')); - expect(Settings.translate('help', map), equals('help')); + BaseSettings.setLocaleByNames(language: 'en'); + expect(BaseSettings.language, 'en'); + expect(BaseSettings.translate('abc', map), equals('abc')); + expect(BaseSettings.translate('help', map), equals('help')); expect( - Settings.translate('wrong name %{0}', map, + BaseSettings.translate('wrong name %{0}', map, placeholders: {'0': 'Joe'}), equals('wrong name Joe')); }); test('translate-de', () { - Settings.setLocaleByNames(language: 'de'); - expect(Settings.translate('abc', map), equals('abc')); - expect(Settings.translate('help', map), equals('Hilfe')); - expect(Settings.translate('wrong name %{0}', map, placeholders: {'0' : 'Joe'}), equals('falscher name Joe')); + BaseSettings.setLocaleByNames(language: 'de'); + expect(BaseSettings.translate('abc', map), equals('abc')); + expect(BaseSettings.translate('help', map), equals('Hilfe')); + expect( + BaseSettings.translate('wrong name %{0}', map, + placeholders: {'0': 'Joe'}), + equals('falscher Name Joe')); }); }); } diff --git a/test/model/model_test.dart b/test/model/model_test.dart index 9e85c68..a9bc067 100644 --- a/test/model/model_test.dart +++ b/test/model/model_test.dart @@ -40,7 +40,7 @@ void main() { }); }); group('ModelBase', () { - test('errors', () { + test('basic', () { logger.clear(); final map = cloneOfMap(userModel); final field = { @@ -94,8 +94,31 @@ void main() { }); }); group('CheckboxModel', () { - logger.clear(); + test('basic', () { + logger.clear(); + final map = cloneOfMap(userModel); + final field = { + 'widgetType': 'checkbox', + 'name': 'hidden', + 'label': 'Hidden', + 'options': 'required', + }; + map['pages'][0]['sections'][0]['children'][0] = field; + var module = Demo1(map, logger); + module.parse(); + var errors = logger.errors; + expect(errors.length, equals(0)); + final checkbox = module.pageByName('create').getField('hidden'); + expect(checkbox.hasOption('required'), isTrue); + checkbox.value = true; + expect(checkbox.value, isTrue); + checkbox.value = false; + expect(checkbox.value, isFalse); + checkbox.value = 'true'; + expect(checkbox.value, isTrue); + }); test('errors', () { + logger.clear(); final map = cloneOfMap(userModel); final field = { 'widgetType': 'checkbox', @@ -107,7 +130,7 @@ void main() { var module = Demo1(map, logger); module.parse(); var errors = logger.errors; - expect(errors.length, equals(2)); + expect(errors.length, equals(0)); final checkbox = module.pageByName('create').getField('hidden'); expect(checkbox, isNotNull); expect(checkbox.dataType, DataType.bool); @@ -138,7 +161,7 @@ void main() { var module = Demo1(map, logger); module.parse(); var errors = logger.errors; - expect(errors.length, equals(2)); + expect(errors.length, equals(0)); final combobox = module.pageByName('create').getField('class'); expect(combobox, isNotNull); expect(combobox.dataType, DataType.int); @@ -153,8 +176,8 @@ void main() { }); }); group('Non field widgets', () { - logger.clear(); test('basic', () { + logger.clear(); final map = cloneOfMap(userModel); final list = [ { @@ -166,15 +189,9 @@ void main() { var module = Demo1(map, logger); module.parse(); var errors = logger.errors; - expect(errors.length, equals(2)); + expect(errors.length, equals(0)); final dump = module.dump(StringBuffer()).toString(); - expect(dump, equals('''= module demo1: options: -== page create: PageModelType.create options: - = section simpleForm1: SectionModelType.simpleForm options: [ - emptyLine: options: - text 24 text: *Hi world*: options: rich - ] # create.simpleForm1 -''')); + expect(dump.contains('text: *Hi world*: options: rich'), isTrue); final page = module.pageByName('create'); final allWidgets = page.getWidgets(null); expect(allWidgets.length, equals(2)); @@ -190,12 +207,6 @@ void main() { expect(nonFieldWidgets.length, equals(2)); }); }); - group('standard-models', () { - test('user', () { - final model = UserModel(logger); - model.parse(); - }); - }); } final userModel = { diff --git a/test/model/standard_test.dart b/test/model/standard_test.dart new file mode 100644 index 0000000..27dd8b1 --- /dev/null +++ b/test/model/standard_test.dart @@ -0,0 +1,114 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:test/test.dart'; + +void main() { + final logger = MemoryLogger(LEVEL_FINE); + group('module', () { + test('role', () { + logger.clear(); + final module = RoleModel(logger); + module.parse(); + expect(module.fullName(), equals('role')); + expect(module.widgetName(), equals('role')); + final errors = logger.errors; + expect(errors.length, equals(0)); + 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_name: DataType.string "Rolle" options: unique notnull + column role_priority: DataType.int "Priorität" options: + column role_active: DataType.bool "Aktiv" options: + column role_createdat: DataType.dateTime "Erzeugt" options: hidden null doStore + column role_createdby: DataType.string "Erzeugt von" options: hidden doStore + column role_changedat: DataType.dateTime "Geändert" options: hidden null doStore + column role_changedby: DataType.string "Geändert von" options: hidden doStore +== page create: PageModelType.create options: + = section simpleForm1: SectionModelType.simpleForm options: [ + allDbFields 10 text: options: + ] # create.simpleForm1 +== page change: PageModelType.change options: + = section simpleForm1: SectionModelType.simpleForm options: [ + allDbFields 12 text: options: + ] # change.simpleForm1 +== page list: PageModelType.list options: + = section filterPanel1: SectionModelType.filterPanel options: [ + textField role_name: options: + ] # list.filterPanel1 +'''); + }); + test('user', () { + logger.clear(); + final module = UserModel(logger); + module.parse(); + expect(module.fullName(), equals('user')); + expect(module.widgetName(), equals('user')); + final errors = logger.errors; + expect(errors.length, equals(0)); + final dump = module.dump(StringBuffer()).toString(); + expect(dump, '''= module user: options: +== table user: options: + column user_id: DataType.int "Id" options: primary notnull unique + 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_role: DataType.reference "Role" options: + column user_createdat: DataType.dateTime "Erzeugt" options: hidden null doStore + column user_createdby: DataType.string "Erzeugt von" options: hidden doStore + column user_changedat: DataType.dateTime "Geändert" options: hidden null doStore + column user_changedby: DataType.string "Geändert von" options: hidden doStore +== page create: PageModelType.create options: + = section simpleForm1: SectionModelType.simpleForm options: [ + allDbFields 26 text: options: + ] # create.simpleForm1 +== page change: PageModelType.change options: + = section simpleForm1: SectionModelType.simpleForm options: [ + allDbFields 28 text: options: + ] # change.simpleForm1 +== page list: PageModelType.list options: + = section filterPanel1: SectionModelType.filterPanel options: [ + textField user_name: options: + combobox user_role: texts: options: + ] # list.filterPanel1 +'''); + }); + test('configuration', () { + logger.clear(); + final module = ConfigurationModel(logger); + module.parse(); + expect(module.fullName(), equals('configuration')); + expect(module.widgetName(), equals('configuration')); + final errors = logger.errors; + expect(errors.length, equals(0)); + final dump = module.dump(StringBuffer()).toString(); + expect(dump, startsWith('''= 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_property: DataType.string "Eigenschaft" options: + column configuration_order: DataType.int "Reihe" options: + column configuration_type: DataType.string "Datentyp" options: + column configuration_value: DataType.string "Wert" options: + column configuration_description: DataType.string "Beschreibung" options: + column configuration_createdat: DataType.dateTime "Erzeugt" options: hidden null doStore + column configuration_createdby: DataType.string "Erzeugt von" options: hidden doStore + column configuration_changedat: DataType.dateTime "Geändert" options: hidden null doStore + column configuration_changedby: DataType.string "Geändert von" options: hidden doStore +== page create: PageModelType.create options: + = section simpleForm1: SectionModelType.simpleForm options: [ + allDbFields ''')); + /* + expect(dump.contains('''text: 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:')); + }); + }); +} diff --git a/test/rest_persistence_test.dart b/test/rest_persistence_test.dart new file mode 100644 index 0000000..bc32ff6 --- /dev/null +++ b/test/rest_persistence_test.dart @@ -0,0 +1,74 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + final logger = MemoryLogger(LEVEL_FINE); + setUpAll(() { + final configuration = BaseConfiguration({ + 'client': { + 'host': 'localhost', + 'port': 58011, + 'schema': 'http', + 'application': 'unittest', + 'version': '1.0.0', + } + }, logger); + RestPersistence.fromConfig(configuration, logger); + }); + group('queries', () { + test('list', () async { + final rest = RestPersistence(); + final list = await rest.list( + module: 'role', sqlName: 'list', params: {'":role_name"': '"A%"'}); + expect(list, isNotNull); + expect(list.length, equals(1)); + expect(list[0].containsKey('role_id'), isTrue); + }); + test('record', () async { + final rest = RestPersistence(); + final record = await rest.record(module: 'role', id: 2); + expect(record, isNotNull); + expect(record.length, greaterThan(4)); + expect(record.containsKey('role_id'), isTrue); + }); + }); + group('modify', () { + test('insert+update+delete', () async { + final rest = RestPersistence(); + final rec = await rest.customQuery( + module: 'role', + sqlName: 'by_role_name', + sqlType: 'record', + params: {':excluded': 0, ':role_name': 'dummy'}); + if (rec != 0 && rec.containsKey('role_id')) { + await rest.delete(module: 'role', id: rec['role_id']); + } + final id = await rest.insert(module: 'role', sqlName: 'insert', data: { + ':role_name': 'dummy', + ':role_priority': 111, + ':role_active': 'F', + ':role_createdby': 'joe' + }); + expect(id is int, isTrue); + final answer2 = + await rest.update(module: 'role', sqlName: 'update', data: { + ':role_id': id, + ':role_name': 'dummy2', + ':role_priority': 112, + ':role_active': 'T', + ':role_changedby': 'eve' + }); + final answer = rest.record(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 023c466..55254ba 100644 --- a/test/tool/tool_test.dart +++ b/test/tool/tool_test.dart @@ -14,13 +14,13 @@ void main() { expect(errors.length, equals(0)); expect(content, equals('''DROP TABLE IF EXISTS role; CREATE TABLE role ( - role_id INT(10) notnull unique, - role_name VARCHAR(32) unique notnull, + role_id INT(10) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT, + role_name VARCHAR(32) UNIQUE NOT NULL, role_priority INT(10), role_active CHAR(1), - role_createdat TIMESTAMP null, + role_createdat TIMESTAMP NULL, role_createdby VARCHAR(16), - role_changedat TIMESTAMP null, + role_changedat TIMESTAMP NULL, role_changedby VARCHAR(16), PRIMARY KEY(role_id) ); @@ -43,24 +43,44 @@ modules: list: - name: insert type: insert - sql: "INSERT INTO role(role_name,role_priority,role_active,role_createdat,role_createdby,role_changedat,role_changedby) - VALUES(:role_name,:role_priority,:role_active,NOW(),:role_createdby,:role_changedat,:role_changedby);" + sql: "INSERT INTO role(role_name,role_priority,role_active,role_createdat,role_createdby) + VALUES(:role_name,:role_priority,:role_active,NOW(),:role_createdby);" - name: update type: update sql: "UPDATE role SET - role_name=:role_name,role_priority=:role_priority,role_active=:role_active,role_createdat=:role_createdat,role_createdby=:role_createdby,role_changedat=NOW(),role_changedby=:role_changedby - WHERE role_id=:role_id"; + role_name=:role_name,role_priority=:role_priority,role_active=:role_active,role_changedat=NOW(),role_changedby=:role_changedby + WHERE role_id=:role_id;" - name: delete type: delete sql: "DELETE from role WHERE role_id=:role_id;" - name: record type: record sql: "SELECT * from role WHERE role_id=:role_id;" + - name: by_role_name + type: record + sql: "SELECT * from role WHERE role_name=:role_name&&role_id!=:excluded;" - name: list type: list sql: "SELECT * from role WHERE role_name like :role_name;" ''')); }); + test('standard_modules', () { + logger.clear(); + final dir = FileSync.tempDirectory('data', subDirs: 'unittest'); + for (var name in standardModules()) { + final module = standardModule(name, logger); + module.parse(); + expect(logger.errors.isEmpty, isTrue); + final content = module.exportSqlBackend(); + FileSync.toFile(FileSync.joinPaths(dir, '$name.yaml'), content); + } + }); + test('standard_modules-errors', () { + logger.clear(); + final module = standardModule('not-exists', logger); + expect(module, isNull); + expect(logger.contains('unknown standard module: not-exists'), isTrue); + }); }); } diff --git a/test/widget/widget_test.dart b/test/widget/widget_test.dart index c662812..f713f73 100644 --- a/test/widget/widget_test.dart +++ b/test/widget/widget_test.dart @@ -29,8 +29,8 @@ void main() { }); group('ModuleController', () { test('basic', () { - PageData pageData = - PageData(BaseConfiguration({}, logger), (title) {}, (context) {}); + PageData pageData = PageData( + BaseConfiguration({}, logger), (title) => null, (context) => null); final role = RoleCreatePage(pageData); if (role.lastState == null) { role.createState();