LinkSupportTicketForm.tsx173 lines · main
| 1 | import { zodResolver } from '@hookform/resolvers/zod' |
| 2 | import { Link2 } from 'lucide-react' |
| 3 | import { useEffect } from 'react' |
| 4 | import type { SubmitHandler } from 'react-hook-form' |
| 5 | import { useForm } from 'react-hook-form' |
| 6 | import { toast } from 'sonner' |
| 7 | import { Button, DialogSectionSeparator, Form, FormControl, FormField, Input } from 'ui' |
| 8 | import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' |
| 9 | |
| 10 | import { CategoryAndSeverityInfo } from './CategoryAndSeverityInfo' |
| 11 | import { |
| 12 | LinkSupportTicketFormSchema, |
| 13 | type LinkSupportTicketFormValues, |
| 14 | } from './LinkSupportTicketForm.schema' |
| 15 | import { OrganizationSelector } from './OrganizationSelector' |
| 16 | import { ProjectAndPlanInfo } from './ProjectAndPlanInfo' |
| 17 | import { DISABLE_SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle' |
| 18 | import { getOrgSubscriptionPlan, NO_ORG_MARKER, NO_PROJECT_MARKER } from './SupportForm.utils' |
| 19 | import { useLinkSupportTicketMutation } from '@/data/feedback/link-support-ticket-mutation' |
| 20 | import { useOrganizationsQuery } from '@/data/organizations/organizations-query' |
| 21 | |
| 22 | interface LinkSupportTicketFormProps { |
| 23 | conversationId: string |
| 24 | onSuccess: () => void |
| 25 | } |
| 26 | |
| 27 | // [Joshen] Am reusing the SupportFormV2 components here but am not sure how to type things properly |
| 28 | // hence marking all the `form` as `any` when passing as props to the components for now |
| 29 | |
| 30 | export const LinkSupportTicketForm = ({ |
| 31 | conversationId, |
| 32 | onSuccess, |
| 33 | }: LinkSupportTicketFormProps) => { |
| 34 | const { data: organizations, isSuccess } = useOrganizationsQuery() |
| 35 | |
| 36 | const form = useForm<LinkSupportTicketFormValues>({ |
| 37 | resolver: zodResolver(LinkSupportTicketFormSchema as any), |
| 38 | defaultValues: { |
| 39 | conversation_id: conversationId, |
| 40 | organizationSlug: NO_ORG_MARKER, |
| 41 | projectRef: NO_PROJECT_MARKER, |
| 42 | category: '' as any, |
| 43 | allowSupportAccess: true, |
| 44 | }, |
| 45 | mode: 'onSubmit', |
| 46 | reValidateMode: 'onBlur', |
| 47 | }) |
| 48 | |
| 49 | const { category, organizationSlug, projectRef } = form.watch() |
| 50 | const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug |
| 51 | const selectedProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef |
| 52 | const subscriptionPlanId = getOrgSubscriptionPlan(organizations, selectedOrgSlug) |
| 53 | |
| 54 | const { mutate: linkSupportTicket, isPending } = useLinkSupportTicketMutation({ |
| 55 | onSuccess: () => { |
| 56 | toast.success('Support ticket linked successfully!') |
| 57 | onSuccess() |
| 58 | }, |
| 59 | onError: (error) => { |
| 60 | let errorMessage = error.message |
| 61 | try { |
| 62 | const parsed = JSON.parse(error.message) |
| 63 | errorMessage = parsed?._error?.message || parsed?.message || error.message |
| 64 | } catch {} |
| 65 | // If parsing fails, use the original message |
| 66 | toast.error(`Failed to link support ticket: ${errorMessage}`) |
| 67 | }, |
| 68 | }) |
| 69 | |
| 70 | const onSubmit: SubmitHandler<LinkSupportTicketFormValues> = (values) => { |
| 71 | if (!organizations) return toast.error('Organizations not loaded. Please try again.') |
| 72 | |
| 73 | const selectedOrg = organizations.find((org) => org.slug === values.organizationSlug) |
| 74 | if (!selectedOrg) return toast.error('Selected organization not found. Please try again.') |
| 75 | |
| 76 | linkSupportTicket({ |
| 77 | conversation_id: values.conversation_id, |
| 78 | org_id: selectedOrg.id, |
| 79 | project_ref: |
| 80 | values.projectRef && values.projectRef !== NO_PROJECT_MARKER |
| 81 | ? values.projectRef |
| 82 | : undefined, |
| 83 | category: values.category, |
| 84 | allow_support_access: |
| 85 | values.category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(values.category) |
| 86 | ? values.allowSupportAccess |
| 87 | : false, |
| 88 | }) |
| 89 | } |
| 90 | |
| 91 | useEffect(() => { |
| 92 | if (!!organizations && organizations.length > 0) { |
| 93 | form.reset({ |
| 94 | conversation_id: conversationId, |
| 95 | organizationSlug: organizations[0].slug, |
| 96 | projectRef: NO_PROJECT_MARKER, |
| 97 | category: '' as any, |
| 98 | allowSupportAccess: true, |
| 99 | }) |
| 100 | } |
| 101 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 102 | }, [isSuccess]) |
| 103 | |
| 104 | return ( |
| 105 | <Form {...form}> |
| 106 | <form |
| 107 | id="link-support-ticket-form" |
| 108 | onSubmit={form.handleSubmit(onSubmit)} |
| 109 | className="flex flex-col" |
| 110 | > |
| 111 | <div className="flex flex-col py-6 gap-y-6"> |
| 112 | <h3 className="px-6 text-xl">Link support ticket to account</h3> |
| 113 | <div className="px-6 flex flex-col gap-y-8"> |
| 114 | <FormField |
| 115 | control={form.control} |
| 116 | name="conversation_id" |
| 117 | render={({ field }) => ( |
| 118 | <FormItemLayout hideMessage layout="vertical" label="Conversation ID"> |
| 119 | <FormControl> |
| 120 | <Input {...field} readOnly /> |
| 121 | </FormControl> |
| 122 | </FormItemLayout> |
| 123 | )} |
| 124 | /> |
| 125 | |
| 126 | <OrganizationSelector form={form as any} orgSlug={organizationSlug} /> |
| 127 | {organizationSlug !== NO_ORG_MARKER && ( |
| 128 | <ProjectAndPlanInfo |
| 129 | form={form as any} |
| 130 | orgSlug={selectedOrgSlug} |
| 131 | projectRef={selectedProjectRef} |
| 132 | subscriptionPlanId={subscriptionPlanId} |
| 133 | category={category} |
| 134 | /> |
| 135 | )} |
| 136 | |
| 137 | <CategoryAndSeverityInfo |
| 138 | showSeverity={false} |
| 139 | showIssueSuggestion={false} |
| 140 | form={form as any} |
| 141 | category={category} |
| 142 | projectRef={projectRef} |
| 143 | /> |
| 144 | </div> |
| 145 | </div> |
| 146 | |
| 147 | <DialogSectionSeparator /> |
| 148 | |
| 149 | {!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && ( |
| 150 | <> |
| 151 | <div className="py-4"> |
| 152 | <SupportAccessToggle form={form as any} /> |
| 153 | </div> |
| 154 | <DialogSectionSeparator /> |
| 155 | </> |
| 156 | )} |
| 157 | |
| 158 | <div className="px-6 py-8"> |
| 159 | <Button |
| 160 | block |
| 161 | type="primary" |
| 162 | htmlType="submit" |
| 163 | size="large" |
| 164 | icon={<Link2 />} |
| 165 | loading={isPending} |
| 166 | > |
| 167 | Link support ticket to account |
| 168 | </Button> |
| 169 | </div> |
| 170 | </form> |
| 171 | </Form> |
| 172 | ) |
| 173 | } |