11import { zodResolver } from '@hookform/resolvers/zod'
22import * as Sentry from '@sentry/nextjs'
3+ import { AnimatePresence , motion } from 'framer-motion'
34import {
45 Book ,
56 Check ,
@@ -13,22 +14,24 @@ import {
1314 X ,
1415} from 'lucide-react'
1516import Link from 'next/link'
17+ import { useRouter } from 'next/router'
1618import { ChangeEvent , useEffect , useMemo , useRef , useState } from 'react'
1719import { SubmitHandler , useForm } from 'react-hook-form'
1820import { toast } from 'sonner'
1921import * as z from 'zod'
2022
2123import { useDocsSearch , useParams , type DocsSearchResult as Page } from 'common'
2224import { CLIENT_LIBRARIES } from 'common/constants'
25+ import CopyButton from 'components/ui/CopyButton'
2326import { OrganizationProjectSelector } from 'components/ui/OrganizationProjectSelector'
2427import { getProjectAuthConfig } from 'data/auth/auth-config-query'
2528import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send'
2629import { useOrganizationsQuery } from 'data/organizations/organizations-query'
27- import { useProjectsQuery } from 'data/projects/projects -query'
30+ import { getProjectDetail } from 'data/projects/project-detail -query'
2831import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
2932import { detectBrowser } from 'lib/helpers'
3033import { useProfile } from 'lib/profile'
31- import { useRouter } from 'next/router '
34+ import { useQueryState } from 'nuqs '
3235import {
3336 Badge ,
3437 Button ,
@@ -72,6 +75,41 @@ const MAX_ATTACHMENTS = 5
7275const INCLUDE_DISCUSSIONS = [ 'Problem' , 'Database_unresponsive' ]
7376const CONTAINER_CLASSES = 'px-6'
7477
78+ const FormSchema = z
79+ . object ( {
80+ organizationSlug : z . string ( ) . min ( 1 , 'Please select an organization' ) ,
81+ projectRef : z . string ( ) . min ( 1 , 'Please select a project' ) ,
82+ category : z . string ( ) . min ( 1 , 'Please select an issue type' ) ,
83+ severity : z . string ( ) ,
84+ library : z . string ( ) ,
85+ subject : z . string ( ) . min ( 1 , 'Please add a subject heading' ) ,
86+ message : z . string ( ) . min ( 1 , "Please add a message about the issue that you're facing" ) ,
87+ affectedServices : z . string ( ) ,
88+ allowSupportAccess : z . boolean ( ) ,
89+ } )
90+ . refine (
91+ ( data ) => {
92+ return ! ( data . category === 'Problem' && data . library === '' )
93+ } ,
94+ {
95+ message : "Please select the library that you're facing issues with" ,
96+ path : [ 'library' ] ,
97+ }
98+ )
99+
100+ const defaultValues = {
101+ organizationSlug : '' ,
102+ // [Joshen TODO] We should refactor this to accept a null value instead of a magic string
103+ projectRef : 'no-project' ,
104+ category : '' ,
105+ severity : 'Low' ,
106+ library : '' ,
107+ subject : '' ,
108+ message : '' ,
109+ affectedServices : '' ,
110+ allowSupportAccess : true ,
111+ }
112+
75113interface SupportFormV2Props {
76114 onProjectSelected : ( value : string ) => void
77115 onOrganizationSelected : ( value : string ) => void
@@ -86,9 +124,12 @@ export const SupportFormV2 = ({
86124 setSentCategory,
87125} : SupportFormV2Props ) => {
88126 const { profile } = useProfile ( )
127+ const [ highlightRef , setHighlightRef ] = useQueryState ( 'highlight' , { defaultValue : '' } )
128+
129+ // [Joshen] Ideally refactor all these to use nuqs
89130 const {
90- projectRef : ref ,
91- slug,
131+ projectRef : urlRef ,
132+ slug : urlSlug ,
92133 category : urlCategory ,
93134 subject : urlSubject ,
94135 message : urlMessage ,
@@ -103,40 +144,6 @@ export const SupportFormV2 = ({
103144 const [ uploadedFiles , setUploadedFiles ] = useState < File [ ] > ( [ ] )
104145 const [ uploadedDataUrls , setUploadedDataUrls ] = useState < string [ ] > ( [ ] )
105146
106- const FormSchema = z
107- . object ( {
108- organizationSlug : z . string ( ) . min ( 1 , 'Please select an organization' ) ,
109- projectRef : z . string ( ) . min ( 1 , 'Please select a project' ) ,
110- category : z . string ( ) . min ( 1 , 'Please select an issue type' ) ,
111- severity : z . string ( ) ,
112- library : z . string ( ) ,
113- subject : z . string ( ) . min ( 1 , 'Please add a subject heading' ) ,
114- message : z . string ( ) . min ( 1 , "Please add a message about the issue that you're facing" ) ,
115- affectedServices : z . string ( ) ,
116- allowSupportAccess : z . boolean ( ) ,
117- } )
118- . refine (
119- ( data ) => {
120- return ! ( data . category === 'Problem' && data . library === '' )
121- } ,
122- {
123- message : "Please select the library that you're facing issues with" ,
124- path : [ 'library' ] ,
125- }
126- )
127-
128- const defaultValues = {
129- organizationSlug : '' ,
130- projectRef : 'no-project' ,
131- category : '' ,
132- severity : 'Low' ,
133- library : '' ,
134- subject : '' ,
135- message : '' ,
136- affectedServices : '' ,
137- allowSupportAccess : true ,
138- }
139-
140147 const form = useForm < z . infer < typeof FormSchema > > ( {
141148 mode : 'onBlur' ,
142149 reValidateMode : 'onBlur' ,
@@ -158,8 +165,6 @@ export const SupportFormV2 = ({
158165 ( ) => organizations ?. find ( ( org ) => org . slug === organizationSlug ) ,
159166 [ organizationSlug , organizations ]
160167 )
161- const { data, isSuccess : isSuccessProjects } = useProjectsQuery ( )
162- const allProjects = data ?. projects ?? [ ]
163168
164169 const { mutate : sendEvent } = useSendEventMutation ( )
165170
@@ -282,28 +287,32 @@ export const SupportFormV2 = ({
282287
283288 useEffect ( ( ) => {
284289 // For prefilling form fields via URL, project ref will taking higher precedence than org slug
285- if ( isSuccessOrganizations && isSuccessProjects ) {
286- if ( organizations . length === 0 ) {
287- form . setValue ( 'organizationSlug' , 'no-org' )
288- } else if ( ref ) {
289- const selectedProject = allProjects . find ( ( p ) => p . ref === ref )
290- if ( selectedProject !== undefined ) {
291- form . setValue ( 'organizationSlug' , selectedProject . organization_slug )
292- form . setValue ( 'projectRef' , selectedProject . ref )
293- }
294- } else if ( slug ) {
295- if ( organizations . some ( ( it ) => it . slug === slug ) ) {
296- form . setValue ( 'organizationSlug' , slug )
297- }
298- } else if ( ref === undefined && slug === undefined ) {
299- const firstOrganization = organizations ?. [ 0 ]
300- if ( firstOrganization !== undefined ) {
301- form . setValue ( 'organizationSlug' , firstOrganization . slug )
290+ const prefillForm = async ( ) => {
291+ if ( isSuccessOrganizations ) {
292+ if ( organizations . length === 0 ) {
293+ form . setValue ( 'organizationSlug' , 'no-org' )
294+ } else if ( urlRef ) {
295+ // Check validity of project via project details
296+ const selectedProject = await getProjectDetail ( { ref : urlRef } )
297+ if ( ! ! selectedProject ) {
298+ const org = organizations . find ( ( x ) => x . id === selectedProject . organization_id )
299+ if ( ! ! org ) form . setValue ( 'organizationSlug' , org . slug )
300+ form . setValue ( 'projectRef' , selectedProject . ref )
301+ }
302+ } else if ( urlSlug ) {
303+ if ( organizations . some ( ( it ) => it . slug === urlSlug ) ) {
304+ form . setValue ( 'organizationSlug' , urlSlug )
305+ }
306+ } else if ( ! urlRef && ! urlSlug ) {
307+ const firstOrganization = organizations ?. [ 0 ]
308+ if ( ! ! firstOrganization ) {
309+ form . setValue ( 'organizationSlug' , firstOrganization . slug )
310+ }
302311 }
303312 }
304313 }
305- // eslint-disable-next-line react-hooks/exhaustive-deps
306- } , [ ref , slug , isSuccessOrganizations , isSuccessProjects ] )
314+ prefillForm ( )
315+ } , [ urlRef , urlSlug , isSuccessOrganizations ] )
307316
308317 useEffect ( ( ) => {
309318 if ( urlCategory ) {
@@ -396,19 +405,21 @@ export const SupportFormV2 = ({
396405 ) }
397406 />
398407
399- < div className = { cn ( CONTAINER_CLASSES , 'flex flex-col gap-y-2' ) } >
408+ < div id = "projectRef-field" className = { cn ( CONTAINER_CLASSES , 'flex flex-col gap-y-2' ) } >
400409 < FormField_Shadcn_
401410 name = "projectRef"
402411 control = { form . control }
403412 render = { ( { field } ) => (
404- < FormItemLayout layout = "vertical" label = "Which project is affected?" >
413+ < FormItemLayout hideMessage layout = "vertical" label = "Which project is affected?" >
405414 < FormControl_Shadcn_ >
406415 < OrganizationProjectSelector
407416 sameWidthAsTrigger
408417 checkPosition = "left"
409418 slug = { organizationSlug }
410419 selectedRef = { field . value }
411- onInitialLoad = { ( projects ) => field . onChange ( projects [ 0 ] ?. ref ?? 'no-project' ) }
420+ onInitialLoad = { ( projects ) => {
421+ if ( ! urlRef ) field . onChange ( projects [ 0 ] ?. ref ?? 'no-project' )
422+ } }
412423 onSelect = { ( project ) => field . onChange ( project . ref ) }
413424 renderTrigger = { ( { isLoading, project } ) => (
414425 < Button
@@ -450,6 +461,46 @@ export const SupportFormV2 = ({
450461 ) }
451462 />
452463
464+ < AnimatePresence >
465+ { projectRef !== 'no-project' && (
466+ < motion . div
467+ initial = { { opacity : 0 , height : 0 } }
468+ animate = { { opacity : 1 , height : 'auto' } }
469+ exit = { { opacity : 0 , height : 0 } }
470+ transition = { { duration : 0.3 } }
471+ className = "flex items-center gap-x-1"
472+ >
473+ < p
474+ className = { cn (
475+ 'text-sm prose transition' ,
476+ highlightRef ? 'text-foreground' : 'text-foreground-lighter'
477+ ) }
478+ >
479+ Project ID:{ ' ' }
480+ < code
481+ className = { cn (
482+ 'transition' ,
483+ highlightRef
484+ ? 'text-brand font-medium border-brand-500 animate-pulse'
485+ : 'text-foreground-light'
486+ ) }
487+ >
488+ { projectRef }
489+ </ code >
490+ </ p >
491+ < CopyButton
492+ iconOnly
493+ type = "text"
494+ text = { projectRef }
495+ onClick = { ( ) => {
496+ toast . success ( 'Copied to clipboard' )
497+ setHighlightRef ( null )
498+ } }
499+ />
500+ </ motion . div >
501+ ) }
502+ </ AnimatePresence >
503+
453504 { organizationSlug &&
454505 subscriptionPlanId !== 'enterprise' &&
455506 category !== 'Login_issues' && (
0 commit comments