From 85a481815f6cc021a6744ef7922936b08b41fe6d Mon Sep 17 00:00:00 2001 From: Hamatoma Date: Sat, 16 Oct 2021 22:55:15 +0200 Subject: [PATCH] Benchmark: all data types, creation of records * 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 --- .gitignore | 2 + lib/base/helper.dart | 126 ++++++++++++- lib/base/validators.dart | 29 ++- lib/common/benchmark_actions.dart | 83 ++++++++ lib/common/random_data.dart | 164 ++++++++++++++++ lib/meta/benchmarks_meta.dart | 19 +- lib/meta/module_meta_data.dart | 23 ++- .../benchmarks/create_benchmark_custom.dart | 40 ++-- .../benchmarks/create_benchmark_page.dart | 9 +- .../benchmarks/delete_benchmark_custom.dart | 59 +++--- .../benchmarks/delete_benchmark_page.dart | 9 +- .../benchmarks/edit_benchmark_custom.dart | 73 +++++--- lib/page/benchmarks/edit_benchmark_page.dart | 9 +- .../benchmarks/list_benchmark_custom.dart | 177 ++++++++++++++---- lib/page/benchmarks/list_benchmark_page.dart | 9 +- lib/page/roles/create_role_custom.dart | 3 + lib/page/roles/create_role_page.dart | 9 +- lib/page/roles/edit_role_custom.dart | 28 ++- lib/page/roles/edit_role_page.dart | 9 +- lib/page/roles/list_role_custom.dart | 92 ++++++--- lib/page/roles/list_role_page.dart | 9 +- .../structures/create_structure_custom.dart | 7 +- .../structures/create_structure_page.dart | 9 +- .../structures/delete_structure_custom.dart | 26 ++- .../structures/delete_structure_page.dart | 9 +- .../structures/edit_structure_custom.dart | 34 +++- lib/page/structures/edit_structure_page.dart | 9 +- .../structures/list_structure_custom.dart | 92 ++++++--- lib/page/structures/list_structure_page.dart | 9 +- lib/page/users/create_user_custom.dart | 5 +- lib/page/users/create_user_page.dart | 9 +- lib/page/users/delete_user_custom.dart | 24 ++- lib/page/users/delete_user_page.dart | 9 +- lib/page/users/edit_user_custom.dart | 32 +++- lib/page/users/edit_user_page.dart | 9 +- lib/page/users/list_user_custom.dart | 94 +++++++--- lib/page/users/list_user_page.dart | 9 +- lib/persistence/file_persistence.dart | 4 +- lib/persistence/persistence.dart | 12 +- lib/persistence/rest_persistence.dart | 8 +- lib/setting/drawer_exhibition.dart | 7 + lib/widget/attended_page.dart | 139 +++++++++++--- lib/widget/attended_widget.dart | 37 +++- lib/widget/widget_form.dart | 2 +- metatool/bin/page_generator.dart | 168 ++++++++++++----- metatool/test/helper_test.dart | 151 ++++++++++----- metatool/test/validators_test.dart | 67 +++++++ rest_server/lib/rest_server.dart | 35 ++-- 48 files changed, 1567 insertions(+), 430 deletions(-) create mode 100644 lib/common/benchmark_actions.dart create mode 100644 lib/common/random_data.dart create mode 100644 metatool/test/validators_test.dart diff --git a/.gitignore b/.gitignore index aa9f965..118e390 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/lib/base/helper.dart b/lib/base/helper.dart index 8658397..7ae09be 100644 --- a/lib/base/helper.dart +++ b/lib/base/helper.dart @@ -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. diff --git a/lib/base/validators.dart b/lib/base/validators.dart index 8baf63a..ad862af 100644 --- a/lib/base/validators.dart +++ b/lib/base/validators.dart @@ -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 index 0000000..9703511 --- /dev/null +++ b/lib/common/benchmark_actions.dart @@ -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 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 = { + '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 index 0000000..e07a564 --- /dev/null +++ b/lib/common/random_data.dart @@ -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 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 +} diff --git a/lib/meta/benchmarks_meta.dart b/lib/meta/benchmarks_meta.dart index 7956efd..39a21e2 100644 --- a/lib/meta/benchmarks_meta.dart +++ b/lib/meta/benchmarks_meta.dart @@ -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', diff --git a/lib/meta/module_meta_data.dart b/lib/meta/module_meta_data.dart index 4959439..1a65781 100644 --- a/lib/meta/module_meta_data.dart +++ b/lib/meta/module_meta_data.dart @@ -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 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 = []; var toDelete = []; diff --git a/lib/page/benchmarks/create_benchmark_custom.dart b/lib/page/benchmarks/create_benchmark_custom.dart index b09328e..62d07c8 100644 --- a/lib/page/benchmarks/create_benchmark_custom.dart +++ b/lib/page/benchmarks/create_benchmark_custom.dart @@ -25,7 +25,6 @@ class CreateBenchmarkCustom extends State 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 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 = [ @@ -69,29 +67,35 @@ class CreateBenchmarkCustom extends State 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 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 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; } } diff --git a/lib/page/benchmarks/create_benchmark_page.dart b/lib/page/benchmarks/create_benchmark_page.dart index fa02c71..4fbfd28 100644 --- a/lib/page/benchmarks/create_benchmark_page.dart +++ b/lib/page/benchmarks/create_benchmark_page.dart @@ -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; } diff --git a/lib/page/benchmarks/delete_benchmark_custom.dart b/lib/page/benchmarks/delete_benchmark_custom.dart index 812cfb8..8a8c767 100644 --- a/lib/page/benchmarks/delete_benchmark_custom.dart +++ b/lib/page/benchmarks/delete_benchmark_custom.dart @@ -26,23 +26,30 @@ class DeleteBenchmarkCustom extends State 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 = [ @@ -75,29 +82,35 @@ class DeleteBenchmarkCustom extends State 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 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 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); } } diff --git a/lib/page/benchmarks/delete_benchmark_page.dart b/lib/page/benchmarks/delete_benchmark_page.dart index 0955fbf..f0fc100 100644 --- a/lib/page/benchmarks/delete_benchmark_page.dart +++ b/lib/page/benchmarks/delete_benchmark_page.dart @@ -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; } diff --git a/lib/page/benchmarks/edit_benchmark_custom.dart b/lib/page/benchmarks/edit_benchmark_custom.dart index 0b151a1..c933ecf 100644 --- a/lib/page/benchmarks/edit_benchmark_custom.dart +++ b/lib/page/benchmarks/edit_benchmark_custom.dart @@ -26,23 +26,30 @@ class EditBenchmarkCustom extends State 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 = [ @@ -75,29 +82,35 @@ class EditBenchmarkCustom extends State 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 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 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 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 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; } } diff --git a/lib/page/benchmarks/edit_benchmark_page.dart b/lib/page/benchmarks/edit_benchmark_page.dart index 32f631a..9fca871 100644 --- a/lib/page/benchmarks/edit_benchmark_page.dart +++ b/lib/page/benchmarks/edit_benchmark_page.dart @@ -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; } diff --git a/lib/page/benchmarks/list_benchmark_custom.dart b/lib/page/benchmarks/list_benchmark_custom.dart index c1701f0..5a54c4b 100644 --- a/lib/page/benchmarks/list_benchmark_custom.dart +++ b/lib/page/benchmarks/list_benchmark_custom.dart @@ -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 { final globalData = GlobalData(); + late Future _futureDbData; AttendedPage? attendedPage; final _fieldData = _FieldData(); final GlobalKey _formKey = GlobalKey(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( + 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( @@ -37,32 +82,57 @@ class ListBenchmarkCustom extends State { 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( @@ -77,33 +147,52 @@ class ListBenchmarkCustom extends State { DataColumn( label: Text(i18n.tr('Birthday')), ), + DataColumn( + label: Text(i18n.tr('Active')), + ), + DataColumn( + label: Text(i18n.tr('Income')), + ), ], - rows: rows, + rows: rows as List, ); - 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 { 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 { } class _FieldData { + int thePageSize = 10; + int theOffset = 0; String text = ''; + String count = ''; } diff --git a/lib/page/benchmarks/list_benchmark_page.dart b/lib/page/benchmarks/list_benchmark_page.dart index d65dcc5..7ab8115 100644 --- a/lib/page/benchmarks/list_benchmark_page.dart +++ b/lib/page/benchmarks/list_benchmark_page.dart @@ -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; } diff --git a/lib/page/roles/create_role_custom.dart b/lib/page/roles/create_role_custom.dart index 3a38b41..87c2b0e 100644 --- a/lib/page/roles/create_role_custom.dart +++ b/lib/page/roles/create_role_custom.dart @@ -108,12 +108,15 @@ class CreateRoleCustom extends State with MessageLine { @override void dispose() { + helperDummyUsage(DataType.int); + globalWidgetDummyUsage(); nameController.dispose(); super.dispose(); } } class _FieldData { + bool isFromBackend = false; String name = ''; diff --git a/lib/page/roles/create_role_page.dart b/lib/page/roles/create_role_page.dart index 4ec6c88..927f061 100644 --- a/lib/page/roles/create_role_page.dart +++ b/lib/page/roles/create_role_page.dart @@ -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; } diff --git a/lib/page/roles/edit_role_custom.dart b/lib/page/roles/edit_role_custom.dart index fe67f51..4108b6a 100644 --- a/lib/page/roles/edit_role_custom.dart +++ b/lib/page/roles/edit_role_custom.dart @@ -27,11 +27,20 @@ class EditRoleCustom extends State 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( @@ -92,7 +101,11 @@ class EditRoleCustom extends State 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 with MessageLine { @override void dispose() { + helperDummyUsage(DataType.int); + globalWidgetDummyUsage(); nameController.dispose(); super.dispose(); } } class _FieldData { + bool isFromBackend = false; String name = ''; void fromMap(Map map) { diff --git a/lib/page/roles/edit_role_page.dart b/lib/page/roles/edit_role_page.dart index ef15ac4..b96e274 100644 --- a/lib/page/roles/edit_role_page.dart +++ b/lib/page/roles/edit_role_page.dart @@ -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; } diff --git a/lib/page/roles/list_role_custom.dart b/lib/page/roles/list_role_custom.dart index 32e6ed8..69af556 100644 --- a/lib/page/roles/list_role_custom.dart +++ b/lib/page/roles/list_role_custom.dart @@ -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 { final globalData = GlobalData(); + late Future _futureDbData; AttendedPage? attendedPage; final _fieldData = _FieldData(); final GlobalKey _formKey = @@ -22,6 +25,44 @@ class ListRoleCustom extends State { 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( + 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( @@ -43,56 +84,47 @@ class ListRoleCustom extends State { 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( label: Text(i18n.tr('d;Name')), ), ], - rows: rows, + rows: rows as List, ); - 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 { } class _FieldData { + int thePageSize = 10; + int theOffset = 0; String text = ''; } diff --git a/lib/page/roles/list_role_page.dart b/lib/page/roles/list_role_page.dart index b42f052..9a0b0a6 100644 --- a/lib/page/roles/list_role_page.dart +++ b/lib/page/roles/list_role_page.dart @@ -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; } diff --git a/lib/page/structures/create_structure_custom.dart b/lib/page/structures/create_structure_custom.dart index c86d7af..f05c1ee 100644 --- a/lib/page/structures/create_structure_custom.dart +++ b/lib/page/structures/create_structure_custom.dart @@ -62,7 +62,7 @@ class CreateStructureCustom extends State 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 with MessageLine @override void dispose() { + helperDummyUsage(DataType.int); + globalWidgetDummyUsage(); scopeController.dispose(); nameController.dispose(); valueController.dispose(); @@ -146,6 +148,7 @@ class CreateStructureCustom extends State 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; } } diff --git a/lib/page/structures/create_structure_page.dart b/lib/page/structures/create_structure_page.dart index 88aa0fb..9be367a 100644 --- a/lib/page/structures/create_structure_page.dart +++ b/lib/page/structures/create_structure_page.dart @@ -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; } diff --git a/lib/page/structures/delete_structure_custom.dart b/lib/page/structures/delete_structure_custom.dart index fc775c3..baad392 100644 --- a/lib/page/structures/delete_structure_custom.dart +++ b/lib/page/structures/delete_structure_custom.dart @@ -30,11 +30,20 @@ class DeleteStructureCustom extends State 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 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 with MessageLine @override void dispose() { + helperDummyUsage(DataType.int); + globalWidgetDummyUsage(); scopeController.dispose(); nameController.dispose(); valueController.dispose(); @@ -143,6 +154,7 @@ class DeleteStructureCustom extends State 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); } } diff --git a/lib/page/structures/delete_structure_page.dart b/lib/page/structures/delete_structure_page.dart index 2bdedf3..1d89c7a 100644 --- a/lib/page/structures/delete_structure_page.dart +++ b/lib/page/structures/delete_structure_page.dart @@ -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; } diff --git a/lib/page/structures/edit_structure_custom.dart b/lib/page/structures/edit_structure_custom.dart index 58fe2a8..11bd3d0 100644 --- a/lib/page/structures/edit_structure_custom.dart +++ b/lib/page/structures/edit_structure_custom.dart @@ -30,11 +30,20 @@ class EditStructureCustom extends State 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 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 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 with MessageLine { @override void dispose() { + helperDummyUsage(DataType.int); + globalWidgetDummyUsage(); scopeController.dispose(); nameController.dispose(); valueController.dispose(); @@ -142,6 +157,7 @@ class EditStructureCustom extends State 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 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; } } diff --git a/lib/page/structures/edit_structure_page.dart b/lib/page/structures/edit_structure_page.dart index c151f66..516fdff 100644 --- a/lib/page/structures/edit_structure_page.dart +++ b/lib/page/structures/edit_structure_page.dart @@ -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; } diff --git a/lib/page/structures/list_structure_custom.dart b/lib/page/structures/list_structure_custom.dart index 28c0a64..1380aad 100644 --- a/lib/page/structures/list_structure_custom.dart +++ b/lib/page/structures/list_structure_custom.dart @@ -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 { final globalData = GlobalData(); + late Future _futureDbData; AttendedPage? attendedPage; final _fieldData = _FieldData(); final GlobalKey _formKey = @@ -22,6 +25,44 @@ class ListStructureCustom extends State { 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( + 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( @@ -43,56 +84,47 @@ class ListStructureCustom extends State { 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( label: Text(i18n.tr('d;Scope;Name;Value;Position')), ), ], - rows: rows, + rows: rows as List, ); - 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 { } class _FieldData { + int thePageSize = 10; + int theOffset = 0; String text = ''; } diff --git a/lib/page/structures/list_structure_page.dart b/lib/page/structures/list_structure_page.dart index e286fbc..ad80fff 100644 --- a/lib/page/structures/list_structure_page.dart +++ b/lib/page/structures/list_structure_page.dart @@ -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; } diff --git a/lib/page/users/create_user_custom.dart b/lib/page/users/create_user_custom.dart index 615970d..d01ac15 100644 --- a/lib/page/users/create_user_custom.dart +++ b/lib/page/users/create_user_custom.dart @@ -141,6 +141,8 @@ class CreateUserCustom extends State with MessageLine { @override void dispose() { + helperDummyUsage(DataType.int); + globalWidgetDummyUsage(); nameController.dispose(); displayNameController.dispose(); emailController.dispose(); @@ -149,6 +151,7 @@ class CreateUserCustom extends State 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; } } diff --git a/lib/page/users/create_user_page.dart b/lib/page/users/create_user_page.dart index 14e69e2..5fed6f1 100644 --- a/lib/page/users/create_user_page.dart +++ b/lib/page/users/create_user_page.dart @@ -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; } diff --git a/lib/page/users/delete_user_custom.dart b/lib/page/users/delete_user_custom.dart index 64f531d..a1df6ea 100644 --- a/lib/page/users/delete_user_custom.dart +++ b/lib/page/users/delete_user_custom.dart @@ -33,11 +33,20 @@ class DeleteUserCustom extends State 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 with MessageLine { @override void dispose() { + helperDummyUsage(DataType.int); + globalWidgetDummyUsage(); nameController.dispose(); displayNameController.dispose(); emailController.dispose(); @@ -146,6 +157,7 @@ class DeleteUserCustom extends State 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); } } diff --git a/lib/page/users/delete_user_page.dart b/lib/page/users/delete_user_page.dart index 85cbfc9..111ddc4 100644 --- a/lib/page/users/delete_user_page.dart +++ b/lib/page/users/delete_user_page.dart @@ -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; } diff --git a/lib/page/users/edit_user_custom.dart b/lib/page/users/edit_user_custom.dart index 70a9f50..374bc2a 100644 --- a/lib/page/users/edit_user_custom.dart +++ b/lib/page/users/edit_user_custom.dart @@ -33,11 +33,20 @@ class EditUserCustom extends State 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 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 with MessageLine { @override void dispose() { + helperDummyUsage(DataType.int); + globalWidgetDummyUsage(); nameController.dispose(); displayNameController.dispose(); emailController.dispose(); @@ -145,6 +160,7 @@ class EditUserCustom extends State 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 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; } } diff --git a/lib/page/users/edit_user_page.dart b/lib/page/users/edit_user_page.dart index 1179663..080f1d7 100644 --- a/lib/page/users/edit_user_page.dart +++ b/lib/page/users/edit_user_page.dart @@ -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; } diff --git a/lib/page/users/list_user_custom.dart b/lib/page/users/list_user_custom.dart index b6b8727..dd50e61 100644 --- a/lib/page/users/list_user_custom.dart +++ b/lib/page/users/list_user_custom.dart @@ -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 { final globalData = GlobalData(); + late Future _futureDbData; AttendedPage? attendedPage; final _fieldData = _FieldData(); final GlobalKey _formKey = @@ -22,6 +25,44 @@ class ListUserCustom extends State { 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( + 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 { 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( @@ -95,30 +122,35 @@ class ListUserCustom extends State { label: Text(i18n.tr('Role')), ), ], - rows: rows, + rows: rows as List, ); - 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 { } class _FieldData { + int thePageSize = 10; + int theOffset = 0; String text = ''; int role = 0; } diff --git a/lib/page/users/list_user_page.dart b/lib/page/users/list_user_page.dart index 1938d3d..d36e2bf 100644 --- a/lib/page/users/list_user_page.dart +++ b/lib/page/users/list_user_page.dart @@ -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; } diff --git a/lib/persistence/file_persistence.dart b/lib/persistence/file_persistence.dart index 2159ee3..66b6e38 100644 --- a/lib/persistence/file_persistence.dart +++ b/lib/persistence/file_persistence.dart @@ -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 { diff --git a/lib/persistence/persistence.dart b/lib/persistence/persistence.dart index cf7794a..ca8c0d5 100644 --- a/lib/persistence/persistence.dart +++ b/lib/persistence/persistence.dart @@ -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 { diff --git a/lib/persistence/rest_persistence.dart b/lib/persistence/rest_persistence.dart index b870ed6..a3a1872 100644 --- a/lib/persistence/rest_persistence.dart +++ b/lib/persistence/rest_persistence.dart @@ -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(answer.length, 8))}'); diff --git a/lib/setting/drawer_exhibition.dart b/lib/setting/drawer_exhibition.dart index 1b46ee3..da7b246 100644 --- a/lib/setting/drawer_exhibition.dart +++ b/lib/setting/drawer_exhibition.dart @@ -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( + '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'), diff --git a/lib/widget/attended_page.dart b/lib/widget/attended_page.dart index 936318b..08b577f 100644 --- a/lib/widget/attended_page.dart +++ b/lib/widget/attended_page.dart @@ -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 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 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 = []; + 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 getRows( - {required String columnList, - required String what, - required Map parameters, + {required DbData dbData, + required String columnList, required Function() onDone, required String routeEdit, required BuildContext context}) { List? 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 = []; - dbData.recordList!.forEach((record) { - final cells = []; - 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 = []; + dbData.recordList!.forEach((record) { + final cells = []; + 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 ?? []; } diff --git a/lib/widget/attended_widget.dart b/lib/widget/attended_widget.dart index cf9cebb..dc9a2bc 100644 --- a/lib/widget/attended_widget.dart +++ b/lib/widget/attended_widget.dart @@ -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> Function( PropertyMetaData propertyMetaData); /// Manages a widget controlled by the meta data. -abstract class AttendedWidget { +abstract class AttendedWidget { + T? value; final AttendedPage attendedPage; final PropertyMetaData propertyMetaData; @@ -15,14 +17,14 @@ abstract class AttendedWidget { Widget widgetOf(); } -class ComboboxAttended extends FieldAttended { - T? value; +class ComboboxAttended extends FieldAttended { MenuItemBuilder menuItemBuilder = (PropertyMetaData _) => >[]; // MenuItemBuilder menuItemBuilder = // ((PropertyMetaData _) => >[]) as MenuItemBuilder; ComboboxAttended(PropertyMetaData propertyMetaData, AttendedPage attendedPage) : super(propertyMetaData, attendedPage); + @override Widget widgetOf() { final rc = DropdownButtonFormField( value: value, @@ -35,7 +37,7 @@ class ComboboxAttended extends FieldAttended { } } -abstract class FieldAttended extends AttendedWidget { +abstract class FieldAttended extends AttendedWidget { 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 { + 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 extends FieldAttended { final controller = TextEditingController(); int minLines = 1; int maxLines = 1; - String value = ''; TextAttended(PropertyMetaData propertyMetaData, AttendedPage attendedPage) : super(propertyMetaData, attendedPage); /// Returns a concrete widget representing the instance. + @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), ); diff --git a/lib/widget/widget_form.dart b/lib/widget/widget_form.dart index 9a86b35..7be3d59 100644 --- a/lib/widget/widget_form.dart +++ b/lib/widget/widget_form.dart @@ -44,7 +44,7 @@ class WidgetForm { } widgets.add(element.widget); }); - rc = Column(children: widgets); + rc = ListView(children: widgets); } else { int position = 0; final List childrenColumn = []; diff --git a/metatool/bin/page_generator.dart b/metatool/bin/page_generator.dart index d9ddf20..cdb17da 100644 --- a/metatool/bin/page_generator.dart +++ b/metatool/bin/page_generator.dart @@ -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 { final globalData = GlobalData(); + late Future _futureDbData; AttendedPage? attendedPage; final _fieldData = _FieldData(); final GlobalKey _formKey = @@ -74,6 +82,44 @@ class ListUserCustom extends State { #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( + 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 = [ #FORM_ITEMS FormItem( @@ -88,52 +134,43 @@ class ListUserCustom extends State { 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: [ #TABLE_HEADER ], - rows: rows, + rows: rows as List, ); - 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 { } class _FieldData { + int thePageSize = 10; + int theOffset = 0; #DEF_FIELDS} '''; @@ -245,11 +284,14 @@ class EditUserCustom extends State 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 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 = []; 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 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"; diff --git a/metatool/test/helper_test.dart b/metatool/test/helper_test.dart index f01c0e8..df176d6 100644 --- a/metatool/test/helper_test.dart +++ b/metatool/test/helper_test.dart @@ -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 index 0000000..fa5c35e --- /dev/null +++ b/metatool/test/validators_test.dart @@ -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'); + }); + }); +} diff --git a/rest_server/lib/rest_server.dart b/rest_server/lib/rest_server.dart index 7a45e64..4f6228e 100644 --- a/rest_server/lib/rest_server.dart +++ b/rest_server/lib/rest_server.dart @@ -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); -- 2.39.5