SupportForm.utils.tsx208 lines · main
| 1 | // End of third-party imports |
| 2 | |
| 3 | import { |
| 4 | DocsSearchResultType as PageType, |
| 5 | type DocsSearchResult as Page, |
| 6 | type DocsSearchResultSection as PageSection, |
| 7 | } from 'common' |
| 8 | import dayjs from 'dayjs' |
| 9 | import { partition } from 'lodash' |
| 10 | import { Book, Github, Hash, MessageSquare } from 'lucide-react' |
| 11 | import { |
| 12 | createLoader, |
| 13 | createParser, |
| 14 | createSerializer, |
| 15 | parseAsString, |
| 16 | type inferParserType, |
| 17 | type UseQueryStatesKeysMap, |
| 18 | } from 'nuqs' |
| 19 | |
| 20 | import { CATEGORY_OPTIONS } from './Support.constants' |
| 21 | import { getProjectDetail } from '@/data/projects/project-detail-query' |
| 22 | import { DOCS_URL } from '@/lib/constants' |
| 23 | import type { Organization } from '@/types' |
| 24 | |
| 25 | export const NO_PROJECT_MARKER = 'no-project' |
| 26 | export const NO_ORG_MARKER = 'no-org' |
| 27 | |
| 28 | export const formatMessage = ({ |
| 29 | message, |
| 30 | attachments = [], |
| 31 | error, |
| 32 | }: { |
| 33 | message: string |
| 34 | attachments?: Array<string> |
| 35 | error: string | null | undefined |
| 36 | }) => { |
| 37 | const [harFiles, images] = partition(attachments, (x) => x.split('?token')[0].endsWith('.har')) |
| 38 | const errorString = error != null ? `\n\nError: ${error}` : '' |
| 39 | |
| 40 | const imagesString = images.length > 0 ? `\n\nImage Attachments:\n${images.join('\n\n')}` : '' |
| 41 | const harFilesString = harFiles.length > 0 ? `\n\nHAR Files:\n${harFiles.join('\n\n')}` : '' |
| 42 | |
| 43 | return `${message}${errorString}${imagesString}${harFilesString}` |
| 44 | } |
| 45 | |
| 46 | export const formatStudioVersion = (commit: { commitSha: string; commitTime: string }): string => { |
| 47 | const formattedTime = |
| 48 | commit.commitTime === 'unknown' |
| 49 | ? 'unknown time' |
| 50 | : dayjs(commit.commitTime).format('YYYY-MM-DD HH:mm:ss Z') |
| 51 | return `SHA ${commit.commitSha} deployed at ${formattedTime}` |
| 52 | } |
| 53 | |
| 54 | export function getPageIcon(page: Page) { |
| 55 | switch (page.type) { |
| 56 | case PageType.Markdown: |
| 57 | case PageType.Reference: |
| 58 | case PageType.Integration: |
| 59 | return <Book strokeWidth={1.5} className="mr-0! w-4! h-4!" /> |
| 60 | case PageType.GithubDiscussion: |
| 61 | return <Github strokeWidth={1.5} className="mr-0! w-4! h-4!" /> |
| 62 | default: |
| 63 | throw new Error(`Unknown page type '${page.type}'`) |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | export function getPageSectionIcon(page: Page) { |
| 68 | switch (page.type) { |
| 69 | case PageType.Markdown: |
| 70 | case PageType.Reference: |
| 71 | case PageType.Integration: |
| 72 | return <Hash strokeWidth={1.5} className="mr-0! w-4! h-4!" /> |
| 73 | case PageType.GithubDiscussion: |
| 74 | return <MessageSquare strokeWidth={1.5} className="mr-0! w-4! h-4!" /> |
| 75 | default: |
| 76 | throw new Error(`Unknown page type '${page.type}'`) |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | export function generateLink(pageType: PageType, link: string): string { |
| 81 | switch (pageType) { |
| 82 | case PageType.Markdown: |
| 83 | case PageType.Reference: |
| 84 | return `${DOCS_URL}${link}` |
| 85 | case PageType.Integration: |
| 86 | return `https://supabase.com${link}` |
| 87 | case PageType.GithubDiscussion: |
| 88 | return link |
| 89 | default: |
| 90 | throw new Error(`Unknown page type '${pageType}'`) |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | export function formatSectionUrl(page: Page, section: PageSection): string { |
| 95 | switch (page.type) { |
| 96 | case PageType.Markdown: |
| 97 | case PageType.GithubDiscussion: |
| 98 | return `${generateLink(page.type, page.path)}#${section.slug ?? ''}` |
| 99 | case PageType.Reference: |
| 100 | return `${generateLink(page.type, page.path)}/${section.slug ?? ''}` |
| 101 | case PageType.Integration: |
| 102 | return generateLink(page.type, page.path) // Assuming no section slug for Integration pages |
| 103 | default: |
| 104 | throw new Error(`Unknown page type '${page.type}'`) |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | export function getOrgSubscriptionPlan(orgs: Organization[] | undefined, orgSlug: string | null) { |
| 109 | if (!orgs || !orgSlug) return undefined |
| 110 | |
| 111 | const selectedOrg = orgs?.find((org) => org.slug === orgSlug) |
| 112 | const subscriptionPlanId = selectedOrg?.plan.id |
| 113 | return subscriptionPlanId |
| 114 | } |
| 115 | |
| 116 | const categoryOptionsLower = CATEGORY_OPTIONS.map((option) => option.value.toLowerCase()) |
| 117 | const parseAsCategoryOption = createParser({ |
| 118 | parse(queryValue) { |
| 119 | const lowerValue = queryValue.toLowerCase() |
| 120 | const matchingIndex = categoryOptionsLower.indexOf(lowerValue) |
| 121 | return matchingIndex !== -1 ? CATEGORY_OPTIONS[matchingIndex].value : null |
| 122 | }, |
| 123 | serialize(value) { |
| 124 | return value ?? null |
| 125 | }, |
| 126 | }) |
| 127 | |
| 128 | const supportFormUrlState = { |
| 129 | projectRef: parseAsString.withDefault(''), |
| 130 | orgSlug: parseAsString.withDefault(''), |
| 131 | category: parseAsCategoryOption, |
| 132 | subject: parseAsString.withDefault(''), |
| 133 | message: parseAsString.withDefault(''), |
| 134 | error: parseAsString, |
| 135 | /** Sentry event ID */ |
| 136 | sid: parseAsString, |
| 137 | } satisfies UseQueryStatesKeysMap |
| 138 | export type SupportFormUrlKeys = inferParserType<typeof supportFormUrlState> |
| 139 | |
| 140 | export const loadSupportFormInitialParams = createLoader(supportFormUrlState) |
| 141 | |
| 142 | export function loadSupportFormInitialParamsFromObject( |
| 143 | initialParams: Partial<SupportFormUrlKeys> |
| 144 | ): SupportFormUrlKeys { |
| 145 | const normalizedParams = Object.fromEntries( |
| 146 | Object.entries(initialParams).flatMap(([key, value]) => |
| 147 | value == null ? [] : [[key, String(value)]] |
| 148 | ) |
| 149 | ) |
| 150 | |
| 151 | return loadSupportFormInitialParams(normalizedParams) |
| 152 | } |
| 153 | |
| 154 | const serializeSupportFormInitialParams = createSerializer(supportFormUrlState) |
| 155 | |
| 156 | export function createSupportFormUrl(initialParams: Partial<SupportFormUrlKeys>) { |
| 157 | const serializedParams = serializeSupportFormInitialParams(initialParams) |
| 158 | const query = serializedParams && serializedParams !== '?' ? serializedParams : '' |
| 159 | return `/support/new${query}` |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * Determines which organization to select based on combination of: |
| 164 | * - Selected project (if any) |
| 165 | * - URL param (if any) |
| 166 | * - Fallback |
| 167 | */ |
| 168 | export async function selectInitialOrgAndProject({ |
| 169 | projectRef, |
| 170 | orgSlug, |
| 171 | orgs, |
| 172 | }: { |
| 173 | projectRef: string | null |
| 174 | orgSlug: string | null |
| 175 | orgs: Organization[] |
| 176 | }): Promise<{ projectRef: string | null; orgSlug: string | null }> { |
| 177 | if (projectRef) { |
| 178 | try { |
| 179 | const projectDetails = await getProjectDetail({ ref: projectRef }) |
| 180 | if (projectDetails?.organization_id) { |
| 181 | const org = orgs.find((o) => o.id === projectDetails.organization_id) |
| 182 | if (org?.slug) { |
| 183 | return { |
| 184 | projectRef, |
| 185 | orgSlug: org.slug, |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | } catch { |
| 190 | // Can safely ignore, consider provided project ref invalid |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | if (orgSlug) { |
| 195 | const org = orgs.find((o) => o.slug === orgSlug) |
| 196 | if (org?.slug) { |
| 197 | return { |
| 198 | projectRef: null, |
| 199 | orgSlug: org.slug, |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | return { |
| 205 | projectRef: null, |
| 206 | orgSlug: orgs[0]?.slug ?? null, |
| 207 | } |
| 208 | } |