SupportFormV2.tsx261 lines · main
1// End of third-party imports
2import { SupportCategories } from '@supabase/shared-types/out/constants'
3import { useConstant, useFlag } from 'common'
4import { CLIENT_LIBRARIES } from 'common/constants'
5import { type Dispatch, type MouseEventHandler } from 'react'
6import type { SubmitHandler, UseFormReturn } from 'react-hook-form'
7import { DialogSectionSeparator, Form } from 'ui'
8
9import {
10 AffectedServicesSelector,
11 CATEGORIES_WITHOUT_AFFECTED_SERVICES,
12} from './AffectedServicesSelector'
13import { AttachmentUploadDisplay, useAttachmentUpload } from './AttachmentUpload'
14import { CategoryAndSeverityInfo } from './CategoryAndSeverityInfo'
15import { ClientLibraryInfo } from './ClientLibraryInfo'
16import {
17 DASHBOARD_LOG_CATEGORIES,
18 getSanitizedBreadcrumbs,
19 uploadDashboardLog,
20} from './dashboard-logs'
21import { DashboardLogsToggle } from './DashboardLogsToggle'
22import { MessageField } from './MessageField'
23import { OrganizationSelector } from './OrganizationSelector'
24import { ProjectAndPlanInfo } from './ProjectAndPlanInfo'
25import { SubjectAndSuggestionsInfo } from './SubjectAndSuggestionsInfo'
26import { SubmitButton } from './SubmitButton'
27import { DISABLE_SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle'
28import type { SupportFormValues } from './SupportForm.schema'
29import type { SupportFormActions, SupportFormState } from './SupportForm.state'
30import {
31 formatMessage,
32 formatStudioVersion,
33 getOrgSubscriptionPlan,
34 NO_ORG_MARKER,
35 NO_PROJECT_MARKER,
36} from './SupportForm.utils'
37import { getProjectAuthConfig } from '@/data/auth/auth-config-query'
38import { useSendSupportTicketMutation } from '@/data/feedback/support-ticket-send'
39import { type OrganizationPlanID } from '@/data/organizations/organization-query'
40import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
41import { useGenerateAttachmentURLsMutation } from '@/data/support/generate-attachment-urls-mutation'
42import { useDeploymentCommitQuery } from '@/data/utils/deployment-commit-query'
43import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
44import { detectBrowser } from '@/lib/helpers'
45import { useProfile } from '@/lib/profile'
46
47const useIsSimplifiedForm = (slug: string, subscriptionPlanId?: OrganizationPlanID) => {
48 const simplifiedSupportForm = useFlag('simplifiedSupportForm')
49
50 if (subscriptionPlanId === 'platform') {
51 return true
52 }
53
54 if (typeof simplifiedSupportForm === 'string') {
55 const slugs = (simplifiedSupportForm as string).split(',').map((x) => x.trim())
56 return slugs.includes(slug)
57 }
58
59 return false
60}
61
62interface SupportFormV2Props {
63 form: UseFormReturn<SupportFormValues>
64 initialError: string | null
65 state: SupportFormState
66 dispatch: Dispatch<SupportFormActions>
67}
68
69export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFormV2Props) => {
70 const { profile } = useProfile()
71 const respondToEmail = profile?.primary_email ?? 'your email'
72
73 const { organizationSlug, projectRef, category, severity, subject, library } = form.watch()
74
75 const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug
76 const selectedProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef
77
78 const { data: organizations } = useOrganizationsQuery()
79 const subscriptionPlanId = getOrgSubscriptionPlan(organizations, selectedOrgSlug)
80 const simplifiedSupportForm = useIsSimplifiedForm(organizationSlug, subscriptionPlanId)
81 const showClientLibraries = useIsFeatureEnabled('support:show_client_libraries')
82
83 const attachmentUpload = useAttachmentUpload()
84 const { mutateAsync: uploadDashboardLogFn } = useGenerateAttachmentURLsMutation()
85
86 const sanitizedLogSnapshot = useConstant(getSanitizedBreadcrumbs)
87
88 const { data: commit } = useDeploymentCommitQuery({
89 staleTime: 1000 * 60 * 10, // 10 minutes
90 })
91
92 const { mutate: submitSupportTicket } = useSendSupportTicketMutation({
93 onSuccess: (_, variables) => {
94 dispatch({
95 type: 'SUCCESS',
96 sentProjectRef: variables.projectRef,
97 sentOrgSlug: variables.organizationSlug,
98 sentCategory: variables.category,
99 })
100 },
101 onError: (error) => {
102 dispatch({
103 type: 'ERROR',
104 message: error.message,
105 })
106 },
107 })
108
109 const onSubmit: SubmitHandler<SupportFormValues> = async (formValues) => {
110 // Library is required when selecting "APIs and Client Libraries" category,
111 // but only when the library selector is visible (not in simplified form)
112 if (
113 !simplifiedSupportForm &&
114 showClientLibraries &&
115 formValues.category === SupportCategories.PROBLEM &&
116 !formValues.library
117 ) {
118 form.setError('library', {
119 type: 'manual',
120 message: "Please select the library that you're facing issues with",
121 })
122 return
123 }
124
125 dispatch({ type: 'SUBMIT' })
126
127 const { attachDashboardLogs: formAttachDashboardLogs, ...values } = formValues
128 const attachDashboardLogs =
129 formAttachDashboardLogs && DASHBOARD_LOG_CATEGORIES.includes(values.category)
130
131 const [attachments, dashboardLogUrl] = await Promise.all([
132 attachmentUpload.createAttachments(),
133 attachDashboardLogs
134 ? uploadDashboardLog({
135 userId: profile?.gotrue_id,
136 sanitizedLogs: sanitizedLogSnapshot,
137 uploadDashboardLogFn,
138 })
139 : undefined,
140 ])
141
142 const selectedLibrary = values.library
143 ? CLIENT_LIBRARIES.find((library) => library.language === values.library)
144 : undefined
145
146 const payload = {
147 ...values,
148 organizationSlug: values.organizationSlug ?? NO_ORG_MARKER,
149 projectRef: values.projectRef ?? NO_PROJECT_MARKER,
150 allowSupportAccess:
151 values.category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(values.category)
152 ? values.allowSupportAccess
153 : false,
154 library:
155 values.category === SupportCategories.PROBLEM && selectedLibrary !== undefined
156 ? selectedLibrary.key
157 : '',
158 message: formatMessage({
159 message: values.message,
160 attachments,
161 error: initialError,
162 }),
163 verified: true,
164 tags: ['dashboard-support-form'],
165 siteUrl: '',
166 additionalRedirectUrls: '',
167 affectedServices: CATEGORIES_WITHOUT_AFFECTED_SERVICES.includes(values.category)
168 ? ''
169 : values.affectedServices
170 .split(',')
171 .map((x) => x.trim().replace(/ /g, '_').toLowerCase())
172 .join(';'),
173 browserInformation: detectBrowser(),
174 dashboardLogs: dashboardLogUrl?.[0],
175 dashboardStudioVersion: commit ? formatStudioVersion(commit) : undefined,
176 }
177
178 if (values.projectRef !== NO_PROJECT_MARKER) {
179 try {
180 const authConfig = await getProjectAuthConfig({
181 projectRef: values.projectRef,
182 })
183 payload.siteUrl = authConfig.SITE_URL
184 payload.additionalRedirectUrls = authConfig.URI_ALLOW_LIST
185 } catch {
186 // [Joshen] No error handler required as fetching these info are nice to haves, not necessary
187 }
188 }
189
190 submitSupportTicket(payload)
191 }
192
193 const handleFormSubmit = form.handleSubmit(onSubmit)
194
195 const handleSubmitButtonClick: MouseEventHandler<HTMLButtonElement> = (event) => {
196 handleFormSubmit(event)
197 }
198
199 return (
200 <Form {...form}>
201 <form id="support-form" className="flex flex-col gap-y-6">
202 <h3 className="px-6 text-xl">How can we help?</h3>
203
204 <div className="px-6 flex flex-col gap-y-8">
205 <OrganizationSelector form={form} orgSlug={organizationSlug} />
206 <ProjectAndPlanInfo
207 form={form}
208 orgSlug={selectedOrgSlug}
209 projectRef={selectedProjectRef}
210 subscriptionPlanId={subscriptionPlanId}
211 category={category}
212 />
213 <CategoryAndSeverityInfo
214 form={form}
215 category={category}
216 severity={severity}
217 projectRef={projectRef}
218 />
219 </div>
220
221 <DialogSectionSeparator />
222
223 <div className="px-6 flex flex-col gap-y-8">
224 <SubjectAndSuggestionsInfo form={form} subject={subject} category={category} />
225 {!simplifiedSupportForm && (
226 <>
227 <ClientLibraryInfo form={form} library={library} category={category} />
228 <AffectedServicesSelector form={form} category={category} />
229 </>
230 )}
231 <MessageField form={form} originalError={initialError} />
232 <AttachmentUploadDisplay {...attachmentUpload} />
233 </div>
234
235 <DialogSectionSeparator />
236
237 {DASHBOARD_LOG_CATEGORIES.includes(category) && (
238 <>
239 <DashboardLogsToggle form={form} sanitizedLog={sanitizedLogSnapshot} className="px-6" />
240 <DialogSectionSeparator />
241 </>
242 )}
243
244 {!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && (
245 <>
246 <SupportAccessToggle form={form} className="px-6" />
247 <DialogSectionSeparator />
248 </>
249 )}
250
251 <div className="px-6 pt-2">
252 <SubmitButton
253 isSubmitting={state.type === 'submitting'}
254 userEmail={respondToEmail}
255 onClick={handleSubmitButtonClick}
256 />
257 </div>
258 </form>
259 </Form>
260 )
261}