diff --git a/packages/frontend/src/components/create-issue.tsx b/packages/frontend/src/components/create-issue.tsx index 82a4c97..5ba6e1e 100644 --- a/packages/frontend/src/components/create-issue.tsx +++ b/packages/frontend/src/components/create-issue.tsx @@ -1,6 +1,7 @@ -import type { UserRecord } from "@issue/shared"; +import { ISSUE_DESCRIPTION_MAX_LENGTH, ISSUE_TITLE_MAX_LENGTH, type UserRecord } from "@issue/shared"; import { type FormEvent, useState } from "react"; import { useAuthenticatedSession } from "@/components/session-provider"; +import { StatusSelect } from "@/components/status-select"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -15,15 +16,19 @@ import { Label } from "@/components/ui/label"; import { UserSelect } from "@/components/user-select"; import { issue } from "@/lib/server"; import { cn } from "@/lib/utils"; +import StatusTag from "./status-tag"; +import { SelectTrigger } from "./ui/select"; export function CreateIssue({ projectId, members, + statuses, trigger, completeAction, }: { projectId?: number; members?: UserRecord[]; + statuses?: string[]; trigger?: React.ReactNode; completeAction?: (issueId: number) => void | Promise; }) { @@ -33,6 +38,7 @@ export function CreateIssue({ const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [assigneeId, setAssigneeId] = useState("unassigned"); + const [status, setStatus] = useState(statuses?.[0] ?? ""); const [submitAttempted, setSubmitAttempted] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -41,6 +47,7 @@ export function CreateIssue({ setTitle(""); setDescription(""); setAssigneeId("unassigned"); + setStatus(statuses?.[0] ?? ""); setSubmitAttempted(false); setSubmitting(false); setError(null); @@ -58,7 +65,11 @@ export function CreateIssue({ setError(null); setSubmitAttempted(true); - if (title.trim() === "" || description.trim().length > 2048) { + if ( + title.trim() === "" || + description.trim().length > ISSUE_DESCRIPTION_MAX_LENGTH || + title.trim().length > ISSUE_TITLE_MAX_LENGTH + ) { return; } @@ -80,6 +91,7 @@ export function CreateIssue({ title, description, assigneeId: assigneeId === "unassigned" ? null : Number(assigneeId), + status: status.trim() === "" ? undefined : status, onSuccess: async (data) => { setOpen(false); reset(); @@ -117,24 +129,62 @@ export function CreateIssue({
-
+
+ {statuses && statuses.length > 0 && ( +
+ + { + if (newValue.trim() === "") return; // TODO: handle this better + // unsure why an empty value is being sent, but preventing it this way for now + setStatus(newValue); + }} + trigger={({ isOpen, value }) => ( + + + + )} + /> +
+ )} + setTitle(e.target.value)} - validate={(v) => (v.trim() === "" ? "Cannot be empty" : undefined)} + validate={(v) => + v.trim() === "" + ? "Cannot be empty" + : v.trim().length > ISSUE_TITLE_MAX_LENGTH + ? `Too long (${ISSUE_TITLE_MAX_LENGTH} character limit)` + : undefined + } submitAttempted={submitAttempted} placeholder="Demo Issue" + maxLength={ISSUE_TITLE_MAX_LENGTH} /> setDescription(e.target.value)} validate={(v) => - v.trim().length > 2048 ? "Too long (2048 character limit)" : undefined + v.trim().length > ISSUE_DESCRIPTION_MAX_LENGTH + ? `Too long (${ISSUE_DESCRIPTION_MAX_LENGTH} character limit)` + : undefined } submitAttempted={submitAttempted} placeholder="Optional details" + maxLength={ISSUE_DESCRIPTION_MAX_LENGTH} /> {members && members.length > 0 && ( @@ -162,8 +212,10 @@ export function CreateIssue({ type="submit" disabled={ submitting || - (title.trim() === "" && submitAttempted) || - (description.trim().length > 2048 && submitAttempted) + ((title.trim() === "" || title.trim().length > ISSUE_TITLE_MAX_LENGTH) && + submitAttempted) || + (description.trim().length > ISSUE_DESCRIPTION_MAX_LENGTH && + submitAttempted) } > {submitting ? "Creating..." : "Create"} diff --git a/packages/frontend/src/components/status-select.tsx b/packages/frontend/src/components/status-select.tsx index 54d240f..8305bb9 100644 --- a/packages/frontend/src/components/status-select.tsx +++ b/packages/frontend/src/components/status-select.tsx @@ -29,7 +29,7 @@ export function StatusSelect({ chevronClassName={"size-3 -mr-1"} isOpen={isOpen} > - {value} + )} diff --git a/packages/frontend/src/lib/server/issue/create.ts b/packages/frontend/src/lib/server/issue/create.ts index c3b46af..ede26c3 100644 --- a/packages/frontend/src/lib/server/issue/create.ts +++ b/packages/frontend/src/lib/server/issue/create.ts @@ -6,6 +6,7 @@ export async function create({ title, description, assigneeId, + status, onSuccess, onError, }: { @@ -13,12 +14,14 @@ export async function create({ title: string; description: string; assigneeId?: number | null; + status?: string; } & ServerQueryInput) { const url = new URL(`${getServerURL()}/issue/create`); url.searchParams.set("projectId", `${projectId}`); url.searchParams.set("title", title.trim()); if (description.trim() !== "") url.searchParams.set("description", description.trim()); if (assigneeId != null) url.searchParams.set("assigneeId", `${assigneeId}`); + if (status != null && status.trim() !== "") url.searchParams.set("status", status.trim()); const csrfToken = getCsrfToken(); const headers: HeadersInit = {}; diff --git a/packages/frontend/src/pages/App.tsx b/packages/frontend/src/pages/App.tsx index b22167c..7ce5a21 100644 --- a/packages/frontend/src/pages/App.tsx +++ b/packages/frontend/src/pages/App.tsx @@ -240,6 +240,7 @@ export default function App() { { if (!selectedProject) return; await refetchIssues();