From d1c6cbacbddb4e35d14acf7abd19737fa0d2a4d3 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:15:51 -0500 Subject: [PATCH] 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. --- .../Support/CategoryAndSeverityInfo.tsx | 10 +-- .../Support/LinkSupportTicketForm.tsx | 21 ++++--- .../interfaces/Support/Support.constants.ts | 8 --- .../Support/SupportAccessToggle.tsx | 18 +++--- .../interfaces/Support/SupportForm.schema.ts | 58 ++++------------- .../interfaces/Support/SupportFormV2.tsx | 62 ++++++++++--------- .../__tests__/SupportFormPage.test.tsx | 6 +- 7 files changed, 73 insertions(+), 110 deletions(-) diff --git a/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx b/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx index 1d6bf95ae82..58720a1cd27 100644 --- a/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx +++ b/apps/studio/components/interfaces/Support/CategoryAndSeverityInfo.tsx @@ -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) { - {CATEGORY_OPTIONS.filter((option) => !option.hidden).map((option) => ( + {CATEGORY_OPTIONS.map((option) => ( {option.label} diff --git a/apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx b/apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx index aeb32893467..68f35b26718 100644 --- a/apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx +++ b/apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx @@ -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 = ({ - {SUPPORT_ACCESS_CATEGORIES.includes(category) && ( + {!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && ( <>
diff --git a/apps/studio/components/interfaces/Support/Support.constants.ts b/apps/studio/components/interfaces/Support/Support.constants.ts index f7ddf76f920..8e02f560bfd 100644 --- a/apps/studio/components/interfaces/Support/Support.constants.ts +++ b/apps/studio/components/interfaces/Support/Support.constants.ts @@ -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 = [ diff --git a/apps/studio/components/interfaces/Support/SupportAccessToggle.tsx b/apps/studio/components/interfaces/Support/SupportAccessToggle.tsx index a5b49bc2e01..0c24ef7d63a 100644 --- a/apps/studio/components/interfaces/Support/SupportAccessToggle.tsx +++ b/apps/studio/components/interfaces/Support/SupportAccessToggle.tsx @@ -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 { diff --git a/apps/studio/components/interfaces/Support/SupportForm.schema.ts b/apps/studio/components/interfaces/Support/SupportForm.schema.ts index 29b160027f8..280780185a8 100644 --- a/apps/studio/components/interfaces/Support/SupportForm.schema.ts +++ b/apps/studio/components/interfaces/Support/SupportForm.schema.ts @@ -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 diff --git a/apps/studio/components/interfaces/Support/SupportFormV2.tsx b/apps/studio/components/interfaces/Support/SupportFormV2.tsx index 38e928934a7..eb1093d28dc 100644 --- a/apps/studio/components/interfaces/Support/SupportFormV2.tsx +++ b/apps/studio/components/interfaces/Support/SupportFormV2.tsx @@ -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 = 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 (
@@ -202,14 +210,12 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo subscriptionPlanId={subscriptionPlanId} category={category} /> - {!simplifiedSupportForm && ( - - )} +
@@ -235,7 +241,7 @@ export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFo )} - {SUPPORT_ACCESS_CATEGORIES.includes(category) && ( + {!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && ( <> diff --git a/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx b/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx index 4c85b45f3ca..9313f6d4d9c 100644 --- a/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx +++ b/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx @@ -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',