added status select to create-issue component

This commit is contained in:
Oliver Bryan
2026-01-10 18:01:19 +00:00
parent 4341e6bcdf
commit 3959854bae
4 changed files with 64 additions and 8 deletions

View File

@@ -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 { type FormEvent, useState } from "react";
import { useAuthenticatedSession } from "@/components/session-provider"; import { useAuthenticatedSession } from "@/components/session-provider";
import { StatusSelect } from "@/components/status-select";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -15,15 +16,19 @@ import { Label } from "@/components/ui/label";
import { UserSelect } from "@/components/user-select"; import { UserSelect } from "@/components/user-select";
import { issue } from "@/lib/server"; import { issue } from "@/lib/server";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import StatusTag from "./status-tag";
import { SelectTrigger } from "./ui/select";
export function CreateIssue({ export function CreateIssue({
projectId, projectId,
members, members,
statuses,
trigger, trigger,
completeAction, completeAction,
}: { }: {
projectId?: number; projectId?: number;
members?: UserRecord[]; members?: UserRecord[];
statuses?: string[];
trigger?: React.ReactNode; trigger?: React.ReactNode;
completeAction?: (issueId: number) => void | Promise<void>; completeAction?: (issueId: number) => void | Promise<void>;
}) { }) {
@@ -33,6 +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 [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);
@@ -41,6 +47,7 @@ export function CreateIssue({
setTitle(""); setTitle("");
setDescription(""); setDescription("");
setAssigneeId("unassigned"); setAssigneeId("unassigned");
setStatus(statuses?.[0] ?? "");
setSubmitAttempted(false); setSubmitAttempted(false);
setSubmitting(false); setSubmitting(false);
setError(null); setError(null);
@@ -58,7 +65,11 @@ export function CreateIssue({
setError(null); setError(null);
setSubmitAttempted(true); 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; return;
} }
@@ -80,6 +91,7 @@ export function CreateIssue({
title, title,
description, description,
assigneeId: assigneeId === "unassigned" ? null : Number(assigneeId), assigneeId: assigneeId === "unassigned" ? null : Number(assigneeId),
status: status.trim() === "" ? undefined : status,
onSuccess: async (data) => { onSuccess: async (data) => {
setOpen(false); setOpen(false);
reset(); reset();
@@ -117,24 +129,62 @@ export function CreateIssue({
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="grid mt-2"> <div className="grid">
{statuses && statuses.length > 0 && (
<div className="flex flex-col gap-2 mb-4">
<Label>Status</Label>
<StatusSelect
statuses={statuses}
value={status}
onChange={(newValue) => {
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 }) => (
<SelectTrigger
className="group flex items-center w-min"
variant="unstyled"
chevronClassName="hidden"
isOpen={isOpen}
>
<StatusTag
status={value}
className="group-hover:bg-foreground/75"
/>
</SelectTrigger>
)}
/>
</div>
)}
<Field <Field
label="Title" label="Title"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => 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} submitAttempted={submitAttempted}
placeholder="Demo Issue" placeholder="Demo Issue"
maxLength={ISSUE_TITLE_MAX_LENGTH}
/> />
<Field <Field
label="Description (optional)" label="Description (optional)"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
validate={(v) => 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} submitAttempted={submitAttempted}
placeholder="Optional details" placeholder="Optional details"
maxLength={ISSUE_DESCRIPTION_MAX_LENGTH}
/> />
{members && members.length > 0 && ( {members && members.length > 0 && (
@@ -162,8 +212,10 @@ export function CreateIssue({
type="submit" type="submit"
disabled={ disabled={
submitting || submitting ||
(title.trim() === "" && submitAttempted) || ((title.trim() === "" || title.trim().length > ISSUE_TITLE_MAX_LENGTH) &&
(description.trim().length > 2048 && submitAttempted) submitAttempted) ||
(description.trim().length > ISSUE_DESCRIPTION_MAX_LENGTH &&
submitAttempted)
} }
> >
{submitting ? "Creating..." : "Create"} {submitting ? "Creating..." : "Create"}

View File

@@ -29,7 +29,7 @@ export function StatusSelect({
chevronClassName={"size-3 -mr-1"} chevronClassName={"size-3 -mr-1"}
isOpen={isOpen} isOpen={isOpen}
> >
<SelectValue placeholder={placeholder}>{value}</SelectValue> <SelectValue placeholder={placeholder} />
</SelectTrigger> </SelectTrigger>
)} )}
<SelectContent side="bottom" position="popper" align="start"> <SelectContent side="bottom" position="popper" align="start">

View File

@@ -6,6 +6,7 @@ export async function create({
title, title,
description, description,
assigneeId, assigneeId,
status,
onSuccess, onSuccess,
onError, onError,
}: { }: {
@@ -13,12 +14,14 @@ export async function create({
title: string; title: string;
description: string; description: string;
assigneeId?: number | null; assigneeId?: number | null;
status?: string;
} & ServerQueryInput) { } & ServerQueryInput) {
const url = new URL(`${getServerURL()}/issue/create`); const url = new URL(`${getServerURL()}/issue/create`);
url.searchParams.set("projectId", `${projectId}`); url.searchParams.set("projectId", `${projectId}`);
url.searchParams.set("title", title.trim()); url.searchParams.set("title", title.trim());
if (description.trim() !== "") url.searchParams.set("description", description.trim()); if (description.trim() !== "") url.searchParams.set("description", description.trim());
if (assigneeId != null) url.searchParams.set("assigneeId", `${assigneeId}`); if (assigneeId != null) url.searchParams.set("assigneeId", `${assigneeId}`);
if (status != null && status.trim() !== "") url.searchParams.set("status", status.trim());
const csrfToken = getCsrfToken(); const csrfToken = getCsrfToken();
const headers: HeadersInit = {}; const headers: HeadersInit = {};

View File

@@ -240,6 +240,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[]}
completeAction={async () => { completeAction={async () => {
if (!selectedProject) return; if (!selectedProject) return;
await refetchIssues(); await refetchIssues();