]> gitweb.hamatoma.de Git - exhibition.git/commitdiff
daily work
authorHamatoma <author.hamatoma.de>
Wed, 29 Sep 2021 22:25:10 +0000 (00:25 +0200)
committerHamatoma <author.hamatoma.de>
Wed, 29 Sep 2021 22:25:10 +0000 (00:25 +0200)
* new: PageMetaData
* common base for meta data driven pages: page_collection
* a 12 column layout manager (like Bootstrap): widget_form.dart

36 files changed:
Prepare [new file with mode: 0755]
bin/generator.dart
bin/meta_tool.dart
lib/base/defines.dart
lib/base/i18n.dart
lib/base/i18n_io.dart
lib/exhibition_app.dart
lib/meta/module_meta_data.dart
lib/meta/modules.dart
lib/meta/roles_meta.dart
lib/meta/users_meta.dart
lib/page/info_page.dart
lib/page/page_collection.dart [new file with mode: 0644]
lib/page/roles/role_data.dart [new file with mode: 0644]
lib/page/start_page.dart
lib/page/users/user_data.dart
lib/page/users/user_state.dart [deleted file]
lib/page/users/users_edit_page.dart [new file with mode: 0644]
lib/page/users/users_list_page.dart [deleted file]
lib/persistence/data_record.dart [new file with mode: 0644]
lib/persistence/data_table.dart [new file with mode: 0644]
lib/persistence/file_persistence.dart [new file with mode: 0644]
lib/persistence/persistence.dart
lib/persistence/rest_persistence.dart
lib/setting/app_bar_exhibition.dart
lib/setting/drawer_exhibition.dart
lib/setting/global_data.dart
lib/widget/attended_page.dart [new file with mode: 0644]
lib/widget/attended_widget.dart [new file with mode: 0644]
lib/widget/widget_form.dart [new file with mode: 0644]
rest_client/pubspec.yaml
rest_client/test/rest_client_test.dart
rest_server/data/sql/roles.sql.yaml [new file with mode: 0644]
rest_server/pubspec.yaml
test/widget_test.dart
tools/PackRestServer

diff --git a/Prepare b/Prepare
new file mode 100755 (executable)
index 0000000..051be5a
--- /dev/null
+++ b/Prepare
@@ -0,0 +1,5 @@
+#! /bin/bash
+cd lib
+dart format --fix .
+cd ..
+
index a6fa433495d672676b4c2c976b88b0ffb65411bd..89b8b7fa3f3a4534de6c21806d4e749c2ac48e6b 100644 (file)
@@ -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 = <String>[];
     final files = <String>[];
@@ -197,7 +230,7 @@ List<String> 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);
   }
 }
index ef419ea18e1f5014731720fd4d16b22377bb05eb..0c30a98356d2da034037d94d440e492e4b6126da 100644 (file)
@@ -18,7 +18,7 @@ void main(List<String> 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<String> 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
index 37a569a689b3aa8953a6f13869e47b9804197c7d..88bbea2f187af11d01e34637dade6feb7ed5fa6e 100644 (file)
@@ -1,9 +1,9 @@
 enum ServerEnvironment { productive, development }
-typedef YamlMap = Map<String, dynamic>;
-typedef YamlList = List<YamlMap>;
+typedef JsonMap = Map<String, dynamic>;
+typedef JsonList = List<JsonMap>;
 
 ///JsonMap or JsonList
-typedef YamlData = Object;
+typedef JsonData = Object;
 
 enum EventSource { undef, localData, remoteData }
 enum EventCardinality { undef, record, list }
index 018810618a8b39310ad42d907efd538707b66199..ab6c45781d5dff20aa31cc6c096cddb9fde09ac5 100644 (file)
@@ -1,6 +1,8 @@
 import 'package:dart_bones/dart_bones.dart';
+
 typedef MapModule = Map<String, String>;
 typedef MapPlural = Map<String, PluralInfo>;
+
 class I18N {
   static I18N? instance;
   final BaseLogger logger;
index e2633a2f4eb28306e958f343d8faa4c6a210734d..b490d14ed7bb724ecfd99c5b81f6d4ef22f6be99 100644 (file)
@@ -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 = <String>[];
               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)!));
