status colours

This commit is contained in:
Oliver Bryan
2026-01-10 21:49:26 +00:00
parent 1a8dc1a57e
commit 5db22961c5
20 changed files with 2033 additions and 62 deletions

View File

@@ -44,6 +44,7 @@
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-opener": "^2",
@@ -302,6 +303,8 @@
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "optionalDependencies": { "@types/react": "19.2.7" }, "peerDependencies": { "react": "19.2.3" } }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "optionalDependencies": { "@types/react": "19.2.7" }, "peerDependencies": { "react": "19.2.3" } }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],

View File

@@ -0,0 +1 @@
ALTER TABLE "Organisation" RENAME COLUMN "statuses" TO "statusColours";

View File

@@ -0,0 +1 @@
ALTER TABLE "Organisation" RENAME COLUMN "statusColours" TO "statuses";

View File

@@ -0,0 +1 @@
ALTER TABLE "Organisation" ALTER COLUMN "statuses" SET DEFAULT '{"TO DO":"#fafafa","IN PROGRESS":"#f97316","REVIEW":"#8952bc","DONE":"#22c55e","REJECTED":"#ef4444","ARCHIVED":"#a1a1a1","MERGED":"#a1a1a1"}'::json;

View File

@@ -0,0 +1,627 @@
{
"id": "52b84100-1751-43fc-b296-93c8130d3df9",
"prevId": "5a513ffa-9a5f-42ef-a2bc-939e12a17824",
"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(256)",
"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
},
"assigneeId": {
"name": "assigneeId",
"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_assigneeId_User_id_fk": {
"name": "Issue_assigneeId_User_id_fk",
"tableFrom": "Issue",
"tableTo": "User",
"columnsFrom": [
"assigneeId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"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(256)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": false
},
"slug": {
"name": "slug",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"statusColours": {
"name": "statusColours",
"type": "json",
"primaryKey": false,
"notNull": true,
"default": "'{\"TO DO\":\"#ffffff\",\"IN PROGRESS\":\"#f97316\",\"REVIEW\":\"#a855f7\",\"DONE\":\"#22c55e\",\"REJECTED\":\"#ef4444\",\"ARCHIVED\":\"oklch(0.708 0 0)\",\"MERGED\":\"oklch(0.708 0 0)\"}'::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(256)",
"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.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": true
},
"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": "no action",
"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
},
"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": {}
}
}

View File

@@ -0,0 +1,627 @@
{
"id": "1b7736f7-778a-4edf-b2f1-06d0ba1c605b",
"prevId": "52b84100-1751-43fc-b296-93c8130d3df9",
"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(256)",
"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
},
"assigneeId": {
"name": "assigneeId",
"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_assigneeId_User_id_fk": {
"name": "Issue_assigneeId_User_id_fk",
"tableFrom": "Issue",
"tableTo": "User",
"columnsFrom": [
"assigneeId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"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(256)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": false
},
"slug": {
"name": "slug",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"statuses": {
"name": "statuses",
"type": "json",
"primaryKey": false,
"notNull": true,
"default": "'{\"TO DO\":\"#fafafa\",\"IN PROGRESS\":\"#f97316\",\"REVIEW\":\"#8952bc\",\"DONE\":\"#22c55e\",\"REJECTED\":\"#ef4444\",\"ARCHIVED\":\"oklch(0.708 0 0)\",\"MERGED\":\"oklch(0.708 0 0)\"}'::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(256)",
"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.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": true
},
"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": "no action",
"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
},
"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": {}
}
}

View File

@@ -0,0 +1,627 @@
{
"id": "2fc85f22-9bff-42da-9898-4f284b091724",
"prevId": "1b7736f7-778a-4edf-b2f1-06d0ba1c605b",
"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(256)",
"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
},
"assigneeId": {
"name": "assigneeId",
"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_assigneeId_User_id_fk": {
"name": "Issue_assigneeId_User_id_fk",
"tableFrom": "Issue",
"tableTo": "User",
"columnsFrom": [
"assigneeId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"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(256)",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "varchar(1024)",
"primaryKey": false,
"notNull": false
},
"slug": {
"name": "slug",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"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"
},
"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(256)",
"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.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": true
},
"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": "no action",
"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
},
"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": {}
}
}

