SupportForm.utils.tsx208 lines · main
1// End of third-party imports
2
3import {
4 DocsSearchResultType as PageType,
5 type DocsSearchResult as Page,
6 type DocsSearchResultSection as PageSection,
7} from 'common'
8import dayjs from 'dayjs'
9import { partition } from 'lodash'
10import { Book, Github, Hash, MessageSquare } from 'lucide-react'
11import {
12 createLoader,
13 createParser,
14 createSerializer,
15 parseAsString,
16 type inferParserType,
17 type UseQueryStatesKeysMap,
18} from 'nuqs'
19
20import { CATEGORY_OPTIONS } from './Support.constants'
21import { getProjectDetail } from '@/data/projects/project-detail-query'
22import { DOCS_URL } from '@/lib/constants'
23import type { Organization } from '@/types'
24
25export const NO_PROJECT_MARKER = 'no-project'
26export const NO_ORG_MARKER = 'no-org'
27
28export 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
46export 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
54export 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
67export 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
80export 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
94export 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
108export 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
116const categoryOptionsLower = CATEGORY_OPTIONS.map((option) => option.value.toLowerCase())
117const 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
128const 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
138export type SupportFormUrlKeys = inferParserType<typeof supportFormUrlState>
139
140export const loadSupportFormInitialParams = createLoader(supportFormUrlState)
141
142export 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
154const serializeSupportFormInitialParams = createSerializer(supportFormUrlState)
155
156export 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 */
168export 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}