fix(studio): support form validation for client library (#42325)

Bug fix

## What is the current behavior?

The client library selector validation was failing when the simplified
support form was used, because the field was required but hidden.

## What is the new behavior?

The client library selector is only required when: client library JSON
flag is on + not using simplified support form + client libraries is
chosen as the problem category.

## Summary by CodeRabbit

* **Bug Fixes**
* Improved support form validation logic and category eligibility for
support access requests.
* Refined conditional rendering of support access toggle based on
selected category.

* **New Features**
* Added feature-flag-based library selection capability for support
submissions.

* **Refactor**
  * Streamlined support category options and removed obsolete entries.
* Simplified form schema initialization and made library field optional
where applicable.
This commit is contained in:
Charis authored and GitHub committed 2026-01-30 15:15:51 -05:00
1 parent 29c5211864
commit d1c6cbacbd
7 files changed
+73 -110

No files matched your search

@@ -1,21 +1,21 @@
import type { UseFormReturn } from 'react-hook-form'
// End of third-party imports
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { InlineLink } from 'components/ui/InlineLink'
import type { UseFormReturn } from 'react-hook-form'
import {
cn,
FormControl_Shadcn_,
FormField_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Select_Shadcn_,
cn,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
CATEGORY_OPTIONS,
type ExtendedSupportCategories,
@@ -91,7 +91,7 @@ function CategorySelector({ form }: CategorySelectorProps) {
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{CATEGORY_OPTIONS.filter((option) => !option.hidden).map((option) => (
{CATEGORY_OPTIONS.map((option) => (
<SelectItem_Shadcn_ key={option.value} value={option.value}>
{option.label}
<span className="block text-xs text-foreground-lighter">
@@ -1,21 +1,21 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useLinkSupportTicketMutation } from 'data/feedback/link-support-ticket-mutation'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { Link2 } from 'lucide-react'
import { useEffect } from 'react'
import type { SubmitHandler } from 'react-hook-form'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { useLinkSupportTicketMutation } from 'data/feedback/link-support-ticket-mutation'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import {
Button,
DialogSectionSeparator,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { CategoryAndSeverityInfo } from './CategoryAndSeverityInfo'
import {
LinkSupportTicketFormSchema,
@@ -23,8 +23,8 @@ import {
} from './LinkSupportTicketForm.schema'
import { OrganizationSelector } from './OrganizationSelector'
import { ProjectAndPlanInfo } from './ProjectAndPlanInfo'
import { SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle'
import { getOrgSubscriptionPlan, NO_ORG_MARKER, NO_PROJECT_MARKER } from './SupportForm.utils'
import { DISABLE_SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle'
import { NO_ORG_MARKER, NO_PROJECT_MARKER, getOrgSubscriptionPlan } from './SupportForm.utils'
interface LinkSupportTicketFormProps {
conversationId: string
@@ -88,9 +88,10 @@ export const LinkSupportTicketForm = ({
? values.projectRef
: undefined,
category: values.category,
allow_support_access: SUPPORT_ACCESS_CATEGORIES.includes(values.category)
? values.allowSupportAccess
: false,
allow_support_access:
values.category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(values.category)
? values.allowSupportAccess
: false,
})
}
@@ -153,7 +154,7 @@ export const LinkSupportTicketForm = ({
<DialogSectionSeparator />
{SUPPORT_ACCESS_CATEGORIES.includes(category) && (
{!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && (
<>
<div className="py-4">
<SupportAccessToggle form={form as any} />
@@ -10,7 +10,6 @@ export const CATEGORY_OPTIONS: {
label: string
description: string
query?: string
hidden?: boolean
}[] = [
{
value: SupportCategories.PROBLEM,
@@ -78,13 +77,6 @@ export const CATEGORY_OPTIONS: {
query: undefined,
},
]),
{
value: 'Others' as const,
label: 'Others',
description: 'Issues that are not related to any of the other categories',
query: undefined,
hidden: true,
},
]
export const SEVERITY_OPTIONS = [
@@ -1,26 +1,26 @@
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import type { UseFormReturn } from 'react-hook-form'
// End of third-party imports
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import type { UseFormReturn } from 'react-hook-form'
import {
Badge,
Collapsible_Shadcn_,
CollapsibleContent_Shadcn_,
CollapsibleTrigger_Shadcn_,
Collapsible_Shadcn_,
FormField_Shadcn_,
Switch,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import type { ExtendedSupportCategories } from './Support.constants'
import type { SupportFormValues } from './SupportForm.schema'
export const SUPPORT_ACCESS_CATEGORIES: ExtendedSupportCategories[] = [
SupportCategories.DATABASE_UNRESPONSIVE,
SupportCategories.PERFORMANCE_ISSUES,
SupportCategories.PROBLEM,
SupportCategories.DASHBOARD_BUG,
export const DISABLE_SUPPORT_ACCESS_CATEGORIES: ExtendedSupportCategories[] = [
SupportCategories.ACCOUNT_DELETION,
SupportCategories.SALES_ENQUIRY,
SupportCategories.REFUND,
]
interface SupportAccessToggleProps {
@@ -1,11 +1,10 @@
import { PLAN_REQUEST_EMPTY_PLACEHOLDER } from 'components/ui/UpgradePlanButton'
import { z } from 'zod'
import { isFeatureEnabled } from 'common'
import { PLAN_REQUEST_EMPTY_PLACEHOLDER } from 'components/ui/UpgradePlanButton'
import { CATEGORY_OPTIONS, type ExtendedSupportCategories } from './Support.constants'
const createFormSchema = (showClientLibraries: boolean) => {
const baseSchema = z.object({
export const SupportFormSchema = z
.object({
organizationSlug: z.string().min(1, 'Please select an organization'),
projectRef: z.string().min(1, 'Please select a project'),
category: z.enum(
@@ -15,7 +14,7 @@ const createFormSchema = (showClientLibraries: boolean) => {
]
),
severity: z.string(),
library: z.string(),
library: z.string().optional(),
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(),
@@ -23,45 +22,14 @@ const createFormSchema = (showClientLibraries: boolean) => {
attachDashboardLogs: z.boolean(),
dashboardSentryIssueId: z.string().optional(),
})
.refine(
(data) => {
return !data.message.includes(PLAN_REQUEST_EMPTY_PLACEHOLDER)
},
{
message: `Please let us know which plan you'd like to upgrade to for your organization`,
path: ['message'],
}
)
if (showClientLibraries) {
return baseSchema
.refine(
(data) => {
return !(data.category === 'Problem' && data.library === '')
},
{
message: "Please select the library that you're facing issues with",
path: ['library'],
}
)
.refine(
(data) => {
return !data.message.includes(PLAN_REQUEST_EMPTY_PLACEHOLDER)
},
{
message: `Please let us know which plan you'd like to upgrade to for your organization`,
path: ['message'],
}
)
}
// When showClientLibraries is false, make library optional and remove the refine validation
return baseSchema
.extend({
library: z.string().optional(),
})
.refine(
(data) => {
return !data.message.includes(PLAN_REQUEST_EMPTY_PLACEHOLDER)
},
{
message: `Please let us know which plan you'd like to upgrade to for your organization`,
path: ['message'],
}
)
}
const showClientLibraries = isFeatureEnabled('support:show_client_libraries')
export const SupportFormSchema = createFormSchema(showClientLibraries)
export type SupportFormValues = z.infer<typeof SupportFormSchema>
@@ -1,7 +1,4 @@
import { useEffect, type Dispatch, type MouseEventHandler } from 'react'
import type { SubmitHandler, UseFormReturn } from 'react-hook-form'
// End of third-party imports
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useConstant, useFlag } from 'common'
import { CLIENT_LIBRARIES } from 'common/constants'
@@ -11,9 +8,13 @@ import { type OrganizationPlanID } from 'data/organizations/organization-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useGenerateAttachmentURLsMutation } from 'data/support/generate-attachment-urls-mutation'
import { useDeploymentCommitQuery } from 'data/utils/deployment-commit-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { detectBrowser } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { type Dispatch, type MouseEventHandler } from 'react'
import type { SubmitHandler, UseFormReturn } from 'react-hook-form'
import { DialogSectionSeparator, Form_Shadcn_ } from 'ui'
import {
AffectedServicesSelector,
CATEGORIES_WITHOUT_AFFECTED_SERVICES,
@@ -27,15 +28,15 @@ import { OrganizationSelector } from './OrganizationSelector'
import { ProjectAndPlanInfo } from './ProjectAndPlanInfo'
import { SubjectAndSuggestionsInfo } from './SubjectAndSuggestionsInfo'
import { SubmitButton } from './SubmitButton'
import { SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle'
import { DISABLE_SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle'
import type { SupportFormValues } from './SupportForm.schema'
import type { SupportFormActions, SupportFormState } from './SupportForm.state'
import {
NO_ORG_MARKER,
NO_PROJECT_MARKER,
formatMessage,
formatStudioVersion,
getOrgSubscriptionPlan,
NO_ORG_MARKER,
NO_PROJECT_MARKER,
} from './SupportForm.utils'
import {
DASHBOARD_LOG_CATEGORIES,
@@ -77,6 +78,7 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo
const { data: organizations } = useOrganizationsQuery()
const subscriptionPlanId = getOrgSubscriptionPlan(organizations, selectedOrgSlug)
const simplifiedSupportForm = useIsSimplifiedForm(organizationSlug, subscriptionPlanId)
const showClientLibraries = useIsFeatureEnabled('support:show_client_libraries')
const attachmentUpload = useAttachmentUpload()
const { mutateAsync: uploadDashboardLogFn } = useGenerateAttachmentURLsMutation()
@@ -105,6 +107,21 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo
})
const onSubmit: SubmitHandler<SupportFormValues> = async (formValues) => {
// Library is required when selecting "APIs and Client Libraries" category,
// but only when the library selector is visible (not in simplified form)
if (
!simplifiedSupportForm &&
showClientLibraries &&
formValues.category === SupportCategories.PROBLEM &&
!formValues.library
) {
form.setError('library', {
type: 'manual',
message: "Please select the library that you're facing issues with",
})
return
}
dispatch({ type: 'SUBMIT' })
const { attachDashboardLogs: formAttachDashboardLogs, ...values } = formValues
@@ -128,12 +145,12 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo
const payload = {
...values,
category,
organizationSlug: values.organizationSlug ?? NO_ORG_MARKER,
projectRef: values.projectRef ?? NO_PROJECT_MARKER,
allowSupportAccess: SUPPORT_ACCESS_CATEGORIES.includes(values.category)
? values.allowSupportAccess
: false,
allowSupportAccess:
values.category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(values.category)
? values.allowSupportAccess
: false,
library:
values.category === SupportCategories.PROBLEM && selectedLibrary !== undefined
? selectedLibrary.key
@@ -179,15 +196,6 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo
handleFormSubmit(event)
}
useEffect(() => {
if (simplifiedSupportForm) {
form.setValue('category', 'Others')
} else {
form.setValue('category', '' as any)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [simplifiedSupportForm])
return (
<Form_Shadcn_ {...form}>
<form id="support-form" className="flex flex-col gap-y-6">
@@ -202,14 +210,12 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo
subscriptionPlanId={subscriptionPlanId}
category={category}
/>
{!simplifiedSupportForm && (
<CategoryAndSeverityInfo
form={form}
category={category}
severity={severity}
projectRef={projectRef}
/>
)}
<CategoryAndSeverityInfo
form={form}
category={category}
severity={severity}
projectRef={projectRef}
/>
</div>
<DialogSectionSeparator />
@@ -235,7 +241,7 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo
</>
)}
{SUPPORT_ACCESS_CATEGORIES.includes(category) && (
{!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && (
<>
<SupportAccessToggle form={form} />
<DialogSectionSeparator />
@@ -891,10 +891,6 @@ describe('SupportFormPage', () => {
await userEvent.clear(messageField)
await userEvent.type(messageField, 'MFA challenge fails with an unknown error code')
expect(
screen.queryByRole('switch', { name: /allow support access to your project/i })
).toBeNull()
await userEvent.click(getSubmitButton(screen))
await waitFor(() => {
@@ -910,7 +906,7 @@ describe('SupportFormPage', () => {
organizationSlug: 'org-2',
library: '',
affectedServices: '',
allowSupportAccess: false,
allowSupportAccess: true,
verified: true,
tags: ['dashboard-support-form'],
siteUrl: 'https://project-2.supabase.dev',