Remove useProjectsQuery from support form (#39026)

* Remove useProjectsQuery from support form

* Add comment for refactor
This commit is contained in:
Joshen Lim
2025-09-26 15:34:10 +08:00
committed by GitHub
parent 8d028b872e
commit 607dafefbd
3 changed files with 140 additions and 121 deletions

View File

@@ -2,6 +2,7 @@ import { Check, ExternalLink, Mail, Search } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import { useProfile } from 'lib/profile'
import { Button, Input, Separator } from 'ui'
import { CATEGORY_OPTIONS } from './Support.constants'
@@ -9,18 +10,16 @@ import { CATEGORY_OPTIONS } from './Support.constants'
interface SuccessProps {
sentCategory?: string
selectedProject?: string
projects?: any[]
}
const Success = ({
sentCategory = '',
selectedProject = 'no-project',
projects = [],
}: SuccessProps) => {
export const Success = ({ sentCategory = '', selectedProject = 'no-project' }: SuccessProps) => {
const { profile } = useProfile()
const respondToEmail = profile?.primary_email ?? 'your email'
const project = projects.find((p) => p.ref === selectedProject)
const { data: project } = useProjectDetailQuery(
{ ref: selectedProject },
{ enabled: selectedProject !== 'no-project' }
)
const projectName = project ? project.name : 'No specific project'
const categoriesToShowAdditionalResources = ['Problem', 'Unresponsive', 'Performance']
@@ -98,5 +97,3 @@ const Success = ({
</div>
)
}
export default Success

View File

@@ -1,5 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod'
import * as Sentry from '@sentry/nextjs'
import { AnimatePresence, motion } from 'framer-motion'
import {
Book,
Check,
@@ -13,6 +14,7 @@ import {
X,
} from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'sonner'
@@ -20,15 +22,16 @@ import * as z from 'zod'
import { useDocsSearch, useParams, type DocsSearchResult as Page } from 'common'
import { CLIENT_LIBRARIES } from 'common/constants'
import CopyButton from 'components/ui/CopyButton'
import { OrganizationProjectSelector } from 'components/ui/OrganizationProjectSelector'
import { getProjectAuthConfig } from 'data/auth/auth-config-query'
import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { getProjectDetail } from 'data/projects/project-detail-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { detectBrowser } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useRouter } from 'next/router'
import { useQueryState } from 'nuqs'
import {
Badge,
Button,
@@ -72,6 +75,41 @@ const MAX_ATTACHMENTS = 5
const INCLUDE_DISCUSSIONS = ['Problem', 'Database_unresponsive']
const CONTAINER_CLASSES = 'px-6'
const FormSchema = z
.object({
organizationSlug: z.string().min(1, 'Please select an organization'),
projectRef: z.string().min(1, 'Please select a project'),
category: z.string().min(1, 'Please select an issue type'),
severity: z.string(),
library: z.string(),
subject: z.string().min(1, 'Please add a subject heading'),
message: z.string().min(1, "Please add a message about the issue that you're facing"),
affectedServices: z.string(),
allowSupportAccess: z.boolean(),
})
.refine(
(data) => {
return !(data.category === 'Problem' && data.library === '')
},
{
message: "Please select the library that you're facing issues with",
path: ['library'],
}
)
const defaultValues = {
organizationSlug: '',
// [Joshen TODO] We should refactor this to accept a null value instead of a magic string
projectRef: 'no-project',
category: '',
severity: 'Low',
library: '',
subject: '',
message: '',
affectedServices: '',
allowSupportAccess: true,
}
interface SupportFormV2Props {
onProjectSelected: (value: string) => void
onOrganizationSelected: (value: string) => void
@@ -86,9 +124,12 @@ export const SupportFormV2 = ({
setSentCategory,
}: SupportFormV2Props) => {
const { profile } = useProfile()
const [highlightRef, setHighlightRef] = useQueryState('highlight', { defaultValue: '' })
// [Joshen] Ideally refactor all these to use nuqs
const {
projectRef: ref,
slug,
projectRef: urlRef,
slug: urlSlug,
category: urlCategory,
subject: urlSubject,
message: urlMessage,
@@ -103,40 +144,6 @@ export const SupportFormV2 = ({
const [uploadedFiles, setUploadedFiles] = useState<File[]>([])
const [uploadedDataUrls, setUploadedDataUrls] = useState<string[]>([])
const FormSchema = z
.object({
organizationSlug: z.string().min(1, 'Please select an organization'),
projectRef: z.string().min(1, 'Please select a project'),
category: z.string().min(1, 'Please select an issue type'),
severity: z.string(),
library: z.string(),
subject: z.string().min(1, 'Please add a subject heading'),
message: z.string().min(1, "Please add a message about the issue that you're facing"),
affectedServices: z.string(),
allowSupportAccess: z.boolean(),
})
.refine(
(data) => {
return !(data.category === 'Problem' && data.library === '')
},
{
message: "Please select the library that you're facing issues with",
path: ['library'],
}
)
const defaultValues = {
organizationSlug: '',
projectRef: 'no-project',
category: '',
severity: 'Low',
library: '',
subject: '',
message: '',
affectedServices: '',
allowSupportAccess: true,
}
const form = useForm<z.infer<typeof FormSchema>>({
mode: 'onBlur',
reValidateMode: 'onBlur',
@@ -158,8 +165,6 @@ export const SupportFormV2 = ({
() => organizations?.find((org) => org.slug === organizationSlug),
[organizationSlug, organizations]
)
const { data, isSuccess: isSuccessProjects } = useProjectsQuery()
const allProjects = data?.projects ?? []
const { mutate: sendEvent } = useSendEventMutation()
@@ -282,28 +287,32 @@ export const SupportFormV2 = ({
useEffect(() => {
// For prefilling form fields via URL, project ref will taking higher precedence than org slug
if (isSuccessOrganizations && isSuccessProjects) {
if (organizations.length === 0) {
form.setValue('organizationSlug', 'no-org')
} else if (ref) {
const selectedProject = allProjects.find((p) => p.ref === ref)
if (selectedProject !== undefined) {
form.setValue('organizationSlug', selectedProject.organization_slug)
form.setValue('projectRef', selectedProject.ref)
}
} else if (slug) {
if (organizations.some((it) => it.slug === slug)) {
form.setValue('organizationSlug', slug)
}
} else if (ref === undefined && slug === undefined) {
const firstOrganization = organizations?.[0]
if (firstOrganization !== undefined) {
form.setValue('organizationSlug', firstOrganization.slug)
const prefillForm = async () => {
if (isSuccessOrganizations) {
if (organizations.length === 0) {
form.setValue('organizationSlug', 'no-org')
} else if (urlRef) {
// Check validity of project via project details
const selectedProject = await getProjectDetail({ ref: urlRef })
if (!!selectedProject) {
const org = organizations.find((x) => x.id === selectedProject.organization_id)
if (!!org) form.setValue('organizationSlug', org.slug)
form.setValue('projectRef', selectedProject.ref)
}
} else if (urlSlug) {
if (organizations.some((it) => it.slug === urlSlug)) {
form.setValue('organizationSlug', urlSlug)
}
} else if (!urlRef && !urlSlug) {
const firstOrganization = organizations?.[0]
if (!!firstOrganization) {
form.setValue('organizationSlug', firstOrganization.slug)
}
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref, slug, isSuccessOrganizations, isSuccessProjects])
prefillForm()
}, [urlRef, urlSlug, isSuccessOrganizations])
useEffect(() => {
if (urlCategory) {
@@ -396,19 +405,21 @@ export const SupportFormV2 = ({
)}
/>
<div className={cn(CONTAINER_CLASSES, 'flex flex-col gap-y-2')}>
<div id="projectRef-field" className={cn(CONTAINER_CLASSES, 'flex flex-col gap-y-2')}>
<FormField_Shadcn_
name="projectRef"
control={form.control}
render={({ field }) => (
<FormItemLayout layout="vertical" label="Which project is affected?">
<FormItemLayout hideMessage layout="vertical" label="Which project is affected?">
<FormControl_Shadcn_>
<OrganizationProjectSelector
sameWidthAsTrigger
checkPosition="left"
slug={organizationSlug}
selectedRef={field.value}
onInitialLoad={(projects) => field.onChange(projects[0]?.ref ?? 'no-project')}
onInitialLoad={(projects) => {
if (!urlRef) field.onChange(projects[0]?.ref ?? 'no-project')
}}
onSelect={(project) => field.onChange(project.ref)}
renderTrigger={({ isLoading, project }) => (
<Button
@@ -450,6 +461,46 @@ export const SupportFormV2 = ({
)}
/>
<AnimatePresence>
{projectRef !== 'no-project' && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="flex items-center gap-x-1"
>
<p
className={cn(
'text-sm prose transition',
highlightRef ? 'text-foreground' : 'text-foreground-lighter'
)}
>
Project ID:{' '}
<code
className={cn(
'transition',
highlightRef
? 'text-brand font-medium border-brand-500 animate-pulse'
: 'text-foreground-light'
)}
>
{projectRef}
</code>
</p>
<CopyButton
iconOnly
type="text"
text={projectRef}
onClick={() => {
toast.success('Copied to clipboard')
setHighlightRef(null)
}}
/>
</motion.div>
)}
</AnimatePresence>
{organizationSlug &&
subscriptionPlanId !== 'enterprise' &&
category !== 'Login_issues' && (

View File

@@ -1,22 +1,23 @@
import { ClipboardIcon, Loader2, Wrench } from 'lucide-react'
import { Loader2, Wrench } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import SVG from 'react-inlinesvg'
import { toast } from 'sonner'
import { AIAssistantOption } from 'components/interfaces/Support/AIAssistantOption'
import Success from 'components/interfaces/Support/Success'
import { Success } from 'components/interfaces/Support/Success'
import { SupportFormV2 } from 'components/interfaces/Support/SupportFormV2'
import AppLayout from 'components/layouts/AppLayout/AppLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import CopyButton from 'components/ui/CopyButton'
import InformationBox from 'components/ui/InformationBox'
import { InlineLink, InlineLinkClassName } from 'components/ui/InlineLink'
import { usePlatformStatusQuery } from 'data/platform/platform-status-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { withAuth } from 'hooks/misc/withAuth'
import { BASE_PATH } from 'lib/constants'
import { toast } from 'sonner'
import { useQueryState } from 'nuqs'
import { NextPageWithLayout } from 'types'
import { Button, copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
const SupportPage: NextPageWithLayout = () => {
const [sentCategory, setSentCategory] = useState<string>()
@@ -27,7 +28,7 @@ const SupportPage: NextPageWithLayout = () => {
const { data, isLoading } = usePlatformStatusQuery()
const isHealthy = data?.isHealthy
const { data: projectsData } = useProjectsQuery()
const [_, setHighlightRef] = useQueryState('highlight', { defaultValue: '' })
return (
<div className="relative flex overflow-y-auto overflow-x-hidden">
@@ -88,11 +89,7 @@ const SupportPage: NextPageWithLayout = () => {
].join(' ')}
>
{sentCategory !== undefined ? (
<Success
sentCategory={sentCategory}
selectedProject={selectedProject}
projects={projectsData?.projects}
/>
<Success sentCategory={sentCategory} selectedProject={selectedProject} />
) : (
<SupportFormV2
onProjectSelected={setSelectedProject}
@@ -106,14 +103,11 @@ const SupportPage: NextPageWithLayout = () => {
title="Having trouble submitting the form?"
description={
<div className="flex flex-col gap-y-4">
<p className="flex items-center gap-x-1">
<p className="flex items-center gap-x-1 ">
Email us directly at{' '}
<Link
href="mailto:support@supabase.com"
className="p-1 font-mono rounded-md text-foreground"
>
<InlineLink href="mailto:support@supabase.com" className="font-mono">
support@supabase.com
</Link>
</InlineLink>
<CopyButton
type="text"
text="support@supabase.com"
@@ -123,39 +117,16 @@ const SupportPage: NextPageWithLayout = () => {
</p>
<p>
Please, make sure to{' '}
<Tooltip>
<TooltipTrigger asChild>
<span className="text-foreground underline">include your project ID</span>
</TooltipTrigger>
<TooltipContent className="px-0">
<ul className="p-2">
<li className="grid pb-1 grid-cols-2 px-2 text-foreground-lighter">
<span>Project name</span>
<span>ID</span>
</li>
{(projectsData?.projects ?? []).map((project) => (
<li key={project.id} className="cursor-default">
<button
onClick={() => {
copyToClipboard(project.ref)
toast.success('Copied to clipboard')
}}
className="w-full group py-1.5 px-2 gap-x-1 text-foreground hover:bg-muted grid grid-cols-2 text-left rounded-sm"
>
<span className="truncate max-w-40">{project.name}</span>
<span className="flex w-full gap-x-1 items-center font-mono">
{project.ref}
<ClipboardIcon
size="14"
className="text-foreground-lighter opacity-0 group-hover:opacity-100 transition-opacity"
/>
</span>
</button>
</li>
))}
</ul>
</TooltipContent>
</Tooltip>{' '}
<span
className={cn(InlineLinkClassName, 'cursor-pointer')}
onClick={() => {
const el = document.getElementById('projectRef-field')
el?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' })
setHighlightRef('true')
}}
>
include your project ID
</span>{' '}
and as much information as possible.
</p>
</div>