]> gitweb.hamatoma.de Git - flutter_bones.git/commitdiff
daily work:
authorHamatoma <author@hamatoma.de>
Tue, 27 Oct 2020 09:39:01 +0000 (10:39 +0100)
committerHamatoma <author@hamatoma.de>
Wed, 28 Oct 2020 23:30:22 +0000 (00:30 +0100)
* error message on network error
* list redraw after create and change

37 files changed:
data/rest/configuration.yaml
data/rest/user.yaml
lib/app.dart
lib/src/helper/string_helper.dart
lib/src/model/all_db_fields_model.dart
lib/src/model/column_model.dart
lib/src/model/combo_base_model.dart [new file with mode: 0644]
lib/src/model/combobox_model.dart
lib/src/model/db_reference_model.dart
lib/src/model/field_model.dart
lib/src/model/model_base.dart
lib/src/model/model_types.dart
lib/src/model/page_model.dart
lib/src/model/standard/configuration_model.dart
lib/src/model/standard/role_model.dart
lib/src/model/standard/user_model.dart
lib/src/model/table_model.dart
lib/src/page/application_data.dart
lib/src/page/configuration/configuration_change_page.dart
lib/src/page/configuration/configuration_controller.dart
lib/src/page/configuration/configuration_create_page.dart
lib/src/page/configuration/configuration_list_page.dart
lib/src/page/demo_page.dart [new file with mode: 0644]
lib/src/page/role/role_change_page.dart
lib/src/page/role/role_controller.dart
lib/src/page/role/role_list_page.dart
lib/src/page/user/user_change_page.dart
lib/src/page/user/user_controller.dart
lib/src/page/user/user_list_page.dart
lib/src/private/bdrawer.dart
lib/src/widget/callback_controller_bones.dart
lib/src/widget/list_form.dart
lib/src/widget/page_controller_bones.dart
lib/src/widget/view.dart
test/helpers/string_helper_test.dart
test/model/db_model_test.dart
test/model/model_test.dart

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