Files
supabase/apps/studio/components/interfaces/Support/LinkSupportTicketForm.tsx
T
Saxon FletcherandGildas Garcia 4452e0ac2e Support form sidebar (#45203)
Refactors our help sidebar within Studio to include the actual support
form itself when contact is selected. This PR also cleans up the initial
state of the sidebar and the options within.

## To test:
- Open an org and click the help icon top right
- Click contact support
- Submit a support ticket
- Click done to return to support sidebar state

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Support form V3 and support sidebar with status button; direct-email
helper and URL prefill
* Success screen supports onFinish callback and customizable finish
label
* AI Assistant and Help options accept optional click callbacks;
resource items gain keyboard/accessibility support

* **Refactor**
  * Help panel split into home/support views with back navigation
* Support components accept flexible align/className props and
layout/styling tweaks
  * Initial URL params loader added for support form

* **Tests**
* New/updated tests for support flows, success screen, and help options
interactions
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
2026-05-08 13:51:49 +10:00

174 lines
5.9 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
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 { Button, DialogSectionSeparator, Form, FormControl, FormField, Input_Shadcn_ } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { CategoryAndSeverityInfo } from './CategoryAndSeverityInfo'
import {
LinkSupportTicketFormSchema,
type LinkSupportTicketFormValues,
} from './LinkSupportTicketForm.schema'
import { OrganizationSelector } from './OrganizationSelector'
import { ProjectAndPlanInfo } from './ProjectAndPlanInfo'
import { DISABLE_SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle'
import { getOrgSubscriptionPlan, NO_ORG_MARKER, NO_PROJECT_MARKER } from './SupportForm.utils'
import { useLinkSupportTicketMutation } from '@/data/feedback/link-support-ticket-mutation'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
interface LinkSupportTicketFormProps {
conversationId: string
onSuccess: () => void
}
// [Joshen] Am reusing the SupportFormV2 components here but am not sure how to type things properly
// hence marking all the `form` as `any` when passing as props to the components for now
export const LinkSupportTicketForm = ({
conversationId,
onSuccess,
}: LinkSupportTicketFormProps) => {
const { data: organizations, isSuccess } = useOrganizationsQuery()
const form = useForm<LinkSupportTicketFormValues>({
resolver: zodResolver(LinkSupportTicketFormSchema),
defaultValues: {
conversation_id: conversationId,
organizationSlug: NO_ORG_MARKER,
projectRef: NO_PROJECT_MARKER,
category: '' as any,
allowSupportAccess: true,
},
mode: 'onSubmit',
reValidateMode: 'onBlur',
})
const { category, organizationSlug, projectRef } = form.watch()
const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug
const selectedProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef
const subscriptionPlanId = getOrgSubscriptionPlan(organizations, selectedOrgSlug)
const { mutate: linkSupportTicket, isPending } = useLinkSupportTicketMutation({
onSuccess: () => {
toast.success('Support ticket linked successfully!')
onSuccess()
},
onError: (error) => {
let errorMessage = error.message
try {
const parsed = JSON.parse(error.message)
errorMessage = parsed?._error?.message || parsed?.message || error.message
} catch {}
// If parsing fails, use the original message
toast.error(`Failed to link support ticket: ${errorMessage}`)
},
})
const onSubmit: SubmitHandler<LinkSupportTicketFormValues> = (values) => {
if (!organizations) return toast.error('Organizations not loaded. Please try again.')
const selectedOrg = organizations.find((org) => org.slug === values.organizationSlug)
if (!selectedOrg) return toast.error('Selected organization not found. Please try again.')
linkSupportTicket({
conversation_id: values.conversation_id,
org_id: selectedOrg.id,
project_ref:
values.projectRef && values.projectRef !== NO_PROJECT_MARKER
? values.projectRef
: undefined,
category: values.category,
allow_support_access:
values.category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(values.category)
? values.allowSupportAccess
: false,
})
}
useEffect(() => {
if (!!organizations && organizations.length > 0) {
form.reset({
conversation_id: conversationId,
organizationSlug: organizations[0].slug,
projectRef: NO_PROJECT_MARKER,
category: '' as any,
allowSupportAccess: true,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess])
return (
<Form {...form}>
<form
id="link-support-ticket-form"
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col"
>
<div className="flex flex-col py-6 gap-y-6">
<h3 className="px-6 text-xl">Link support ticket to account</h3>
<div className="px-6 flex flex-col gap-y-8">
<FormField
control={form.control}
name="conversation_id"
render={({ field }) => (
<FormItemLayout hideMessage layout="vertical" label="Conversation ID">
<FormControl>
<Input_Shadcn_ {...field} readOnly />
</FormControl>
</FormItemLayout>
)}
/>
<OrganizationSelector form={form as any} orgSlug={organizationSlug} />
{organizationSlug !== NO_ORG_MARKER && (
<ProjectAndPlanInfo
form={form as any}
orgSlug={selectedOrgSlug}
projectRef={selectedProjectRef}
subscriptionPlanId={subscriptionPlanId}
category={category}
/>
)}
<CategoryAndSeverityInfo
showSeverity={false}
showIssueSuggestion={false}
form={form as any}
category={category}
projectRef={projectRef}
/>
</div>
</div>
<DialogSectionSeparator />
{!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && (
<>
<div className="py-4">
<SupportAccessToggle form={form as any} />
</div>
<DialogSectionSeparator />
</>
)}
<div className="px-6 py-8">
<Button
block
type="primary"
htmlType="submit"
size="large"
icon={<Link2 />}
loading={isPending}
>
Link support ticket to account
</Button>
</div>
</form>
</Form>
)
}