index d3957ccc796607de8a94fb18a49fd2ec546e2b5e..bc485e871301c18d8f4ed438a3c374d4fcd29d4c 100644 (file)
@@ -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<dynamic>? _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<void>(
+      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<ExhibitionApp> {
     );
   }
 }
-
-Route<dynamic>? _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<void>(
-      settings: settings,
-      builder: (BuildContext context) => page!,
-      fullscreenDialog: false,
-    );
-  }
-  return route;
-}
index 7d316128b2cc5f395b80655c3f4b317d71be6335..71943ce86876ab612e656c051bb679475daa5033 100644 (file)
@@ -1,11 +1,17 @@
+import 'package:flutter/material.dart';
+
 import '../base/defines.dart';
 
+typedef MenuItemBuilder = List<DropdownMenuItem<T>> Function<T>(
+    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<PropertyMetaData> list;
+  String tableName = '';
+
+  /// If true the fields create, createdBy ... have the same ("short") column name
+  /// instead of prefix and name.
+  bool shortModifiedLabel = false;
+  List<PropertyMetaData> propertyList;
+  List<PageMetaData> pageList;
   final Map<String, PropertyMetaData> 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<PropertyMetaData> 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<FieldMetaData>? 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.
index 7ef09551c0346eeec3576bb1a85d84ef6d247141..2ee28919518c3671b3244888155b1e79874b203c 100644 (file)
@@ -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<String> moduleNames(){
+List<String> moduleNames() {
   return [
+    'Roles',
     'Users',
   ];
 }
-
index 209a139889457cf78d046dfdb61211fd4a413329..e45c3472a8df6c46ea2bad24492297b74321ed75 100644 (file)
@@ -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, []),
+        ]);
 }
index 095fc44a56d77d7bd02da554ac198e763a425b96..c627751554363bfc4c0fe764f10832f97fb099e7 100644 (file)
@@ -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, []),
+        ]);
 }
index 9e8b83900e16815deaff1332d113f94d3d0eb63d..ea8b7bc6e06e8a3957614281682ff980152f8d0b 100644 (file)
@@ -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 (file)
index 0000000..2187508
--- /dev/null
@@ -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 = <String, StatefulWidget>{};
+
+  /// 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 (file)
index 0000000..3a0da8b
--- /dev/null
@@ -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> {
+  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;
+  }
+}
index fbec21d40fe1574f26c330213c345d973cd059da..71f72c62714841a91542ae415dae0b1242c69e27 100644 (file)
@@ -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<StartPage> {
                     RestPersistence.fromConfig(configuration, logger),
                     logger);
                 globalData.initializeAsync().then((value) {
-                  Navigator.pushNamed(context, '/users/list');
+                  globalData.navigate(context, '/users/edit');
                 });
               }
             });
           });
-        };
+        }
+        ;
       });
     });
   }
index 8ff59ad08801bfeaaf16f060674e1c4c2ff3f02d..5562cfdc9d07ff348da75ee594c11230ae1111a0 100644 (file)
@@ -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> {
   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 (file)
index e23b4a5..0000000
+++ /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<String, dynamic> filters;
-  RemoteListUserEvent(this.filters)
-      : super(EventSource.remoteData, EventCardinality.list);
-  @override
-  List<Object> get props => [filters];
-}
-
-class UserBloc extends Bloc<UserEvent, UserState> {
-  final YamlList? recordList;
-
-  UserBloc({this.recordList}) : super(UserStateInitial());
-
-  @override
-  Stream<UserState> 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<Object> get props => [records];
-}
-
-abstract class UserState extends Equatable {}
-
-class UserStateInitial extends UserState {
-  @override
-  List<Object> get props => [];
-}
diff --git a/lib/page/users/users_edit_page.dart b/lib/page/users/users_edit_page.dart
new file mode 100644 (file)
index 0000000..4f66ce6
--- /dev/null
@@ -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<UsersEditPage> {
+  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 (file)
index 505c897..0000000
+++ /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<UsersListPage> {
-  double? width;
-
-  @override
-  void initState() {
-    //context.read<UserBloc>().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<UserBloc, UserState>(
-      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 (file)
index 0000000..0ab6561
--- /dev/null
@@ -0,0 +1,31 @@
+typedef DataMap = Map<String, dynamic>;
+
+/// 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<T> {
+  /// 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 (file)
index 0000000..5f9f924
--- /dev/null
@@ -0,0 +1,39 @@
+import 'data_record.dart';
+
+typedef RecordFilter = bool Function<T>(DataRecord<T> element);
+typedef KeyFilter = bool Function<T>(T key);
+
+/// Implements a container of [DataRecord]s.
+///
+/// T is the data type of the primary key in [DataRecord].
+class DataTable<T> {
+  final Map<T, DataRecord<T>> records = {};
+  final keys = <List<T>>[];
+
+  /// Returns the record with the given [key] or null if not found.
+  DataRecord<T>? 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<DataRecord<T>> recordsOf<T>(
+      {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<T>));
+    return rc as Iterable<DataRecord<T>>;
+  }
+}
diff --git a/lib/persistence/file_persistence.dart b/lib/persistence/file_persistence.dart
new file mode 100644 (file)
index 0000000..c1bee7d
--- /dev/null
@@ -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<dynamic> 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<String> 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;
+  }
+}
index 2f847d0c1ec9d47032c3ecd7293098b21276e474..1ef20cad55648c7996ce7e85198e3cd983fcbed4 100644 (file)
@@ -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<Map> instance).
-  Future<dynamic> query({required String what, Map<String, dynamic>? data});
+  Future<dynamic> 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<String> store(
-      {required String what, required Map<String, dynamic> data});
+      {required String what, DataMap? map, DataRecord? dataContainer});
 }
