Implemented insertion of new items into table
This commit is contained in:
parent
0c14da73df
commit
dfbea7cb6c
4 changed files with 1107 additions and 1 deletions
942
lib/pages/dialogs/open_table_dialog.dart
Normal file
942
lib/pages/dialogs/open_table_dialog.dart
Normal file
|
|
@ -0,0 +1,942 @@
|
||||||
|
import 'package:data_table_2/data_table_2.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_fast_forms/flutter_fast_forms.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:omni_datetime_picker/omni_datetime_picker.dart';
|
||||||
|
import 'package:recase/recase.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:tuuli_api/tuuli_api.dart';
|
||||||
|
import 'package:tuuli_app/api_controller.dart';
|
||||||
|
import 'package:tuuli_app/models/db_column_definition.dart';
|
||||||
|
import 'package:tuuli_app/models/user_definition.dart';
|
||||||
|
import 'package:tuuli_app/utils.dart';
|
||||||
|
import 'package:tuuli_app/widgets/data_input_dialog.dart';
|
||||||
|
|
||||||
|
class OpenTableController extends GetxController {
|
||||||
|
final TableDefinition table;
|
||||||
|
|
||||||
|
OpenTableController({required this.table});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
|
refreshTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
final _userCache = <int, UserDefinition?>{}.obs;
|
||||||
|
UserDefinition? getUserFromCache(int id) {
|
||||||
|
return _userCache[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
void putUserInCache(UserDefinition user) {
|
||||||
|
_userCache[user.id] = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
final _assetsCache = <int, Asset?>{}.obs;
|
||||||
|
Asset? getAssetFromCache(int id) {
|
||||||
|
return _assetsCache[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
void putAssetInCache(Asset asset) {
|
||||||
|
_assetsCache[asset.id] = asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
final _tableData = <Map<String, dynamic>>[].obs;
|
||||||
|
List<Map<String, dynamic>> get tableData => _tableData;
|
||||||
|
|
||||||
|
final _newRowData = <String, dynamic>{}.obs;
|
||||||
|
Map<String, dynamic> get newRowData => _newRowData;
|
||||||
|
void setNewRowData(String key, dynamic value) {
|
||||||
|
_newRowData[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearNewRowData() {
|
||||||
|
_newRowData.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshTableData() async {
|
||||||
|
try {
|
||||||
|
final resp = await ApiController.to.apiClient.getItemsFromTable(
|
||||||
|
tableName: table.tableName,
|
||||||
|
itemsSelector: const ItemsSelector(
|
||||||
|
fields: [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
where: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final respData = resp.data;
|
||||||
|
if (respData == null) {
|
||||||
|
throw Exception("No data in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
_tableData.clear();
|
||||||
|
_tableData.addAll(respData);
|
||||||
|
} on DioError catch (e) {
|
||||||
|
final respData = e.response?.data;
|
||||||
|
if (respData != null) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to get table data",
|
||||||
|
"${respData['error']}",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to get table data",
|
||||||
|
"$e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to get table data",
|
||||||
|
"$e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UserDefinition?> showUserPicker() async {
|
||||||
|
final username = "".obs;
|
||||||
|
|
||||||
|
final user = await Get.dialog<UserDefinition>(
|
||||||
|
AlertDialog(
|
||||||
|
title: const Text("Select user"),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
onChanged: (value) {
|
||||||
|
username.value = value;
|
||||||
|
},
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Username",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Obx(
|
||||||
|
() => FutureBuilder<List<UserDefinition>>(
|
||||||
|
future: () async {
|
||||||
|
if (username.value.isEmpty) {
|
||||||
|
return <UserDefinition>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final resp =
|
||||||
|
await ApiController.to.apiClient.getItemsFromTable(
|
||||||
|
tableName: "users",
|
||||||
|
itemsSelector: ItemsSelector(
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
],
|
||||||
|
where: [
|
||||||
|
ColumnConditionCompat(
|
||||||
|
column: "username",
|
||||||
|
operator_:
|
||||||
|
ColumnConditionCompatOperator.contains,
|
||||||
|
value: username.value,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
final respData = resp.data;
|
||||||
|
if (respData == null) {
|
||||||
|
throw Exception("No data in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
return respData
|
||||||
|
.map((e) => UserDefinition(
|
||||||
|
id: e["id"],
|
||||||
|
username: e["username"],
|
||||||
|
password: "",
|
||||||
|
accessToken: "",
|
||||||
|
))
|
||||||
|
.toList(growable: false);
|
||||||
|
}(),
|
||||||
|
initialData: const [],
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Text("${snapshot.error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
final users = snapshot.data!;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (final user in users)
|
||||||
|
ListTile(
|
||||||
|
title: Text(user.username),
|
||||||
|
onTap: () {
|
||||||
|
Get.back(result: user);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back(result: false);
|
||||||
|
},
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Asset?> showAssetPicker() async {
|
||||||
|
final name = "".obs;
|
||||||
|
|
||||||
|
final asset = await Get.dialog<Asset>(
|
||||||
|
AlertDialog(
|
||||||
|
title: const Text("Select asset"),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
onChanged: (value) {
|
||||||
|
name.value = value;
|
||||||
|
},
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Filename",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Obx(
|
||||||
|
() => FutureBuilder<List<Asset>>(
|
||||||
|
future: () async {
|
||||||
|
if (name.value.isEmpty) {
|
||||||
|
return <Asset>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final resp =
|
||||||
|
await ApiController.to.apiClient.getItemsFromTable(
|
||||||
|
tableName: "assets",
|
||||||
|
itemsSelector: ItemsSelector(
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
],
|
||||||
|
where: [
|
||||||
|
ColumnConditionCompat(
|
||||||
|
column: "name",
|
||||||
|
operator_:
|
||||||
|
ColumnConditionCompatOperator.contains,
|
||||||
|
value: name.value,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
final respData = resp.data;
|
||||||
|
if (respData == null) {
|
||||||
|
throw Exception("No data in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
return respData
|
||||||
|
.map((e) => Asset(
|
||||||
|
id: e["id"],
|
||||||
|
name: e["name"],
|
||||||
|
description: e["description"],
|
||||||
|
fid: "",
|
||||||
|
tags: "",
|
||||||
|
))
|
||||||
|
.toList(growable: false);
|
||||||
|
}(),
|
||||||
|
initialData: const [],
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Text("${snapshot.error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assets = snapshot.data!;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
for (final asset in assets)
|
||||||
|
ListTile(
|
||||||
|
title: Text(asset.name),
|
||||||
|
subtitle: Text(asset.description),
|
||||||
|
onTap: () {
|
||||||
|
Get.back(result: asset);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back(result: null);
|
||||||
|
},
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addNewRow() async {
|
||||||
|
try {
|
||||||
|
final resp = await ApiController.to.apiClient.createItem(
|
||||||
|
tableName: table.tableName,
|
||||||
|
itemDefinition: convertToPayload(newRowData),
|
||||||
|
);
|
||||||
|
|
||||||
|
final respData = resp.data;
|
||||||
|
if (respData == null) {
|
||||||
|
throw Exception("No data in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
clearNewRowData();
|
||||||
|
refreshTableData();
|
||||||
|
} on DioError catch (e) {
|
||||||
|
final respData = e.response?.data;
|
||||||
|
if (respData != null) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to get table data",
|
||||||
|
"${respData['error']}",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to get table data",
|
||||||
|
"$e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to get table data",
|
||||||
|
"$e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateItem(
|
||||||
|
Map<String, dynamic> originalItem,
|
||||||
|
String columnName,
|
||||||
|
dynamic data,
|
||||||
|
) async {
|
||||||
|
final idCol = table.parsedColumns
|
||||||
|
.firstWhereOrNull((e) => e is PrimarySerialColumnDefinition);
|
||||||
|
try {
|
||||||
|
final resp = await ApiController.to.apiClient.updateItemInTable(
|
||||||
|
tableName: table.tableName,
|
||||||
|
itemUpdate: ItemUpdate(
|
||||||
|
oldItem: idCol == null
|
||||||
|
? convertToPayload(originalItem)
|
||||||
|
: {
|
||||||
|
idCol.name: originalItem[idCol.name],
|
||||||
|
},
|
||||||
|
item: convertToPayload({columnName: data}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final respData = resp.data;
|
||||||
|
if (respData == null) {
|
||||||
|
throw Exception("No data in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTableData();
|
||||||
|
} on DioError catch (e) {
|
||||||
|
final respData = e.response?.data;
|
||||||
|
if (respData != null) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to update table data",
|
||||||
|
"${respData['error']}",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to update table data",
|
||||||
|
"$e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to update table data",
|
||||||
|
"$e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteItem(Map<String, dynamic> e) async {
|
||||||
|
final idCol = table.parsedColumns
|
||||||
|
.firstWhereOrNull((e) => e is PrimarySerialColumnDefinition);
|
||||||
|
try {
|
||||||
|
final resp = await ApiController.to.apiClient.deleteItemFromTable(
|
||||||
|
tableName: table.tableName,
|
||||||
|
columnConditionCompat: [
|
||||||
|
if (idCol != null)
|
||||||
|
ColumnConditionCompat(
|
||||||
|
column: idCol.name,
|
||||||
|
operator_: ColumnConditionCompatOperator.eq,
|
||||||
|
value: e[idCol.name],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
...convertToPayload(e).entries.map(
|
||||||
|
(e) => ColumnConditionCompat(
|
||||||
|
column: e.key,
|
||||||
|
operator_: ColumnConditionCompatOperator.eq,
|
||||||
|
value: e.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final respData = resp.data;
|
||||||
|
if (respData == null) {
|
||||||
|
throw Exception("No data in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTableData();
|
||||||
|
} on DioError catch (e) {
|
||||||
|
final respData = e.response?.data;
|
||||||
|
if (respData != null) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to update table data",
|
||||||
|
"${respData['error']}",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to update table data",
|
||||||
|
"$e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
"Error trying to update table data",
|
||||||
|
"$e",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OpenTableDialog extends GetView<OpenTableController> {
|
||||||
|
const OpenTableDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text(controller.table.tableName.pascalCase),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.refreshTableData(),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Obx(
|
||||||
|
() => DataTable2(
|
||||||
|
columns: [
|
||||||
|
for (final col in controller.table.parsedColumns)
|
||||||
|
DataColumn(
|
||||||
|
label: Text(
|
||||||
|
col.name.pascalCase,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const DataColumn(label: Text("Actions")),
|
||||||
|
],
|
||||||
|
empty: const Text("No data"),
|
||||||
|
rows: [
|
||||||
|
DataRow(
|
||||||
|
cells: [
|
||||||
|
for (final col in controller.table.parsedColumns)
|
||||||
|
if (col is PrimarySerialColumnDefinition)
|
||||||
|
const DataCell(
|
||||||
|
Text(
|
||||||
|
"AUTO",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (col is TextColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Obx(
|
||||||
|
() => TextField(
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: controller.newRowData[col.name] ?? ""),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label: Text(col.name.pascalCase),
|
||||||
|
),
|
||||||
|
onChanged: (value) => controller.setNewRowData(
|
||||||
|
col.name,
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (col is BooleanColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Obx(
|
||||||
|
() => Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: controller.newRowData[col.name] ?? false,
|
||||||
|
onChanged: (value) => controller.setNewRowData(
|
||||||
|
col.name,
|
||||||
|
value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (col is TimestampColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Obx(
|
||||||
|
() => TextField(
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: () {
|
||||||
|
final dt = controller.newRowData[col.name];
|
||||||
|
if (dt == null || dt is! DateTime) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return postgresDateFormat(dt);
|
||||||
|
}(),
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label: Text(col.name.pascalCase),
|
||||||
|
),
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () async {
|
||||||
|
final dt = await showOmniDateTimePicker(
|
||||||
|
context: context,
|
||||||
|
is24HourMode: true,
|
||||||
|
isForce2Digits: true,
|
||||||
|
);
|
||||||
|
if (dt != null) {
|
||||||
|
controller.setNewRowData(
|
||||||
|
col.name,
|
||||||
|
dt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (col is DoubleColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Obx(
|
||||||
|
() => TextField(
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: (controller.newRowData[col.name] as double?)
|
||||||
|
?.toString() ??
|
||||||
|
""),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label: Text(col.name.pascalCase),
|
||||||
|
),
|
||||||
|
onChanged: (value) => controller.setNewRowData(
|
||||||
|
col.name,
|
||||||
|
double.tryParse(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (col is IntegerColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Obx(
|
||||||
|
() => TextField(
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: (controller.newRowData[col.name] as int?)
|
||||||
|
?.toString() ??
|
||||||
|
""),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label: Text(col.name.pascalCase),
|
||||||
|
),
|
||||||
|
onChanged: (value) => controller.setNewRowData(
|
||||||
|
col.name,
|
||||||
|
int.tryParse(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (col is UserRefColumnDefinition)
|
||||||
|
DataCell(Obx(
|
||||||
|
() => TextField(
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: (controller.newRowData[col.name]
|
||||||
|
as UserDefinition?)
|
||||||
|
?.username ??
|
||||||
|
""),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label: Text(col.name.pascalCase),
|
||||||
|
),
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () async {
|
||||||
|
final user = await controller.showUserPicker();
|
||||||
|
if (user == null) return;
|
||||||
|
|
||||||
|
controller.setNewRowData(
|
||||||
|
col.name,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
))
|
||||||
|
else if (col is AssetRefColumnDefinition)
|
||||||
|
DataCell(Obx(
|
||||||
|
() => TextField(
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: (controller.newRowData[col.name] as Asset?)
|
||||||
|
?.name ??
|
||||||
|
""),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label: Text(col.name.pascalCase),
|
||||||
|
),
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () async {
|
||||||
|
final asset = await controller.showAssetPicker();
|
||||||
|
if (asset == null) return;
|
||||||
|
|
||||||
|
controller.setNewRowData(
|
||||||
|
col.name,
|
||||||
|
asset,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
))
|
||||||
|
else
|
||||||
|
DataCell.empty,
|
||||||
|
DataCell(Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.addNewRow();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.clearNewRowData(),
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...controller.tableData.map((e) {
|
||||||
|
return DataRow(
|
||||||
|
cells: [
|
||||||
|
for (final col in controller.table.parsedColumns)
|
||||||
|
if (col is PrimarySerialColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Tooltip(
|
||||||
|
message: e[col.name].toString(),
|
||||||
|
child: Text(
|
||||||
|
e[col.name].toString(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (col is TextColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Tooltip(
|
||||||
|
message: e[col.name].toString(),
|
||||||
|
child: Text(
|
||||||
|
e[col.name].toString(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDoubleTap: () async {
|
||||||
|
final text = await showStringInputDialog(
|
||||||
|
originalValue: e[col.name].toString());
|
||||||
|
if (text != null) {
|
||||||
|
await controller.updateItem(
|
||||||
|
e,
|
||||||
|
col.name,
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (col is BooleanColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Checkbox(
|
||||||
|
value: e[col.name],
|
||||||
|
onChanged: (v) async {
|
||||||
|
await controller.updateItem(
|
||||||
|
e,
|
||||||
|
col.name,
|
||||||
|
v ?? false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (col is TimestampColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
() {
|
||||||
|
final msg = () {
|
||||||
|
final dt = e[col.name];
|
||||||
|
if (dt == null) {
|
||||||
|
return "#error#";
|
||||||
|
}
|
||||||
|
if (dt is String) {
|
||||||
|
final rdt = DateTime.parse(dt);
|
||||||
|
return postgresDateFormat(rdt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return postgresDateFormat(dt);
|
||||||
|
}();
|
||||||
|
return Tooltip(
|
||||||
|
message: msg,
|
||||||
|
child: Text(
|
||||||
|
msg,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}(),
|
||||||
|
onDoubleTap: () async {
|
||||||
|
final dt = await showOmniDateTimePicker(
|
||||||
|
context: context,
|
||||||
|
is24HourMode: true,
|
||||||
|
isForce2Digits: true,
|
||||||
|
);
|
||||||
|
if (dt != null) {
|
||||||
|
await controller.updateItem(
|
||||||
|
e,
|
||||||
|
col.name,
|
||||||
|
dt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (col is DoubleColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Tooltip(
|
||||||
|
message: e[col.name].toString(),
|
||||||
|
child: Text(
|
||||||
|
e[col.name].toString(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDoubleTap: () async {
|
||||||
|
final dblVal = await showDoubleInputDialog(
|
||||||
|
originalValue: e[col.name]);
|
||||||
|
if (dblVal != null) {
|
||||||
|
await controller.updateItem(
|
||||||
|
e,
|
||||||
|
col.name,
|
||||||
|
dblVal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (col is IntegerColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
Tooltip(
|
||||||
|
message: e[col.name].toString(),
|
||||||
|
child: Text(
|
||||||
|
e[col.name].toString(),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDoubleTap: () async {
|
||||||
|
final intVal = await showIntInputDialog(
|
||||||
|
originalValue: e[col.name]);
|
||||||
|
if (intVal != null) {
|
||||||
|
await controller.updateItem(
|
||||||
|
e,
|
||||||
|
col.name,
|
||||||
|
intVal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (col is UserRefColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
FutureBuilder<String>(
|
||||||
|
future: () async {
|
||||||
|
final cachedUser =
|
||||||
|
controller.getUserFromCache(e[col.name]);
|
||||||
|
if (cachedUser != null) {
|
||||||
|
return cachedUser.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = await ApiController.to.apiClient
|
||||||
|
.getItemsFromTable(
|
||||||
|
tableName: "users",
|
||||||
|
itemsSelector: ItemsSelector(
|
||||||
|
fields: ["username"],
|
||||||
|
where: [
|
||||||
|
ColumnConditionCompat(
|
||||||
|
column: "id",
|
||||||
|
operator_: ColumnConditionCompatOperator.eq,
|
||||||
|
value: e[col.name],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ud = user.data;
|
||||||
|
if (ud == null ||
|
||||||
|
ud.isEmpty ||
|
||||||
|
ud.first["username"] == null) {
|
||||||
|
return "#error#";
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.putUserInCache(UserDefinition(
|
||||||
|
id: e[col.name],
|
||||||
|
username: ud.first["username"],
|
||||||
|
password: "",
|
||||||
|
accessToken: "",
|
||||||
|
));
|
||||||
|
|
||||||
|
return ud.first["username"].toString();
|
||||||
|
}(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.waiting) {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Tooltip(
|
||||||
|
message: snapshot.data ?? "#error#",
|
||||||
|
child: Text(
|
||||||
|
snapshot.data ?? "#error#",
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onDoubleTap: () async {
|
||||||
|
final user = await controller.showUserPicker();
|
||||||
|
if (user != null) {
|
||||||
|
await controller.updateItem(
|
||||||
|
e,
|
||||||
|
col.name,
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else if (col is AssetRefColumnDefinition)
|
||||||
|
DataCell(
|
||||||
|
FutureBuilder<String>(
|
||||||
|
future: () async {
|
||||||
|
final cachedAsset =
|
||||||
|
controller.getAssetFromCache(e[col.name]);
|
||||||
|
if (cachedAsset != null) {
|
||||||
|
return cachedAsset.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
final asset = await ApiController.to.apiClient
|
||||||
|
.getItemsFromTable(
|
||||||
|
tableName: "assets",
|
||||||
|
itemsSelector: ItemsSelector(
|
||||||
|
fields: ["name"],
|
||||||
|
where: [
|
||||||
|
ColumnConditionCompat(
|
||||||
|
column: "id",
|
||||||
|
operator_: ColumnConditionCompatOperator.eq,
|
||||||
|
value: e[col.name],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ad = asset.data;
|
||||||
|
if (ad == null ||
|
||||||
|
ad.isEmpty ||
|
||||||
|
ad.first["name"] == null) {
|
||||||
|
return "#error#";
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.putAssetInCache(Asset(
|
||||||
|
id: e[col.name],
|
||||||
|
name: ad.first["name"],
|
||||||
|
description: "",
|
||||||
|
fid: "",
|
||||||
|
tags: "",
|
||||||
|
));
|
||||||
|
|
||||||
|
return ad.first["name"].toString();
|
||||||
|
}(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState ==
|
||||||
|
ConnectionState.waiting) {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Tooltip(
|
||||||
|
message: snapshot.data ?? "#error#",
|
||||||
|
child: Text(
|
||||||
|
snapshot.data ?? "#error#",
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onDoubleTap: () async {
|
||||||
|
final asset = await controller.showAssetPicker();
|
||||||
|
if (asset != null) {
|
||||||
|
await controller.updateItem(
|
||||||
|
e,
|
||||||
|
col.name,
|
||||||
|
asset.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
DataCell.empty,
|
||||||
|
DataCell(Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => controller.deleteItem(e),
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).constrained(width: Get.width * 0.9, height: Get.height * 0.9),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> show(TableDefinition table) async {
|
||||||
|
Get.lazyPut<OpenTableController>(() => OpenTableController(table: table));
|
||||||
|
|
||||||
|
await Get.dialog(
|
||||||
|
const OpenTableDialog(),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:tuuli_app/api_controller.dart';
|
||||||
import 'package:recase/recase.dart';
|
import 'package:recase/recase.dart';
|
||||||
import 'package:tuuli_app/models/db_column_definition.dart';
|
import 'package:tuuli_app/models/db_column_definition.dart';
|
||||||
import 'package:tuuli_app/pages/dialogs/create_table_dialog.dart';
|
import 'package:tuuli_app/pages/dialogs/create_table_dialog.dart';
|
||||||
|
import 'package:tuuli_app/pages/dialogs/open_table_dialog.dart';
|
||||||
|
|
||||||
class TablesListPanelController extends GetxController {
|
class TablesListPanelController extends GetxController {
|
||||||
@override
|
@override
|
||||||
|
|
@ -62,7 +63,9 @@ class TablesListPanelController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openTable(TableDefinition table) async {}
|
Future<void> openTable(TableDefinition table) async {
|
||||||
|
await OpenTableDialog.show(table);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TablesListPanel extends GetView<TablesListPanelController> {
|
class TablesListPanel extends GetView<TablesListPanelController> {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:tuuli_api/tuuli_api.dart';
|
||||||
|
import 'package:tuuli_app/models/user_definition.dart';
|
||||||
|
|
||||||
Random _random = Random();
|
Random _random = Random();
|
||||||
|
|
||||||
String randomHexString(int length) {
|
String randomHexString(int length) {
|
||||||
|
|
@ -9,3 +12,35 @@ String randomHexString(int length) {
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String postgresDateFormat(DateTime dt) {
|
||||||
|
int yearSign = dt.year.sign;
|
||||||
|
int absYear = dt.year.abs();
|
||||||
|
String y = absYear
|
||||||
|
.toString()
|
||||||
|
.padLeft((dt.year >= -9999 && dt.year <= 9999) ? 4 : 6, "0");
|
||||||
|
if (yearSign == -1) {
|
||||||
|
y = "-$y";
|
||||||
|
}
|
||||||
|
String m = dt.month.toString().padLeft(2, "0");
|
||||||
|
String d = dt.day.toString().padLeft(2, "0");
|
||||||
|
String h = dt.hour.toString().padLeft(2, "0");
|
||||||
|
String min = dt.minute.toString().padLeft(2, "0");
|
||||||
|
String sec = dt.second.toString().padLeft(2, "0");
|
||||||
|
|
||||||
|
return "$y-$m-$d $h:$min:$sec";
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> convertToPayload(Map<String, dynamic> data) {
|
||||||
|
return data.map((key, value) {
|
||||||
|
if (value is UserDefinition) {
|
||||||
|
return MapEntry(key, value.id);
|
||||||
|
} else if (value is Asset) {
|
||||||
|
return MapEntry(key, value.id);
|
||||||
|
} else if (value is DateTime) {
|
||||||
|
return MapEntry(key, postgresDateFormat(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapEntry(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
126
lib/widgets/data_input_dialog.dart
Normal file
126
lib/widgets/data_input_dialog.dart
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_fast_forms/flutter_fast_forms.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
Future<String?> showStringInputDialog({String? originalValue}) async {
|
||||||
|
final strVal = (originalValue ?? "").obs;
|
||||||
|
return await Get.dialog<String>(
|
||||||
|
AlertDialog(
|
||||||
|
title: const Text("Enter a string"),
|
||||||
|
content: TextField(
|
||||||
|
controller: TextEditingController(text: originalValue),
|
||||||
|
onChanged: (value) {
|
||||||
|
strVal.value = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back(result: null);
|
||||||
|
},
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back(result: strVal.value);
|
||||||
|
},
|
||||||
|
child: const Text("OK"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<double?> showDoubleInputDialog({double? originalValue}) async {
|
||||||
|
final strVal = (originalValue?.toString() ?? "").obs;
|
||||||
|
return await Get.dialog<double>(
|
||||||
|
AlertDialog(
|
||||||
|
title: const Text("Enter a number"),
|
||||||
|
content: FastTextField(
|
||||||
|
name: "Number",
|
||||||
|
initialValue: originalValue?.toString(),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return "Please enter a number";
|
||||||
|
}
|
||||||
|
final parsed = double.tryParse(value);
|
||||||
|
if (parsed == null) {
|
||||||
|
return "Please enter a valid number";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
strVal.value = value ?? "";
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back(result: null);
|
||||||
|
},
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final d = double.tryParse(strVal.value);
|
||||||
|
if (d != null) {
|
||||||
|
Get.back(result: d);
|
||||||
|
} else {
|
||||||
|
Get.back(result: null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("OK"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int?> showIntInputDialog({int? originalValue}) async {
|
||||||
|
final strVal = (originalValue?.toString() ?? "").obs;
|
||||||
|
return await Get.dialog<int>(
|
||||||
|
AlertDialog(
|
||||||
|
title: const Text("Enter a number"),
|
||||||
|
content: FastTextField(
|
||||||
|
name: "Number",
|
||||||
|
initialValue: originalValue?.toString(),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return "Please enter a number";
|
||||||
|
}
|
||||||
|
final parsed = int.tryParse(value);
|
||||||
|
if (parsed == null) {
|
||||||
|
return "Please enter a valid number";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onChanged: (value) {
|
||||||
|
strVal.value = value ?? "";
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back(result: null);
|
||||||
|
},
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
final i = int.tryParse(strVal.value);
|
||||||
|
if (i != null) {
|
||||||
|
Get.back(result: i);
|
||||||
|
} else {
|
||||||
|
Get.back(result: null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("OK"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue