diff --git a/apps/blade/src/app/_components/issues/create-edit-dialog.tsx b/apps/blade/src/app/_components/issues/create-edit-dialog.tsx index 7b8efd0e2..68fc7b3e3 100644 --- a/apps/blade/src/app/_components/issues/create-edit-dialog.tsx +++ b/apps/blade/src/app/_components/issues/create-edit-dialog.tsx @@ -1,183 +1,156 @@ "use client"; -import * as React from "react"; -import { Trash2, X } from "lucide-react"; -import { createPortal } from "react-dom"; - -import { EVENTS, ISSUE } from "@forge/consts"; +import type { FormEvent, MouseEvent, ReactElement, ReactNode } from "react"; +import { + cloneElement, + isValidElement, + useCallback, + useId, + useMemo, + useState, +} from "react"; +import { LayoutTemplate, Loader2, Plus, Trash2 } from "lucide-react"; +import { z } from "zod"; + +import type { EVENTS } from "@forge/consts"; +import { ISSUE } from "@forge/consts"; import { cn } from "@forge/ui"; import { Button } from "@forge/ui/button"; import { Checkbox } from "@forge/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@forge/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@forge/ui/dropdown-menu"; import { Input } from "@forge/ui/input"; import { Label } from "@forge/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@forge/ui/select"; import { Textarea } from "@forge/ui/textarea"; +import { toast } from "@forge/ui/toast"; import { api } from "~/trpc/react"; +import { + defaultEventForm, + getTaskDueDateInputValue, + normalizeTaskDueDate, + parseEventDateTime, +} from "./issue-dialog-utils"; +import { + EventFormFields, + PrioritySelect, + RoleCheckboxGroup, + StatusSelect, + TeamSelect, +} from "./issue-form-fields"; +import { + addChildToIssueNode, + IssueNode, + newIssueNode, + removeIssueNode, + updateIssueNode, + validateIssueNodes, +} from "./issue-node"; const baseField = "w-full"; -function getStatusLabel(status: string) { - return status - .toLowerCase() - .replace(/_/g, " ") - .replace(/\b\w/g, (char) => char.toUpperCase()); -} - -function normalizeTaskDueDate(dateValue?: string | Date) { - const dueDate = dateValue ? new Date(dateValue) : new Date(); - if (Number.isNaN(dueDate.getTime())) { - const fallback = new Date(); - fallback.setHours(ISSUE.TASK_DUE_HOURS, ISSUE.TASK_DUE_MINUTES, 0, 0); - return fallback; - } - - dueDate.setHours(ISSUE.TASK_DUE_HOURS, ISSUE.TASK_DUE_MINUTES, 0, 0); - return dueDate; -} - -function getTaskDueDateInputValue(dateValue: Date) { - return normalizeTaskDueDate(dateValue).toISOString().slice(0, 10); -} - -function parseTimeTo12h(timeValue?: string): { - hour: string; - minute: string; - amPm: (typeof ISSUE.EVENT_TIME_AM_PM_OPTIONS)[number]; -} { - const [hRaw, mRaw] = (timeValue ?? "").split(":"); - const h = Number(hRaw); - const m = Number(mRaw); - - if (Number.isNaN(h) || Number.isNaN(m)) { - return { - hour: "", - minute: "", - amPm: "PM" as (typeof ISSUE.EVENT_TIME_AM_PM_OPTIONS)[number], - }; - } - - const amPm: (typeof ISSUE.EVENT_TIME_AM_PM_OPTIONS)[number] = - h >= 12 ? "PM" : "AM"; - const hour24 = h % 12 || 12; - return { - hour: hour24.toString().padStart(2, "0"), - minute: m.toString().padStart(2, "0"), - amPm, - }; -} - -function to24h( - hour12: string, - amPm: (typeof ISSUE.EVENT_TIME_AM_PM_OPTIONS)[number], -) { - let h = Number(hour12); - if (Number.isNaN(h)) { - h = 0; - } - if (amPm === "PM" && h < 12) { - h += 12; - } - if (amPm === "AM" && h === 12) { - h = 0; - } - return h.toString().padStart(2, "0"); -} - -function toAmPmValue( - value: string, -): (typeof ISSUE.EVENT_TIME_AM_PM_OPTIONS)[number] { - return value === "AM" ? "AM" : "PM"; -} +const issueFormSchema = z + .object({ + name: z.string().min(1), + description: z.string().min(1), + team: z.string().min(1), + isEvent: z.boolean(), + date: z.date(), + eventData: z + .object({ + location: z.string().min(1), + description: z.string().min(1), + startDate: z.string().min(1), + startTime: z.string().min(1), + endDate: z.string().min(1), + endTime: z.string().min(1), + isOperationsCalendar: z.boolean().optional(), + discordChannelId: z.string().optional(), + }) + .optional(), + }) + .superRefine((data, ctx) => { + if (!data.isEvent) { + if (Number.isNaN(data.date.getTime())) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid date", + path: ["date"], + }); + } + return; + } -function parseEventDateTime(dateValue?: string, timeValue?: string) { - if (!dateValue || !timeValue) { - return null; - } + const ed = data.eventData; + if (!ed) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Event data required", + }); + return; + } - const [year, month, day] = dateValue.split("-").map(Number); - const [hour, minute] = timeValue.split(":").map(Number); - if (!year || !month || !day || Number.isNaN(hour) || Number.isNaN(minute)) { - return null; - } + const start = parseEventDateTime(ed.startDate, ed.startTime); + const end = parseEventDateTime(ed.endDate, ed.endTime); - const parsed = new Date(year, month - 1, day, hour, minute, 0, 0); - if (Number.isNaN(parsed.getTime())) { - return null; - } + if (!start || !end) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Invalid start or end datetime", + }); + return; + } - return parsed; -} + if (end <= start) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "End time must be after start time", + }); + } + }); -interface IssueDialogFormValues { - id?: string; - status: (typeof ISSUE.ISSUE_STATUS)[number]; - name: string; - description: string; - links: string[]; - date: Date; - priority: (typeof ISSUE.PRIORITY)[number]; - team: string; - parent?: string; - isEvent: boolean; - event?: ISSUE.UUID | null; - eventData?: ISSUE.EventFormValues; - teamVisibilityIds?: string[]; - assigneeIds?: string[]; - roles: string[]; -} type CreateEditDialogComponentProps = Omit< ISSUE.CreateEditDialogProps, "open" > & { open?: boolean; onOpenChange?: (open: boolean) => void; - children?: React.ReactNode; -}; - -const defaultEventForm = (): ISSUE.EventFormValues => { - return { - discordId: "", - googleId: "", - name: "", - tag: EVENTS.EVENT_TAGS[0], - description: "", - startDate: "", - startTime: "", - endDate: "", - endTime: "", - location: "", - roles: [], - dues_paying: false, - isOperationsCalendar: false, - discordChannelId: "", - points: undefined, - hackathonId: undefined, - }; + children?: ReactNode; }; -const defaultForm = (): IssueDialogFormValues => { - return { - status: ISSUE.ISSUE_STATUS[0], - name: "", - description: "", - links: [], - date: normalizeTaskDueDate(), - priority: ISSUE.PRIORITY[0], - team: "", - parent: undefined, - isEvent: false, - event: null, - eventData: undefined, - roles: [], - }; -}; +const defaultForm = (): ISSUE.IssueEditNode => newIssueNode(); + +function templateToIssueNodes( + items: ISSUE.IssueTemplate[], + parentDate: Date, +): ISSUE.IssueEditNode[] { + return items.map((item) => { + const date = item.dateMs + ? new Date(parentDate.getTime() - item.dateMs) + : undefined; + return newIssueNode({ + name: item.title, + description: item.description ?? "", + team: item.team ?? "", + date, + children: templateToIssueNodes( + item.children ?? [], + date ?? normalizeTaskDueDate(), + ), + }); + }); +} export function CreateEditDialog(props: CreateEditDialogComponentProps) { const { @@ -190,26 +163,33 @@ export function CreateEditDialog(props: CreateEditDialogComponentProps) { onSubmit, children, } = props; - const [internalOpen, setInternalOpen] = React.useState(false); + const [internalOpen, setInternalOpen] = useState(false); const isControlled = open !== undefined; const isOpen = isControlled ? open : internalOpen; + const utils = api.useUtils(); const rolesQuery = api.roles.getAllLinks.useQuery(); const hackathonsQuery = api.hackathon.getHackathons.useQuery(); + const createIssueMutation = api.issues.createIssue.useMutation(); + const createEventMutation = api.event.createEvent.useMutation(); + const updateIssueMutation = api.issues.updateIssue.useMutation(); + const isPending = + createIssueMutation.isPending || + createEventMutation.isPending || + updateIssueMutation.isPending; const rolesData = rolesQuery.data; const hackathons = hackathonsQuery.data; const isRolesLoading = rolesQuery.isLoading; const isHackathonsLoading = hackathonsQuery.isLoading; const rolesError = rolesQuery.error; const hackathonsError = hackathonsQuery.error; - const [portalElement, setPortalElement] = React.useState( - null, - ); - const buildInitialFormValues = React.useCallback(() => { + const buildInitialFormValues = useCallback(() => { const defaults = defaultForm(); - const initial = (initialValues ?? {}) as Partial; + const { + children: _children, + ...initial + }: Partial = initialValues ?? {}; const resolvedEventData = initial.eventData; - const resolvedRoles = - initial.roles ?? initial.teamVisibilityIds ?? defaults.roles; + const resolvedRoles = initial.teamVisibilityIds ?? defaults.roles; if (initial.isEvent) { return { ...defaults, @@ -233,37 +213,36 @@ export function CreateEditDialog(props: CreateEditDialogComponentProps) { roles: resolvedRoles, }; }, [initialValues]); - const [formValues, setFormValues] = React.useState( + const [formValues, setFormValues] = useState( buildInitialFormValues, ); + const [childIssues, setChildIssues] = useState([]); - const handleClose = React.useCallback(() => { + const handleClose = useCallback(() => { if (isControlled) { - if (onOpenChange) { - onOpenChange(false); - } + onOpenChange?.(false); } else { setInternalOpen(false); } onClose?.(); - }, [isControlled, onClose, onOpenChange]); + setFormValues(buildInitialFormValues()); + setChildIssues([]); + }, [buildInitialFormValues, isControlled, onClose, onOpenChange]); - const trigger = React.useMemo(() => { - if (!children || !React.isValidElement(children)) { + const trigger = useMemo(() => { + if (!children || !isValidElement(children)) { return null; } - const child = children as React.ReactElement<{ - onClick?: (event: React.MouseEvent) => void; + const child = children as ReactElement<{ + onClick?: (event: MouseEvent) => void; }>; - return React.cloneElement(child, { - onClick: (event: React.MouseEvent) => { + return cloneElement(child, { + onClick: (event: MouseEvent) => { child.props.onClick?.(event); if (isControlled) { - if (onOpenChange) { - onOpenChange(true); - } + onOpenChange?.(true); } else { setInternalOpen(true); } @@ -271,9 +250,9 @@ export function CreateEditDialog(props: CreateEditDialogComponentProps) { }); }, [children, isControlled, onOpenChange]); - const updateForm = ( + const updateForm = ( key: K, - value: IssueDialogFormValues[K], + value: ISSUE.IssueEditNode[K], ) => { setFormValues((previous) => ({ ...previous, @@ -281,68 +260,25 @@ export function CreateEditDialog(props: CreateEditDialogComponentProps) { })); }; - const baseId = React.useId(); - const startDateTime = parseEventDateTime( - formValues.eventData?.startDate, - formValues.eventData?.startTime, - ); - const endDateTime = parseEventDateTime( - formValues.eventData?.endDate, - formValues.eventData?.endTime, - ); - const [nowTimestamp, setNowTimestamp] = React.useState(null); - React.useEffect(() => { - if (isOpen) { - setNowTimestamp(Date.now()); - } - }, [isOpen]); - const isNameValid = formValues.name.trim().length > 0; - const isTeamValid = formValues.team.trim().length > 0; - const isDescriptionValid = formValues.description.trim().length > 0; - const isRolesValid = !rolesError; - const isTaskDateValid = !Number.isNaN(formValues.date.getTime()); - - const hasEventData = !!formValues.eventData; - const hasEventLocation = !!formValues.eventData?.location.trim(); - const hasEventDescription = !!formValues.eventData?.description.trim(); - const hasEventStartTime = !!startDateTime; - const hasEventEndTime = !!endDateTime; - const isInternalEvent = formValues.eventData?.isOperationsCalendar ?? false; - const hasDiscordChannelId = - !isInternalEvent || !!(formValues.eventData?.discordChannelId ?? "").trim(); - const isEventTimingValid = - hasEventStartTime && hasEventEndTime && endDateTime > startDateTime; - const isEventStartInFuture = Boolean( - hasEventStartTime && - nowTimestamp !== null && - startDateTime.getTime() > nowTimestamp, - ); - - const hasRequiredBaseFields = - isNameValid && isTeamValid && isDescriptionValid && isRolesValid; - const isTaskValid = isTaskDateValid; - const isEventValid = - hasEventData && - hasEventLocation && - hasEventDescription && - hasEventStartTime && - hasEventEndTime && - hasDiscordChannelId && - isEventTimingValid && - isEventStartInFuture; + const baseId = useId(); + const effectiveTeam = formValues.team || (rolesData?.[0]?.id ?? ""); + const isFormValid = issueFormSchema.safeParse({ + ...formValues, + team: effectiveTeam, + }).success; + const isRolesValid = !rolesError; const isSubmitDisabled = - !hasRequiredBaseFields || - (formValues.isEvent ? !isEventValid : !isTaskValid); - const roleIdSet = React.useMemo( + !isFormValid || !isRolesValid || !validateIssueNodes(childIssues); + const roleIdSet = useMemo( () => new Set((rolesData ?? []).map((role) => role.id)), [rolesData], ); - const safeVisibilityIds = React.useMemo( + const safeVisibilityIds = useMemo( () => formValues.roles.filter((roleId) => roleIdSet.has(roleId)), [formValues.roles, roleIdSet], ); - const safeEventVisibilityIds = React.useMemo( + const safeEventVisibilityIds = useMemo( () => (formValues.eventData?.roles ?? []).filter((roleId) => roleIdSet.has(roleId), @@ -364,151 +300,208 @@ export function CreateEditDialog(props: CreateEditDialogComponentProps) { })); }; - const updateEventTimePart = ( - which: "start" | "end", - part: "hour" | "minute" | "amPm", - value: string, - ) => { - const key = which === "start" ? "startTime" : "endTime"; - const parsed = parseTimeTo12h(formValues.eventData?.[key]); - const next: { - hour: string; - minute: string; - amPm: (typeof ISSUE.EVENT_TIME_AM_PM_OPTIONS)[number]; - } = { - hour: part === "hour" ? value : parsed.hour, - minute: part === "minute" ? value : parsed.minute, - amPm: part === "amPm" ? toAmPmValue(value) : parsed.amPm, - }; - - // Keep time editable when only one selector is changed by filling missing - // pieces with the first available option instead of clearing the field. - if (!next.hour) { - next.hour = ISSUE.EVENT_TIME_HOURS[0] ?? "01"; - } - if (!next.minute) { - next.minute = ISSUE.EVENT_TIME_MINUTES[0] ?? "00"; - } - - updateEventData(key, `${to24h(next.hour, next.amPm)}:${next.minute}`); - }; - - React.useEffect(() => { - setPortalElement(document.body); - }, []); + const handleUpdateIssue = useCallback( + (clientId: string, patch: Partial) => + setChildIssues((prev) => updateIssueNode(prev, clientId, patch)), + [], + ); + const handleRemoveIssue = useCallback( + (clientId: string) => + setChildIssues((prev) => removeIssueNode(prev, clientId)), + [], + ); + const handleAddChildIssue = useCallback( + (parentClientId: string) => + setChildIssues((prev) => addChildToIssueNode(prev, parentClientId)), + [], + ); - React.useEffect(() => { - if (!isOpen) { - return; - } + const { data: templates = [], isLoading: isTemplatesLoading } = + api.issues.getTemplates.useQuery(undefined, { enabled: isOpen }); - setFormValues(buildInitialFormValues()); - }, [buildInitialFormValues, isOpen]); + const applyTemplate = (template: { name: string; body: unknown }) => { + const body = template.body as ISSUE.IssueTemplate[]; + const root = Array.isArray(body) ? body[0] : undefined; - React.useEffect(() => { - if (!isOpen) { - return; - } + setFormValues((prev) => ({ + ...prev, + name: root?.title ?? template.name, + description: root?.description ?? prev.description, + ...(root?.team ? { team: root.team } : {}), + })); - const previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; + setChildIssues( + templateToIssueNodes( + root?.children ?? [], + normalizeTaskDueDate(formValues.date), + ), + ); + }; - const handleKeydown = (event: KeyboardEvent) => { - if (event.key === "Escape") { - event.preventDefault(); - handleClose(); + async function buildIssueNodes( + nodes: ISSUE.IssueEditNode[], + ): Promise { + const results: ISSUE.IssueSubmitNode[] = []; + for (const node of nodes) { + let eventId: string | null = null; + + if (node.isEvent && node.eventData) { + const ed = node.eventData; + const startDatetime = parseEventDateTime(ed.startDate, ed.startTime); + const endDatetime = parseEventDateTime(ed.endDate, ed.endTime); + + if (startDatetime && endDatetime) { + const createdEvent = await createEventMutation.mutateAsync({ + name: node.name.trim(), + tag: ed.tag as (typeof EVENTS.EVENT_TAGS)[number], + description: ed.description.trim(), + start_datetime: startDatetime, + end_datetime: endDatetime, + location: ed.location, + dues_paying: ed.dues_paying, + isOperationsCalendar: ed.isOperationsCalendar ?? false, + discordChannelId: ed.discordChannelId, + roles: ed.roles ?? [], + points: ed.points, + hackathonId: ed.hackathonId ?? undefined, + }); + eventId = createdEvent.id; + } } - }; - - window.addEventListener("keydown", handleKeydown); - - return () => { - document.body.style.overflow = previousOverflow; - window.removeEventListener("keydown", handleKeydown); - }; - }, [handleClose, isOpen]); - - React.useEffect(() => { - if (!isOpen || formValues.team || !rolesData?.length) { - return; - } - - const firstRole = rolesData[0]; - if (!firstRole) { - return; - } - updateForm("team", firstRole.id); - }, [formValues.team, isOpen, rolesData]); - - const handleOverlayPointerDown = ( - event: React.MouseEvent, - ) => { - if (event.target === event.currentTarget) { - handleClose(); + results.push({ + status: node.status, + name: node.name, + description: node.description.trim(), + links: node.links, + date: eventId + ? (parseEventDateTime( + node.eventData?.startDate, + node.eventData?.startTime, + ) ?? normalizeTaskDueDate(node.date)) + : normalizeTaskDueDate(node.date), + priority: node.priority, + team: node.team, + event: eventId, + teamVisibilityIds: node.roles.length > 0 ? node.roles : undefined, + assigneeIds: node.assigneeIds, + children: + node.children.length > 0 + ? await buildIssueNodes(node.children) + : undefined, + }); } - }; + return results; + } - const handleSubmit = (event: React.FormEvent) => { + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); - if (isSubmitDisabled) { + if (isSubmitDisabled || isPending) { return; } - const toSubmitValues = ( - date: Date, - eventDataValue?: ISSUE.EventFormValues, - ): ISSUE.IssueSubmitValues => ({ - id: formValues.id, + const baseIssueFields = { status: formValues.status, - name: formValues.name, + name: formValues.name.trim(), description: formValues.description.trim(), links: formValues.links, - date, priority: formValues.priority, - team: formValues.team, - parent: formValues.parent, - isEvent: formValues.isEvent, - event: formValues.event ?? null, - eventData: eventDataValue, + team: effectiveTeam, teamVisibilityIds: safeVisibilityIds.length > 0 ? safeVisibilityIds : undefined, assigneeIds: formValues.assigneeIds, - }); - - // If not event, clear event field - if (!formValues.isEvent) { - onSubmit?.( - toSubmitValues(normalizeTaskDueDate(formValues.date), undefined), - ); - if (!isControlled) { - setInternalOpen(false); - } - } else { - const startDate = formValues.eventData?.startDate; - const startTime = formValues.eventData?.startTime; - const linkedIssueDate = parseEventDateTime(startDate, startTime); + }; - if ( - !linkedIssueDate || - !isEventTimingValid || - linkedIssueDate.getTime() <= Date.now() - ) { - return; - } + try { + if (intent === "create") { + const submitIssueNodes = + childIssues.length > 0 + ? await buildIssueNodes(childIssues) + : undefined; + + if (formValues.isEvent) { + const ed = formValues.eventData ?? defaultEventForm(); + const startDatetime = parseEventDateTime(ed.startDate, ed.startTime); + const endDatetime = parseEventDateTime(ed.endDate, ed.endTime); + + if (!startDatetime || !endDatetime) return; + + const createdEvent = await createEventMutation.mutateAsync({ + name: formValues.name.trim(), + tag: ed.tag as (typeof EVENTS.EVENT_TAGS)[number], + description: ed.description.trim(), + start_datetime: startDatetime, + end_datetime: endDatetime, + location: ed.location, + dues_paying: ed.dues_paying, + isOperationsCalendar: ed.isOperationsCalendar ?? false, + discordChannelId: ed.discordChannelId, + roles: safeEventVisibilityIds, + points: ed.points, + hackathonId: ed.hackathonId ?? undefined, + }); + + await createIssueMutation.mutateAsync({ + ...baseIssueFields, + date: startDatetime, + event: createdEvent.id, + children: submitIssueNodes, + }); + } else { + await createIssueMutation.mutateAsync({ + ...baseIssueFields, + date: normalizeTaskDueDate(formValues.date), + event: null, + children: submitIssueNodes, + }); + } - onSubmit?.( - toSubmitValues(linkedIssueDate, { - ...(formValues.eventData ?? defaultEventForm()), + await utils.issues.invalidate(); + toast.success("Issue created successfully"); + onSubmit?.({ + ...baseIssueFields, + parent: formValues.parent, + date: normalizeTaskDueDate(formValues.date), + isEvent: formValues.isEvent, + event: formValues.event ?? null, + eventData: formValues.eventData, + }); + } else if (formValues.id) { + await updateIssueMutation.mutateAsync({ + id: formValues.id, + status: formValues.status, name: formValues.name.trim(), - description: (formValues.eventData?.description ?? "").trim(), - roles: safeEventVisibilityIds, - }), - ); - if (!isControlled) { - setInternalOpen(false); + description: formValues.description.trim(), + priority: formValues.priority, + team: effectiveTeam, + parent: formValues.parent ?? null, + date: formValues.isEvent + ? undefined + : normalizeTaskDueDate(formValues.date), + teamVisibilityIds: + safeVisibilityIds.length > 0 ? safeVisibilityIds : undefined, + assigneeIds: formValues.assigneeIds, + }); + + await utils.issues.invalidate(); + toast.success("Issue updated successfully"); + onSubmit?.({ + ...baseIssueFields, + id: formValues.id, + parent: formValues.parent, + date: normalizeTaskDueDate(formValues.date), + isEvent: formValues.isEvent, + event: formValues.event ?? null, + eventData: formValues.eventData, + }); } + + handleClose(); + } catch (err) { + const message = + err instanceof Error ? err.message : "Something went wrong"; + toast.error(message); } }; @@ -520,9 +513,9 @@ export function CreateEditDialog(props: CreateEditDialogComponentProps) { name: formValues.name, description: formValues.description, links: formValues.links, - date: formValues.date, + date: normalizeTaskDueDate(formValues.date), priority: formValues.priority, - team: formValues.team, + team: effectiveTeam, parent: formValues.parent, isEvent: formValues.isEvent, event: formValues.event ?? null, @@ -534,676 +527,257 @@ export function CreateEditDialog(props: CreateEditDialogComponentProps) { } }; - if (!portalElement) { - return trigger; - } - return ( <> {trigger} - {isOpen && - createPortal( -
{ + if (!open) handleClose(); + }} + > + +
- event.stopPropagation()} - > - - -
-

- {intent === "edit" - ? formValues.isEvent - ? "Edit Event" - : "Edit Task" - : "Create Issue"} -

-

- {intent === "edit" - ? formValues.isEvent - ? "Update the event details below" - : "Update the task details below" - : "Enter the issue details below"} -

-
- -
-
-
- + +

+ {intent === "edit" + ? formValues.isEvent + ? "Edit Event" + : "Edit Task" + : "Create Issue"} +

+ + {intent === "edit" + ? formValues.isEvent + ? "Update the event details below" + : "Update the task details below" + : "Enter the issue details below"} + +
+ +
+
+
+ + updateForm("name", event.target.value)} + /> +
- - updateForm("name", event.target.value) - } +
+ +
+ { + const nextIsEvent = checked === true; + setFormValues((previous) => ({ + ...previous, + isEvent: nextIsEvent, + eventData: nextIsEvent + ? (previous.eventData ?? defaultEventForm()) + : undefined, + })); + }} />
+
-
- -
- { - const nextIsEvent = checked === true; - if (nextIsEvent) { - setFormValues((previous) => ({ - ...previous, - isEvent: true, - eventData: - previous.eventData ?? defaultEventForm(), - })); - return; - } +
+ + updateForm("status", v)} + /> +
- setFormValues((previous) => ({ - ...previous, - isEvent: false, - eventData: undefined, - })); - }} + {/* Date/Time fields */} + {formValues.isEvent && formValues.eventData ? ( + + ) : ( + <> +
+ + + updateForm( + "date", + normalizeTaskDueDate(e.target.value), + ) + } />
-
-
- - -
- - {/* Date/Time fields */} - {formValues.isEvent ? ( - <> -
- - -
- -
- - -
- -
- - - updateEventData("startDate", e.target.value) - } - /> -
- -
- -
- - - : - - - - -
-
- -
- - - updateEventData("endDate", e.target.value) - } - /> -
- -
- -
- - - : - - - - -
-
- -
- - - updateEventData("location", e.target.value) - } - /> -
- -
- -
-
- - updateEventData( - "isOperationsCalendar", - checked === true, - ) - } - /> - - Use Operations Calendar (Hide from public events) - -
-
-
- - {(formValues.eventData?.isOperationsCalendar ?? - false) && ( -
- - - updateEventData( - "discordChannelId", - event.target.value, - ) - } - /> -
- )} - -
- -
- - updateEventData("dues_paying", checked === true) - } - /> -
-
- - ) : ( - <> -
- - - updateForm( - "date", - normalizeTaskDueDate(e.target.value), - ) - } - /> -
- -
- - -
- - )} - -
- - - - - - {ISSUE.PRIORITY.map((priority) => ( - - {priority} - - ))} - - -
+ value={ISSUE.TASK_DUE_TIME} + readOnly + disabled + /> +
+ + )} + +
+ + updateForm("priority", v)} + /> +
-
- - -
+
+ + updateForm("team", v)} + roles={rolesData ?? []} + isLoading={isRolesLoading} + error={rolesError} + /> +
+
+ +