diff --git a/packages/backend/scripts/db-seed.ts b/packages/backend/scripts/db-seed.ts index 8dc1b4b..56ad15e 100644 --- a/packages/backend/scripts/db-seed.ts +++ b/packages/backend/scripts/db-seed.ts @@ -1,5 +1,16 @@ import "dotenv/config"; -import { Issue, Organisation, OrganisationMember, Project, User } from "@sprint/shared"; +import { + DEFAULT_ISSUE_TYPES, + DEFAULT_STATUS_COLOURS, + Issue, + IssueAssignee, + IssueComment, + Organisation, + OrganisationMember, + Project, + Sprint, + User, +} from "@sprint/shared"; import bcrypt from "bcrypt"; import { drizzle } from "drizzle-orm/node-postgres"; @@ -66,6 +77,24 @@ const issues = [ { title: "Add batch processing", description: "Need to process large datasets efficiently." }, ]; +const issueStatuses = Object.keys(DEFAULT_STATUS_COLOURS); +const issueTypes = Object.keys(DEFAULT_ISSUE_TYPES); + +const issueComments = [ + "started looking into this, will share updates soon", + "i can reproduce this on staging", + "adding details in the description", + "should be a small fix, pairing with u2", + "blocked on api response shape", + "added logs, issue still happening", + "fix is ready for review", + "can we confirm expected behavior?", + "this seems related to recent deploy", + "i will take this one", + "qa verified on latest build", + "needs product input before proceeding", +]; + const passwordHash = await hashPassword("a"); const users = [ { name: "user 1", username: "u1", passwordHash, avatarURL: null }, @@ -146,14 +175,48 @@ async function seed() { console.log(`created ${projects.length} projects`); - // create 3-6 issues per project + console.log("creating sprints..."); + const sprintValues = []; + const now = new Date(); + + for (const project of projects) { + sprintValues.push( + { + projectId: project.id, + name: "Sprint 1", + color: "#3b82f6", + startDate: new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000), + endDate: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000), + }, + { + projectId: project.id, + name: "Sprint 2", + color: "#22c55e", + startDate: new Date(now.getTime()), + endDate: new Date(now.getTime() + 13 * 24 * 60 * 60 * 1000), + }, + ); + } + + const createdSprints = await db.insert(Sprint).values(sprintValues).returning(); + const sprintsByProject = new Map(); + + for (const sprint of createdSprints) { + const list = sprintsByProject.get(sprint.projectId) ?? []; + list.push(sprint); + sprintsByProject.set(sprint.projectId, list); + } + + console.log(`created ${createdSprints.length} sprints`); + + // create 6-12 issues per project console.log("creating issues..."); const allUsers = [u1, u2]; const issueValues = []; let issueIndex = 0; for (const project of projects) { - const numIssues = Math.floor(Math.random() * 4) + 3; // 3-6 issues + const numIssues = Math.floor(Math.random() * 7) + 6; // 6-12 issues for (let i = 1; i <= numIssues; i++) { const creator = allUsers[Math.floor(Math.random() * allUsers.length)]; if (!creator) { @@ -164,20 +227,89 @@ async function seed() { throw new Error("failed to select issue"); } issueIndex++; + const status = issueStatuses[Math.floor(Math.random() * issueStatuses.length)]; + const type = issueTypes[Math.floor(Math.random() * issueTypes.length)]; + if (!status || !type) { + throw new Error("failed to select issue status or type"); + } + const projectSprints = sprintsByProject.get(project.id); + if (!projectSprints || projectSprints.length === 0) { + throw new Error("failed to select project sprint"); + } + const sprint = projectSprints[Math.floor(Math.random() * projectSprints.length)]; + if (!sprint) { + throw new Error("failed to select sprint"); + } issueValues.push({ projectId: project.id, number: i, title: issue.title, description: issue.description, + status, + type, creatorId: creator.id, + sprintId: sprint.id, }); } } - await db.insert(Issue).values(issueValues); + const createdIssues = await db.insert(Issue).values(issueValues).returning(); - console.log(`created ${issueValues.length} issues`); + console.log(`created ${createdIssues.length} issues`); + + console.log("creating issue assignees..."); + const assigneeValues = []; + + for (const issue of createdIssues) { + const assigneeCount = Math.floor(Math.random() * 3); + const picked = new Set(); + for (let i = 0; i < assigneeCount; i++) { + const assignee = usersDB[Math.floor(Math.random() * usersDB.length)]; + if (!assignee) { + throw new Error("failed to select issue assignee"); + } + if (picked.has(assignee.id)) { + continue; + } + picked.add(assignee.id); + assigneeValues.push({ + issueId: issue.id, + userId: assignee.id, + }); + } + } + + if (assigneeValues.length > 0) { + await db.insert(IssueAssignee).values(assigneeValues); + } + + console.log(`created ${assigneeValues.length} issue assignees`); + + console.log("creating issue comments..."); + const commentValues = []; + + for (const issue of createdIssues) { + const commentCount = Math.floor(Math.random() * 3); + for (let i = 0; i < commentCount; i++) { + const commenter = usersDB[Math.floor(Math.random() * usersDB.length)]; + const comment = issueComments[Math.floor(Math.random() * issueComments.length)]; + if (!commenter || !comment) { + throw new Error("failed to select issue comment data"); + } + commentValues.push({ + issueId: issue.id, + userId: commenter.id, + body: comment, + }); + } + } + + if (commentValues.length > 0) { + await db.insert(IssueComment).values(commentValues); + } + + console.log(`created ${commentValues.length} issue comments`); console.log("database seeding complete"); console.log("\ndemo accounts (password: a):"); diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 22ecdb6..31913c1 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -40,7 +40,7 @@ const main = async () => { "/user/by-username": withGlobal(withAuth(routes.userByUsername)), "/user/update": withGlobal(withAuth(withCSRF(routes.userUpdate))), - "/user/upload-avatar": withGlobal(withAuth(withCSRF(routes.userUploadAvatar))), + "/user/upload-avatar": withGlobal(routes.userUploadAvatar), "/issue/create": withGlobal(withAuth(withCSRF(routes.issueCreate))), "/issue/by-id": withGlobal(withAuth(routes.issueById)), diff --git a/packages/frontend/src/App.css b/packages/frontend/src/App.css index 19f940f..b2dcc3a 100644 --- a/packages/frontend/src/App.css +++ b/packages/frontend/src/App.css @@ -66,10 +66,10 @@ --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); + --destructive: oklch(61.275% 0.20731 24.986); --border: oklch(73.802% 0.00008 271.152); --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); + --ring: var(--personality); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); @@ -105,7 +105,7 @@ --destructive: oklch(0.704 0.191 22.216); --border: oklch(100% 0.00011 271.152 / 0.22); --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); + --ring: var(--personality); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); @@ -132,6 +132,10 @@ background-color: var(--personality); color: var(--background); } + a:focus-visible { + outline: 1px solid var(--personality); + outline-offset: 2px; + } } * { diff --git a/packages/frontend/src/components/avatar.tsx b/packages/frontend/src/components/avatar.tsx index cbed993..3ccc9c2 100644 --- a/packages/frontend/src/components/avatar.tsx +++ b/packages/frontend/src/components/avatar.tsx @@ -49,6 +49,7 @@ export default function Avatar({ size, textClass = "text-xs", strong = false, + skipOrgCheck = false, className, }: { avatarURL?: string | null; @@ -57,6 +58,7 @@ export default function Avatar({ size?: number; textClass?: string; strong?: boolean; + skipOrgCheck?: boolean; className?: string; }) { // if the username matches the authed user, use their avatarURL and name (avoid stale data) @@ -69,13 +71,15 @@ export default function Avatar({ ? FALLBACK_COLOURS[hashStringToIndex(username, FALLBACK_COLOURS.length)] : "bg-muted"; + const showAvatar = skipOrgCheck || selectedOrganisation?.Organisation.features.userAvatars; + return (
- {selectedOrganisation?.Organisation.features.userAvatars && avatarURL ? ( + {showAvatar && avatarURL ? ( Avatar {isAuthor ? ( handleDelete(comment)} disabled={deletingId === comment.Comment.id} title="Delete comment" diff --git a/packages/frontend/src/components/issue-details.tsx b/packages/frontend/src/components/issue-details.tsx index f9ffa86..faa1be1 100644 --- a/packages/frontend/src/components/issue-details.tsx +++ b/packages/frontend/src/components/issue-details.tsx @@ -454,7 +454,7 @@ export function IssueDetails({ />
- {organisation?.Organisation.features.description && + {organisation?.Organisation.features.issueDescriptions && (description || isEditingDescription ? (