// End of third-party imports import { SupportCategories } from '@supabase/shared-types/out/constants' import { DocsSearchResultType as PageType, type DocsSearchResult as Page, type DocsSearchResultSection as PageSection, } from 'common' import dayjs from 'dayjs' import { partition } from 'lodash' import { Book, Github, Hash, MessageSquare } from 'lucide-react' import { createLoader, createParser, createSerializer, parseAsString, type inferParserType, type UseQueryStatesKeysMap, } from 'nuqs' import { CATEGORY_OPTIONS, type ExtendedSupportCategories } from './Support.constants' import { getProjectDetail } from '@/data/projects/project-detail-query' import { DOCS_URL } from '@/lib/constants' import type { Organization } from '@/types' export const NO_PROJECT_MARKER = 'no-project' export const NO_ORG_MARKER = 'no-org' export const DISABLE_SUPPORT_ACCESS_CATEGORIES: ExtendedSupportCategories[] = [ SupportCategories.ACCOUNT_DELETION, SupportCategories.SALES_ENQUIRY, SupportCategories.REFUND, ] export function canAllowSupportAccess( category: ExtendedSupportCategories | undefined, projectRef: string ): boolean { return ( !!category && !DISABLE_SUPPORT_ACCESS_CATEGORIES.includes(category) && projectRef !== NO_PROJECT_MARKER ) } export const formatMessage = ({ message, attachments = [], error, }: { message: string attachments?: Array error: string | null | undefined }) => { const [harFiles, images] = partition(attachments, (x) => x.split('?token')[0].endsWith('.har')) const errorString = error != null ? `\n\nError: ${error}` : '' const imagesString = images.length > 0 ? `\n\nImage Attachments:\n${images.join('\n\n')}` : '' const harFilesString = harFiles.length > 0 ? `\n\nHAR Files:\n${harFiles.join('\n\n')}` : '' return `${message}${errorString}${imagesString}${harFilesString}` } export const formatStudioVersion = (commit: { commitSha: string; commitTime: string }): string => { const formattedTime = commit.commitTime === 'unknown' ? 'unknown time' : dayjs(commit.commitTime).format('YYYY-MM-DD HH:mm:ss Z') return `SHA ${commit.commitSha} deployed at ${formattedTime}` } export function getPageIcon(page: Page) { switch (page.type) { case PageType.Markdown: case PageType.Reference: case PageType.Integration: return case PageType.GithubDiscussion: return 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 case PageType.GithubDiscussion: return 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(''), orgSlug: parseAsString.withDefault(''), category: parseAsCategoryOption, subject: parseAsString.withDefault(''), message: parseAsString.withDefault(''), error: parseAsString, /** Sentry event ID */ sid: parseAsString, } satisfies UseQueryStatesKeysMap export type SupportFormUrlKeys = inferParserType export const loadSupportFormInitialParams = createLoader(supportFormUrlState) export function loadSupportFormInitialParamsFromObject( initialParams: Partial ): SupportFormUrlKeys { const normalizedParams = Object.fromEntries( Object.entries(initialParams).flatMap(([key, value]) => value == null ? [] : [[key, String(value)]] ) ) return loadSupportFormInitialParams(normalizedParams) } const serializeSupportFormInitialParams = createSerializer(supportFormUrlState) export function createSupportFormUrl(initialParams: Partial) { const serializedParams = serializeSupportFormInitialParams(initialParams) const query = serializedParams && serializedParams !== '?' ? serializedParams : '' return `/support/new${query}` } /** * Determines which organization to select based on combination of: * - Selected project (if any) * - URL param (if any) * - Fallback */ export async function selectInitialOrgAndProject({ 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 }) const projectOrg = orgs.find((o) => o.id === projectDetails?.organization_id) if (projectOrg?.slug) { return { projectRef, orgSlug: projectOrg.slug, } } } catch { // Can safely ignore, fall through to the orgSlug fallback below } } // Reached when there's no projectRef, or the project lookup above failed to // resolve a known org (e.g. the Management API request errored). Returning // `projectRef` here (rather than hardcoding `null`) preserves it when we // still have one, instead of silently discarding it. if (orgSlug) { const org = orgs.find((o) => o.slug === orgSlug) if (org?.slug) { return { projectRef, orgSlug: org.slug, } } } return { projectRef: null, orgSlug: orgs[0]?.slug ?? null, } } // Chrome blocks scrollIntoView (triggered by .focus()) when overflow-x: hidden and // overflow-y: auto are on the same element. Manually scroll the sidebar instead. export function scrollToRequiredField(formId: string, fieldName: string) { const form = document.getElementById(formId) if (!form) return // Selects use data-support-field since they have no name attr; inputs/textareas use name. const el = form.querySelector(`[data-support-field="${fieldName}"]`) ?? form.querySelector(`[name="${fieldName}"]`) if (!(el instanceof HTMLElement)) return const scrollContainer = findScrollableParent(el) if (scrollContainer) { const offset = el.getBoundingClientRect().top - scrollContainer.getBoundingClientRect().top - 80 scrollContainer.scrollTo({ top: scrollContainer.scrollTop + offset, behavior: 'smooth' }) } el.focus({ preventScroll: true }) } function findScrollableParent(el: HTMLElement): HTMLElement | null { let parent = el.parentElement while (parent && parent !== document.body) { const { overflowY } = window.getComputedStyle(parent) if (overflowY === 'auto' || overflowY === 'scroll') return parent parent = parent.parentElement } return null }