SupportFormV3.tsx316 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 { Form, Separator } 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 { PlanExpectationInfoContent, 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 { SupportFormDirectEmailContent } from './SupportFormDirectEmailInfo' |
| 38 | import { getProjectAuthConfig } from '@/data/auth/auth-config-query' |
| 39 | import { useSendSupportTicketMutation } from '@/data/feedback/support-ticket-send' |
| 40 | import { type OrganizationPlanID } from '@/data/organizations/organization-query' |
| 41 | import { useOrganizationsQuery } from '@/data/organizations/organizations-query' |
| 42 | import { useGenerateAttachmentURLsMutation } from '@/data/support/generate-attachment-urls-mutation' |
| 43 | import { useDeploymentCommitQuery } from '@/data/utils/deployment-commit-query' |
| 44 | import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' |
| 45 | import { detectBrowser } from '@/lib/helpers' |
| 46 | import { useProfile } from '@/lib/profile' |
| 47 | |
| 48 | const useIsSimplifiedForm = (slug: string, subscriptionPlanId?: OrganizationPlanID) => { |
| 49 | const simplifiedSupportForm = useFlag('simplifiedSupportForm') |
| 50 | |
| 51 | if (subscriptionPlanId === 'platform') { |
| 52 | return true |
| 53 | } |
| 54 | |
| 55 | if (typeof simplifiedSupportForm === 'string') { |
| 56 | const slugs = (simplifiedSupportForm as string).split(',').map((x) => x.trim()) |
| 57 | return slugs.includes(slug) |
| 58 | } |
| 59 | |
| 60 | return false |
| 61 | } |
| 62 | |
| 63 | interface SupportFormV3Props { |
| 64 | form: UseFormReturn<SupportFormValues> |
| 65 | initialError: string | null |
| 66 | state: SupportFormState |
| 67 | dispatch: Dispatch<SupportFormActions> |
| 68 | selectedProjectRef?: string | null |
| 69 | } |
| 70 | |
| 71 | export const SupportFormV3 = ({ |
| 72 | form, |
| 73 | initialError, |
| 74 | state, |
| 75 | dispatch, |
| 76 | selectedProjectRef, |
| 77 | }: SupportFormV3Props) => { |
| 78 | const { profile } = useProfile() |
| 79 | const respondToEmail = profile?.primary_email ?? 'your email' |
| 80 | |
| 81 | const { organizationSlug, projectRef, category, severity, subject, library } = form.watch() |
| 82 | |
| 83 | const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug |
| 84 | const currentProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef |
| 85 | |
| 86 | const { data: organizations } = useOrganizationsQuery() |
| 87 | const subscriptionPlanId = getOrgSubscriptionPlan(organizations, selectedOrgSlug) |
| 88 | const simplifiedSupportForm = useIsSimplifiedForm(organizationSlug, subscriptionPlanId) |
| 89 | const showClientLibraries = useIsFeatureEnabled('support:show_client_libraries') |
| 90 | |
| 91 | const attachmentUpload = useAttachmentUpload() |
| 92 | const { mutateAsync: uploadDashboardLogFn } = useGenerateAttachmentURLsMutation() |
| 93 | |
| 94 | const sanitizedLogSnapshot = useConstant(getSanitizedBreadcrumbs) |
| 95 | |
| 96 | const { data: commit } = useDeploymentCommitQuery({ |
| 97 | staleTime: 1000 * 60 * 10, |
| 98 | }) |
| 99 | |
| 100 | const { mutate: submitSupportTicket } = useSendSupportTicketMutation({ |
| 101 | onSuccess: (_, variables) => { |
| 102 | dispatch({ |
| 103 | type: 'SUCCESS', |
| 104 | sentProjectRef: variables.projectRef, |
| 105 | sentOrgSlug: variables.organizationSlug, |
| 106 | sentCategory: variables.category, |
| 107 | }) |
| 108 | }, |
| 109 | onError: (error) => { |
| 110 | dispatch({ |
| 111 | type: 'ERROR', |
| 112 | message: error.message, |
| 113 | }) |
| 114 | }, |
| 115 | }) |
| 116 | |
| 117 | const onSubmit: SubmitHandler<SupportFormValues> = async (formValues) => { |
| 118 | if ( |
| 119 | !simplifiedSupportForm && |
| 120 | showClientLibraries && |
| 121 | formValues.category === SupportCategories.PROBLEM && |
| 122 | !formValues.library |
| 123 | ) { |
| 124 | form.setError('library', { |
| 125 | type: 'manual', |
| 126 | message: "Please select the library that you're facing issues with", |
| 127 | }) |
| 128 | return |
| 129 | } |
| 130 | |
| 131 | dispatch({ type: 'SUBMIT' }) |
| 132 | |
| 133 | const { attachDashboardLogs: formAttachDashboardLogs, ...values } = formValues |
| 134 | const attachDashboardLogs = |
| 135 | formAttachDashboardLogs && DASHBOARD_LOG_CATEGORIES.includes(values.category) |
| 136 | |
| 137 | const [attachments, dashboardLogUrl] = await Promise.all([ |
| 138 | attachmentUpload.createAttachments(), |
| 139 | attachDashboardLogs |
| 140 | ? uploadDashboardLog({ |
| 141 | userId: profile?.gotrue_id, |
| 142 | sanitizedLogs: sanitizedLogSnapshot, |
| 143 | uploadDashboardLogFn, |
| 144 | }) |
| 145 | : undefined, |
| 146 | ]) |
| 147 | |
| 148 | const selectedLibrary = values.library |
| 149 | ? CLIENT_LIBRARIES.find((library) => library.language === values.library) |
| 150 | : undefined |
| 151 | |
| 152 | const payload = { |
| 153 | ...values, |
| 154 | organizationSlug: values.organizationSlug ?? NO_ORG_MARKER, |
| 155 | projectRef: values.projectRef ?? NO_PROJECT_MARKER, |
| 156 | allowSupportAccess: |
| 157 | values.category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(values.category) |
| 158 | ? values.allowSupportAccess |
| 159 | : false, |
| 160 | library: |
| 161 | values.category === SupportCategories.PROBLEM && selectedLibrary !== undefined |
| 162 | ? selectedLibrary.key |
| 163 | : '', |
| 164 | message: formatMessage({ |
| 165 | message: values.message, |
| 166 | attachments, |
| 167 | error: initialError, |
| 168 | }), |
| 169 | verified: true, |
| 170 | tags: ['dashboard-support-form'], |
| 171 | siteUrl: '', |
| 172 | additionalRedirectUrls: '', |
| 173 | affectedServices: CATEGORIES_WITHOUT_AFFECTED_SERVICES.includes(values.category) |
| 174 | ? '' |
| 175 | : values.affectedServices |
| 176 | .split(',') |
| 177 | .map((x) => x.trim().replace(/ /g, '_').toLowerCase()) |
| 178 | .join(';'), |
| 179 | browserInformation: detectBrowser(), |
| 180 | dashboardLogs: dashboardLogUrl?.[0], |
| 181 | dashboardStudioVersion: commit ? formatStudioVersion(commit) : undefined, |
| 182 | } |
| 183 | |
| 184 | if (values.projectRef !== NO_PROJECT_MARKER) { |
| 185 | try { |
| 186 | const authConfig = await getProjectAuthConfig({ |
| 187 | projectRef: values.projectRef, |
| 188 | }) |
| 189 | payload.siteUrl = authConfig.SITE_URL |
| 190 | payload.additionalRedirectUrls = authConfig.URI_ALLOW_LIST |
| 191 | } catch { |
| 192 | // Nice-to-have only |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | submitSupportTicket(payload) |
| 197 | } |
| 198 | |
| 199 | const handleFormSubmit = form.handleSubmit(onSubmit) |
| 200 | |
| 201 | const handleSubmitButtonClick: MouseEventHandler<HTMLButtonElement> = (event) => { |
| 202 | handleFormSubmit(event) |
| 203 | } |
| 204 | |
| 205 | const showPlanExpectationInfo = |
| 206 | !!selectedOrgSlug && |
| 207 | subscriptionPlanId !== 'enterprise' && |
| 208 | subscriptionPlanId !== 'platform' && |
| 209 | category !== 'Login_issues' |
| 210 | const showDirectEmailInfo = state.type !== 'success' && selectedProjectRef !== undefined |
| 211 | |
| 212 | return ( |
| 213 | <Form {...form}> |
| 214 | <form id="support-form" className="flex min-h-full flex-col"> |
| 215 | <div className="flex flex-col gap-y-6"> |
| 216 | <OrganizationSelector form={form} orgSlug={organizationSlug} /> |
| 217 | <ProjectAndPlanInfo |
| 218 | form={form} |
| 219 | orgSlug={selectedOrgSlug} |
| 220 | projectRef={currentProjectRef} |
| 221 | subscriptionPlanId={subscriptionPlanId} |
| 222 | category={category} |
| 223 | /> |
| 224 | <CategoryAndSeverityInfo |
| 225 | form={form} |
| 226 | category={category} |
| 227 | severity={severity} |
| 228 | projectRef={projectRef} |
| 229 | /> |
| 230 | </div> |
| 231 | |
| 232 | <div className="flex flex-col gap-y-6 py-6"> |
| 233 | <SubjectAndSuggestionsInfo form={form} subject={subject} category={category} /> |
| 234 | {!simplifiedSupportForm && ( |
| 235 | <> |
| 236 | <ClientLibraryInfo form={form} library={library} category={category} /> |
| 237 | <AffectedServicesSelector form={form} category={category} /> |
| 238 | </> |
| 239 | )} |
| 240 | <MessageField form={form} originalError={initialError} /> |
| 241 | <AttachmentUploadDisplay {...attachmentUpload} /> |
| 242 | </div> |
| 243 | |
| 244 | {(DASHBOARD_LOG_CATEGORIES.includes(category) || |
| 245 | (!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category)) || |
| 246 | showPlanExpectationInfo || |
| 247 | showDirectEmailInfo) && ( |
| 248 | <div className="flex flex-col gap-y-6"> |
| 249 | <Separator /> |
| 250 | |
| 251 | {DASHBOARD_LOG_CATEGORIES.includes(category) && ( |
| 252 | <DashboardLogsToggle form={form} sanitizedLog={sanitizedLogSnapshot} align="right" /> |
| 253 | )} |
| 254 | |
| 255 | {!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && ( |
| 256 | <SupportAccessToggle form={form} align="right" /> |
| 257 | )} |
| 258 | |
| 259 | {(showPlanExpectationInfo || showDirectEmailInfo) && ( |
| 260 | <SupportFormV3AdditionalInfoSection |
| 261 | orgSlug={selectedOrgSlug} |
| 262 | subscriptionPlanId={subscriptionPlanId} |
| 263 | projectRef={currentProjectRef} |
| 264 | showPlanExpectationInfo={showPlanExpectationInfo} |
| 265 | showDirectEmailInfo={showDirectEmailInfo} |
| 266 | /> |
| 267 | )} |
| 268 | </div> |
| 269 | )} |
| 270 | |
| 271 | <div className="sticky bottom-0 z-10 -mx-5 mt-6 border-t bg-panel-footer-light px-5 py-4"> |
| 272 | <SubmitButton |
| 273 | isSubmitting={state.type === 'submitting'} |
| 274 | userEmail={respondToEmail} |
| 275 | onClick={handleSubmitButtonClick} |
| 276 | descriptionClassName="pr-0" |
| 277 | /> |
| 278 | </div> |
| 279 | </form> |
| 280 | </Form> |
| 281 | ) |
| 282 | } |
| 283 | |
| 284 | interface SupportFormV3AdditionalInfoSectionProps { |
| 285 | orgSlug: string | null |
| 286 | subscriptionPlanId?: OrganizationPlanID |
| 287 | projectRef: string | null |
| 288 | showPlanExpectationInfo: boolean |
| 289 | showDirectEmailInfo: boolean |
| 290 | } |
| 291 | |
| 292 | function SupportFormV3AdditionalInfoSection({ |
| 293 | orgSlug, |
| 294 | subscriptionPlanId, |
| 295 | projectRef, |
| 296 | showPlanExpectationInfo, |
| 297 | showDirectEmailInfo, |
| 298 | }: SupportFormV3AdditionalInfoSectionProps) { |
| 299 | return ( |
| 300 | <div className="flex flex-col gap-y-5"> |
| 301 | {showPlanExpectationInfo && orgSlug && ( |
| 302 | <div className="flex flex-col gap-y-2"> |
| 303 | <h5 className="text-foreground">Support varies by plan</h5> |
| 304 | <PlanExpectationInfoContent orgSlug={orgSlug} planId={subscriptionPlanId} /> |
| 305 | </div> |
| 306 | )} |
| 307 | |
| 308 | {showDirectEmailInfo && ( |
| 309 | <div className="flex flex-col gap-y-2"> |
| 310 | <h5 className="text-foreground">Having trouble submitting the form?</h5> |
| 311 | <SupportFormDirectEmailContent projectRef={projectRef} /> |
| 312 | </div> |
| 313 | )} |
| 314 | </div> |
| 315 | ) |
| 316 | } |