index 54206b78d830811a567819e9fa83da37e13c3a8e..a82556252d644e8e0a67f8aa852408558b6a5c2d 100644 (file)
@@ -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<String> store(
-      {required String what, Map<String, dynamic>? 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 }
index 1ec7c6aa1e0614e44bb350cf4a731b20dc881a33..9ad43f1509bfb04d4ee125354f670915cac664d6 100644 (file)
@@ -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);
 }
index a8f7d1bb253c16712ad91404314a03183019fe71..70b884753f191a2d86c42e197717bd3e84f0f2d5 100644 (file)
@@ -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(
index de67da352d64db25367d2035c8c75acf2ee828d5..1d8f79bac2811f56b0394120b65d56fc16c79fa8 100644 (file)
@@ -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 (file)
index 0000000..7dae3c0
--- /dev/null
@@ -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<StatefulWidget> 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<AttendedWidget> attendedWidgets() {
+    final rc = <AttendedWidget>[];
+    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 (file)
index 0000000..e88c5b8
--- /dev/null
@@ -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<T> extends FieldAttended {
+  T? value;
+  MenuItemBuilder menuItemBuilder =
+      <T>(PropertyMetaData _) => <DropdownMenuItem<T>>[];
+  // MenuItemBuilder menuItemBuilder =
+  //     ((PropertyMetaData _) => <DropdownMenuItem<T>>[]) as MenuItemBuilder;
+  ComboboxAttended(PropertyMetaData propertyMetaData, AttendedPage attendedPage)
+      : super(propertyMetaData, attendedPage);
+  Widget widgetOf() {
+    final rc = DropdownButtonFormField<T>(
+      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 (file)
index 0000000..7b6cdab
--- /dev/null
@@ -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<AttendedWidget> 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<Widget> childrenColumn = [];
+      List<Widget> 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;
+  }
+}
index c7c4f5473b7f65aa820289b17c60682f1cf0a7bf..515341f34ca04d8d5c610661258a3aa0d4c4d5b3 100644 (file)
@@ -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
index f50395f351afc95477827461818af2fa47598d5b..c6f196b9126567315253baf582b711f55f5a4b40 100644 (file)
@@ -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 (file)
index 0000000..e1ad906
--- /dev/null
@@ -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());"
index 1f915698b78cbb5e6e34cb5aa732c187c40d8b15..86d165e062f86588976bcb08d2d03656b809891a 100644 (file)
@@ -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
index b40338f04f67e54102928690a1a35e7ca3372faf..43d1db20d78164101438faf62f319beec7384529 100644 (file)
@@ -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.
index 52766811650231a235c7d444fe4edd179c374378..3fc61f450213d6ae4084a3838cc5b4d5c14ec2a3 100755 (executable)
@@ -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 <<EOS >$BASE_DIR/INSTALL.TXT
 # ------------