diff --git a/apps/studio/components/interfaces/Support/SupportForm.tsx b/apps/studio/components/interfaces/Support/SupportForm.tsx index 6a4beafef72..1771064a222 100644 --- a/apps/studio/components/interfaces/Support/SupportForm.tsx +++ b/apps/studio/components/interfaces/Support/SupportForm.tsx @@ -1,6 +1,29 @@ -import * as Sentry from '@sentry/nextjs' -import { useParams } from 'common' import { CLIENT_LIBRARIES } from 'common/constants' +import { + AlertCircle, + Book, + ChevronRight, + ExternalLink, + Github, + Hash, + HelpCircle, + Loader2, + Mail, + MessageSquare, + Plus, + X, +} from 'lucide-react' + +import { + DocsSearchResultType as PageType, + useDocsSearch, + useParams, + type DocsSearchResult as Page, + type DocsSearchResultSection as PageSection, +} from 'common' + +import * as Sentry from '@sentry/nextjs' + import InformationBox from 'components/ui/InformationBox' import ShimmeringLoader from 'components/ui/ShimmeringLoader' import { getProjectAuthConfig } from 'data/auth/auth-config-query' @@ -13,7 +36,8 @@ import { useFlag } from 'hooks' import useLatest from 'hooks/misc/useLatest' import { detectBrowser } from 'lib/helpers' import { useProfile } from 'lib/profile' -import { AlertCircle, ExternalLink, HelpCircle, Loader2, Mail, Plus, X } from 'lucide-react' +import { useCommandMenu } from 'ui-patterns/Cmdk' + import Link from 'next/link' import { useRouter } from 'next/router' import { ChangeEvent, useEffect, useRef, useState } from 'react' @@ -28,12 +52,15 @@ import { Input, Listbox, Separator, + cn, } from 'ui' import MultiSelect from 'ui-patterns/MultiSelectDeprecated' import DisabledStateForFreeTier from './DisabledStateForFreeTier' import { CATEGORY_OPTIONS, SERVICE_OPTIONS, SEVERITY_OPTIONS } from './Support.constants' import { formatMessage, uploadAttachments } from './SupportForm.utils' +import { TextHighlighter } from 'ui-patterns/Cmdk/Command.utils' + const MAX_ATTACHMENTS = 5 const INCLUDE_DISCUSSIONS = ['Problem', 'Database_unresponsive'] @@ -43,8 +70,28 @@ export interface SupportFormProps { } const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) => { + const { handleDocsSearchDebounced, searchState, searchState: state } = useDocsSearch() + 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, subject, category, message } = useParams() + const { ref, slug, category, message } = useParams() const uploadButtonRef = useRef() const enableFreeSupport = useFlag('enableFreeSupport') @@ -131,6 +178,9 @@ const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) allowSupportAccess: false, } + const { site } = useCommandMenu() + const router = useRouter() + const onFilesUpload = async (event: ChangeEvent) => { event.persist() const items = event.target.files || (event as any).dataTransfer.items @@ -218,6 +268,10 @@ const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) ] 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 @@ -236,6 +290,183 @@ const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) } }, [isSuccessProjects]) + const IconContainer = ( + props: React.DetailedHTMLProps, HTMLDivElement> + ) => ( +
+ ) + + function generateLink(pageType: PageType, link: string): string { + switch (pageType) { + case PageType.Markdown: + case PageType.Reference: + if (site === 'docs') { + return link + } else if (site === 'website') { + return `/docs${link}` + } else { + return `https://supabase.com/docs${link}` + } + case PageType.Integration: + if (site === 'website') { + return link + } else { + return `https://supabase.com${link}` + } + case PageType.GithubDiscussion: + return link + default: + throw new Error(`Unknown page type '${pageType}'`) + } + } + + async function handleLinkClick(pageType: PageType, link: string) { + switch (pageType) { + case PageType.Markdown: + case PageType.Reference: + if (site === 'docs') { + await router.push(link) + } else if (site === 'website') { + await router.push(`/docs${link}`) + } else { + window.open(`https://supabase.com/docs${link}`, '_blank') + } + break + case PageType.Integration: + if (site === 'website') { + router.push(link) + } else { + window.open(`https://supabase.com${link}`, '_blank') + } + break + case PageType.GithubDiscussion: + window.open(link, '_blank') + break + 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) => { + console.log('page', page) + 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 (
{({ resetForm, values }: any) => { @@ -552,6 +783,7 @@ const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) id="subject" label="Subject" placeholder="Summary of the problem you have" + onChange={(e) => setSubject(e.target.value)} descriptionText={ values.subject.length > 0 && INCLUDE_DISCUSSIONS.includes(values.category) ? ( @@ -573,6 +805,18 @@ const SupportForm = ({ setSentCategory, setSelectedProject }: SupportFormProps) } />
+ + {docsResults.length > 0 && hasResults && ( +
+

+ Suggested resources +

+ {docsResults.slice(0, 5).map((page, i) => ( + + ))} +
+ )} + {values.category === 'Problem' && (
+ 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}'`) + } +}