From d5a0829badf0b9d8d5d3b22cb40772091a00684e Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Sun, 25 Jan 2026 00:15:14 +0000 Subject: [PATCH] issue types (task/bug) --- packages/backend/drizzle/0023_loving_raza.sql | 1 + .../backend/drizzle/0024_military_stryfe.sql | 2 + .../backend/drizzle/meta/0023_snapshot.json | 923 +++++++++++++++++ .../backend/drizzle/meta/0024_snapshot.json | 930 ++++++++++++++++++ packages/backend/drizzle/meta/_journal.json | 14 + packages/backend/src/db/queries/issues.ts | 1 + packages/backend/src/routes/issue/update.ts | 10 +- .../src/components/issue-comments.tsx | 2 +- .../frontend/src/components/issue-details.tsx | 68 +- .../frontend/src/components/issue-form.tsx | 87 +- .../frontend/src/components/issues-table.tsx | 13 + .../frontend/src/components/type-select.tsx | 59 ++ packages/frontend/src/components/ui/icon.tsx | 23 +- packages/frontend/src/main.tsx | 2 +- packages/shared/src/api-schemas.ts | 10 +- packages/shared/src/constants.ts | 1 + packages/shared/src/schema.ts | 14 +- todo.md | 13 +- 18 files changed, 2129 insertions(+), 44 deletions(-) create mode 100644 packages/backend/drizzle/0023_loving_raza.sql create mode 100644 packages/backend/drizzle/0024_military_stryfe.sql create mode 100644 packages/backend/drizzle/meta/0023_snapshot.json create mode 100644 packages/backend/drizzle/meta/0024_snapshot.json create mode 100644 packages/frontend/src/components/type-select.tsx diff --git a/packages/backend/drizzle/0023_loving_raza.sql b/packages/backend/drizzle/0023_loving_raza.sql new file mode 100644 index 0000000..de4c693 --- /dev/null +++ b/packages/backend/drizzle/0023_loving_raza.sql @@ -0,0 +1 @@ +ALTER TABLE "Organisation" ADD COLUMN "issueTypes" json DEFAULT '{"Task":{"icon":"checkBox","color":"#e4bd47"},"Bug":{"icon":"bug","color":"#ef4444"}}'::json NOT NULL; \ No newline at end of file diff --git a/packages/backend/drizzle/0024_military_stryfe.sql b/packages/backend/drizzle/0024_military_stryfe.sql new file mode 100644 index 0000000..258cd5c --- /dev/null +++ b/packages/backend/drizzle/0024_military_stryfe.sql @@ -0,0 +1,2 @@ +ALTER TABLE "Organisation" ALTER COLUMN "features" SET DEFAULT '{"userAvatars":true,"issueTypes":true,"issueStatus":true,"issueDescriptions":true,"issueTimeTracking":true,"issueAssignees":true,"issueAssigneesShownInTable":true,"issueCreator":true,"issueComments":true,"sprints":true}'::json;--> statement-breakpoint +ALTER TABLE "Issue" ADD COLUMN "type" varchar(16) DEFAULT 'Task' NOT NULL; \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0023_snapshot.json b/packages/backend/drizzle/meta/0023_snapshot.json new file mode 100644 index 0000000..5068086 --- /dev/null +++ b/packages/backend/drizzle/meta/0023_snapshot.json @@ -0,0 +1,923 @@ +{ + "id": "c0d457d4-fbb8-412b-bc31-6647297996ee", + "prevId": "a147434f-8cb1-41e1-aec4-e745a5030012", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Issue": { + "name": "Issue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Issue_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'TO DO'" + }, + "creatorId": { + "name": "creatorId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sprintId": { + "name": "sprintId", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_project_issue_number": { + "name": "unique_project_issue_number", + "columns": [ + { + "expression": "projectId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Issue_projectId_Project_id_fk": { + "name": "Issue_projectId_Project_id_fk", + "tableFrom": "Issue", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Issue_creatorId_User_id_fk": { + "name": "Issue_creatorId_User_id_fk", + "tableFrom": "Issue", + "tableTo": "User", + "columnsFrom": [ + "creatorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Issue_sprintId_Sprint_id_fk": { + "name": "Issue_sprintId_Sprint_id_fk", + "tableFrom": "Issue", + "tableTo": "Sprint", + "columnsFrom": [ + "sprintId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IssueAssignee": { + "name": "IssueAssignee", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "IssueAssignee_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "issueId": { + "name": "issueId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assignedAt": { + "name": "assignedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_issue_user": { + "name": "unique_issue_user", + "columns": [ + { + "expression": "issueId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "IssueAssignee_issueId_Issue_id_fk": { + "name": "IssueAssignee_issueId_Issue_id_fk", + "tableFrom": "IssueAssignee", + "tableTo": "Issue", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "IssueAssignee_userId_User_id_fk": { + "name": "IssueAssignee_userId_User_id_fk", + "tableFrom": "IssueAssignee", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IssueComment": { + "name": "IssueComment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "IssueComment_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "issueId": { + "name": "issueId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "IssueComment_issueId_Issue_id_fk": { + "name": "IssueComment_issueId_Issue_id_fk", + "tableFrom": "IssueComment", + "tableTo": "Issue", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "IssueComment_userId_User_id_fk": { + "name": "IssueComment_userId_User_id_fk", + "tableFrom": "IssueComment", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Organisation": { + "name": "Organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Organisation_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "iconURL": { + "name": "iconURL", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "statuses": { + "name": "statuses", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"TO DO\":\"#fafafa\",\"IN PROGRESS\":\"#f97316\",\"REVIEW\":\"#8952bc\",\"DONE\":\"#22c55e\",\"REJECTED\":\"#ef4444\",\"ARCHIVED\":\"#a1a1a1\",\"MERGED\":\"#a1a1a1\"}'::json" + }, + "issueTypes": { + "name": "issueTypes", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"Task\":{\"icon\":\"checkBox\",\"color\":\"#e4bd47\"},\"Bug\":{\"icon\":\"bug\",\"color\":\"#ef4444\"}}'::json" + }, + "features": { + "name": "features", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"userAvatars\":true,\"issueStatus\":true,\"issueDescriptions\":true,\"issueTimeTracking\":true,\"issueAssignees\":true,\"issueAssigneesShownInTable\":true,\"issueCreator\":true,\"issueComments\":true,\"sprints\":true}'::json" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Organisation_slug_unique": { + "name": "Organisation_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OrganisationMember": { + "name": "OrganisationMember", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "OrganisationMember_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "organisationId": { + "name": "organisationId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "OrganisationMember_organisationId_Organisation_id_fk": { + "name": "OrganisationMember_organisationId_Organisation_id_fk", + "tableFrom": "OrganisationMember", + "tableTo": "Organisation", + "columnsFrom": [ + "organisationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "OrganisationMember_userId_User_id_fk": { + "name": "OrganisationMember_userId_User_id_fk", + "tableFrom": "OrganisationMember", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Project": { + "name": "Project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Project_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "key": { + "name": "key", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "organisationId": { + "name": "organisationId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "creatorId": { + "name": "creatorId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Project_organisationId_Organisation_id_fk": { + "name": "Project_organisationId_Organisation_id_fk", + "tableFrom": "Project", + "tableTo": "Organisation", + "columnsFrom": [ + "organisationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Project_creatorId_User_id_fk": { + "name": "Project_creatorId_User_id_fk", + "tableFrom": "Project", + "tableTo": "User", + "columnsFrom": [ + "creatorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Session": { + "name": "Session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Session_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "csrfToken": { + "name": "csrfToken", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Session_userId_User_id_fk": { + "name": "Session_userId_User_id_fk", + "tableFrom": "Session", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Sprint": { + "name": "Sprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Sprint_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#a1a1a1'" + }, + "startDate": { + "name": "startDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Sprint_projectId_Project_id_fk": { + "name": "Sprint_projectId_Project_id_fk", + "tableFrom": "Sprint", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TimedSession": { + "name": "TimedSession", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "TimedSession_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issueId": { + "name": "issueId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamps": { + "name": "timestamps", + "type": "timestamp[]", + "primaryKey": false, + "notNull": true + }, + "endedAt": { + "name": "endedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "TimedSession_userId_User_id_fk": { + "name": "TimedSession_userId_User_id_fk", + "tableFrom": "TimedSession", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "TimedSession_issueId_Issue_id_fk": { + "name": "TimedSession_issueId_Issue_id_fk", + "tableFrom": "TimedSession", + "tableTo": "Issue", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "User_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatarURL": { + "name": "avatarURL", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "iconPreference": { + "name": "iconPreference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'lucide'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "User_username_unique": { + "name": "User_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0024_snapshot.json b/packages/backend/drizzle/meta/0024_snapshot.json new file mode 100644 index 0000000..c9400ed --- /dev/null +++ b/packages/backend/drizzle/meta/0024_snapshot.json @@ -0,0 +1,930 @@ +{ + "id": "95c0fe99-9c32-4baf-a635-3be4c92b90de", + "prevId": "c0d457d4-fbb8-412b-bc31-6647297996ee", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.Issue": { + "name": "Issue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Issue_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'Task'" + }, + "status": { + "name": "status", + "type": "varchar(24)", + "primaryKey": false, + "notNull": true, + "default": "'TO DO'" + }, + "title": { + "name": "title", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true + }, + "creatorId": { + "name": "creatorId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sprintId": { + "name": "sprintId", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "unique_project_issue_number": { + "name": "unique_project_issue_number", + "columns": [ + { + "expression": "projectId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Issue_projectId_Project_id_fk": { + "name": "Issue_projectId_Project_id_fk", + "tableFrom": "Issue", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Issue_creatorId_User_id_fk": { + "name": "Issue_creatorId_User_id_fk", + "tableFrom": "Issue", + "tableTo": "User", + "columnsFrom": [ + "creatorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Issue_sprintId_Sprint_id_fk": { + "name": "Issue_sprintId_Sprint_id_fk", + "tableFrom": "Issue", + "tableTo": "Sprint", + "columnsFrom": [ + "sprintId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IssueAssignee": { + "name": "IssueAssignee", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "IssueAssignee_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "issueId": { + "name": "issueId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assignedAt": { + "name": "assignedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "unique_issue_user": { + "name": "unique_issue_user", + "columns": [ + { + "expression": "issueId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "IssueAssignee_issueId_Issue_id_fk": { + "name": "IssueAssignee_issueId_Issue_id_fk", + "tableFrom": "IssueAssignee", + "tableTo": "Issue", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "IssueAssignee_userId_User_id_fk": { + "name": "IssueAssignee_userId_User_id_fk", + "tableFrom": "IssueAssignee", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.IssueComment": { + "name": "IssueComment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "IssueComment_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "issueId": { + "name": "issueId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "varchar(2048)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "IssueComment_issueId_Issue_id_fk": { + "name": "IssueComment_issueId_Issue_id_fk", + "tableFrom": "IssueComment", + "tableTo": "Issue", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "IssueComment_userId_User_id_fk": { + "name": "IssueComment_userId_User_id_fk", + "tableFrom": "IssueComment", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Organisation": { + "name": "Organisation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Organisation_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "iconURL": { + "name": "iconURL", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "statuses": { + "name": "statuses", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"TO DO\":\"#fafafa\",\"IN PROGRESS\":\"#f97316\",\"REVIEW\":\"#8952bc\",\"DONE\":\"#22c55e\",\"REJECTED\":\"#ef4444\",\"ARCHIVED\":\"#a1a1a1\",\"MERGED\":\"#a1a1a1\"}'::json" + }, + "issueTypes": { + "name": "issueTypes", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"Task\":{\"icon\":\"checkBox\",\"color\":\"#e4bd47\"},\"Bug\":{\"icon\":\"bug\",\"color\":\"#ef4444\"}}'::json" + }, + "features": { + "name": "features", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"userAvatars\":true,\"issueTypes\":true,\"issueStatus\":true,\"issueDescriptions\":true,\"issueTimeTracking\":true,\"issueAssignees\":true,\"issueAssigneesShownInTable\":true,\"issueCreator\":true,\"issueComments\":true,\"sprints\":true}'::json" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Organisation_slug_unique": { + "name": "Organisation_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OrganisationMember": { + "name": "OrganisationMember", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "OrganisationMember_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "organisationId": { + "name": "organisationId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "OrganisationMember_organisationId_Organisation_id_fk": { + "name": "OrganisationMember_organisationId_Organisation_id_fk", + "tableFrom": "OrganisationMember", + "tableTo": "Organisation", + "columnsFrom": [ + "organisationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "OrganisationMember_userId_User_id_fk": { + "name": "OrganisationMember_userId_User_id_fk", + "tableFrom": "OrganisationMember", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Project": { + "name": "Project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Project_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "key": { + "name": "key", + "type": "varchar(4)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "organisationId": { + "name": "organisationId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "creatorId": { + "name": "creatorId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Project_organisationId_Organisation_id_fk": { + "name": "Project_organisationId_Organisation_id_fk", + "tableFrom": "Project", + "tableTo": "Organisation", + "columnsFrom": [ + "organisationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "Project_creatorId_User_id_fk": { + "name": "Project_creatorId_User_id_fk", + "tableFrom": "Project", + "tableTo": "User", + "columnsFrom": [ + "creatorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Session": { + "name": "Session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Session_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "csrfToken": { + "name": "csrfToken", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Session_userId_User_id_fk": { + "name": "Session_userId_User_id_fk", + "tableFrom": "Session", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Sprint": { + "name": "Sprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Sprint_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "projectId": { + "name": "projectId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "default": "'#a1a1a1'" + }, + "startDate": { + "name": "startDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Sprint_projectId_Project_id_fk": { + "name": "Sprint_projectId_Project_id_fk", + "tableFrom": "Sprint", + "tableTo": "Project", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.TimedSession": { + "name": "TimedSession", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "TimedSession_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issueId": { + "name": "issueId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamps": { + "name": "timestamps", + "type": "timestamp[]", + "primaryKey": false, + "notNull": true + }, + "endedAt": { + "name": "endedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "TimedSession_userId_User_id_fk": { + "name": "TimedSession_userId_User_id_fk", + "tableFrom": "TimedSession", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "TimedSession_issueId_Issue_id_fk": { + "name": "TimedSession_issueId_Issue_id_fk", + "tableFrom": "TimedSession", + "tableTo": "Issue", + "columnsFrom": [ + "issueId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "User_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "avatarURL": { + "name": "avatarURL", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "iconPreference": { + "name": "iconPreference", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'lucide'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "User_username_unique": { + "name": "User_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/backend/drizzle/meta/_journal.json b/packages/backend/drizzle/meta/_journal.json index 12da009..8c5e050 100644 --- a/packages/backend/drizzle/meta/_journal.json +++ b/packages/backend/drizzle/meta/_journal.json @@ -162,6 +162,20 @@ "when": 1769265175455, "tag": "0022_unique_johnny_blaze", "breakpoints": true + }, + { + "idx": 23, + "version": "7", + "when": 1769294969462, + "tag": "0023_loving_raza", + "breakpoints": true + }, + { + "idx": 24, + "version": "7", + "when": 1769295524021, + "tag": "0024_military_stryfe", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/backend/src/db/queries/issues.ts b/packages/backend/src/db/queries/issues.ts index 070e831..88ee70d 100644 --- a/packages/backend/src/db/queries/issues.ts +++ b/packages/backend/src/db/queries/issues.ts @@ -62,6 +62,7 @@ export async function updateIssue( title?: string; description?: string; sprintId?: number | null; + type?: string; status?: string; }, ) { diff --git a/packages/backend/src/routes/issue/update.ts b/packages/backend/src/routes/issue/update.ts index 8484c7d..e0c3c43 100644 --- a/packages/backend/src/routes/issue/update.ts +++ b/packages/backend/src/routes/issue/update.ts @@ -13,12 +13,13 @@ export default async function issueUpdate(req: AuthedRequest) { const parsed = await parseJsonBody(req, IssueUpdateRequestSchema); if ("error" in parsed) return parsed.error; - const { id, title, description, status, assigneeIds, sprintId } = parsed.data; + const { id, title, description, type, status, assigneeIds, sprintId } = parsed.data; // check that at least one field is being updated if ( title === undefined && description === undefined && + type === undefined && status === undefined && assigneeIds === undefined && sprintId === undefined @@ -27,7 +28,11 @@ export default async function issueUpdate(req: AuthedRequest) { } const hasIssueFieldUpdates = - title !== undefined || description !== undefined || status !== undefined || sprintId !== undefined; + title !== undefined || + description !== undefined || + type !== undefined || + status !== undefined || + sprintId !== undefined; const existingIssue = await getIssueByID(id); if (!existingIssue) { @@ -53,6 +58,7 @@ export default async function issueUpdate(req: AuthedRequest) { title, description, sprintId, + type, status, }); } diff --git a/packages/frontend/src/components/issue-comments.tsx b/packages/frontend/src/components/issue-comments.tsx index 833e3ab..d376820 100644 --- a/packages/frontend/src/components/issue-comments.tsx +++ b/packages/frontend/src/components/issue-comments.tsx @@ -122,7 +122,7 @@ export function IssueComments({ issueId, className }: { issueId: number; classNa disabled={deletingId === comment.Comment.id} title="Delete comment" > - + ) : null} diff --git a/packages/frontend/src/components/issue-details.tsx b/packages/frontend/src/components/issue-details.tsx index 38be039..9f5236e 100644 --- a/packages/frontend/src/components/issue-details.tsx +++ b/packages/frontend/src/components/issue-details.tsx @@ -11,9 +11,10 @@ import { StatusSelect } from "@/components/status-select"; import StatusTag from "@/components/status-tag"; import { TimerDisplay } from "@/components/timer-display"; import { TimerModal } from "@/components/timer-modal"; +import { TypeSelect } from "@/components/type-select"; import { Button } from "@/components/ui/button"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; -import Icon from "@/components/ui/icon"; +import Icon, { type IconName } from "@/components/ui/icon"; import { IconButton } from "@/components/ui/icon-button"; import { Input } from "@/components/ui/input"; import { SelectTrigger } from "@/components/ui/select"; @@ -58,6 +59,7 @@ export function IssueDetails({ const [assigneeIds, setAssigneeIds] = useState([]); const [sprintId, setSprintId] = useState("unassigned"); const [status, setStatus] = useState(""); + const [type, setType] = useState(""); const [deleteOpen, setDeleteOpen] = useState(false); const [linkCopied, setLinkCopied] = useState(false); const copyTimeoutRef = useRef(null); @@ -72,6 +74,11 @@ export function IssueDetails({ const [isSavingDescription, setIsSavingDescription] = useState(false); const descriptionRef = useRef(null); + const issueTypes = (organisation?.Organisation.issueTypes ?? {}) as Record< + string, + { icon: string; color: string } + >; + const isAssignee = assigneeIds.some((id) => user?.id === Number(id)); const actualAssigneeIds = assigneeIds.filter((id) => id !== "unassigned"); const hasMultipleAssignees = actualAssigneeIds.length > 1; @@ -80,6 +87,7 @@ export function IssueDetails({ setSprintId(issueData.Issue.sprintId?.toString() ?? "unassigned"); setAssigneeIds(assigneesToStringArray(issueData.Assignees)); setStatus(issueData.Issue.status); + setType(issueData.Issue.type); setTitle(issueData.Issue.title); setOriginalTitle(issueData.Issue.title); setDescription(issueData.Issue.description); @@ -206,6 +214,38 @@ export function IssueDetails({ } }; + const handleTypeChange = async (value: string) => { + setType(value); + const typeConfig = issueTypes[value]; + + try { + await updateIssue.mutateAsync({ + id: issueData.Issue.id, + type: value, + }); + toast.success( + + {issueID(projectKey, issueData.Issue.number)}'s type updated to{" "} + {typeConfig ? ( + + + {value} + + ) : ( + value + )} + , + { dismissible: false }, + ); + } catch (error) { + console.error("error updating type:", error); + setType(issueData.Issue.type); + toast.error(`Error updating type: ${parseError(error as Error)}`, { + dismissible: false, + }); + } + }; + const handleDelete = () => { setDeleteOpen(true); }; @@ -310,7 +350,7 @@ export function IssueDetails({ {linkCopied ? : } - + @@ -321,6 +361,30 @@ export function IssueDetails({
+ {organisation?.Organisation.features.issueTypes && Object.keys(issueTypes).length > 0 && ( + { + const typeConfig = issueTypes[value]; + return ( + + {typeConfig ? ( + + ) : ( + Type + )} + + ); + }} + /> + )} {organisation?.Organisation.features.issueStatus && ( membersData.map((member) => member.User), [membersData]); const statuses = selectedOrganisation?.Organisation.statuses ?? {}; + const issueTypes = (selectedOrganisation?.Organisation.issueTypes ?? {}) as Record< + string, + { icon: string; color: string } + >; const statusOptions = useMemo(() => Object.keys(statuses), [statuses]); + const typeOptions = useMemo(() => Object.keys(issueTypes), [issueTypes]); const defaultStatus = statusOptions[0] ?? ""; + const defaultType = typeOptions[0] ?? ""; const [open, setOpen] = useState(false); const [title, setTitle] = useState(""); @@ -48,6 +56,7 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) { const [sprintId, setSprintId] = useState("unassigned"); const [assigneeIds, setAssigneeIds] = useState(["unassigned"]); const [status, setStatus] = useState(defaultStatus); + const [type, setType] = useState(defaultType); const [submitAttempted, setSubmitAttempted] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -58,6 +67,7 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) { setSprintId("unassigned"); setAssigneeIds(["unassigned"]); setStatus(defaultStatus); + setType(defaultType); setSubmitAttempted(false); setSubmitting(false); setError(null); @@ -103,6 +113,7 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) { sprintId: sprintId === "unassigned" ? null : Number(sprintId), assigneeIds: assigneeIds.filter((id) => id !== "unassigned").map((id) => Number(id)), status: status.trim() === "" ? undefined : status, + type: type.trim() === "" ? undefined : type, }); setOpen(false); reset(); @@ -136,27 +147,61 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) {
- {statusOptions.length > 0 && ( -
- - { - if (newValue.trim() === "") return; - setStatus(newValue); - }} - trigger={({ isOpen, value }) => ( - - - - )} - /> + {(typeOptions.length > 0 || statusOptions.length > 0) && ( +
+ {selectedOrganisation?.Organisation.features.issueTypes && typeOptions.length > 0 && ( +
+ + { + if (newValue.trim() === "") return; + setType(newValue); + }} + trigger={({ isOpen, value }) => { + const typeConfig = issueTypes[value]; + return ( + + {typeConfig ? ( + + ) : ( + Type + )} + + ); + }} + /> +
+ )} + {statusOptions.length > 0 && ( +
+ + { + if (newValue.trim() === "") return; + setStatus(newValue); + }} + trigger={({ isOpen, value }) => ( + + + + )} + /> +
+ )}
)} diff --git a/packages/frontend/src/components/issues-table.tsx b/packages/frontend/src/components/issues-table.tsx index d96d03a..3dd57bb 100644 --- a/packages/frontend/src/components/issues-table.tsx +++ b/packages/frontend/src/components/issues-table.tsx @@ -2,6 +2,7 @@ import { useMemo } from "react"; import Avatar from "@/components/avatar"; import { useSelection } from "@/components/selection-provider"; import StatusTag from "@/components/status-tag"; +import Icon, { type IconName } from "@/components/ui/icon"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { useIssues, useSelectedOrganisation, useSelectedProject } from "@/lib/query/hooks"; import { cn } from "@/lib/utils"; @@ -18,6 +19,10 @@ export function IssuesTable({ const selectedOrganisation = useSelectedOrganisation(); const selectedProject = useSelectedProject(); const statuses = selectedOrganisation?.Organisation.statuses ?? {}; + const issueTypes = (selectedOrganisation?.Organisation.issueTypes ?? {}) as Record< + string, + { icon: string; color: string } + >; const issues = useMemo(() => [...issuesData].reverse(), [issuesData]); @@ -90,6 +95,14 @@ export function IssuesTable({ onClick={handleLinkClick} className="flex items-center gap-2 min-w-0 w-full h-full px-2 py-1 text-inherit hover:underline decoration-transparent" > + {selectedOrganisation?.Organisation.features.issueTypes && + issueTypes[issueData.Issue.type] && ( + + )} {selectedOrganisation?.Organisation.features.issueStatus && (columns.status == null || columns.status === true) && ( diff --git a/packages/frontend/src/components/type-select.tsx b/packages/frontend/src/components/type-select.tsx new file mode 100644 index 0000000..388aa3b --- /dev/null +++ b/packages/frontend/src/components/type-select.tsx @@ -0,0 +1,59 @@ +import type { ReactNode } from "react"; +import { useState } from "react"; +import Icon, { type IconName } from "@/components/ui/icon"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +type IssueTypeConfig = { + icon: string; + color: string; +}; + +export function TypeSelect({ + issueTypes, + value, + onChange, + placeholder = "Select type", + trigger, +}: { + issueTypes: Record; + value: string; + onChange: (value: string) => void; + placeholder?: string; + trigger?: (args: { isOpen: boolean; value: string }) => ReactNode; +}) { + const [isOpen, setIsOpen] = useState(false); + const selectedType = issueTypes[value]; + + return ( + + ); +} diff --git a/packages/frontend/src/components/ui/icon.tsx b/packages/frontend/src/components/ui/icon.tsx index b5651a9..2e4c311 100644 --- a/packages/frontend/src/components/ui/icon.tsx +++ b/packages/frontend/src/components/ui/icon.tsx @@ -1,6 +1,7 @@ import { Alert as PixelAlert, Check as PixelCheck, + Checkbox as PixelCheckbox, ChevronDown as PixelChevronDown, ChevronLeft as PixelChevronLeft, ChevronRight as PixelChevronRight, @@ -8,6 +9,7 @@ import { Circle as PixelCircle, Clock as PixelClock, Close as PixelClose, + Debug as PixelDebug, Edit as PixelEdit, Home as PixelHome, InfoBox as PixelInfo, @@ -26,8 +28,10 @@ import { ViewportWide as PixelViewportWide, } from "@nsmr/pixelart-react"; import { + BugIcon as PhosphorBug, CheckIcon as PhosphorCheck, CheckCircleIcon as PhosphorCheckCircle, + CheckSquareIcon as PhosphorCheckSquare, CaretDownIcon as PhosphorChevronDown, CaretLeftIcon as PhosphorChevronLeft, CaretRightIcon as PhosphorChevronRight, @@ -59,6 +63,7 @@ import { import type { IconStyle } from "@sprint/shared"; import { AlertTriangle, + Bug, Check, CheckIcon, ChevronDown, @@ -84,6 +89,7 @@ import { OctagonXIcon, Plus, ServerIcon, + SquareCheck, Sun, Timer, Trash, @@ -95,10 +101,14 @@ import { } from "lucide-react"; import { useSessionSafe } from "@/components/session-provider"; +// lucide: https://lucide.dev/icons +// pixel: https://pixelarticons.com/ (CLICK "Legacy") - these ones are free const icons = { alertTriangle: { lucide: AlertTriangle, pixel: PixelAlert, phosphor: PhosphorWarning }, + bug: { lucide: Bug, pixel: PixelDebug, phosphor: PhosphorBug }, check: { lucide: Check, pixel: PixelCheck, phosphor: PhosphorCheck }, checkIcon: { lucide: CheckIcon, pixel: PixelCheck, phosphor: PhosphorCheck }, + checkBox: { lucide: SquareCheck, pixel: PixelCheckbox, phosphor: PhosphorCheckSquare }, chevronDown: { lucide: ChevronDown, pixel: PixelChevronDown, phosphor: PhosphorChevronDown }, chevronDownIcon: { lucide: ChevronDownIcon, pixel: PixelChevronDown, phosphor: PhosphorChevronDown }, chevronLeftIcon: { lucide: ChevronLeftIcon, pixel: PixelChevronLeft, phosphor: PhosphorChevronLeft }, @@ -149,6 +159,7 @@ export default function Icon({ icon, iconStyle, size = 24, + color, ...props }: { icon: IconName; @@ -174,12 +185,14 @@ export default function Icon({ ); diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx index 2e18358..ca29436 100644 --- a/packages/frontend/src/main.tsx +++ b/packages/frontend/src/main.tsx @@ -1,7 +1,7 @@ import "./App.css"; import React from "react"; import ReactDOM from "react-dom/client"; -import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; import { QueryProvider } from "@/components/query-provider"; import { SelectionProvider } from "@/components/selection-provider"; import { RequireAuth, SessionProvider } from "@/components/session-provider"; diff --git a/packages/shared/src/api-schemas.ts b/packages/shared/src/api-schemas.ts index f20a04b..6f2be76 100644 --- a/packages/shared/src/api-schemas.ts +++ b/packages/shared/src/api-schemas.ts @@ -4,6 +4,7 @@ import { ISSUE_DESCRIPTION_MAX_LENGTH, ISSUE_STATUS_MAX_LENGTH, ISSUE_TITLE_MAX_LENGTH, + ISSUE_TYPE_MAX_LENGTH, ORG_DESCRIPTION_MAX_LENGTH, ORG_NAME_MAX_LENGTH, ORG_SLUG_MAX_LENGTH, @@ -66,9 +67,10 @@ export type AuthResponse = z.infer; export const IssueCreateRequestSchema = z.object({ projectId: z.number().int().positive("projectId must be a positive integer"), + type: z.string().max(ISSUE_TYPE_MAX_LENGTH).optional(), + status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(), title: z.string().min(1, "Title is required").max(ISSUE_TITLE_MAX_LENGTH), description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).default(""), - status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(), assigneeIds: z.array(z.number().int().positive()).optional(), sprintId: z.number().int().positive().nullable().optional(), }); @@ -77,9 +79,10 @@ export type IssueCreateRequest = z.infer; export const IssueUpdateRequestSchema = z.object({ id: z.number().int().positive("id must be a positive integer"), + type: z.string().max(ISSUE_TYPE_MAX_LENGTH).optional(), + status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(), title: z.string().min(1, "Title must be at least 1 character").max(ISSUE_TITLE_MAX_LENGTH).optional(), description: z.string().max(ISSUE_DESCRIPTION_MAX_LENGTH).optional(), - status: z.string().max(ISSUE_STATUS_MAX_LENGTH).optional(), assigneeIds: z.array(z.number().int().positive()).nullable().optional(), sprintId: z.number().int().positive().nullable().optional(), }); @@ -388,9 +391,10 @@ export const IssueRecordSchema = z.object({ id: z.number(), projectId: z.number(), number: z.number(), + type: z.string(), + status: z.string(), title: z.string(), description: z.string(), - status: z.string(), creatorId: z.number(), sprintId: z.number().nullable(), }); diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 7428b65..7937a8b 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -11,5 +11,6 @@ export const PROJECT_SLUG_MAX_LENGTH = 64; export const ISSUE_TITLE_MAX_LENGTH = 64; export const ISSUE_DESCRIPTION_MAX_LENGTH = 2048; +export const ISSUE_TYPE_MAX_LENGTH = 16; export const ISSUE_STATUS_MAX_LENGTH = 24; export const ISSUE_COMMENT_MAX_LENGTH = 2048; diff --git a/packages/shared/src/schema.ts b/packages/shared/src/schema.ts index 3e6c62a..e92ca8b 100644 --- a/packages/shared/src/schema.ts +++ b/packages/shared/src/schema.ts @@ -6,6 +6,7 @@ import { ISSUE_DESCRIPTION_MAX_LENGTH, ISSUE_STATUS_MAX_LENGTH, ISSUE_TITLE_MAX_LENGTH, + ISSUE_TYPE_MAX_LENGTH, ORG_DESCRIPTION_MAX_LENGTH, ORG_NAME_MAX_LENGTH, ORG_SLUG_MAX_LENGTH, @@ -28,9 +29,15 @@ export const DEFAULT_STATUS_COLOURS: Record = { MERGED: DEFAULT_STATUS_COLOUR, }; +export const DEFAULT_ISSUE_TYPES: Record = { + Task: { icon: "checkBox", color: "#e4bd47" }, + Bug: { icon: "bug", color: "#ef4444" }, +}; + export const DEFAULT_FEATURES: Record = { userAvatars: true, + issueTypes: true, issueStatus: true, issueDescriptions: true, issueTimeTracking: true, @@ -63,6 +70,10 @@ export const Organisation = pgTable("Organisation", { slug: varchar({ length: ORG_SLUG_MAX_LENGTH }).notNull().unique(), iconURL: varchar({ length: 512 }), statuses: json("statuses").$type>().notNull().default(DEFAULT_STATUS_COLOURS), + issueTypes: json("issueTypes") + .$type>() + .notNull() + .default(DEFAULT_ISSUE_TYPES), features: json("features").$type>().notNull().default(DEFAULT_FEATURES), createdAt: timestamp({ withTimezone: false }).defaultNow(), updatedAt: timestamp({ withTimezone: false }).defaultNow(), @@ -135,9 +146,10 @@ export const Issue = pgTable( number: integer("number").notNull(), + type: varchar({ length: ISSUE_TYPE_MAX_LENGTH }).notNull().default("Task"), + status: varchar({ length: ISSUE_STATUS_MAX_LENGTH }).notNull().default("TO DO"), title: varchar({ length: ISSUE_TITLE_MAX_LENGTH }).notNull(), description: varchar({ length: ISSUE_DESCRIPTION_MAX_LENGTH }).notNull(), - status: varchar({ length: ISSUE_STATUS_MAX_LENGTH }).notNull().default("TO DO"), creatorId: integer() .notNull() diff --git a/todo.md b/todo.md index af8c555..04df177 100644 --- a/todo.md +++ b/todo.md @@ -1,28 +1,25 @@ # HIGH PRIORITY - FEATURES: -- issues - - issue type (options stored on Organisation) +- org settings: + - manage issue types + - create, edit, delete + - assign icons to issue types (ensure each available icon is in EACH icon set) - filters - time tracking: - add overlay in the bottom left for active timers if there are any. this should be minimal with the issue key (API-005), the time, and a play/pause + end button - pricing page - see jira and other competitors - explore payment providers (stripe is the only one i know) +- add "modal=true" in issue urls that are copied, and open issue-modal.tsx instead of the issue-detail-pane.tsx # LOW PRIORITY - dedicated /register route (currently login/register are combined on /login) - real logo -- org settings - - manage issue types, default is [bug, feature] - - create, edit, delete - - assign icons to issue types (ensure each available icon is in EACH icon set) - issues - assignee "note" for extra context on their role in the task - deadline - - comments - - admins are capable of deleting comments from members who are at their permission level or below (not sure if this should apply, or if ANYONE should have control over others' comments - people in an org tend to be trusted to not be trolls) - user preferences - colour scheme - "assign to me by default" option for new issues