diff --git a/drizzle/0008_kanban_status_type.sql b/drizzle/0008_kanban_status_type.sql new file mode 100644 index 0000000..f868e5d --- /dev/null +++ b/drizzle/0008_kanban_status_type.sql @@ -0,0 +1,7 @@ +ALTER TABLE `kanban_columns` ADD `status_type` text DEFAULT 'backlog' NOT NULL; +--> statement-breakpoint +UPDATE `kanban_columns` SET `status_type` = 'planned' WHERE `slug` = 'todo'; +--> statement-breakpoint +UPDATE `kanban_columns` SET `status_type` = 'started' WHERE `slug` = 'in-progress'; +--> statement-breakpoint +UPDATE `kanban_columns` SET `status_type` = 'completed' WHERE `slug` = 'done'; diff --git a/drizzle/0009_damp_microbe.sql b/drizzle/0009_damp_microbe.sql new file mode 100644 index 0000000..785d837 --- /dev/null +++ b/drizzle/0009_damp_microbe.sql @@ -0,0 +1,11 @@ +ALTER TABLE `feature_requests` RENAME COLUMN "label_tid" TO "label_updated_at";--> statement-breakpoint +ALTER TABLE `kanban_tasks` RENAME COLUMN "label_tid" TO "label_updated_at";--> statement-breakpoint +CREATE TABLE `label_pds_records` ( + `entity_id` text NOT NULL, + `entity_type` text NOT NULL, + `actor_did` text NOT NULL, + `rkey` text NOT NULL, + PRIMARY KEY(`entity_id`, `entity_type`, `actor_did`) +); +--> statement-breakpoint +ALTER TABLE `kanban_columns` ADD `status_type` text DEFAULT 'backlog' NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..815acfe --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,1581 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "e3c5292c-aa87-4b07-8df2-08ed9e38c774", + "prevId": "eec14d3f-541d-45c5-844c-4b45eb164b0c", + "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": {} + }, + "label_pds_records": { + "name": "label_pds_records", + "columns": { + "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 + }, + "actor_did": { + "name": "actor_did", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rkey": { + "name": "rkey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "label_pds_records_entity_id_entity_type_actor_did_pk": { + "columns": [ + "entity_id", + "entity_type", + "actor_did" + ], + "name": "label_pds_records_entity_id_entity_type_actor_did_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 + }, + "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_updated_at": { + "name": "label_updated_at", + "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 + } + }, + "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 + }, + "status_type": { + "name": "status_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'backlog'" + }, + "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_updated_at": { + "name": "label_updated_at", + "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": { + "\"feature_requests\".\"label_tid\"": "\"feature_requests\".\"label_updated_at\"", + "\"kanban_tasks\".\"label_tid\"": "\"kanban_tasks\".\"label_updated_at\"" + } + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 389910c..0768ee9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -57,6 +57,20 @@ "when": 1776681600000, "tag": "0007_label_pds_records", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1776800000000, + "tag": "0008_kanban_status_type", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1776516974088, + "tag": "0009_damp_microbe", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/packages/core/src/generated/lexicon-records.ts b/packages/core/src/generated/lexicon-records.ts index 76fbc8c..e51e95c 100644 --- a/packages/core/src/generated/lexicon-records.ts +++ b/packages/core/src/generated/lexicon-records.ts @@ -62,6 +62,8 @@ export interface KanbanEntryRecord { description?: string; /** Column slug the task belongs to. Values are user-defined per Sphere. */ status: string; + /** Canonical status type the column maps to. Third-party indexers use this to categorize user-defined slugs without needing the sphere's column config. */ + statusType: string; /** did — DID of the Sphere owner (the identity hosting the site.exosphere.sphere.profile record). */ subject: string; /** did — DID of the member assigned to this task. */ @@ -92,6 +94,8 @@ export interface KanbanStatusRecord { subject: string; /** Column slug the task is moved to. Values are user-defined per Sphere. */ status: string; + /** Canonical status type the column maps to. Third-party indexers use this to categorize user-defined slugs without needing the sphere's column config. */ + statusType: string; } export interface ModerationRecord { diff --git a/packages/kanban/src/__tests__/schemas.test.ts b/packages/kanban/src/__tests__/schemas.test.ts index c0ae7e6..023e660 100644 --- a/packages/kanban/src/__tests__/schemas.test.ts +++ b/packages/kanban/src/__tests__/schemas.test.ts @@ -6,8 +6,11 @@ import { updateStatusSchema, assignTaskSchema, reorderSchema, + defaultColumns, } from "../schemas/task.ts"; import { createCommentSchema, updateCommentSchema } from "../schemas/comment.ts"; +import { createColumnSchema, updateColumnSchema } from "../schemas/column.ts"; +import { STATUS_TYPES } from "../schemas/status-type.ts"; describe("createTaskSchema", () => { const valid = { title: "Implement auth flow" }; @@ -142,3 +145,48 @@ describe("updateCommentSchema", () => { expect(() => updateCommentSchema.parse({ content: "" })).toThrow(); }); }); + +describe("createColumnSchema", () => { + it("accepts a valid label and statusType", () => { + const result = createColumnSchema.parse({ label: "In review", statusType: "started" }); + expect(result.label).toBe("In review"); + expect(result.statusType).toBe("started"); + }); + + it("rejects an unknown statusType", () => { + expect(() => createColumnSchema.parse({ label: "Weird", statusType: "made-up" })).toThrow(); + }); + + it("rejects a missing statusType", () => { + expect(() => createColumnSchema.parse({ label: "No type" })).toThrow(); + }); +}); + +describe("updateColumnSchema", () => { + it("accepts a label-only update", () => { + const result = updateColumnSchema.parse({ label: "Renamed" }); + expect(result.label).toBe("Renamed"); + expect(result.statusType).toBeUndefined(); + }); + + it("accepts a statusType-only update", () => { + const result = updateColumnSchema.parse({ statusType: "canceled" }); + expect(result.statusType).toBe("canceled"); + }); + + it("rejects an empty body", () => { + expect(() => updateColumnSchema.parse({})).toThrow(); + }); + + it("rejects an unknown statusType", () => { + expect(() => updateColumnSchema.parse({ statusType: "nope" })).toThrow(); + }); +}); + +describe("defaultColumns", () => { + it("every entry maps to a valid StatusType", () => { + for (const col of defaultColumns) { + expect(STATUS_TYPES).toContain(col.statusType); + } + }); +}); diff --git a/packages/kanban/src/api/columns.ts b/packages/kanban/src/api/columns.ts index 844a9c0..74774bb 100644 --- a/packages/kanban/src/api/columns.ts +++ b/packages/kanban/src/api/columns.ts @@ -12,21 +12,24 @@ import { import { getColumns, insertColumn, - updateColumnLabel, + updateColumn, deleteColumnAndReassign, reorderColumns, } from "../db/operations.ts"; import type { KanbanColumnDef } from "../types.ts"; +import type { StatusType } from "../schemas/status-type.ts"; +import type { KanbanColumn } from "../db/schema.ts"; const MODULE = "kanban"; -function toColumnDef(col: { - id: string; - slug: string; - label: string; - position: number; -}): KanbanColumnDef { - return { id: col.id, slug: col.slug, label: col.label, position: col.position }; +export function toColumnDef(col: KanbanColumn): KanbanColumnDef { + return { + id: col.id, + slug: col.slug, + label: col.label, + statusType: col.statusType as StatusType, + position: col.position, + }; } const app = new Hono(); @@ -43,11 +46,15 @@ app.post("/columns", requireAuth, requirePermission(MODULE, "manageSettings"), a const result = createColumnSchema.safeParse(body); if (!result.success) return c.json({ error: z.flattenError(result.error) }, 400); - const col = insertColumn({ sphereId: c.var.sphereId, label: result.data.label }); + const col = insertColumn({ + sphereId: c.var.sphereId, + label: result.data.label, + statusType: result.data.statusType, + }); return c.json({ column: toColumnDef(col) }, 201); }); -// Rename column +// Update column (rename and/or retype) app.put("/columns/:id", requireAuth, requirePermission(MODULE, "manageSettings"), async (c) => { const id = c.req.param("id"); const body = await c.req.json(); @@ -58,8 +65,14 @@ app.put("/columns/:id", requireAuth, requirePermission(MODULE, "manageSettings") const existing = cols.find((col) => col.id === id); if (!existing) return c.json({ error: "Column not found" }, 404); - updateColumnLabel(id, result.data.label); - return c.json({ column: toColumnDef({ ...existing, label: result.data.label }) }); + updateColumn(id, result.data); + return c.json({ + column: toColumnDef({ + ...existing, + label: result.data.label ?? existing.label, + statusType: result.data.statusType ?? existing.statusType, + }), + }); }); // Delete column (reassign tasks to another) diff --git a/packages/kanban/src/api/tasks.ts b/packages/kanban/src/api/tasks.ts index f5d594b..45f1b9d 100644 --- a/packages/kanban/src/api/tasks.ts +++ b/packages/kanban/src/api/tasks.ts @@ -34,6 +34,7 @@ import { setEntityLabelsSchema, writeLabelPdsRecord, } from "@exosphere/core/sphere"; +import { toColumnDef } from "./columns.ts"; const COLLECTION = "site.exosphere.kanban.entry"; const STATUS_COLLECTION = "site.exosphere.kanban.status"; @@ -159,7 +160,7 @@ app.get("/", async (c) => { } return c.json({ - columns: cols.map(({ id, slug, label, position }) => ({ id, slug, label, position })), + columns: cols.map(toColumnDef), tasksByColumn, tasks, }); @@ -183,9 +184,8 @@ app.post("/", requireAuth, requirePermission(MODULE, "create"), async (c) => { // Validate status against this sphere's columns; fall back to first column const cols = getColumns(sphereId); - if (!cols.some((col) => col.slug === status)) { - status = cols[0].slug; - } + const column = cols.find((col) => col.slug === status) ?? cols[0]; + status = column.slug; let pdsUri: string | null = null; @@ -195,6 +195,7 @@ app.post("/", requireAuth, requirePermission(MODULE, "create"), async (c) => { title, description, status, + statusType: column.statusType, subject: sphereOwnerDid, assigneeDid: assigneeDid ?? undefined, }); @@ -276,10 +277,12 @@ app.put("/:id", requireAuth, async (c) => { if (existing.pdsUri) { const session = c.var.session; + const column = getColumnBySlug(sphereId, existing.status); await putPdsRecord(session, COLLECTION, id, { title: fields.title ?? existing.title, description: fields.description ?? existing.description, status: existing.status, + statusType: column?.statusType ?? "backlog", subject: sphereOwnerDid, assigneeDid: existing.assigneeDid ?? undefined, updatedAt: new Date().toISOString(), @@ -343,7 +346,8 @@ app.post("/:id/status", requireAuth, requirePermission(MODULE, "changeStatus"), const sphereId = c.var.sphereId; const sphereVisibility = c.var.sphereVisibility; - if (!getColumnBySlug(sphereId, status)) { + const column = getColumnBySlug(sphereId, status); + if (!column) { return c.json({ error: "Unknown column" }, 400); } @@ -368,6 +372,7 @@ app.post("/:id/status", requireAuth, requirePermission(MODULE, "changeStatus"), pdsUri = await putPdsRecord(session, STATUS_COLLECTION, statusId, { subject: existing.pdsUri, status, + statusType: column.statusType, }); } insertStatusChangeAndUpdateTask({ diff --git a/packages/kanban/src/db/operations.ts b/packages/kanban/src/db/operations.ts index ab6b308..4ccfce2 100644 --- a/packages/kanban/src/db/operations.ts +++ b/packages/kanban/src/db/operations.ts @@ -14,6 +14,7 @@ import { } from "./schema.ts"; import type { KanbanColumn } from "./schema.ts"; import { defaultColumns } from "../schemas/task.ts"; +import type { StatusType } from "../schemas/status-type.ts"; const POSITION_GAP = 1000; @@ -51,6 +52,7 @@ export function seedDefaultColumns(sphereId: string): KanbanColumn[] { sphereId, slug: defaultColumns[i].slug, label: defaultColumns[i].label, + statusType: defaultColumns[i].statusType, position: (i + 1) * POSITION_GAP, }) .run(); @@ -88,7 +90,11 @@ function generateSlug(label: string, sphereId: string): string { } } -export function insertColumn(params: { sphereId: string; label: string }): KanbanColumn { +export function insertColumn(params: { + sphereId: string; + label: string; + statusType: StatusType; +}): KanbanColumn { const db = getDb(); const id = generateRkey(); const slug = generateSlug(params.label, params.sphereId); @@ -101,14 +107,24 @@ export function insertColumn(params: { sphereId: string; label: string }): Kanba const position = (lastPosition?.maxPosition ?? 0) + POSITION_GAP; db.insert(kanbanColumns) - .values({ id, sphereId: params.sphereId, slug, label: params.label, position }) + .values({ + id, + sphereId: params.sphereId, + slug, + label: params.label, + statusType: params.statusType, + position, + }) .run(); return db.select().from(kanbanColumns).where(eq(kanbanColumns.id, id)).get()!; } -export function updateColumnLabel(id: string, label: string): void { - getDb().update(kanbanColumns).set({ label }).where(eq(kanbanColumns.id, id)).run(); +export function updateColumn( + id: string, + fields: { label?: string; statusType?: StatusType }, +): void { + getDb().update(kanbanColumns).set(fields).where(eq(kanbanColumns.id, id)).run(); } export function deleteColumnAndReassign( diff --git a/packages/kanban/src/db/schema.ts b/packages/kanban/src/db/schema.ts index 571cb35..debc175 100644 --- a/packages/kanban/src/db/schema.ts +++ b/packages/kanban/src/db/schema.ts @@ -12,6 +12,7 @@ export const kanbanColumns = sqliteTable( .references(() => spheres.id), slug: text("slug").notNull(), label: text("label").notNull(), + statusType: text("status_type").notNull().default("backlog"), position: integer("position").notNull(), createdAt: text("created_at") .notNull() diff --git a/packages/kanban/src/schemas/column.ts b/packages/kanban/src/schemas/column.ts index 0d1bf6b..02d2b3e 100644 --- a/packages/kanban/src/schemas/column.ts +++ b/packages/kanban/src/schemas/column.ts @@ -1,12 +1,19 @@ import { z } from "zod"; +import { STATUS_TYPES } from "./status-type.ts"; export const createColumnSchema = z.object({ label: z.string().min(1).max(50), + statusType: z.enum(STATUS_TYPES), }); -export const updateColumnSchema = z.object({ - label: z.string().min(1).max(50), -}); +export const updateColumnSchema = z + .object({ + label: z.string().min(1).max(50).optional(), + statusType: z.enum(STATUS_TYPES).optional(), + }) + .refine((v) => v.label !== undefined || v.statusType !== undefined, { + message: "At least one of label or statusType must be provided", + }); export const reorderColumnsSchema = z.object({ columnIds: z.array(z.string().min(1)), diff --git a/packages/kanban/src/schemas/status-type.ts b/packages/kanban/src/schemas/status-type.ts new file mode 100644 index 0000000..04b3a10 --- /dev/null +++ b/packages/kanban/src/schemas/status-type.ts @@ -0,0 +1,3 @@ +export const STATUS_TYPES = ["backlog", "planned", "started", "completed", "canceled"] as const; + +export type StatusType = (typeof STATUS_TYPES)[number]; diff --git a/packages/kanban/src/schemas/task.ts b/packages/kanban/src/schemas/task.ts index 2d9cf71..23a5c46 100644 --- a/packages/kanban/src/schemas/task.ts +++ b/packages/kanban/src/schemas/task.ts @@ -1,12 +1,13 @@ import { z } from "zod"; +import type { StatusType } from "./status-type.ts"; /** Default columns seeded when a sphere's kanban board is first accessed. */ export const defaultColumns = [ - { slug: "backlog", label: "Backlog" }, - { slug: "todo", label: "To do" }, - { slug: "in-progress", label: "In progress" }, - { slug: "done", label: "Done" }, -] as const; + { slug: "backlog", label: "Backlog", statusType: "backlog" }, + { slug: "todo", label: "To do", statusType: "planned" }, + { slug: "in-progress", label: "In progress", statusType: "started" }, + { slug: "done", label: "Done", statusType: "completed" }, +] as const satisfies ReadonlyArray<{ slug: string; label: string; statusType: StatusType }>; export const createTaskSchema = z.object({ title: z.string().min(1).max(200), diff --git a/packages/kanban/src/types.ts b/packages/kanban/src/types.ts index 1ea67f3..ac0fa90 100644 --- a/packages/kanban/src/types.ts +++ b/packages/kanban/src/types.ts @@ -7,12 +7,14 @@ export type { import type { KanbanTask, KanbanTaskComment } from "./db/schema.ts"; import type { LabelInfo } from "@exosphere/core/sphere"; +import type { StatusType } from "./schemas/status-type.ts"; /** Column definition as returned by the API. */ export type KanbanColumnDef = { id: string; slug: string; label: string; + statusType: StatusType; position: number; }; diff --git a/packages/kanban/src/ui/api/columns.ts b/packages/kanban/src/ui/api/columns.ts index 71920ea..880918d 100644 --- a/packages/kanban/src/ui/api/columns.ts +++ b/packages/kanban/src/ui/api/columns.ts @@ -1,5 +1,6 @@ import { moduleFetch } from "@exosphere/client/module-api"; import type { KanbanColumnDef } from "../../types.ts"; +import type { StatusType } from "../../schemas/status-type.ts"; export type { KanbanColumnDef }; @@ -7,19 +8,19 @@ export function getColumns() { return moduleFetch<{ columns: KanbanColumnDef[] }>("/kanban/columns"); } -export function createColumn(label: string) { +export function createColumn(label: string, statusType: StatusType) { return moduleFetch<{ column: KanbanColumnDef }>("/kanban/columns", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ label }), + body: JSON.stringify({ label, statusType }), }); } -export function renameColumn(id: string, label: string) { +export function updateColumnApi(id: string, fields: { label?: string; statusType?: StatusType }) { return moduleFetch<{ column: KanbanColumnDef }>(`/kanban/columns/${encodeURIComponent(id)}`, { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ label }), + body: JSON.stringify(fields), }); } diff --git a/packages/kanban/src/ui/components/column-manager.tsx b/packages/kanban/src/ui/components/column-manager.tsx index b6bec55..8f6bf4e 100644 --- a/packages/kanban/src/ui/components/column-manager.tsx +++ b/packages/kanban/src/ui/components/column-manager.tsx @@ -4,7 +4,35 @@ import { GripVertical } from "lucide-preact"; import * as ui from "@exosphere/client/ui.css"; import * as kbUi from "../ui.css.ts"; import type { KanbanColumnDef } from "../../types.ts"; -import { createColumn, renameColumn, deleteColumnApi, reorderColumnsApi } from "../api/columns.ts"; +import { STATUS_TYPES, type StatusType } from "../../schemas/status-type.ts"; +import { + createColumn, + updateColumnApi, + deleteColumnApi, + reorderColumnsApi, +} from "../api/columns.ts"; + +function StatusTypeSelect({ + value, + onChange, +}: { + value: StatusType; + onChange: (v: StatusType) => void; +}) { + return ( + + ); +} export function ColumnManager({ columns, @@ -14,9 +42,11 @@ export function ColumnManager({ onChanged: () => void; }) { const newLabel = useSignal(""); + const newStatusType = useSignal("backlog"); const adding = useSignal(false); const editingId = useSignal(null); const editLabel = useSignal(""); + const editStatusType = useSignal("backlog"); const deletingId = useSignal(null); const reassignSlug = useSignal(""); const busy = useSignal(false); @@ -25,8 +55,9 @@ export function ColumnManager({ if (!newLabel.value.trim() || adding.value) return; adding.value = true; try { - await createColumn(newLabel.value.trim()); + await createColumn(newLabel.value.trim(), newStatusType.value); newLabel.value = ""; + newStatusType.value = "backlog"; onChanged(); } catch (err) { console.error("Failed to add column:", err); @@ -35,15 +66,20 @@ export function ColumnManager({ } }; - const handleRename = async (id: string) => { - if (!editLabel.value.trim() || busy.value) return; + const handleSaveEdit = async (col: KanbanColumnDef) => { + const label = editLabel.value.trim(); + if (!label || busy.value) return; + if (label === col.label && editStatusType.value === col.statusType) { + editingId.value = null; + return; + } busy.value = true; try { - await renameColumn(id, editLabel.value.trim()); + await updateColumnApi(col.id, { label, statusType: editStatusType.value }); editingId.value = null; onChanged(); } catch (err) { - console.error("Failed to rename column:", err); + console.error("Failed to update column:", err); } finally { busy.value = false; } @@ -125,6 +161,7 @@ export function ColumnManager({ const startEdit = (col: KanbanColumnDef) => { editingId.value = col.id; editLabel.value = col.label; + editStatusType.value = col.statusType; deletingId.value = null; }; @@ -157,22 +194,31 @@ export function ColumnManager({ {editingId.value === col.id ? ( - (editLabel.value = (e.target as HTMLInputElement).value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleRename(col.id); - if (e.key === "Escape") editingId.value = null; - }} - autoFocus - /> + <> + (editLabel.value = (e.target as HTMLInputElement).value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleSaveEdit(col); + if (e.key === "Escape") editingId.value = null; + }} + autoFocus + /> + (editStatusType.value = v)} + /> + ) : ( - - {col.label} ({col.slug}) - + <> + + {col.label} ({col.slug}) + + {col.statusType} + )} {editingId.value === col.id ? ( @@ -180,7 +226,7 @@ export function ColumnManager({ @@ -191,7 +237,7 @@ export function ColumnManager({ ) : ( <> {columns.length > 1 && (