import 'defines.dart';
+/// Handles the [input] as a pattern in a filter list:
+///
+/// Appends '*' if input does not end with a '*'.
+/// Replaces the jokers '*' and '?' with the SQL equivalents '%' and '_'.
+///
+/// Returns a SQL pattern string.
+String asPattern(String input) {
+ final rc = input.isEmpty || input.endsWith('*') ? input : input + '*';
+ return rc.replaceAll('*', '%').replaceAll('?', '_');
+}
+
+/// Converts a [name] to a camel case string.
+///
+/// A camelCase string starts with a uppercase character.
String toCamelCase(String name) {
final rc = name.isEmpty ? '' : (name[0].toUpperCase() + name.substring(1));
return rc;
class ListPageMetaData extends PageMetaData {
final String tableHeaders;
final String tableColumns;
+ final String whereCondition;
+ final String orderBy;
+ final String selectItems;
+ final String joinItems;
+
+ /// Constructor.
+ ///
+ /// [name] is the page name. It must be unique over all pages in the module.
+ ///
+ /// [fields]: the field in the filter section.
+ ///
+ /// [tableColumns]: a semicolon delimited list of table columns used in the
+ /// table displaying the filtered records. Example: 'user_id;user_name;role'
+ ///
+ /// [tableHeaders]: an auto delimited list of headers for the table. Example:
+ /// ';Id;Name;Role'. Auto delimited: the first char defines the delimiter.
+ ///
+ /// [globalComboBoxes]: If there are filter combo boxes that can be constructed
+ /// by "global methods" the should be listed here.
+ /// Example: 'comboRoles;comboUsers'
+ ///
+ /// [whereCondition]: the filter condition in the SQL statement. Example:
+ /// '(:text IS NULL OR user_name like :text OR user_displayname like :text)'
+ ///
+ /// [orderBy]: the default order by clause. Example: 'changed desc,user_id'
+ ///
+ /// [selectItems]: additional select entries. Must end with ','. Example:
+ /// '''(SELECT count(*) FROM sessions WHERE sessions.user_id=t0.user_id) as count,
+ /// t1.role_created as roledate,'''
+ ///
+ /// [joinItems]: additional joins. Convention: use table ids different from
+ /// 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'''
ListPageMetaData(String label,
{String name = '',
required List<WidgetMetaData> fields,
required this.tableColumns,
required this.tableHeaders,
- String globalComboBoxes = ''})
+ String globalComboBoxes = '',
+ this.whereCondition = '',
+ this.orderBy = '',
+ this.selectItems = '',
+ this.joinItems = ''})
: super(label, PageType.list,
name: name, fields: fields, globalComboBoxes: globalComboBoxes);
}
ListPageMetaData(
'Users Overview',
fields: [
- PropertyMetaData('text', i18n.tr('Text'), DataType.string, '',
+ PropertyMetaData(
+ 'text', i18n.tr('Text'), DataType.string, ':pattern:',
size: 64),
PropertyMetaData('role', i18n.tr('Role'), DataType.reference, '',
displayType: DisplayType.combobox,
foreignKey: 'roles.role_id;role_name;role'),
],
- tableColumns: 'user_id;user_displayname;user_email;role',
- tableHeaders: i18n.tr('Id;Display Name;Email;Role'),
+ tableColumns: 'user_id;user_name;user_displayname;user_email;role',
+ tableHeaders: i18n.tr(';Id;Name;Display Name;Email;Role'),
globalComboBoxes: 'comboRoles',
+ whereCondition: '''(:text='' OR user_name like :text
+ OR user_displayname like :text OR user_email like :text)
+ AND (:role=0 OR :role=user_role)''',
+ orderBy: 'user_id',
),
]);
@override
// It will never overridden by the meta_tool.
import 'package:flutter/material.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 '../../services/global_widget.dart';
import 'list_user_page.dart';
final i18n = I18N();
screenWidth: attendedPage!.pageStates.screenWidth,
padding: padding))));
final rows = attendedPage!.getRows(
- columnList: 'user_id;user_displayname;user_email;role',
+ columnList: 'user_id;user_name;user_displayname;user_email;role',
what: 'query',
parameters: {
'module': 'Users',
'sql': 'list',
'offset': '0',
'size': '10',
- ':text': _fieldData.text,
+ ':text': asPattern(_fieldData.text),
':role': _fieldData.role.toString(),
},
onDone: () => setState(() => 1),
DataColumn(
label: Text(i18n.tr('Id')),
),
+ DataColumn(
+ label: Text(i18n.tr('Name')),
+ ),
DataColumn(
label: Text(i18n.tr('Display Name')),
),
return rc;
}
- void search() {
- //@ToDo
+ @override
+ void dispose() {
+ textController.dispose();
+ super.dispose();
}
+
@override
void initState() {
super.initState();
}
- @override
- void dispose() {
- textController.dispose();
- super.dispose();
+ void search() {
+ attendedPage!.pageStates.dbDataState.clear();
+ if (_formKey.currentState!.validate()) {
+ _formKey.currentState!.save();
+ setState(() => 1);
+ }
}
}
// It will never overridden by the meta_tool.
import 'package:flutter/material.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 '../../services/global_widget.dart';
import 'list_user_page.dart';
final i18n = I18N();
return rc;
}
- void search() {
- //@ToDo
+ @override
+ void dispose() {
+#DISPOSE_CONTROLLER super.dispose();
}
+
@override
void initState() {
super.initState();
}
- @override
- void dispose(){
-#DISPOSE_CONTROLLER super.dispose();
+
+ void search() {
+ attendedPage!.pageStates.dbDataState.clear();
+ if (_formKey.currentState!.validate()) {
+ _formKey.currentState!.save();
+ setState(() => 1);
+ }
}
}
#INIT_COMBO#LOAD_RECORD#ASSIGN_CONTROLLER final formItems = <FormItem>[
#FORM_ITEMS FormItem(
ElevatedButton(
- onPressed: () => #ACTION2(), child: Text(i18n.tr('#BUTTON'))),
+ onPressed: () => #ACTION2(),
+ child: Text(i18n.tr('#BUTTON'))),
weight: 8,
gapAbove: 2 * padding),
FormItem(
formItems,
screenWidth: attendedPage!.pageStates.screenWidth,
padding: padding,
- ))))));
+ ))))));
return rc;
}
};
#SET_PRIMARY#CALL_TO_MAP globalData.restPersistence!
.store(what: 'store', map: parameters)
- .then((answer) {
-#STORAGE_DONE
- });
+ .then((answer) {#STORAGE_DONE});
}
void #ACTION2() {
#FROM_MAP#TO_MAP}
''';
static final templateStorageDoneCreate =
- ''' if (answer.startsWith('id:')) {
+ '''
+
+ if (answer.startsWith('id:')) {
final id = int.tryParse(answer.substring(3));
if (id == null || id == 0) {
setError(i18n.tr('Saving data failed: \$answer'));
attendedPage!.pageStates.dbDataState.clear();
globalData.navigate(context, '/#MODULE/edit;\$id');
}
- }''';
+ }
+ ''';
static final templateStorageDoneEdit = ' setState(() => 1);';
static final templateStorageDoneDelete =
- " globalData.navigate(context, '/#MODULE/list');";
+ "\n globalData.navigate(context, '/#MODULE/list');\n ";
static final templateFromMap = ''' void fromMap(Map<String, dynamic> map) {
#BODY_FROM }
''';
PageGenerator(BaseLogger logger) : super(logger);
+ /// Creates the part for initializing the [TextEditingController]s.
+ ///
+ /// "xxxController.text = _fieldData.yyy".
String buildAssignControllers(PageMetaData page) {
final buffer = StringBuffer();
for (var field in page.fields) {
return buffer.toString();
}
+ /// Creates a button with [onPressed] and [label].
+ ///
+ /// [indent]: the additional indention of each created line.
String buildButton(
{required String onPressed, required String label, String indent = ''}) {
final rc = '''^ElevatedButton(
return rc;
}
+ /// Creates the text controllers of the [page].
+ ///
+ /// Each text field get its own controller.
String buildDefinitionControllers(PageMetaData page) {
final buffer = StringBuffer();
for (var field in page.fields) {
return buffer.toString();
}
+ /// Creates the field definitions of the [page] in the class _FieldData.
String buildDefinitionFields(PageMetaData page) {
final buffer = StringBuffer();
for (var field in page.fields) {
return buffer.toString();
}
+ /// Creates the dispose statements of the [page] for the [TextEditingController]s.
+ ///
+ /// Each text field has its own controller.
String buildDisposeControllers(PageMetaData page) {
final buffer = StringBuffer();
for (var field in page.fields) {
return buffer.toString();
}
+ /// Creates the widget of the [field] depending on the widget type.
+ ///
+ /// [indent]: the additional indention of each created line.
+ ///
+ /// [withController]: if true the text field uses a [TextEditingController].
String buildField(PropertyMetaData field,
{String indent = '', bool withController = false}) {
final name = field.name;
return rc;
}
+ /// Creates the form items of the [page].
+ ///
+ /// A form item stores the data for creating automatically a multi column
+ /// form.
+ ///
+ /// [withController]: if true the text field uses a [TextEditingController].
String buildFormItems(PageMetaData page, {bool withController = false}) {
final buffer = StringBuffer();
for (var field in page.fields) {
return buffer.toString();
}
+ /// Creates the method "fromMap" in the class _FieldData for the [page].
String buildFromMap(PageMetaData page) {
String? rc;
if (page.pageType == PageType.edit || page.pageType == PageType.delete) {
return rc ?? '';
}
+ /// Creates the part to initialize the combo boxes for the [page].
String buildInitializeComboBoxes(PageMetaData page) {
final buffer = StringBuffer();
if (page.globalComboBoxes.isNotEmpty) {
return buffer.toString();
}
+ /// Creates the part for loading the field data from the backend for [page].
String buildLoadRecord(PageMetaData page) {
String? rc;
if (page.pageType == PageType.edit || page.pageType == PageType.delete) {
return rc ?? '';
}
+ /// Creates the parameter definition of the [page].
+ ///
+ /// The parameters will be used for storing the record at the backend.
String buildParamDefinitions(PageMetaData page,
{bool withController = false}) {
final buffer = StringBuffer();
(field as PropertyMetaData).displayType != DisplayType.text
? '.toString()'
: '';
- buffer.writeln(" ':$name': _fieldData.$name$suffix,");
+ var value = '_fieldData.$name$suffix';
+ if (field is PropertyMetaData && field.hasOption(':pattern')){
+ value = 'asPattern($value)';
+ }
+ buffer.writeln(" ':$name': $value,");
}
return buffer.toString();
}
+ /// Builds the table header of the [page].
String buildTableHeader(ListPageMetaData page) {
final buffer = StringBuffer();
- for (var item in page.tableHeaders.split(';')) {
- buffer.writeln(''' DataColumn(
+ final headers = page.tableHeaders;
+ if (headers.length < 2) {
+ logger.error('tableHeaders is too short: $headers');
+ } else {
+ // tableHeaders is auto delimited: first char defines the delimiter
+ for (var item in headers.substring(1).split(headers[0])) {
+ buffer.writeln(''' DataColumn(
label: Text(i18n.tr('$item')),
),''');
+ }
}
return buffer.toString();
}
+ /// Creates the method "toMap" in class _FieldData of the [page].
String buildToMap(PageMetaData page) {
String? rc;
if (page.pageType == PageType.edit || page.pageType == PageType.create) {
- final buffer = StringBuffer(' void toMap(Map<String, dynamic> map) {\n');
+ final buffer = StringBuffer('\n void toMap(Map<String, dynamic> map) {\n');
if (page.pageType == PageType.edit) {
buffer.writeln(" // please set outside: map[':id'] = primaryKey;");
}
return buffer.toString();
}
+ /// Creates the sections for pages with PageType.list of the [module].
+ String createListSections(ModuleMetaData module) {
+ final buffer = StringBuffer();
+ List<PageMetaData>? listPages;
+ try {
+ listPages = module.pageList
+ .where((element) => element.pageType == PageType.list)
+ .toList(growable: false);
+ } on Exception catch (exc) {
+ logger.error('no page with list type: $exc');
+ listPages = null;
+ }
+ if (listPages != null) {
+ final tableName = module.tableName;
+ for (var page in listPages) {
+ if (page is ListPageMetaData) {
+ var whereCondition = page.whereCondition;
+ if (whereCondition.endsWith('\n')){
+ whereCondition = whereCondition.substring(0, whereCondition.length - 1);
+ }
+ String selectItems2 = '';
+ var selectItems =
+ page.selectItems.isEmpty ? '' : indent(page.selectItems, ' ');
+ var joins = findReferences(module, (x) => selectItems2 = x);
+ if (page.joinItems.isNotEmpty) {
+ joins += indent(page.joinItems, ' ');
+ if (!joins.endsWith('\n')) {
+ joins += '\n';
+ }
+ }
+ var parameters = '';
+ if (whereCondition.isNotEmpty) {
+ parameters = findParameters(whereCondition, page.fields);
+ whereCondition = ' WHERE\n' + indent(whereCondition, ' ');
+ }
+ final order = page.orderBy.isNotEmpty
+ ? page.orderBy
+ : module.primaryOf()!.columnName;
+ buffer.writeln(page.name + ':');
+ buffer.writeln(''' type: list
+ parameters: [$parameters]
+ order: "$order"
+ sql: "SELECT
+$selectItems t0.*$selectItems2
+ FROM $tableName t0
+$joins$whereCondition ;"''');
+ }
+ }
+ }
+ return buffer.toString();
+ }
+
/// Returns the SQL statements for insert, update, delete...
/// for a given [module].
/// This yaml file is a configuration for the rest_server.
final list = module.propertyList;
final buffer = StringBuffer();
buffer.write('---\n');
+ final lists = createListSections(module);
buffer.write('# DO NOT CHANGE. This file is created by the meta_tool\n');
var sqlText = '''# SQL statements of the module "$moduleName":\n
module: $moduleName
-list:
- type: list
- parameters: []
- sql: "SELECT
- t0.*SELECTS
- FROM $tableName t0
-JOINS
- ;"
-byId:
+${lists}byId:
type: record
parameters: [ ":${list[0].name}" ]
sql: "SELECT * FROM $tableName WHERE ${list[0].columnName}=:${list[0].name};"
update:
type: update
''';
- sqlText = handleReferences(sqlText, module);
buffer.write(sqlText);
var items = module.standardColumns('changedBy');
var parameters = addToBuffer(' parameters: [', maxLength: 80);
buffer.writeln(sql2);
return buffer.toString();
}
+ static final regExprParameters = RegExp(r'(:\w+)');
+ /// Searches the parameters in the [whereCondition] and checks it against
+ /// the [fields].
+ String findParameters(String whereCondition, List<WidgetMetaData> fields) {
+ final names = regExprParameters.allMatches(whereCondition)
+ .map((element) => element.group(1))
+ .toSet().toList();
+ return '"' + names.join('","') + '"';
+ }
- /// Handles the foreign keys in the [sqlText] of the given [module].
- ///
- /// Replaces the placeholders SELECTS and JOINS in [sqlText].
+ /// Creates the joins and select items the foreign keys of the given [module].
///
- /// Returns the modified SQL text.
- String handleReferences(String sqlText, ModuleMetaData module) {
+ /// [storeSelectItems]: a function that stores the select items to the caller.
+ String findReferences(ModuleMetaData module,
+ String Function(String selectItems) storeSelectItems) {
var joins = '';
var selects = '';
var referenceNo = 0;
} else {
++referenceNo;
joins += ' JOIN ${keyParts[0]} t$referenceNo ON '
- 't$referenceNo.${keyParts[1]}=t0.${property.columnName}';
+ 't$referenceNo.${keyParts[1]}=t0.${property.columnName}\n';
selects += ',t$referenceNo.${parts[1]} AS ${parts[2]}';
}
}
}
}
- final rc =
- sqlText.replaceFirst('JOINS', joins).replaceFirst('SELECTS', selects);
+ storeSelectItems(selects);
+ return joins;
+ }
+
+ /// Adds to all lines in the string [lines] a given [indentString].
+ String indent(String lines, String indentString) {
+ final lines2 = lines.split('\n');
+ final rc = lines2.fold<String>(
+ '', (previous, element) => previous + indentString + element + '\n');
return rc;
}
list:
type: list
parameters: []
+ order: "role_id"
sql: "SELECT
t0.*
FROM roles t0
-
;"
byId:
type: record
list:
type: list
parameters: []
+ order: "structure_id"
sql: "SELECT
t0.*
FROM structures t0
-
;"
byId:
type: record
module: Users
list:
type: list
- parameters: []
+ parameters: [":text",":role"]
+ order: "user_id"
sql: "SELECT
t0.*,t1.role_name AS role
FROM users t0
JOIN roles t1 ON t1.role_id=t0.user_role
+ WHERE
+ (:text='' OR user_name like :text
+ OR user_displayname like :text OR user_email like :text)
+ AND (:role=0 OR :role=user_role)
;"
byId:
type: record