View File

@@ -92,6 +92,27 @@
"when": 1768072909149, "when": 1768072909149,
"tag": "0012_yielding_inertia", "tag": "0012_yielding_inertia",
"breakpoints": true "breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1768078024355,
"tag": "0013_overrated_chamber",
"breakpoints": true
},
{
"idx": 14,
"version": "7",
"when": 1768080336334,
"tag": "0014_lucky_mother_askani",
"breakpoints": true
},
{
"idx": 15,
"version": "7",
"when": 1768081741089,
"tag": "0015_brainy_xavin",
"breakpoints": true
} }
] ]
} }

View File

@@ -81,7 +81,12 @@ export async function getOrganisationsByUserId(userId: number) {
export async function updateOrganisation( export async function updateOrganisation(
organisationId: number, organisationId: number,
updates: { name?: string; description?: string; slug?: string; statuses?: string[] }, updates: {
name?: string;
description?: string;
slug?: string;
statuses?: Record<string, string>;
},
) { ) {
const [organisation] = await db const [organisation] = await db
.update(Organisation) .update(Organisation)

View File

@@ -2,7 +2,7 @@ import { ISSUE_STATUS_MAX_LENGTH } from "@issue/shared";
import type { BunRequest } from "bun"; import type { BunRequest } from "bun";
import { getOrganisationById, updateOrganisation } from "../../db/queries"; import { getOrganisationById, updateOrganisation } from "../../db/queries";
// /organisation/update?id=1&name=New%20Name&description=New%20Description&slug=new-slug&statuses=["TO DO","IN PROGRESS"] // /organisation/update?id=1&name=New%20Name&description=New%20Description&slug=new-slug
export default async function organisationUpdate(req: BunRequest) { export default async function organisationUpdate(req: BunRequest) {
const url = new URL(req.url); const url = new URL(req.url);
const id = url.searchParams.get("id"); const id = url.searchParams.get("id");
@@ -11,24 +11,28 @@ export default async function organisationUpdate(req: BunRequest) {
const slug = url.searchParams.get("slug") || undefined; const slug = url.searchParams.get("slug") || undefined;
const statusesParam = url.searchParams.get("statuses"); const statusesParam = url.searchParams.get("statuses");
let statuses: string[] | undefined; let statuses: Record<string, string> | undefined;
if (statusesParam) { if (statusesParam) {
try { try {
statuses = JSON.parse(statusesParam); const parsed = JSON.parse(statusesParam);
if (!Array.isArray(statuses) || !statuses.every((s) => typeof s === "string")) { if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return new Response("statuses must be an array of strings", { status: 400 }); return new Response("statuses must be an object", { status: 400 });
} }
if (statuses.length === 0) { const entries = Object.entries(parsed);
if (entries.length === 0) {
return new Response("statuses must have at least one status", { status: 400 }); return new Response("statuses must have at least one status", { status: 400 });
} }
if (!entries.every(([key, value]) => typeof key === "string" && typeof value === "string")) {
if (statuses.some((s) => s.length > ISSUE_STATUS_MAX_LENGTH)) { return new Response("statuses values must be strings", { status: 400 });
}
if (entries.some(([key]) => key.length > ISSUE_STATUS_MAX_LENGTH)) {
return new Response(`status must be <= ${ISSUE_STATUS_MAX_LENGTH} characters`, { return new Response(`status must be <= ${ISSUE_STATUS_MAX_LENGTH} characters`, {
status: 400, status: 400,
}); });
} }
statuses = parsed;
} catch { } catch {
return new Response("invalid statuses format (must be JSON array)", { status: 400 }); return new Response("invalid statuses format (must be JSON object)", { status: 400 });
} }
} }

View File

@@ -28,7 +28,7 @@ export function CreateIssue({
}: { }: {
projectId?: number; projectId?: number;
members?: UserRecord[]; members?: UserRecord[];
statuses?: string[]; statuses: Record<string, string>;
trigger?: React.ReactNode; trigger?: React.ReactNode;
completeAction?: (issueId: number) => void | Promise<void>; completeAction?: (issueId: number) => void | Promise<void>;
}) { }) {
@@ -38,7 +38,7 @@ export function CreateIssue({
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [assigneeId, setAssigneeId] = useState<string>("unassigned"); const [assigneeId, setAssigneeId] = useState<string>("unassigned");
const [status, setStatus] = useState<string>(statuses?.[0] ?? ""); const [status, setStatus] = useState<string>(Object.keys(statuses)[0] ?? "");
const [submitAttempted, setSubmitAttempted] = useState(false); const [submitAttempted, setSubmitAttempted] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -130,7 +130,7 @@ export function CreateIssue({
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="grid"> <div className="grid">
{statuses && statuses.length > 0 && ( {statuses && Object.keys(statuses).length > 0 && (
<div className="flex flex-col gap-2 mb-4"> <div className="flex flex-col gap-2 mb-4">
<Label>Status</Label> <Label>Status</Label>
<StatusSelect <StatusSelect
@@ -150,7 +150,8 @@ export function CreateIssue({
> >
<StatusTag <StatusTag
status={value} status={value}
className="group-hover:bg-foreground/75" colour={statuses[value]}
className="hover:opacity-85"
/> />
</SelectTrigger> </SelectTrigger>
)} )}

View File

@@ -23,7 +23,7 @@ export function IssueDetailPane({
project: ProjectResponse; project: ProjectResponse;
issueData: IssueResponse; issueData: IssueResponse;
members: UserRecord[]; members: UserRecord[];
statuses: string[]; statuses: Record<string, string>;
close: () => void; close: () => void;
onIssueUpdate?: () => void; onIssueUpdate?: () => void;
}) { }) {
@@ -98,7 +98,11 @@ export function IssueDetailPane({
chevronClassName="hidden" chevronClassName="hidden"
isOpen={isOpen} isOpen={isOpen}
> >
<StatusTag status={value} className="group-hover:bg-foreground/75" /> <StatusTag
status={value}
colour={statuses[value]}
className="hover:opacity-85"
/>
</SelectTrigger> </SelectTrigger>
)} )}
/> />

View File

@@ -8,11 +8,13 @@ export function IssuesTable({
issuesData, issuesData,
columns = {}, columns = {},
issueSelectAction, issueSelectAction,
statuses,
className, className,
}: { }: {
issuesData: IssueResponse[]; issuesData: IssueResponse[];
columns?: { id?: boolean; title?: boolean; description?: boolean; status?: boolean; assignee?: boolean }; columns?: { id?: boolean; title?: boolean; description?: boolean; status?: boolean; assignee?: boolean };
issueSelectAction?: (issue: IssueResponse) => void; issueSelectAction?: (issue: IssueResponse) => void;
statuses: Record<string, string>;
className: string; className: string;
}) { }) {
return ( return (
@@ -48,7 +50,10 @@ export function IssuesTable({
<TableCell> <TableCell>
<span className="flex items-center gap-2 max-w-full truncate"> <span className="flex items-center gap-2 max-w-full truncate">
{(columns.status == null || columns.status === true) && ( {(columns.status == null || columns.status === true) && (
<StatusTag status={issueData.Issue.status} /> <StatusTag
status={issueData.Issue.status}
colour={statuses[issueData.Issue.status]}
/>
)} )}
{issueData.Issue.title} {issueData.Issue.title}
</span> </span>

View File

@@ -1,4 +1,5 @@
import { import {
DEFAULT_STATUS_COLOUR,
ISSUE_STATUS_MAX_LENGTH, ISSUE_STATUS_MAX_LENGTH,
type OrganisationMemberResponse, type OrganisationMemberResponse,
type OrganisationResponse, type OrganisationResponse,
@@ -38,7 +39,7 @@ function OrganisationsDialog({
const [activeTab, setActiveTab] = useState("info"); const [activeTab, setActiveTab] = useState("info");
const [members, setMembers] = useState<OrganisationMemberResponse[]>([]); const [members, setMembers] = useState<OrganisationMemberResponse[]>([]);
const [statuses, setStatuses] = useState<string[]>([]); const [statuses, setStatuses] = useState<Record<string, string>>({});
const [isCreatingStatus, setIsCreatingStatus] = useState(false); const [isCreatingStatus, setIsCreatingStatus] = useState(false);
const [newStatusName, setNewStatusName] = useState(""); const [newStatusName, setNewStatusName] = useState("");
const [statusError, setStatusError] = useState<string | null>(null); const [statusError, setStatusError] = useState<string | null>(null);
@@ -161,16 +162,13 @@ function OrganisationsDialog({
useEffect(() => { useEffect(() => {
if (selectedOrganisation) { if (selectedOrganisation) {
const orgStatuses = (selectedOrganisation.Organisation as unknown as { statuses: string[] }) setStatuses(selectedOrganisation.Organisation.statuses);
.statuses;
setStatuses(
Array.isArray(orgStatuses) ? orgStatuses : ["TO DO", "IN PROGRESS", "REVIEW", "DONE"],
);
} }
}, [selectedOrganisation]); }, [selectedOrganisation]);
const updateStatuses = async (newStatuses: string[]) => { const updateStatuses = async (newStatuses: Record<string, string>) => {
if (!selectedOrganisation) return; if (!selectedOrganisation) return;
try { try {
await organisation.update({ await organisation.update({
organisationId: selectedOrganisation.Organisation.id, organisationId: selectedOrganisation.Organisation.id,
@@ -197,14 +195,14 @@ function OrganisationsDialog({
return; return;
} }
if (statuses.includes(trimmed)) { if (Object.keys(statuses).includes(trimmed)) {
setNewStatusName(""); setNewStatusName("");
setIsCreatingStatus(false); setIsCreatingStatus(false);
setStatusError(null); setStatusError(null);
return; return;
} }
const newStatuses = { ...statuses };
const newStatuses = [...statuses, trimmed]; newStatuses[trimmed] = DEFAULT_STATUS_COLOUR;
await updateStatuses(newStatuses); await updateStatuses(newStatuses);
setNewStatusName(""); setNewStatusName("");
setIsCreatingStatus(false); setIsCreatingStatus(false);
@@ -212,26 +210,25 @@ function OrganisationsDialog({
}; };
const handleRemoveStatusClick = (status: string) => { const handleRemoveStatusClick = (status: string) => {
if (statuses.length <= 1) return; if (Object.keys(statuses).length <= 1) return;
setStatusToRemove(status); setStatusToRemove(status);
const remaining = statuses.filter((s) => s !== status); const remaining = Object.keys(statuses).filter((s) => s !== status);
setReassignToStatus(remaining[0] || ""); setReassignToStatus(remaining[0] || "");
}; };
const moveStatus = async (status: string, direction: "up" | "down") => { const moveStatus = async (status: string, direction: "up" | "down") => {
const currentIndex = statuses.indexOf(status); const currentIndex = Object.keys(statuses).indexOf(status);
if (currentIndex === -1) return; if (currentIndex === -1) return;
const nextIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1; const nextIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1;
if (nextIndex < 0 || nextIndex >= statuses.length) return; if (nextIndex < 0 || nextIndex >= Object.keys(statuses).length) return;
const nextStatuses = [...Object.keys(statuses)];
const nextStatuses = [...statuses];
[nextStatuses[currentIndex], nextStatuses[nextIndex]] = [ [nextStatuses[currentIndex], nextStatuses[nextIndex]] = [
nextStatuses[nextIndex], nextStatuses[nextIndex],
nextStatuses[currentIndex], nextStatuses[currentIndex],
]; ];
await updateStatuses(nextStatuses); await updateStatuses(Object.fromEntries(nextStatuses.map((status) => [status, statuses[status]])));
}; };
const confirmRemoveStatus = async () => { const confirmRemoveStatus = async () => {
@@ -242,8 +239,10 @@ function OrganisationsDialog({
oldStatus: statusToRemove, oldStatus: statusToRemove,
newStatus: reassignToStatus, newStatus: reassignToStatus,
onSuccess: async () => { onSuccess: async () => {
const newStatuses = statuses.filter((s) => s !== statusToRemove); const newStatuses = Object.keys(statuses).filter((s) => s !== statusToRemove);
await updateStatuses(newStatuses); await updateStatuses(
Object.fromEntries(newStatuses.map((status) => [status, statuses[status]])),
);
setStatusToRemove(null); setStatusToRemove(null);
setReassignToStatus(""); setReassignToStatus("");
}, },
@@ -406,13 +405,19 @@ function OrganisationsDialog({
<div className="border p-2 min-w-0 overflow-hidden"> <div className="border p-2 min-w-0 overflow-hidden">
<h2 className="text-xl font-600 mb-2">Issue Statuses</h2> <h2 className="text-xl font-600 mb-2">Issue Statuses</h2>
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<div className="flex flex-col gap-2 max-h-56 overflow-y-scroll"> <div className="flex flex-col gap-2 max-h-86 overflow-y-scroll grid grid-cols-2">
{statuses.map((status, index) => ( {Object.keys(statuses).map((status, index) => (
<div <div
key={status} key={status}
className="flex items-center justify-between p-2 border" className="flex items-center justify-between p-2 border"
> >
<StatusTag status={status} /> <div className="flex items-center gap-2">
<span className="text-sm">{index + 1}</span>
<StatusTag
status={status}
colour={statuses[status]}
/>
</div>
{isAdmin && ( {isAdmin && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
@@ -427,7 +432,9 @@ function OrganisationsDialog({
<Button <Button
variant="dummy" variant="dummy"
size="none" size="none"
disabled={index === statuses.length - 1} disabled={
index === Object.keys(statuses).length - 1
}
onClick={() => onClick={() =>
void moveStatus(status, "down") void moveStatus(status, "down")
} }
@@ -435,7 +442,7 @@ function OrganisationsDialog({
> >
<ChevronDown className="size-5 text-muted-foreground" /> <ChevronDown className="size-5 text-muted-foreground" />
</Button> </Button>
{statuses.length > 1 && ( {Object.keys(statuses).length > 1 && (
<Button <Button
variant="dummy" variant="dummy"
size="none" size="none"
@@ -563,11 +570,11 @@ function OrganisationsDialog({
<SelectValue placeholder="Select status" /> <SelectValue placeholder="Select status" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{statuses {Object.keys(statuses)
.filter((s) => s !== statusToRemove) .filter((s) => s !== statusToRemove)
.map((status) => ( .map((status) => (
<SelectItem key={status} value={status}> <SelectItem key={status} value={status}>
{status} <StatusTag status={status} colour={statuses[status]} />
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>

View File

@@ -10,7 +10,7 @@ export function StatusSelect({
placeholder = "Select status", placeholder = "Select status",
trigger, trigger,
}: { }: {
statuses: string[]; statuses: Record<string, string>;
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
placeholder?: string; placeholder?: string;
@@ -33,9 +33,9 @@ export function StatusSelect({
</SelectTrigger> </SelectTrigger>
)} )}
<SelectContent side="bottom" position="popper" align="start"> <SelectContent side="bottom" position="popper" align="start">
{statuses.map((status) => ( {Object.entries(statuses).map(([status, colour]) => (
<SelectItem key={status} value={status} textClassName="text-xs"> <SelectItem key={status} value={status} textClassName="text-xs">
<StatusTag status={status} className="" /> <StatusTag status={status} colour={colour} />
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>

View File

@@ -1,12 +1,35 @@
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export default function StatusTag({ status, className }: { status: string; className?: string }) { const DARK_TEXT_COLOUR = "#0a0a0a";
const THRESHOLD = 0.6;
const isLight = (hex: string): boolean => {
const num = Number.parseInt(hex.replace("#", ""), 16);
const r = (num >> 16) & 255;
const g = (num >> 8) & 255;
const b = num & 255;
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > THRESHOLD;
};
export default function StatusTag({
status,
colour,
className,
}: {
status: string;
colour: string;
className?: string;
}) {
const textColour = isLight(colour) ? DARK_TEXT_COLOUR : "var(--foreground)";
return ( return (
<div <div
className={cn( className={cn(
"text-xs px-1 bg-foreground/85 rounded text-background inline-flex whitespace-nowrap", "text-xs px-1 rounded inline-flex whitespace-nowrap border border-foreground/10",
className, className,
)} )}
style={{ backgroundColor: colour, color: textColour }}
> >
{status} {status}
</div> </div>

View File

@@ -14,14 +14,16 @@ export async function update({
name?: string; name?: string;
description?: string; description?: string;
slug?: string; slug?: string;
statuses?: string[]; statuses?: Record<string, string>;
} & ServerQueryInput) { } & ServerQueryInput) {
const url = new URL(`${getServerURL()}/organisation/update`); const url = new URL(`${getServerURL()}/organisation/update`);
url.searchParams.set("id", `${organisationId}`); url.searchParams.set("id", `${organisationId}`);
if (name !== undefined) url.searchParams.set("name", name); if (name !== undefined) url.searchParams.set("name", name);
if (description !== undefined) url.searchParams.set("description", description); if (description !== undefined) url.searchParams.set("description", description);
if (slug !== undefined) url.searchParams.set("slug", slug); if (slug !== undefined) url.searchParams.set("slug", slug);
if (statuses !== undefined) url.searchParams.set("statuses", JSON.stringify(statuses)); if (statuses !== undefined) {
url.searchParams.set("statuses", JSON.stringify(statuses));
}
const csrfToken = getCsrfToken(); const csrfToken = getCsrfToken();
const headers: HeadersInit = {}; const headers: HeadersInit = {};

View File

@@ -1,4 +1,5 @@
/** biome-ignore-all lint/correctness/useExhaustiveDependencies: <> */ /** biome-ignore-all lint/correctness/useExhaustiveDependencies: <> */
import type { import type {
IssueResponse, IssueResponse,
OrganisationMemberResponse, OrganisationMemberResponse,
@@ -240,7 +241,7 @@ export default function App() {
<CreateIssue <CreateIssue
projectId={selectedProject?.Project.id} projectId={selectedProject?.Project.id}
members={members} members={members}
statuses={selectedOrganisation.Organisation.statuses as unknown as string[]} statuses={selectedOrganisation.Organisation.statuses}
completeAction={async () => { completeAction={async () => {
if (!selectedProject) return; if (!selectedProject) return;
await refetchIssues(); await refetchIssues();
@@ -288,13 +289,14 @@ export default function App() {
</div> </div>
{/* main body */} {/* main body */}
{selectedProject && issues.length > 0 && ( {selectedOrganisation && selectedProject && issues.length > 0 && (
<ResizablePanelGroup className={`flex-1`}> <ResizablePanelGroup className={`flex-1`}>
<ResizablePanel id={"left"} minSize={400}> <ResizablePanel id={"left"} minSize={400}>
{/* issues list (table) */} {/* issues list (table) */}
<IssuesTable <IssuesTable
issuesData={issues} issuesData={issues}
columns={{ description: false }} columns={{ description: false }}
statuses={selectedOrganisation.Organisation.statuses}
issueSelectAction={(issue) => { issueSelectAction={(issue) => {
if (issue.Issue.id === selectedIssue?.Issue.id) setSelectedIssue(null); if (issue.Issue.id === selectedIssue?.Issue.id) setSelectedIssue(null);
else setSelectedIssue(issue); else setSelectedIssue(issue);
@@ -313,9 +315,7 @@ export default function App() {
project={selectedProject} project={selectedProject}
issueData={selectedIssue} issueData={selectedIssue}
members={members} members={members}
statuses={ statuses={selectedOrganisation.Organisation.statuses}
selectedOrganisation.Organisation.statuses as unknown as string[]
}
close={() => setSelectedIssue(null)} close={() => setSelectedIssue(null)}
onIssueUpdate={refetchIssues} onIssueUpdate={refetchIssues}
/> />

View File

@@ -11,7 +11,6 @@ export {
USER_NAME_MAX_LENGTH, USER_NAME_MAX_LENGTH,
USER_USERNAME_MAX_LENGTH, USER_USERNAME_MAX_LENGTH,
} from "./constants"; } from "./constants";
export type { export type {
IssueInsert, IssueInsert,
IssueRecord, IssueRecord,
@@ -33,6 +32,8 @@ export type {
UserRecord, UserRecord,
} from "./schema"; } from "./schema";
export { export {
DEFAULT_STATUS_COLOUR,
DEFAULT_STATUS_COLOURS,
Issue, Issue,
IssueInsertSchema, IssueInsertSchema,
IssueSelectSchema, IssueSelectSchema,

View File

@@ -1,4 +1,4 @@
import { integer, pgTable, timestamp, uniqueIndex, varchar } from "drizzle-orm/pg-core"; import { integer, json, pgTable, timestamp, uniqueIndex, varchar } from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod"; import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import type { z } from "zod"; import type { z } from "zod";
import { import {
@@ -13,6 +13,18 @@ import {
USER_USERNAME_MAX_LENGTH, USER_USERNAME_MAX_LENGTH,
} from "./constants"; } from "./constants";
export const DEFAULT_STATUS_COLOUR = "#a1a1a1";
export const DEFAULT_STATUS_COLOURS: Record<string, string> = {
"TO DO": "#fafafa",
"IN PROGRESS": "#f97316",
REVIEW: "#8952bc",
DONE: "#22c55e",
REJECTED: "#ef4444",
ARCHIVED: DEFAULT_STATUS_COLOUR,
MERGED: DEFAULT_STATUS_COLOUR,
};
export const User = pgTable("User", { export const User = pgTable("User", {
id: integer().primaryKey().generatedAlwaysAsIdentity(), id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: USER_NAME_MAX_LENGTH }).notNull(), name: varchar({ length: USER_NAME_MAX_LENGTH }).notNull(),
@@ -28,10 +40,7 @@ export const Organisation = pgTable("Organisation", {
name: varchar({ length: ORG_NAME_MAX_LENGTH }).notNull(), name: varchar({ length: ORG_NAME_MAX_LENGTH }).notNull(),
description: varchar({ length: ORG_DESCRIPTION_MAX_LENGTH }), description: varchar({ length: ORG_DESCRIPTION_MAX_LENGTH }),
slug: varchar({ length: ORG_SLUG_MAX_LENGTH }).notNull().unique(), slug: varchar({ length: ORG_SLUG_MAX_LENGTH }).notNull().unique(),
statuses: varchar({ length: ISSUE_STATUS_MAX_LENGTH }) statuses: json("statuses").$type<Record<string, string>>().notNull().default(DEFAULT_STATUS_COLOURS),
.array()
.notNull()
.default(["TO DO", "IN PROGRESS", "REVIEW", "DONE", "ARCHIVED", "MERGED"]),
createdAt: timestamp({ withTimezone: false }).defaultNow(), createdAt: timestamp({ withTimezone: false }).defaultNow(),
updatedAt: timestamp({ withTimezone: false }).defaultNow(), updatedAt: timestamp({ withTimezone: false }).defaultNow(),
}); });
@@ -135,7 +144,9 @@ export const TimedSessionInsertSchema = createInsertSchema(TimedSession);
export type UserRecord = z.infer<typeof UserSelectSchema>; export type UserRecord = z.infer<typeof UserSelectSchema>;
export type UserInsert = z.infer<typeof UserInsertSchema>; export type UserInsert = z.infer<typeof UserInsertSchema>;
export type OrganisationRecord = z.infer<typeof OrganisationSelectSchema>; export type OrganisationRecord = z.infer<typeof OrganisationSelectSchema> & {
statuses: Record<string, string>;
};
export type OrganisationInsert = z.infer<typeof OrganisationInsertSchema>; export type OrganisationInsert = z.infer<typeof OrganisationInsertSchema>;
export type OrganisationMemberRecord = z.infer<typeof OrganisationMemberSelectSchema>; export type OrganisationMemberRecord = z.infer<typeof OrganisationMemberSelectSchema>;