feat(todo): comments on a task (ADR 0033, D36)
Plain text with links, written, edited and deleted in the task's sheet, counted on its row. They are sealed in `task_comment`, the task they belong to with them, and go with their task. Whats-New: Comments: write on a task, edit or delete what you wrote, and open its links. Whats-New-De: Kommentare: Schreibe zu einer Aufgabe, bearbeite oder lösche es und öffne Links darin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
6385d84cff
commit
016b74ff74
40 changed files with 20953 additions and 6 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,696 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseDefinition",
|
||||
"schemaVersion": 2,
|
||||
"moduleName": "halkyon_todo",
|
||||
"tables": [
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "content_key",
|
||||
"dartName": "ContentKey",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "dataKeyId",
|
||||
"fieldName": "dataKeyId",
|
||||
"columnType": 0,
|
||||
"isNullable": false,
|
||||
"dartType": "String"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "sealedKey",
|
||||
"fieldName": "sealedKey",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "content_key_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "content_key_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "label",
|
||||
"dartName": "Label",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "label_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "label_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "project",
|
||||
"dartName": "Project",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "project_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "project_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "saved_filter",
|
||||
"dartName": "SavedFilter",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "saved_filter_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "saved_filter_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "section",
|
||||
"dartName": "Section",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "section_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "section_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "task",
|
||||
"dartName": "Task",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "task_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "task_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "task_comment",
|
||||
"dartName": "TaskComment",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "task_comment_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "task_comment_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "todo_settings",
|
||||
"dartName": "TodoSettings",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "todo_settings_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "todo_settings_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
],
|
||||
"installedModules": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "halkyon_base",
|
||||
"version": "20261026140329453-assistant-budgets"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod",
|
||||
"version": "20260824182259319"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod_auth_idp",
|
||||
"version": "20260924105404509"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod_offline_sync",
|
||||
"version": "20260914143806119"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "halkyon_oidc",
|
||||
"version": "20261023180644988"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationVersion",
|
||||
"module": "serverpod_auth_core",
|
||||
"version": "20260924105232991"
|
||||
}
|
||||
],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "createTable",
|
||||
"createTable": {
|
||||
"__className__": "serverpod.TableDefinition",
|
||||
"name": "task_comment",
|
||||
"dartName": "TaskComment",
|
||||
"module": "halkyon_todo",
|
||||
"schema": "public",
|
||||
"columns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "id",
|
||||
"fieldName": "id",
|
||||
"columnType": 7,
|
||||
"isNullable": false,
|
||||
"columnDefault": "random_v7",
|
||||
"dartType": "UuidValue?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "spaceId",
|
||||
"fieldName": "spaceId",
|
||||
"columnType": 6,
|
||||
"isNullable": true,
|
||||
"dartType": "int?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyId",
|
||||
"fieldName": "keyId",
|
||||
"columnType": 0,
|
||||
"isNullable": true,
|
||||
"dartType": "String?"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "payload",
|
||||
"fieldName": "payload",
|
||||
"columnType": 5,
|
||||
"isNullable": false,
|
||||
"dartType": "dart:typed_data:ByteData"
|
||||
},
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "nonce",
|
||||
"fieldName": "nonce",
|
||||
"columnType": 5,
|
||||
"isNullable": true,
|
||||
"dartType": "dart:typed_data:ByteData?"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"__className__": "serverpod.ForeignKeyDefinition",
|
||||
"constraintName": "task_comment_fk_0",
|
||||
"columns": [
|
||||
"spaceId"
|
||||
],
|
||||
"referenceTable": "offline_sync_spaces",
|
||||
"referenceTableSchema": "public",
|
||||
"referenceColumns": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": 3,
|
||||
"onDelete": 4
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"__className__": "serverpod.IndexDefinition",
|
||||
"indexName": "task_comment_space_idx",
|
||||
"elements": [
|
||||
{
|
||||
"__className__": "serverpod.IndexElementDefinition",
|
||||
"type": 0,
|
||||
"definition": "spaceId"
|
||||
}
|
||||
],
|
||||
"type": "btree",
|
||||
"isUnique": false,
|
||||
"isPrimary": false
|
||||
}
|
||||
],
|
||||
"managed": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ part '20261101000019144-projects/migration.dart';
|
|||
part '20261101000604776-tasks/migration.dart';
|
||||
part '20261101180755366-settings/migration.dart';
|
||||
part '20261106183802735-filters/migration.dart';
|
||||
part '20261106184947570-comments/migration.dart';
|
||||
|
||||
/// Migration registry for the client-side database.
|
||||
class MigrationRegistry {
|
||||
|
|
@ -18,6 +19,7 @@ class MigrationRegistry {
|
|||
_Migration20261101000604776_tasks(),
|
||||
_Migration20261101180755366_settings(),
|
||||
_Migration20261106183802735_filters(),
|
||||
_Migration20261106184947570_comments(),
|
||||
];
|
||||
|
||||
/// List of all client-side database migration versions.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,672 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:typed_data' as _idt;
|
||||
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
||||
import 'package:serverpod_database/serverpod_database.dart' as _isd;
|
||||
|
||||
/// A comment on a task (ADR 0033, D36): its task, its text and when it was
|
||||
/// written, in its sealed payload (D5); the instance sees that it exists,
|
||||
/// its size and when it changed, not which task it belongs to. Changed
|
||||
/// when edited, the last writer winning. Synced.
|
||||
abstract class TaskComment
|
||||
implements _isd.TableRow<_isc.UuidValue?>, _isc.ProtocolSerialization {
|
||||
TaskComment._({
|
||||
this.id,
|
||||
this.spaceId,
|
||||
this.keyId,
|
||||
required this.payload,
|
||||
this.nonce,
|
||||
});
|
||||
|
||||
factory TaskComment({
|
||||
_isc.UuidValue? id,
|
||||
int? spaceId,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
}) = _TaskCommentImpl;
|
||||
|
||||
factory TaskComment.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return TaskComment(
|
||||
id: jsonSerialization['id'] == null
|
||||
? null
|
||||
: _isc.UuidValueJsonExtension.fromJson(jsonSerialization['id']),
|
||||
spaceId: jsonSerialization['spaceId'] as int?,
|
||||
keyId: jsonSerialization['keyId'] as String?,
|
||||
payload: _isc.ByteDataJsonExtension.fromJson(
|
||||
jsonSerialization['payload'],
|
||||
),
|
||||
nonce: jsonSerialization['nonce'] == null
|
||||
? null
|
||||
: _isc.ByteDataJsonExtension.fromJson(jsonSerialization['nonce']),
|
||||
);
|
||||
}
|
||||
|
||||
static final t = TaskCommentTable();
|
||||
|
||||
static const db = TaskCommentRepository._();
|
||||
|
||||
@override
|
||||
_isc.UuidValue? id;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
int? spaceId;
|
||||
|
||||
/// The key it is sealed with (ADR 0002's envelope); never null in Todo,
|
||||
/// where everything is sealed.
|
||||
String? keyId;
|
||||
|
||||
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
|
||||
_idt.ByteData payload;
|
||||
|
||||
/// The sealing's nonce (ADR 0002's envelope).
|
||||
_idt.ByteData? nonce;
|
||||
|
||||
@override
|
||||
_isd.Table<_isc.UuidValue?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [TaskComment]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
TaskComment copyWith({
|
||||
_isc.UuidValue? id,
|
||||
int? spaceId,
|
||||
String? keyId,
|
||||
_idt.ByteData? payload,
|
||||
_idt.ByteData? nonce,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'TaskComment',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'TaskComment',
|
||||
if (id != null) 'id': id?.toJson(),
|
||||
if (spaceId != null) 'spaceId': spaceId,
|
||||
if (keyId != null) 'keyId': keyId,
|
||||
'payload': payload.toJson(),
|
||||
if (nonce != null) 'nonce': nonce?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
static TaskCommentInclude include() {
|
||||
return TaskCommentInclude._();
|
||||
}
|
||||
|
||||
static TaskCommentIncludeList includeList({
|
||||
_isd.WhereExpressionBuilder<TaskCommentTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_isd.OrderByBuilder<TaskCommentTable>? orderBy,
|
||||
_isd.OrderByListBuilder<TaskCommentTable>? orderByList,
|
||||
TaskCommentInclude? include,
|
||||
}) {
|
||||
return TaskCommentIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(TaskComment.t),
|
||||
orderByList: orderByList?.call(TaskComment.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _isc.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _TaskCommentImpl extends TaskComment {
|
||||
_TaskCommentImpl({
|
||||
_isc.UuidValue? id,
|
||||
int? spaceId,
|
||||
String? keyId,
|
||||
required _idt.ByteData payload,
|
||||
_idt.ByteData? nonce,
|
||||
}) : super._(
|
||||
id: id,
|
||||
spaceId: spaceId,
|
||||
keyId: keyId,
|
||||
payload: payload,
|
||||
nonce: nonce,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [TaskComment]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_isc.useResult
|
||||
@override
|
||||
TaskComment copyWith({
|
||||
Object? id = _Undefined,
|
||||
Object? spaceId = _Undefined,
|
||||
Object? keyId = _Undefined,
|
||||
_idt.ByteData? payload,
|
||||
Object? nonce = _Undefined,
|
||||
}) {
|
||||
return TaskComment(
|
||||
id: id is _isc.UuidValue? ? id : this.id,
|
||||
spaceId: spaceId is int? ? spaceId : this.spaceId,
|
||||
keyId: keyId is String? ? keyId : this.keyId,
|
||||
payload: payload ?? this.payload.clone(),
|
||||
nonce: nonce is _idt.ByteData? ? nonce : this.nonce?.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TaskCommentUpdateTable extends _isd.UpdateTable<TaskCommentTable> {
|
||||
TaskCommentUpdateTable(super.table);
|
||||
|
||||
_isd.ColumnValue<int, int> spaceId(int? value) => _isd.ColumnValue(
|
||||
table.spaceId,
|
||||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<String, String> keyId(String? value) => _isd.ColumnValue(
|
||||
table.keyId,
|
||||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> payload(_idt.ByteData value) =>
|
||||
_isd.ColumnValue(
|
||||
table.payload,
|
||||
value,
|
||||
);
|
||||
|
||||
_isd.ColumnValue<_idt.ByteData, _idt.ByteData> nonce(_idt.ByteData? value) =>
|
||||
_isd.ColumnValue(
|
||||
table.nonce,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class TaskCommentTable extends _isd.Table<_isc.UuidValue?> {
|
||||
TaskCommentTable({super.tableRelation}) : super(tableName: 'task_comment') {
|
||||
updateTable = TaskCommentUpdateTable(this);
|
||||
spaceId = _isd.ColumnInt(
|
||||
'spaceId',
|
||||
this,
|
||||
);
|
||||
keyId = _isd.ColumnString(
|
||||
'keyId',
|
||||
this,
|
||||
);
|
||||
payload = _isd.ColumnByteData(
|
||||
'payload',
|
||||
this,
|
||||
);
|
||||
nonce = _isd.ColumnByteData(
|
||||
'nonce',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final TaskCommentUpdateTable updateTable;
|
||||
|
||||
/// The space owning this row. Maintained by the sync engine.
|
||||
late final _isd.ColumnInt spaceId;
|
||||
|
||||
/// The key it is sealed with (ADR 0002's envelope); never null in Todo,
|
||||
/// where everything is sealed.
|
||||
late final _isd.ColumnString keyId;
|
||||
|
||||
/// The content, sealed: UTF-8 JSON with a `schemaVersion` once opened.
|
||||
late final _isd.ColumnByteData payload;
|
||||
|
||||
/// The sealing's nonce (ADR 0002's envelope).
|
||||
late final _isd.ColumnByteData nonce;
|
||||
|
||||
@override
|
||||
List<_isd.Column> get columns => [
|
||||
id,
|
||||
spaceId,
|
||||
keyId,
|
||||
payload,
|
||||
nonce,
|
||||
];
|
||||
}
|
||||
|
||||
class TaskCommentInclude extends _isd.IncludeObject {
|
||||
TaskCommentInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _isd.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_isd.Table<_isc.UuidValue?> get table => TaskComment.t;
|
||||
}
|
||||
|
||||
class TaskCommentIncludeList extends _isd.IncludeList {
|
||||
TaskCommentIncludeList._({
|
||||
_isd.WhereExpressionBuilder<TaskCommentTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(TaskComment.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _isd.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_isd.Table<_isc.UuidValue?> get table => TaskComment.t;
|
||||
}
|
||||
|
||||
class TaskCommentRepository {
|
||||
const TaskCommentRepository._();
|
||||
|
||||
/// Returns a list of [TaskComment]s matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order of the items use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// The maximum number of items can be set by [limit]. If no limit is set,
|
||||
/// all items matching the query will be returned.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which [limit] (or all)
|
||||
/// items are read from the database.
|
||||
///
|
||||
/// ```dart
|
||||
/// var persons = await Persons.db.find(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.firstName,
|
||||
/// limit: 100,
|
||||
/// );
|
||||
/// ```
|
||||
Future<List<TaskComment>> find(
|
||||
_isd.DatabaseSession session, {
|
||||
_isd.WhereExpressionBuilder<TaskCommentTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_isd.OrderByBuilder<TaskCommentTable>? orderBy,
|
||||
_isd.OrderByListBuilder<TaskCommentTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
_isd.LockMode? lockMode,
|
||||
_isd.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<TaskComment>(
|
||||
where: where?.call(TaskComment.t),
|
||||
orderBy: orderBy?.call(TaskComment.t),
|
||||
orderByList: orderByList?.call(TaskComment.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [TaskComment] matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which the next one will be picked.
|
||||
///
|
||||
/// ```dart
|
||||
/// var youngestPerson = await Persons.db.findFirstRow(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.age,
|
||||
/// );
|
||||
/// ```
|
||||
Future<TaskComment?> findFirstRow(
|
||||
_isd.DatabaseSession session, {
|
||||
_isd.WhereExpressionBuilder<TaskCommentTable>? where,
|
||||
int? offset,
|
||||
_isd.OrderByBuilder<TaskCommentTable>? orderBy,
|
||||
_isd.OrderByListBuilder<TaskCommentTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
_isd.LockMode? lockMode,
|
||||
_isd.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<TaskComment>(
|
||||
where: where?.call(TaskComment.t),
|
||||
orderBy: orderBy?.call(TaskComment.t),
|
||||
orderByList: orderByList?.call(TaskComment.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [TaskComment] by its [id] or null if no such row exists.
|
||||
Future<TaskComment?> findById(
|
||||
_isd.DatabaseSession session,
|
||||
_isc.UuidValue id, {
|
||||
_isd.Transaction? transaction,
|
||||
_isd.LockMode? lockMode,
|
||||
_isd.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<TaskComment>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [TaskComment]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [TaskComment]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// insert, none of the rows will be inserted.
|
||||
///
|
||||
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
|
||||
/// rows are silently skipped, and only the successfully inserted rows are
|
||||
/// returned.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the inserted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<TaskComment>> insert(
|
||||
_isd.DatabaseSession session,
|
||||
List<TaskComment> rows, {
|
||||
_isd.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<TaskComment>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [TaskComment] and returns the inserted row.
|
||||
///
|
||||
/// The returned [TaskComment] will have its `id` field set.
|
||||
Future<TaskComment> insertRow(
|
||||
_isd.DatabaseSession session,
|
||||
TaskComment row, {
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<TaskComment>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [TaskComment]s in the list and returns the resulting rows.
|
||||
///
|
||||
/// If a row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated with the new values. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies to rows matching the
|
||||
/// given expression. Conflicting rows that don't match are skipped and not
|
||||
/// returned, so the resulting list may be shorter than [rows].
|
||||
///
|
||||
/// The returned [TaskComment]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails,
|
||||
/// none of the rows will be affected.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the resulting rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<TaskComment>> upsert(
|
||||
_isd.DatabaseSession session,
|
||||
List<TaskComment> rows, {
|
||||
required _isd.ColumnSelections<TaskCommentTable> conflictColumns,
|
||||
_isd.ColumnSelections<TaskCommentTable>? updateColumns,
|
||||
_isd.WhereExpressionBuilder<TaskCommentTable>? updateWhere,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<TaskComment>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(TaskComment.t),
|
||||
updateColumns: updateColumns?.call(TaskComment.t),
|
||||
updateWhere: updateWhere?.call(TaskComment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [TaskComment] and returns the resulting row.
|
||||
///
|
||||
/// If the row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies when the existing
|
||||
/// row matches the expression. Returns `null` if no row was affected — for
|
||||
/// example when [updateWhere] does not match the conflicting row.
|
||||
///
|
||||
/// The returned [TaskComment] will have its `id` field set.
|
||||
Future<TaskComment?> upsertRow(
|
||||
_isd.DatabaseSession session,
|
||||
TaskComment row, {
|
||||
required _isd.ColumnSelections<TaskCommentTable> conflictColumns,
|
||||
_isd.ColumnSelections<TaskCommentTable>? updateColumns,
|
||||
_isd.WhereExpressionBuilder<TaskCommentTable>? updateWhere,
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<TaskComment>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(TaskComment.t),
|
||||
updateColumns: updateColumns?.call(TaskComment.t),
|
||||
updateWhere: updateWhere?.call(TaskComment.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [TaskComment]s in the list and returns the updated rows. If
|
||||
/// [columns] is provided, only those columns will be updated. Defaults to
|
||||
/// all columns.
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// update, none of the rows will be updated.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<TaskComment>> update(
|
||||
_isd.DatabaseSession session,
|
||||
List<TaskComment> rows, {
|
||||
_isd.ColumnSelections<TaskCommentTable>? columns,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<TaskComment>(
|
||||
rows,
|
||||
columns: columns?.call(TaskComment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [TaskComment]. The row needs to have its id set.
|
||||
/// Optionally, a list of [columns] can be provided to only update those
|
||||
/// columns. Defaults to all columns.
|
||||
Future<TaskComment> updateRow(
|
||||
_isd.DatabaseSession session,
|
||||
TaskComment row, {
|
||||
_isd.ColumnSelections<TaskCommentTable>? columns,
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<TaskComment>(
|
||||
row,
|
||||
columns: columns?.call(TaskComment.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [TaskComment] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<TaskComment?> updateById(
|
||||
_isd.DatabaseSession session,
|
||||
_isc.UuidValue id, {
|
||||
required _isd.ColumnValueListBuilder<TaskCommentUpdateTable> columnValues,
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<TaskComment>(
|
||||
id,
|
||||
columnValues: columnValues(TaskComment.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [TaskComment]s matching the [where] expression with the specified [columnValues].
|
||||
/// Returns the list of updated rows.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<TaskComment>> updateWhere(
|
||||
_isd.DatabaseSession session, {
|
||||
required _isd.ColumnValueListBuilder<TaskCommentUpdateTable> columnValues,
|
||||
required _isd.WhereExpressionBuilder<TaskCommentTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_isd.OrderByBuilder<TaskCommentTable>? orderBy,
|
||||
_isd.OrderByListBuilder<TaskCommentTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<TaskComment>(
|
||||
columnValues: columnValues(TaskComment.t.updateTable),
|
||||
where: where(TaskComment.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(TaskComment.t),
|
||||
orderByList: orderByList?.call(TaskComment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [TaskComment]s in the list and returns the deleted rows.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fail to
|
||||
/// be deleted, none of the rows will be deleted.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<TaskComment>> delete(
|
||||
_isd.DatabaseSession session,
|
||||
List<TaskComment> rows, {
|
||||
_isd.OrderByBuilder<TaskCommentTable>? orderBy,
|
||||
_isd.OrderByListBuilder<TaskCommentTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<TaskComment>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(TaskComment.t),
|
||||
orderByList: orderByList?.call(TaskComment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [TaskComment].
|
||||
Future<TaskComment> deleteRow(
|
||||
_isd.DatabaseSession session,
|
||||
TaskComment row, {
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<TaskComment>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all rows matching the [where] expression.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<TaskComment>> deleteWhere(
|
||||
_isd.DatabaseSession session, {
|
||||
required _isd.WhereExpressionBuilder<TaskCommentTable> where,
|
||||
_isd.OrderByBuilder<TaskCommentTable>? orderBy,
|
||||
_isd.OrderByListBuilder<TaskCommentTable>? orderByList,
|
||||
_isd.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<TaskComment>(
|
||||
where: where(TaskComment.t),
|
||||
orderBy: orderBy?.call(TaskComment.t),
|
||||
orderByList: orderByList?.call(TaskComment.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_isd.DatabaseSession session, {
|
||||
_isd.WhereExpressionBuilder<TaskCommentTable>? where,
|
||||
int? limit,
|
||||
_isd.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<TaskComment>(
|
||||
where: where?.call(TaskComment.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [TaskComment] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_isd.DatabaseSession session, {
|
||||
required _isd.WhereExpressionBuilder<TaskCommentTable> where,
|
||||
required _isd.LockMode lockMode,
|
||||
required _isd.Transaction transaction,
|
||||
_isd.LockBehavior lockBehavior = _isd.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<TaskComment>(
|
||||
where: where(TaskComment.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
|||
import 'package:serverpod_database/serverpod_database.dart' as _isd;
|
||||
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart'
|
||||
as _ipulbpi2;
|
||||
import 'comment/task_comment.dart' as _i68ws957;
|
||||
import 'filter/saved_filter.dart' as _i9q9fhid;
|
||||
import 'label/label.dart' as _ivak08z4;
|
||||
import 'privacy/content_key.dart' as _imed3x5d;
|
||||
|
|
@ -34,6 +35,7 @@ import 'search/place_point.dart' as _itf0ur2u;
|
|||
import 'search/search_refused.dart' as _ihbxkwke;
|
||||
import 'settings/todo_settings.dart' as _icak60vd;
|
||||
import 'task/task.dart' as _ib4f8uyw;
|
||||
export 'comment/task_comment.dart';
|
||||
export 'filter/saved_filter.dart';
|
||||
export 'label/label.dart';
|
||||
export 'privacy/content_key.dart';
|
||||
|
|
@ -452,6 +454,73 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
],
|
||||
managed: true,
|
||||
),
|
||||
_isd.TableDefinition(
|
||||
name: 'task_comment',
|
||||
dartName: 'TaskComment',
|
||||
schema: 'public',
|
||||
module: 'halkyon_todo',
|
||||
columns: [
|
||||
_isd.ColumnDefinition(
|
||||
name: 'id',
|
||||
columnType: _isd.ColumnType.uuid,
|
||||
isNullable: false,
|
||||
dartType: 'UuidValue?',
|
||||
columnDefault: 'random_v7',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'spaceId',
|
||||
columnType: _isd.ColumnType.bigint,
|
||||
isNullable: true,
|
||||
dartType: 'int?',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'keyId',
|
||||
columnType: _isd.ColumnType.text,
|
||||
isNullable: true,
|
||||
dartType: 'String?',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'payload',
|
||||
columnType: _isd.ColumnType.bytea,
|
||||
isNullable: false,
|
||||
dartType: 'dart:typed_data:ByteData',
|
||||
),
|
||||
_isd.ColumnDefinition(
|
||||
name: 'nonce',
|
||||
columnType: _isd.ColumnType.bytea,
|
||||
isNullable: true,
|
||||
dartType: 'dart:typed_data:ByteData?',
|
||||
),
|
||||
],
|
||||
foreignKeys: [
|
||||
_isd.ForeignKeyDefinition(
|
||||
constraintName: 'task_comment_fk_0',
|
||||
columns: ['spaceId'],
|
||||
referenceTable: 'offline_sync_spaces',
|
||||
referenceTableSchema: 'public',
|
||||
referenceColumns: ['id'],
|
||||
onUpdate: _isd.ForeignKeyAction.noAction,
|
||||
onDelete: _isd.ForeignKeyAction.cascade,
|
||||
matchType: null,
|
||||
),
|
||||
],
|
||||
indexes: [
|
||||
_isd.IndexDefinition(
|
||||
indexName: 'task_comment_space_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isd.IndexElementDefinition(
|
||||
type: _isd.IndexElementDefinitionType.column,
|
||||
definition: 'spaceId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isd.TableDefinition(
|
||||
name: 'todo_settings',
|
||||
dartName: 'TodoSettings',
|
||||
|
|
@ -568,6 +637,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
}
|
||||
}
|
||||
|
||||
if (t == _i68ws957.TaskComment) {
|
||||
return _i68ws957.TaskComment.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i9q9fhid.SavedFilter) {
|
||||
return _i9q9fhid.SavedFilter.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -601,6 +673,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (t == _ib4f8uyw.Task) {
|
||||
return _ib4f8uyw.Task.fromJson(data) as T;
|
||||
}
|
||||
if (t == _isc.getType<_i68ws957.TaskComment?>()) {
|
||||
return (data != null ? _i68ws957.TaskComment.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _isc.getType<_i9q9fhid.SavedFilter?>()) {
|
||||
return (data != null ? _i9q9fhid.SavedFilter.fromJson(data) : null) as T;
|
||||
}
|
||||
|
|
@ -663,6 +738,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
|
||||
static String? getClassNameForType(Type type) {
|
||||
return switch (type) {
|
||||
_i68ws957.TaskComment => 'TaskComment',
|
||||
_i9q9fhid.SavedFilter => 'SavedFilter',
|
||||
_ivak08z4.Label => 'Label',
|
||||
_imed3x5d.ContentKey => 'ContentKey',
|
||||
|
|
@ -691,6 +767,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
}
|
||||
|
||||
switch (data) {
|
||||
case _i68ws957.TaskComment():
|
||||
return 'TaskComment';
|
||||
case _i9q9fhid.SavedFilter():
|
||||
return 'SavedFilter';
|
||||
case _ivak08z4.Label():
|
||||
|
|
@ -749,6 +827,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
if (dataClassName is! String) {
|
||||
return super.deserializeByClassName(data);
|
||||
}
|
||||
if (dataClassName == 'TaskComment') {
|
||||
return deserialize<_i68ws957.TaskComment>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'SavedFilter') {
|
||||
return deserialize<_i9q9fhid.SavedFilter>(data['data']);
|
||||
}
|
||||
|
|
@ -861,6 +942,8 @@ class Protocol extends _isd.DatabaseSerializationManager {
|
|||
}
|
||||
}
|
||||
switch (t) {
|
||||
case _i68ws957.TaskComment:
|
||||
return _i68ws957.TaskComment.t;
|
||||
case _i9q9fhid.SavedFilter:
|
||||
return _i9q9fhid.SavedFilter.t;
|
||||
case _ivak08z4.Label:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod_database/serverpod_database.dart' as _isd;
|
||||
import 'comment/task_comment.dart' as _i68ws957;
|
||||
import 'filter/saved_filter.dart' as _i9q9fhid;
|
||||
import 'label/label.dart' as _ivak08z4;
|
||||
import 'privacy/content_key.dart' as _imed3x5d;
|
||||
|
|
@ -29,5 +30,6 @@ final List<_isd.Table> syncTables = [
|
|||
_i9q9fhid.SavedFilter.t,
|
||||
_imadkvi9.Section.t,
|
||||
_ib4f8uyw.Task.t,
|
||||
_i68ws957.TaskComment.t,
|
||||
_icak60vd.TodoSettings.t,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -55,6 +55,15 @@
|
|||
"filterMatches": "{count, plural, =1{1 Aufgabe passt} other{{count} Aufgaben passen}}",
|
||||
"filterNotRead": "Nicht verstanden bei „{near}“",
|
||||
"filterNotReadEnd": "Die Abfrage endet zu früh",
|
||||
"commentsTitle": "Kommentare",
|
||||
"commentAdd": "Kommentar schreiben",
|
||||
"commentSend": "Kommentar senden",
|
||||
"commentMenu": "Kommentarmenü",
|
||||
"commentEdit": "Kommentar bearbeiten",
|
||||
"commentCopy": "Text kopieren",
|
||||
"commentDelete": "Kommentar löschen",
|
||||
"commentDeleteTitle": "Kommentar löschen?",
|
||||
"commentEdited": "{when}, bearbeitet",
|
||||
"labelsTitle": "Labels",
|
||||
"labelsNone": "Noch keine Labels.",
|
||||
"labelsNoneSet": "Keine Labels",
|
||||
|
|
|
|||
|
|
@ -115,6 +115,23 @@
|
|||
"@filterNotReadEnd": {
|
||||
"description": "The filter editor when its query stops before it is complete."
|
||||
},
|
||||
"commentsTitle": "Comments",
|
||||
"commentAdd": "Add a comment",
|
||||
"commentSend": "Send comment",
|
||||
"commentMenu": "Comment menu",
|
||||
"commentEdit": "Edit comment",
|
||||
"commentCopy": "Copy text",
|
||||
"commentDelete": "Delete comment",
|
||||
"commentDeleteTitle": "Delete the comment?",
|
||||
"commentEdited": "{when}, edited",
|
||||
"@commentEdited": {
|
||||
"description": "When a comment was written, once it was changed.",
|
||||
"placeholders": {
|
||||
"when": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"labelsTitle": "Labels",
|
||||
"labelsNone": "No labels yet.",
|
||||
"labelsNoneSet": "No labels",
|
||||
|
|
|
|||
|
|
@ -430,6 +430,60 @@ abstract class AppLocalizations {
|
|||
/// **'This query ends too soon'**
|
||||
String get filterNotReadEnd;
|
||||
|
||||
/// No description provided for @commentsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Comments'**
|
||||
String get commentsTitle;
|
||||
|
||||
/// No description provided for @commentAdd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add a comment'**
|
||||
String get commentAdd;
|
||||
|
||||
/// No description provided for @commentSend.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send comment'**
|
||||
String get commentSend;
|
||||
|
||||
/// No description provided for @commentMenu.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Comment menu'**
|
||||
String get commentMenu;
|
||||
|
||||
/// No description provided for @commentEdit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit comment'**
|
||||
String get commentEdit;
|
||||
|
||||
/// No description provided for @commentCopy.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy text'**
|
||||
String get commentCopy;
|
||||
|
||||
/// No description provided for @commentDelete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete comment'**
|
||||
String get commentDelete;
|
||||
|
||||
/// No description provided for @commentDeleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete the comment?'**
|
||||
String get commentDeleteTitle;
|
||||
|
||||
/// When a comment was written, once it was changed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{when}, edited'**
|
||||
String commentEdited(String when);
|
||||
|
||||
/// No description provided for @labelsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -203,6 +203,35 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get filterNotReadEnd => 'Die Abfrage endet zu früh';
|
||||
|
||||
@override
|
||||
String get commentsTitle => 'Kommentare';
|
||||
|
||||
@override
|
||||
String get commentAdd => 'Kommentar schreiben';
|
||||
|
||||
@override
|
||||
String get commentSend => 'Kommentar senden';
|
||||
|
||||
@override
|
||||
String get commentMenu => 'Kommentarmenü';
|
||||
|
||||
@override
|
||||
String get commentEdit => 'Kommentar bearbeiten';
|
||||
|
||||
@override
|
||||
String get commentCopy => 'Text kopieren';
|
||||
|
||||
@override
|
||||
String get commentDelete => 'Kommentar löschen';
|
||||
|
||||
@override
|
||||
String get commentDeleteTitle => 'Kommentar löschen?';
|
||||
|
||||
@override
|
||||
String commentEdited(String when) {
|
||||
return '$when, bearbeitet';
|
||||
}
|
||||
|
||||
@override
|
||||
String get labelsTitle => 'Labels';
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,35 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get filterNotReadEnd => 'This query ends too soon';
|
||||
|
||||
@override
|
||||
String get commentsTitle => 'Comments';
|
||||
|
||||
@override
|
||||
String get commentAdd => 'Add a comment';
|
||||
|
||||
@override
|
||||
String get commentSend => 'Send comment';
|
||||
|
||||
@override
|
||||
String get commentMenu => 'Comment menu';
|
||||
|
||||
@override
|
||||
String get commentEdit => 'Edit comment';
|
||||
|
||||
@override
|
||||
String get commentCopy => 'Copy text';
|
||||
|
||||
@override
|
||||
String get commentDelete => 'Delete comment';
|
||||
|
||||
@override
|
||||
String get commentDeleteTitle => 'Delete the comment?';
|
||||
|
||||
@override
|
||||
String commentEdited(String when) {
|
||||
return '$when, edited';
|
||||
}
|
||||
|
||||
@override
|
||||
String get labelsTitle => 'Labels';
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import 'package:halkyon_todo_client/halkyon_todo_client.dart'
|
|||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/core/privacy/sync_content_key_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/comment/data/comment_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/comment/data/sync_comment_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/filter/data/filter_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/filter/data/sync_filter_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/label/data/label_repository.dart';
|
||||
|
|
@ -67,6 +69,9 @@ class TodoPerson(
|
|||
/// Their filters, sealed and synced (D35).
|
||||
required final FilterRepository filters,
|
||||
|
||||
/// Their comments on tasks, sealed and synced (D36).
|
||||
required final CommentRepository comments,
|
||||
|
||||
/// Their settings of Todo's (Upcoming's Display), sealed and synced.
|
||||
required final SettingsRepository settings,
|
||||
|
||||
|
|
@ -405,9 +410,11 @@ Future<TodoPerson> _openPersonData(
|
|||
final projects = ProjectRepository(SyncProjectGateway(db, sync), keys);
|
||||
final sections = SectionRepository(SyncSectionGateway(db, sync), keys);
|
||||
final settings = SettingsRepository(SyncSettingsGateway(db, sync), keys);
|
||||
final comments = CommentRepository(SyncCommentGateway(db, sync), keys);
|
||||
final tasks = TaskRepository(
|
||||
SyncTaskGateway(db, sync),
|
||||
keys,
|
||||
onDeleted: comments.deleteOn,
|
||||
automaticReminder: () async => (await settings.read()).reminders.automatic,
|
||||
);
|
||||
final labels = LabelRepository(SyncLabelGateway(db, sync), keys);
|
||||
|
|
@ -429,6 +436,7 @@ Future<TodoPerson> _openPersonData(
|
|||
tasks: tasks,
|
||||
labels: labels,
|
||||
filters: filters,
|
||||
comments: comments,
|
||||
settings: settings,
|
||||
reminders: reminders,
|
||||
notifications: notifications,
|
||||
|
|
@ -442,6 +450,7 @@ Future<TodoPerson> _openPersonData(
|
|||
await tasks.dispose();
|
||||
await labels.dispose();
|
||||
await filters.dispose();
|
||||
await comments.dispose();
|
||||
await settings.dispose();
|
||||
await sync.dispose();
|
||||
await db.close();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_todo_flutter/src/core/sealed/json_payload.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/comment/model/comment.dart';
|
||||
|
||||
/// The schema of a comment's payload this app writes and reads.
|
||||
const commentSchemaVersion = 1;
|
||||
|
||||
/// [content] as its row's payload, before sealing.
|
||||
ByteData encodeComment(CommentContent content) =>
|
||||
encodeJson(commentSchemaVersion, {
|
||||
'taskId': content.taskId,
|
||||
'text': content.text,
|
||||
'writtenAt': content.writtenAt.toUtc().toIso8601String(),
|
||||
'editedAt': ?content.editedAt?.toUtc().toIso8601String(),
|
||||
});
|
||||
|
||||
/// The comment [payload] holds, opened; null for one it can't read.
|
||||
CommentContent? decodeComment(ByteData payload) => decodeJson(
|
||||
payload,
|
||||
version: commentSchemaVersion,
|
||||
name: 'Comment',
|
||||
read: (json) => CommentContent(
|
||||
taskId: json['taskId']! as String,
|
||||
text: json['text']! as String,
|
||||
writtenAt: DateTime.parse(json['writtenAt']! as String),
|
||||
editedAt: switch (json['editedAt']) {
|
||||
final String at => DateTime.parse(at),
|
||||
_ => null,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
|
||||
import 'package:halkyon_todo_flutter/src/core/sealed/sealed_row_gateway.dart';
|
||||
import 'package:halkyon_todo_flutter/src/core/sealed/sealed_rows.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/comment/data/comment_payload.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/comment/model/comment.dart';
|
||||
|
||||
/// The person's comments on their tasks (ADR 0033, D36), each sealed with
|
||||
/// their key for its row (D5, [SealedRows]); which task one belongs to is
|
||||
/// sealed with it.
|
||||
class CommentRepository(final SealedRowGateway _gateway, final TodoKeys _keys) {
|
||||
late final _rows = SealedRows<CommentContent>(
|
||||
_gateway,
|
||||
_keys,
|
||||
table: 'task_comment',
|
||||
encode: encodeComment,
|
||||
decode: decodeComment,
|
||||
);
|
||||
|
||||
/// Fires when the comments changed, here or on another device.
|
||||
Stream<void> get changes => _rows.changes;
|
||||
|
||||
/// Every comment, oldest first.
|
||||
Future<List<StoredComment>> all() async => [
|
||||
for (final (:id, :content) in await _rows.all()) StoredComment(id, content),
|
||||
];
|
||||
|
||||
/// Keeps a new comment and returns its ID; throws [KeysMissing] where
|
||||
/// this device can't seal.
|
||||
Future<String> add(CommentContent content) => _rows.insert(content);
|
||||
|
||||
Future<void> update(String id, CommentContent content) =>
|
||||
_rows.update(id, content);
|
||||
|
||||
/// Deletes the comments [ids].
|
||||
Future<void> delete(Iterable<String> ids) => _rows.delete(ids);
|
||||
|
||||
/// Deletes the comments on the tasks [taskIds], which are gone.
|
||||
Future<void> deleteOn(Set<String> taskIds) async {
|
||||
final comments = await all();
|
||||
await _rows.delete([
|
||||
for (final comment in comments)
|
||||
if (taskIds.contains(comment.content?.taskId)) comment.id,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Closes what it holds open.
|
||||
Future<void> dispose() => _rows.dispose();
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_todo_client/halkyon_todo_client.dart' as protocol;
|
||||
import 'package:halkyon_todo_flutter/src/core/sealed/sealed_row_gateway.dart';
|
||||
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
||||
|
||||
/// The person's comments on their local database ([SealedRowGateway]), which the
|
||||
/// sync engine keeps in step with their other devices.
|
||||
class SyncCommentGateway(
|
||||
final OfflineSyncDatabaseSession _db,
|
||||
final SyncService _sync,
|
||||
) implements SealedRowGateway {
|
||||
@override
|
||||
Future<List<SealedRow>> rows() async => [
|
||||
for (final row in await protocol.TaskComment.db.find(
|
||||
_db,
|
||||
orderBy: (t) => t.id,
|
||||
))
|
||||
(
|
||||
id: row.id!.uuid,
|
||||
envelope: (keyId: row.keyId, nonce: row.nonce, payload: row.payload),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> insert(SealedRow row) async {
|
||||
await protocol.TaskComment.db.insertRow(
|
||||
_db,
|
||||
protocol.TaskComment(
|
||||
id: protocol.UuidValue.withValidation(row.id),
|
||||
keyId: row.envelope.keyId,
|
||||
nonce: row.envelope.nonce,
|
||||
payload: row.envelope.payload,
|
||||
),
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(String id, Envelope envelope) async {
|
||||
if (await protocol.TaskComment.db.findById(
|
||||
_db,
|
||||
protocol.UuidValue.withValidation(id),
|
||||
)
|
||||
case final row?) {
|
||||
// Only the named columns change (ADR 0001, rule 1).
|
||||
await protocol.TaskComment.db.updateRow(
|
||||
_db,
|
||||
row.copyWith(
|
||||
keyId: envelope.keyId,
|
||||
nonce: envelope.nonce,
|
||||
payload: envelope.payload,
|
||||
),
|
||||
columns: (t) => [t.keyId, t.nonce, t.payload],
|
||||
);
|
||||
_sync.requestSync();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(Iterable<String> ids) async {
|
||||
final uuids = <protocol.UuidValue>{
|
||||
for (final id in ids) protocol.UuidValue.withValidation(id),
|
||||
};
|
||||
if (uuids.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await _db.db.transaction((transaction) async {
|
||||
// Blanked first (ADR 0002, rule 2): the instance keeps deleted rows as
|
||||
// they last were, until the space goes.
|
||||
for (final row in await protocol.TaskComment.db.find(
|
||||
_db,
|
||||
where: (t) => t.id.inSet(uuids),
|
||||
transaction: transaction,
|
||||
)) {
|
||||
await protocol.TaskComment.db.updateRow(
|
||||
_db,
|
||||
protocol.TaskComment(
|
||||
id: row.id,
|
||||
spaceId: row.spaceId,
|
||||
payload: ByteData(0),
|
||||
),
|
||||
columns: (t) => [t.keyId, t.nonce, t.payload],
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
await protocol.TaskComment.db.deleteWhere(
|
||||
_db,
|
||||
where: (t) => t.id.inSet(uuids),
|
||||
transaction: transaction,
|
||||
);
|
||||
});
|
||||
_sync.requestSync();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<void> get remoteChanges => _sync.merges;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// A comment on the task [taskId] (ADR 0033, D36): plain [text], with links
|
||||
/// shown as such, [writtenAt], and [editedAt] once it was changed. Markdown
|
||||
/// can come later without touching what is kept.
|
||||
final class const CommentContent({
|
||||
required final String taskId,
|
||||
required final String text,
|
||||
required final DateTime writtenAt,
|
||||
final DateTime? editedAt,
|
||||
}) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [taskId, text, writtenAt, editedAt];
|
||||
}
|
||||
|
||||
/// A comment as kept: its [id], and [content] null where it doesn't open.
|
||||
final class const StoredComment(final String id, final CommentContent? content)
|
||||
with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [id, content];
|
||||
}
|
||||
|
||||
/// A piece of a comment's text: the link it stands for, or plain text.
|
||||
typedef TextPiece = ({String text, Uri? url});
|
||||
|
||||
final _link = RegExp(r'https?://[^\s<>"]+', caseSensitive: false);
|
||||
|
||||
/// [text] cut into plain pieces and links: http and https alone, so that a
|
||||
/// comment can never start anything else on the device, and without the
|
||||
/// punctuation that ends a sentence after a link.
|
||||
List<TextPiece> linkPieces(String text) {
|
||||
final pieces = <TextPiece>[];
|
||||
var at = 0;
|
||||
for (final match in _link.allMatches(text)) {
|
||||
final found = match[0]!.replaceFirst(RegExp(r'[.,;:!?)\]]+$'), '');
|
||||
final url = Uri.tryParse(found);
|
||||
if (url == null || url.host.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (match.start > at) {
|
||||
pieces.add((text: text.substring(at, match.start), url: null));
|
||||
}
|
||||
pieces.add((text: found, url: url));
|
||||
at = match.start + found.length;
|
||||
}
|
||||
if (at < text.length) {
|
||||
pieces.add((text: text.substring(at), url: null));
|
||||
}
|
||||
return pieces;
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||
import 'package:halkyon_todo_flutter/src/core/widget/menu_entry.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/comment/model/comment.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_texts.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// The key of the field writing a comment.
|
||||
const commentFieldKey = ValueKey('comment-field');
|
||||
|
||||
/// The key of the button sending a comment.
|
||||
const commentSendKey = ValueKey('comment-send');
|
||||
|
||||
/// The comments on the task [taskId] in its sheet (ADR 0033, D36): each with
|
||||
/// when it was written and a menu to edit, copy or delete it, its links
|
||||
/// opening in the browser, and a field to write one more.
|
||||
class const TaskComments({required final String taskId, super.key})
|
||||
extends StatefulWidget {
|
||||
@override
|
||||
State<TaskComments> createState() => _TaskCommentsState();
|
||||
}
|
||||
|
||||
class _TaskCommentsState() extends State<TaskComments> {
|
||||
final _text = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_text.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _send(TodoPerson todo) async {
|
||||
final text = _text.text.trim();
|
||||
if (text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_text.clear();
|
||||
setState(() {});
|
||||
await todo.comments.add(
|
||||
CommentContent(
|
||||
taskId: widget.taskId,
|
||||
text: text,
|
||||
writtenAt: clock.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final todo = PersonScope.of<TodoPerson>(context);
|
||||
final comments = context.watch<TodoCubit>().state.commentsOn(widget.taskId);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text(
|
||||
comments.isEmpty
|
||||
? l10n.commentsTitle
|
||||
: '${l10n.commentsTitle} ${comments.length}',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final comment in comments) _Comment(comment: comment),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 4, 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
key: commentFieldKey,
|
||||
controller: _text,
|
||||
minLines: 1,
|
||||
maxLines: 6,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(hintText: l10n.commentAdd),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
key: commentSendKey,
|
||||
tooltip: l10n.commentSend,
|
||||
icon: const Icon(LucideIcons.arrowUp),
|
||||
onPressed: _text.text.trim().isEmpty
|
||||
? null
|
||||
: () => unawaited(_send(todo)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class const _Comment({required final StoredComment comment})
|
||||
extends StatelessWidget {
|
||||
Future<void> _edit(BuildContext context, CommentContent content) async {
|
||||
final todo = PersonScope.of<TodoPerson>(context);
|
||||
final edited = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => _EditComment(text: content.text),
|
||||
);
|
||||
if (edited != null && edited.isNotEmpty && edited != content.text) {
|
||||
await todo.comments.update(
|
||||
comment.id,
|
||||
CommentContent(
|
||||
taskId: content.taskId,
|
||||
text: edited,
|
||||
writtenAt: content.writtenAt,
|
||||
editedAt: clock.now(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final muted = theme.colorScheme.onSurfaceVariant;
|
||||
final content = comment.content!;
|
||||
final local = content.writtenAt.toLocal();
|
||||
final when =
|
||||
'${dayText(context, Day.of(local), Day.of(clock.now()))}, '
|
||||
'${timeText(context, local.hour * 60 + local.minute)}';
|
||||
final todo = PersonScope.of<TodoPerson>(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 16, bottom: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
content.editedAt == null ? when : l10n.commentEdited(when),
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: muted),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
_LinkedText(text: content.text),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuButton<VoidCallback>(
|
||||
tooltip: l10n.commentMenu,
|
||||
icon: const Icon(LucideIcons.ellipsis),
|
||||
onSelected: (action) => action(),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: () => unawaited(_edit(context, content)),
|
||||
child: MenuEntry(LucideIcons.pencil, l10n.commentEdit),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: () => unawaited(
|
||||
Clipboard.setData(ClipboardData(text: content.text)),
|
||||
),
|
||||
child: MenuEntry(LucideIcons.copy, l10n.commentCopy),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: () async {
|
||||
if (await confirmDestructiveAction(
|
||||
context,
|
||||
title: l10n.commentDeleteTitle,
|
||||
message: content.text,
|
||||
confirmLabel: l10n.commentDelete,
|
||||
)) {
|
||||
await todo.comments.delete([comment.id]);
|
||||
}
|
||||
},
|
||||
child: MenuEntry(
|
||||
LucideIcons.trash2,
|
||||
l10n.commentDelete,
|
||||
destructive: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [text] with its links underlined, a tap opening them in the browser.
|
||||
class const _LinkedText({required final String text}) extends StatefulWidget {
|
||||
@override
|
||||
State<_LinkedText> createState() => _LinkedTextState();
|
||||
}
|
||||
|
||||
class _LinkedTextState() extends State<_LinkedText> {
|
||||
final _taps = <TapGestureRecognizer>[];
|
||||
|
||||
void _release() {
|
||||
for (final tap in _taps) {
|
||||
tap.dispose();
|
||||
}
|
||||
_taps.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_release();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_release();
|
||||
final link = TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
return SelectableText.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
for (final piece in linkPieces(widget.text))
|
||||
switch (piece.url) {
|
||||
final url? => TextSpan(
|
||||
text: piece.text,
|
||||
style: link,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => unawaited(
|
||||
launchUrl(url, mode: LaunchMode.externalApplication),
|
||||
),
|
||||
),
|
||||
null => TextSpan(text: piece.text),
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class const _EditComment({required final String text}) extends StatefulWidget {
|
||||
@override
|
||||
State<_EditComment> createState() => _EditCommentState();
|
||||
}
|
||||
|
||||
class _EditCommentState() extends State<_EditComment> {
|
||||
late final _text = TextEditingController(text: widget.text);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_text.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return AlertDialog(
|
||||
title: Text(l10n.commentEdit),
|
||||
content: TextField(
|
||||
controller: _text,
|
||||
autofocus: true,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(_text.text.trim()),
|
||||
child: Text(l10n.save),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue