]> gitweb.hamatoma.de Git - exhibition.git/commitdiff
Benchmark: all data types, creation of records
authorHamatoma <author.hamatoma.de>
Sat, 16 Oct 2021 20:55:15 +0000 (22:55 +0200)
committerHamatoma <author.hamatoma.de>
Wed, 20 Oct 2021 22:30:02 +0000 (00:30 +0200)
* Generator:
** List display: Conversion from DB texts to human readable texts.
** Page list refactoring: fetching backend data with FutureBuilder()
** generated pages without "import not used" warning
** new display type: switchWidget for boolean
** page edit: conditional loadRecord() call

* RestServer:
** SQL statement from pages of type list do not have an ending ';'

* helper: new secondsAsTime()
* validators: new isInt() isNat()
* new: common/benchmark_actions + common/random_data

48 files changed:
.gitignore
lib/base/helper.dart
lib/base/validators.dart
lib/common/benchmark_actions.dart [new file with mode: 0644]
lib/common/random_data.dart [new file with mode: 0644]
lib/meta/benchmarks_meta.dart
lib/meta/module_meta_data.dart
lib/page/benchmarks/create_benchmark_custom.dart
lib/page/benchmarks/create_benchmark_page.dart
lib/page/benchmarks/delete_benchmark_custom.dart
lib/page/benchmarks/delete_benchmark_page.dart
lib/page/benchmarks/edit_benchmark_custom.dart
lib/page/benchmarks/edit_benchmark_page.dart
lib/page/benchmarks/list_benchmark_custom.dart
lib/page/benchmarks/list_benchmark_page.dart
lib/page/roles/create_role_custom.dart
lib/page/roles/create_role_page.dart
lib/page/roles/edit_role_custom.dart
lib/page/roles/edit_role_page.dart
lib/page/roles/list_role_custom.dart
lib/page/roles/list_role_page.dart
lib/page/structures/create_structure_custom.dart
lib/page/structures/create_structure_page.dart
lib/page/structures/delete_structure_custom.dart
lib/page/structures/delete_structure_page.dart
lib/page/structures/edit_structure_custom.dart
lib/page/structures/edit_structure_page.dart
lib/page/structures/list_structure_custom.dart
lib/page/structures/list_structure_page.dart
lib/page/users/create_user_custom.dart
lib/page/users/create_user_page.dart
lib/page/users/delete_user_custom.dart
lib/page/users/delete_user_page.dart
lib/page/users/edit_user_custom.dart
lib/page/users/edit_user_page.dart
lib/page/users/list_user_custom.dart
lib/page/users/list_user_page.dart
lib/persistence/file_persistence.dart
lib/persistence/persistence.dart
lib/persistence/rest_persistence.dart
lib/setting/drawer_exhibition.dart
lib/widget/attended_page.dart
lib/widget/attended_widget.dart
lib/widget/widget_form.dart
metatool/bin/page_generator.dart
metatool/test/helper_test.dart
metatool/test/validators_test.dart [new file with mode: 0644]
rest_server/lib/rest_server.dart

index aa9f9658d2adcd8554226ceda38bf265d22cb31b..118e3904304b381c0e1a6c4d2ae6d7f956faccbf 100644 (file)
@@ -62,3 +62,5 @@ data/i18n/main.lqa
 data/i18n/lokalize-scripts/
 rest_server/tools/rest_server
 /UpdRestSv
+/lib/page/benchmarks/list_benchmark_custom.01.dart
+/lib/page/users/list_user_custom.01.dart
index 8658397628268790081c961e619599d5418a4db9..7ae09bee407d92b16bf13fd907e29015bed9e5ae 100644 (file)
@@ -78,6 +78,29 @@ String asString(dynamic object,
   return rc;
 }
 
+String dbValueToString(dynamic value, DataType dataType) {
+  String rc;
+  if (!(value is String)) {
+    rc = asString(value);
+  } else {
+    switch (dataType) {
+      case DataType.bool:
+        rc = value == 'T' ? i18n.tr('Yes') : i18n.tr('No');
+        break;
+      case DataType.date:
+        rc = sortableDateToStandard(value, dateOnly: true);
+        break;
+      case DataType.datetime:
+        rc = sortableDateToStandard(value);
+        break;
+      default:
+        rc = value;
+        break;
+    }
+  }
+  return rc;
+}
+
 /// Converts a string [value] into a [dataType] specific object.
 ///
 /// If conversion is not possible (wrong input), the [defaultValue] is returned
@@ -153,10 +176,111 @@ dynamic fromString(String value,
 
 /// This function is only used to avoid compiler warning "unused import"
 /// for generated code.
-void helperDummyUsage() {
+void helperDummyUsage([dynamic dummy]) {
   // nothing to do
 }
 
+/// Converts a [value] coming from a json decode operation into the "native" value
+/// given by the [dataType].
+///
+/// If the [value] is null the return value is null if [nullReturnsNull] is true
+/// or [defaultValue] otherwise.
+///
+/// In the case of an error the [defaultValue] is returned.
+dynamic jsonToObject(dynamic value,
+    {required DataType dataType,
+    dynamic defaultValue,
+    bool nullReturnsNull = false}) {
+  var rc;
+  if (value == null) {
+    rc = nullReturnsNull ? null : defaultValue;
+  } else if (value is String) {
+    rc = dataType == DataType.string
+        ? value
+        : fromString(value, dataType: dataType, defaultValue: defaultValue);
+  } else if (value is int) {
+    rc = dataType == DataType.int ||
+            dataType == DataType.nat ||
+            dataType == DataType.reference
+        ? value
+        : (dataType == DataType.float || dataType == DataType.currency
+            ? rc = value as double
+            : defaultValue);
+  } else if (value is double) {
+    rc = dataType == DataType.float || dataType == DataType.currency
+        ? value
+        : defaultValue;
+  } else if (value is DateTime) {
+    rc = dataType == DataType.date || dataType == DataType.datetime
+        ? value
+        : defaultValue;
+  } else if (value is bool) {
+    rc = dataType == DataType.bool ? value : defaultValue;
+  } else {
+    rc = defaultValue;
+  }
+  return rc;
+}
+
+/// Converts a number of seconds or milliseconds into a human readable format.
+///
+/// [value]: the value to convert. See [isMilliSeconds].
+///
+/// [milliSeconds]: true: [value] is a number of milliseconds. false: [value]
+/// is a number of seconds.
+///
+/// Returns a days:hours:minutes:seconds.milliseconds format, e.g. '10:59:59.123'.
+/// Leading zero values are ignored: the result never starts with '0:'.
+String secondsAsTime(int value, {bool isMilliSeconds = false}) {
+  final seconds = isMilliSeconds ? value ~/ 1000 : value;
+  var rc = '';
+  if (seconds > 24 * 3600) {
+    rc = sprintf('%d:%02d:%02d:%02d', [
+      seconds ~/ (24 * 3600),
+      seconds % (24 * 3600) ~/ 3600,
+      seconds % 3600 ~/ 60,
+      seconds % 60
+    ]);
+  } else if (seconds > 3600) {
+    rc = sprintf('%02d:%02d:%02d',
+        [seconds ~/ 3600, seconds % 3600 ~/ 60, seconds % 60]);
+  } else if (seconds > 60) {
+    rc = sprintf('%02d:%02d', [seconds ~/ 60, seconds % 60]);
+  } else {
+    rc = seconds.toString();
+  }
+  if (isMilliSeconds) {
+    rc += sprintf('.%03d', [value % 1000]);
+  }
+  return rc;
+}
+
+/// Converts a sortable [date] or date and time ("2021-12-03") into a standard
+/// format ("03.12.2021").
+///
+/// [dateOnly]: if true only the date part (without time) is in the result.
+///
+/// [withSeconds]: if false the seconds are not part of the result.
+///
+/// [separator]: the separator between the date parts: '.' or '-'.
+String sortableDateToStandard(String date,
+    {bool dateOnly = false, bool withSeconds = false, String separator = '.'}) {
+  // 2021-12-22 10:12
+  // 0123456789 12345
+  var rc = date.substring(8, 10) +
+      separator +
+      date.substring(5, 7) +
+      separator +
+      date.substring(0, 4);
+  if (!dateOnly && date.length > 10) {
+    rc += date.substring(10);
+  }
+  if (!withSeconds && rc.length > 16) {
+    rc = rc.substring(0, 16);
+  }
+  return rc;
+}
+
 /// Converts a [name] to a camel case string.
 ///
 /// A camelCase string starts with a uppercase character.
index 8baf63a6d2bf56b32af45ee5b2def848fac05ace..ad862af0939fa03f59058704a8e8425f82f3df34 100644 (file)
@@ -14,7 +14,7 @@ String? isEmail(String? input) {
   if (input != null) {
     RegExpMatch? match = _regExprEMailChar.firstMatch(input);
     if (match != null) {
-      rc = i18n.trArgs('Illegal character "{0} in email address', '!global',
+      rc = i18n.trArgs('Illegal character "{0}" in email address', '!global',
           [match.group(0)!]);
     } else if (_regExprEMailFormat.firstMatch(input) == null) {
       rc = i18n.tr('Not an email address: ') + input;
@@ -23,6 +23,33 @@ String? isEmail(String? input) {
   return rc;
 }
 
+/// Tests whether the [input] is a not negative integer.
+String? isInt(String? input) {
+  String? rc;
+  if (input != null) {
+    final value = int.tryParse(input);
+    if (value == null) {
+      rc = i18n.trArgs('Not an integer: {0}', '!global', [input]);
+    }
+  }
+  return rc;
+}
+
+/// Tests whether the [input] is a not negative integer.
+String? isNat(String? input) {
+  String? rc;
+  if (input != null) {
+    final value = int.tryParse(input);
+    if (value == null) {
+      rc = i18n.trArgs('Not an integer: {0}', '!global', [input]);
+    } else if (value < 0) {
+      rc = i18n.trArgs(
+          'Not negative integer expected, not: {0}', '!global', [input]);
+    }
+  }
+  return rc;
+}
+
 /// Tests whether the input is not empty.
 String? notEmpty(String? input) {
   final rc = input == null || input.isEmpty ? i18n.tr('Please fill in.') : null;
diff --git a/lib/common/benchmark_actions.dart b/lib/common/benchmark_actions.dart
new file mode 100644 (file)
index 0000000..9703511
--- /dev/null
@@ -0,0 +1,83 @@
+import 'package:dart_bones/dart_bones.dart';
+import '../base/helper.dart';
+import '../common/random_data.dart';
+import '../setting/global_data.dart';
+
+class BenchmarkActions {
+  final GlobalData globalData;
+  var random = KissRandom(MemoryLogger());
+  BenchmarkActions(this.globalData) {
+    random = KissRandom(globalData.logger);
+    random.setSeed(DateTime.now().microsecondsSinceEpoch.toString());
+  }
+  String anyName() {
+    String? rc;
+    switch (random.nextInt(max: 4)) {
+      case 0:
+        rc = RandomData.constructWord([
+          WordPart(WordType.adjectiveOrColor, random.nextInt()),
+          WordPart(WordType.animalOrThing, random.nextInt()),
+        ]);
+        break;
+      case 1:
+        rc = RandomData.constructWord([
+          WordPart(WordType.verb, random.nextInt()),
+          WordPart(WordType.thing, random.nextInt()),
+        ]);
+        break;
+      case 2:
+        rc = RandomData.constructWord([
+          WordPart(WordType.color, random.nextInt()),
+          WordPart(WordType.adjective, random.nextInt()),
+          WordPart(WordType.animalOrThing, random.nextInt()),
+        ]);
+        break;
+      default:
+        rc = RandomData.constructWord([
+          WordPart(WordType.verb, random.nextInt()),
+          WordPart(WordType.color, random.nextInt()),
+          WordPart(WordType.adjective, random.nextInt()),
+          WordPart(WordType.animalOrThing, random.nextInt()),
+        ]);
+        break;
+    }
+    return rc;
+  }
+
+  /// Creates a [count] of entries in the table benchmarks.
+  ///
+  /// Returns an error message (if it starts with '+++') or a notice about
+  /// the duration.
+  Future<String> createMultiple(int count) async {
+    final start = DateTime.now();
+    String? rc;
+    for (var ix = 0; ix < count; ix++) {
+      final lastname = anyName();
+      final threeYearSinceEpoche = random.nextInt(max: 24 * 3600 * 30);
+      final birthday =
+          DateTime.fromMillisecondsSinceEpoch(threeYearSinceEpoche * 1000);
+      final parameters = <String, dynamic>{
+        'module': 'Benchmarks',
+        'sql': 'insert',
+        ':lastName': lastname,
+        ':firstName': RandomData.firstname(random.nextInt()),
+        ':email': '$lastname$ix@hamatoma.de',
+        ':birthday': asString(birthday, dbFormat: true),
+        ':income': random.nextDouble() * 10000.0,
+        ':weight': 50 + random.nextDouble() * 40,
+        ':active': 'T',
+        ':createdBy': 'benchmark'
+      };
+      final answer = await globalData.restPersistence!
+          .store(what: 'store', map: parameters);
+      if (!answer.startsWith('id:')) {
+        rc = '+++ storage failed: $answer';
+        break;
+      }
+    }
+    rc ??= 'duration: ' +
+        secondsAsTime(DateTime.now().difference(start).inMilliseconds,
+            isMilliSeconds: true);
+    return rc;
+  }
+}
diff --git a/lib/common/random_data.dart b/lib/common/random_data.dart
new file mode 100644 (file)
index 0000000..e07a564
--- /dev/null
@@ -0,0 +1,164 @@
+class RandomData {
+  static final verbs = const [
+    'go', 'read', 'eat', 'jump', 'write', 'say', 'talk', 'pray', 'love',
+    'hate', 'join', 'select', 'walk', 'hear', 'type', // 15
+    'cry', 'cook', 'fill', 'bide', 'paint'
+  ];
+  static final adjectives = const [
+    'high', 'low', 'hot', 'cold', 'wide', 'small', 'pretty', 'dirty',
+    'simple', 'rich', 'poor', 'full', 'empty', 'happy', 'angry', // 15
+    'strong', 'wise', 'dirty', 'silly', ''
+  ];
+  static final animals = const [
+    'dog', 'cat', 'mouse', 'cow', 'unicorn', 'lion', 'tiger', 'sheep', 'bull',
+    'bison', 'bug', 'turtle', 'camel', 'spider', 'swan', // 15
+    'bunny', 'deer', 'hare', 'fox', 'pig'
+  ];
+  static final things = const [
+    'door', 'chair', 'wood', 'ink', 'floor', 'bed', 'bag', 'coat',
+    'house', 'palace', 'street', 'way', 'city', 'table', 'wall', // 15
+    'tree', 'gras', 'sky', 'hell', 'garden'
+  ];
+  static final colors = const [
+    'red',
+    'blue',
+    'white',
+    'green',
+    'yellow',
+    'brown',
+    'lila',
+    'purpur',
+    'orange',
+    'pink'
+  ];
+  static final firstnames = const [
+    'Adam',
+    'Alice',
+    'Bill',
+    'Berta',
+    'Charly',
+    'Celine',
+    'Dagobert',
+    'Dora',
+    'Emil',
+    'Eve',
+    'Fred',
+    'Frida',
+    'Guido',
+    'Gerda',
+    'Henry',
+    'Helga',
+    'Isak',
+    'Isabelle',
+    'Jeronimo',
+    'Jutta',
+    'Karl',
+    'Karla',
+    'Luis',
+    'Lea',
+    'Marc',
+    'Mia',
+    'Nick',
+    'Natalie',
+    'Olaf',
+    'Oda',
+    'Peter',
+    'Pia',
+    'Rolf',
+    'Renate',
+    'Siegfried',
+    'Susi',
+    'Tom',
+    'Tara',
+    'Uwe',
+    'Udine',
+    'Wolf',
+    'Walpurga',
+    'Xaver',
+    'Xanthippe',
+    'Yolando',
+    'Yvonne',
+    'Zephyr',
+    'Zara'
+  ];
+  static final adjectivesOrColors = [...adjectives, ...colors];
+  static final animalsOrThings = [...animals, ...things];
+
+  /// Returns a word constructed with different word types.
+  ///
+  /// [wordParts]: defines which word types should be used.
+  ///
+  /// [normalName]: true: the result starts with an uppercase char, all other
+  /// chars are lowercase.
+  ///
+  /// [gerund]: true: a ver is used in the gerund form: this is better to
+  /// to read and remember.
+  static String constructWord(List<WordPart> wordParts,
+      {bool normalName = true, gerund: true}) {
+    String rc = '';
+    for (var item in wordParts) {
+      switch (item.wordType) {
+        case WordType.adjective:
+          rc += adjectives[item.index % adjectives.length];
+          break;
+        case WordType.verb:
+          var next = verbs[item.index % verbs.length];
+          if (gerund) {
+            next = (next.endsWith('e')
+                    ? next.substring(0, next.length - 1)
+                    : next) +
+                'ing';
+          }
+          rc += next;
+          break;
+        case WordType.animal:
+          rc += animals[item.index % animals.length];
+          break;
+        case WordType.thing:
+          rc += things[item.index % things.length];
+          break;
+        case WordType.firstname:
+          rc += firstnames[item.index % firstnames.length];
+          break;
+        case WordType.color:
+          rc += colors[item.index % colors.length];
+          break;
+        case WordType.adjectiveOrColor:
+          rc += adjectivesOrColors[item.index % adjectivesOrColors.length];
+          break;
+        case WordType.animalOrThing:
+          rc += animalsOrThings[item.index % animalsOrThings.length];
+          break;
+      }
+    }
+    if (normalName) {
+      rc = rc[0].toUpperCase() + rc.substring(1).toLowerCase();
+    }
+    return rc;
+  }
+
+  /// Returns a random firstname.
+  ///
+  /// [random]: a random integer number
+  static firstname(int random) {
+    final rc = firstnames[random % firstnames.length];
+    return rc;
+  }
+}
+
+class WordPart {
+  final WordType wordType;
+  final int index;
+  WordPart(this.wordType, this.index);
+}
+
+enum WordType {
+  verb,
+  adjective,
+  animal,
+  thing,
+  color,
+  firstname,
+  adjectiveOrColor,
+  animalOrThing
+}
index 7956efdb6edb8cfed11e41dd03768c824011da1c..39a21e2dfa031e99ca71c251b67c1d639827b20d 100644 (file)
@@ -18,19 +18,18 @@ class BenchmarkMeta extends ModuleMetaData {
               'lastName', i18n.tr('Last Name'), DataType.string, ':notnull:',
               size: 64),
           PropertyMetaData('firstName', i18n.tr('First Name', M),
-              DataType.string, ':unique:notnull:',
+              DataType.string, ':notnull:',
               size: 32),
           PropertyMetaData(
-              'email', i18n.tr('EMail', M), DataType.string, ':unique:notnull:',
+              'email', i18n.tr('EMail', M), DataType.string, ':notnull:',
               size: 255, validators: ['isEmail(input)']),
           PropertyMetaData(
               'birthday', i18n.tr('Birthday'), DataType.date, ':notnull:'),
           PropertyMetaData(
-              'active', i18n.tr('Active'), DataType.bool, ':notnull:'),
-          PropertyMetaData(
-              'weight', i18n.tr('Weight'), DataType.float, ''),
-          PropertyMetaData(
-              'income', i18n.tr('Income'), DataType.currency, ''),
+              'active', i18n.tr('Active'), DataType.bool, ':notnull:',
+              displayType: DisplayType.switchWidget),
+          PropertyMetaData('weight', i18n.tr('Weight'), DataType.float, ''),
+          PropertyMetaData('income', i18n.tr('Income'), DataType.currency, ''),
           PropertyMetaData(
               'created', i18n.tr('Created'), DataType.datetime, ':hidden:'),
           PropertyMetaData(
@@ -64,8 +63,10 @@ class BenchmarkMeta extends ModuleMetaData {
                   'text', i18n.tr('Text'), DataType.string, ':pattern:',
                   size: 64),
             ],
-            tableColumns: 'benchmark_id;benchmark_lastname;benchmark_firstname;benchmark_birthday',
-            tableHeaders: i18n.tr(';Id;Last Name;First Name;Birthday'),
+            tableColumns:
+                'benchmark_id;benchmark_lastname;benchmark_firstname;benchmark_birthday;benchmark_active;benchmark_income',
+            tableHeaders:
+                i18n.tr(';Id;Last Name;First Name;Birthday;Active;Income'),
             whereCondition: '''(:text='' OR benchmark_lastname like :text
   OR benchmark_firstname like :text)''',
             orderBy: 'benchmark_id',
index 4959439f23027423bc1439d019524c110ec4211a..1a657817a7ed799a1d1b4917f7a558587f44522f 100644 (file)
@@ -30,6 +30,7 @@ enum DisplayType {
   checkbox,
   combobox,
   custom,
+  switchWidget,
   text,
 }
 
@@ -54,6 +55,7 @@ class ListPageMetaData extends PageMetaData {
   final String orderBy;
   final String selectItems;
   final String joinItems;
+  final String widgetsBelowFilter;
 
   /// Constructor.
   ///
@@ -84,6 +86,8 @@ class ListPageMetaData extends PageMetaData {
   /// the created joins (t1, t2, ...), use j1, j2 ...
   /// Example: '''JOIN sessions j1 ON j1.user_id=t0.user_id
   /// JOIN logins j2 ON j2.user_id=t0.user_id'''
+  ///
+  /// [widgetsBelowFilter]: additional widget
   ListPageMetaData(String label,
       {String name = '',
       required List<WidgetMetaData> fields,
@@ -93,7 +97,8 @@ class ListPageMetaData extends PageMetaData {
       this.whereCondition = '',
       this.orderBy = '',
       this.selectItems = '',
-      this.joinItems = ''})
+      this.joinItems = '',
+      this.widgetsBelowFilter = ''})
       : super(label, PageType.list,
             name: name, fields: fields, globalComboBoxes: globalComboBoxes);
 }
@@ -290,6 +295,18 @@ class ModuleMetaData {
     return rc;
   }
 
+  /// Returns a property given by the [columnName].
+  PropertyMetaData? propertyByColumnName(String columnName) {
+    PropertyMetaData? rc;
+    for (var field in propertyList) {
+      if (field.columnName == columnName) {
+        rc = field;
+        break;
+      }
+    }
+    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.
@@ -314,6 +331,10 @@ class PageMetaData {
     }
     this.name = name;
   }
+
+  /// Does things when the instance is inititialized.
+  ///
+  /// Must be called after the constructor.
   void onInitialized() {
     var newFields = <WidgetMetaData>[];
     var toDelete = <WidgetMetaData>[];
index b09328e1ef28b7478cb157fa67d1c2da8401c486..62d07c87a10f63d1a97f7867dd3466fb539d5746 100644 (file)
@@ -25,7 +25,6 @@ class CreateBenchmarkCustom extends State<CreateBenchmarkPage> with MessageLine
   final firstNameController = TextEditingController();
   final emailController = TextEditingController();
   final birthdayController = TextEditingController();
-  final activeController = TextEditingController();
   final weightController = TextEditingController();
   final incomeController = TextEditingController();
   CreateBenchmarkCustom();
@@ -35,8 +34,7 @@ class CreateBenchmarkCustom extends State<CreateBenchmarkPage> with MessageLine
     lastNameController.text = _fieldData.lastName;
     firstNameController.text = _fieldData.firstName;
     emailController.text = _fieldData.email;
-    birthdayController.text = asString(_fieldData.birthday);
-    activeController.text = asString(_fieldData.active);
+    birthdayController.text = asString(_fieldData.birthday, dateOnly: true);
     weightController.text = asString(_fieldData.weight);
     incomeController.text = asString(_fieldData.income);
     final formItems = <FormItem>[
@@ -69,29 +67,35 @@ class CreateBenchmarkCustom extends State<CreateBenchmarkPage> with MessageLine
             controller: birthdayController,
             decoration: InputDecoration(labelText: i18n.tr('Birthday')),
             validator: (input) => notEmpty(input),
-            onSaved: (value) => _fieldData.birthday = fromString(value ?? '', dataType: DataType.date)
+            onSaved: (value) => _fieldData.birthday = jsonToObject(value ?? '', dataType: DataType.date)
           ),
           weight: 6),
       FormItem(
-          TextFormField(
-            controller: activeController,
-            decoration: InputDecoration(labelText: i18n.tr('Active')),
-            validator: (input) => notEmpty(input),
-            onSaved: (value) => _fieldData.active = fromString(value ?? '', dataType: DataType.bool)
-          ),
+          Row(children: [
+            Switch(
+              value: _fieldData.active,
+              onChanged: (bool value) {
+                //_fieldData.active = value;
+                setState(() => _fieldData.active = value);
+              },
+            ),
+            Expanded(
+              child: Text(i18n.tr('Active')),
+            ),
+          ]),
           weight: 6),
       FormItem(
           TextFormField(
             controller: weightController,
             decoration: InputDecoration(labelText: i18n.tr('Weight')),
-            onSaved: (value) => _fieldData.weight = fromString(value ?? '', dataType: DataType.float)
+            onSaved: (value) => _fieldData.weight = jsonToObject(value ?? '', dataType: DataType.float)
           ),
           weight: 6),
       FormItem(
           TextFormField(
             controller: incomeController,
             decoration: InputDecoration(labelText: i18n.tr('Income')),
-            onSaved: (value) => _fieldData.income = fromString(value ?? '', dataType: DataType.currency)
+            onSaved: (value) => _fieldData.income = jsonToObject(value ?? '', dataType: DataType.currency)
           ),
           weight: 6),
       FormItem(
@@ -166,11 +170,12 @@ class CreateBenchmarkCustom extends State<CreateBenchmarkPage> with MessageLine
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     lastNameController.dispose();
     firstNameController.dispose();
     emailController.dispose();
     birthdayController.dispose();
-    activeController.dispose();
     weightController.dispose();
     incomeController.dispose();
     super.dispose();
@@ -178,6 +183,7 @@ class CreateBenchmarkCustom extends State<CreateBenchmarkPage> with MessageLine
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String lastName = '';
   String firstName = '';
   String email = '';
@@ -191,10 +197,10 @@ class _FieldData {
     map[':lastName'] = lastName;
     map[':firstName'] = firstName;
     map[':email'] = email;
-    map[':birthday'] = asString(birthday);
-    map[':active'] = asString(active);
-    map[':weight'] = asString(weight);
-    map[':income'] = asString(income);
+    map[':birthday'] = asString(birthday, dbFormat: true, dateOnly: true);
+    map[':active'] = asString(active, dbFormat: true);
+    map[':weight'] = asString(weight, dbFormat: true);
+    map[':income'] = asString(income, dbFormat: true);
     map[':createdBy'] = GlobalData.loginUserName;
   }
 }
index fa02c71fe851608f73eb33a5cd6b5d40bc41bd5c..4fbfd288281aa237785bc3eb8dd839a3cd686c61 100644 (file)
@@ -12,8 +12,13 @@ class CreateBenchmarkPage extends StatefulWidget {
   @override
   _CreateBenchmarkPageState createState() {
     final rc = _CreateBenchmarkPageState();
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), BenchmarkMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        BenchmarkMeta.instance.pageByName('create')!,
+        BenchmarkMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 812cfb8f8ee850d0741ddcb15e74f877f515b7ea..8a8c7677687d0c3c1c6911e776a5caf3aa9f12a5 100644 (file)
@@ -26,23 +26,30 @@ class DeleteBenchmarkCustom extends State<DeleteBenchmarkPage> with MessageLine
   final firstNameController = TextEditingController();
   final emailController = TextEditingController();
   final birthdayController = TextEditingController();
-  final activeController = TextEditingController();
   final weightController = TextEditingController();
   final incomeController = TextEditingController();
   DeleteBenchmarkCustom(this.primaryKey);
   @override
   Widget build(BuildContext context) {
     final padding = GlobalThemeData.padding;
-    attendedPage?.loadRecord(
-        name: 'record',
-        reload: () => setState(() => 1),
-        onDone: (record) => _fieldData.fromMap(record),
-        parameters: {'module': 'Benchmarks', 'sql': 'byId', ':id': primaryKey});
+    if (!_fieldData.isFromBackend) {
+      attendedPage?.loadRecord(
+          name: 'record',
+          reload: () => setState(() => 1),
+          onDone: (record) {
+            _fieldData.fromMap(record);
+            _fieldData.isFromBackend = true;
+          },
+          parameters: {
+            'module': 'Benchmarks',
+            'sql': 'byId',
+            ':id': primaryKey
+          });
+    }
     lastNameController.text = _fieldData.lastName;
     firstNameController.text = _fieldData.firstName;
     emailController.text = _fieldData.email;
-    birthdayController.text = asString(_fieldData.birthday);
-    activeController.text = asString(_fieldData.active);
+    birthdayController.text = asString(_fieldData.birthday, dateOnly: true);
     weightController.text = asString(_fieldData.weight);
     incomeController.text = asString(_fieldData.income);
     final formItems = <FormItem>[
@@ -75,29 +82,35 @@ class DeleteBenchmarkCustom extends State<DeleteBenchmarkPage> with MessageLine
             controller: birthdayController,
             decoration: InputDecoration(labelText: i18n.tr('Birthday')),
             validator: (input) => notEmpty(input),
-            onSaved: (value) => _fieldData.birthday = fromString(value ?? '', dataType: DataType.date)
+            onSaved: (value) => _fieldData.birthday = jsonToObject(value ?? '', dataType: DataType.date)
           ),
           weight: 6),
       FormItem(
-          TextFormField(
-            controller: activeController,
-            decoration: InputDecoration(labelText: i18n.tr('Active')),
-            validator: (input) => notEmpty(input),
-            onSaved: (value) => _fieldData.active = fromString(value ?? '', dataType: DataType.bool)
-          ),
+          Row(children: [
+            Switch(
+              value: _fieldData.active,
+              onChanged: (bool value) {
+                //_fieldData.active = value;
+                setState(() => _fieldData.active = value);
+              },
+            ),
+            Expanded(
+              child: Text(i18n.tr('Active')),
+            ),
+          ]),
           weight: 6),
       FormItem(
           TextFormField(
             controller: weightController,
             decoration: InputDecoration(labelText: i18n.tr('Weight')),
-            onSaved: (value) => _fieldData.weight = fromString(value ?? '', dataType: DataType.float)
+            onSaved: (value) => _fieldData.weight = jsonToObject(value ?? '', dataType: DataType.float)
           ),
           weight: 6),
       FormItem(
           TextFormField(
             controller: incomeController,
             decoration: InputDecoration(labelText: i18n.tr('Income')),
-            onSaved: (value) => _fieldData.income = fromString(value ?? '', dataType: DataType.currency)
+            onSaved: (value) => _fieldData.income = jsonToObject(value ?? '', dataType: DataType.currency)
           ),
           weight: 6),
       FormItem(
@@ -163,11 +176,12 @@ class DeleteBenchmarkCustom extends State<DeleteBenchmarkPage> with MessageLine
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     lastNameController.dispose();
     firstNameController.dispose();
     emailController.dispose();
     birthdayController.dispose();
-    activeController.dispose();
     weightController.dispose();
     incomeController.dispose();
     super.dispose();
@@ -175,6 +189,7 @@ class DeleteBenchmarkCustom extends State<DeleteBenchmarkPage> with MessageLine
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String lastName = '';
   String firstName = '';
   String email = '';
@@ -187,9 +202,9 @@ class _FieldData {
     lastName = map['benchmark_lastname'];
     firstName = map['benchmark_firstname'];
     email = map['benchmark_email'];
-    birthday = map['benchmark_birthday'];
-    active = map['benchmark_active'];
-    weight = map['benchmark_weight'];
-    income = map['benchmark_income'];
+    birthday = jsonToObject(map['benchmark_birthday'], dataType: DataType.date);
+    active = jsonToObject(map['benchmark_active'], dataType: DataType.bool);
+    weight = jsonToObject(map['benchmark_weight'], dataType: DataType.float);
+    income = jsonToObject(map['benchmark_income'], dataType: DataType.currency);
   }
 }
index 0955fbfed880caaff464c2bd9ddd1186705217de..f0fc1002005a4e7c0eccf039122ba896a5e57669 100644 (file)
@@ -13,8 +13,13 @@ class DeleteBenchmarkPage extends StatefulWidget {
   @override
   _DeleteBenchmarkPageState createState() {
     final rc = _DeleteBenchmarkPageState(this.primaryKey);
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), BenchmarkMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        BenchmarkMeta.instance.pageByName('delete')!,
+        BenchmarkMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 0b151a19d4ef79a4e2ea085e3bc0e8f30887c6ee..c933ecfdc304a8e766060cb186df72ebc2f94665 100644 (file)
@@ -26,23 +26,30 @@ class EditBenchmarkCustom extends State<EditBenchmarkPage> with MessageLine {
   final firstNameController = TextEditingController();
   final emailController = TextEditingController();
   final birthdayController = TextEditingController();
-  final activeController = TextEditingController();
   final weightController = TextEditingController();
   final incomeController = TextEditingController();
   EditBenchmarkCustom(this.primaryKey);
   @override
   Widget build(BuildContext context) {
     final padding = GlobalThemeData.padding;
-    attendedPage?.loadRecord(
-        name: 'record',
-        reload: () => setState(() => 1),
-        onDone: (record) => _fieldData.fromMap(record),
-        parameters: {'module': 'Benchmarks', 'sql': 'byId', ':id': primaryKey});
+    if (!_fieldData.isFromBackend) {
+      attendedPage?.loadRecord(
+          name: 'record',
+          reload: () => setState(() => 1),
+          onDone: (record) {
+            _fieldData.fromMap(record);
+            _fieldData.isFromBackend = true;
+          },
+          parameters: {
+            'module': 'Benchmarks',
+            'sql': 'byId',
+            ':id': primaryKey
+          });
+    }
     lastNameController.text = _fieldData.lastName;
     firstNameController.text = _fieldData.firstName;
     emailController.text = _fieldData.email;
-    birthdayController.text = asString(_fieldData.birthday);
-    activeController.text = asString(_fieldData.active);
+    birthdayController.text = asString(_fieldData.birthday, dateOnly: true);
     weightController.text = asString(_fieldData.weight);
     incomeController.text = asString(_fieldData.income);
     final formItems = <FormItem>[
@@ -75,29 +82,35 @@ class EditBenchmarkCustom extends State<EditBenchmarkPage> with MessageLine {
             controller: birthdayController,
             decoration: InputDecoration(labelText: i18n.tr('Birthday')),
             validator: (input) => notEmpty(input),
-            onSaved: (value) => _fieldData.birthday = fromString(value ?? '', dataType: DataType.date)
+            onSaved: (value) => _fieldData.birthday = jsonToObject(value ?? '', dataType: DataType.date)
           ),
           weight: 6),
       FormItem(
-          TextFormField(
-            controller: activeController,
-            decoration: InputDecoration(labelText: i18n.tr('Active')),
-            validator: (input) => notEmpty(input),
-            onSaved: (value) => _fieldData.active = fromString(value ?? '', dataType: DataType.bool)
-          ),
+          Row(children: [
+            Switch(
+              value: _fieldData.active,
+              onChanged: (bool value) {
+                //_fieldData.active = value;
+                setState(() => _fieldData.active = value);
+              },
+            ),
+            Expanded(
+              child: Text(i18n.tr('Active')),
+            ),
+          ]),
           weight: 6),
       FormItem(
           TextFormField(
             controller: weightController,
             decoration: InputDecoration(labelText: i18n.tr('Weight')),
-            onSaved: (value) => _fieldData.weight = fromString(value ?? '', dataType: DataType.float)
+            onSaved: (value) => _fieldData.weight = jsonToObject(value ?? '', dataType: DataType.float)
           ),
           weight: 6),
       FormItem(
           TextFormField(
             controller: incomeController,
             decoration: InputDecoration(labelText: i18n.tr('Income')),
-            onSaved: (value) => _fieldData.income = fromString(value ?? '', dataType: DataType.currency)
+            onSaved: (value) => _fieldData.income = jsonToObject(value ?? '', dataType: DataType.currency)
           ),
           weight: 6),
       FormItem(
@@ -150,7 +163,11 @@ class EditBenchmarkCustom extends State<EditBenchmarkPage> with MessageLine {
     _fieldData.toMap(parameters);
     globalData.restPersistence!
         .store(what: 'store', map: parameters)
-        .then((answer) {});
+        .then((answer) {
+      _fieldData.isFromBackend = false;
+      attendedPage!.pageStates.dbDataState.clear();
+      setState(() => 1);
+    });
   }
 
   void verifyAndStore() {
@@ -162,11 +179,12 @@ class EditBenchmarkCustom extends State<EditBenchmarkPage> with MessageLine {
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     lastNameController.dispose();
     firstNameController.dispose();
     emailController.dispose();
     birthdayController.dispose();
-    activeController.dispose();
     weightController.dispose();
     incomeController.dispose();
     super.dispose();
@@ -174,6 +192,7 @@ class EditBenchmarkCustom extends State<EditBenchmarkPage> with MessageLine {
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String lastName = '';
   String firstName = '';
   String email = '';
@@ -186,10 +205,10 @@ class _FieldData {
     lastName = map['benchmark_lastname'];
     firstName = map['benchmark_firstname'];
     email = map['benchmark_email'];
-    birthday = map['benchmark_birthday'];
-    active = map['benchmark_active'];
-    weight = map['benchmark_weight'];
-    income = map['benchmark_income'];
+    birthday = jsonToObject(map['benchmark_birthday'], dataType: DataType.date);
+    active = jsonToObject(map['benchmark_active'], dataType: DataType.bool);
+    weight = jsonToObject(map['benchmark_weight'], dataType: DataType.float);
+    income = jsonToObject(map['benchmark_income'], dataType: DataType.currency);
   }
 
   void toMap(Map<String, dynamic> map) {
@@ -197,10 +216,10 @@ class _FieldData {
     map[':lastName'] = lastName;
     map[':firstName'] = firstName;
     map[':email'] = email;
-    map[':birthday'] = asString(birthday);
-    map[':active'] = asString(active);
-    map[':weight'] = asString(weight);
-    map[':income'] = asString(income);
+    map[':birthday'] = asString(birthday, dbFormat: true, dateOnly: true);
+    map[':active'] = asString(active, dbFormat: true);
+    map[':weight'] = asString(weight, dbFormat: true);
+    map[':income'] = asString(income, dbFormat: true);
     map[':changedBy'] = GlobalData.loginUserName;
   }
 }
index 32f631ae64138d06e833a482834324fbb4a85015..9fca8712730bdd6bf7407d687bd41e730a37b94e 100644 (file)
@@ -13,8 +13,13 @@ class EditBenchmarkPage extends StatefulWidget {
   @override
   _EditBenchmarkPageState createState() {
     final rc = _EditBenchmarkPageState(this.primaryKey);
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), BenchmarkMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        BenchmarkMeta.instance.pageByName('edit')!,
+        BenchmarkMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index c1701f0a95a6b2afa714dd08286746c9efb6fdf5..5a54c4b6a27e49582e0ae3b458eed970ca3e02db 100644 (file)
@@ -2,26 +2,71 @@
 // It will never overridden by the meta_tool.
 import 'package:flutter/material.dart';
 
+import '../../base/defines.dart';
 import '../../base/helper.dart';
 import '../../base/i18n.dart';
+import '../../base/validators.dart';
 import '../../services/global_widget.dart';
 import '../../setting/global_data.dart';
 import '../../widget/attended_page.dart';
 import '../../widget/widget_form.dart';
+import '../../persistence/persistence.dart';
+import '../../common/benchmark_actions.dart';
 import 'list_benchmark_page.dart';
 
 final i18n = I18N();
 
 class ListBenchmarkCustom extends State<ListBenchmarkPage> {
   final globalData = GlobalData();
+  late Future<DbData> _futureDbData;
   AttendedPage? attendedPage;
   final _fieldData = _FieldData();
   final GlobalKey<FormState> _formKey =
       GlobalKey<FormState>(debugLabel: 'CreateBenchmark');
   final textController = TextEditingController();
+  final countController = TextEditingController();
+  final resultController = TextEditingController();
   ListBenchmarkCustom();
   @override
   Widget build(BuildContext context) {
+    final rc = Scaffold(
+      appBar: globalData.appBarBuilder(i18n.tr('Overview benchmarks')),
+      drawer: globalData.drawerBuilder(context),
+      floatingActionButton: FloatingActionButton(
+        onPressed: () {
+          globalData.navigate(context, '/Benchmarks/create');
+        },
+        child: const Icon(Icons.add),
+      ),
+      body: SafeArea(
+          child: FutureBuilder<DbData>(
+        future: _futureDbData,
+        builder: (context, snapshot) {
+          Widget rc;
+          if (snapshot.connectionState != ConnectionState.done) {
+            rc = const CircularProgressIndicator();
+          } else {
+            if (snapshot.hasData) {
+              final rows = attendedPage!.getRows(dbData: snapshot.data!,
+                  columnList: 'benchmark_id;benchmark_lastname;benchmark_firstname;benchmark_birthday;benchmark_active;benchmark_income',
+                  onDone: () => setState(() => 1),
+                  routeEdit: '/Benchmarks/edit',
+                  context: context);
+              rc = buildFrame(rows: rows);
+            } else if (snapshot.hasError) {
+              rc = Text('Backend problem: ${snapshot.error}');
+            } else {
+              rc = const CircularProgressIndicator();
+            }
+          }
+          return rc;
+        },
+      )),
+    );
+    return rc;
+  }
+
+  Widget buildFrame({required JsonList rows}){
     final padding = GlobalThemeData.padding;
     final formItems = <FormItem>[
       FormItem(
@@ -37,32 +82,57 @@ class ListBenchmarkCustom extends State<ListBenchmarkPage> {
           weight: 12,
           gapAbove: padding),
     ];
+    final belowForm = Padding(
+        padding: EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+        child: Column(children: [
+          SizedBox(
+            height: padding,
+          ),
+          Row(children: [
+            Expanded(
+                child: TextFormField(
+              controller: countController,
+              decoration: InputDecoration(labelText: i18n.tr('Count')),
+              onSaved: (value) => _fieldData.count = value ?? '',
+              validator: (input) => isNat(input),
+            )),
+            SizedBox(width: padding),
+            Expanded(
+                child: ElevatedButton(
+                    onPressed: () => createNew(),
+                    child: Text(i18n.tr('Create new elements')))),
+            SizedBox(width: padding),
+            Expanded(
+                child: ElevatedButton(
+                    onPressed: () => readItems(),
+                    child: Text(i18n.tr('Read existing elements')))),
+          ]),
+          SizedBox(height: padding),
+          Row(children: [
+            Expanded(
+                child: TextFormField(
+              controller: resultController,
+              decoration: InputDecoration(labelText: i18n.tr('Message')),
+              readOnly: true,
+            ))
+          ])
+        ]));
     final form = Form(
         key: _formKey,
-        child: Card(
-            color: GlobalThemeData.formBackgroundColor,
-            elevation: GlobalThemeData.formElevation,
-            margin:
-                EdgeInsets.symmetric(vertical: padding, horizontal: padding),
-            child: Padding(
-                padding: EdgeInsets.symmetric(
-                    vertical: padding, horizontal: padding),
-                child: WidgetForm.flexibleGrid(formItems,
-                    screenWidth: attendedPage!.pageStates.screenWidth,
-                    padding: padding))));
-    final rows = attendedPage!.getRows(
-        columnList: 'benchmark_id;benchmark_lastname;benchmark_firstname;benchmark_birthday',
-        what: 'query',
-        parameters: {
-          'module': 'Benchmarks',
-          'sql': 'list',
-          'offset': '0',
-          'size': '10',
-          ':text': asPattern(_fieldData.text),
-        },
-        onDone: () => setState(() => 1),
-        routeEdit: '/Benchmarks/edit',
-        context: context);
+        child: Column(children: [
+          Card(
+              color: GlobalThemeData.formBackgroundColor,
+              elevation: GlobalThemeData.formElevation,
+              margin:
+                  EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+              child: Padding(
+                  padding: EdgeInsets.symmetric(
+                      vertical: padding, horizontal: padding),
+                  child: WidgetForm.flexibleGrid(formItems,
+                      screenWidth: attendedPage!.pageStates.screenWidth,
+                      padding: padding))),
+          belowForm,
+        ]));
     final table = DataTable(
       columns: <DataColumn>[
         DataColumn(
@@ -77,33 +147,52 @@ class ListBenchmarkCustom extends State<ListBenchmarkPage> {
         DataColumn(
           label: Text(i18n.tr('Birthday')),
         ),
+        DataColumn(
+          label: Text(i18n.tr('Active')),
+        ),
+        DataColumn(
+          label: Text(i18n.tr('Income')),
+        ),
       ],
-      rows: rows,
+      rows: rows as List<DataRow>,
     );
-    final frameWidget = Column(children: [
+    Widget? tabBar =
+    attendedPage!.buildChipBar(onTap: (offset) => setState(() => offset));
+    final frameWidget = ListView(children: [
       form,
+      if (tabBar != null) tabBar,
       SizedBox(height: padding),
       SizedBox(width: double.infinity, child: table),
     ]);
-    final rc = Scaffold(
-        appBar: globalData.appBarBuilder(i18n.tr('Overview benchmarks')),
-        drawer: globalData.drawerBuilder(context),
-        floatingActionButton: FloatingActionButton(
-          onPressed: () {
-            globalData.navigate(context, '/Benchmarks/create');
-          },
-          child: const Icon(Icons.add),
-          //backgroundColor: Colors.green,
-        ),
-        body: SafeArea(child: frameWidget));
-    return rc;
+    return frameWidget;
   }
 
   @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    _futureDbData = globalData.restPersistence!.query(what: 'query', data: {
+      'module': 'Benchmarks',
+      'sql': 'list',
+      'offset': _fieldData.theOffset,
+      'size': _fieldData.thePageSize,
+      ':text': asPattern(_fieldData.text),
+    });
+  }
+
+  void createNew() {
+    final benchMark = BenchmarkActions(globalData);
+    benchMark
+        .createMultiple(int.tryParse(countController.text) ?? 1)
+        .then((message) => setState(() => resultController.text = message));
+  }
+
+@override
   void dispose() {
-    helperDummyUsage();
+    helperDummyUsage(DataType.string);
     globalWidgetDummyUsage();
     textController.dispose();
+    countController.dispose();
+    resultController.dispose();
     super.dispose();
   }
 
@@ -112,6 +201,13 @@ class ListBenchmarkCustom extends State<ListBenchmarkPage> {
     super.initState();
   }
 
+  void readItems() {
+    final benchMark = BenchmarkActions(globalData);
+    // resultController.text = benchMark.createMultiple(
+    //    int.tryParse(_fieldData.count) ?? 1);
+    setState(() => 1);
+  }
+
   void search() {
     attendedPage!.pageStates.dbDataState.clear();
     if (_formKey.currentState!.validate()) {
@@ -122,5 +218,8 @@ class ListBenchmarkCustom extends State<ListBenchmarkPage> {
 }
 
 class _FieldData {
+  int thePageSize = 10;
+  int theOffset = 0;
   String text = '';
+  String count = '';
 }
index d65dcc560fc494016ce1b88fd73ae9761960c7aa..7ab8115dee7e5f65be8a057be3929434a54e16d2 100644 (file)
@@ -12,8 +12,13 @@ class ListBenchmarkPage extends StatefulWidget {
   @override
   _ListBenchmarkPageState createState() {
     final rc = _ListBenchmarkPageState();
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), BenchmarkMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        BenchmarkMeta.instance.pageByName('list')!,
+        BenchmarkMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 3a38b4182d19ee8db5f293f8facc0247e62ed3a5..87c2b0ec19e3f7f4954a00b10f0043b25803ca71 100644 (file)
@@ -108,12 +108,15 @@ class CreateRoleCustom extends State<CreateRolePage> with MessageLine {
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     nameController.dispose();
     super.dispose();
   }
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String name = '';
 
 
index 4ec6c88d247f4f8f2e59f42a4159ecb4d1dbb6e0..927f06196ee2c30004c7f774a330566538ebddd9 100644 (file)
@@ -12,8 +12,13 @@ class CreateRolePage extends StatefulWidget {
   @override
   _CreateRolePageState createState() {
     final rc = _CreateRolePageState();
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), RoleMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        RoleMeta.instance.pageByName('create')!,
+        RoleMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index fe67f51cc253ba7a07c74074262fecd67ceeefb6..4108b6a2ada46bf07d0ac28ae04c4906cc09ad52 100644 (file)
@@ -27,11 +27,20 @@ class EditRoleCustom extends State<EditRolePage> with MessageLine {
   @override
   Widget build(BuildContext context) {
     final padding = GlobalThemeData.padding;
-    attendedPage?.loadRecord(
-        name: 'record',
-        reload: () => setState(() => 1),
-        onDone: (record) => _fieldData.fromMap(record),
-        parameters: {'module': 'Roles', 'sql': 'byId', ':id': primaryKey});
+    if (!_fieldData.isFromBackend) {
+      attendedPage?.loadRecord(
+          name: 'record',
+          reload: () => setState(() => 1),
+          onDone: (record) {
+            _fieldData.fromMap(record);
+            _fieldData.isFromBackend = true;
+          },
+          parameters: {
+            'module': 'Roles',
+            'sql': 'byId',
+            ':id': primaryKey
+          });
+    }
     nameController.text = _fieldData.name;
     final formItems = <FormItem>[
       FormItem(
@@ -92,7 +101,11 @@ class EditRoleCustom extends State<EditRolePage> with MessageLine {
     _fieldData.toMap(parameters);
     globalData.restPersistence!
         .store(what: 'store', map: parameters)
-        .then((answer) {});
+        .then((answer) {
+      _fieldData.isFromBackend = false;
+      attendedPage!.pageStates.dbDataState.clear();
+      setState(() => 1);
+    });
   }
 
   void verifyAndStore() {
@@ -104,12 +117,15 @@ class EditRoleCustom extends State<EditRolePage> with MessageLine {
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     nameController.dispose();
     super.dispose();
   }
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String name = '';
 
   void fromMap(Map<String, dynamic> map) {
index ef15ac45d8661adb74e1558911f0d05741b1b6b9..b96e2749efb2713949c020cbdfcb26e2e82f287b 100644 (file)
@@ -13,8 +13,13 @@ class EditRolePage extends StatefulWidget {
   @override
   _EditRolePageState createState() {
     final rc = _EditRolePageState(this.primaryKey);
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), RoleMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        RoleMeta.instance.pageByName('edit')!,
+        RoleMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 32e6ed81b15bfccbea06682ef5746a09ffc7c222..69af556923cba672e57fcc402ed108662307cb95 100644 (file)
@@ -2,18 +2,21 @@
 // It will never overridden by the meta_tool.
 import 'package:flutter/material.dart';
 
+import '../../base/defines.dart';
 import '../../base/helper.dart';
 import '../../base/i18n.dart';
 import '../../services/global_widget.dart';
 import '../../setting/global_data.dart';
 import '../../widget/attended_page.dart';
 import '../../widget/widget_form.dart';
+import '../../persistence/persistence.dart';
 import 'list_role_page.dart';
 
 final i18n = I18N();
 
 class ListRoleCustom extends State<ListRolePage> {
   final globalData = GlobalData();
+  late Future<DbData> _futureDbData;
   AttendedPage? attendedPage;
   final _fieldData = _FieldData();
   final GlobalKey<FormState> _formKey =
@@ -22,6 +25,44 @@ class ListRoleCustom extends State<ListRolePage> {
   ListRoleCustom();
   @override
   Widget build(BuildContext context) {
+    final rc = Scaffold(
+      appBar: globalData.appBarBuilder(i18n.tr('Overview roles')),
+      drawer: globalData.drawerBuilder(context),
+      floatingActionButton: FloatingActionButton(
+        onPressed: () {
+          globalData.navigate(context, '/Roles/create');
+        },
+        child: const Icon(Icons.add),
+      ),
+      body: SafeArea(
+          child: FutureBuilder<DbData>(
+        future: _futureDbData,
+        builder: (context, snapshot) {
+          Widget rc;
+          if (snapshot.connectionState != ConnectionState.done) {
+            rc = const CircularProgressIndicator();
+          } else {
+            if (snapshot.hasData) {
+              final rows = attendedPage!.getRows(dbData: snapshot.data!,
+                  columnList: 'role_id;role_name',
+                  onDone: () => setState(() => 1),
+                  routeEdit: '/Roles/edit',
+                  context: context);
+              rc = buildFrame(rows: rows);
+            } else if (snapshot.hasError) {
+              rc = Text('Backend problem: ${snapshot.error}');
+            } else {
+              rc = const CircularProgressIndicator();
+            }
+          }
+          return rc;
+        },
+      )),
+    );
+    return rc;
+  }
+
+  Widget buildFrame({required JsonList rows}){
     final padding = GlobalThemeData.padding;
     final formItems = <FormItem>[
       FormItem(
@@ -43,56 +84,47 @@ class ListRoleCustom extends State<ListRolePage> {
             color: GlobalThemeData.formBackgroundColor,
             elevation: GlobalThemeData.formElevation,
             margin:
-                EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+            EdgeInsets.symmetric(vertical: padding, horizontal: padding),
             child: Padding(
                 padding: EdgeInsets.symmetric(
                     vertical: padding, horizontal: padding),
                 child: WidgetForm.flexibleGrid(formItems,
                     screenWidth: attendedPage!.pageStates.screenWidth,
                     padding: padding))));
-    final rows = attendedPage!.getRows(
-        columnList: 'role_id;role_name',
-        what: 'query',
-        parameters: {
-          'module': 'Roles',
-          'sql': 'list',
-          'offset': '0',
-          'size': '10',
-          ':text': _fieldData.text,
-        },
-        onDone: () => setState(() => 1),
-        routeEdit: '/Roles/edit',
-        context: context);
     final table = DataTable(
       columns: <DataColumn>[
         DataColumn(
           label: Text(i18n.tr('d;Name')),
         ),
       ],
-      rows: rows,
+      rows: rows as List<DataRow>,
     );
-    final frameWidget = Column(children: [
+    Widget? tabBar =
+    attendedPage!.buildChipBar(onTap: (offset) => setState(() => offset));
+    final frameWidget = ListView(children: [
       form,
+      if (tabBar != null) tabBar,
       SizedBox(height: padding),
       SizedBox(width: double.infinity, child: table),
     ]);
-    final rc = Scaffold(
-        appBar: globalData.appBarBuilder(i18n.tr('Overview roles')),
-        drawer: globalData.drawerBuilder(context),
-        floatingActionButton: FloatingActionButton(
-          onPressed: () {
-            globalData.navigate(context, '/Roles/create');
-          },
-          child: const Icon(Icons.add),
-          //backgroundColor: Colors.green,
-        ),
-        body: SafeArea(child: frameWidget));
-    return rc;
+    return frameWidget;
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    _futureDbData = globalData.restPersistence!.query(what: 'query', data: {
+      'module': 'Roles',
+      'sql': 'list',
+      'offset': _fieldData.theOffset,
+      'size': _fieldData.thePageSize,
+      ':text': _fieldData.text,
+    });
   }
 
   @override
   void dispose() {
-    helperDummyUsage();
+    helperDummyUsage(DataType.string);
     globalWidgetDummyUsage();
     textController.dispose();
     super.dispose();
@@ -113,5 +145,7 @@ class ListRoleCustom extends State<ListRolePage> {
 }
 
 class _FieldData {
+  int thePageSize = 10;
+  int theOffset = 0;
   String text = '';
 }
index b42f052403af7c5e5350e744c837b61cbd7126b8..9a0b0a6be7d5a855a7418cd57086dfafeabe3928 100644 (file)
@@ -12,8 +12,13 @@ class ListRolePage extends StatefulWidget {
   @override
   _ListRolePageState createState() {
     final rc = _ListRolePageState();
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), RoleMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        RoleMeta.instance.pageByName('list')!,
+        RoleMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index c86d7afb1d875c6011a5d5d428bd9d98f69e25a9..f05c1ee24a7f6a8b760123a5248761e1e8655c80 100644 (file)
@@ -62,7 +62,7 @@ class CreateStructureCustom extends State<CreateStructurePage> with MessageLine
           TextFormField(
             controller: positionController,
             decoration: InputDecoration(labelText: i18n.tr('Position')),
-            onSaved: (value) => _fieldData.position = fromString(value ?? '', dataType: DataType.int)
+            onSaved: (value) => _fieldData.position = jsonToObject(value ?? '', dataType: DataType.int)
           ),
           weight: 6),
       FormItem(
@@ -137,6 +137,8 @@ class CreateStructureCustom extends State<CreateStructurePage> with MessageLine
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     scopeController.dispose();
     nameController.dispose();
     valueController.dispose();
@@ -146,6 +148,7 @@ class CreateStructureCustom extends State<CreateStructurePage> with MessageLine
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String scope = '';
   String name = '';
   String value = '';
@@ -156,7 +159,7 @@ class _FieldData {
     map[':scope'] = scope;
     map[':name'] = name;
     map[':value'] = value;
-    map[':position'] = asString(position);
+    map[':position'] = asString(position, dbFormat: true);
     map[':createdBy'] = GlobalData.loginUserName;
   }
 }
index 88aa0fbe5e25be84fab24c4989cfcb8ab4c49d92..9be367aee76937ed8e2077d9ae6ef516bd274e3f 100644 (file)
@@ -12,8 +12,13 @@ class CreateStructurePage extends StatefulWidget {
   @override
   _CreateStructurePageState createState() {
     final rc = _CreateStructurePageState();
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), StructureMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        StructureMeta.instance.pageByName('create')!,
+        StructureMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index fc775c3d52ace5f134d8cb6d22a3367e37704fd9..baad392504e32b9409bc94a48eadc5fbf46b8583 100644 (file)
@@ -30,11 +30,20 @@ class DeleteStructureCustom extends State<DeleteStructurePage> with MessageLine
   @override
   Widget build(BuildContext context) {
     final padding = GlobalThemeData.padding;
-    attendedPage?.loadRecord(
-        name: 'record',
-        reload: () => setState(() => 1),
-        onDone: (record) => _fieldData.fromMap(record),
-        parameters: {'module': 'Structures', 'sql': 'byId', ':id': primaryKey});
+    if (!_fieldData.isFromBackend) {
+      attendedPage?.loadRecord(
+          name: 'record',
+          reload: () => setState(() => 1),
+          onDone: (record) {
+            _fieldData.fromMap(record);
+            _fieldData.isFromBackend = true;
+          },
+          parameters: {
+            'module': 'Structures',
+            'sql': 'byId',
+            ':id': primaryKey
+          });
+    }
     scopeController.text = _fieldData.scope;
     nameController.text = _fieldData.name;
     valueController.text = _fieldData.value;
@@ -68,7 +77,7 @@ class DeleteStructureCustom extends State<DeleteStructurePage> with MessageLine
           TextFormField(
             controller: positionController,
             decoration: InputDecoration(labelText: i18n.tr('Position')),
-            onSaved: (value) => _fieldData.position = fromString(value ?? '', dataType: DataType.int)
+            onSaved: (value) => _fieldData.position = jsonToObject(value ?? '', dataType: DataType.int)
           ),
           weight: 6),
       FormItem(
@@ -134,6 +143,8 @@ class DeleteStructureCustom extends State<DeleteStructurePage> with MessageLine
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     scopeController.dispose();
     nameController.dispose();
     valueController.dispose();
@@ -143,6 +154,7 @@ class DeleteStructureCustom extends State<DeleteStructurePage> with MessageLine
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String scope = '';
   String name = '';
   String value = '';
@@ -152,6 +164,6 @@ class _FieldData {
     scope = map['structure_scope'];
     name = map['structure_name'];
     value = map['structure_value'];
-    position = map['structure_position'];
+    position = jsonToObject(map['structure_position'], dataType: DataType.int);
   }
 }
index 2bdedf31d7acb9ce08674906ee7eb17e63100090..1d89c7abfbbfd9de83e4496fcbe3bc7bc4d9b366 100644 (file)
@@ -13,8 +13,13 @@ class DeleteStructurePage extends StatefulWidget {
   @override
   _DeleteStructurePageState createState() {
     final rc = _DeleteStructurePageState(this.primaryKey);
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), StructureMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        StructureMeta.instance.pageByName('delete')!,
+        StructureMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 58fe2a8fabd1eddd51320b7322443b9df0801166..11bd3d0bf91f96411a62553096634f7b840a79a9 100644 (file)
@@ -30,11 +30,20 @@ class EditStructureCustom extends State<EditStructurePage> with MessageLine {
   @override
   Widget build(BuildContext context) {
     final padding = GlobalThemeData.padding;
-    attendedPage?.loadRecord(
-        name: 'record',
-        reload: () => setState(() => 1),
-        onDone: (record) => _fieldData.fromMap(record),
-        parameters: {'module': 'Structures', 'sql': 'byId', ':id': primaryKey});
+    if (!_fieldData.isFromBackend) {
+      attendedPage?.loadRecord(
+          name: 'record',
+          reload: () => setState(() => 1),
+          onDone: (record) {
+            _fieldData.fromMap(record);
+            _fieldData.isFromBackend = true;
+          },
+          parameters: {
+            'module': 'Structures',
+            'sql': 'byId',
+            ':id': primaryKey
+          });
+    }
     scopeController.text = _fieldData.scope;
     nameController.text = _fieldData.name;
     valueController.text = _fieldData.value;
@@ -68,7 +77,7 @@ class EditStructureCustom extends State<EditStructurePage> with MessageLine {
           TextFormField(
             controller: positionController,
             decoration: InputDecoration(labelText: i18n.tr('Position')),
-            onSaved: (value) => _fieldData.position = fromString(value ?? '', dataType: DataType.int)
+            onSaved: (value) => _fieldData.position = jsonToObject(value ?? '', dataType: DataType.int)
           ),
           weight: 6),
       FormItem(
@@ -121,7 +130,11 @@ class EditStructureCustom extends State<EditStructurePage> with MessageLine {
     _fieldData.toMap(parameters);
     globalData.restPersistence!
         .store(what: 'store', map: parameters)
-        .then((answer) {});
+        .then((answer) {
+      _fieldData.isFromBackend = false;
+      attendedPage!.pageStates.dbDataState.clear();
+      setState(() => 1);
+    });
   }
 
   void verifyAndStore() {
@@ -133,6 +146,8 @@ class EditStructureCustom extends State<EditStructurePage> with MessageLine {
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     scopeController.dispose();
     nameController.dispose();
     valueController.dispose();
@@ -142,6 +157,7 @@ class EditStructureCustom extends State<EditStructurePage> with MessageLine {
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String scope = '';
   String name = '';
   String value = '';
@@ -151,7 +167,7 @@ class _FieldData {
     scope = map['structure_scope'];
     name = map['structure_name'];
     value = map['structure_value'];
-    position = map['structure_position'];
+    position = jsonToObject(map['structure_position'], dataType: DataType.int);
   }
 
   void toMap(Map<String, dynamic> map) {
@@ -159,7 +175,7 @@ class _FieldData {
     map[':scope'] = scope;
     map[':name'] = name;
     map[':value'] = value;
-    map[':position'] = asString(position);
+    map[':position'] = asString(position, dbFormat: true);
     map[':changedBy'] = GlobalData.loginUserName;
   }
 }
index c151f661d5bdbd2569f26da42d8f1c67298cc1af..516fdff680d177e919e9e7d0f19d1f92a5159ceb 100644 (file)
@@ -13,8 +13,13 @@ class EditStructurePage extends StatefulWidget {
   @override
   _EditStructurePageState createState() {
     final rc = _EditStructurePageState(this.primaryKey);
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), StructureMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        StructureMeta.instance.pageByName('edit')!,
+        StructureMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 28c0a643473a502681c6e197402302003b15f325..1380aad64bcfae4e03ec2ba3a53388e7c59060d9 100644 (file)
@@ -2,18 +2,21 @@
 // It will never overridden by the meta_tool.
 import 'package:flutter/material.dart';
 
+import '../../base/defines.dart';
 import '../../base/helper.dart';
 import '../../base/i18n.dart';
 import '../../services/global_widget.dart';
 import '../../setting/global_data.dart';
 import '../../widget/attended_page.dart';
 import '../../widget/widget_form.dart';
+import '../../persistence/persistence.dart';
 import 'list_structure_page.dart';
 
 final i18n = I18N();
 
 class ListStructureCustom extends State<ListStructurePage> {
   final globalData = GlobalData();
+  late Future<DbData> _futureDbData;
   AttendedPage? attendedPage;
   final _fieldData = _FieldData();
   final GlobalKey<FormState> _formKey =
@@ -22,6 +25,44 @@ class ListStructureCustom extends State<ListStructurePage> {
   ListStructureCustom();
   @override
   Widget build(BuildContext context) {
+    final rc = Scaffold(
+      appBar: globalData.appBarBuilder(i18n.tr('Overview structures')),
+      drawer: globalData.drawerBuilder(context),
+      floatingActionButton: FloatingActionButton(
+        onPressed: () {
+          globalData.navigate(context, '/Structures/create');
+        },
+        child: const Icon(Icons.add),
+      ),
+      body: SafeArea(
+          child: FutureBuilder<DbData>(
+        future: _futureDbData,
+        builder: (context, snapshot) {
+          Widget rc;
+          if (snapshot.connectionState != ConnectionState.done) {
+            rc = const CircularProgressIndicator();
+          } else {
+            if (snapshot.hasData) {
+              final rows = attendedPage!.getRows(dbData: snapshot.data!,
+                  columnList: 'structure_id;structure_scope;structure_name;structure_value;structure_position',
+                  onDone: () => setState(() => 1),
+                  routeEdit: '/Structures/edit',
+                  context: context);
+              rc = buildFrame(rows: rows);
+            } else if (snapshot.hasError) {
+              rc = Text('Backend problem: ${snapshot.error}');
+            } else {
+              rc = const CircularProgressIndicator();
+            }
+          }
+          return rc;
+        },
+      )),
+    );
+    return rc;
+  }
+
+  Widget buildFrame({required JsonList rows}){
     final padding = GlobalThemeData.padding;
     final formItems = <FormItem>[
       FormItem(
@@ -43,56 +84,47 @@ class ListStructureCustom extends State<ListStructurePage> {
             color: GlobalThemeData.formBackgroundColor,
             elevation: GlobalThemeData.formElevation,
             margin:
-                EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+            EdgeInsets.symmetric(vertical: padding, horizontal: padding),
             child: Padding(
                 padding: EdgeInsets.symmetric(
                     vertical: padding, horizontal: padding),
                 child: WidgetForm.flexibleGrid(formItems,
                     screenWidth: attendedPage!.pageStates.screenWidth,
                     padding: padding))));
-    final rows = attendedPage!.getRows(
-        columnList: 'structure_id;structure_scope;structure_name;structure_value;structure_position',
-        what: 'query',
-        parameters: {
-          'module': 'Structures',
-          'sql': 'list',
-          'offset': '0',
-          'size': '10',
-          ':text': _fieldData.text,
-        },
-        onDone: () => setState(() => 1),
-        routeEdit: '/Structures/edit',
-        context: context);
     final table = DataTable(
       columns: <DataColumn>[
         DataColumn(
           label: Text(i18n.tr('d;Scope;Name;Value;Position')),
         ),
       ],
-      rows: rows,
+      rows: rows as List<DataRow>,
     );
-    final frameWidget = Column(children: [
+    Widget? tabBar =
+    attendedPage!.buildChipBar(onTap: (offset) => setState(() => offset));
+    final frameWidget = ListView(children: [
       form,
+      if (tabBar != null) tabBar,
       SizedBox(height: padding),
       SizedBox(width: double.infinity, child: table),
     ]);
-    final rc = Scaffold(
-        appBar: globalData.appBarBuilder(i18n.tr('Overview structures')),
-        drawer: globalData.drawerBuilder(context),
-        floatingActionButton: FloatingActionButton(
-          onPressed: () {
-            globalData.navigate(context, '/Structures/create');
-          },
-          child: const Icon(Icons.add),
-          //backgroundColor: Colors.green,
-        ),
-        body: SafeArea(child: frameWidget));
-    return rc;
+    return frameWidget;
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    _futureDbData = globalData.restPersistence!.query(what: 'query', data: {
+      'module': 'Structures',
+      'sql': 'list',
+      'offset': _fieldData.theOffset,
+      'size': _fieldData.thePageSize,
+      ':text': _fieldData.text,
+    });
   }
 
   @override
   void dispose() {
-    helperDummyUsage();
+    helperDummyUsage(DataType.string);
     globalWidgetDummyUsage();
     textController.dispose();
     super.dispose();
@@ -113,5 +145,7 @@ class ListStructureCustom extends State<ListStructurePage> {
 }
 
 class _FieldData {
+  int thePageSize = 10;
+  int theOffset = 0;
   String text = '';
 }
index e286fbc4ae0f50a1a783ec73762412bb8bc7e659..ad80fff8e3d8b4e924c0ff6343f3292eac1e4525 100644 (file)
@@ -12,8 +12,13 @@ class ListStructurePage extends StatefulWidget {
   @override
   _ListStructurePageState createState() {
     final rc = _ListStructurePageState();
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), StructureMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        StructureMeta.instance.pageByName('list')!,
+        StructureMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 615970d4b70e1a9fd0d0ddaf5379ed3f3811e9fe..d01ac1525a6efb4808b83dc54fdcb2cd730ab617 100644 (file)
@@ -141,6 +141,8 @@ class CreateUserCustom extends State<CreateUserPage> with MessageLine {
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     nameController.dispose();
     displayNameController.dispose();
     emailController.dispose();
@@ -149,6 +151,7 @@ class CreateUserCustom extends State<CreateUserPage> with MessageLine {
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String name = '';
   String displayName = '';
   String email = '';
@@ -159,7 +162,7 @@ class _FieldData {
     map[':name'] = name;
     map[':displayName'] = displayName;
     map[':email'] = email;
-    map[':role'] = asString(role);
+    map[':role'] = asString(role, dbFormat: true);
     map[':createdBy'] = GlobalData.loginUserName;
   }
 }
index 14e69e258185a24630f5a76f39e934f03133ccc6..5fed6f1b3039a93ec91bd6d820a3f4747dbe78b5 100644 (file)
@@ -12,8 +12,13 @@ class CreateUserPage extends StatefulWidget {
   @override
   _CreateUserPageState createState() {
     final rc = _CreateUserPageState();
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), UserMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        UserMeta.instance.pageByName('create')!,
+        UserMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 64f531d726c149031218783da0f460584cf217ab..a1df6ea79685151cc247d133fdca9be1a47c40f7 100644 (file)
@@ -33,11 +33,20 @@ class DeleteUserCustom extends State<DeleteUserPage> with MessageLine {
         attendedPage: attendedPage!, onDone: () => setState(() => 1));
     final itemsRoles = comboRoles(
         i18n.trDyn(GlobalTranslations.comboboxSelect), attendedPage!);
-    attendedPage?.loadRecord(
-        name: 'record',
-        reload: () => setState(() => 1),
-        onDone: (record) => _fieldData.fromMap(record),
-        parameters: {'module': 'Users', 'sql': 'byId', ':id': primaryKey});
+    if (!_fieldData.isFromBackend) {
+      attendedPage?.loadRecord(
+          name: 'record',
+          reload: () => setState(() => 1),
+          onDone: (record) {
+            _fieldData.fromMap(record);
+            _fieldData.isFromBackend = true;
+          },
+          parameters: {
+            'module': 'Users',
+            'sql': 'byId',
+            ':id': primaryKey
+          });
+    }
     nameController.text = _fieldData.name;
     displayNameController.text = _fieldData.displayName;
     emailController.text = _fieldData.email;
@@ -138,6 +147,8 @@ class DeleteUserCustom extends State<DeleteUserPage> with MessageLine {
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     nameController.dispose();
     displayNameController.dispose();
     emailController.dispose();
@@ -146,6 +157,7 @@ class DeleteUserCustom extends State<DeleteUserPage> with MessageLine {
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String name = '';
   String displayName = '';
   String email = '';
@@ -155,6 +167,6 @@ class _FieldData {
     name = map['user_name'];
     displayName = map['user_displayname'];
     email = map['user_email'];
-    role = map['user_role'];
+    role = jsonToObject(map['user_role'], dataType: DataType.reference);
   }
 }
index 85cbfc907ceebe2c637ac1bc32c7ee0433342d34..111ddc4dc9a0f07134592bde48706f536521bf09 100644 (file)
@@ -13,8 +13,13 @@ class DeleteUserPage extends StatefulWidget {
   @override
   _DeleteUserPageState createState() {
     final rc = _DeleteUserPageState(this.primaryKey);
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), UserMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        UserMeta.instance.pageByName('delete')!,
+        UserMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 70a9f5078d8a361f54c03846b0480fe86f846c90..374bc2ad2def88a580c5379df73b0f0080381f98 100644 (file)
@@ -33,11 +33,20 @@ class EditUserCustom extends State<EditUserPage> with MessageLine {
         attendedPage: attendedPage!, onDone: () => setState(() => 1));
     final itemsRoles = comboRoles(
         i18n.trDyn(GlobalTranslations.comboboxSelect), attendedPage!);
-    attendedPage?.loadRecord(
-        name: 'record',
-        reload: () => setState(() => 1),
-        onDone: (record) => _fieldData.fromMap(record),
-        parameters: {'module': 'Users', 'sql': 'byId', ':id': primaryKey});
+    if (!_fieldData.isFromBackend) {
+      attendedPage?.loadRecord(
+          name: 'record',
+          reload: () => setState(() => 1),
+          onDone: (record) {
+            _fieldData.fromMap(record);
+            _fieldData.isFromBackend = true;
+          },
+          parameters: {
+            'module': 'Users',
+            'sql': 'byId',
+            ':id': primaryKey
+          });
+    }
     nameController.text = _fieldData.name;
     displayNameController.text = _fieldData.displayName;
     emailController.text = _fieldData.email;
@@ -125,7 +134,11 @@ class EditUserCustom extends State<EditUserPage> with MessageLine {
     _fieldData.toMap(parameters);
     globalData.restPersistence!
         .store(what: 'store', map: parameters)
-        .then((answer) {});
+        .then((answer) {
+      _fieldData.isFromBackend = false;
+      attendedPage!.pageStates.dbDataState.clear();
+      setState(() => 1);
+    });
   }
 
   void verifyAndStore() {
@@ -137,6 +150,8 @@ class EditUserCustom extends State<EditUserPage> with MessageLine {
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
     nameController.dispose();
     displayNameController.dispose();
     emailController.dispose();
@@ -145,6 +160,7 @@ class EditUserCustom extends State<EditUserPage> with MessageLine {
 }
 
 class _FieldData {
+  bool isFromBackend = false;
   String name = '';
   String displayName = '';
   String email = '';
@@ -154,7 +170,7 @@ class _FieldData {
     name = map['user_name'];
     displayName = map['user_displayname'];
     email = map['user_email'];
-    role = map['user_role'];
+    role = jsonToObject(map['user_role'], dataType: DataType.reference);
   }
 
   void toMap(Map<String, dynamic> map) {
@@ -162,7 +178,7 @@ class _FieldData {
     map[':name'] = name;
     map[':displayName'] = displayName;
     map[':email'] = email;
-    map[':role'] = asString(role);
+    map[':role'] = asString(role, dbFormat: true);
     map[':changedBy'] = GlobalData.loginUserName;
   }
 }
index 11796632306f7b6a0d8a5c1b26bd2b9bd80e6d06..080f1d7900157f89fa102b57a003efcf54fae64f 100644 (file)
@@ -13,8 +13,13 @@ class EditUserPage extends StatefulWidget {
   @override
   _EditUserPageState createState() {
     final rc = _EditUserPageState(this.primaryKey);
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), UserMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        UserMeta.instance.pageByName('edit')!,
+        UserMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index b6b8727a5b290f1dc8389fbd2f0ed4161839b824..dd50e6145b38d6d1e48bd77d12aeb4eae90f7fbf 100644 (file)
@@ -2,18 +2,21 @@
 // It will never overridden by the meta_tool.
 import 'package:flutter/material.dart';
 
+import '../../base/defines.dart';
 import '../../base/helper.dart';
 import '../../base/i18n.dart';
 import '../../services/global_widget.dart';
 import '../../setting/global_data.dart';
 import '../../widget/attended_page.dart';
 import '../../widget/widget_form.dart';
+import '../../persistence/persistence.dart';
 import 'list_user_page.dart';
 
 final i18n = I18N();
 
 class ListUserCustom extends State<ListUserPage> {
   final globalData = GlobalData();
+  late Future<DbData> _futureDbData;
   AttendedPage? attendedPage;
   final _fieldData = _FieldData();
   final GlobalKey<FormState> _formKey =
@@ -22,6 +25,44 @@ class ListUserCustom extends State<ListUserPage> {
   ListUserCustom();
   @override
   Widget build(BuildContext context) {
+    final rc = Scaffold(
+      appBar: globalData.appBarBuilder(i18n.tr('Overview users')),
+      drawer: globalData.drawerBuilder(context),
+      floatingActionButton: FloatingActionButton(
+        onPressed: () {
+          globalData.navigate(context, '/Users/create');
+        },
+        child: const Icon(Icons.add),
+      ),
+      body: SafeArea(
+          child: FutureBuilder<DbData>(
+        future: _futureDbData,
+        builder: (context, snapshot) {
+          Widget rc;
+          if (snapshot.connectionState != ConnectionState.done) {
+            rc = const CircularProgressIndicator();
+          } else {
+            if (snapshot.hasData) {
+              final rows = attendedPage!.getRows(dbData: snapshot.data!,
+                  columnList: 'user_id;user_name;user_displayname;user_email;role',
+                  onDone: () => setState(() => 1),
+                  routeEdit: '/Users/edit',
+                  context: context);
+              rc = buildFrame(rows: rows);
+            } else if (snapshot.hasError) {
+              rc = Text('Backend problem: ${snapshot.error}');
+            } else {
+              rc = const CircularProgressIndicator();
+            }
+          }
+          return rc;
+        },
+      )),
+    );
+    return rc;
+  }
+
+  Widget buildFrame({required JsonList rows}){
     final padding = GlobalThemeData.padding;
     comboRolesFromBackend(
         attendedPage: attendedPage!, onDone: () => setState(() => 1));
@@ -56,27 +97,13 @@ class ListUserCustom extends State<ListUserPage> {
             color: GlobalThemeData.formBackgroundColor,
             elevation: GlobalThemeData.formElevation,
             margin:
-                EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+            EdgeInsets.symmetric(vertical: padding, horizontal: padding),
             child: Padding(
                 padding: EdgeInsets.symmetric(
                     vertical: padding, horizontal: padding),
                 child: WidgetForm.flexibleGrid(formItems,
                     screenWidth: attendedPage!.pageStates.screenWidth,
                     padding: padding))));
-    final rows = attendedPage!.getRows(
-        columnList: 'user_id;user_name;user_displayname;user_email;role',
-        what: 'query',
-        parameters: {
-          'module': 'Users',
-          'sql': 'list',
-          'offset': '0',
-          'size': '10',
-          ':text': asPattern(_fieldData.text),
-          ':role': _fieldData.role.toString(),
-        },
-        onDone: () => setState(() => 1),
-        routeEdit: '/Users/edit',
-        context: context);
     final table = DataTable(
       columns: <DataColumn>[
         DataColumn(
@@ -95,30 +122,35 @@ class ListUserCustom extends State<ListUserPage> {
           label: Text(i18n.tr('Role')),
         ),
       ],
-      rows: rows,
+      rows: rows as List<DataRow>,
     );
-    final frameWidget = Column(children: [
+    Widget? tabBar =
+    attendedPage!.buildChipBar(onTap: (offset) => setState(() => offset));
+    final frameWidget = ListView(children: [
       form,
+      if (tabBar != null) tabBar,
       SizedBox(height: padding),
       SizedBox(width: double.infinity, child: table),
     ]);
-    final rc = Scaffold(
-        appBar: globalData.appBarBuilder(i18n.tr('Overview users')),
-        drawer: globalData.drawerBuilder(context),
-        floatingActionButton: FloatingActionButton(
-          onPressed: () {
-            globalData.navigate(context, '/Users/create');
-          },
-          child: const Icon(Icons.add),
-          //backgroundColor: Colors.green,
-        ),
-        body: SafeArea(child: frameWidget));
-    return rc;
+    return frameWidget;
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    _futureDbData = globalData.restPersistence!.query(what: 'query', data: {
+      'module': 'Users',
+      'sql': 'list',
+      'offset': _fieldData.theOffset,
+      'size': _fieldData.thePageSize,
+      ':text': asPattern(_fieldData.text),
+      ':role': _fieldData.role.toString(),
+    });
   }
 
   @override
   void dispose() {
-    helperDummyUsage();
+    helperDummyUsage(DataType.string);
     globalWidgetDummyUsage();
     textController.dispose();
     super.dispose();
@@ -139,6 +171,8 @@ class ListUserCustom extends State<ListUserPage> {
 }
 
 class _FieldData {
+  int thePageSize = 10;
+  int theOffset = 0;
   String text = '';
   int role = 0;
 }
index 1938d3d9a841fa556eb29744f91b85a73d9fde16..d36e2bf41b3d0d06eeff9d340a58e3f14aa2f436 100644 (file)
@@ -12,8 +12,13 @@ class ListUserPage extends StatefulWidget {
   @override
   _ListUserPageState createState() {
     final rc = _ListUserPageState();
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), UserMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        UserMeta.instance.pageByName('list')!,
+        UserMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
index 2159ee3811ebab3178931a2f781468192b57b5a8..66b6e384b7262f288eaaa85640613e98d47f9e58 100644 (file)
@@ -40,10 +40,12 @@ class FilePersistence extends Persistence {
     } else {
       final content = await file.readAsString();
       final data = convert.jsonDecode(content);
+      final pageSize = data['pageSize'] ?? 10;
+      final offset = data['offset'] ?? 0;
       if (data is Map) {
         rc = DbData.single(data as JsonMap);
       } else if (data is List) {
-        rc = DbData.list(data, -1);
+        rc = DbData.list(data, -1, offset, pageSize);
       } else if (data is String) {
         rc = DbData.message(data);
       } else {
index cf7794a4bcce8f453cc7ec64b804926aab595c57..ca8c0d5d5208a9f3683f81ece8a2c0d2b442230a 100644 (file)
@@ -7,10 +7,14 @@ class DbData {
   final JsonList? recordList;
   final int? count;
   final String? message;
-  DbData(this.singleRecord, this.recordList, this.count, this.message);
-  DbData.single(JsonMap record) : this(record, null, null, null);
-  DbData.list(JsonList records, int count) : this(null, records, count, null);
-  DbData.message(String message) : this(null, null, null, message);
+  final int pageSize;
+  final int offset;
+  DbData(this.singleRecord, this.recordList, this.count, this.message,
+      this.offset, this.pageSize);
+  DbData.single(JsonMap record) : this(record, null, null, null, 0, 0);
+  DbData.list(JsonList records, int count, int offset, int pageSize)
+      : this(null, records, count, null, offset, pageSize);
+  DbData.message(String message) : this(null, null, null, message, 0, 0);
 }
 
 abstract class Persistence {
index b870ed6142a369159273c61380b500e152fe5887..a3a18724fba0971ceae5dc1fccf2bf4eb4f87207 100644 (file)
@@ -105,13 +105,15 @@ class RestPersistence extends Persistence {
     } else if (answer.startsWith('{')) {
       rc = DbData.single(convert.jsonDecode(answer));
     } else if (answer.startsWith('[')) {
-      rc = DbData.list(convert.jsonDecode(answer), -1);
+      rc = DbData.list(convert.jsonDecode(answer), -1, data?['offset'] ?? 0,
+          data?['pageSize'] ?? 0);
     } else if (answer.startsWith('#')) {
       int ix = answer.indexOf('[');
       if (ix >= 2 && ix < 8) {
         final count = int.tryParse(answer.substring(1, ix));
-        final data = convert.jsonDecode(answer.substring(ix));
-        rc = DbData.list(data as JsonList, count ?? -1);
+        final data2 = convert.jsonDecode(answer.substring(ix));
+        rc = DbData.list(data2 as JsonList, count ?? -1, data?['offset'] ?? 0,
+            data?['pageSize'] ?? 0);
       } else {
         rc = DbData.message('query(): unexpected answer prefix: '
             '${answer.substring(0, min<int>(answer.length, 8))}');
index 1b46ee344c242d3b51fb652e1d6396d75b682d61..da7b246e6f41f6442042b799f51cbf8b2916de17 100644 (file)
@@ -94,6 +94,9 @@ class MenuConverter {
       case 'settings_applications_outlined':
         rc = Icons.settings_applications_outlined;
         break;
+      case 'analytics_outlined':
+        rc = Icons.analytics_outlined;
+        break;
       default:
         logger.error('MenuConverter.iconByName(): unknown icon $name');
         break;
@@ -114,6 +117,10 @@ class MenuItem {
     final logger = globalData.logger;
     final collection = PageManager();
     return <MenuItem>[
+      MenuItem(
+          'Benchmarks',
+          () => collection.newPageByRoute('/Benchmarks/list'),
+          converter.iconByName('analytics_outlined', logger)),
       MenuItem('Users', () => collection.newPageByRoute('/Users/list'),
           converter.iconByName('people_outlined', logger)),
       MenuItem('Protokoll', () => collection.newPageByRoute('/log'),
index 936318b268f3059da879b4097f77458e2bec8ff7..08b577fd713bae1f19c6b4717cb5e80ccebbd0a7 100644 (file)
@@ -2,6 +2,7 @@ import 'package:exhibition/base/defines.dart';
 import 'package:flutter/material.dart';
 import 'package:synchronized/synchronized.dart';
 
+import '../../base/helper.dart';
 import '../../base/i18n.dart';
 import '../../persistence/persistence.dart';
 import '../../setting/global_data.dart';
@@ -18,12 +19,13 @@ final i18n = I18N();
 /// states directly in this class. Store it in [PageStates].
 class AttendedPage {
   final ModuleMetaData moduleMetaData;
+  final PageMetaData pageMetaData;
   final GlobalData globalData;
   final StatefulWidget statefulWidget;
   final State<StatefulWidget> state;
   final PageStates pageStates;
   AttendedPage(this.statefulWidget, this.state, this.globalData,
-      this.moduleMetaData, this.pageStates);
+      this.pageMetaData, this.moduleMetaData, this.pageStates);
 
   /// Returns the list of attended widgets of this page.
   List<AttendedWidget> attendedWidgets() {
@@ -34,6 +36,9 @@ class AttendedPage {
           case DisplayType.text:
             rc.add(TextAttended(item, this));
             break;
+          case DisplayType.switchWidget:
+            rc.add(SwitchAttended(item, this));
+            break;
           case DisplayType.combobox:
             rc.add(ComboboxAttended(item, this));
             break;
@@ -49,7 +54,89 @@ class AttendedPage {
     return rc;
   }
 
+  /// Creates a chip bar: a row of ChoiceChip instances (special buttons).
+  ///
+  /// That chips allow to change the page of the db result.
+  ///
+  /// [onTap]: a function which is called if another chip is selected.
+  Widget? buildChipBar({required Function(int offset) onTap}) {
+    final dbData = pageStates.dbDataState.dataOf('rows');
+    final pageSize = dbData?.pageSize ?? 10;
+    //pageSize = 10;
+    final totalCount = dbData?.count ?? pageSize;
+    //totalCount = 1;
+    final offset = dbData?.offset ?? 0;
+    // offset = 30;
+    final pageCount = (totalCount + pageSize - 1) ~/ pageSize;
+    final currentPage = 1 + offset ~/ pageSize;
+    int currentIndex;
+    var chipCount = pageCount >= 7 ? 7 : pageCount;
+    final inTop = currentPage < 7 - 1;
+    int pageNoOfIndex1;
+    bool inTail = pageCount > 7 && currentPage >= pageCount - 3;
+    if (pageCount <= 7 || inTop) {
+      pageNoOfIndex1 = 2;
+      currentIndex = currentPage - 1;
+    } else if (inTail) {
+      pageNoOfIndex1 = pageCount - 6 + 1;
+      currentIndex = 1 + currentPage - pageNoOfIndex1;
+    } else {
+      currentIndex = 3;
+      pageNoOfIndex1 = currentPage - 2;
+    }
+    final widgets = <Widget>[];
+    for (var ix = 0; ix < chipCount; ix++) {
+      int label;
+      IconData? icon;
+      if (ix == 0) {
+        icon = currentPage == 1
+            ? Icons.beenhere_outlined
+            : Icons.first_page_outlined;
+      } else if (ix == chipCount - 1) {
+        icon = currentPage == 1
+            ? Icons.beenhere_outlined
+            : Icons.last_page_outlined;
+      } else if (ix == currentIndex) {
+        icon = Icons.beenhere_outlined;
+      } else if (ix < currentIndex) {
+        icon = Icons.chevron_left_outlined;
+      } else {
+        icon = Icons.chevron_right_outlined;
+      }
+      if (ix == 0) {
+        label = 1;
+      } else if (ix == chipCount - 1) {
+        label = pageCount;
+      } else {
+        label = pageNoOfIndex1 + ix - 1;
+      }
+      final currentChip = ChoiceChip(
+        avatar: CircleAvatar(child: Icon(icon)),
+        label: Text(label.toString()),
+        selected: currentIndex == ix,
+        onSelected: (selected) {
+          if (selected) {
+            onTap((label - 1) * pageSize);
+          }
+        },
+      );
+      widgets.add(currentChip);
+      if (pageCount > 7 && (ix == 0 && !inTop || ix == 7 - 2 && !inTail)) {
+        widgets.add(Text(' ... '));
+      }
+    }
+    Widget? rc = Wrap(children: widgets);
+    return rc;
+  }
+
   /// Returns a database record with a given [primaryKey].
+  ///
+  /// [what] specifies the backend service, normally 'query'.
+  ///
+  /// [parameters] is a map specifying the data request.
+  /// Example: {'module': 'Users', 'sql': 'record', ':id': 223 }
+  ///
+  /// [onDone]: a function called when the answer is arrived.
   JsonMap? getRecord(
       {required int primaryKey,
       required String what,
@@ -80,40 +167,32 @@ class AttendedPage {
   ///
   /// [columnList]: a ';' delimited list of column names, e.g. 'user_id;user_name'
   List<DataRow> getRows(
-      {required String columnList,
-      required String what,
-      required Map<String, dynamic> parameters,
+      {required DbData dbData,
+      required String columnList,
       required Function() onDone,
       required String routeEdit,
       required BuildContext context}) {
     List<DataRow>? rc;
-    String name = 'rows';
-    if (!pageStates.dbDataState.hasEntry(name)) {
-      pageStates.dbDataState.addRequest(name).then((_) => globalData
-              .restPersistence!
-              .query(what: what, data: parameters)
-              .then((value) {
-            pageStates.dbDataState
-                .storeDbResult(name, value)
-                .then((_) => onDone());
-          }));
-    } else {
-      final dbData = pageStates.dbDataState.dataOf(name);
-      if (dbData != null && dbData.recordList != null) {
-        rc = <DataRow>[];
-        dbData.recordList!.forEach((record) {
-          final cells = <DataCell>[];
-          for (var column in columnList.split(';')) {
-            final primaryKey = moduleMetaData.primaryOf()?.columnName;
-            cells.add(DataCell(Text(record[column].toString()), onTap: () {
-              globalData.navigate(
-                  context, routeEdit + ';${record[primaryKey]}');
-            }));
+    if (dbData != null && dbData.recordList != null) {
+      rc = <DataRow>[];
+      dbData.recordList!.forEach((record) {
+        final cells = <DataCell>[];
+        for (var column in columnList.split(';')) {
+          final primaryKey = moduleMetaData.primaryOf()?.columnName;
+          String value;
+          final property = moduleMetaData.propertyByColumnName(column);
+          if (property == null) {
+            value = record[column].toString();
+          } else {
+            value = dbValueToString(record[column], property.dataType);
           }
-          final rc3 = DataRow(cells: cells);
-          rc!.add(rc3);
-        });
-      }
+          cells.add(DataCell(Text(value), onTap: () {
+            globalData.navigate(context, routeEdit + ';${record[primaryKey]}');
+          }));
+        }
+        final rc3 = DataRow(cells: cells);
+        rc!.add(rc3);
+      });
     }
     return rc ?? <DataRow>[];
   }
index cf9cebbfad1d5f589baecae9da0be1722083f8e0..dc9a2bce8d5e57b747c1f5f88a980fbe717acc2c 100644 (file)
@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 
+import '../base/helper.dart';
 import '../meta/module_meta_data.dart';
 import '../widget/attended_page.dart';
 
@@ -7,7 +8,8 @@ typedef MenuItemBuilder = List<DropdownMenuItem<T>> Function<T>(
     PropertyMetaData propertyMetaData);
 
 /// Manages a widget controlled by the meta data.
-abstract class AttendedWidget {
+abstract class AttendedWidget<T> {
+  T? value;
   final AttendedPage attendedPage;
   final PropertyMetaData propertyMetaData;
 
@@ -15,14 +17,14 @@ abstract class AttendedWidget {
   Widget widgetOf();
 }
 
-class ComboboxAttended<T> extends FieldAttended {
-  T? value;
+class ComboboxAttended<T> extends FieldAttended<T> {
   MenuItemBuilder menuItemBuilder =
       <T>(PropertyMetaData _) => <DropdownMenuItem<T>>[];
   // MenuItemBuilder menuItemBuilder =
   //     ((PropertyMetaData _) => <DropdownMenuItem<T>>[]) as MenuItemBuilder;
   ComboboxAttended(PropertyMetaData propertyMetaData, AttendedPage attendedPage)
       : super(propertyMetaData, attendedPage);
+  @override
   Widget widgetOf() {
     final rc = DropdownButtonFormField<T>(
       value: value,
@@ -35,7 +37,7 @@ class ComboboxAttended<T> extends FieldAttended {
   }
 }
 
-abstract class FieldAttended extends AttendedWidget {
+abstract class FieldAttended<T> extends AttendedWidget<T> {
   var readonly = false;
   var enabled = true;
   FieldAttended(PropertyMetaData propertyMetaData, AttendedPage attendedPage)
@@ -44,22 +46,43 @@ abstract class FieldAttended extends AttendedWidget {
 
 /// Implements a single or multiline line text field controlled by the
 /// meta data of a module.
-class TextAttended extends FieldAttended {
+class SwitchAttended extends FieldAttended<bool> {
+  SwitchAttended(PropertyMetaData propertyMetaData, AttendedPage attendedPage)
+      : super(propertyMetaData, attendedPage);
+
+  /// Returns a concrete widget representing the instance.
+  @override
+  Widget widgetOf() {
+    final rc = SwitchListTile(
+      title: Text(propertyMetaData.label),
+      value: value ?? false,
+      onChanged: (bool value) {
+        this.value = jsonToObject(value, dataType: propertyMetaData.dataType);
+      },
+    );
+    return rc;
+  }
+}
+
+/// Implements a single or multiline line text field controlled by the
+/// meta data of a module.
+class TextAttended<T> extends FieldAttended<T> {
   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.
+  @override
   Widget widgetOf() {
     final rc = TextFormField(
       controller: controller,
       minLines: minLines,
       maxLines: maxLines,
-      onSaved: (value) => this.value = value ?? '',
+      onSaved: (value) => this.value =
+          fromString(value ?? '', dataType: propertyMetaData.dataType),
       validator: (value) => attendedPage.validateText(value, this),
       decoration: InputDecoration(labelText: propertyMetaData.label),
     );
index 9a86b35dc910c168fb94baf5946488e005d44bd5..7be3d59d2318fa2d613b161631714f57f46280bd 100644 (file)
@@ -44,7 +44,7 @@ class WidgetForm {
         }
         widgets.add(element.widget);
       });
-      rc = Column(children: widgets);
+      rc = ListView(children: widgets);
     } else {
       int position = 0;
       final List<Widget> childrenColumn = [];
index d9ddf20eee4615d9a9a2771c2b8a603f69eadcef..cdb17daae0becbd640836619c385340be188e9a8 100644 (file)
@@ -26,8 +26,13 @@ DEF_PRIMARY  final PageStates pageStates = PageStates();
   @override
   _EditUserPageState createState() {
     final rc = _EditUserPageState(THIS_PRIMARY);
-    rc.attendedPage =
-        AttendedPage(this, rc, GlobalData(), UserMeta.instance, pageStates);
+    rc.attendedPage = AttendedPage(
+        this,
+        rc,
+        GlobalData(),
+        UserMeta.instance.pageByName('edit')!,
+        UserMeta.instance,
+        pageStates);
     pageStates.attendedPage = rc.attendedPage;
     return rc;
   }
@@ -55,18 +60,21 @@ class _EditUserPageState extends EditUserCustom {
 // It will never overridden by the meta_tool.
 import 'package:flutter/material.dart';
 
+import '../../base/defines.dart';
 import '../../base/helper.dart';
 import '../../base/i18n.dart';
 import '../../services/global_widget.dart';
 import '../../setting/global_data.dart';
 import '../../widget/attended_page.dart';
 import '../../widget/widget_form.dart';
+import '../../persistence/persistence.dart';
 import 'list_user_page.dart';
 
 final i18n = I18N();
 
 class ListUserCustom extends State<ListUserPage> {
   final globalData = GlobalData();
+  late Future<DbData> _futureDbData;
   AttendedPage? attendedPage;
   final _fieldData = _FieldData();
   final GlobalKey<FormState> _formKey =
@@ -74,6 +82,44 @@ class ListUserCustom extends State<ListUserPage> {
 #DEF_CONTROLLERS  ListUserCustom();
   @override
   Widget build(BuildContext context) {
+    final rc = Scaffold(
+      appBar: globalData.appBarBuilder(i18n.tr('Overview users')),
+      drawer: globalData.drawerBuilder(context),
+      floatingActionButton: FloatingActionButton(
+        onPressed: () {
+          globalData.navigate(context, '/Users/create');
+        },
+        child: const Icon(Icons.add),
+      ),
+      body: SafeArea(
+          child: FutureBuilder<DbData>(
+        future: _futureDbData,
+        builder: (context, snapshot) {
+          Widget rc;
+          if (snapshot.connectionState != ConnectionState.done) {
+            rc = const CircularProgressIndicator();
+          } else {
+            if (snapshot.hasData) {
+              final rows = attendedPage!.getRows(dbData: snapshot.data!,
+                  columnList: '#ROW_COLUMNS',
+                  onDone: () => setState(() => 1),
+                  route#EDIT1: '/Users/#EDIT2',
+                  context: context);
+              rc = buildFrame(rows: rows);
+            } else if (snapshot.hasError) {
+              rc = Text('Backend problem: \${snapshot.error}');
+            } else {
+              rc = const CircularProgressIndicator();
+            }
+          }
+          return rc;
+        },
+      )),
+    );
+    return rc;
+  }
+
+  Widget buildFrame({required JsonList rows}){
     final padding = GlobalThemeData.padding;
 #INIT_COMBOS    final formItems = <FormItem>[
 #FORM_ITEMS      FormItem(
@@ -88,52 +134,43 @@ class ListUserCustom extends State<ListUserPage> {
             color: GlobalThemeData.formBackgroundColor,
             elevation: GlobalThemeData.formElevation,
             margin:
-                EdgeInsets.symmetric(vertical: padding, horizontal: padding),
+            EdgeInsets.symmetric(vertical: padding, horizontal: padding),
             child: Padding(
                 padding: EdgeInsets.symmetric(
                     vertical: padding, horizontal: padding),
                 child: WidgetForm.flexibleGrid(formItems,
                     screenWidth: attendedPage!.pageStates.screenWidth,
                     padding: padding))));
-    final rows = attendedPage!.getRows(
-        columnList: '#ROW_COLUMNS',
-        what: 'query',
-        parameters: {
-          'module': 'Users',
-          'sql': 'list',
-          'offset': '0',
-          'size': '10',
-#PARAM_DEF        },
-        onDone: () => setState(() => 1),
-        route#EDIT1: '/Users/#EDIT2',
-        context: context);
     final table = DataTable(
       columns: <DataColumn>[
 #TABLE_HEADER      ],
-      rows: rows,
+      rows: rows as List<DataRow>,
     );
-    final frameWidget = Column(children: [
+    Widget? tabBar =
+    attendedPage!.buildChipBar(onTap: (offset) => setState(() => offset));
+    final frameWidget = ListView(children: [
       form,
+      if (tabBar != null) tabBar,
       SizedBox(height: padding),
       SizedBox(width: double.infinity, child: table),
     ]);
-    final rc = Scaffold(
-        appBar: globalData.appBarBuilder(i18n.tr('Overview users')),
-        drawer: globalData.drawerBuilder(context),
-        floatingActionButton: FloatingActionButton(
-          onPressed: () {
-            globalData.navigate(context, '/Users/create');
-          },
-          child: const Icon(Icons.add),
-          //backgroundColor: Colors.green,
-        ),
-        body: SafeArea(child: frameWidget));
-    return rc;
+    return frameWidget;
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    _futureDbData = globalData.restPersistence!.query(what: 'query', data: {
+      'module': 'Users',
+      'sql': 'list',
+      'offset': _fieldData.theOffset,
+      'size': _fieldData.thePageSize,
+#PARAM_DEF    });
   }
 
   @override
   void dispose() {
-    helperDummyUsage();
+    helperDummyUsage(DataType.string);
     globalWidgetDummyUsage();
 #DISPOSE_CONTROLLER    super.dispose();
   }
@@ -153,6 +190,8 @@ class ListUserCustom extends State<ListUserPage> {
 }
 
 class _FieldData {
+  int thePageSize = 10;
+  int theOffset = 0;
 #DEF_FIELDS}
 ''';
 
@@ -245,11 +284,14 @@ class EditUserCustom extends State<EditUserPage> with MessageLine {
 
   @override
   void dispose() {
+    helperDummyUsage(DataType.int);
+    globalWidgetDummyUsage();
 #DISP_CONTROLLER    super.dispose();
   }
 }
 
 class _FieldData {
+  bool isFromBackend = false;
 #DEF_FIELD
 #FROM_MAP#TO_MAP}
 ''';
@@ -266,7 +308,12 @@ class _FieldData {
         }
       }
     ''';
-  static final templateStorageDoneEdit = '      setState(() => 1);';
+  static final templateStorageDoneEdit = '''
+
+      _fieldData.isFromBackend = false;
+      attendedPage!.pageStates.dbDataState.clear();
+      setState(() => 1);
+    ''';
   static final templateStorageDoneDelete =
       "\n      globalData.navigate(context, '/#MODULE/list');\n    ";
   static final templateFromMap = '''  void fromMap(Map<String, dynamic> map) {
@@ -356,7 +403,8 @@ StatefulWidget? customPageByRoute(String route) {
           (field as PropertyMetaData).displayType == DisplayType.text) {
         var value = '_fieldData.${field.name}';
         if (field.dataType != DataType.string) {
-          value = 'asString($value)';
+          value = field.dataType == DataType.date ? 'asString($value, dateOnly: true)'
+          : 'asString($value)';
         }
         buffer.writeln('    ${field.name}Controller.text = $value;');
       }
@@ -469,6 +517,23 @@ StatefulWidget? customPageByRoute(String route) {
     final name = field.name;
     String rc = '';
     switch (field.displayType) {
+      case DisplayType.switchWidget:
+        rc = '''^Row(children: [
+^  Switch(
+^    value: _fieldData.#NAME,
+^    onChanged: (bool value) {
+^      //_fieldData.#NAME = value;
+^      setState(() => _fieldData.#NAME = value);
+^    },
+^  ),
+^  Expanded(
+^    child: Text(i18n.tr('#LABEL')),
+^  ),
+^])'''
+            .replaceAll('#NAME', field.name)
+            .replaceFirst('#LABEL', field.label)
+            .replaceAll('^', indent);
+        break;
       case DisplayType.text:
         final validators = <String>[];
         if (field.hasOption(':notnull:')) {
@@ -488,8 +553,8 @@ StatefulWidget? customPageByRoute(String route) {
         final value = field.dataType == DataType.string
             ? "value ?? ''"
             : (field.dataType == DataType.date
-                ? "fromString(value ?? '', dataType: DataType.date)"
-                : "fromString(value ?? '', dataType: ${field.dataType})");
+                ? "jsonToObject(value ?? '', dataType: DataType.date)"
+                : "jsonToObject(value ?? '', dataType: ${field.dataType})");
         rc = '''^TextFormField(
 #CONTR^  decoration: InputDecoration(labelText: i18n.tr('${field.label}')),
 #VALIDATOR^  onSaved: (value) => _fieldData.$name = $value
@@ -559,7 +624,11 @@ StatefulWidget? customPageByRoute(String route) {
           StringBuffer('  void fromMap(Map<String, dynamic> map) {\n');
       for (var field in page.fields) {
         if (field is PropertyMetaData && !field.hasOption(':primary:')) {
-          buffer.writeln("    ${field.name} = map['${field.columnName}'];");
+          var value = "map['${field.columnName}']";
+          if (field.dataType != DataType.string) {
+            value = 'jsonToObject($value, dataType: ${field.dataType})';
+          }
+          buffer.writeln("    ${field.name} = $value;");
         }
       }
       buffer.writeln('  }');
@@ -587,11 +656,20 @@ StatefulWidget? customPageByRoute(String route) {
   String buildLoadRecord(PageMetaData page) {
     String? rc;
     if (page.pageType == PageType.edit || page.pageType == PageType.delete) {
-      rc = '''    attendedPage?.loadRecord(
-        name: 'record',
-        reload: () => setState(() => 1),
-        onDone: (record) => _fieldData.fromMap(record),
-        parameters: {'module': '${page.module.moduleName}', 'sql': 'byId', ':id': primaryKey});
+      rc = '''    if (!_fieldData.isFromBackend) {
+      attendedPage?.loadRecord(
+          name: 'record',
+          reload: () => setState(() => 1),
+          onDone: (record) {
+            _fieldData.fromMap(record);
+            _fieldData.isFromBackend = true;
+          },
+          parameters: {
+            'module': '${page.module.moduleName}',
+            'sql': 'byId',
+            ':id': primaryKey
+          });
+    }
 ''';
     }
     return rc ?? '';
@@ -613,7 +691,7 @@ StatefulWidget? customPageByRoute(String route) {
       if (field is PropertyMetaData && field.hasOption(':pattern')) {
         value = 'asPattern($value)';
       }
-      buffer.writeln("          ':$name': $value,");
+      buffer.writeln("      ':$name': $value,");
     }
     return buffer.toString();
   }
@@ -649,8 +727,11 @@ StatefulWidget? customPageByRoute(String route) {
           continue;
         }
         final name = field.name;
-        final value =
-            field.dataType == DataType.string ? name : 'asString($name)';
+        final value = field.dataType == DataType.string
+            ? name
+            : (field.dataType == DataType.date
+                ? 'asString($name, dbFormat: true, dateOnly: true)'
+                : 'asString($name, dbFormat: true)');
         buffer.writeln("    map[':$name'] = $value;");
       }
       buffer.writeln("    map[':#'] = GlobalData.loginUserName;".replaceFirst(
@@ -745,7 +826,6 @@ StatefulWidget? customPageByRoute(String route) {
         sqlType = 'update';
         storageDone = templateStorageDoneEdit;
         hasPrimary = true;
-        storageDone = '';
         break;
       case PageType.delete:
         setPrimary = "    parameters[':id'] = primaryKey;\n";
index f01c0e87496e1e9cd3bed2cdc5b028479f56f3af..df176d65a686c911a0e8f91fd48ab4aa557e830f 100644 (file)
@@ -16,72 +16,128 @@ void main() {
   //final baseDir = init(logger);
   //final targetDir = join(baseDir, nodeTarget);
   //fileSync.ensureDirectory(targetDir);
+  group('secondsAsTime', () {
+    test('days', () {
+      expect(secondsAsTime(3 * 86400 + 5 * 3600 + 14 * 60 + 32), '3:05:14:32');
+    });
+    test('hours', () {
+      expect(secondsAsTime(23 * 3600 + 9 * 60 + 4), '23:09:04');
+    });
+    test('minutes', () {
+      expect(secondsAsTime(3 * 60 + 19), '03:19');
+    });
+    test('seconds', () {
+      expect(secondsAsTime(59), '59');
+    });
+    test('days-millisecnds', () {
+      expect(
+          secondsAsTime((3 * 86400 + 5 * 3600 + 14 * 60 + 32) * 1000 + 49,
+              isMilliSeconds: true),
+          '3:05:14:32.049');
+    });
+  });
+  group('jsonToObject', () {
+    test('bool', () {
+      expect(jsonToObject('T', dataType: DataType.bool, defaultValue: false),
+          isTrue);
+      expect(jsonToObject('F', dataType: DataType.bool, defaultValue: true),
+          isFalse);
+      expect(jsonToObject('x', dataType: DataType.bool, defaultValue: true),
+          isTrue);
+      expect(jsonToObject('y', dataType: DataType.bool, defaultValue: false),
+          isFalse);
+    });
+    test('datetime', () {
+      expect(
+          jsonToObject('2021-8-3 2:4',
+              dataType: DataType.datetime, defaultValue: false),
+          DateTime(2021, 8, 3, 2, 4));
+    });
+    test('date', () {
+      expect(
+          jsonToObject('2021-10-3',
+              dataType: DataType.date, defaultValue: false),
+          DateTime(2021, 10, 3));
+    });
+    test('int-nat-reference', () {
+      expect(jsonToObject(332211, dataType: DataType.nat), 332211);
+      expect(jsonToObject('332209', dataType: DataType.int), 332209);
+      expect(jsonToObject('220944', dataType: DataType.reference), 220944);
+      expect(jsonToObject(2209443, dataType: DataType.reference), 2209443);
+      expect(jsonToObject('xy', dataType: DataType.reference), isNull);
+    });
+  });
   group('fromString', () {
     test('bool', () {
-      expect(fromString('T', dataType: DataType.bool, defaultValue: false), isTrue);
-      expect(fromString('F', dataType: DataType.bool, defaultValue: true), isFalse);
-      expect(fromString(i18n.tr('Yes'), dataType: DataType.bool, defaultValue: false), isTrue);
-      expect(fromString(i18n.tr('No'), dataType: DataType.bool, defaultValue: true), isFalse);
-      expect(fromString('', dataType: DataType.bool, defaultValue: false), isFalse);
-      expect(fromString('', dataType: DataType.bool, defaultValue: true), isTrue);
-    });
-    test('int', ()
-    {
-      expect(fromString('123', dataType: DataType.int, defaultValue: -1),
-          123);
-      expect(fromString('-1234', dataType: DataType.int, defaultValue: -1),
-          -1234);
-      expect(fromString('', dataType: DataType.int, defaultValue: -1),
-          -1);
-      expect(fromString('a15', dataType: DataType.int, defaultValue: -1),
-          -1);
+      expect(fromString('T', dataType: DataType.bool, defaultValue: false),
+          isTrue);
+      expect(fromString('F', dataType: DataType.bool, defaultValue: true),
+          isFalse);
+      expect(
+          fromString(i18n.tr('Yes'),
+              dataType: DataType.bool, defaultValue: false),
+          isTrue);
+      expect(
+          fromString(i18n.tr('No'),
+              dataType: DataType.bool, defaultValue: true),
+          isFalse);
+      expect(fromString('', dataType: DataType.bool, defaultValue: false),
+          isFalse);
+      expect(
+          fromString('', dataType: DataType.bool, defaultValue: true), isTrue);
     });
-    test('nat', ()
-    {
-      expect(fromString('123', dataType: DataType.nat, defaultValue: -1),
-          123);
-      expect(fromString('-1234', dataType: DataType.nat, defaultValue: -1),
-          -1);
-      expect(fromString('', dataType: DataType.nat, defaultValue: -1),
-          -1);
-      expect(fromString('a15', dataType: DataType.nat, defaultValue: -1),
-          -1);
+    test('int', () {
+      expect(fromString('123', dataType: DataType.int, defaultValue: -1), 123);
+      expect(
+          fromString('-1234', dataType: DataType.int, defaultValue: -1), -1234);
+      expect(fromString('', dataType: DataType.int, defaultValue: -1), -1);
+      expect(fromString('a15', dataType: DataType.int, defaultValue: -1), -1);
     });
-    test('reference', ()
-    {
+    test('nat', () {
+      expect(fromString('123', dataType: DataType.nat, defaultValue: -1), 123);
+      expect(fromString('-1234', dataType: DataType.nat, defaultValue: -1), -1);
+      expect(fromString('', dataType: DataType.nat, defaultValue: -1), -1);
+      expect(fromString('a15', dataType: DataType.nat, defaultValue: -1), -1);
+    });
+    test('reference', () {
       expect(fromString('123', dataType: DataType.reference, defaultValue: -1),
           123);
-      expect(fromString('-1234', dataType: DataType.reference, defaultValue: -1),
+      expect(
+          fromString('-1234', dataType: DataType.reference, defaultValue: -1),
           -1234);
-      expect(fromString('', dataType: DataType.reference, defaultValue: -1),
-          -1);
+      expect(
+          fromString('', dataType: DataType.reference, defaultValue: -1), -1);
       expect(fromString('a15', dataType: DataType.reference, defaultValue: -1),
           -1);
     });
-    test('float', ()
-    {
+    test('float', () {
       expect(fromString('123.76', dataType: DataType.float, defaultValue: -1),
           123.76);
       expect(fromString('-4123.76', dataType: DataType.float, defaultValue: -1),
           -4123.76);
       expect(fromString('', dataType: DataType.float, defaultValue: -99999999),
           -99999999);
-      expect(fromString('Nothing', dataType: DataType.float, defaultValue: -99999999),
+      expect(
+          fromString('Nothing',
+              dataType: DataType.float, defaultValue: -99999999),
           -99999999);
     });
-    test('currency', ()
-    {
-      expect(fromString('123.76', dataType: DataType.currency, defaultValue: -1),
+    test('currency', () {
+      expect(
+          fromString('123.76', dataType: DataType.currency, defaultValue: -1),
           123.76);
-      expect(fromString('-4123.76', dataType: DataType.currency, defaultValue: -1),
+      expect(
+          fromString('-4123.76', dataType: DataType.currency, defaultValue: -1),
           -4123.76);
-      expect(fromString('', dataType: DataType.currency, defaultValue: -99999999),
+      expect(
+          fromString('', dataType: DataType.currency, defaultValue: -99999999),
           -99999999);
-      expect(fromString('Nothing', dataType: DataType.currency, defaultValue: -99999999),
+      expect(
+          fromString('Nothing',
+              dataType: DataType.currency, defaultValue: -99999999),
           -99999999);
     });
-    test('date', ()
-    {
+    test('date', () {
       expect(fromString('7.3.1987', dataType: DataType.date),
           DateTime(1987, 3, 7));
       expect(fromString('17.12.2035', dataType: DataType.date),
@@ -90,11 +146,9 @@ void main() {
           DateTime(2012, 6, 2));
       expect(fromString('1923.10.31', dataType: DataType.date),
           DateTime(1923, 10, 31));
-      expect(fromString('17.12.21', dataType: DataType.date),
-          isNull);
+      expect(fromString('17.12.21', dataType: DataType.date), isNull);
     });
-    test('datetime', ()
-    {
+    test('datetime', () {
       expect(fromString('17.12.2035', dataType: DataType.datetime),
           DateTime(2035, 12, 17));
       expect(fromString('7.3.1987', dataType: DataType.datetime),
@@ -103,8 +157,7 @@ void main() {
           DateTime(2012, 6, 2));
       expect(fromString('1923.10.31', dataType: DataType.datetime),
           DateTime(1923, 10, 31));
-      expect(fromString('17.12.21', dataType: DataType.datetime),
-          isNull);
+      expect(fromString('17.12.21', dataType: DataType.datetime), isNull);
       expect(fromString('7.3.1987T2:44', dataType: DataType.datetime),
           DateTime(1987, 3, 7, 2, 44));
       expect(fromString('17.12.2035 13:59:22', dataType: DataType.datetime),
diff --git a/metatool/test/validators_test.dart b/metatool/test/validators_test.dart
new file mode 100644 (file)
index 0000000..fa5c35e
--- /dev/null
@@ -0,0 +1,67 @@
+import 'package:dart_bones/dart_bones.dart';
+import 'package:test/test.dart';
+
+import '../lib/base/validators.dart' as validators;
+import '../lib/base/i18n.dart';
+
+final i18n = I18N();
+
+void main() {
+  final logger = MemoryLogger(LEVEL_FINE);
+  I18N.internal(logger);
+  FileSync.initialize(logger);
+  group('isEmail', () {
+    test('standard', () {
+      expect(validators.isEmail('abc@example.com'), isNull);
+      expect(validators.isEmail(null), isNull);
+    });
+    test('wrong char', () {
+      expect(validators.isEmail('/abc@example.com'), 'Illegal character "/" in email address');
+    });
+    test('wrong syntax', () {
+      expect(validators.isEmail('abc@example'), 'Not an email address: abc@example');
+    });
+  });
+  group('isInt', () {
+    test('OK', () {
+      expect(validators.isInt('123456'), isNull);
+      expect(validators.isInt('-123456'), isNull);
+      expect(validators.isInt(null), isNull);
+    });
+    test('errors', () {
+      expect(validators.isInt('x'), 'Not an integer: x');
+      expect(validators.isInt('123.77'), 'Not an integer: 123.77');
+    });
+  });
+  group('isNat', () {
+    test('OK', () {
+      expect(validators.isNat('123456'), isNull);
+      expect(validators.isNat('0'), isNull);
+      expect(validators.isNat(null), isNull);
+    });
+    test('errors', () {
+      expect(validators.isNat('-123456'), 'Not negative integer expected, not: -123456');
+      expect(validators.isNat('x'), 'Not an integer: x');
+      expect(validators.isNat('123.77'), 'Not an integer: 123.77');
+    });
+  });
+  group('notEmpty', () {
+    test('OK', () {
+      expect(validators.notEmpty('Go'), isNull);
+    });
+    test('error', () {
+      expect(validators.notEmpty(''), 'Please fill in.');
+      expect(validators.notEmpty(null), 'Please fill in.');
+    });
+  });
+  group('validateMultiple', () {
+    test('OK', () {
+      expect(validators.validateMultiple('abc@example.com', [(x) => validators.notEmpty(x),
+        (x) => validators.isEmail(x)]), isNull);
+    });
+    test('Error', () {
+      expect(validators.validateMultiple('0x72z', [(x) => validators.notEmpty(x),
+        (x) => validators.isInt(x)]), 'Not an integer: 0x72z');
+    });
+  });
+}
index 7a45e64ff0d6717d8167c1dab3435cb1972227d2..4f6228e2cbf1001baa43fc3260c49aee5743a017 100644 (file)
@@ -584,24 +584,25 @@ class ServiceWorker {
         sql2 = sql;
       } else {
         final ix = sql.lastIndexOf(';');
-        sql2 = sql.substring(0, ix) +
-            ' limit ${parameters["offset"]},${parameters["size"]};';
-        final ix2 = sql.indexOf(' FROM ${module.toLowerCase()} t0 ');
-        if (ix2 > 0)
-        {
-          final sqlCount =
-              'SELECT count(*) ' + sql.substring(ix2);
-          final countParams = '?'
-              .allMatches(sqlCount)
-              .length;
-          final positionalParameters2 = countParams ==
-              positionalParameters.length
-              ? positionalParameters
-              : positionalParameters
-              .skip(positionalParameters.length - countParams)
-              .toList();
+        if (ix > 0) {
+          sql2 = sql.substring(0, ix) +
+              ' limit ${parameters["offset"]},${parameters["size"]};';
+        } else {
+          sql2 = sql + ' limit ${parameters["offset"]},${parameters["size"]};';
+        }
+        final fromPart = 'FROM ${module.toLowerCase()} t0';
+        final ix2 = sql.indexOf(fromPart);
+        if (ix2 > 0) {
+          final sqlCount = 'SELECT count(*) ' + sql.substring(ix2);
+          final countParams = '?'.allMatches(sqlCount).length;
+          final positionalParameters2 =
+              countParams == positionalParameters.length
+                  ? positionalParameters
+                  : positionalParameters
+                      .skip(positionalParameters.length - countParams)
+                      .toList();
           prefix =
-          '#${await db!.readOneInt(sqlCount, params: positionalParameters2)}';
+              '#${await db!.readOneInt(sqlCount, params: positionalParameters2)}';
         }
       }
       final records = await db!.readAll(sql2, params: positionalParameters);