From 664c865f79d7cee0b75d7c498b586b93894b7ca2 Mon Sep 17 00:00:00 2001 From: Hamatoma Date: Thu, 30 Sep 2021 00:25:10 +0200 Subject: [PATCH] daily work * new: PageMetaData * common base for meta data driven pages: page_collection * a 12 column layout manager (like Bootstrap): widget_form.dart --- Prepare | 5 + bin/generator.dart | 102 ++++++++++++----- bin/meta_tool.dart | 13 ++- lib/base/defines.dart | 6 +- lib/base/i18n.dart | 2 + lib/base/i18n_io.dart | 5 +- lib/exhibition_app.dart | 62 +++++------ lib/meta/module_meta_data.dart | 113 +++++++++++++------ lib/meta/modules.dart | 10 +- lib/meta/roles_meta.dart | 45 ++++---- lib/meta/users_meta.dart | 64 ++++++----- lib/page/info_page.dart | 2 +- lib/page/page_collection.dart | 35 ++++++ lib/page/roles/role_data.dart | 96 ++++++++++++++++ lib/page/start_page.dart | 7 +- lib/page/users/user_data.dart | 147 +++++++++++++++++-------- lib/page/users/user_state.dart | 55 --------- lib/page/users/users_edit_page.dart | 55 +++++++++ lib/page/users/users_list_page.dart | 54 --------- lib/persistence/data_record.dart | 31 ++++++ lib/persistence/data_table.dart | 39 +++++++ lib/persistence/file_persistence.dart | 67 +++++++++++ lib/persistence/persistence.dart | 27 +++-- lib/persistence/rest_persistence.dart | 22 ++-- lib/setting/app_bar_exhibition.dart | 2 +- lib/setting/drawer_exhibition.dart | 1 - lib/setting/global_data.dart | 24 +++- lib/widget/attended_page.dart | 71 ++++++++++++ lib/widget/attended_widget.dart | 65 +++++++++++ lib/widget/widget_form.dart | 74 +++++++++++++ rest_client/pubspec.yaml | 2 +- rest_client/test/rest_client_test.dart | 6 +- rest_server/data/sql/roles.sql.yaml | 28 +++++ rest_server/pubspec.yaml | 2 +- test/widget_test.dart | 2 - tools/PackRestServer | 1 + 36 files changed, 1005 insertions(+), 337 deletions(-) create mode 100755 Prepare create mode 100644 lib/page/page_collection.dart create mode 100644 lib/page/roles/role_data.dart delete mode 100644 lib/page/users/user_state.dart create mode 100644 lib/page/users/users_edit_page.dart delete mode 100644 lib/page/users/users_list_page.dart create mode 100644 lib/persistence/data_record.dart create mode 100644 lib/persistence/data_table.dart create mode 100644 lib/persistence/file_persistence.dart create mode 100644 lib/widget/attended_page.dart create mode 100644 lib/widget/attended_widget.dart create mode 100644 lib/widget/widget_form.dart create mode 100644 rest_server/data/sql/roles.sql.yaml diff --git a/Prepare b/Prepare new file mode 100755 index 0000000..051be5a --- /dev/null +++ b/Prepare @@ -0,0 +1,5 @@ +#! /bin/bash +cd lib +dart format --fix . +cd .. + diff --git a/bin/generator.dart b/bin/generator.dart index a6fa433..89b8b7f 100644 --- a/bin/generator.dart +++ b/bin/generator.dart @@ -14,7 +14,7 @@ String classToFilename(String className) { if (className[ix] == upperCase[ix] && ix > 0) { rc += '_'; } - rc = className[ix]; + rc += className[ix]; } return rc.toLowerCase(); } @@ -35,6 +35,7 @@ String filenameToClass(String filename) { class Generator { BaseLogger logger = globalLogger; Generator(this.logger); + /// Appends a [string] at the end of a [buffer]. /// If [buffer] is null a new instance is created. /// If the last line in [buffer] has a length greater than [maxLength] @@ -72,7 +73,7 @@ class Generator { /// Returns a DDL statement creating the database table of the [module]. String createDbTable(ModuleMetaData module) { final tableName = module.tableName; - final list = module.list; + final list = module.propertyList; final buffer = StringBuffer(); buffer.write('-- DO NOT CHANGE. This file is created by the meta_tool\n'); buffer.write('CREATE TABLE $tableName (\n'); @@ -93,37 +94,56 @@ class Generator { /// Returns a Dart class definition of the [module]. String createModuleData(ModuleMetaData module) { final buffer = StringBuffer(); - buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n'); - buffer.write("import '../../base/defines.dart';\n"); - buffer.write("import '../../base/helper.dart';\n"); - buffer.write('class ${module.moduleNameSingular}Data{\n'); - for (var item in module.list) { - buffer.write(' ' + module.dartType(item.dataType) + '? ${item.name};\n'); + PropertyMetaData? primary = module.primaryOf(); + buffer.writeln('// DO NOT CHANGE. This file is created by the meta_tool'); + buffer.writeln("import '../../base/defines.dart';"); + buffer.writeln("import '../../base/helper.dart';"); + buffer.writeln("import '../../persistence/data_record.dart';"); + final type = 'int'; + buffer.writeln( + 'class ${module.moduleNameSingular}Data extends DataRecord<$type>{'); + for (var item in module.propertyList) { + buffer.writeln(' ' + module.dartType(item.dataType) + '? ${item.name};'); } - buffer.write(' ${module.moduleNameSingular}Data({'); + buffer.writeln(' ${module.moduleNameSingular}Data({'); String separator = ''; - for (var item in module.list) { + for (var item in module.propertyList) { buffer.write('$separator this.${item.name}'); separator = ','; } - buffer.write('});\n'); - buffer - .write(' ${module.moduleNameSingular}Data.fromYaml(YamlMap data) {\n'); - for (var item in module.list) { + buffer.writeln('});'); + buffer.writeln(' ${module.moduleNameSingular}Data.createFromMap(DataMap map) {'); + buffer.writeln(' fromMap(map);'); + buffer.writeln(' }'); + buffer.writeln(' @override'); + buffer.writeln(' void fromMap(DataMap map) {'); + for (var item in module.propertyList) { final name = item.columnName; final type = item.dataType.toString(); //id = data.containsKey('id') ? int.tryParse(data['id']) : null; - buffer.write( - " ${item.name} = data.containsKey('$name') ? valueOf($type, data['$name']) : null;\n"); + buffer.writeln( + " ${item.name} = map.containsKey('$name') ? valueOf($type, " + "map['$name']) : null;"); } - buffer.write(' }\n'); - buffer.write(' static DataType? dataTypeOf(String name) {\n'); - buffer.write(' DataType? rc;\n'); - buffer.write(' switch(name){\n'); - for (var item in module.list) { - buffer.write(" case '${item.name}':\n"); - buffer.write(' rc = ${item.dataType};\n'); - buffer.write(' break;\n'); + buffer.writeln(' }'); + final name = primary == null ? 'id' : primary.name; + final name2 = primary == null ? 'id' : primary.columnName; + buffer.write(''' @override + int keyOf() { + return $name ?? 0; + } + @override + String nameOfKey(){ + return '$name2'; + } +'''); + buffer.writeln(' static DataType? dataTypeOf(String name) {'); + buffer.writeln(' DataType? rc;'); + buffer.writeln(' switch(name){'); + for (var item in module.propertyList) { + buffer.writeln(" case '${item.name}':"); + buffer.writeln(' rc = ${item.dataType};'); + buffer.writeln(' break;'); } buffer.write(''' default: break; @@ -131,8 +151,22 @@ class Generator { return rc; } '''); - buffer.write('}\n'); - + buffer.write(''' @override + DataMap toMap({DataMap? map, bool clear = true}) { + map ??= DataMap(); + if (clear) { + map.clear(); + } +'''); + for (var item in module.propertyList) { + final name1 = item.columnName; + final name2 = item.name; + buffer.writeln(" map['$name1'] = $name2;"); + } + buffer.write(''' return map; + } +} +'''); return buffer.toString(); } @@ -140,7 +174,6 @@ class Generator { String createModules() { final buffer = StringBuffer(); buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n'); - buffer.write("import 'dart:io';\n"); buffer.write("import 'module_meta_data.dart';\n"); final modules = []; final files = []; @@ -197,7 +230,7 @@ List moduleNames(){ String createSqlStatements(ModuleMetaData module) { final moduleName = module.moduleName; final tableName = module.tableName; - final list = module.list; + final list = module.propertyList; final buffer = StringBuffer(); buffer.write('---\n'); buffer.write('# DO NOT CHANGE. This file is created by the meta_tool\n'); @@ -303,6 +336,7 @@ update: void out(String line) { logger.log(line); } + /// Generates all modules defined in the meta data. void updateModules(Generator generator) { writeFile('lib/meta/modules.dart', createModules()); @@ -320,6 +354,12 @@ update: } } } + + /// Generates the modules.dart. + void updateModulesList(Generator generator) { + writeFile('lib/meta/modules.dart', createModules()); + } + /// Generates the Sql statement file for each module defined in the meta data. void updateSql(Generator generator) { final modules = moduleNames(); @@ -328,15 +368,21 @@ update: if (module == null) { logger.error('+++ unknown module: $name'); } else { + logger.log('current directory: ${Directory.current.path}'); String filename = 'rest_server/data/sql/${name.toLowerCase()}.sql.yaml'; writeFile(filename, generator.createSqlStatements(module)); } } } + /// Writes a given [contents] into a file named [filename]; void writeFile(String filename, String contents) { logger.log('creating $filename ...'); final file = File(filename); + final parent = Directory(dirname(filename)); + if (!parent.existsSync()) { + parent.createSync(recursive: true); + } file.writeAsStringSync(contents); } } diff --git a/bin/meta_tool.dart b/bin/meta_tool.dart index ef419ea..0c30a98 100644 --- a/bin/meta_tool.dart +++ b/bin/meta_tool.dart @@ -18,7 +18,7 @@ void main(List args) { case 'all-modules': generator.out(moduleNames().join('\n')); break; - case 'print-modules': + case 'print-modules-list': generator.out(generator.createModules()); break; case 'print-table': @@ -39,6 +39,9 @@ void main(List args) { case 'update-modules': generator.updateModules(generator); break; + case 'update-modules-list': + generator.updateModulesList(generator); + break; case 'update-sql': generator.updateSql(generator); break; @@ -56,14 +59,20 @@ void usage(Generator generator) { MODE: all-modules Prints the module names. - print-modules + print-modules-list Prints the file lib/meta/modules.dart. + That works without of meta-data: It can be called before + creating a the meta_tool. print-table MODULE Prints the SQL table definition of MODULE. print-data MODULE Creates the data storage class of MODULE print-sql MODULE Creates the SQL statements (for the rest_server) of MODULE + update-modules-list + Generates the modules.dart file in the correct directory. + That works without of meta-data: It can be called before + creating a the meta_tool. update-modules Generates all module files depending on the meta data. update-sql diff --git a/lib/base/defines.dart b/lib/base/defines.dart index 37a569a..88bbea2 100644 --- a/lib/base/defines.dart +++ b/lib/base/defines.dart @@ -1,9 +1,9 @@ enum ServerEnvironment { productive, development } -typedef YamlMap = Map; -typedef YamlList = List; +typedef JsonMap = Map; +typedef JsonList = List; ///JsonMap or JsonList -typedef YamlData = Object; +typedef JsonData = Object; enum EventSource { undef, localData, remoteData } enum EventCardinality { undef, record, list } diff --git a/lib/base/i18n.dart b/lib/base/i18n.dart index 0188106..ab6c457 100644 --- a/lib/base/i18n.dart +++ b/lib/base/i18n.dart @@ -1,6 +1,8 @@ import 'package:dart_bones/dart_bones.dart'; + typedef MapModule = Map; typedef MapPlural = Map; + class I18N { static I18N? instance; final BaseLogger logger; diff --git a/lib/base/i18n_io.dart b/lib/base/i18n_io.dart index e2633a2..b490d14 100644 --- a/lib/base/i18n_io.dart +++ b/lib/base/i18n_io.dart @@ -11,7 +11,8 @@ class I18nIo extends I18N { final regExpTranslation = RegExp(r'^msgstr "(.+?)"$'); final regExpPlural = RegExp(r'^msg_plural "(.+?)"$'); final regExpList = RegExp(r'^msgstr\[\d+\] "(.+?)"$'); - I18nIo.internal(this.dataDirectory, BaseLogger logger) : super.internal(logger); + I18nIo.internal(this.dataDirectory, BaseLogger logger) + : super.internal(logger); /// Puts a [key] with its [translation] into the [mapModule]. /// Returns the [module] entry of [mapModule]. @@ -89,7 +90,7 @@ class I18nIo extends I18N { if (lastEntry.list == null) { lastEntry.list = []; int count = lastEntry.list!.length; - if (count != int.parse(match!.group(1)!)){ + if (count != int.parse(match!.group(1)!)) { logger.error('wrong index in $line. Expected: $count'); } lastEntry.list!.add(fromConstant(match.group(2)!)); diff --git a/lib/exhibition_app.dart b/lib/exhibition_app.dart index d3957cc..bc485e8 100644 --- a/lib/exhibition_app.dart +++ b/lib/exhibition_app.dart @@ -1,12 +1,39 @@ -import 'package:dart_bones/dart_bones.dart'; -import 'package:exhibition/page/start_page.dart'; +import 'package:exhibition/page/page_collection.dart'; import 'package:flutter/material.dart'; -import 'page/users/users_list_page.dart'; +import 'package:dart_bones/dart_bones.dart'; + +import 'page/start_page.dart'; import 'page/info_page.dart'; import 'page/log_page.dart'; - import 'setting/global_data.dart'; +Route? _getRoute(RouteSettings settings) { + MaterialPageRoute? route; + StatefulWidget? page; + switch (settings.name) { + case '/start': + page = StartPage(globalLogger); + break; + case '/info': + page = InfoPage(GlobalData()); + break; + case '/log': + page = LogPage(GlobalData()); + break; + default: + page = PageCollection().newPageByRoute(settings.name ?? ''); + break; + } + if (page != null) { + route = MaterialPageRoute( + settings: settings, + builder: (BuildContext context) => page!, + fullscreenDialog: false, + ); + } + return route; +} + class ExhibitionApp extends StatefulWidget { static BaseLogger logger = globalLogger; @@ -31,30 +58,3 @@ class ExhibitionAppState extends State { ); } } - -Route? _getRoute(RouteSettings settings) { - MaterialPageRoute? route; - StatefulWidget? page; - switch (settings.name) { - case '/users/list': - page = UsersListPage(GlobalData()); - break; - case '/start': - page = StartPage(globalLogger); - break; - case '/info': - page = InfoPage(GlobalData()); - break; - case '/log': - page = LogPage(GlobalData()); - break; - } - if (page != null) { - route = MaterialPageRoute( - settings: settings, - builder: (BuildContext context) => page!, - fullscreenDialog: false, - ); - } - return route; -} diff --git a/lib/meta/module_meta_data.dart b/lib/meta/module_meta_data.dart index 7d31612..71943ce 100644 --- a/lib/meta/module_meta_data.dart +++ b/lib/meta/module_meta_data.dart @@ -1,11 +1,17 @@ +import 'package:flutter/material.dart'; + import '../base/defines.dart'; +typedef MenuItemBuilder = List> Function( + PropertyMetaData propertyMetaData); + /// Describes a button of a page. class ButtonMetaData extends WidgetMetaData { ButtonMetaData(String name) : super(name, WidgetType.button) { //@ToDo } } + /// Describes a field related to a database column. class DbFieldMetaData extends WidgetMetaData { PropertyMetaData? reference; @@ -16,9 +22,14 @@ class DbFieldMetaData extends WidgetMetaData { } enum DisplayType { - line, - multiLine, + text, combobox, + checkbox, + custom, +} + +class DummyModule extends ModuleMetaData { + DummyModule() : super('', [], []); } /// Describes a field of the page. @@ -27,7 +38,7 @@ class FieldMetaData extends WidgetMetaData { DisplayType displayType; final String options; FieldMetaData(String name, this.options, - {this.dataType = DataType.string, this.displayType = DisplayType.line}) + {this.dataType = DataType.string, this.displayType = DisplayType.text}) : super(name, WidgetType.field); } @@ -45,41 +56,54 @@ class ModuleMetaData { ]; /// The module name, e.g. users - final String moduleName; + String moduleName = ''; /// The singular version of the module name, e.g. 'user' - String moduleNameSingular; + String moduleNameSingular = ''; /// The related database table. - String tableName; - final bool shortModifiedLabel; - List list; + String tableName = ''; + + /// If true the fields create, createdBy ... have the same ("short") column name + /// instead of prefix and name. + bool shortModifiedLabel = false; + List propertyList; + List pageList; final Map properties = {}; - String columnPrefix; - ModuleMetaData(this.moduleName, this.list, - {this.tableName = '', - this.moduleNameSingular = '', - this.columnPrefix = '', - this.shortModifiedLabel = false}) { - tableName = tableName.isEmpty ? moduleName : tableName; - moduleNameSingular = moduleNameSingular.isEmpty - ? (moduleName.endsWith('s') - ? moduleName.substring(0, moduleName.length - 1) - : moduleName) - : moduleNameSingular; - columnPrefix = columnPrefix.isNotEmpty + String columnPrefix = ''; + ModuleMetaData(this.moduleName, this.propertyList, this.pageList, + {String tableName = '', + String moduleNameSingular = '', + String columnPrefix = '', + bool shortModifiedLabel = false}) { + this.tableName = tableName.isEmpty ? moduleName : tableName; + this.shortModifiedLabel = shortModifiedLabel; + if (moduleNameSingular.isEmpty) { + if (!moduleName.endsWith('s')) { + moduleNameSingular = moduleName; + } else { + final length = moduleName.length - 1; + moduleNameSingular = moduleName.substring(0, length); + } + } + this.moduleNameSingular = moduleNameSingular; + this.columnPrefix = columnPrefix.isNotEmpty ? columnPrefix - : moduleNameSingular.toLowerCase(); - for (var item in list) { + : this.moduleNameSingular.toLowerCase(); + + for (var item in propertyList) { item.module = this; properties[item.name] = item; if (item.columnName.isEmpty) { final prefix = shortModifiedLabel && metaColumns.contains(item.name) ? '' - : columnPrefix + '_'; + : (this.columnPrefix + '_'); item.columnName = prefix + item.name.toLowerCase(); } } + for (var item in pageList) { + item.module = this; + } } /// Returns a given [dataType] as string. @@ -175,11 +199,19 @@ class ModuleMetaData { return rc; } + /// Returns the primary key of the relation. + PropertyMetaData? primaryOf() { + final rc = propertyList.firstWhere( + (element) => element.options.contains('primary'), + orElse: null); + return rc; + } + /// Returns the properties that are not in [metaColumns]. /// : if the name of the property is [included] the property is always part of /// the result. Iterable standardColumns([String included = '']) { - final rc = list.where( + final rc = propertyList.where( (item) => item.name == included || !metaColumns.contains(item.name)); return rc; } @@ -188,6 +220,7 @@ class ModuleMetaData { class PageMetaData { final String name; final PageType pageType; + ModuleMetaData module = DummyModule(); List? fields; PageMetaData( this.name, @@ -202,15 +235,17 @@ enum PageType { create, custom, delete, edit, list } class PropertyMetaData { /// Name of the property (Dart name). final String name; - ModuleMetaData module = ModuleMetaData('', []); + ModuleMetaData module = DummyModule(); final String label; + /// Relative width of the field in a 12 column form: 1 <= width <= 12 + int weight = 6; + /// A colon delimited list of options, e.g. ':notnull:unique:'. final String options; - - /// Empty or 'combobox' or 'checkbox'. - final String widgetType; + final DisplayType displayType; final DataType dataType; + MenuItemBuilder? menuItemBuilder; /// The size if dataType is DataType.string. final int size; @@ -218,9 +253,23 @@ class PropertyMetaData { /// The foreign key if dataType is DataType.reference, e.g. 'users.user_id' String? reference; - PropertyMetaData( - this.name, this.dataType, this.options, this.widgetType, this.label, - {this.columnName = '', this.size = 0, this.reference}); + PropertyMetaData(this.name, this.label, this.dataType, this.options, + {this.displayType = DisplayType.text, + this.columnName = '', + this.size = 0, + this.reference, + int weight = 6}) { + this.weight = weight > 12 ? 12 : weight; + } + + /// Returns whether a given [option] is set. + /// + /// [option] is a word delimited by ':', e.g. ":notnull:" + /// + /// Returns true, if [option] is part of [options], false otherwise. + bool hasOption(String option) { + return options.contains(option); + } } /// Describes a widget used in the page. diff --git a/lib/meta/modules.dart b/lib/meta/modules.dart index 7ef0955..2ee2891 100644 --- a/lib/meta/modules.dart +++ b/lib/meta/modules.dart @@ -1,11 +1,16 @@ // DO NOT CHANGE. This file is created by the meta_tool import 'module_meta_data.dart'; +import 'roles_meta.dart'; import 'users_meta.dart'; + /// Returns the meta data of the module given by [name]. /// Returns null if not found. ModuleMetaData? moduleByName(String name) { ModuleMetaData? rc; switch (name) { + case 'Roles': + rc = RoleMeta(); + break; case 'Users': rc = UserMeta(); break; @@ -14,10 +19,11 @@ ModuleMetaData? moduleByName(String name) { } return rc; } + /// Returns the module names as string list. -List moduleNames(){ +List moduleNames() { return [ + 'Roles', 'Users', ]; } - diff --git a/lib/meta/roles_meta.dart b/lib/meta/roles_meta.dart index 209a139..e45c347 100644 --- a/lib/meta/roles_meta.dart +++ b/lib/meta/roles_meta.dart @@ -1,34 +1,35 @@ import '../base/defines.dart'; -import 'module_meta_data.dart'; import '../base/i18n.dart'; +import 'module_meta_data.dart'; final i18n = I18N(); final M = i18n.module("Roles"); class RoleMeta extends ModuleMetaData { static RoleMeta instance = RoleMeta.internal(); - RoleMeta.internal() - : super( - 'Roles', - [ - PropertyMetaData('id', DataType.reference, ':primary:', 'combo', - i18n.tr('Id')), - PropertyMetaData( - 'name', DataType.string, ':notnull:', '', i18n.tr('Name'), - size: 64), - PropertyMetaData('created', DataType.datetime, ':hidden:', '', - i18n.tr('Created')), - PropertyMetaData('createdBy', DataType.string, ':hidden:', '', - i18n.tr('Created by'), - size: 32), - PropertyMetaData('changed', DataType.datetime, ':hidden:', '', - i18n.tr('Changed')), - PropertyMetaData('changedBy', DataType.string, ':hidden:', '', - i18n.tr('Changed by'), - size: 32), - ], - ); factory RoleMeta() { return instance; } + RoleMeta.internal() + : super('Roles', [ + PropertyMetaData('id', i18n.tr('Id'), DataType.reference, ':primary:', + displayType: DisplayType.combobox), + PropertyMetaData( + 'name', i18n.tr('Name'), DataType.string, ':notnull:', + size: 64), + PropertyMetaData( + 'created', i18n.tr('Created'), DataType.datetime, ':hidden:'), + PropertyMetaData( + 'createdBy', i18n.tr('Created by'), DataType.string, ':hidden:', + size: 32), + PropertyMetaData( + 'changed', i18n.tr('Changed'), DataType.datetime, ':hidden:'), + PropertyMetaData( + 'changedBy', i18n.tr('Changed by'), DataType.string, ':hidden:', + size: 32), + ], [ + PageMetaData('New Role', PageType.create, []), + PageMetaData('Change Role', PageType.edit, []), + PageMetaData('Roles Overview', PageType.list, []), + ]); } diff --git a/lib/meta/users_meta.dart b/lib/meta/users_meta.dart index 095fc44..c627751 100644 --- a/lib/meta/users_meta.dart +++ b/lib/meta/users_meta.dart @@ -1,41 +1,45 @@ import '../base/defines.dart'; -import 'module_meta_data.dart'; import '../base/i18n.dart'; +import 'module_meta_data.dart'; + final i18n = I18N(); final M = i18n.module("Users"); class UserMeta extends ModuleMetaData { static UserMeta instance = UserMeta.internal(); - UserMeta.internal() - : super( - 'Users', - [ - PropertyMetaData('id', DataType.reference, ':primary:', 'combo', - i18n.tr('Id')), - PropertyMetaData('name', DataType.string, ':notnull:', '', - i18n.tr('Name'), - size: 64), - PropertyMetaData('displayName', DataType.string, ':unique:notnull:', - '', i18n.tr('Display name', M), - size: 32), - PropertyMetaData('email', DataType.string, ':unique:notnull:', '', - i18n.tr('EMail', M), - size: 255), - PropertyMetaData('role', DataType.reference, ':notnull:', '', - i18n.tr('Role'), reference: 'roles.role_id'), - PropertyMetaData('created', DataType.datetime, ':hidden:', '', - i18n.tr('Created')), - PropertyMetaData('createdBy', DataType.string, ':hidden:', '', - i18n.tr('Created by'), - size: 32), - PropertyMetaData('changed', DataType.datetime, ':hidden:', '', - i18n.tr('Changed')), - PropertyMetaData('changedBy', DataType.string, ':hidden:', '', - i18n.tr('Changed by'), - size: 32), - ], - ); factory UserMeta() { return instance; } + UserMeta.internal() + : super('Users', [ + PropertyMetaData('id', i18n.tr('Id'), DataType.reference, ':primary:', + displayType: DisplayType.combobox), + PropertyMetaData( + 'name', i18n.tr('Name'), DataType.string, ':notnull:', + size: 64), + PropertyMetaData('displayName', i18n.tr('Display name', M), + DataType.string, ':unique:notnull:', + size: 32), + PropertyMetaData( + 'email', i18n.tr('EMail', M), DataType.string, ':unique:notnull:', + size: 255), + PropertyMetaData( + 'role', i18n.tr('Role'), DataType.reference, ':notnull:', + displayType: DisplayType.combobox, reference: 'roles.role_id'), + PropertyMetaData( + 'created', i18n.tr('Created'), DataType.datetime, ':hidden:'), + PropertyMetaData( + 'createdBy', i18n.tr('Created by'), DataType.string, ':hidden:', + size: 32), + PropertyMetaData( + 'changed', i18n.tr('Changed'), DataType.datetime, ':hidden:'), + PropertyMetaData( + 'changedBy', i18n.tr('Changed by'), DataType.string, ':hidden:', + size: 32), + ], [ + PageMetaData('New User', PageType.create, []), + PageMetaData('Change User', PageType.edit, []), + PageMetaData('Delete User', PageType.delete, []), + PageMetaData('Users Overview', PageType.list, []), + ]); } diff --git a/lib/page/info_page.dart b/lib/page/info_page.dart index 9e8b839..ea8b7bc 100644 --- a/lib/page/info_page.dart +++ b/lib/page/info_page.dart @@ -70,7 +70,7 @@ Zur Entwicklung wurden einige **Opensource-Pakete** verwendet: ) async { if (href!.startsWith('file:')) { if (++clickCounter > 3) { - Navigator.pushNamed(context, '/configuration'); + // globalData.navigate(context, '/configurations/base'); } } else { await canLaunch(href) diff --git a/lib/page/page_collection.dart b/lib/page/page_collection.dart new file mode 100644 index 0000000..2187508 --- /dev/null +++ b/lib/page/page_collection.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import '../setting/global_data.dart'; + +import 'users/users_edit_page.dart'; + +/// Manages all meta data driven pages of the program. +class PageCollection { + static PageCollection? _instance; + PageCollection.internal(); + factory PageCollection() { + _instance ??= PageCollection.internal(); + return _instance!; + } + final map = {}; + + /// Creates a page defined by a [route]. + StatefulWidget? newPageByRoute(String route) { + StatefulWidget? rc; + final globalData = GlobalData(); + switch (route) { + case '/users/edit': + rc = UsersEditPage(globalData); + break; + } + if (rc != null) { + map[route] = rc; + } + return rc; + } + + /// Returns the last generated page of a given [route]. + StatefulWidget? existingPageByRoute(String route) { + return map[route]; + } +} diff --git a/lib/page/roles/role_data.dart b/lib/page/roles/role_data.dart new file mode 100644 index 0000000..3a0da8b --- /dev/null +++ b/lib/page/roles/role_data.dart @@ -0,0 +1,96 @@ +// DO NOT CHANGE. This file is created by the meta_tool +import '../../base/defines.dart'; +import '../../base/helper.dart'; +import '../../persistence/data_record.dart'; + +class RoleData extends DataRecord { + int? id; + String? name; + DateTime? created; + String? createdBy; + DateTime? changed; + String? changedBy; + RoleData( + {this.id, + this.name, + this.created, + this.createdBy, + this.changed, + this.changedBy}); + RoleData.createFromMap(DataMap map) { + fromMap(map); + } + @override + void fromMap(DataMap map) { + id = map.containsKey('role_id') + ? valueOf(DataType.reference, map['role_id']) + : null; + name = map.containsKey('role_name') + ? valueOf(DataType.string, map['role_name']) + : null; + created = map.containsKey('role_created') + ? valueOf(DataType.datetime, map['role_created']) + : null; + createdBy = map.containsKey('role_createdby') + ? valueOf(DataType.string, map['role_createdby']) + : null; + changed = map.containsKey('role_changed') + ? valueOf(DataType.datetime, map['role_changed']) + : null; + changedBy = map.containsKey('role_changedby') + ? valueOf(DataType.string, map['role_changedby']) + : null; + } + + @override + int keyOf() { + return id ?? 0; + } + + @override + String nameOfKey() { + return 'role_id'; + } + + static DataType? dataTypeOf(String name) { + DataType? rc; + switch (name) { + case 'id': + rc = DataType.reference; + break; + case 'name': + rc = DataType.string; + break; + case 'created': + rc = DataType.datetime; + break; + case 'createdBy': + rc = DataType.string; + break; + case 'changed': + rc = DataType.datetime; + break; + case 'changedBy': + rc = DataType.string; + break; + default: + break; + } + return rc; + } + + @override + DataMap toMap({DataMap? map, bool clear = true}) { + map ??= DataMap(); + if (clear) { + map.clear(); + } + map['role_id'] = id; + map['role_name'] = name; + map['role_created'] = created; + map['role_createdby'] = createdBy; + map['role_changed'] = changed; + map['role_changedby'] = changedBy; + return map; + } +} diff --git a/lib/page/start_page.dart b/lib/page/start_page.dart index fbec21d..71f72c6 100644 --- a/lib/page/start_page.dart +++ b/lib/page/start_page.dart @@ -7,9 +7,9 @@ import 'package:exhibition/setting/drawer_exhibition.dart'; import 'package:exhibition/setting/footer_exhibition.dart'; import 'package:flutter/material.dart'; +import '../setting/global_data.dart'; //import '../helper/exhibition_defines.dart'; import '../setting/installation.dart'; -import '../setting/global_data.dart'; class StartPage extends StatefulWidget { final BaseLogger logger; @@ -96,12 +96,13 @@ class StartPageState extends State { RestPersistence.fromConfig(configuration, logger), logger); globalData.initializeAsync().then((value) { - Navigator.pushNamed(context, '/users/list'); + globalData.navigate(context, '/users/edit'); }); } }); }); - }; + } + ; }); }); } diff --git a/lib/page/users/user_data.dart b/lib/page/users/user_data.dart index 8ff59ad..5562cfd 100644 --- a/lib/page/users/user_data.dart +++ b/lib/page/users/user_data.dart @@ -1,7 +1,9 @@ // DO NOT CHANGE. This file is created by the meta_tool import '../../base/defines.dart'; import '../../base/helper.dart'; -class UserData{ +import '../../persistence/data_record.dart'; + +class UserData extends DataRecord { int? id; String? name; String? displayName; @@ -11,52 +13,111 @@ class UserData{ String? createdBy; DateTime? changed; String? changedBy; - UserData({ this.id, this.name, this.displayName, this.email, this.role, this.created, this.createdBy, this.changed, this.changedBy}); - UserData.fromYaml(YamlMap data) { - id = data.containsKey('user_id') ? valueOf(DataType.reference, data['user_id']) : null; - name = data.containsKey('user_name') ? valueOf(DataType.string, data['user_name']) : null; - displayName = data.containsKey('user_displayname') ? valueOf(DataType.string, data['user_displayname']) : null; - email = data.containsKey('user_email') ? valueOf(DataType.string, data['user_email']) : null; - role = data.containsKey('user_role') ? valueOf(DataType.reference, data['user_role']) : null; - created = data.containsKey('created') ? valueOf(DataType.datetime, data['created']) : null; - createdBy = data.containsKey('createdby') ? valueOf(DataType.string, data['createdby']) : null; - changed = data.containsKey('changed') ? valueOf(DataType.datetime, data['changed']) : null; - changedBy = data.containsKey('changedby') ? valueOf(DataType.string, data['changedby']) : null; + UserData( + {this.id, + this.name, + this.displayName, + this.email, + this.role, + this.created, + this.createdBy, + this.changed, + this.changedBy}); + UserData.createFromMap(DataMap map) { + fromMap(map); + } + @override + void fromMap(DataMap map) { + id = map.containsKey('user_id') + ? valueOf(DataType.reference, map['user_id']) + : null; + name = map.containsKey('user_name') + ? valueOf(DataType.string, map['user_name']) + : null; + displayName = map.containsKey('user_displayname') + ? valueOf(DataType.string, map['user_displayname']) + : null; + email = map.containsKey('user_email') + ? valueOf(DataType.string, map['user_email']) + : null; + role = map.containsKey('user_role') + ? valueOf(DataType.reference, map['user_role']) + : null; + created = map.containsKey('user_created') + ? valueOf(DataType.datetime, map['user_created']) + : null; + createdBy = map.containsKey('user_createdby') + ? valueOf(DataType.string, map['user_createdby']) + : null; + changed = map.containsKey('user_changed') + ? valueOf(DataType.datetime, map['user_changed']) + : null; + changedBy = map.containsKey('user_changedby') + ? valueOf(DataType.string, map['user_changedby']) + : null; + } + + @override + int keyOf() { + return id ?? 0; + } + + @override + String nameOfKey() { + return 'user_id'; } + static DataType? dataTypeOf(String name) { DataType? rc; - switch(name){ - case 'id': - rc = DataType.reference; - break; - case 'name': - rc = DataType.string; - break; - case 'displayName': - rc = DataType.string; - break; - case 'email': - rc = DataType.string; - break; - case 'role': - rc = DataType.reference; - break; - case 'created': - rc = DataType.datetime; - break; - case 'createdBy': - rc = DataType.string; - break; - case 'changed': - rc = DataType.datetime; - break; - case 'changedBy': - rc = DataType.string; - break; - default: - break; + switch (name) { + case 'id': + rc = DataType.reference; + break; + case 'name': + rc = DataType.string; + break; + case 'displayName': + rc = DataType.string; + break; + case 'email': + rc = DataType.string; + break; + case 'role': + rc = DataType.reference; + break; + case 'created': + rc = DataType.datetime; + break; + case 'createdBy': + rc = DataType.string; + break; + case 'changed': + rc = DataType.datetime; + break; + case 'changedBy': + rc = DataType.string; + break; + default: + break; } return rc; } -} + @override + DataMap toMap({DataMap? map, bool clear = true}) { + map ??= DataMap(); + if (clear) { + map.clear(); + } + map['user_id'] = id; + map['user_name'] = name; + map['user_displayname'] = displayName; + map['user_email'] = email; + map['user_role'] = role; + map['user_created'] = created; + map['user_createdby'] = createdBy; + map['user_changed'] = changed; + map['user_changedby'] = changedBy; + return map; + } +} diff --git a/lib/page/users/user_state.dart b/lib/page/users/user_state.dart deleted file mode 100644 index e23b4a5..0000000 --- a/lib/page/users/user_state.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:exhibition/setting/global_data.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../base/defines.dart'; -import '../../persistence/rest_persistence.dart'; -import 'user_data.dart'; - -class RemoteListUserEvent extends UserEvent { - final Map filters; - RemoteListUserEvent(this.filters) - : super(EventSource.remoteData, EventCardinality.list); - @override - List get props => [filters]; -} - -class UserBloc extends Bloc { - final YamlList? recordList; - - UserBloc({this.recordList}) : super(UserStateInitial()); - - @override - Stream mapEventToState( - UserEvent event, - ) async* { - if (event is RemoteListUserEvent) { - final list = await GlobalData() - .restPersistence - ?.query(what: 'query', data: event.filters); - yield UserListRemote(list ?? []); - } else { - yield UserStateInitial(); - } - } -} - -abstract class UserEvent extends Equatable { - final EventSource eventSource; - final EventCardinality eventCardinality; - UserEvent(this.eventSource, this.eventCardinality); -} - -class UserListRemote extends UserState { - final YamlList records; - UserListRemote(this.records); - @override - List get props => [records]; -} - -abstract class UserState extends Equatable {} - -class UserStateInitial extends UserState { - @override - List get props => []; -} diff --git a/lib/page/users/users_edit_page.dart b/lib/page/users/users_edit_page.dart new file mode 100644 index 0000000..4f66ce6 --- /dev/null +++ b/lib/page/users/users_edit_page.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import '../../base/i18n.dart'; +import '../../meta/users_meta.dart'; +import '../../setting/global_data.dart'; +import '../../widget/attended_page.dart'; +import '../../widget/widget_form.dart'; + +final i18n = I18N(); + +class UsersEditPage extends StatefulWidget { + final GlobalData globalData; + final PageStates pageStates = PageStates(); + UsersEditPage(this.globalData) : super(); + @override + _UsersEditPageState createState() { + final rc = _UsersEditPageState(); + rc.attendedPage = + AttendedPage(this, rc, globalData, UserMeta.instance, pageStates); + pageStates.attendedPage = rc.attendedPage; + return rc; + } +} + +class _UsersEditPageState extends State { + AttendedPage? attendedPage; + + final nameController = TextEditingController(); + final globalData = GlobalData(); + @override + Widget build(BuildContext context) { + final padding = 16.0; + final rc = Scaffold( + appBar: globalData.appBarBuilder(i18n.tr('Change User data')), + drawer: globalData.drawerBuilder(context), + body: SafeArea( + child: WidgetForm.flexibleGrid(attendedPage!.attendedWidgets(), + screenWidth: attendedPage!.pageStates.screenWidth, + padding: padding))); + return rc; + } + + @override + void didChangeDependencies() { + final size = MediaQuery.of(context).size; + attendedPage!.pageStates.screenWidth = size.width; + attendedPage!.pageStates.screenHeight = size.height; + super.didChangeDependencies(); + } + + @override + void initState() { + super.initState(); + } +} diff --git a/lib/page/users/users_list_page.dart b/lib/page/users/users_list_page.dart deleted file mode 100644 index 505c897..0000000 --- a/lib/page/users/users_list_page.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:exhibition/setting/global_data.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../base/defines.dart'; -import 'user_state.dart'; -import 'package:equatable/equatable.dart'; -import '../../base/i18n.dart'; -final i18n = I18N(); -class UsersListPage extends StatefulWidget { - final GlobalData globalData; - UsersListPage(this.globalData): super(); - @override - _UsersListPageState createState() => _UsersListPageState(); -} - -class _UsersListPageState extends State { - double? width; - - @override - void initState() { - //context.read().add(UserEvent(EventSource.undef, EventCardinality.undef)); - super.initState(); - } - - @override - void didChangeDependencies() { - width = MediaQuery.of(context).size.width; - super.didChangeDependencies(); - } - final nameController = TextEditingController(); - @override - Widget build(BuildContext context) { - final padding = 16.0; - return BlocBuilder( - builder: (context, UserState state) { - return Column( - children: [ - TextFormField( - controller: nameController, - readOnly: true, - decoration: InputDecoration(labelText: i18n.tr('Name')), - ), - SizedBox(height: padding), - Text('Liste'), - Spacer(), - //UserHistoryContainer( - // calculations: state.history.reversed.toList()) - ], - ); - }, - ); - } - -} diff --git a/lib/persistence/data_record.dart b/lib/persistence/data_record.dart new file mode 100644 index 0000000..0ab6561 --- /dev/null +++ b/lib/persistence/data_record.dart @@ -0,0 +1,31 @@ +typedef DataMap = Map; + +/// Implements a base class for entities handled in [Persistence]. +/// +/// Such an entity is a collection of "values" like a table in a relational +/// database with its columns as "values". +/// +/// T is the data type of the key "value", which is unique over all records. +abstract class DataRecord { + /// Takes the data from the [map] and put it in the instance of the + /// derived class. + void fromMap(DataMap map); + + /// Returns the "key" of the record. This a unique value over all + /// existing records. + T keyOf(); + + /// Returns name of the "key" of the record, for example "id". + String nameOfKey(); + + /// Stores the "values" of the derived class as a Json like map. + /// + /// If [map] is not null this map is used for storage and that map is returned. + /// + /// If [clear] is true and [map] is not null [map] will be cleared before + /// populating that. + /// + /// Returns a map with the "values" of the derived class. If [map] is not + /// null [map] will be returned. + DataMap toMap({DataMap? map, bool clear = true}); +} diff --git a/lib/persistence/data_table.dart b/lib/persistence/data_table.dart new file mode 100644 index 0000000..5f9f924 --- /dev/null +++ b/lib/persistence/data_table.dart @@ -0,0 +1,39 @@ +import 'data_record.dart'; + +typedef RecordFilter = bool Function(DataRecord element); +typedef KeyFilter = bool Function(T key); + +/// Implements a container of [DataRecord]s. +/// +/// T is the data type of the primary key in [DataRecord]. +class DataTable { + final Map> records = {}; + final keys = >[]; + + /// Returns the record with the given [key] or null if not found. + DataRecord? byKey(T key) { + final rc = records[key]; + return rc; + } + + /// Returns a sequence of [DataRecord]s. + /// + /// [recordFilter]: Must return true on a given record. + /// + /// [keyFilter]: Must return true on a given key. + /// + /// If both filters are not null the [keyFilter] is applied first. + /// + /// Returns the records matching the filters. If no filter is given all + /// records are returned. + Iterable> recordsOf( + {RecordFilter? recordFilter, KeyFilter? keyFilter}) { + final keys2 = + keyFilter == null ? keys : keys.where((element) => keyFilter(element)); + final records2 = keys2.map((key) => records[key]); + final rc = recordFilter == null + ? records2 + : records2.where((element) => recordFilter(element as DataRecord)); + return rc as Iterable>; + } +} diff --git a/lib/persistence/file_persistence.dart b/lib/persistence/file_persistence.dart new file mode 100644 index 0000000..c1bee7d --- /dev/null +++ b/lib/persistence/file_persistence.dart @@ -0,0 +1,67 @@ +import 'dart:convert' as convert; +import 'dart:io'; + +import 'package:dart_bones/dart_bones.dart'; +import 'package:path/path.dart' as path; + +import 'persistence.dart'; +import 'data_record.dart'; + +/// Implements a persistence layer storing data in Json files. +class FilePersistence extends Persistence { + final BaseLogger logger; + String dataDirectory; + //String configDirectory; + + FilePersistence( + {required this.dataDirectory, + //required this.configDirectory, + required this.logger}); + + FilePersistence.fromConfig(BaseConfiguration configuration, BaseLogger logger, + {String section = 'persistence'}) + : this( + dataDirectory: + configuration.asString('sampleDirectory', section: section) ?? + '', + // configDirectory: + // configuration.asString('configDirectory', section: section) ?? + // '', + logger: logger); + + @override + Future query({required String what, DataMap? data}) async { + var rc; + final fn = path.join(dataDirectory, '$what.json'); + final file = File(fn); + if (!await file.exists()) { + logger.error('missing $fn'); + } else { + final content = await file.readAsString(); + rc = convert.jsonDecode(content); + } + + return rc; + } + + @override + Future store( + {required String what, DataMap? map, DataRecord? dataContainer}) async { + String rc = 'OK'; + assert(map != null && dataContainer == null || + map == null && dataContainer != null); + if (dataContainer != null) { + map = dataContainer.toMap(); + } + final fn = path.join(dataDirectory, '$what.json'); + final file = File(fn); + logger.log('Storing data in $what.json'); + try { + final content = convert.jsonEncode(map); + await file.writeAsString(content); + } on FileSystemException catch (exc) { + logger.error(rc = 'cannot write into $fn: $exc'); + } + return rc; + } +} diff --git a/lib/persistence/persistence.dart b/lib/persistence/persistence.dart index 2f847d0..1ef20ca 100644 --- a/lib/persistence/persistence.dart +++ b/lib/persistence/persistence.dart @@ -1,15 +1,26 @@ +import 'data_record.dart'; + abstract class Persistence { /// Fetches data specified by [what] and some [data]. + /// /// Returns null or a record (as a Map instance) /// or a list of records (as a List instance). - Future query({required String what, Map? data}); + Future query({required String what, DataMap? data}); - /// Executes a SQL statement with not or a very short result: - /// insert: answers the primary key. - /// update: returns the count of changed records. - /// delete: ... - /// Returns the answer. If a blank is part of the answer that is an error - /// message. + /// Stores a map or a data container in the persistence layer. + /// + /// [what] specifies the kind of data. The backend server must "understand" + /// that value. + /// + /// [map] is a Json like map with the data to store. + /// + /// [dataContainer] is the container with the values to store. + /// + /// Exactly one of [map] must be null and the other must be not null. + /// + /// Returns the "answer" of the persistence layer. Convention: a single word + /// defines a state like "OK" and a blank inside defines an human readable + /// error message. Future store( - {required String what, required Map data}); + {required String what, DataMap? map, DataRecord? dataContainer}); } diff --git a/lib/persistence/rest_persistence.dart b/lib/persistence/rest_persistence.dart index 54206b7..a825562 100644 --- a/lib/persistence/rest_persistence.dart +++ b/lib/persistence/rest_persistence.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:dart_bones/dart_bones.dart'; import 'package:http/http.dart' as http; +import 'data_record.dart'; import 'persistence.dart'; import '../setting/error_handler_exhibition.dart'; @@ -67,7 +68,7 @@ class RestPersistence extends Persistence { netStatus = NetStatus.noConnection; errorHandler?.criticalError(exc.toString(), caller: 'RestPersistence.runRequest'); - } on Exception catch(exc){ + } on Exception catch (exc) { logger.error('$exc'); netStatus = NetStatus.noConnection; } @@ -76,10 +77,15 @@ class RestPersistence extends Persistence { @override Future store( - {required String what, Map? data}) async { + {required String what, DataMap? map, DataRecord? dataContainer}) async { var rc = 'OK'; - final data2 = data == null ? '{}' : convert.jsonEncode(data); - final answer = await runRequest(what, body: data2, headers: jsonHeader); + assert(map != null && dataContainer == null || + map == null && dataContainer != null); + if (dataContainer != null) { + map = dataContainer.toMap(); + } + final data = convert.jsonEncode(map); + final answer = await runRequest(what, body: data, headers: jsonHeader); if (answer.isNotEmpty) { rc = answer; } @@ -92,7 +98,8 @@ class RestPersistence extends Persistence { String rc; final params2 = data == null ? '{}' : convert.jsonEncode(data); final answer = await runRequest(what, body: params2, headers: jsonHeader); - if (answer.isNotEmpty && (answer.startsWith('{') || answer.startsWith('['))) { + if (answer.isNotEmpty && + (answer.startsWith('{') || answer.startsWith('['))) { rc = convert.jsonDecode(answer); } else { rc = answer; @@ -103,6 +110,5 @@ class RestPersistence extends Persistence { return rc; } } -enum NetStatus { - undefined, noConnection, fail, ok -} \ No newline at end of file + +enum NetStatus { undefined, noConnection, fail, ok } diff --git a/lib/setting/app_bar_exhibition.dart b/lib/setting/app_bar_exhibition.dart index 1ec7c6a..9ad43f1 100644 --- a/lib/setting/app_bar_exhibition.dart +++ b/lib/setting/app_bar_exhibition.dart @@ -6,5 +6,5 @@ class AppBarExhibition extends AppBar { : super(title: Text(title), key: key); static AppBarBuilder builder() => - (String title) => AppBarExhibition(title: title); + (String title) => AppBarExhibition(title: title); } diff --git a/lib/setting/drawer_exhibition.dart b/lib/setting/drawer_exhibition.dart index a8f7d1b..70b8847 100644 --- a/lib/setting/drawer_exhibition.dart +++ b/lib/setting/drawer_exhibition.dart @@ -58,7 +58,6 @@ class DrawerExhibition extends Drawer { .map((item) => ListTile( title: Text(item.title), onTap: () { - // What happens after you tap the navigation item Navigator.push( context, MaterialPageRoute( diff --git a/lib/setting/global_data.dart b/lib/setting/global_data.dart index de67da3..1d8f79b 100644 --- a/lib/setting/global_data.dart +++ b/lib/setting/global_data.dart @@ -1,5 +1,7 @@ import 'package:dart_bones/dart_bones.dart'; +import 'package:exhibition/page/page_collection.dart'; import 'package:exhibition/persistence/rest_persistence.dart'; +import 'package:exhibition/widget/attended_page.dart'; import 'package:flutter/material.dart'; import '../base/defines.dart'; @@ -38,6 +40,7 @@ class GlobalData { final FooterBuilder footerBuilder; final BaseConfiguration configuration; final RestPersistence? restPersistence; + AttendedPage? currentPage; factory GlobalData() => _instance ?? GlobalData(); GlobalData.dummy() @@ -47,14 +50,27 @@ class GlobalData { /// [configuration]: general settings. /// [appBarBuilder]: a factory to create the Hamburger menu. /// [footerBuilder]: a factory to create a footer area. - GlobalData.internal(this.configuration, this.appBarBuilder, - this.drawerBuilder, this.footerBuilder, - this.restPersistence, this.logger) { + GlobalData.internal( + this.configuration, + this.appBarBuilder, + this.drawerBuilder, + this.footerBuilder, + this.restPersistence, + this.logger) { logger.log('Start'); _instance = this; } - Future initializeAsync() async{ + Future initializeAsync() async { // Do customize! return; } + + /// Switches the page given by a [route]. + void navigate(BuildContext context, String route) { + final page = PageCollection().existingPageByRoute(route); + if (page != null) { + currentPage = page as AttendedPage; + } + Navigator.pushNamed(context, route); + } } diff --git a/lib/widget/attended_page.dart b/lib/widget/attended_page.dart new file mode 100644 index 0000000..7dae3c0 --- /dev/null +++ b/lib/widget/attended_page.dart @@ -0,0 +1,71 @@ +import 'package:exhibition/widget/attended_widget.dart'; +import 'package:flutter/material.dart'; + +import '../../base/i18n.dart'; +import '../../setting/global_data.dart'; +import '../meta/module_meta_data.dart'; + +final i18n = I18N(); + +/// Links some instances belonging to a module page (given as +/// member "statefulWidget"). +/// +/// Note: There may be several instances of AttendedPage belonging to one +/// derived class of StatefulWidget (member "statefulWidget"). Do not store +/// states directly in this class. Store it in [PageStates]. +class AttendedPage { + final ModuleMetaData moduleMetaData; + final GlobalData globalData; + final StatefulWidget statefulWidget; + final State state; + final PageStates pageStates; + AttendedPage(this.statefulWidget, this.state, this.globalData, + this.moduleMetaData, this.pageStates); + + /// Returns the list of attended widgets of this page. + List attendedWidgets() { + final rc = []; + for (var item in moduleMetaData.propertyList) { + if (!item.hasOption(':hidden:')) { + switch (item.displayType) { + case DisplayType.text: + rc.add(TextAttended(item, this)); + break; + case DisplayType.combobox: + rc.add(ComboboxAttended(item, this)); + break; + case DisplayType.checkbox: + throw FormatException( + 'AttendedPage.widgetOf(): not implemented: checkbox'); + case DisplayType.custom: + throw FormatException( + 'AttendedPage.widgetOf(): not implemented: custom'); + } + } + } + return rc; + } + + /// Validates the text [value] of the [textAttended]. + /// + /// Returns null on success or the error message on error. + String? validateText(String? value, TextAttended textAttended) { + PropertyMetaData property = textAttended.propertyMetaData; + String? rc; + if ((value == null || value.isEmpty) && + (property.hasOption(':mandatory:') || + property.hasOption(':notnull:'))) { + rc = i18n.tr('This field must not be empty.'); + } + return rc; + } +} + +enum DeliveryState { undef, waiting, delivered } + +class PageStates { + final DeliveryState deliveryState = DeliveryState.undef; + double screenWidth = 0; + double screenHeight = 0; + AttendedPage? attendedPage; +} diff --git a/lib/widget/attended_widget.dart b/lib/widget/attended_widget.dart new file mode 100644 index 0000000..e88c5b8 --- /dev/null +++ b/lib/widget/attended_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +import '../meta/module_meta_data.dart'; +import '../widget/attended_page.dart'; + +/// Manages a widget controlled by the meta data. +abstract class AttendedWidget { + final AttendedPage attendedPage; + final PropertyMetaData propertyMetaData; + + AttendedWidget(this.propertyMetaData, this.attendedPage); + Widget widgetOf(); +} + +class ComboboxAttended extends FieldAttended { + T? value; + MenuItemBuilder menuItemBuilder = + (PropertyMetaData _) => >[]; + // MenuItemBuilder menuItemBuilder = + // ((PropertyMetaData _) => >[]) as MenuItemBuilder; + ComboboxAttended(PropertyMetaData propertyMetaData, AttendedPage attendedPage) + : super(propertyMetaData, attendedPage); + Widget widgetOf() { + final rc = DropdownButtonFormField( + value: value, + items: menuItemBuilder(propertyMetaData), + isExpanded: true, + decoration: InputDecoration(labelText: propertyMetaData.label), + onChanged: (value) => this.value = value, + ); + return rc; + } +} + +abstract class FieldAttended extends AttendedWidget { + var readonly = false; + var enabled = true; + FieldAttended(PropertyMetaData propertyMetaData, AttendedPage attendedPage) + : super(propertyMetaData, attendedPage); +} + +/// Implements a single or multiline line text field controlled by the +/// meta data of a module. +class TextAttended extends FieldAttended { + final controller = TextEditingController(); + int minLines = 1; + int maxLines = 1; + String value = ''; + + TextAttended(PropertyMetaData propertyMetaData, AttendedPage attendedPage) + : super(propertyMetaData, attendedPage); + + /// Returns a concrete widget representing the instance. + Widget widgetOf() { + final rc = TextFormField( + controller: controller, + minLines: minLines, + maxLines: maxLines, + onSaved: (value) => this.value = value ?? '', + validator: (value) => attendedPage.validateText(value, this), + decoration: InputDecoration(labelText: propertyMetaData.label), + ); + return rc; + } +} diff --git a/lib/widget/widget_form.dart b/lib/widget/widget_form.dart new file mode 100644 index 0000000..7b6cdab --- /dev/null +++ b/lib/widget/widget_form.dart @@ -0,0 +1,74 @@ +import 'package:exhibition/widget/attended_widget.dart'; +import 'package:flutter/material.dart'; + +class WidgetForm { + /// Places the widgets in a flexible grid. + /// + /// Returns a column of rows. + /// + /// The place of the row is divided in 12 segments with the same width. + /// Each widget has a "weight" which means the count of segments to use. + /// + /// [widgets] is the list of widgets to position in the grid. + /// + /// [screenWidth] is the width of the output device in pixel. + /// + /// [minWidth] is the minimum width of the above defined segment. + /// If the [screenwidth] has not place for 6 segments all widgets are placed + /// in a single row. + static Widget flexibleGrid(List widgets, + {required double screenWidth, + double minWidth = 800, + double padding = 16.0}) { + Widget rc; + if (minWidth > screenWidth) { + final children = widgets.map((element) => element.widgetOf()).toList(); + rc = Column(children: children); + } else { + int position = 0; + final List childrenColumn = []; + List childrenRow = []; + for (var widget in widgets) { + final flex = widget.propertyMetaData.weight; + if (position + flex <= 12) { + var child2 = position == 0 + ? widget.widgetOf() + : Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox(width: padding), + Expanded(child: widget.widgetOf()) + ], + ); + childrenRow.add(Expanded(flex: flex, child: child2)); + position += flex; + } else { + if (position < 12) { + childrenRow + .add(Expanded(flex: 12 - position, child: SizedBox(width: 1))); + } + position = 0; + childrenColumn.add(Row(children: childrenRow)); + childrenRow = []; + final child2 = position == 0 + ? widget.widgetOf() + : Row( + children: [ + SizedBox(width: padding), + Expanded(child: widget.widgetOf()) + ], + ); + childrenRow.add(Expanded(flex: flex, child: child2)); + position = flex; + } + } + if (position < 12) { + childrenRow + .add(Expanded(flex: 12 - position, child: SizedBox(width: 1))); + } + childrenColumn.add(Row(children: childrenRow)); + rc = Column(children: childrenColumn); + } + return rc; + } +} diff --git a/rest_client/pubspec.yaml b/rest_client/pubspec.yaml index c7c4f54..515341f 100644 --- a/rest_client/pubspec.yaml +++ b/rest_client/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.1.0 # homepage: https://www.example.com environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.14.0 <3.0.0' dependencies: http: ^0.13.0 diff --git a/rest_client/test/rest_client_test.dart b/rest_client/test/rest_client_test.dart index f50395f..c6f196b 100644 --- a/rest_client/test/rest_client_test.dart +++ b/rest_client/test/rest_client_test.dart @@ -60,7 +60,7 @@ void main() async { ':email': 'mozart@salzburg.au', ':createdby': 'unittest' }; - var result = await client.store(what: 'store', data: parameters); + var result = await client.store(what: 'store', map: parameters); expect(result, isNotNull); final match = RegExp(r'^id:(\d+)$').firstMatch(result); expect(match, isNotNull); @@ -75,7 +75,7 @@ void main() async { ':email': 'bach@wien.at', ':changedby': 'unittest' }; - var result = await client.store(what: 'store', data: parameters); + var result = await client.store(what: 'store', map: parameters); expect(result, isNotNull); final match = RegExp(r'^rows:(\d+)$').firstMatch(result); expect(match, isNotNull); @@ -94,7 +94,7 @@ void main() async { 'sql': 'delete', ':id': id.toString(), }; - var result = await client.store(what: 'store', data: parameters); + var result = await client.store(what: 'store', map: parameters); expect(result, isNotNull); final match = RegExp(r'^rows:(\d+)$').firstMatch(result); expect(match, isNotNull); diff --git a/rest_server/data/sql/roles.sql.yaml b/rest_server/data/sql/roles.sql.yaml new file mode 100644 index 0000000..e1ad906 --- /dev/null +++ b/rest_server/data/sql/roles.sql.yaml @@ -0,0 +1,28 @@ +--- +# DO NOT CHANGE. This file is created by the meta_tool +# SQL statements of the module "Roles": + +module: Roles +list: + type: list + parameters: [] + sql: "select * from Roles;" +byId: + type: record + parameters: [ "id" ] + sql: "select * from Roles where role_id=:id;" +delete: + type: delete + parameters: [ "id" ] + sql: "delete * from Roles where role_id=:id;" +update: + type: update + parameters: [":id",":name",":changedBy"] + sql: "UPDATE Roles SET + role_id=:id,role_name=:name,role_changedby=:changedBy,role_changed=NOW() + WHERE role_id=:id;" +insert: + type: insert + parameters: [":id",":name",":createdBy"] + sql: "INSERT INTO Roles(role_id,role_name,role_createdby,role_created) + VALUES(:id,:name,:createdBy,NOW());" diff --git a/rest_server/pubspec.yaml b/rest_server/pubspec.yaml index 1f91569..86d165e 100644 --- a/rest_server/pubspec.yaml +++ b/rest_server/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.1.0 # homepage: https://www.example.com environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.14.0 <3.0.0' dependencies: http: ^0.13.0 diff --git a/test/widget_test.dart b/test/widget_test.dart index b40338f..43d1db2 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -9,8 +9,6 @@ import 'package:exhibition/exhibition_app.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:exhibition/main.dart'; - void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. diff --git a/tools/PackRestServer b/tools/PackRestServer index 5276681..3fc61f4 100755 --- a/tools/PackRestServer +++ b/tools/PackRestServer @@ -14,6 +14,7 @@ function DoIt(){ test -d data/sql/precedence || mkdir -p data/sql/precedence cd .. cp -av rest_server/tools/rest_server $BASE_DIR/$EXE + mkdir -p $BASE_DIR/data/sql tools/yaml_merger rest_server/data/sql rest_server/data/sql/precedence $BASE_DIR/data/sql cat <$BASE_DIR/INSTALL.TXT # ------------ -- 2.39.5