From 0b72345c6bed6d3e53cf3bd8559032665bd9b849 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 1 Mar 2026 16:33:18 -0600 Subject: [PATCH] fix: add domainId to project DTOs and project create UI --- .../src/projects/dto/create-project.dto.ts | 5 + .../src/projects/dto/update-project.dto.ts | 5 + apps/api/src/projects/projects.service.ts | 15 +++ .../src/app/(authenticated)/projects/page.tsx | 102 +++++++++++++++++- apps/web/src/lib/api/projects.ts | 2 + 5 files changed, 128 insertions(+), 1 deletion(-) diff --git a/apps/api/src/projects/dto/create-project.dto.ts b/apps/api/src/projects/dto/create-project.dto.ts index 5da0e00..3ce42ea 100644 --- a/apps/api/src/projects/dto/create-project.dto.ts +++ b/apps/api/src/projects/dto/create-project.dto.ts @@ -8,6 +8,7 @@ import { MinLength, MaxLength, Matches, + IsUUID, } from "class-validator"; /** @@ -43,6 +44,10 @@ export class CreateProjectDto { }) color?: string; + @IsOptional() + @IsUUID("4", { message: "domainId must be a valid UUID" }) + domainId?: string; + @IsOptional() @IsObject({ message: "metadata must be an object" }) metadata?: Record; diff --git a/apps/api/src/projects/dto/update-project.dto.ts b/apps/api/src/projects/dto/update-project.dto.ts index 0087426..3588380 100644 --- a/apps/api/src/projects/dto/update-project.dto.ts +++ b/apps/api/src/projects/dto/update-project.dto.ts @@ -8,6 +8,7 @@ import { MinLength, MaxLength, Matches, + IsUUID, } from "class-validator"; /** @@ -45,6 +46,10 @@ export class UpdateProjectDto { }) color?: string | null; + @IsOptional() + @IsUUID("4", { message: "domainId must be a valid UUID" }) + domainId?: string | null; + @IsOptional() @IsObject({ message: "metadata must be an object" }) metadata?: Record; diff --git a/apps/api/src/projects/projects.service.ts b/apps/api/src/projects/projects.service.ts index 92697a5..2618722 100644 --- a/apps/api/src/projects/projects.service.ts +++ b/apps/api/src/projects/projects.service.ts @@ -47,6 +47,9 @@ export class ProjectsService { createProjectDto: CreateProjectDto ): Promise { const data: Prisma.ProjectCreateInput = { + ...(createProjectDto.domainId + ? { domain: { connect: { id: createProjectDto.domainId } } } + : {}), name: createProjectDto.name, description: createProjectDto.description ?? null, color: createProjectDto.color ?? null, @@ -221,6 +224,18 @@ export class ProjectsService { if (updateProjectDto.startDate !== undefined) updateData.startDate = updateProjectDto.startDate; if (updateProjectDto.endDate !== undefined) updateData.endDate = updateProjectDto.endDate; if (updateProjectDto.color !== undefined) updateData.color = updateProjectDto.color; + if (updateProjectDto.domainId !== undefined) + updateData.domain = updateProjectDto.domainId + ? { connect: { id: updateProjectDto.domainId } } + : { disconnect: true }; + if (updateProjectDto.domainId !== undefined) + updateData.domain = updateProjectDto.domainId + ? { + connect: { + id: updateProjectDto.domainId, + }, + } + : { disconnect: true }; if (updateProjectDto.metadata !== undefined) { updateData.metadata = updateProjectDto.metadata as unknown as Prisma.InputJsonValue; } diff --git a/apps/web/src/app/(authenticated)/projects/page.tsx b/apps/web/src/app/(authenticated)/projects/page.tsx index 6d97d80..253bdbb 100644 --- a/apps/web/src/app/(authenticated)/projects/page.tsx +++ b/apps/web/src/app/(authenticated)/projects/page.tsx @@ -17,6 +17,8 @@ import { import { fetchProjects, createProject, deleteProject, ProjectStatus } from "@/lib/api/projects"; import type { Project, CreateProjectDto } from "@/lib/api/projects"; import { useWorkspaceId } from "@/lib/hooks"; +import { fetchDomains } from "@/lib/api/domains"; +import type { Domain } from "@mosaic/shared"; /* --------------------------------------------------------------------------- Status badge helpers @@ -65,11 +67,14 @@ interface ProjectCardProps { project: Project; onDelete: (id: string) => void; onClick: (id: string) => void; + domains: Domain[]; } -function ProjectCard({ project, onDelete, onClick }: ProjectCardProps): ReactElement { +function ProjectCard({ project, onDelete, onClick, domains }: ProjectCardProps): ReactElement { const [hovered, setHovered] = useState(false); const status = getStatusStyle(project.status); + // Find domain if project has a domainId + const domain = project.domainId ? domains.find((d) => d.id === project.domainId) : undefined; return (
{status.label} + {domain && ( + + {domain.name} + + )} {/* Timestamps */} void; onSubmit: (data: CreateProjectDto) => Promise; isSubmitting: boolean; + domains: Domain[]; } function CreateProjectDialog({ @@ -236,20 +258,24 @@ function CreateProjectDialog({ onOpenChange, onSubmit, isSubmitting, + domains, }: CreateDialogProps): ReactElement { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [formError, setFormError] = useState(null); + const [domainId, setDomainId] = useState(""); function resetForm(): void { setName(""); setDescription(""); setFormError(null); + setDomainId(""); } async function handleSubmit(e: SyntheticEvent): Promise { e.preventDefault(); setFormError(null); + setDomainId(""); const trimmedName = name.trim(); if (!trimmedName) { @@ -263,6 +289,9 @@ function CreateProjectDialog({ if (trimmedDesc) { payload.description = trimmedDesc; } + if (domainId) { + payload.domainId = domainId; + } await onSubmit(payload); resetForm(); } catch (err: unknown) { @@ -382,6 +411,47 @@ function CreateProjectDialog({ />
+ {/* Domain */} +
+ + +
+ {/* Form error */} {formError !== null && (

@@ -532,6 +602,7 @@ export default function ProjectsPage(): ReactElement { const workspaceId = useWorkspaceId(); const [projects, setProjects] = useState([]); + const [domains, setDomains] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -601,6 +672,33 @@ export default function ProjectsPage(): ReactElement { }; }, [workspaceId]); + // Load domains + useEffect(() => { + if (!workspaceId) { + return; + } + + let cancelled = false; + const wsId = workspaceId; + + async function loadDomains(): Promise { + try { + const response = await fetchDomains(undefined, wsId); + if (!cancelled) { + setDomains(response.data); + } + } catch (err: unknown) { + console.error("[Projects] Failed to fetch domains:", err); + } + } + + void loadDomains(); + + return (): void => { + cancelled = true; + }; + }, [workspaceId]); + function handleRetry(): void { void loadProjects(workspaceId); } @@ -779,6 +877,7 @@ export default function ProjectsPage(): ReactElement { project={project} onDelete={handleDeleteRequest} onClick={handleCardClick} + domains={domains} /> ))} @@ -790,6 +889,7 @@ export default function ProjectsPage(): ReactElement { onOpenChange={setCreateOpen} onSubmit={handleCreate} isSubmitting={isCreating} + domains={domains} /> {/* Delete Confirmation Dialog */} diff --git a/apps/web/src/lib/api/projects.ts b/apps/web/src/lib/api/projects.ts index d746d91..e9a926f 100644 --- a/apps/web/src/lib/api/projects.ts +++ b/apps/web/src/lib/api/projects.ts @@ -95,6 +95,7 @@ export interface CreateProjectDto { startDate?: string; endDate?: string; color?: string; + domainId?: string; metadata?: Record; } @@ -108,6 +109,7 @@ export interface UpdateProjectDto { startDate?: string | null; endDate?: string | null; color?: string | null; + domainId?: string | null; metadata?: Record; } -- 2.49.1