SupportFormV2.tsx261 lines · main
| 1 | // End of third-party imports |
| 2 | import { SupportCategories } from '@supabase/shared-types/out/constants' |
| 3 | import { useConstant, useFlag } from 'common' |
| 4 | import { CLIENT_LIBRARIES } from 'common/constants' |
| 5 | import { type Dispatch, type MouseEventHandler } from 'react' |
| 6 | import type { SubmitHandler, UseFormReturn } from 'react-hook-form' |
| 7 | import { DialogSectionSeparator, Form } from 'ui' |
| 8 | |
| 9 | import { |
| 10 | AffectedServicesSelector, |
| 11 | CATEGORIES_WITHOUT_AFFECTED_SERVICES, |
| 12 | } from './AffectedServicesSelector' |
| 13 | import { AttachmentUploadDisplay, useAttachmentUpload } from './AttachmentUpload' |
| 14 | import { CategoryAndSeverityInfo } from './CategoryAndSeverityInfo' |
| 15 | import { ClientLibraryInfo } from './ClientLibraryInfo' |
| 16 | import { |
| 17 | DASHBOARD_LOG_CATEGORIES, |
| 18 | getSanitizedBreadcrumbs, |
| 19 | uploadDashboardLog, |
| 20 | } from './dashboard-logs' |
| 21 | import { DashboardLogsToggle } from './DashboardLogsToggle' |
| 22 | import { MessageField } from './MessageField' |
| 23 | import { OrganizationSelector } from './OrganizationSelector' |
| 24 | import { ProjectAndPlanInfo } from './ProjectAndPlanInfo' |
| 25 | import { SubjectAndSuggestionsInfo } from './SubjectAndSuggestionsInfo' |
| 26 | import { SubmitButton } from './SubmitButton' |
| 27 | import { DISABLE_SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle' |
| 28 | import type { SupportFormValues } from './SupportForm.schema' |
| 29 | import type { SupportFormActions, SupportFormState } from './SupportForm.state' |
| 30 | import { |
| 31 | formatMessage, |
| 32 | formatStudioVersion, |
| 33 | getOrgSubscriptionPlan, |
| 34 | NO_ORG_MARKER, |
| 35 | NO_PROJECT_MARKER, |
| 36 | } from './SupportForm.utils' |
| 37 | import { getProjectAuthConfig } from '@/data/auth/auth-config-query' |
| 38 | import { useSendSupportTicketMutation } from '@/data/feedback/support-ticket-send' |
| 39 | import { type OrganizationPlanID } from '@/data/organizations/organization-query' |
| 40 | import { useOrganizationsQuery } from '@/data/organizations/organizations-query' |
| 41 | import { useGenerateAttachmentURLsMutation } from '@/data/support/generate-attachment-urls-mutation' |
| 42 | import { useDeploymentCommitQuery } from '@/data/utils/deployment-commit-query' |
| 43 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 44 | import { detectBrowser } from '@/lib/helpers' |
| 45 | import { useProfile } from '@/lib/profile' |
| 46 | |
| 47 | const 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 | |
| 62 | interface SupportFormV2Props { |
| 63 | form: UseFormReturn<SupportFormValues> |
| 64 | initialError: string | null |
| 65 | state: SupportFormState |
| 66 | dispatch: Dispatch<SupportFormActions> |
| 67 | } |
| 68 | |
| 69 | export 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 | } |