diff --git a/drizzle/0005_volatile_mystique.sql b/drizzle/0005_volatile_mystique.sql new file mode 100644 index 0000000..8d88558 --- /dev/null +++ b/drizzle/0005_volatile_mystique.sql @@ -0,0 +1,2 @@ +ALTER TABLE `feature_requests` ADD `label_tid` text;--> statement-breakpoint +ALTER TABLE `kanban_tasks` ADD `label_tid` text; \ No newline at end of file diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..b17f8bb --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -0,0 +1,1401 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f024260b-c00f-40d3-981d-89f2c8962fe8", + "prevId": "36c47020-9349-4833-8827-8c5bde6e1b17", + "tables": { + "oauth_sessions": { + "name": "oauth_sessions", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session": { + "name": "session", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_states": { + "name": "oauth_states", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "indexer_cursor": { + "name": "indexer_cursor", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": "'jetstream'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sphere_entry_counter": { + "name": "sphere_entry_counter", + "columns": { + "sphere_id": { + "name": "sphere_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "last_number": { + "name": "last_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "sphere_entry_counter_sphere_id_spheres_id_fk": { + "name": "sphere_entry_counter_sphere_id_spheres_id_fk", + "tableFrom": "sphere_entry_counter", + "tableTo": "spheres", + "columnsFrom": ["sphere_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entity_labels": { + "name": "entity_labels", + "columns": { + "label_id": { + "name": "label_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_entity_labels_entity": { + "name": "idx_entity_labels_entity", + "columns": ["entity_id", "entity_type"], + "isUnique": false + } + }, + "foreignKeys": { + "entity_labels_label_id_sphere_labels_id_fk": { + "name": "entity_labels_label_id_sphere_labels_id_fk", + "tableFrom": "entity_labels", + "tableTo": "sphere_labels", + "columnsFrom": ["label_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "entity_labels_label_id_entity_id_pk": { + "columns": ["label_id", "entity_id"], + "name": "entity_labels_label_id_entity_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sphere_labels": { + "name": "sphere_labels", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sphere_id": { + "name": "sphere_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "idx_sphere_labels_sphere_name": { + "name": "idx_sphere_labels_sphere_name", + "columns": ["sphere_id", "name"], + "isUnique": true + }, + "idx_sphere_labels_sphere_position": { + "name": "idx_sphere_labels_sphere_position", + "columns": ["sphere_id", "position"], + "isUnique": false + } + }, + "foreignKeys": { + "sphere_labels_sphere_id_spheres_id_fk": { + "name": "sphere_labels_sphere_id_spheres_id_fk", + "tableFrom": "sphere_labels", + "tableTo": "spheres", + "columnsFrom": ["sphere_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sphere_members": { + "name": "sphere_members", + "columns": { + "sphere_id": { + "name": "sphere_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "did": { + "name": "did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'invited'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "approval_pds_uri": { + "name": "approval_pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "idx_sphere_members_did": { + "name": "idx_sphere_members_did", + "columns": ["did"], + "isUnique": false + } + }, + "foreignKeys": { + "sphere_members_sphere_id_spheres_id_fk": { + "name": "sphere_members_sphere_id_spheres_id_fk", + "tableFrom": "sphere_members", + "tableTo": "spheres", + "columnsFrom": ["sphere_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sphere_members_sphere_id_did_pk": { + "columns": ["sphere_id", "did"], + "name": "sphere_members_sphere_id_did_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sphere_modules": { + "name": "sphere_modules", + "columns": { + "sphere_id": { + "name": "sphere_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "module_name": { + "name": "module_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled_at": { + "name": "enabled_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "sphere_modules_sphere_id_spheres_id_fk": { + "name": "sphere_modules_sphere_id_spheres_id_fk", + "tableFrom": "sphere_modules", + "tableTo": "spheres", + "columnsFrom": ["sphere_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sphere_modules_sphere_id_module_name_pk": { + "columns": ["sphere_id", "module_name"], + "name": "sphere_modules_sphere_id_module_name_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sphere_permissions": { + "name": "sphere_permissions", + "columns": { + "sphere_id": { + "name": "sphere_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action_key": { + "name": "action_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "min_role": { + "name": "min_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": {}, + "foreignKeys": { + "sphere_permissions_sphere_id_spheres_id_fk": { + "name": "sphere_permissions_sphere_id_spheres_id_fk", + "tableFrom": "sphere_permissions", + "tableTo": "spheres", + "columnsFrom": ["sphere_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sphere_permissions_sphere_id_action_key_pk": { + "columns": ["sphere_id", "action_key"], + "name": "sphere_permissions_sphere_id_action_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "spheres": { + "name": "spheres", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "handle": { + "name": "handle", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'public'" + }, + "owner_did": { + "name": "owner_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "spheres_handle_unique": { + "name": "spheres_handle_unique", + "columns": ["handle"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feature_request_comment_votes": { + "name": "feature_request_comment_votes", + "columns": { + "comment_id": { + "name": "comment_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "idx_feature_request_comment_votes_comment": { + "name": "idx_feature_request_comment_votes_comment", + "columns": ["comment_id"], + "isUnique": false + } + }, + "foreignKeys": { + "feature_request_comment_votes_comment_id_feature_request_comments_id_fk": { + "name": "feature_request_comment_votes_comment_id_feature_request_comments_id_fk", + "tableFrom": "feature_request_comment_votes", + "tableTo": "feature_request_comments", + "columnsFrom": ["comment_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "feature_request_comment_votes_comment_id_author_did_pk": { + "columns": ["comment_id", "author_did"], + "name": "feature_request_comment_votes_comment_id_author_did_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feature_request_comments": { + "name": "feature_request_comments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "hidden_at": { + "name": "hidden_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "moderated_by": { + "name": "moderated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_feature_request_comments_request": { + "name": "idx_feature_request_comments_request", + "columns": ["request_id"], + "isUnique": false + }, + "idx_feature_request_comments_author_request": { + "name": "idx_feature_request_comments_author_request", + "columns": ["author_did", "request_id"], + "isUnique": false + } + }, + "foreignKeys": { + "feature_request_comments_request_id_feature_requests_id_fk": { + "name": "feature_request_comments_request_id_feature_requests_id_fk", + "tableFrom": "feature_request_comments", + "tableTo": "feature_requests", + "columnsFrom": ["request_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feature_request_statuses": { + "name": "feature_request_statuses", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_feature_request_statuses_request": { + "name": "idx_feature_request_statuses_request", + "columns": ["request_id"], + "isUnique": false + } + }, + "foreignKeys": { + "feature_request_statuses_request_id_feature_requests_id_fk": { + "name": "feature_request_statuses_request_id_feature_requests_id_fk", + "tableFrom": "feature_request_statuses", + "tableTo": "feature_requests", + "columnsFrom": ["request_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feature_request_votes": { + "name": "feature_request_votes", + "columns": { + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "idx_feature_request_votes_request": { + "name": "idx_feature_request_votes_request", + "columns": ["request_id"], + "isUnique": false + } + }, + "foreignKeys": { + "feature_request_votes_request_id_feature_requests_id_fk": { + "name": "feature_request_votes_request_id_feature_requests_id_fk", + "tableFrom": "feature_request_votes", + "tableTo": "feature_requests", + "columnsFrom": ["request_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "feature_request_votes_request_id_author_did_pk": { + "columns": ["request_id", "author_did"], + "name": "feature_request_votes_request_id_author_did_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feature_requests": { + "name": "feature_requests", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sphere_id": { + "name": "sphere_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'general'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'requested'" + }, + "duplicate_of_id": { + "name": "duplicate_of_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "moderated_by": { + "name": "moderated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label_tid": { + "name": "label_tid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "idx_feature_requests_sphere_number": { + "name": "idx_feature_requests_sphere_number", + "columns": ["sphere_id", "number"], + "isUnique": true + }, + "idx_feature_requests_sphere": { + "name": "idx_feature_requests_sphere", + "columns": ["sphere_id"], + "isUnique": false + }, + "idx_feature_requests_status": { + "name": "idx_feature_requests_status", + "columns": ["status"], + "isUnique": false + }, + "idx_feature_requests_category": { + "name": "idx_feature_requests_category", + "columns": ["category"], + "isUnique": false + } + }, + "foreignKeys": { + "feature_requests_sphere_id_spheres_id_fk": { + "name": "feature_requests_sphere_id_spheres_id_fk", + "tableFrom": "feature_requests", + "tableTo": "spheres", + "columnsFrom": ["sphere_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "feed_posts": { + "name": "feed_posts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "idx_feed_posts_parent": { + "name": "idx_feed_posts_parent", + "columns": ["parent_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "kanban_columns": { + "name": "kanban_columns", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sphere_id": { + "name": "sphere_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "idx_kanban_columns_sphere_slug": { + "name": "idx_kanban_columns_sphere_slug", + "columns": ["sphere_id", "slug"], + "isUnique": true + }, + "idx_kanban_columns_sphere_position": { + "name": "idx_kanban_columns_sphere_position", + "columns": ["sphere_id", "position"], + "isUnique": false + } + }, + "foreignKeys": { + "kanban_columns_sphere_id_spheres_id_fk": { + "name": "kanban_columns_sphere_id_spheres_id_fk", + "tableFrom": "kanban_columns", + "tableTo": "spheres", + "columnsFrom": ["sphere_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "kanban_task_comments": { + "name": "kanban_task_comments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + }, + "hidden_at": { + "name": "hidden_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "moderated_by": { + "name": "moderated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_kanban_task_comments_task": { + "name": "idx_kanban_task_comments_task", + "columns": ["task_id"], + "isUnique": false + }, + "idx_kanban_task_comments_author_task": { + "name": "idx_kanban_task_comments_author_task", + "columns": ["author_did", "task_id"], + "isUnique": false + } + }, + "foreignKeys": { + "kanban_task_comments_task_id_kanban_tasks_id_fk": { + "name": "kanban_task_comments_task_id_kanban_tasks_id_fk", + "tableFrom": "kanban_task_comments", + "tableTo": "kanban_tasks", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "kanban_task_status_changes": { + "name": "kanban_task_status_changes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_kanban_task_status_changes_task": { + "name": "idx_kanban_task_status_changes_task", + "columns": ["task_id"], + "isUnique": false + } + }, + "foreignKeys": { + "kanban_task_status_changes_task_id_kanban_tasks_id_fk": { + "name": "kanban_task_status_changes_task_id_kanban_tasks_id_fk", + "tableFrom": "kanban_task_status_changes", + "tableTo": "kanban_tasks", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "kanban_tasks": { + "name": "kanban_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sphere_id": { + "name": "sphere_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_did": { + "name": "author_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'backlog'" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "assignee_did": { + "name": "assignee_did", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pds_uri": { + "name": "pds_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "moderated_by": { + "name": "moderated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "label_tid": { + "name": "label_tid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(datetime('now'))" + } + }, + "indexes": { + "idx_kanban_tasks_sphere_number": { + "name": "idx_kanban_tasks_sphere_number", + "columns": ["sphere_id", "number"], + "isUnique": true + }, + "idx_kanban_tasks_sphere": { + "name": "idx_kanban_tasks_sphere", + "columns": ["sphere_id"], + "isUnique": false + }, + "idx_kanban_tasks_status": { + "name": "idx_kanban_tasks_status", + "columns": ["status"], + "isUnique": false + }, + "idx_kanban_tasks_sphere_status_position": { + "name": "idx_kanban_tasks_sphere_status_position", + "columns": ["sphere_id", "status", "position"], + "isUnique": false + } + }, + "foreignKeys": { + "kanban_tasks_sphere_id_spheres_id_fk": { + "name": "kanban_tasks_sphere_id_spheres_id_fk", + "tableFrom": "kanban_tasks", + "tableTo": "spheres", + "columnsFrom": ["sphere_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 0ee8afd..2d90b0d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1776203212384, "tag": "0004_melted_brother_voodoo", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1776250350625, + "tag": "0005_volatile_mystique", + "breakpoints": true } ] } diff --git a/packages/app/src/server.ts b/packages/app/src/server.ts index a942267..d7a5602 100644 --- a/packages/app/src/server.ts +++ b/packages/app/src/server.ts @@ -197,9 +197,7 @@ if (isProd) { } // For individual kanban tasks, also prefetch comments - const taskData = pageData["kanban-task"] as - | { task?: { id: string } } - | undefined; + const taskData = pageData["kanban-task"] as { task?: { id: string } } | undefined; if (taskData?.task?.id) { try { const res = await app.request( diff --git a/packages/app/src/vite-ssr-plugin.ts b/packages/app/src/vite-ssr-plugin.ts index c908b16..38a1c1a 100644 --- a/packages/app/src/vite-ssr-plugin.ts +++ b/packages/app/src/vite-ssr-plugin.ts @@ -167,15 +167,12 @@ export function ssrDevPlugin({ isMultiSphere = false } = {}): Plugin { } // For individual kanban tasks, also prefetch comments - const taskData = pageData["kanban-task"] as - | { task?: { id: string } } - | undefined; + const taskData = pageData["kanban-task"] as { task?: { id: string } } | undefined; if (taskData?.task?.id) { try { - const res = await fetch( - `${apiBase}/kanban/${taskData.task.id}/comments`, - { headers: { cookie } }, - ); + const res = await fetch(`${apiBase}/kanban/${taskData.task.id}/comments`, { + headers: { cookie }, + }); if (res.ok) pageData["kanban-task-comments"] = await res.json(); } catch { /* prefetch failed — client will fetch */ diff --git a/packages/core/src/db/migrate.ts b/packages/core/src/db/migrate.ts index 44acab6..b1ee115 100644 --- a/packages/core/src/db/migrate.ts +++ b/packages/core/src/db/migrate.ts @@ -4,6 +4,14 @@ import { getDb } from "./index.ts"; const migrationsFolder = process.env.MIGRATIONS_PATH || "drizzle"; +const categoryDefaults = [ + { name: "General", slug: "general", color: "#6b7280", position: 0 }, + { name: "Enhancement", slug: "enhancement", color: "#3b82f6", position: 1 }, + { name: "Bug", slug: "bug", color: "#ef4444", position: 2 }, + { name: "Integration", slug: "integration", color: "#8b5cf6", position: 3 }, + { name: "UI / UX", slug: "ui-ux", color: "#f59e0b", position: 4 }, +] as const; + migrate(getDb(), { migrationsFolder }); // Data migration: renumber feature requests and kanban tasks to use the @@ -89,14 +97,6 @@ function seedEntryCounters() { db.close(); } -const categoryDefaults = [ - { name: "General", slug: "general", color: "#6b7280", position: 0 }, - { name: "Enhancement", slug: "enhancement", color: "#3b82f6", position: 1 }, - { name: "Bug", slug: "bug", color: "#ef4444", position: 2 }, - { name: "Integration", slug: "integration", color: "#8b5cf6", position: 3 }, - { name: "UI / UX", slug: "ui-ux", color: "#f59e0b", position: 4 }, -] as const; - function seedLabelsFromCategories() { const dbPath = process.env.DATABASE_PATH || "exosphere.sqlite"; const db = new Database(dbPath); diff --git a/packages/core/src/generated/lexicon-records.ts b/packages/core/src/generated/lexicon-records.ts index 979b563..e771ebc 100644 --- a/packages/core/src/generated/lexicon-records.ts +++ b/packages/core/src/generated/lexicon-records.ts @@ -95,6 +95,13 @@ export interface KanbanStatusRecord { status: string; } +export interface LabelRecord { + /** at-uri — AT URI of the labeled entity. */ + subject: string; + /** Label names assigned to the entity. Replaces any previous set. */ + labels: string[]; +} + export interface ModerationRecord { /** at-uri — AT URI of the Sphere. */ sphere: string; @@ -156,6 +163,7 @@ export interface PdsRecordMap { "site.exosphere.kanban.entry": KanbanEntryRecord; "site.exosphere.kanban.permissions": KanbanPermissionsRecord; "site.exosphere.kanban.status": KanbanStatusRecord; + "site.exosphere.label": LabelRecord; "site.exosphere.moderation": ModerationRecord; "site.exosphere.sphere.member": SphereMemberRecord; "site.exosphere.sphere.memberApproval": SphereMemberApprovalRecord; diff --git a/packages/core/src/sphere/index.ts b/packages/core/src/sphere/index.ts index 84f809c..6ea0af5 100644 --- a/packages/core/src/sphere/index.ts +++ b/packages/core/src/sphere/index.ts @@ -5,9 +5,19 @@ export { getPendingInvitations, } from "./routes.ts"; export { createCoreIndexer } from "./indexer.ts"; -export { getActiveMemberRole, registerModerationHandler, findSphereByAtUri } from "./operations.ts"; -export type { ModerationHandler } from "./operations.ts"; +export { + getActiveMemberRole, + registerModerationHandler, + registerLabelHandler, + findSphereByAtUri, +} from "./operations.ts"; +export type { ModerationHandler, LabelHandler } from "./operations.ts"; export { sphereContext } from "./middleware.ts"; -export { getLabelsForSphere, getLabelsForEntities, setEntityLabels } from "./label-operations.ts"; +export { + getLabelsForSphere, + getLabelsForEntities, + setEntityLabels, + resolveLabelIdsByName, +} from "./label-operations.ts"; export type { LabelInfo } from "./label-operations.ts"; export { setEntityLabelsSchema } from "./schemas.ts"; diff --git a/packages/core/src/sphere/indexer.ts b/packages/core/src/sphere/indexer.ts index 9055cca..2faf83f 100644 --- a/packages/core/src/sphere/indexer.ts +++ b/packages/core/src/sphere/indexer.ts @@ -7,6 +7,7 @@ import { deleteSphereMemberApproval, deleteSphereMember, indexModerationAction, + indexLabelAssignment, syncModulePermissionsFromRecord, deleteModulePermissions, } from "./operations.ts"; @@ -16,6 +17,7 @@ const SPHERE_COLLECTION = "site.exosphere.sphere.profile"; const MEMBER_COLLECTION = "site.exosphere.sphere.member"; const APPROVAL_COLLECTION = "site.exosphere.sphere.memberApproval"; const MODERATION_COLLECTION = "site.exosphere.moderation"; +const LABEL_COLLECTION = "site.exosphere.label"; /** Build the core indexer dynamically — includes permission collections from registered modules. */ export function createCoreIndexer(): ModuleIndexer { @@ -31,6 +33,7 @@ export function createCoreIndexer(): ModuleIndexer { MEMBER_COLLECTION, APPROVAL_COLLECTION, MODERATION_COLLECTION, + LABEL_COLLECTION, ]; const allCollections = [...baseCollections, ...collectionToModule.keys()]; @@ -64,6 +67,9 @@ export function createCoreIndexer(): ModuleIndexer { case MODERATION_COLLECTION: indexModerationAction(did, record); break; + case LABEL_COLLECTION: + indexLabelAssignment(did, rkey, record); + break; } }, diff --git a/packages/core/src/sphere/label-operations.ts b/packages/core/src/sphere/label-operations.ts index aa773a7..74bcd11 100644 --- a/packages/core/src/sphere/label-operations.ts +++ b/packages/core/src/sphere/label-operations.ts @@ -93,6 +93,17 @@ export function reorderLabels(sphereId: string, labelIds: string[]): void { export type LabelInfo = { id: string; name: string; color: string }; +/** Resolve label names to local IDs for a sphere. Unknown names are silently skipped. */ +export function resolveLabelIdsByName(sphereId: string, names: string[]): string[] { + if (names.length === 0) return []; + return getDb() + .select({ id: sphereLabels.id }) + .from(sphereLabels) + .where(and(eq(sphereLabels.sphereId, sphereId), inArray(sphereLabels.name, names))) + .all() + .map((r) => r.id); +} + /** Batch-fetch labels for multiple entities. Returns a Map keyed by entity ID. */ export function getLabelsForEntities( entityIds: string[], diff --git a/packages/core/src/sphere/operations.ts b/packages/core/src/sphere/operations.ts index a74601d..5645f91 100644 --- a/packages/core/src/sphere/operations.ts +++ b/packages/core/src/sphere/operations.ts @@ -293,3 +293,35 @@ export function indexModerationAction(did: string, record: Record boolean; + +const labelHandlers: LabelHandler[] = []; + +/** Register a module-level handler for label assignment records. */ +export function registerLabelHandler(handler: LabelHandler): void { + labelHandlers.push(handler); +} + +export function indexLabelAssignment( + did: string, + rkey: string, + record: Record, +): void { + const subjectUri = record.subject as string; + const labels = record.labels as string[]; + if (!subjectUri || !Array.isArray(labels)) return; + + for (const handler of labelHandlers) { + if (handler(subjectUri, labels, did, rkey)) return; + } +} diff --git a/packages/feature-requests/src/api/requests.ts b/packages/feature-requests/src/api/requests.ts index 14bf50b..83e9a13 100644 --- a/packages/feature-requests/src/api/requests.ts +++ b/packages/feature-requests/src/api/requests.ts @@ -24,6 +24,7 @@ import { } from "@exosphere/core/sphere"; const COLLECTION = "site.exosphere.featureRequest.entry"; +const LABEL_COLLECTION = "site.exosphere.label"; const MODERATION_COLLECTION = "site.exosphere.moderation"; const app = new Hono(); @@ -176,13 +177,12 @@ app.post("/", requireAuth, requirePermission("feature-requests", "create"), asyn let pdsUri: string | null = null; - // Write to PDS for public spheres + // Write to PDS for public spheres (category deprecated — no longer written) if (sphereVisibility === "public") { const session = c.var.session; pdsUri = await putPdsRecord(session, COLLECTION, id, { title, description, - category, subject: sphereOwnerDid, }); } @@ -205,6 +205,20 @@ app.post("/", requireAuth, requirePermission("feature-requests", "create"), asyn ? (getLabelsForEntities([id], "feature-request").get(id) ?? []) : []; + // Write label PDS record if labels were set and entity has a PDS URI + if (pdsUri && labels.length > 0) { + const session = c.var.session; + const labelTid = generateRkey(); + const labelPdsUri = await putPdsRecord(session, LABEL_COLLECTION, labelTid, { + subject: pdsUri, + labels: labels.map((l) => l.name), + }); + if (labelPdsUri) { + const db = getDb(); + db.update(featureRequests).set({ labelTid }).where(eq(featureRequests.id, id)).run(); + } + } + return c.json({ featureRequest: row ? { ...row, createdAt: tidToDate(id), labels } : row }, 201); }); @@ -403,7 +417,11 @@ app.post("/:id/labels", requireAuth, async (c) => { const sphereId = c.var.sphereId; const existing = db - .select({ id: featureRequests.id, authorDid: featureRequests.authorDid }) + .select({ + id: featureRequests.id, + authorDid: featureRequests.authorDid, + pdsUri: featureRequests.pdsUri, + }) .from(featureRequests) .where(and(eq(featureRequests.id, id), eq(featureRequests.sphereId, sphereId))) .get(); @@ -426,6 +444,20 @@ app.post("/:id/labels", requireAuth, async (c) => { setEntityLabels(sphereId, id, "feature-request", parsed.data.labelIds); const labels = getLabelsForEntities([id], "feature-request").get(id) ?? []; + + // Write label PDS record + if (c.var.sphereVisibility === "public" && existing.pdsUri) { + const session = c.var.session; + const labelTid = generateRkey(); + const labelPdsUri = await putPdsRecord(session, LABEL_COLLECTION, labelTid, { + subject: existing.pdsUri, + labels: labels.map((l) => l.name), + }); + if (labelPdsUri) { + db.update(featureRequests).set({ labelTid }).where(eq(featureRequests.id, id)).run(); + } + } + return c.json({ labels }); }); diff --git a/packages/feature-requests/src/db/schema.ts b/packages/feature-requests/src/db/schema.ts index eebc40a..5ae6b55 100644 --- a/packages/feature-requests/src/db/schema.ts +++ b/packages/feature-requests/src/db/schema.ts @@ -32,6 +32,7 @@ export const featureRequests = sqliteTable( pdsUri: text("pds_uri"), hiddenAt: text("hidden_at"), moderatedBy: text("moderated_by"), + labelTid: text("label_tid"), updatedAt: text("updated_at") .notNull() .default(sql`(datetime('now'))`), diff --git a/packages/feature-requests/src/indexer.ts b/packages/feature-requests/src/indexer.ts index cf5ca16..3c6f62f 100644 --- a/packages/feature-requests/src/indexer.ts +++ b/packages/feature-requests/src/indexer.ts @@ -1,6 +1,12 @@ import type { ModuleIndexer, JetstreamCommitEvent } from "@exosphere/core/types"; import { buildAtUri, parseAtUri } from "@exosphere/core/indexer"; -import { registerModerationHandler, getActiveMemberRole } from "@exosphere/core/sphere"; +import { + registerModerationHandler, + registerLabelHandler, + getActiveMemberRole, + setEntityLabels, + resolveLabelIdsByName, +} from "@exosphere/core/sphere"; import { checkPermission } from "@exosphere/core/permissions"; import { getDb } from "@exosphere/core/db"; import { eq, and } from "@exosphere/core/db/drizzle"; @@ -21,9 +27,6 @@ import { handleFeatureRequestModeration, } from "./db/operations.ts"; -// Register this module's moderation handler with core -registerModerationHandler(handleFeatureRequestModeration); - const MODULE_NAME = "feature-requests"; const COLLECTION = "site.exosphere.featureRequest.entry"; const VOTE_COLLECTION = "site.exosphere.featureRequest.vote"; @@ -31,6 +34,42 @@ const COMMENT_COLLECTION = "site.exosphere.featureRequest.comment"; const COMMENT_VOTE_COLLECTION = "site.exosphere.featureRequest.commentVote"; const STATUS_COLLECTION = "site.exosphere.featureRequest.status"; +// Register this module's moderation handler with core +registerModerationHandler(handleFeatureRequestModeration); + +// Register label handler: resolve label PDS records targeting feature requests +registerLabelHandler((subjectUri, labelNames, actorDid, tid) => { + const parsed = parseAtUri(subjectUri); + if (!parsed || parsed.collection !== COLLECTION) return false; + + const db = getDb(); + const fr = db + .select({ + id: featureRequests.id, + sphereId: featureRequests.sphereId, + authorDid: featureRequests.authorDid, + labelTid: featureRequests.labelTid, + }) + .from(featureRequests) + .where(eq(featureRequests.id, parsed.rkey)) + .get(); + if (!fr) return false; + + // TID-based ordering: skip if we already have a newer label record + if (fr.labelTid && fr.labelTid >= tid) return true; + + // Permission check: author can always set labels, otherwise need changeStatus + if (fr.authorDid !== actorDid) { + const role = getActiveMemberRole(fr.sphereId, actorDid); + if (!checkPermission(fr.sphereId, MODULE_NAME, "changeStatus", role)) return true; + } + + const labelIds = resolveLabelIdsByName(fr.sphereId, labelNames); + setEntityLabels(fr.sphereId, fr.id, "feature-request", labelIds); + db.update(featureRequests).set({ labelTid: tid }).where(eq(featureRequests.id, fr.id)).run(); + return true; +}); + function findSphereForAccess( sphereOwnerDid: string, did: string, diff --git a/packages/kanban/src/api/tasks.ts b/packages/kanban/src/api/tasks.ts index 180ddd9..72a4fab 100644 --- a/packages/kanban/src/api/tasks.ts +++ b/packages/kanban/src/api/tasks.ts @@ -35,6 +35,7 @@ import { } from "@exosphere/core/sphere"; const COLLECTION = "site.exosphere.kanban.entry"; +const LABEL_COLLECTION = "site.exosphere.label"; const STATUS_COLLECTION = "site.exosphere.kanban.status"; const MODERATION_COLLECTION = "site.exosphere.moderation"; @@ -216,6 +217,20 @@ app.post("/", requireAuth, requirePermission(MODULE, "create"), async (c) => { const labels = labelIds?.length ? (getLabelsForEntities([id], "kanban-task").get(id) ?? []) : []; + // Write label PDS record if labels were set and entity has a PDS URI + if (pdsUri && labels.length > 0) { + const session = c.var.session; + const labelTid = generateRkey(); + const labelPdsUri = await putPdsRecord(session, LABEL_COLLECTION, labelTid, { + subject: pdsUri, + labels: labels.map((l) => l.name), + }); + if (labelPdsUri) { + const db = getDb(); + db.update(kanbanTasks).set({ labelTid }).where(eq(kanbanTasks.id, id)).run(); + } + } + return c.json({ task: row ? { ...row, createdAt: tidToDate(id), labels } : row }, 201); }); @@ -573,7 +588,11 @@ app.post("/:id/labels", requireAuth, async (c) => { const sphereId = c.var.sphereId; const existing = db - .select({ id: kanbanTasks.id, authorDid: kanbanTasks.authorDid }) + .select({ + id: kanbanTasks.id, + authorDid: kanbanTasks.authorDid, + pdsUri: kanbanTasks.pdsUri, + }) .from(kanbanTasks) .where(and(eq(kanbanTasks.id, id), eq(kanbanTasks.sphereId, sphereId))) .get(); @@ -596,6 +615,20 @@ app.post("/:id/labels", requireAuth, async (c) => { setEntityLabels(sphereId, id, "kanban-task", parsed.data.labelIds); const labels = getLabelsForEntities([id], "kanban-task").get(id) ?? []; + + // Write label PDS record + if (c.var.sphereVisibility === "public" && existing.pdsUri) { + const session = c.var.session; + const labelTid = generateRkey(); + const labelPdsUri = await putPdsRecord(session, LABEL_COLLECTION, labelTid, { + subject: existing.pdsUri, + labels: labels.map((l) => l.name), + }); + if (labelPdsUri) { + db.update(kanbanTasks).set({ labelTid }).where(eq(kanbanTasks.id, id)).run(); + } + } + return c.json({ labels }); }); diff --git a/packages/kanban/src/db/schema.ts b/packages/kanban/src/db/schema.ts index 621dcb8..54caaed 100644 --- a/packages/kanban/src/db/schema.ts +++ b/packages/kanban/src/db/schema.ts @@ -40,6 +40,7 @@ export const kanbanTasks = sqliteTable( pdsUri: text("pds_uri"), hiddenAt: text("hidden_at"), moderatedBy: text("moderated_by"), + labelTid: text("label_tid"), updatedAt: text("updated_at") .notNull() .default(sql`(datetime('now'))`), diff --git a/packages/kanban/src/indexer.ts b/packages/kanban/src/indexer.ts index d2f348f..e5f028e 100644 --- a/packages/kanban/src/indexer.ts +++ b/packages/kanban/src/indexer.ts @@ -1,6 +1,12 @@ import type { ModuleIndexer, JetstreamCommitEvent } from "@exosphere/core/types"; import { buildAtUri, parseAtUri } from "@exosphere/core/indexer"; -import { registerModerationHandler, getActiveMemberRole } from "@exosphere/core/sphere"; +import { + registerModerationHandler, + registerLabelHandler, + getActiveMemberRole, + setEntityLabels, + resolveLabelIdsByName, +} from "@exosphere/core/sphere"; import { checkPermission } from "@exosphere/core/permissions"; import { getDb } from "@exosphere/core/db"; import { eq, and } from "@exosphere/core/db/drizzle"; @@ -17,14 +23,47 @@ import { handleKanbanModeration, } from "./db/operations.ts"; -// Register this module's moderation handler with core -registerModerationHandler(handleKanbanModeration); - const MODULE_NAME = "kanban"; const COLLECTION = "site.exosphere.kanban.entry"; const COMMENT_COLLECTION = "site.exosphere.kanban.comment"; const STATUS_COLLECTION = "site.exosphere.kanban.status"; +// Register this module's moderation handler with core +registerModerationHandler(handleKanbanModeration); + +// Register label handler: resolve label PDS records targeting kanban tasks +registerLabelHandler((subjectUri, labelNames, actorDid, tid) => { + const parsed = parseAtUri(subjectUri); + if (!parsed || parsed.collection !== COLLECTION) return false; + + const db = getDb(); + const task = db + .select({ + id: kanbanTasks.id, + sphereId: kanbanTasks.sphereId, + authorDid: kanbanTasks.authorDid, + labelTid: kanbanTasks.labelTid, + }) + .from(kanbanTasks) + .where(eq(kanbanTasks.id, parsed.rkey)) + .get(); + if (!task) return false; + + // TID-based ordering: skip if we already have a newer label record + if (task.labelTid && task.labelTid >= tid) return true; + + // Permission check: author can always set labels, otherwise need manageTasks + if (task.authorDid !== actorDid) { + const role = getActiveMemberRole(task.sphereId, actorDid); + if (!checkPermission(task.sphereId, MODULE_NAME, "manageTasks", role)) return true; + } + + const labelIds = resolveLabelIdsByName(task.sphereId, labelNames); + setEntityLabels(task.sphereId, task.id, "kanban-task", labelIds); + db.update(kanbanTasks).set({ labelTid: tid }).where(eq(kanbanTasks.id, task.id)).run(); + return true; +}); + function findSphereForAccess( sphereOwnerDid: string, did: string, diff --git a/packages/kanban/src/ui/components/column.tsx b/packages/kanban/src/ui/components/column.tsx index 06ffc9c..4db1572 100644 --- a/packages/kanban/src/ui/components/column.tsx +++ b/packages/kanban/src/ui/components/column.tsx @@ -40,12 +40,7 @@ export function Column({ if (isOver) headerClasses.push(kbUi.columnHeaderDragOver); return ( -
+
{title} {tasks.length} diff --git a/packages/kanban/src/ui/pages/board.tsx b/packages/kanban/src/ui/pages/board.tsx index eb48fe4..ca46793 100644 --- a/packages/kanban/src/ui/pages/board.tsx +++ b/packages/kanban/src/ui/pages/board.tsx @@ -83,7 +83,11 @@ export function BoardPage() {
(showForm.value = false)} title="New task"> - + {pending && !data ? ( @@ -98,8 +102,14 @@ export function BoardPage() { {[0, 1].map((j) => (
-
-
+
+
))}
@@ -124,7 +134,9 @@ export function BoardPage() { onDrop={dnd.onColumnDrop(col.slug)} onCardDragStart={dnd.onDragStart} onCardDragEnd={dnd.onDragEnd} - onAddTask={isAuthenticated && canCreate.value ? () => openForm(col.slug) : undefined} + onAddTask={ + isAuthenticated && canCreate.value ? () => openForm(col.slug) : undefined + } /> ))}