Support request successfully sent!
We will reach out to you at {respondToEmail} .
@@ -64,14 +65,14 @@ const Success = ({
setSearchValue(e.target.value)}
- icon={ }
+ icon={ }
actions={[
}
+ icon={ }
>
void
- setSelectedProject: (value: string) => void
-}
-
-const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) => {
- const supabaseClient = useSupabaseClient()
- const {
- handleDocsSearchDebounced,
- searchState,
- searchState: state,
- } = useDocsSearch(supabaseClient)
- const [subject, setSubject] = useState('')
- const [docsResults, setDocsResults] = useState([])
-
- useEffect(() => {
- if (subject.trim().length > 0) {
- handleDocsSearchDebounced(subject.trim())
- } else {
- setDocsResults([])
- }
- }, [subject])
-
- useEffect(() => {
- if (subject.trim().length > 0 && searchState.status === 'fullResults') {
- setDocsResults(searchState.results)
- } else if (searchState.status === 'noResults' || searchState.status === 'error') {
- setDocsResults([])
- }
- }, [searchState])
-
- const { isReady } = useRouter()
- const { ref, slug, category, message } = useParams()
-
- const uploadButtonRef = useRef()
- const enableFreeSupport = useFlag('enableFreeSupport')
- const [isSubmitting, setIsSubmitting] = useState(false)
- const [uploadedFiles, setUploadedFiles] = useState([])
- const [uploadedDataUrls, setUploadedDataUrls] = useState([])
- const [selectedServices, setSelectedServices] = useState([])
- const [textAreaValue, setTextAreaValue] = useState('')
-
- const {
- data: organizations,
- isLoading: isLoadingOrganizations,
- isError: isErrorOrganizations,
- isSuccess: isSuccessOrganizations,
- } = useOrganizationsQuery()
- // for use in useEffect
- const organizationsRef = useLatest(organizations)
-
- const {
- data: allProjects,
- isLoading: isLoadingProjects,
- isError: isErrorProjects,
- isSuccess: isSuccessProjects,
- } = useProjectsQuery()
-
- const { mutate: submitSupportTicket } = useSendSupportTicketMutation({
- onSuccess: (res, variables) => {
- toast.success('Support request sent. Thank you!')
- setSentCategory(variables.category)
- setSelectedProject(variables.projectRef ?? 'no-project')
- },
- onError: (error) => {
- toast.error(`Failed to submit support ticket: ${error.message}`)
- Sentry.captureMessage('Failed to submit Support Form: ' + error.message)
- setIsSubmitting(false)
- },
- })
-
- const projectDefaults: Partial[] = [{ ref: 'no-project', name: 'No specific project' }]
-
- const projects = [...(allProjects ?? []), ...projectDefaults]
- const selectedProjectFromUrl = projects.find((project) => project.ref === ref)
- const selectedOrganizationFromUrl = organizations?.find((org) => org.slug === slug)
- const selectedCategoryFromUrl = CATEGORY_OPTIONS.find((option) => {
- if (option.value.toLowerCase() === ((category as string) ?? '').toLowerCase()) return option
- })
-
- const [selectedProjectRef, setSelectedProjectRef] = useState(
- selectedProjectFromUrl !== undefined
- ? selectedProjectFromUrl.ref
- : projects.length > 0
- ? projects[0].ref
- : 'no-project'
- )
-
- const selectedOrganizationSlug =
- selectedOrganizationFromUrl !== undefined
- ? selectedOrganizationFromUrl.slug
- : selectedProjectRef !== 'no-project'
- ? organizations?.find((org) => {
- const project = projects.find((project) => project.ref === selectedProjectRef)
- return org.id === project?.organization_id
- })?.slug
- : organizations?.[0]?.slug
-
- const { data: subscription, isLoading: isLoadingSubscription } = useOrgSubscriptionQuery({
- orgSlug: selectedOrganizationSlug,
- })
-
- const { profile } = useProfile()
- const respondToEmail = profile?.primary_email ?? 'your email'
-
- const initialValues = {
- category:
- selectedCategoryFromUrl !== undefined
- ? selectedCategoryFromUrl.value
- : CATEGORY_OPTIONS[0].value,
- severity: 'Low',
- projectRef: selectedProjectRef,
- organizationSlug: selectedOrganizationSlug,
- library: 'no-library',
- subject: subject ?? '',
- message: message || '',
- allowSupportAccess: false,
- }
-
- const router = useRouter()
-
- const onFilesUpload = async (event: ChangeEvent) => {
- event.persist()
- const items = event.target.files || (event as any).dataTransfer.items
- const itemsCopied = Array.prototype.map.call(items, (item) => item) as File[]
- const itemsToBeUploaded = itemsCopied.slice(0, MAX_ATTACHMENTS - uploadedFiles.length)
-
- setUploadedFiles(uploadedFiles.concat(itemsToBeUploaded))
- if (items.length + uploadedFiles.length > MAX_ATTACHMENTS) {
- toast(`Only up to ${MAX_ATTACHMENTS} attachments are allowed`)
- }
-
- event.target.value = ''
- }
-
- const removeUploadedFile = (idx: number) => {
- const updatedFiles = uploadedFiles?.slice()
- updatedFiles.splice(idx, 1)
- setUploadedFiles(updatedFiles)
-
- const updatedDataUrls = uploadedDataUrls.slice()
- uploadedDataUrls.splice(idx, 1)
- setUploadedDataUrls(updatedDataUrls)
- }
-
- const onValidate = (values: any) => {
- const errors: any = {}
- if (!values.subject) errors.subject = 'Please add a subject heading'
- if (!values.message) errors.message = "Please add a message about the issue that you're facing"
- if (values.category === 'Problem' && values.library === 'no-library')
- errors.library = "Please select the library that you're facing issues with"
- return errors
- }
-
- const onSubmit = async (values: any) => {
- setIsSubmitting(true)
- const attachments =
- uploadedFiles.length > 0 ? await uploadAttachments(values.projectRef, uploadedFiles) : []
- const selectedLibrary = CLIENT_LIBRARIES.find((library) => library.language === values.library)
-
- const payload = {
- ...values,
- library:
- values.category === 'Problem' && selectedLibrary !== undefined ? selectedLibrary.key : '',
- message: formatMessage(values.message, attachments),
- verified: true,
- tags: ['dashboard-support-form'],
- siteUrl: '',
- additionalRedirectUrls: '',
- affectedServices: selectedServices
- .map((service) => service.replace(/ /g, '_').toLowerCase())
- .join(';'),
- browserInformation: detectBrowser(),
- }
-
- if (values.projectRef !== 'no-project') {
- try {
- const authConfig = await getProjectAuthConfig({ projectRef: values.projectRef })
- payload.siteUrl = authConfig.SITE_URL
- payload.additionalRedirectUrls = authConfig.URI_ALLOW_LIST
- } catch (error) {
- // [Joshen] No error handler required as fetching these info are nice to haves, not necessary
- }
- }
-
- submitSupportTicket(payload)
- }
-
- const handleTextMessageChange = (event: React.ChangeEvent) => {
- setTextAreaValue(event.target.value)
- }
-
- const ipv4MigrationStrings = [
- 'ipv4',
- 'ipv6',
- 'supavisor',
- 'pgbouncer',
- '5432',
- 'ENETUNREACH',
- 'ECONNREFUSED',
- 'P1001',
- 'connect: no route to',
- 'network is unreac',
- 'could not translate host name',
- 'address family not supported by protocol',
- ]
-
- const ipv4MigrationStringMatched = ipv4MigrationStrings.some((str) => textAreaValue.includes(str))
- const hasResults =
- state.status === 'fullResults' ||
- state.status === 'partialResults' ||
- (state.status === 'loading' && state.staleResults.length > 0)
-
- useEffect(() => {
- if (!uploadedFiles) return
- const objectUrls = uploadedFiles.map((file) => URL.createObjectURL(file))
- setUploadedDataUrls(objectUrls)
-
- return () => {
- objectUrls.forEach((url: any) => URL.revokeObjectURL(url))
- }
- }, [uploadedFiles])
-
- useEffect(() => {
- if (isSuccessProjects && ref !== undefined) {
- const selectedProjectFromUrl = projects.find((project) => project.ref === ref)
- if (selectedProjectFromUrl !== undefined) setSelectedProjectRef(selectedProjectFromUrl.ref)
- }
- }, [isSuccessProjects])
-
- const IconContainer = (
- props: React.DetailedHTMLProps, HTMLDivElement>
- ) => (
-
- )
-
- function generateLink(pageType: PageType, link: string): string {
- switch (pageType) {
- case PageType.Markdown:
- case PageType.Reference:
- return `https://supabase.com/docs${link}`
- case PageType.Integration:
- return `https://supabase.com${link}`
- case PageType.GithubDiscussion:
- return link
- default:
- throw new Error(`Unknown page type '${pageType}'`)
- }
- }
-
- 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}'`)
- }
- }
-
- const cardBaseClasses =
- 'bg-200 rounded-lg hover:bg-surface-200 p-3 transition-colors hover:border-overlay hover:shadow-sm flex items-center justify-between'
-
- interface DocsLinkGroup {
- page: Page
- }
-
- const DocsLinkGroup = ({ page }: DocsLinkGroup) => {
- const link = generateLink(page.type, page.path)
-
- return (
-
-
-
-
-
{getPageIcon(page)}
-
-
-
-
- {(page.description || page.subtitle) && (
-
-
-
- )}
-
-
-
-
- {page.sections.length > 0 && (
-
- {page.sections.map((section: PageSection, i) => (
-
- ))}
-
- )}
-
-
- )
- }
-
- interface DocsLinkSection {
- page: Page
- section: PageSection
- }
-
- const DocsLinkSection = ({ page, section }: DocsLinkSection) => {
- const sectionLink = formatSectionUrl(page, section)
-
- return (
-
-
-
-
-
{getPageIcon(page)}
-
- {page.type !== 'github-discussions' && (
-
-
-
- )}
-
- {section.heading && (
-
-
-
- )}
-
-
-
-
-
-
- )
- }
-
- return (
-
- )
-}
-
-export default SupportForm
-
-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}'`)
- }
-}
diff --git a/apps/studio/components/interfaces/Support/SupportForm.utils.ts b/apps/studio/components/interfaces/Support/SupportForm.utils.ts
deleted file mode 100644
index cc080f2ce22..00000000000
--- a/apps/studio/components/interfaces/Support/SupportForm.utils.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { compact } from 'lodash'
-import { createClient } from '@supabase/supabase-js'
-import { uuidv4 } from 'lib/helpers'
-
-const SUPPORT_API_URL = process.env.NEXT_PUBLIC_SUPPORT_API_URL || ''
-const SUPPORT_API_KEY = process.env.NEXT_PUBLIC_SUPPORT_ANON_KEY || ''
-
-export const uploadAttachments = async (ref: string, files: File[]) => {
- const supportSupabaseClient = createClient(SUPPORT_API_URL, SUPPORT_API_KEY, {
- auth: {
- persistSession: false,
- autoRefreshToken: false,
- // @ts-ignore
- multiTab: false,
- detectSessionInUrl: false,
- localStorage: {
- getItem: (key: string) => undefined,
- setItem: (key: string, value: string) => {},
- removeItem: (key: string) => {},
- },
- },
- })
-
- const filesToUpload = Array.from(files)
- const uploadedFiles = await Promise.all(
- filesToUpload.map(async (file) => {
- const suffix = file.type.split('/')[1]
- const prefix = `${ref}/${uuidv4()}.${suffix}`
- const options = { cacheControl: '3600' }
-
- const { data, error } = await supportSupabaseClient.storage
- .from('support-attachments')
- .upload(prefix, file, options)
-
- if (error) console.error('Failed to upload:', file.name, error)
- return data
- })
- )
- const keys = compact(uploadedFiles).map((file) => file.path)
-
- const { data, error } = await supportSupabaseClient.storage
- .from('support-attachments')
- .createSignedUrls(keys, 10 * 365 * 24 * 60 * 60)
- if (error) {
- console.error('Failed to retrieve URLs for attachments', error)
- }
- return data ? data.map((file) => file.signedUrl) : []
-}
-
-export const formatMessage = (message: string, attachments: string[]) => {
- if (attachments.length > 0) {
- const attachmentsImg = attachments.map((url) => `\n${url}`)
- return `${message}\n${attachmentsImg.join('')}`
- } else {
- return message
- }
-}
diff --git a/apps/studio/components/interfaces/Support/SupportForm.utils.tsx b/apps/studio/components/interfaces/Support/SupportForm.utils.tsx
new file mode 100644
index 00000000000..c0e6c49b22b
--- /dev/null
+++ b/apps/studio/components/interfaces/Support/SupportForm.utils.tsx
@@ -0,0 +1,120 @@
+import { createClient } from '@supabase/supabase-js'
+import { compact } from 'lodash'
+import { Book, Github, Hash, MessageSquare } from 'lucide-react'
+
+import {
+ DocsSearchResultType as PageType,
+ type DocsSearchResult as Page,
+ type DocsSearchResultSection as PageSection,
+} from 'common'
+import { uuidv4 } from 'lib/helpers'
+
+const SUPPORT_API_URL = process.env.NEXT_PUBLIC_SUPPORT_API_URL || ''
+const SUPPORT_API_KEY = process.env.NEXT_PUBLIC_SUPPORT_ANON_KEY || ''
+
+export const uploadAttachments = async (ref: string, files: File[]) => {
+ const supportSupabaseClient = createClient(SUPPORT_API_URL, SUPPORT_API_KEY, {
+ auth: {
+ persistSession: false,
+ autoRefreshToken: false,
+ // @ts-ignore
+ multiTab: false,
+ detectSessionInUrl: false,
+ localStorage: {
+ getItem: (key: string) => undefined,
+ setItem: (key: string, value: string) => {},
+ removeItem: (key: string) => {},
+ },
+ },
+ })
+
+ const filesToUpload = Array.from(files)
+ const uploadedFiles = await Promise.all(
+ filesToUpload.map(async (file) => {
+ const suffix = file.type.split('/')[1]
+ const prefix = `${ref}/${uuidv4()}.${suffix}`
+ const options = { cacheControl: '3600' }
+
+ const { data, error } = await supportSupabaseClient.storage
+ .from('support-attachments')
+ .upload(prefix, file, options)
+
+ if (error) console.error('Failed to upload:', file.name, error)
+ return data
+ })
+ )
+ const keys = compact(uploadedFiles).map((file) => file.path)
+
+ if (keys.length === 0) return []
+
+ const { data, error } = await supportSupabaseClient.storage
+ .from('support-attachments')
+ .createSignedUrls(keys, 10 * 365 * 24 * 60 * 60)
+ if (error) {
+ console.error('Failed to retrieve URLs for attachments', error)
+ }
+ return data ? data.map((file) => file.signedUrl) : []
+}
+
+export const formatMessage = (message: string, attachments: string[]) => {
+ if (attachments.length > 0) {
+ const attachmentsImg = attachments.map((url) => `\n${url}`)
+ return `${message}\n${attachmentsImg.join('')}`
+ } else {
+ return message
+ }
+}
+
+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 `https://supabase.com/docs${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}'`)
+ }
+}
diff --git a/apps/studio/components/interfaces/Support/SupportFormV2.tsx b/apps/studio/components/interfaces/Support/SupportFormV2.tsx
new file mode 100644
index 00000000000..2227a81094a
--- /dev/null
+++ b/apps/studio/components/interfaces/Support/SupportFormV2.tsx
@@ -0,0 +1,719 @@
+import { zodResolver } from '@hookform/resolvers/zod'
+import * as Sentry from '@sentry/nextjs'
+import { useSupabaseClient } from '@supabase/auth-helpers-react'
+import { ExternalLink, Loader2, Mail, Plus, X } from 'lucide-react'
+import Link from 'next/link'
+import { ChangeEvent, useEffect, useRef, useState } from 'react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { toast } from 'sonner'
+import * as z from 'zod'
+
+import { useDocsSearch, useParams, type DocsSearchResult as Page } from 'common'
+import { CLIENT_LIBRARIES } from 'common/constants'
+import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send'
+import { useOrganizationsQuery } from 'data/organizations/organizations-query'
+import type { Project } from 'data/projects/project-detail-query'
+import { useProjectsQuery } from 'data/projects/projects-query'
+import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
+import { useFlag } from 'hooks/ui/useFlag'
+import { useProfile } from 'lib/profile'
+import {
+ Badge,
+ Button,
+ Checkbox_Shadcn_,
+ cn,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Input_Shadcn_,
+ ScrollArea,
+ Select_Shadcn_,
+ SelectContent_Shadcn_,
+ SelectGroup_Shadcn_,
+ SelectItem_Shadcn_,
+ SelectTrigger_Shadcn_,
+ SelectValue_Shadcn_,
+ Separator,
+ TextArea_Shadcn_,
+} from 'ui'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
+import DisabledStateForFreeTier from './DisabledStateForFreeTier'
+import { DocsLinkGroup } from './DocsLink'
+import { PlanExpectationInfoBox } from './PlanExpectationInfoBox'
+import {
+ CATEGORY_OPTIONS,
+ IPV4_MIGRATION_STRINGS,
+ SERVICE_OPTIONS,
+ SEVERITY_OPTIONS,
+} from './Support.constants'
+import { LibrarySuggestions } from './LibrarySuggestions'
+import { MultiSelectV2 } from 'ui-patterns/MultiSelectDeprecated/MultiSelectV2'
+import { IPV4SuggestionAlert } from './IPV4SuggestionAlert'
+import { formatMessage, uploadAttachments } from './SupportForm.utils'
+import { detectBrowser } from 'lib/helpers'
+import { getProjectAuthConfig } from 'data/auth/auth-config-query'
+
+const MAX_ATTACHMENTS = 5
+const INCLUDE_DISCUSSIONS = ['Problem', 'Database_unresponsive']
+const CONTAINER_CLASSES = 'px-6'
+
+interface SupportFormV2Props {
+ setSentCategory: (value: string) => void
+ setSelectedProject: (value: string) => void
+}
+
+// [Joshen] Just naming it as V2 for now for PR review purposes so its easier to view
+// This is a rewrite of the old SupportForm to use the new form components
+export const SupportFormV2 = ({ setSentCategory, setSelectedProject }: SupportFormV2Props) => {
+ const { profile } = useProfile()
+ const supabaseClient = useSupabaseClient()
+ const { ref, slug, category: urlCategory, subject: urlSubject, message: urlMessage } = useParams()
+ const enableFreeSupport = useFlag('enableFreeSupport')
+
+ const uploadButtonRef = useRef(null)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [docsResults, setDocsResults] = useState([])
+ const [uploadedFiles, setUploadedFiles] = useState([])
+ const [uploadedDataUrls, setUploadedDataUrls] = useState([])
+
+ const FormSchema = z
+ .object({
+ organizationSlug: z.string().min(1, 'Please select an organization'),
+ projectRef: z.string().min(1, 'Please select a project'),
+ category: z.string(),
+ severity: z.string(),
+ library: z.string(),
+ subject: z.string().min(1, 'Please add a subject heading'),
+ message: z.string().min(1, "Please add a message about the issue that you're facing"),
+ affectedServices: z.string(),
+ allowSupportAccess: z.boolean(),
+ })
+ .refine(
+ (data) => {
+ return !(data.category === 'Problem' && data.library === '')
+ },
+ {
+ message: "Please select the library that you're facing issues with",
+ path: ['library'],
+ }
+ )
+
+ const defaultValues = {
+ organizationSlug: '',
+ projectRef: 'no-project',
+ category: CATEGORY_OPTIONS[0].value,
+ severity: 'Low',
+ library: '',
+ subject: '',
+ message: '',
+ affectedServices: '',
+ allowSupportAccess: false,
+ }
+
+ const form = useForm>({
+ mode: 'onBlur',
+ reValidateMode: 'onBlur',
+ resolver: zodResolver(FormSchema),
+ defaultValues,
+ })
+
+ const { organizationSlug, projectRef, category, severity, subject, library } = form.watch()
+
+ const {
+ handleDocsSearchDebounced,
+ searchState,
+ searchState: state,
+ } = useDocsSearch(supabaseClient)
+
+ const {
+ data: organizations,
+ isLoading: isLoadingOrganizations,
+ isSuccess: isSuccessOrganizations,
+ } = useOrganizationsQuery()
+
+ const {
+ data: subscription,
+ isLoading: isLoadingSubscription,
+ isSuccess: isSuccessSubscription,
+ } = useOrgSubscriptionQuery({
+ orgSlug: organizationSlug,
+ })
+
+ const {
+ data: allProjects,
+ isLoading: isLoadingProjects,
+ isSuccess: isSuccessProjects,
+ } = useProjectsQuery()
+
+ const { mutate: submitSupportTicket } = useSendSupportTicketMutation({
+ onSuccess: (res, variables) => {
+ toast.success('Support request sent. Thank you!')
+ setSentCategory(variables.category)
+ setSelectedProject(variables.projectRef ?? 'no-project')
+ },
+ onError: (error) => {
+ toast.error(`Failed to submit support ticket: ${error.message}`)
+ Sentry.captureMessage('Failed to submit Support Form: ' + error.message)
+ setIsSubmitting(false)
+ },
+ })
+
+ const respondToEmail = profile?.primary_email ?? 'your email'
+ const subscriptionPlanId = subscription?.plan.id
+ const projects = [
+ ...(allProjects ?? []).filter((project) => project.organization_slug === organizationSlug),
+ { ref: 'no-project', name: 'No specific project' } as Partial,
+ ]
+ const hasResults =
+ state.status === 'fullResults' ||
+ state.status === 'partialResults' ||
+ (state.status === 'loading' && state.staleResults.length > 0)
+
+ const onFilesUpload = async (event: ChangeEvent) => {
+ event.persist()
+ const items = event.target.files || (event as any).dataTransfer.items
+ const itemsCopied = Array.prototype.map.call(items, (item) => item) as File[]
+ const itemsToBeUploaded = itemsCopied.slice(0, MAX_ATTACHMENTS - uploadedFiles.length)
+
+ setUploadedFiles(uploadedFiles.concat(itemsToBeUploaded))
+ if (items.length + uploadedFiles.length > MAX_ATTACHMENTS) {
+ toast(`Only up to ${MAX_ATTACHMENTS} attachments are allowed`)
+ }
+ event.target.value = ''
+ }
+
+ const removeUploadedFile = (idx: number) => {
+ const updatedFiles = uploadedFiles?.slice()
+ updatedFiles.splice(idx, 1)
+ setUploadedFiles(updatedFiles)
+
+ const updatedDataUrls = uploadedDataUrls.slice()
+ uploadedDataUrls.splice(idx, 1)
+ setUploadedDataUrls(updatedDataUrls)
+ }
+
+ const onSubmit: SubmitHandler> = async (values) => {
+ setIsSubmitting(true)
+ const attachments =
+ uploadedFiles.length > 0 ? await uploadAttachments(values.projectRef, uploadedFiles) : []
+ const selectedLibrary = CLIENT_LIBRARIES.find((library) => library.language === values.library)
+
+ const payload = {
+ ...values,
+ library:
+ values.category === 'Problem' && selectedLibrary !== undefined ? selectedLibrary.key : '',
+ message: formatMessage(values.message, attachments),
+ verified: true,
+ tags: ['dashboard-support-form'],
+ siteUrl: '',
+ additionalRedirectUrls: '',
+ affectedServices: values.affectedServices
+ .split(',')
+ .map((x) => x.trim().replace(/ /g, '_').toLowerCase())
+ .join(';'),
+ browserInformation: detectBrowser(),
+ }
+
+ if (values.projectRef !== 'no-project') {
+ try {
+ const authConfig = await getProjectAuthConfig({ projectRef: values.projectRef })
+ payload.siteUrl = authConfig.SITE_URL
+ payload.additionalRedirectUrls = authConfig.URI_ALLOW_LIST
+ } catch (error) {
+ // [Joshen] No error handler required as fetching these info are nice to haves, not necessary
+ }
+ }
+
+ submitSupportTicket(payload)
+ }
+
+ useEffect(() => {
+ if (subject !== urlSubject && subject.trim().length > 0) {
+ handleDocsSearchDebounced(subject.trim())
+ } else {
+ setDocsResults([])
+ }
+ }, [subject, urlSubject])
+
+ useEffect(() => {
+ if (subject.trim().length > 0 && searchState.status === 'fullResults') {
+ setDocsResults(searchState.results)
+ } else if (searchState.status === 'noResults' || searchState.status === 'error') {
+ setDocsResults([])
+ }
+ }, [searchState])
+
+ useEffect(() => {
+ if (!uploadedFiles) return
+ const objectUrls = uploadedFiles.map((file) => URL.createObjectURL(file))
+ setUploadedDataUrls(objectUrls)
+
+ return () => {
+ objectUrls.forEach((url: any) => URL.revokeObjectURL(url))
+ }
+ }, [uploadedFiles])
+
+ useEffect(() => {
+ // For prefilling form fields via URL, project ref will taking higher precedence than org slug
+ if (isSuccessOrganizations && isSuccessProjects) {
+ if (ref) {
+ const selectedProject = allProjects.find((p) => p.ref === ref)
+ if (selectedProject !== undefined) {
+ form.setValue('organizationSlug', selectedProject.organization_slug)
+ form.setValue('projectRef', selectedProject.ref)
+ }
+ } else if (slug) {
+ const selectedOrganization = organizations?.find((org) => org.slug === slug)
+ if (selectedOrganization !== undefined) {
+ form.setValue('organizationSlug', selectedOrganization.slug)
+ }
+ } else if (ref === undefined && slug === undefined) {
+ form.setValue('organizationSlug', organizations[0].slug)
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [ref, slug, isSuccessOrganizations, isSuccessProjects])
+
+ useEffect(() => {
+ if (urlCategory) {
+ const validCategory = CATEGORY_OPTIONS.find((option) => {
+ if (option.value.toLowerCase() === ((urlCategory as string) ?? '').toLowerCase())
+ return option
+ })
+ if (validCategory !== undefined) form.setValue('category', validCategory.value)
+ }
+ }, [urlCategory])
+
+ useEffect(() => {
+ if (urlSubject) form.setValue('subject', urlSubject)
+ }, [urlSubject])
+
+ useEffect(() => {
+ if (urlMessage) form.setValue('message', urlMessage)
+ }, [urlMessage])
+
+ return (
+
+
+
+ )
+}
diff --git a/apps/studio/components/ui/InformationBox.tsx b/apps/studio/components/ui/InformationBox.tsx
index 1ea7c26661c..4bcfc2e4116 100644
--- a/apps/studio/components/ui/InformationBox.tsx
+++ b/apps/studio/components/ui/InformationBox.tsx
@@ -1,6 +1,8 @@
+import { ExternalLink, Maximize2, Minimize2 } from 'lucide-react'
import Link from 'next/link'
import { ReactNode, useState } from 'react'
-import { Button, IconExternalLink, IconMaximize2, IconMinimize2 } from 'ui'
+
+import { Button } from 'ui'
interface InformationBoxProps {
icon?: ReactNode
@@ -48,9 +50,9 @@ const InformationBox = ({
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
-
+
) : (
-
+
)}
) : null}
@@ -66,7 +68,7 @@ const InformationBox = ({
{url && (
{
projects={projectsData}
/>
) : (
-