import { ORG_DESCRIPTION_MAX_LENGTH, ORG_NAME_MAX_LENGTH, ORG_SLUG_MAX_LENGTH, type OrganisationRecord, } from "@sprint/shared"; import { type FormEvent, useEffect, useState } from "react"; import { toast } from "sonner"; import { useAuthenticatedSession } from "@/components/session-provider"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Field } from "@/components/ui/field"; import { Label } from "@/components/ui/label"; import { UploadOrgIcon } from "@/components/upload-org-icon"; import { useCreateOrganisation, useUpdateOrganisation } from "@/lib/query/hooks"; import { parseError } from "@/lib/server"; import { cn } from "@/lib/utils"; const slugify = (value: string) => value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+/, "") .replace(/-{2,}/g, "-"); export function OrganisationForm({ trigger, completeAction, errorAction, mode = "create", existingOrganisation, open: controlledOpen, onOpenChange: controlledOnOpenChange, }: { trigger?: React.ReactNode; completeAction?: (org: OrganisationRecord) => void | Promise; errorAction?: (errorMessage: string) => void | Promise; mode?: "create" | "edit"; existingOrganisation?: OrganisationRecord; open?: boolean; onOpenChange?: (open: boolean) => void; }) { const { user } = useAuthenticatedSession(); const createOrganisation = useCreateOrganisation(); const updateOrganisation = useUpdateOrganisation(); const isControlled = controlledOpen !== undefined; const [internalOpen, setInternalOpen] = useState(false); const open = isControlled ? controlledOpen : internalOpen; const setOpen = isControlled ? (controlledOnOpenChange ?? (() => {})) : setInternalOpen; const [name, setName] = useState(""); const [slug, setSlug] = useState(""); const [description, setDescription] = useState(""); const [iconURL, setIconURL] = useState(null); const [slugManuallyEdited, setSlugManuallyEdited] = useState(false); const [submitAttempted, setSubmitAttempted] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const isEdit = mode === "edit"; useEffect(() => { if (isEdit && existingOrganisation && open) { setName(existingOrganisation.name); setSlug(existingOrganisation.slug); setDescription(existingOrganisation.description ?? ""); setIconURL(existingOrganisation.iconURL ?? null); setSlugManuallyEdited(true); } }, [isEdit, existingOrganisation, open]); const reset = () => { setName(""); setSlug(""); setDescription(""); setIconURL(null); setSlugManuallyEdited(false); setSubmitAttempted(false); setSubmitting(false); setError(null); }; const onOpenChange = (nextOpen: boolean) => { setOpen(nextOpen); if (!nextOpen) { reset(); } }; const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); setSubmitAttempted(true); if (name.trim() === "" || name.trim().length > ORG_NAME_MAX_LENGTH) return; if (slug.trim() === "" || slug.trim().length > ORG_SLUG_MAX_LENGTH) return; if (description.trim().length > ORG_DESCRIPTION_MAX_LENGTH) return; if (!user.id) { setError(`you must be logged in to ${isEdit ? "edit" : "create"} an organisation`); return; } setSubmitting(true); try { if (isEdit && existingOrganisation) { const data = await updateOrganisation.mutateAsync({ id: existingOrganisation.id, name, slug, description, }); setOpen(false); reset(); toast.success("Organisation updated"); try { await completeAction?.(data); } catch (actionErr) { console.error(actionErr); } } else { const data = await createOrganisation.mutateAsync({ name, slug, description, }); setOpen(false); reset(); toast.success(`Created Organisation ${data.name}`, { dismissible: false, }); try { await completeAction?.(data); } catch (actionErr) { console.error(actionErr); } } } catch (err) { const message = parseError(err as Error); console.error(err); setError(message || `failed to ${isEdit ? "update" : "create"} organisation`); setSubmitting(false); try { await errorAction?.(message || `failed to ${isEdit ? "update" : "create"} organisation`); } catch (actionErr) { console.error(actionErr); } } }; const dialogContent = ( {isEdit ? "Edit Organisation" : "Create Organisation"}
{isEdit && existingOrganisation && ( )} { const nextName = e.target.value; setName(nextName); if (!slugManuallyEdited) { setSlug(slugify(nextName)); } }} validate={(v) => { if (v.trim() === "") return "Cannot be empty"; if (v.trim().length > ORG_NAME_MAX_LENGTH) { return `Too long (${ORG_NAME_MAX_LENGTH} character limit)`; } return undefined; }} submitAttempted={submitAttempted} placeholder="Demo Organisation" maxLength={ORG_NAME_MAX_LENGTH} /> { setSlug(slugify(e.target.value)); setSlugManuallyEdited(true); }} validate={(v) => { if (v.trim() === "") return "Cannot be empty"; if (v.trim().length > ORG_SLUG_MAX_LENGTH) { return `Too long (${ORG_SLUG_MAX_LENGTH} character limit)`; } return undefined; }} submitAttempted={submitAttempted} placeholder="demo-organisation" maxLength={ORG_SLUG_MAX_LENGTH} /> setDescription(e.target.value)} validate={(v) => { if (v.trim().length > ORG_DESCRIPTION_MAX_LENGTH) { return `Too long (${ORG_DESCRIPTION_MAX_LENGTH} character limit)`; } return undefined; }} submitAttempted={submitAttempted} placeholder="What is this organisation for?" maxLength={ORG_DESCRIPTION_MAX_LENGTH} />
{error ? ( ) : ( )}
); if (isControlled) { return ( {dialogContent} ); } return ( {trigger || } {dialogContent} ); }