Submatter/apps/web/components/mission-control/create-convoy-dialog.tsx

172 lines
5.6 KiB
TypeScript
Raw Permalink Normal View History

"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Loader2, Plus, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { api } from "@/lib/trpc";
import type { ConvoyClassification } from "@tasks/shared/convoy-lifecycle";
const CLASSIFICATIONS: ConvoyClassification[] = [
"feature",
"hotfix",
"docs-only",
"infra-only",
"server-only",
"config-only",
];
type FormState = {
slug: string;
classification: ConvoyClassification;
successMetric: string;
ideaMarkdown: string;
};
const EMPTY: FormState = {
slug: "",
classification: "feature",
successMetric: "",
ideaMarkdown: "",
};
export function CreateConvoyDialog({
workspaceSlug,
open,
onOpenChange,
onCreated,
}: {
workspaceSlug: string;
open: boolean;
onOpenChange: (open: boolean) => void;
onCreated: (convoyId: string) => void;
}) {
const utils = api.useUtils();
const [form, setForm] = React.useState<FormState>(EMPTY);
const createMut = api.convoys.create.useMutation({
onSuccess: (result) => {
void utils.convoys.kanban.invalidate();
onCreated(result.convoyId);
onOpenChange(false);
setForm(EMPTY);
},
});
return (
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50" />
<DialogPrimitive.Content className="fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-background p-6 shadow-lg">
<div className="mb-4 flex items-center justify-between">
<DialogPrimitive.Title className="text-lg font-semibold">
New convoy
</DialogPrimitive.Title>
<DialogPrimitive.Close asChild>
<button type="button" className="rounded p-1 hover:bg-muted" aria-label="Close">
<X className="size-4" />
</button>
</DialogPrimitive.Close>
</div>
<div className="space-y-3">
<div>
<label className="mb-1 block text-sm font-medium" htmlFor="convoy-slug">
Slug
</label>
<Input
id="convoy-slug"
placeholder="my-feature"
value={form.slug}
onChange={(e) => setForm((f) => ({ ...f, slug: e.target.value }))}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium" htmlFor="convoy-class">
Classification
</label>
<select
id="convoy-class"
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
value={form.classification}
onChange={(e) =>
setForm((f) => ({
...f,
classification: e.target.value as ConvoyClassification,
}))
}
>
{CLASSIFICATIONS.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
</div>
<div>
<label className="mb-1 block text-sm font-medium" htmlFor="convoy-metric">
Success metric
</label>
<Input
id="convoy-metric"
value={form.successMetric}
onChange={(e) => setForm((f) => ({ ...f, successMetric: e.target.value }))}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium" htmlFor="convoy-idea">
Idea / plan markdown
</label>
<textarea
id="convoy-idea"
className="min-h-[120px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
value={form.ideaMarkdown}
onChange={(e) => setForm((f) => ({ ...f, ideaMarkdown: e.target.value }))}
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<DialogPrimitive.Close asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogPrimitive.Close>
<Button
type="button"
disabled={
createMut.isPending ||
!form.slug.trim() ||
!form.successMetric.trim() ||
!form.ideaMarkdown.trim()
}
onClick={() =>
createMut.mutate({
workspace: workspaceSlug,
slug: form.slug.trim(),
classification: form.classification,
successMetric: form.successMetric.trim(),
ideaMarkdown: form.ideaMarkdown.trim(),
})
}
>
{createMut.isPending ? (
<Loader2 className="size-4 animate-spin" />
) : (
<>
<Plus className="mr-1 size-4" />
Create
</>
)}
</Button>
</div>
{createMut.error && (
<p className="text-xs text-destructive">{createMut.error.message}</p>
)}
</div>
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
);
}