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