useSupportForm.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { useEffect, useRef, useState, type Dispatch } from 'react'
  3. import { useForm, useWatch, type DefaultValues, type UseFormReturn } from 'react-hook-form'
  4. import { SupportFormSchema, type SupportFormValues } from './SupportForm.schema'
  5. import type { SupportFormActions } from './SupportForm.state'
  6. import {
  7. loadSupportFormInitialParams,
  8. loadSupportFormInitialParamsFromObject,
  9. NO_ORG_MARKER,
  10. NO_PROJECT_MARKER,
  11. selectInitialOrgAndProject,
  12. type SupportFormUrlKeys,
  13. } from './SupportForm.utils'
  14. // End of third-party imports
  15. import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
  16. const supportFormDefaultValues: DefaultValues<SupportFormValues> = {
  17. organizationSlug: NO_ORG_MARKER,
  18. projectRef: NO_PROJECT_MARKER,
  19. severity: 'Low',
  20. category: undefined,
  21. library: '',
  22. subject: '',
  23. message: '',
  24. affectedServices: '',
  25. allowSupportAccess: true,
  26. attachDashboardLogs: true,
  27. dashboardSentryIssueId: '',
  28. }
  29. interface UseSupportFormResult {
  30. form: UseFormReturn<SupportFormValues>
  31. initialError: string | null
  32. projectRef: string | null
  33. orgSlug: string | null
  34. }
  35. export function useSupportForm(
  36. dispatch: Dispatch<SupportFormActions>,
  37. initialParams?: Partial<SupportFormUrlKeys>
  38. ): UseSupportFormResult {
  39. const form = useForm<SupportFormValues>({
  40. mode: 'onBlur',
  41. reValidateMode: 'onBlur',
  42. resolver: zodResolver(SupportFormSchema as any),
  43. defaultValues: supportFormDefaultValues,
  44. })
  45. const urlParamsRef = useRef<SupportFormUrlKeys | null>(null)
  46. const providedInitialParamsRef = useRef(initialParams)
  47. const [initialError, setInitialError] = useState<string | null>(null)
  48. // Load initial values from URL params after mount so SSR/SSG render with
  49. // bare defaults (no `window` access) and the client hydrates against the
  50. // same HTML. URL-derived values are applied here, post-hydration.
  51. useEffect(() => {
  52. const params =
  53. providedInitialParamsRef.current !== undefined
  54. ? loadSupportFormInitialParamsFromObject(providedInitialParamsRef.current)
  55. : loadSupportFormInitialParams(window.location.search)
  56. urlParamsRef.current = params
  57. setInitialError(params.error ?? null)
  58. if (params.category && !form.getFieldState('category').isDirty) {
  59. form.setValue('category', params.category, { shouldDirty: false })
  60. }
  61. if (typeof params.subject === 'string' && !form.getFieldState('subject').isDirty) {
  62. form.setValue('subject', params.subject, { shouldDirty: false })
  63. }
  64. if (typeof params.message === 'string' && !form.getFieldState('message').isDirty) {
  65. form.setValue('message', params.message, { shouldDirty: false })
  66. }
  67. if (params.sid && !form.getFieldState('dashboardSentryIssueId').isDirty) {
  68. form.setValue('dashboardSentryIssueId', params.sid, {
  69. shouldDirty: false,
  70. })
  71. }
  72. }, [form])
  73. const hasAppliedOrgProjectRef = useRef(false)
  74. const { data: organizations, isPending: organizationsLoading } = useOrganizationsQuery()
  75. // Organization slug and project ref need to be validated after loading from
  76. // URL params
  77. useEffect(() => {
  78. if (hasAppliedOrgProjectRef.current) return
  79. if (!urlParamsRef.current) return
  80. if (organizationsLoading) return
  81. hasAppliedOrgProjectRef.current = true
  82. const orgSlugFromUrl =
  83. urlParamsRef.current.orgSlug && urlParamsRef.current.orgSlug !== NO_ORG_MARKER
  84. ? urlParamsRef.current.orgSlug
  85. : null
  86. const projectRefFromUrl = urlParamsRef.current.projectRef ?? null
  87. selectInitialOrgAndProject({
  88. projectRef: projectRefFromUrl,
  89. orgSlug: orgSlugFromUrl,
  90. orgs: organizations ?? [],
  91. })
  92. .then(({ orgSlug, projectRef }) => {
  93. if (!form.getFieldState('organizationSlug').isDirty) {
  94. form.setValue('organizationSlug', orgSlug ?? NO_ORG_MARKER, {
  95. shouldDirty: false,
  96. })
  97. }
  98. if (!form.getFieldState('projectRef').isDirty) {
  99. form.setValue('projectRef', projectRef ?? NO_PROJECT_MARKER, {
  100. shouldDirty: false,
  101. })
  102. }
  103. })
  104. .catch(() => {
  105. // Ignored: fall back to defaults when lookup fails
  106. })
  107. .finally(() => {
  108. dispatch({ type: 'INITIALIZE', debugSource: 'useSupportForm' })
  109. })
  110. }, [organizations, organizationsLoading, form, dispatch])
  111. const watchedProjectRef = useWatch({
  112. control: form.control,
  113. name: 'projectRef',
  114. })
  115. const watchedOrgSlug = useWatch({
  116. control: form.control,
  117. name: 'organizationSlug',
  118. })
  119. const projectRef =
  120. watchedProjectRef && watchedProjectRef !== NO_PROJECT_MARKER ? watchedProjectRef : null
  121. const orgSlug = watchedOrgSlug && watchedOrgSlug !== NO_ORG_MARKER ? watchedOrgSlug : null
  122. return {
  123. form,
  124. initialError,
  125. projectRef,
  126. orgSlug,
  127. }
  128. }