mirror of
https://github.com/supabase/supabase.git
synced 2026-05-09 10:19:50 -04:00
4452e0ac2e
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>
213 lines
7.4 KiB
TypeScript
213 lines
7.4 KiB
TypeScript
// End of third-party imports
|
|
|
|
import { useParams } from 'common'
|
|
import { AnimatePresence, motion } from 'framer-motion'
|
|
import { Check, ChevronsUpDown, ExternalLink } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import type { UseFormReturn } from 'react-hook-form'
|
|
import { toast } from 'sonner'
|
|
import { Button, cn, CommandGroup_Shadcn_, CommandItem_Shadcn_, FormControl, FormField } from 'ui'
|
|
import { Admonition } from 'ui-patterns'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import type { ExtendedSupportCategories } from './Support.constants'
|
|
import type { SupportFormValues } from './SupportForm.schema'
|
|
import { NO_ORG_MARKER, NO_PROJECT_MARKER } from './SupportForm.utils'
|
|
import CopyButton from '@/components/ui/CopyButton'
|
|
import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
|
|
interface ProjectAndPlanProps {
|
|
form: UseFormReturn<SupportFormValues>
|
|
orgSlug: string | null
|
|
projectRef: string | null
|
|
category: ExtendedSupportCategories
|
|
subscriptionPlanId: string | undefined
|
|
}
|
|
|
|
export function ProjectAndPlanInfo({
|
|
form,
|
|
orgSlug,
|
|
projectRef,
|
|
category: _category,
|
|
subscriptionPlanId: _subscriptionPlanId,
|
|
}: ProjectAndPlanProps) {
|
|
const hasProjectSelected = projectRef && projectRef !== NO_PROJECT_MARKER
|
|
|
|
return (
|
|
<div className="flex flex-col gap-y-2">
|
|
<ProjectSelector form={form} orgSlug={orgSlug} projectRef={projectRef} />
|
|
<ProjectRefHighlighted projectRef={projectRef} />
|
|
|
|
{!hasProjectSelected && <Admonition type="default" title="No project has been selected" />}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface ProjectSelectorProps {
|
|
form: UseFormReturn<SupportFormValues>
|
|
orgSlug: string | null
|
|
projectRef: string | null
|
|
}
|
|
|
|
function ProjectSelector({ form, orgSlug, projectRef }: ProjectSelectorProps) {
|
|
const { projectRef: urlProjectRef } = useParams()
|
|
|
|
return (
|
|
<FormField
|
|
name="projectRef"
|
|
control={form.control}
|
|
render={({ field }) => (
|
|
<FormItemLayout hideMessage layout="vertical" label="Which project is affected?">
|
|
<FormControl>
|
|
<OrganizationProjectSelector
|
|
key={orgSlug}
|
|
sameWidthAsTrigger
|
|
fetchOnMount
|
|
checkPosition="left"
|
|
slug={!orgSlug || orgSlug === NO_ORG_MARKER ? undefined : orgSlug}
|
|
selectedRef={field.value}
|
|
onInitialLoad={(projects) => {
|
|
if (!urlProjectRef && (!projectRef || projectRef === NO_PROJECT_MARKER))
|
|
field.onChange(projects[0]?.ref ?? NO_PROJECT_MARKER)
|
|
}}
|
|
onSelect={(project) => field.onChange(project.ref)}
|
|
renderTrigger={({ isLoading, project, listboxId, open }) => {
|
|
return (
|
|
<Button
|
|
block
|
|
type="default"
|
|
role="combobox"
|
|
aria-label="Select a project"
|
|
aria-expanded={open}
|
|
aria-controls={listboxId}
|
|
size="small"
|
|
className="justify-between"
|
|
iconRight={<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />}
|
|
>
|
|
{!!orgSlug && isLoading ? (
|
|
<ShimmeringLoader className="w-44 py-2" />
|
|
) : !field.value || field.value === NO_PROJECT_MARKER ? (
|
|
'No specific project'
|
|
) : (
|
|
(project?.name ?? 'Unknown project')
|
|
)}
|
|
</Button>
|
|
)
|
|
}}
|
|
renderActions={(setOpen) => (
|
|
<CommandGroup_Shadcn_>
|
|
<CommandItem_Shadcn_
|
|
className="w-full gap-x-2"
|
|
onSelect={() => {
|
|
field.onChange(NO_PROJECT_MARKER)
|
|
setOpen(false)
|
|
}}
|
|
>
|
|
{field.value === NO_PROJECT_MARKER && <Check size={16} />}
|
|
<p className={cn(field.value !== NO_PROJECT_MARKER && 'ml-6')}>
|
|
No specific project
|
|
</p>
|
|
</CommandItem_Shadcn_>
|
|
</CommandGroup_Shadcn_>
|
|
)}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
)
|
|
}
|
|
|
|
interface ProjectRefHighlightedProps {
|
|
projectRef: string | null
|
|
}
|
|
|
|
function ProjectRefHighlighted({ projectRef }: ProjectRefHighlightedProps) {
|
|
const isVisible = !!projectRef && projectRef !== NO_PROJECT_MARKER
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isVisible && (
|
|
<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="text-sm transition text-foreground-lighter">
|
|
Project ID:{' '}
|
|
<code className="text-code-inline text-foreground-light!">{projectRef}</code>
|
|
</p>
|
|
<CopyButton
|
|
iconOnly
|
|
type="text"
|
|
text={projectRef}
|
|
onClick={() => toast.success('Copied project ID to clipboard')}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
)
|
|
}
|
|
|
|
interface PlanExpectationInfoContentProps {
|
|
orgSlug: string
|
|
planId?: string
|
|
}
|
|
|
|
export const PlanExpectationInfoContent = ({
|
|
orgSlug,
|
|
planId,
|
|
}: PlanExpectationInfoContentProps) => {
|
|
const { billingAll } = useIsFeatureEnabled(['billing:all'])
|
|
const shouldShowUpgradeActions = billingAll && planId !== 'enterprise'
|
|
|
|
return (
|
|
<div className="flex flex-col gap-y-3 text-sm text-foreground-light">
|
|
{planId === 'free' && (
|
|
<p>
|
|
Support on the Free plan is provided through the community and by the team on a
|
|
best-effort basis. For a guaranteed response time, we recommend upgrading to the Pro plan.
|
|
Enhanced support SLAs are available on the Enterprise plan.
|
|
</p>
|
|
)}
|
|
|
|
{planId === 'pro' && (
|
|
<p>
|
|
Pro includes email support with typical 1-business-day responses; upgrade to Team for
|
|
prioritized ticketing and engineering escalation, or Enterprise for enhanced SLAs.
|
|
</p>
|
|
)}
|
|
|
|
{planId === 'team' && (
|
|
<p>
|
|
The Team plan includes email support with prioritized ticketing and escalation to product
|
|
engineering. Low, normal, and high-severity tickets are typically handled within 1
|
|
business day. Urgent issues are handled within 1 day, 365 days a year. Enhanced support
|
|
SLAs are available on the Enterprise plan.
|
|
</p>
|
|
)}
|
|
|
|
{shouldShowUpgradeActions && (
|
|
<div className="flex flex-wrap gap-2 pt-1">
|
|
<Button asChild size="tiny">
|
|
<Link
|
|
href={`/org/${orgSlug}/billing?panel=subscriptionPlan&source=planSupportExpectationInfoBox`}
|
|
>
|
|
Upgrade plan
|
|
</Link>
|
|
</Button>
|
|
<Button asChild type="default" size="tiny" icon={<ExternalLink />}>
|
|
<Link href="https://supabase.com/contact/enterprise" target="_blank" rel="noreferrer">
|
|
Enquire about Enterprise
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|