From 3b23cd7c1682717cb89d44a7770263d8e7d2e1be Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Thu, 29 Jan 2026 11:53:16 +0000 Subject: [PATCH 1/9] new seed data --- packages/backend/.env.example | 5 ++++- packages/backend/scripts/db-seed.ts | 31 ++++++++++++++++++----------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/backend/.env.example b/packages/backend/.env.example index c1ab18f..3e01033 100644 --- a/packages/backend/.env.example +++ b/packages/backend/.env.example @@ -16,4 +16,7 @@ STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key STRIPE_SECRET_KEY=your_stripe_secret_key RESEND_API_KEY=re_xxxxxxxxxxxxxxxx -EMAIL_FROM=Sprint \ No newline at end of file +EMAIL_FROM=Sprint + +# password for demo accounts in seed data +SEED_PASSWORD=change_me_in_production diff --git a/packages/backend/scripts/db-seed.ts b/packages/backend/scripts/db-seed.ts index ccb0ee2..05dea8f 100644 --- a/packages/backend/scripts/db-seed.ts +++ b/packages/backend/scripts/db-seed.ts @@ -95,17 +95,24 @@ const issueComments = [ "needs product input before proceeding", ]; -const passwordHash = await hashPassword("a"); +const SEED_PASSWORD = process.env.SEED_PASSWORD; + +if (!SEED_PASSWORD) { + console.error("SEED_PASSWORD is not set"); + process.exit(1); +} + +const passwordHash = await hashPassword(SEED_PASSWORD); const users = [ - { name: "user 1", username: "u1", email: "user1@example.com", passwordHash, avatarURL: null }, - { name: "user 2", username: "u2", email: "user2@example.com", passwordHash, avatarURL: null }, + { name: "demo user 1", username: "demo1", email: "demo1@example.com", passwordHash, avatarURL: null }, + { name: "demo user 2", username: "demo2", email: "demo2@example.com", passwordHash, avatarURL: null }, // anything past here is just to have more users to assign issues to - { name: "user 3", username: "u3", email: "user3@example.com", passwordHash, avatarURL: null }, - { name: "user 4", username: "u4", email: "user4@example.com", passwordHash, avatarURL: null }, - { name: "user 5", username: "u5", email: "user5@example.com", passwordHash, avatarURL: null }, - { name: "user 6", username: "u6", email: "user6@example.com", passwordHash, avatarURL: null }, - { name: "user 7", username: "u7", email: "user7@example.com", passwordHash, avatarURL: null }, - { name: "user 8", username: "u8", email: "user8@example.com", passwordHash, avatarURL: null }, + { name: "demo user 3", username: "demo3", email: "demo3@example.com", passwordHash, avatarURL: null }, + { name: "demo user 4", username: "demo4", email: "demo4@example.com", passwordHash, avatarURL: null }, + { name: "demo user 5", username: "demo5", email: "demo5@example.com", passwordHash, avatarURL: null }, + { name: "demo user 6", username: "demo6", email: "demo6@example.com", passwordHash, avatarURL: null }, + { name: "demo user 7", username: "demo7", email: "demo7@example.com", passwordHash, avatarURL: null }, + { name: "demo user 8", username: "demo8", email: "demo8@example.com", passwordHash, avatarURL: null }, ]; async function seed() { @@ -312,9 +319,9 @@ async function seed() { console.log(`created ${commentValues.length} issue comments`); console.log("database seeding complete"); - console.log("\ndemo accounts (password: a):"); - console.log(" - u1"); - console.log(" - u2"); + console.log("\ndemo accounts:"); + console.log(" - demo1"); + console.log(" - demo2"); } catch (error) { console.error("failed to seed database:", error); process.exit(1); From bcdf71198b2f3469a09f9234635d3d9a4cc5aa75 Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Thu, 29 Jan 2026 11:55:25 +0000 Subject: [PATCH 2/9] removed under construction warning --- .../frontend/src/components/login-form.tsx | 304 +++++++----------- 1 file changed, 111 insertions(+), 193 deletions(-) diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index 70bf9cd..6ce57a3 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -2,33 +2,16 @@ import { USER_EMAIL_MAX_LENGTH, USER_NAME_MAX_LENGTH, USER_USERNAME_MAX_LENGTH } from "@sprint/shared"; import { useEffect, useState } from "react"; -import Avatar from "@/components/avatar"; import { useSession } from "@/components/session-provider"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Field } from "@/components/ui/field"; -import Icon from "@/components/ui/icon"; -import { IconButton } from "@/components/ui/icon-button"; import { Label } from "@/components/ui/label"; import { UploadAvatar } from "@/components/upload-avatar"; import { capitalise, cn, getServerURL, setCsrfToken } from "@/lib/utils"; -const DEMO_USERS = [ - { name: "User 1", username: "u1", password: "a" }, - { name: "User 2", username: "u2", password: "a" }, -]; - -export default function LogInForm({ - showWarning, - setShowWarning, -}: { - showWarning: boolean; - setShowWarning: (value: boolean) => void; -}) { +export default function LogInForm() { const { setUser, setEmailVerified } = useSession(); - const [loginDetailsOpen, setLoginDetailsOpen] = useState(false); - const [mode, setMode] = useState<"login" | "register">("login"); const [name, setName] = useState(""); @@ -141,189 +124,124 @@ export default function LogInForm({ }; return ( - <> - {/* under construction warning */} - {showWarning && ( -
- { - localStorage.setItem("hide-under-construction", "true"); - setShowWarning(false); - }} - > - - - -
-

