Files
supabase/apps/studio/components/interfaces/Support/SupportFormPage.tsx
Alaister Young ca2b50a0a7 chore(ui-patterns): collapse the admonition shim into ui-patterns/Admonition (#48377)
Follow-up to #48344: collapses the two resolution paths for the
Admonition module into one.

`src/admonition.tsx` was a back-compat shim re-exporting
`src/Admonition/`. Two ways to resolve one module is exactly what
produced the macOS self-import bug fixed in #48344, and the local
typecheck errors that #48374 worked around. This removes the shim and
standardizes on the PascalCase subpath, matching every other export in
the package.

**Changed:**

- Codemodded all 246 `ui-patterns/admonition` imports to
`ui-patterns/Admonition` (240 `.tsx`, 5 `.mdx`, 1 `.ts` across studio,
docs, www, design-system, and lite-studio)
- Pointed the 5 internal `'../admonition'` imports back at the
`'../Admonition'` directory

**Removed:**

- `packages/ui-patterns/src/admonition.tsx`, and its `./admonition`
entry in the exports map (regenerated with `pnpm gen:exports`)

## To test

- `grep -r "ui-patterns/admonition" --include='*.ts*'` → no hits
- `pnpm test:case-hazards` → passes
- `pnpm typecheck` → all 15 tasks green
- `pnpm --filter studio run lint:ratchet` → passes
- `pnpm --filter ui-patterns vitest run src/Admonition` → 11 tests pass

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

## Summary by CodeRabbit

* **Bug Fixes**
* Standardized Admonition component imports across the application and
documentation.
* Improved compatibility with case-sensitive environments by using the
canonical component path.
  * Removed the legacy Admonition import entry point.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
2026-07-29 00:48:56 +08:00

278 lines
9.1 KiB
TypeScript

import * as Sentry from '@sentry/nextjs'
import { Loader2, Wrench } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useReducer, type Dispatch, type PropsWithChildren } from 'react'
import type { UseFormReturn } from 'react-hook-form'
import SVG from 'react-inlinesvg'
import { toast } from 'sonner'
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { AIAssistantOption } from './AIAssistantOption'
import { DiscordCTACard } from './DiscordCTACard'
import { IncidentAdmonition } from './IncidentAdmonition'
import { Success } from './Success'
import type { ExtendedSupportCategories } from './Support.constants'
import type { SupportFormValues } from './SupportForm.schema'
import {
createInitialSupportFormState,
supportFormReducer,
type SupportFormActions,
type SupportFormState,
} from './SupportForm.state'
import { NO_PROJECT_MARKER } from './SupportForm.utils'
import { SupportFormV2 } from './SupportFormV2'
import { useSupportForm } from './useSupportForm'
import CopyButton from '@/components/ui/CopyButton'
import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
import { useStateTransition } from '@/hooks/misc/useStateTransition'
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
export { SupportForm, SupportFormStatusButton } from './SupportSidebarForm'
function useSupportFormTelemetry() {
const track = useTrack()
return useCallback(
({
projectRef,
orgSlug,
category,
}: {
projectRef: string | undefined
orgSlug: string | undefined
category: ExtendedSupportCategories
}) =>
track(
'support_ticket_submitted',
{ ticketCategory: category },
{ project: projectRef, organization: orgSlug }
),
[track]
)
}
export function SupportFormPage() {
return <SupportFormPageContent />
}
function SupportFormPageContent() {
const [state, dispatch] = useReducer(supportFormReducer, undefined, createInitialSupportFormState)
const { form, initialError, projectRef, orgSlug } = useSupportForm(dispatch)
const {
data: allStatusPageEvents,
isPending: isIncidentsPending,
isError: isIncidentsError,
} = useIncidentStatusQuery()
const { incidents = [] } = allStatusPageEvents ?? {}
const hasActiveIncidents =
!isIncidentsPending && !isIncidentsError && incidents && incidents.length > 0
const sendTelemetry = useSupportFormTelemetry()
useStateTransition(state, 'submitting', 'success', (_, curr) => {
toast.success('Support request sent. Thank you!')
sendTelemetry({
projectRef: curr.sentProjectRef,
orgSlug: curr.sentOrgSlug,
category: curr.sentCategory,
})
})
useStateTransition(state, 'submitting', 'error', (_, curr) => {
toast.error(`Failed to submit support ticket: ${curr.message}`)
if (curr.code !== 429) {
Sentry.captureMessage(`Failed to submit Support Form: ${curr.message}`)
}
dispatch({ type: 'RETURN_TO_EDITING' })
})
const isSuccess = state.type === 'success'
return (
<SupportFormWrapper>
<SupportFormHeader />
<IncidentAdmonition isActive={hasActiveIncidents} />
{!isSuccess && !hasActiveIncidents && (
<div className="flex flex-col gap-y-4">
<AIAssistantOption projectRef={projectRef} organizationSlug={orgSlug} />
<DiscordCTACard organizationSlug={orgSlug} />
</div>
)}
<SupportFormBody
form={form}
state={state}
dispatch={dispatch}
initialError={initialError}
selectedProjectRef={projectRef}
/>
{!isSuccess && <SupportFormDirectEmailInfo projectRef={projectRef} />}
</SupportFormWrapper>
)
}
function SupportFormWrapper({ children }: PropsWithChildren) {
return (
<div className="relative overflow-y-auto overflow-x-hidden">
<div className="mx-auto my-16 max-w-2xl w-full px-4 lg:px-6">
<div className="flex flex-col gap-y-8">{children}</div>
</div>
</div>
)
}
function SupportFormHeader() {
const { data: allStatusPageEvents, isPending: isLoading, isError } = useIncidentStatusQuery()
const { incidents = [], maintenanceEvents = [] } = allStatusPageEvents ?? {}
const isMaintenance = maintenanceEvents.length > 0
const isIncident = incidents.length > 0
return (
<div className="flex flex-col items-start justify-between gap-y-2 sm:flex-row sm:items-center">
<div className="flex items-center space-x-3">
<SVG src={`${BASE_PATH}/img/supabase-logo.svg`} className="h-4 w-4" />
<h3 className="m-0 text-lg">Supabase support</h3>
</div>
<div className="flex items-center gap-x-3">
<Button asChild variant="default" icon={<Wrench />}>
<Link
href={`${DOCS_URL}/guides/troubleshooting?products=platform`}
target="_blank"
rel="noreferrer"
>
Troubleshooting
</Link>
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
asChild
variant="default"
icon={
isLoading ? (
<Loader2 className="animate-spin" />
) : (
<div
className={cn('h-2 w-2 rounded-full', isIncident ? 'bg-warning' : 'bg-brand')}
/>
)
}
>
<Link href="https://status.supabase.com/" target="_blank" rel="noreferrer">
{isLoading
? 'Checking status'
: isError
? 'Failed to check status'
: isIncident
? 'Active incident ongoing'
: isMaintenance
? 'Scheduled maintenance'
: 'All systems operational'}
</Link>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" align="center">
Check the Supabase status page
</TooltipContent>
</Tooltip>
</div>
</div>
)
}
interface SupportFormDirectEmailInfoProps {
projectRef: string | null
}
function SupportFormDirectEmailInfo({ projectRef }: SupportFormDirectEmailInfoProps) {
const hasProjectRef = projectRef && projectRef !== NO_PROJECT_MARKER
return (
<Admonition
type="default"
title="Having trouble submitting the form?"
description={
<>
<p className="mb-2.5!">
Please email us directly. Include your project ID and as much information as possible.
</p>
<p className="flex items-center gap-x-1.5 flex-wrap">
Email:{' '}
<span className="inline-flex items-center gap-x-1">
<a
href={`mailto:support@supabase.com?subject=${encodeURIComponent('Support Request')}${hasProjectRef ? `${encodeURIComponent(' for Project ID: ')}${encodeURIComponent(projectRef)}` : ''}&body=${encodeURIComponent('Here is a detailed description of the problem I am experiencing and any other information that might be helpful...')}`}
className="hover:text-foreground transition-colors duration-100"
>
<code className="text-code-inline text-foreground-light! underline decoration-foreground-lighter/50 hover:decoration-foreground-lighter/80 transition-colors duration-100">
support@supabase.com
</code>
</a>
<CopyButton
variant="text"
text="support@supabase.com"
iconOnly
onClick={() => toast.success('Copied email address to clipboard')}
/>
</span>
</p>
{hasProjectRef && (
<p className="flex items-center gap-x-1.5 flex-wrap">
Project ID:{' '}
<span className="inline-flex items-center gap-x-1">
<code className="text-code-inline text-foreground-light!">{projectRef}</code>
<CopyButton
iconOnly
variant="text"
text={projectRef}
onClick={() => toast.success('Copied project ID to clipboard')}
/>
</span>
</p>
)}
</>
}
/>
)
}
interface SupportFromBodyProps {
form: UseFormReturn<SupportFormValues>
state: SupportFormState
dispatch: Dispatch<SupportFormActions>
initialError: string | null
selectedProjectRef: string | null
}
function SupportFormBody({
form,
state,
dispatch,
initialError,
selectedProjectRef,
}: SupportFromBodyProps) {
const isSuccess = state.type === 'success'
return (
<div
className={cn(
'min-w-full w-full space-y-12 rounded-sm border bg-panel-body-light shadow-md py-8',
'border-default'
)}
>
{isSuccess ? (
<Success
selectedProject={selectedProjectRef ?? undefined}
sentCategory={state.sentCategory}
/>
) : (
<SupportFormV2 form={form} initialError={initialError} state={state} dispatch={dispatch} />
)}
</div>
)
}