LinkSupportTicketForm.tsx173 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { Link2 } from 'lucide-react'
3import { useEffect } from 'react'
4import type { SubmitHandler } from 'react-hook-form'
5import { useForm } from 'react-hook-form'
6import { toast } from 'sonner'
7import { Button, DialogSectionSeparator, Form, FormControl, FormField, Input } from 'ui'
8import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
9
10import { CategoryAndSeverityInfo } from './CategoryAndSeverityInfo'
11import {
12 LinkSupportTicketFormSchema,
13 type LinkSupportTicketFormValues,
14} from './LinkSupportTicketForm.schema'
15import { OrganizationSelector } from './OrganizationSelector'
16import { ProjectAndPlanInfo } from './ProjectAndPlanInfo'
17import { DISABLE_SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle'
18import { getOrgSubscriptionPlan, NO_ORG_MARKER, NO_PROJECT_MARKER } from './SupportForm.utils'
19import { useLinkSupportTicketMutation } from '@/data/feedback/link-support-ticket-mutation'
20import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
21
22interface 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
30export 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}