Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 19 additions & 21 deletions apps/docs/components/Feedback/Feedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import { createClient } from '@supabase/supabase-js'
import { IS_PLATFORM } from '~/lib/constants'
import { useSendFeedbackMutation } from '~/lib/fetch/feedback'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { useConstant, useIsLoggedIn, type Database } from 'common'
import { gotrueClient, useConstant, useIsLoggedIn, type Database } from 'common'
import { Check, MessageSquareQuote, X } from 'lucide-react'
import { usePathname } from 'next/navigation'
import {
Expand All @@ -17,7 +16,7 @@ import {
} from 'react'
import { Button, cn } from 'ui'

import { getLinearTeam, getSanitizedTabParams } from './Feedback.utils'
import { getSanitizedTabParams } from './Feedback.utils'
import { FeedbackModal, type FeedbackFields } from './FeedbackModal'

const FeedbackButton = forwardRef<
Expand Down Expand Up @@ -78,7 +77,6 @@ function Feedback({ className }: { className?: string }) {

const pathname = usePathname() ?? ''
const sendTelemetryEvent = useSendTelemetryEvent()
const { mutate: sendFeedbackComment } = useSendFeedbackMutation()
const supabase = useConstant(() =>
IS_PLATFORM
? createClient<Database>(
Expand All @@ -96,14 +94,9 @@ function Feedback({ className }: { className?: string }) {

async function sendFeedbackVote(response: Response) {
if (!supabase) return

const { error } = await supabase.from('feedback').insert({
vote: response,
page: pathname,
metadata: {
query: getSanitizedTabParams(),
},
})
const { error } = await supabase
.from('feedback')
.insert({ vote: response, page: pathname, metadata: { query: getSanitizedTabParams() } })
if (error) console.error(error)
}

Expand All @@ -128,15 +121,20 @@ function Feedback({ className }: { className?: string }) {
}, 100)
}

async function handleSubmit({ page, comment, title }: FeedbackFields) {
sendFeedbackComment({
message: comment,
pathname: page,
title,
// @ts-expect-error -- can't click this button without having a state.response
isHelpful: state.response === 'yes',
team: getLinearTeam(pathname),
})
async function handleSubmit({ comment, title }: FeedbackFields) {
if (supabase) {
const userId = (await gotrueClient.getSession()).data.session?.user?.id ?? null
const { error } = await supabase.from('feedback_comments').insert({
page: pathname,
// @ts-expect-error -- the comment modal only opens after a vote, so state.response is set
vote: state.response,
title,
comment,
user_id: userId,
metadata: { query: getSanitizedTabParams() },
})
if (error) console.error(error)
}
setModalOpen(false)
refocusButton()
}
Expand Down
42 changes: 1 addition & 41 deletions apps/docs/components/Feedback/Feedback.utils.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,5 @@
import { pick } from 'lodash-es'

/**
* Gets the Notion team to send feedback to based on the pathname.
*/
const getLinearTeam = (pathname: string) => {
const DEFAULT_TEAM = 'Docs'

// Pathname has format `/guides/(team)/**`
const pathParts = pathname.split('/')

if (pathParts[1] !== 'guides' || !pathParts[2]) return DEFAULT_TEAM

switch (pathParts[2]) {
case 'database':
return 'Postgres'
case 'auth':
return 'Auth'
case 'storage':
return 'Storage'
case 'functions':
return 'Functions'
case 'realtime':
return 'Realtime'
case 'ai':
return 'AI'
case 'local-development':
case 'self-hosting':
case 'deployment':
return 'Dev Workflows'
case 'integrations':
return 'API'
case 'security':
return 'Security'
case 'platform':
case 'monitoring-troubleshooting':
return 'Infra'
default:
return DEFAULT_TEAM
}
}

/**
* Gets the tab selection state from the URL search params.
*
Expand All @@ -53,4 +13,4 @@ const getSanitizedTabParams = () => {
return pick(Object.fromEntries(searchParams.entries()), queryGroups)
}

export { getLinearTeam, getSanitizedTabParams }
export { getSanitizedTabParams }
50 changes: 0 additions & 50 deletions apps/docs/lib/fetch/feedback.ts

This file was deleted.

59 changes: 59 additions & 0 deletions apps/docs/lib/mdx/plugins/remarkAdmonition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { fromDocsMarkdown } from '~/features/directives/utils.server'
import type { Content, Paragraph, Root } from 'mdast'
import { gfmToMarkdown } from 'mdast-util-gfm'
import type { MdxJsxFlowElement } from 'mdast-util-mdx'
import { mdxToMarkdown } from 'mdast-util-mdx'
import { toMarkdown } from 'mdast-util-to-markdown'
import { describe, expect, it } from 'vitest'

import remarkMkDocsAdmonition from './remarkAdmonition'

function transformAndSerialize(markdown: string) {
const mdast = fromDocsMarkdown(markdown)
const transformed = remarkMkDocsAdmonition()(mdast)
const output = toMarkdown(transformed, { extensions: [mdxToMarkdown(), gfmToMarkdown()] })
const reparsed = fromDocsMarkdown(output)
return { transformed, output, reparsed }
}

function getAdmonition(root: Root): MdxJsxFlowElement {
const node = root.children[0]
expect(node.type).toBe('mdxJsxFlowElement')
return node as MdxJsxFlowElement
}

function getParagraph(node: Content): Paragraph {
expect(node.type).toBe('paragraph')
return node as Paragraph
}

describe('remarkMkDocsAdmonition', () => {
it('wraps inline admonition content in a single paragraph', () => {
const markdown = `!!! note "About Authentication"
HubSpot deprecated their legacy API Keys in November 2022. The \`api_key\` option in this wrapper accepts a **Private App Access Token**, which is HubSpot's recommended authentication method. See [HubSpot Private Apps](https://developers.hubspot.com/docs/guides/apps/private-apps/overview) for setup instructions.`

const { transformed, reparsed } = transformAndSerialize(markdown)

const admonition = getAdmonition(transformed)
expect(admonition.children).toHaveLength(1)
getParagraph(admonition.children[0])

const reparsedAdmonition = getAdmonition(reparsed)
expect(reparsedAdmonition.children).toHaveLength(1)
getParagraph(reparsedAdmonition.children[0])
})

it('keeps indented sibling blocks as separate children', () => {
const markdown = `!!! note

First indented paragraph.

Second indented paragraph.`

const { transformed } = transformAndSerialize(markdown)

const admonition = getAdmonition(transformed)
expect(admonition.children.length).toBeGreaterThanOrEqual(2)
expect(admonition.children.every((child) => child.type === 'paragraph')).toBe(true)
})
})
24 changes: 20 additions & 4 deletions apps/docs/lib/mdx/plugins/remarkAdmonition.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Content, Paragraph, Root } from 'mdast'
import type { BlockContent, Content, Paragraph, Root } from 'mdast'
import type { MdxJsxFlowElement } from 'mdast-util-mdx'
import type { AdmonitionProps } from 'ui-patterns/admonition'
import type { Node } from 'unist'
Expand Down Expand Up @@ -35,7 +35,18 @@ const remarkMkDocsAdmonition = function () {
// Extract sibling nodes that should be linked to this admonition
const siblingsToNest = extractLinkedSiblings(parent, paragraph, index)

const children: any[] = [...paragraph.children, ...siblingsToNest]
const inlineChildren = paragraph.children.filter((child) => {
if (child.type === 'text') return child.value.trim().length > 0
return true
})

// Wrap inline content in a single paragraph so MDX does not emit one <p> per node.
const children: MdxJsxFlowElement['children'] = [
...(inlineChildren.length > 0
? [{ type: 'paragraph' as const, children: inlineChildren }]
: []),
...siblingsToNest,
]

// Generate a Supabase Admonition JSX element
const admonitionElement: MdxJsxFlowElement = {
Expand Down Expand Up @@ -77,7 +88,12 @@ const remarkMkDocsAdmonition = function () {
*
* Splices the discovered siblings out of the original parent and returns them.
*/
function extractLinkedSiblings(parent: Root, node: Node, index: number, indentAmount = 4) {
function extractLinkedSiblings(
parent: Root,
node: Node,
index: number,
indentAmount = 4
): BlockContent[] {
const { column } = node.position?.start || { column: 0 }

let nextSibling: Content
Expand All @@ -87,7 +103,7 @@ function extractLinkedSiblings(parent: Root, node: Node, index: number, indentAm
nextSibling = parent.children[++i]
} while (nextSibling?.position && nextSibling.position.start.column === column + indentAmount)

return parent.children.splice(index + 1, i - index - 1)
return parent.children.splice(index + 1, i - index - 1) as BlockContent[]
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,44 +98,44 @@ export const AccountConnections = () => {
alt={`GitHub icon`}
/>
<div>
<p className="text-sm">GitHub</p>
<div className="flex items-center gap-x-2">
<p className="text-sm">GitHub</p>
{isConnected && <Badge variant="success">Connected</Badge>}
</div>
<p className="text-sm text-foreground-lighter">
Sync repos to Supabase projects for automatic branch creation and merging
</p>
</div>
</div>
<div className="flex items-center gap-x-2 ml-2">
{isConnected ? (
<>
<Badge variant="success">Connected</Badge>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button iconRight={<ChevronDown size={14} />} variant="default">
<span>Manage</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-44">
<DropdownMenuItem
className="space-x-2"
onSelect={(event) => {
event.preventDefault()
handleReauthenticate()
}}
>
<RefreshCw size={14} />
<p>Re-authenticate</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="space-x-2"
onSelect={() => setIsRemoveModalOpen(true)}
>
<Unlink size={14} />
<p>Remove connection</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button iconRight={<ChevronDown size={14} />} variant="default">
<span>Manage</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-44">
<DropdownMenuItem
className="space-x-2"
onSelect={(event) => {
event.preventDefault()
handleReauthenticate()
}}
>
<RefreshCw size={14} />
<p>Re-authenticate</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="space-x-2"
onSelect={() => setIsRemoveModalOpen(true)}
>
<Unlink size={14} />
<p>Remove connection</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button variant="primary" onClick={handleConnect}>
Connect
Expand Down
Loading
Loading