SupportFormV3.tsx 11 KB

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