Files
supabase/apps/studio/components/interfaces/Support/SupportForm.utils.tsx
Charis f5ff10e195 refactor,tests(support form) (#39410)
* refactor: refactor support form

Refactor support form to make it easier to maintain:
- Split up large components into smaller components and hooks
- Lift state up so we don't have to do complex child/parent
state-syncing via useEffect
- Use nuqs parsing for consistent serialization/deserialization of
support form prefilled fields

* test: support form

Add comprehensive tests for support form

* fix(support form): project and org empty state

* Nit clean up

* More clean up

* cleannnn

* fix(support form): allow case-insensitive category in url

* clean(support form tests): remove unused param

* fix(support form): incorrect logic for sending affected services in payload

* clean(support form): use NO_ORG_MARKER and NO_PROJECT_MARKER instead of strings

* fix(support form): don't show upgrade cta if already on enterprise

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2025-10-14 11:04:33 +08:00

182 lines
5.3 KiB
TypeScript

import { Book, Github, Hash, MessageSquare } from 'lucide-react'
import {
createLoader,
createParser,
createSerializer,
type inferParserType,
parseAsString,
parseAsStringLiteral,
type UseQueryStatesKeysMap,
} from 'nuqs'
// End of third-party imports
import {
type DocsSearchResult as Page,
type DocsSearchResultSection as PageSection,
DocsSearchResultType as PageType,
} from 'common'
import { getProjectDetail } from 'data/projects/project-detail-query'
import { DOCS_URL } from 'lib/constants'
import type { Organization } from 'types'
import { CATEGORY_OPTIONS } from './Support.constants'
export const NO_PROJECT_MARKER = 'no-project'
export const NO_ORG_MARKER = 'no-org'
export const formatMessage = (
message: string,
attachments: string[],
error: string | null | undefined
) => {
const errorString = error != null ? `\nError: ${error}` : ''
if (attachments.length > 0) {
const attachmentsImg = attachments.map((url) => `\n${url}`)
return `${message}\n${attachmentsImg.join('')}${errorString}`
} else {
return `${message}${errorString}`
}
}
export function getPageIcon(page: Page) {
switch (page.type) {
case PageType.Markdown:
case PageType.Reference:
case PageType.Integration:
return <Book strokeWidth={1.5} className="!mr-0 !w-4 !h-4" />
case PageType.GithubDiscussion:
return <Github strokeWidth={1.5} className="!mr-0 !w-4 !h-4" />
default:
throw new Error(`Unknown page type '${page.type}'`)
}
}
export function getPageSectionIcon(page: Page) {
switch (page.type) {
case PageType.Markdown:
case PageType.Reference:
case PageType.Integration:
return <Hash strokeWidth={1.5} className="!mr-0 !w-4 !h-4" />
case PageType.GithubDiscussion:
return <MessageSquare strokeWidth={1.5} className="!mr-0 !w-4 !h-4" />
default:
throw new Error(`Unknown page type '${page.type}'`)
}
}
export function generateLink(pageType: PageType, link: string): string {
switch (pageType) {
case PageType.Markdown:
case PageType.Reference:
return `${DOCS_URL}${link}`
case PageType.Integration:
return `https://supabase.com${link}`
case PageType.GithubDiscussion:
return link
default:
throw new Error(`Unknown page type '${pageType}'`)
}
}
export function formatSectionUrl(page: Page, section: PageSection): string {
switch (page.type) {
case PageType.Markdown:
case PageType.GithubDiscussion:
return `${generateLink(page.type, page.path)}#${section.slug ?? ''}`
case PageType.Reference:
return `${generateLink(page.type, page.path)}/${section.slug ?? ''}`
case PageType.Integration:
return generateLink(page.type, page.path) // Assuming no section slug for Integration pages
default:
throw new Error(`Unknown page type '${page.type}'`)
}
}
export function getOrgSubscriptionPlan(orgs: Organization[] | undefined, orgSlug: string | null) {
if (!orgs || !orgSlug) return undefined
const selectedOrg = orgs?.find((org) => org.slug === orgSlug)
const subscriptionPlanId = selectedOrg?.plan.id
return subscriptionPlanId
}
const categoryOptionsLower = CATEGORY_OPTIONS.map((option) => option.value.toLowerCase())
const parseAsCategoryOption = createParser({
parse(queryValue) {
const lowerValue = queryValue.toLowerCase()
const matchingIndex = categoryOptionsLower.indexOf(lowerValue)
return matchingIndex !== -1 ? CATEGORY_OPTIONS[matchingIndex].value : null
},
serialize(value) {
return value ?? null
},
})
const supportFormUrlState = {
projectRef: parseAsString.withDefault(NO_PROJECT_MARKER),
orgSlug: parseAsString.withDefault(NO_ORG_MARKER),
category: parseAsCategoryOption,
subject: parseAsString.withDefault(''),
message: parseAsString.withDefault(''),
error: parseAsString,
/** Sentry event ID */
sid: parseAsString,
} satisfies UseQueryStatesKeysMap
export type SupportFormUrlKeys = inferParserType<typeof supportFormUrlState>
export const loadSupportFormInitialParams = createLoader(supportFormUrlState)
const serializeSupportFormInitialParams = createSerializer(supportFormUrlState)
export function createSupportFormUrl(initialParams: SupportFormUrlKeys) {
const serializedParams = serializeSupportFormInitialParams(initialParams)
return `/support/new${serializedParams ?? ''}`
}
/**
* Determines which organization to select based on combination of:
* - Selected project (if any)
* - URL param (if any)
* - Fallback
*/
export async function selectInitalOrgAndProject({
projectRef,
orgSlug,
orgs,
}: {
projectRef: string | null
orgSlug: string | null
orgs: Organization[]
}): Promise<{ projectRef: string | null; orgSlug: string | null }> {
if (projectRef) {
try {
const projectDetails = await getProjectDetail({ ref: projectRef })
if (projectDetails?.organization_id) {
const org = orgs.find((o) => o.id === projectDetails.organization_id)
if (org?.slug) {
return {
projectRef,
orgSlug: org.slug,
}
}
}
} catch {
// Can safely ignore, consider provided project ref invalid
}
}
if (orgSlug) {
const org = orgs.find((o) => o.slug === orgSlug)
if (org?.slug) {
return {
projectRef: null,
orgSlug: org.slug,
}
}
}
return {
projectRef: null,
orgSlug: orgs[0]?.slug ?? null,
}
}