- This application is currently under construction. Your data is very likely to be lost at some - point. -

-

- It is not recommended for production use. -

-

But you're more than welcome to have a look around!

- - - Login Details - - - Demo Login Credentials -
- {DEMO_USERS.map((user) => ( - - ))} -
-
-
-
-
- )} -
-
-
- {capitalise(mode)} +
+ +
+ {capitalise(mode)} -
- {mode === "register" && ( - <> - - {avatarURL && ( - - )} - setName(e.target.value)} - validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} - submitAttempted={submitAttempted} - spellcheck={false} - maxLength={USER_NAME_MAX_LENGTH} - /> - setEmail(e.target.value)} - validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} - submitAttempted={submitAttempted} - spellcheck={false} - maxLength={USER_EMAIL_MAX_LENGTH} - /> - - )} - setUsername(e.target.value)} - validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} - submitAttempted={submitAttempted} - spellcheck={false} - maxLength={USER_USERNAME_MAX_LENGTH} - showCounter={mode === "register"} - /> - setPassword(e.target.value)} - validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} - hidden={true} - submitAttempted={submitAttempted} - spellcheck={false} - /> -
- - {mode === "login" ? ( +
+ {mode === "register" && ( <> - - - - ) : ( - <> - - + + {avatarURL && ( + + )} + setName(e.target.value)} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} + spellcheck={false} + maxLength={USER_NAME_MAX_LENGTH} + /> + setEmail(e.target.value)} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} + spellcheck={false} + maxLength={USER_EMAIL_MAX_LENGTH} + /> )} + setUsername(e.target.value)} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + submitAttempted={submitAttempted} + spellcheck={false} + maxLength={USER_USERNAME_MAX_LENGTH} + showCounter={mode === "register"} + /> + setPassword(e.target.value)} + validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + hidden={true} + submitAttempted={submitAttempted} + spellcheck={false} + />
- -
- {error !== "" ? ( - + + {mode === "login" ? ( + <> + + + ) : ( - + <> + + + )}
+ +
+ {error !== "" ? ( + + ) : ( + + )}
- +
); } From fdf2494337f97761eec40f20e8004fbd735c8fbf Mon Sep 17 00:00:00 2001 From: Oliver Bryan <65399431+hex248@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:59:54 +0000 Subject: [PATCH 3/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1da159..023069e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Sprint +# [Sprint](https://sprintpm.org) Super simple project management tool for developers. From 7605020e65a5ea19ba8227a101833a51b650fb75 Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Thu, 29 Jan 2026 15:28:19 +0000 Subject: [PATCH 4/9] fixed LoginForm usage --- packages/frontend/src/components/login-form.tsx | 1 + packages/frontend/src/components/login-modal.tsx | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index 6ce57a3..4fb9d98 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -9,6 +9,7 @@ import { Label } from "@/components/ui/label"; import { UploadAvatar } from "@/components/upload-avatar"; import { capitalise, cn, getServerURL, setCsrfToken } from "@/lib/utils"; + export default function LogInForm() { const { setUser, setEmailVerified } = useSession(); diff --git a/packages/frontend/src/components/login-modal.tsx b/packages/frontend/src/components/login-modal.tsx index dd482ef..43a4371 100644 --- a/packages/frontend/src/components/login-modal.tsx +++ b/packages/frontend/src/components/login-modal.tsx @@ -17,9 +17,6 @@ export function LoginModal({ open, onOpenChange, onSuccess, dismissible = true } const [searchParams] = useSearchParams(); const { user, isLoading, emailVerified } = useSession(); const [hasRedirected, setHasRedirected] = useState(false); - const [showWarning, setShowWarning] = useState(() => { - return localStorage.getItem("hide-under-construction") !== "true"; - }); useEffect(() => { if (open && !isLoading && user && emailVerified && !hasRedirected) { @@ -46,9 +43,9 @@ export function LoginModal({ open, onOpenChange, onSuccess, dismissible = true } return ( - + Log In or Register - + ); From 9d44c2c65870336bc4153974223eafe63dd4e0c9 Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Thu, 29 Jan 2026 15:31:19 +0000 Subject: [PATCH 5/9] removed FAQ link from header on landing --- packages/frontend/src/components/login-form.tsx | 1 - packages/frontend/src/components/login-modal.tsx | 2 +- packages/frontend/src/pages/Landing.tsx | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/login-form.tsx b/packages/frontend/src/components/login-form.tsx index 4fb9d98..6ce57a3 100644 --- a/packages/frontend/src/components/login-form.tsx +++ b/packages/frontend/src/components/login-form.tsx @@ -9,7 +9,6 @@ import { Label } from "@/components/ui/label"; import { UploadAvatar } from "@/components/upload-avatar"; import { capitalise, cn, getServerURL, setCsrfToken } from "@/lib/utils"; - export default function LogInForm() { const { setUser, setEmailVerified } = useSession(); diff --git a/packages/frontend/src/components/login-modal.tsx b/packages/frontend/src/components/login-modal.tsx index 43a4371..51d2c91 100644 --- a/packages/frontend/src/components/login-modal.tsx +++ b/packages/frontend/src/components/login-modal.tsx @@ -45,7 +45,7 @@ export function LoginModal({ open, onOpenChange, onSuccess, dismissible = true } Log In or Register - + ); diff --git a/packages/frontend/src/pages/Landing.tsx b/packages/frontend/src/pages/Landing.tsx index 6fcb824..765a926 100644 --- a/packages/frontend/src/pages/Landing.tsx +++ b/packages/frontend/src/pages/Landing.tsx @@ -58,12 +58,12 @@ export default function Landing() { > Pricing */} - FAQ - + */}
{!isLoading && user ? ( From 14cdc99b740e12adb615a48ea472ba39ab916b75 Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Thu, 29 Jan 2026 20:59:41 +0000 Subject: [PATCH 6/9] added cn() to textClass --- packages/frontend/src/components/org-icon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/org-icon.tsx b/packages/frontend/src/components/org-icon.tsx index 6950898..c27d705 100644 --- a/packages/frontend/src/components/org-icon.tsx +++ b/packages/frontend/src/components/org-icon.tsx @@ -70,7 +70,7 @@ export default function OrgIcon({ {iconURL ? ( {name} ) : ( - {getInitials(name)} + {getInitials(name)} )}
); From 21d723541917c03d0ad62cfd937c89cb815faf59 Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Thu, 29 Jan 2026 21:08:26 +0000 Subject: [PATCH 7/9] improved formatting of non-avatar org icons --- packages/frontend/src/components/org-icon.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/org-icon.tsx b/packages/frontend/src/components/org-icon.tsx index c27d705..5abc430 100644 --- a/packages/frontend/src/components/org-icon.tsx +++ b/packages/frontend/src/components/org-icon.tsx @@ -70,7 +70,9 @@ export default function OrgIcon({ {iconURL ? ( {name} ) : ( - {getInitials(name)} +
+ {getInitials(name)} +
)}
); From 130f564c330e3a26c63dd276f08ffa13a34f9aa0 Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Thu, 29 Jan 2026 21:15:19 +0000 Subject: [PATCH 8/9] another stab at fixing the OrgIcon sizing --- packages/frontend/src/components/org-icon.tsx | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/components/org-icon.tsx b/packages/frontend/src/components/org-icon.tsx index 5abc430..4cb91d0 100644 --- a/packages/frontend/src/components/org-icon.tsx +++ b/packages/frontend/src/components/org-icon.tsx @@ -62,17 +62,30 @@ export default function OrgIcon({ "flex items-center justify-center rounded-sm overflow-hidden", "text-white font-medium select-none", !iconURL && backgroundClass, - `w-${size || 6}`, - `h-${size || 6}`, className, )} + style={{ width: `calc(var(--spacing) * ${size || 6})`, height: `calc(var(--spacing) * ${size || 6})` }} > {iconURL ? ( - {name} + {name} ) : ( -
- {getInitials(name)} -
+ + {getInitials(name)} + )}
); From e339274069a5b2ce2a96feee6baf138cf0cab57e Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Thu, 29 Jan 2026 22:43:02 +0000 Subject: [PATCH 9/9] 'Assign to me by default' toggle in Account.tsx --- .../backend/drizzle/0029_fantastic_venom.sql | 1 + .../backend/drizzle/meta/0029_snapshot.json | 1361 +++++++++++++++++ packages/backend/drizzle/meta/_journal.json | 7 + packages/backend/src/db/queries/users.ts | 1 + packages/backend/src/routes/auth/me.ts | 1 + packages/backend/src/routes/user/update.ts | 14 +- packages/frontend/src/components/account.tsx | 16 + .../frontend/src/components/issue-form.tsx | 8 + packages/shared/src/api-schemas.ts | 2 + packages/shared/src/schema.ts | 12 +- 10 files changed, 1418 insertions(+), 5 deletions(-) create mode 100644 packages/backend/drizzle/0029_fantastic_venom.sql create mode 100644 packages/backend/drizzle/meta/0029_snapshot.json diff --git a/packages/backend/drizzle/0029_fantastic_venom.sql b/packages/backend/drizzle/0029_fantastic_venom.sql new file mode 100644 index 0000000..a700726 --- /dev/null +++ b/packages/backend/drizzle/0029_fantastic_venom.sql @@ -0,0 +1 @@ +ALTER TABLE "User" ADD COLUMN "preferences" json DEFAULT '{"assignByDefault":false}'::json NOT NULL; \ No newline at end of file diff --git a/packages/backend/drizzle/meta/0029_snapshot.json b/packages/backend/drizzle/meta/0029_snapshot.json new file mode 100644 index 0000000..5ab646d --- /dev/null +++ b/packages/backend/drizzle/meta/0029_snapshot.json @@ -0,0 +1,1361 @@ +{ + "id": "2ec59d99-3bc5-4220-89c9-095197348175", + "prevId": "4e8f597a-39c9-47c6-9eb4-a085a88bc1b5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.EmailJob": { + "name": "EmailJob", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "EmailJob_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 + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "scheduledFor": { + "name": "scheduledFor", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sentAt": { + "name": "sentAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failedAt": { + "name": "failedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "EmailJob_userId_User_id_fk": { + "name": "EmailJob_userId_User_id_fk", + "tableFrom": "EmailJob", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EmailVerification": { + "name": "EmailVerification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "EmailVerification_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 + }, + "code": { + "name": "code", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "maxAttempts": { + "name": "maxAttempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "verifiedAt": { + "name": "verifiedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "EmailVerification_userId_User_id_fk": { + "name": "EmailVerification_userId_User_id_fk", + "tableFrom": "EmailVerification", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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.Payment": { + "name": "Payment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Payment_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "subscriptionId": { + "name": "subscriptionId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "stripePaymentIntentId": { + "name": "stripePaymentIntentId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "varchar(3)", + "primaryKey": false, + "notNull": true, + "default": "'gbp'" + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Payment_subscriptionId_Subscription_id_fk": { + "name": "Payment_subscriptionId_Subscription_id_fk", + "tableFrom": "Payment", + "tableTo": "Subscription", + "columnsFrom": [ + "subscriptionId" + ], + "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.Subscription": { + "name": "Subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "Subscription_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 + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionId": { + "name": "stripeSubscriptionId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripeSubscriptionItemId": { + "name": "stripeSubscriptionItemId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "stripePriceId": { + "name": "stripePriceId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'incomplete'" + }, + "currentPeriodStart": { + "name": "currentPeriodStart", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "currentPeriodEnd": { + "name": "currentPeriodEnd", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelAtPeriodEnd": { + "name": "cancelAtPeriodEnd", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trialEnd": { + "name": "trialEnd", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Subscription_userId_User_id_fk": { + "name": "Subscription_userId_User_id_fk", + "tableFrom": "Subscription", + "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": 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 + }, + "email": { + "name": "email", + "type": "varchar(256)", + "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": "'pixel'" + }, + "plan": { + "name": "plan", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "emailVerifiedAt": { + "name": "emailVerifiedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "preferences": { + "name": "preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"assignByDefault\":false}'::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": { + "User_username_unique": { + "name": "User_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "User_email_unique": { + "name": "User_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "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 a2bcbb9..c563b2c 100644 --- a/packages/backend/drizzle/meta/_journal.json +++ b/packages/backend/drizzle/meta/_journal.json @@ -204,6 +204,13 @@ "when": 1769643481882, "tag": "0028_quick_supernaut", "breakpoints": true + }, + { + "idx": 29, + "version": "7", + "when": 1769726204311, + "tag": "0029_fantastic_venom", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/backend/src/db/queries/users.ts b/packages/backend/src/db/queries/users.ts index ca9a567..daa190c 100644 --- a/packages/backend/src/db/queries/users.ts +++ b/packages/backend/src/db/queries/users.ts @@ -39,6 +39,7 @@ export async function updateById( avatarURL?: string | null; iconPreference?: IconStyle; plan?: string; + preferences?: Record; }, ): Promise { const [user] = await db.update(User).set(updates).where(eq(User.id, id)).returning(); diff --git a/packages/backend/src/routes/auth/me.ts b/packages/backend/src/routes/auth/me.ts index 3007479..1e0ce06 100644 --- a/packages/backend/src/routes/auth/me.ts +++ b/packages/backend/src/routes/auth/me.ts @@ -14,5 +14,6 @@ export default async function me(req: AuthedRequest) { user: safeUser as Omit, csrfToken: req.csrfToken, emailVerified: user.emailVerified, + preferences: user.preferences, }); } diff --git a/packages/backend/src/routes/user/update.ts b/packages/backend/src/routes/user/update.ts index dd23e96..5139eba 100644 --- a/packages/backend/src/routes/user/update.ts +++ b/packages/backend/src/routes/user/update.ts @@ -8,16 +8,16 @@ export default async function update(req: AuthedRequest) { const parsed = await parseJsonBody(req, UserUpdateRequestSchema); if ("error" in parsed) return parsed.error; - const { name, password, avatarURL, iconPreference } = parsed.data; + const { name, password, avatarURL, iconPreference, preferences } = parsed.data; const user = await getUserById(req.userId); if (!user) { return errorResponse("user not found", "USER_NOT_FOUND", 404); } - if (!name && !password && avatarURL === undefined && !iconPreference) { + if (!name && !password && avatarURL === undefined && !iconPreference && preferences === undefined) { return errorResponse( - "at least one of name, password, avatarURL, or iconPreference must be provided", + "at least one of name, password, avatarURL, iconPreference, or preferences must be provided", "NO_UPDATES", 400, ); @@ -42,7 +42,13 @@ export default async function update(req: AuthedRequest) { } const { updateById } = await import("../../db/queries/users"); - const updatedUser = await updateById(user.id, { name, passwordHash, avatarURL, iconPreference }); + const updatedUser = await updateById(user.id, { + name, + passwordHash, + avatarURL, + iconPreference, + preferences, + }); if (!updatedUser) { return errorResponse("failed to update user", "UPDATE_FAILED", 500); diff --git a/packages/frontend/src/components/account.tsx b/packages/frontend/src/components/account.tsx index d6f8073..537e682 100644 --- a/packages/frontend/src/components/account.tsx +++ b/packages/frontend/src/components/account.tsx @@ -11,6 +11,7 @@ import { Field } from "@/components/ui/field"; import Icon from "@/components/ui/icon"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { UploadAvatar } from "@/components/upload-avatar"; import { useUpdateUser } from "@/lib/query/hooks"; import { parseError } from "@/lib/server"; @@ -29,6 +30,7 @@ function Account({ trigger }: { trigger?: ReactNode }) { const [password, setPassword] = useState(""); const [avatarURL, setAvatarUrl] = useState(null); const [iconPreference, setIconPreference] = useState("pixel"); + const [preferences, setPreferences] = useState>({}); const [error, setError] = useState(""); const [submitAttempted, setSubmitAttempted] = useState(false); @@ -40,6 +42,7 @@ function Account({ trigger }: { trigger?: ReactNode }) { setAvatarUrl(currentUser.avatarURL || null); const effectiveIconStyle = (currentUser.iconPreference as IconStyle) ?? DEFAULT_ICON_STYLE; setIconPreference(effectiveIconStyle); + setPreferences(currentUser.preferences ?? {}); setPassword(""); setError(""); @@ -60,6 +63,7 @@ function Account({ trigger }: { trigger?: ReactNode }) { password: password.trim() || undefined, avatarURL, iconPreference, + preferences, }); setError(""); setUser(data); @@ -129,6 +133,18 @@ function Account({ trigger }: { trigger?: ReactNode }) { /> +
+
+ { + setPreferences((prev) => ({ ...prev, assignByDefault: checked })); + }} + /> + Assign to me by default +
+
+
diff --git a/packages/frontend/src/components/issue-form.tsx b/packages/frontend/src/components/issue-form.tsx index 7c3848e..a900534 100644 --- a/packages/frontend/src/components/issue-form.tsx +++ b/packages/frontend/src/components/issue-form.tsx @@ -66,6 +66,14 @@ export function IssueForm({ trigger }: { trigger?: React.ReactNode }) { const [assigneeIds, setAssigneeIds] = useState(["unassigned"]); const [status, setStatus] = useState(defaultStatus); const [type, setType] = useState(defaultType); + + // set default assignee based on user preference when dialog opens + useEffect(() => { + if (open && user.preferences?.assignByDefault) { + setAssigneeIds([`${user.id}`]); + } + }, [open, user]); + useEffect(() => { if (!status && defaultStatus) setStatus(defaultStatus); if (!type && defaultType) setType(defaultType); diff --git a/packages/shared/src/api-schemas.ts b/packages/shared/src/api-schemas.ts index 883e9c0..b16c42c 100644 --- a/packages/shared/src/api-schemas.ts +++ b/packages/shared/src/api-schemas.ts @@ -412,6 +412,7 @@ export const UserUpdateRequestSchema = z.object({ .optional(), avatarURL: z.string().url().nullable().optional(), iconPreference: z.enum(["lucide", "pixel", "phosphor"]).optional(), + preferences: z.record(z.boolean()).optional(), }); export type UserUpdateRequest = z.infer; @@ -431,6 +432,7 @@ export const UserResponseSchema = z.object({ avatarURL: z.string().nullable(), iconPreference: z.enum(["lucide", "pixel", "phosphor"]), plan: z.string().nullable().optional(), + preferences: z.record(z.boolean()).optional(), createdAt: z.string().nullable().optional(), updatedAt: z.string().nullable().optional(), }); diff --git a/packages/shared/src/schema.ts b/packages/shared/src/schema.ts index c286cec..b895094 100644 --- a/packages/shared/src/schema.ts +++ b/packages/shared/src/schema.ts @@ -50,6 +50,10 @@ export const DEFAULT_FEATURES: Record = { sprints: true, }; +export const DEFAULT_USER_PREFERENCES: Record = { + assignByDefault: false, +}; + export const iconStyles = ["pixel", "lucide", "phosphor"] as const; export type IconStyle = (typeof iconStyles)[number]; @@ -64,6 +68,10 @@ export const User = pgTable("User", { plan: varchar({ length: 32 }).notNull().default("free"), emailVerified: boolean().notNull().default(false), emailVerifiedAt: timestamp({ withTimezone: false }), + preferences: json("preferences") + .$type>() + .notNull() + .default(DEFAULT_USER_PREFERENCES), createdAt: timestamp({ withTimezone: false }).defaultNow(), updatedAt: timestamp({ withTimezone: false }).defaultNow(), }); @@ -227,7 +235,9 @@ export const SessionInsertSchema = createInsertSchema(Session); export const TimedSessionSelectSchema = createSelectSchema(TimedSession); export const TimedSessionInsertSchema = createInsertSchema(TimedSession); -export type UserRecord = z.infer; +export type UserRecord = z.infer & { + preferences: Record; +}; export type UserInsert = z.infer; export type OrganisationRecord = z.infer & {