SupportFormV2.tsx 9.1 KB

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