Files
supabase/apps/studio/components/interfaces/Support/SupportFormV2.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

262 lines
9.1 KiB
TypeScript

// End of third-party imports
import { SupportCategories } from '@supabase/shared-types/out/constants'
import { useConstant, useFlag } from 'common'
import { CLIENT_LIBRARIES } from 'common/constants'
import { type Dispatch, type MouseEventHandler } from 'react'
import type { SubmitHandler, UseFormReturn } from 'react-hook-form'
import { DialogSectionSeparator, Form } from 'ui'
import {
AffectedServicesSelector,
CATEGORIES_WITHOUT_AFFECTED_SERVICES,
} from './AffectedServicesSelector'
import { AttachmentUploadDisplay, useAttachmentUpload } from './AttachmentUpload'
import { CategoryAndSeverityInfo } from './CategoryAndSeverityInfo'
import { ClientLibraryInfo } from './ClientLibraryInfo'
import {
DASHBOARD_LOG_CATEGORIES,
getSanitizedBreadcrumbs,
uploadDashboardLog,
} from './dashboard-logs'
import { DashboardLogsToggle } from './DashboardLogsToggle'
import { MessageField } from './MessageField'
import { OrganizationSelector } from './OrganizationSelector'
import { ProjectAndPlanInfo } from './ProjectAndPlanInfo'
import { SubjectAndSuggestionsInfo } from './SubjectAndSuggestionsInfo'
import { SubmitButton } from './SubmitButton'
import { DISABLE_SUPPORT_ACCESS_CATEGORIES, SupportAccessToggle } from './SupportAccessToggle'
import type { SupportFormValues } from './SupportForm.schema'
import type { SupportFormActions, SupportFormState } from './SupportForm.state'
import {
formatMessage,
formatStudioVersion,
getOrgSubscriptionPlan,
NO_ORG_MARKER,
NO_PROJECT_MARKER,
} from './SupportForm.utils'
import { getProjectAuthConfig } from '@/data/auth/auth-config-query'
import { useSendSupportTicketMutation } from '@/data/feedback/support-ticket-send'
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'
const useIsSimplifiedForm = (slug: string, subscriptionPlanId?: OrganizationPlanID) => {
const simplifiedSupportForm = useFlag('simplifiedSupportForm')
if (subscriptionPlanId === 'platform') {
return true
}
if (typeof simplifiedSupportForm === 'string') {
const slugs = (simplifiedSupportForm as string).split(',').map((x) => x.trim())
return slugs.includes(slug)
}
return false
}
interface SupportFormV2Props {
form: UseFormReturn<SupportFormValues>
initialError: string | null
state: SupportFormState
dispatch: Dispatch<SupportFormActions>
}
export const SupportFormV2 = ({ form, initialError, state, dispatch }: SupportFormV2Props) => {
const { profile } = useProfile()
const respondToEmail = profile?.primary_email ?? 'your email'
const { organizationSlug, projectRef, category, severity, subject, library } = form.watch()
const selectedOrgSlug = organizationSlug === NO_ORG_MARKER ? null : organizationSlug
const selectedProjectRef = projectRef === NO_PROJECT_MARKER ? null : projectRef
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()
const sanitizedLogSnapshot = useConstant(getSanitizedBreadcrumbs)
const { data: commit } = useDeploymentCommitQuery({
staleTime: 1000 * 60 * 10, // 10 minutes
})
const { mutate: submitSupportTicket } = useSendSupportTicketMutation({
onSuccess: (_, variables) => {
dispatch({
type: 'SUCCESS',
sentProjectRef: variables.projectRef,
sentOrgSlug: variables.organizationSlug,
sentCategory: variables.category,
})
},
onError: (error) => {
dispatch({
type: 'ERROR',
message: error.message,
})
},
})
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
const attachDashboardLogs =
formAttachDashboardLogs && DASHBOARD_LOG_CATEGORIES.includes(values.category)
const [attachments, dashboardLogUrl] = await Promise.all([
attachmentUpload.createAttachments(),
attachDashboardLogs
? uploadDashboardLog({
userId: profile?.gotrue_id,
sanitizedLogs: sanitizedLogSnapshot,
uploadDashboardLogFn,
})
: undefined,
])
const selectedLibrary = values.library
? CLIENT_LIBRARIES.find((library) => library.language === values.library)
: undefined
const payload = {
...values,
organizationSlug: values.organizationSlug ?? NO_ORG_MARKER,
projectRef: values.projectRef ?? NO_PROJECT_MARKER,
allowSupportAccess:
values.category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(values.category)
? values.allowSupportAccess
: false,
library:
values.category === SupportCategories.PROBLEM && selectedLibrary !== undefined
? selectedLibrary.key
: '',
message: formatMessage({
message: values.message,
attachments,
error: initialError,
}),
verified: true,
tags: ['dashboard-support-form'],
siteUrl: '',
additionalRedirectUrls: '',
affectedServices: CATEGORIES_WITHOUT_AFFECTED_SERVICES.includes(values.category)
? ''
: values.affectedServices
.split(',')
.map((x) => x.trim().replace(/ /g, '_').toLowerCase())
.join(';'),
browserInformation: detectBrowser(),
dashboardLogs: dashboardLogUrl?.[0],
dashboardStudioVersion: commit ? formatStudioVersion(commit) : undefined,
}
if (values.projectRef !== NO_PROJECT_MARKER) {
try {
const authConfig = await getProjectAuthConfig({
projectRef: values.projectRef,
})
payload.siteUrl = authConfig.SITE_URL
payload.additionalRedirectUrls = authConfig.URI_ALLOW_LIST
} catch {
// [Joshen] No error handler required as fetching these info are nice to haves, not necessary
}
}
submitSupportTicket(payload)
}
const handleFormSubmit = form.handleSubmit(onSubmit)
const handleSubmitButtonClick: MouseEventHandler<HTMLButtonElement> = (event) => {
handleFormSubmit(event)
}
return (
<Form {...form}>
<form id="support-form" className="flex flex-col gap-y-6">
<h3 className="px-6 text-xl">How can we help?</h3>
<div className="px-6 flex flex-col gap-y-8">
<OrganizationSelector form={form} orgSlug={organizationSlug} />
<ProjectAndPlanInfo
form={form}
orgSlug={selectedOrgSlug}
projectRef={selectedProjectRef}
subscriptionPlanId={subscriptionPlanId}
category={category}
/>
<CategoryAndSeverityInfo
form={form}
category={category}
severity={severity}
projectRef={projectRef}
/>
</div>
<DialogSectionSeparator />
<div className="px-6 flex flex-col gap-y-8">
<SubjectAndSuggestionsInfo form={form} subject={subject} category={category} />
{!simplifiedSupportForm && (
<>
<ClientLibraryInfo form={form} library={library} category={category} />
<AffectedServicesSelector form={form} category={category} />
</>
)}
<MessageField form={form} originalError={initialError} />
<AttachmentUploadDisplay {...attachmentUpload} />
</div>
<DialogSectionSeparator />
{DASHBOARD_LOG_CATEGORIES.includes(category) && (
<>
<DashboardLogsToggle form={form} sanitizedLog={sanitizedLogSnapshot} className="px-6" />
<DialogSectionSeparator />
</>
)}
{!!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && (
<>
<SupportAccessToggle form={form} className="px-6" />
<DialogSectionSeparator />
</>
)}
<div className="px-6 pt-2">
<SubmitButton
isSubmitting={state.type === 'submitting'}
userEmail={respondToEmail}
onClick={handleSubmitButtonClick}
/>
</div>
</form>
</Form>
)
}