fix: refine contribute panel actions

This commit is contained in:
lucaronin
2026-04-24 23:35:28 +02:00
parent 122aba7878
commit b9f3dc9e1a
4 changed files with 144 additions and 59 deletions

View File

@@ -2,10 +2,11 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { FeedbackDialog } from './FeedbackDialog'
import {
TOLARIA_CANNY_URL,
TOLARIA_GITHUB_CONTRIBUTING_URL,
TOLARIA_GITHUB_DISCUSSIONS_URL,
TOLARIA_GITHUB_ISSUES_URL,
TOLARIA_GITHUB_PULL_REQUESTS_URL,
TOLARIA_PRODUCT_BOARD_URL,
} from '../constants/feedback'
import { APP_COMMAND_EVENT_NAME, APP_COMMAND_IDS } from '../hooks/appCommandDispatcher'
import { rememberFeedbackDialogOpener } from '../lib/feedbackDialogOpener'
@@ -33,15 +34,20 @@ describe('FeedbackDialog', () => {
render(<FeedbackDialog open={true} onClose={vi.fn()} buildNumber="b281" releaseChannel="alpha" />)
expect(screen.getByTestId('feedback-dialog')).toBeInTheDocument()
expect(screen.getByText('Contribute to Tolaria')).toBeInTheDocument()
expect(screen.getByText('Feature request / improvement idea')).toBeInTheDocument()
expect(screen.getByText('Community / discussion')).toBeInTheDocument()
expect(screen.getByText('Pick the path that fits what you want to do! Any type of help is appreciated')).toBeInTheDocument()
expect(screen.getByText('Feature requests')).toBeInTheDocument()
expect(screen.getByText('Discussions')).toBeInTheDocument()
expect(screen.getByText('Contribute code')).toBeInTheDocument()
expect(screen.getByText('Report a bug')).toBeInTheDocument()
expect(screen.getByText(/Search on the product board first/i)).toBeInTheDocument()
expect(screen.getByText(/Use GitHub Discussions for/i)).toBeInTheDocument()
expect(screen.getByText(/Attach the sanitized diagnostic bundle please!/i)).toBeInTheDocument()
expect(screen.queryByText(/Sanitized and optional/i)).not.toBeInTheDocument()
})
it('focuses the primary CTA when opened', async () => {
render(<FeedbackDialog open={true} onClose={vi.fn()} buildNumber="b281" releaseChannel={null} />)
const cta = screen.getByRole('button', { name: 'Open Canny' })
const cta = screen.getByRole('button', { name: 'Open Product Board' })
await waitFor(() => expect(cta).toHaveFocus())
})
@@ -49,15 +55,17 @@ describe('FeedbackDialog', () => {
const onClose = vi.fn()
render(<FeedbackDialog open={true} onClose={onClose} buildNumber="b281" releaseChannel={null} />)
fireEvent.click(screen.getByRole('button', { name: 'Open Canny' }))
fireEvent.click(screen.getByRole('button', { name: 'Open Product Board' }))
fireEvent.click(screen.getByRole('button', { name: 'Open Discussions' }))
fireEvent.click(screen.getByRole('button', { name: 'Open Pull Requests' }))
fireEvent.click(screen.getByRole('button', { name: 'Open Contributing Guide' }))
fireEvent.click(screen.getByRole('button', { name: 'Open GitHub Issues' }))
await waitFor(() => expect(openExternalUrl).toHaveBeenNthCalledWith(1, TOLARIA_CANNY_URL))
await waitFor(() => expect(openExternalUrl).toHaveBeenNthCalledWith(1, TOLARIA_PRODUCT_BOARD_URL))
expect(openExternalUrl).toHaveBeenNthCalledWith(2, TOLARIA_GITHUB_DISCUSSIONS_URL)
expect(openExternalUrl).toHaveBeenNthCalledWith(3, TOLARIA_GITHUB_CONTRIBUTING_URL)
expect(openExternalUrl).toHaveBeenNthCalledWith(4, TOLARIA_GITHUB_ISSUES_URL)
expect(openExternalUrl).toHaveBeenNthCalledWith(3, TOLARIA_GITHUB_PULL_REQUESTS_URL)
expect(openExternalUrl).toHaveBeenNthCalledWith(4, TOLARIA_GITHUB_CONTRIBUTING_URL)
expect(openExternalUrl).toHaveBeenNthCalledWith(5, TOLARIA_GITHUB_ISSUES_URL)
expect(onClose).not.toHaveBeenCalled()
expect(screen.getByTestId('feedback-dialog')).toBeInTheDocument()
})
@@ -71,7 +79,7 @@ describe('FeedbackDialog', () => {
render(<FeedbackDialog open={true} onClose={vi.fn()} buildNumber="b281" releaseChannel="alpha" />)
fireEvent.click(screen.getByRole('button', { name: 'Copy diagnostics' }))
fireEvent.click(screen.getByRole('button', { name: 'Copy sanitized diagnostics' }))
await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1))
expect(writeText.mock.calls[0]?.[0]).toContain('Tolaria sanitized diagnostics')
@@ -84,10 +92,10 @@ describe('FeedbackDialog', () => {
openExternalUrl.mockRejectedValueOnce(new Error('blocked'))
render(<FeedbackDialog open={true} onClose={vi.fn()} buildNumber="b281" releaseChannel={null} />)
fireEvent.click(screen.getByRole('button', { name: 'Open Canny' }))
fireEvent.click(screen.getByRole('button', { name: 'Open Product Board' }))
expect(await screen.findByText(/couldnt open Canny automatically/i)).toBeInTheDocument()
expect(screen.getByText(TOLARIA_CANNY_URL)).toBeInTheDocument()
expect(await screen.findByText(/couldnt open Product Board automatically/i)).toBeInTheDocument()
expect(screen.getByText(TOLARIA_PRODUCT_BOARD_URL)).toBeInTheDocument()
})
it('closes when pressing Escape', () => {
@@ -97,7 +105,7 @@ describe('FeedbackDialog', () => {
expect(onClose).toHaveBeenCalledOnce()
})
it('closes when clicking Close', () => {
it('closes when clicking the top-right Close control', () => {
const onClose = vi.fn()
render(<FeedbackDialog open={true} onClose={onClose} buildNumber="b281" releaseChannel={null} />)
fireEvent.click(screen.getByRole('button', { name: 'Close' }))

View File

@@ -14,20 +14,21 @@ import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
TOLARIA_CANNY_URL,
TOLARIA_GITHUB_CONTRIBUTING_URL,
TOLARIA_GITHUB_DISCUSSIONS_URL,
TOLARIA_GITHUB_ISSUES_URL,
TOLARIA_GITHUB_PULL_REQUESTS_URL,
TOLARIA_PRODUCT_BOARD_URL,
} from '../constants/feedback'
import {
buildSanitizedDiagnosticBundle,
startFeedbackDiagnosticsCapture,
} from '../lib/feedbackDiagnostics'
import { cn } from '../lib/utils'
import { takeFeedbackDialogOpener } from '../lib/feedbackDialogOpener'
import { useBuildNumber } from '../hooks/useBuildNumber'
import { APP_COMMAND_EVENT_NAME, APP_COMMAND_IDS } from '../hooks/appCommandDispatcher'
@@ -45,6 +46,7 @@ interface ContributionCardProps {
description: string
ctaLabel: string
icon: typeof Lightbulb
tone: ContributionTone
onAction: () => void
autoFocus?: boolean
secondaryAction?: ReactNode
@@ -62,6 +64,14 @@ interface ContributionPath {
label: string
url: string
icon: typeof Lightbulb
tone: ContributionTone
secondaryLink?: ContributionLink
}
interface ContributionLink {
ctaLabel: string
label: string
url: string
}
const EMPTY_DIALOG_OPENER: ReturnType<typeof takeFeedbackDialogOpener> = {
@@ -69,38 +79,94 @@ const EMPTY_DIALOG_OPENER: ReturnType<typeof takeFeedbackDialogOpener> = {
reopenCommandPalette: false,
}
type ContributionTone = 'green' | 'yellow' | 'purple' | 'red'
const CONTRIBUTION_TONE_CLASSES: Record<ContributionTone, string> = {
green: 'bg-[var(--accent-green-light)] text-[var(--accent-green)]',
yellow: 'bg-[var(--accent-yellow-light)] text-[var(--accent-yellow)]',
purple: 'bg-[var(--accent-purple-light)] text-[var(--accent-purple)]',
red: 'bg-[var(--accent-red-light)] text-[var(--accent-red)]',
}
const CONTRIBUTION_BUTTON_CLASSES: Record<ContributionTone, string> = {
green: 'border-[var(--accent-green)] hover:bg-[var(--accent-green-light)] [&_svg]:text-[var(--accent-green)]',
yellow: 'border-[var(--accent-yellow)] hover:bg-[var(--accent-yellow-light)] [&_svg]:text-[var(--accent-yellow)]',
purple: 'border-[var(--accent-purple)] hover:bg-[var(--accent-purple-light)] [&_svg]:text-[var(--accent-purple)]',
red: 'border-[var(--accent-red)] hover:bg-[var(--accent-red-light)] [&_svg]:text-[var(--accent-red)]',
}
const CONTRIBUTION_PATHS: ContributionPath[] = [
{
title: 'Feature request / improvement idea',
description: 'Search Canny first, upvote an existing idea if it is already there, and only create a new post when it is genuinely new.',
ctaLabel: 'Open Canny',
label: 'Canny',
url: TOLARIA_CANNY_URL,
title: 'Feature requests',
description: 'Search on the product board first, upvote an existing idea if already there, and only create a new post when genuinely new!',
ctaLabel: 'Open Product Board',
label: 'Product Board',
url: TOLARIA_PRODUCT_BOARD_URL,
icon: Lightbulb,
tone: 'green',
},
{
title: 'Community / discussion',
description: 'Use Discussions for questions, broader conversations, idea sharing, and community context that is not a concrete bug report.',
title: 'Discussions',
description: 'Use GitHub Discussions for questions, broader conversations, idea sharing, and community context that is not a concrete bug report.',
ctaLabel: 'Open Discussions',
label: 'GitHub Discussions',
url: TOLARIA_GITHUB_DISCUSSIONS_URL,
icon: MessagesSquare,
tone: 'purple',
},
{
title: 'Contribute code',
description: 'Small, focused pull requests are welcome. Check planned or in-progress work first so you are building in the right place.',
ctaLabel: 'Open Contributing Guide',
label: 'the contributing guide',
url: TOLARIA_GITHUB_CONTRIBUTING_URL,
ctaLabel: 'Open Pull Requests',
label: 'GitHub Pull Requests',
url: TOLARIA_GITHUB_PULL_REQUESTS_URL,
icon: GitPullRequest,
tone: 'yellow',
secondaryLink: {
ctaLabel: 'Open Contributing Guide',
label: 'the contributing guide',
url: TOLARIA_GITHUB_CONTRIBUTING_URL,
},
},
]
function ContributionLinkButton({
label,
tone,
onAction,
autoFocus = false,
accented = true,
}: {
label: string
tone: ContributionTone
onAction: () => void
autoFocus?: boolean
accented?: boolean
}) {
return (
<Button
type="button"
variant="outline"
className={cn(
'w-full justify-between',
accented && 'bg-background text-foreground hover:text-foreground',
accented && CONTRIBUTION_BUTTON_CLASSES[tone],
)}
autoFocus={autoFocus}
onClick={onAction}
>
{label}
<ArrowUpRight size={14} />
</Button>
)
}
function ContributionCard({
title,
description,
ctaLabel,
icon: Icon,
tone,
onAction,
autoFocus = false,
secondaryAction,
@@ -109,7 +175,7 @@ function ContributionCard({
<Card className="gap-4 border-border/70 py-4 shadow-none">
<CardHeader className="gap-3 px-4">
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
<span className="rounded-md bg-muted p-2 text-muted-foreground">
<span className={cn('rounded-md p-2', CONTRIBUTION_TONE_CLASSES[tone])}>
<Icon size={16} />
</span>
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
@@ -119,10 +185,7 @@ function ContributionCard({
</CardDescription>
</CardHeader>
<CardContent className="px-4">
<Button type="button" variant="secondary" className="w-full justify-between" autoFocus={autoFocus} onClick={onAction}>
{ctaLabel}
<ArrowUpRight size={14} />
</Button>
<ContributionLinkButton label={ctaLabel} tone={tone} autoFocus={autoFocus} onAction={onAction} />
</CardContent>
{secondaryAction ? <CardFooter className="px-4 pt-0">{secondaryAction}</CardFooter> : null}
</Card>
@@ -150,6 +213,10 @@ function LinkFallbackBanner({ linkFallback }: { linkFallback: LinkFallback | nul
)
}
function getCopyDiagnosticsLabel(copyState: 'idle' | 'copied' | 'failed') {
return copyState === 'copied' ? 'Diagnostics copied' : 'Copy sanitized diagnostics'
}
function BugReportActions({
copyState,
canCopyDiagnostics,
@@ -168,12 +235,9 @@ function BugReportActions({
onClick={onCopyDiagnostics}
disabled={!canCopyDiagnostics}
>
{copyState === 'copied' ? 'Diagnostics copied' : 'Copy diagnostics'}
{getCopyDiagnosticsLabel(copyState)}
{copyState === 'copied' ? <Check size={14} /> : <Copy size={14} />}
</Button>
<p className="text-xs leading-5 text-muted-foreground">
Sanitized and optional. Paths and token-like strings are redacted by default.
</p>
{copyState === 'copied' ? (
<p className="text-xs font-medium text-foreground">Diagnostics copied.</p>
) : null}
@@ -273,22 +337,36 @@ function ContributionGrid({
}) {
return (
<div className="grid gap-4 sm:grid-cols-2">
{CONTRIBUTION_PATHS.map((path, index) => (
<ContributionCard
key={path.title}
title={path.title}
description={path.description}
ctaLabel={path.ctaLabel}
icon={path.icon}
autoFocus={index === 0}
onAction={() => onOpenLink(path.label, path.url)}
/>
))}
{CONTRIBUTION_PATHS.map((path, index) => {
const secondaryLink = path.secondaryLink
return (
<ContributionCard
key={path.title}
title={path.title}
description={path.description}
ctaLabel={path.ctaLabel}
icon={path.icon}
tone={path.tone}
autoFocus={index === 0}
onAction={() => onOpenLink(path.label, path.url)}
secondaryAction={secondaryLink ? (
<ContributionLinkButton
label={secondaryLink.ctaLabel}
tone={path.tone}
accented={false}
onAction={() => onOpenLink(secondaryLink.label, secondaryLink.url)}
/>
) : undefined}
/>
)
})}
<ContributionCard
title="Report a bug"
description="A strong report explains the steps to reproduce, what you expected, and what actually happened. You can optionally paste a sanitized diagnostic bundle."
description="Explain how to reproduce, what you expected, and what actually happened. Check for duplicate bugs first. Attach the sanitized diagnostic bundle please!"
ctaLabel="Open GitHub Issues"
icon={Bug}
tone="red"
onAction={() => onOpenLink('GitHub Issues', TOLARIA_GITHUB_ISSUES_URL)}
secondaryAction={(
<BugReportActions
@@ -333,15 +411,14 @@ export function FeedbackDialog({
return (
<Dialog open={open} onOpenChange={(next) => { if (!next) handleClose() }}>
<DialogContent showCloseButton={false} className="max-h-[85vh] overflow-y-auto sm:max-w-[760px]" data-testid="feedback-dialog">
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-[760px]" data-testid="feedback-dialog">
<DialogHeader className="space-y-2">
<DialogTitle className="flex items-center gap-2">
<Megaphone size={18} weight="duotone" />
Contribute to Tolaria
</DialogTitle>
<DialogDescription>
Pick the path that fits what you want to do. The links stay focused, the diagnostics are sanitized,
and you can work through the whole flow with the keyboard.
Pick the path that fits what you want to do! Any type of help is appreciated
</DialogDescription>
</DialogHeader>
@@ -352,12 +429,6 @@ export function FeedbackDialog({
canCopyDiagnostics={canCopyDiagnostics}
onCopyDiagnostics={handleCopyDiagnostics}
/>
<DialogFooter className="sm:justify-between">
<Button type="button" variant="outline" onClick={handleClose}>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)

View File

@@ -1,4 +1,5 @@
export const TOLARIA_CANNY_URL = 'https://tolaria.canny.io/'
export const TOLARIA_PRODUCT_BOARD_URL = 'https://tolaria.canny.io/'
export const TOLARIA_GITHUB_DISCUSSIONS_URL = 'https://github.com/refactoringhq/tolaria/discussions'
export const TOLARIA_GITHUB_CONTRIBUTING_URL = 'https://github.com/refactoringhq/tolaria/blob/main/CONTRIBUTING.md'
export const TOLARIA_GITHUB_ISSUES_URL = 'https://github.com/refactoringhq/tolaria/issues'
export const TOLARIA_GITHUB_PULL_REQUESTS_URL = 'https://github.com/refactoringhq/tolaria/pulls'

View File

@@ -39,7 +39,7 @@ test.describe('Contribute modal', () => {
await expect(page.getByTestId('feedback-dialog')).toBeVisible()
await expect(page.getByRole('heading', { name: 'Contribute to Tolaria' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Open Canny' })).toBeFocused()
await expect(page.getByRole('button', { name: 'Open Product Board' })).toBeFocused()
await page.keyboard.press('Enter')
await expect.poll(async () => page.evaluate(() => (window as typeof window & { __tolariaOpenedUrls: string[] }).__tolariaOpenedUrls)).toContain('https://tolaria.canny.io/')
@@ -50,8 +50,13 @@ test.describe('Contribute modal', () => {
await expect.poll(async () => page.evaluate(() => (window as typeof window & { __tolariaOpenedUrls: string[] }).__tolariaOpenedUrls)).toContain('https://github.com/refactoringhq/tolaria/discussions')
await page.keyboard.press('Tab')
await expect(page.getByRole('button', { name: 'Open Contributing Guide' })).toBeFocused()
await expect(page.getByRole('button', { name: 'Open Pull Requests' })).toBeFocused()
await page.keyboard.press('Enter')
await expect.poll(async () => page.evaluate(() => (window as typeof window & { __tolariaOpenedUrls: string[] }).__tolariaOpenedUrls)).toContain('https://github.com/refactoringhq/tolaria/pulls')
await page.keyboard.press('Tab')
await expect(page.getByRole('button', { name: 'Open Contributing Guide' })).toBeFocused()
await page.keyboard.press('Space')
await expect.poll(async () => page.evaluate(() => (window as typeof window & { __tolariaOpenedUrls: string[] }).__tolariaOpenedUrls)).toContain('https://github.com/refactoringhq/tolaria/blob/main/CONTRIBUTING.md')
await page.keyboard.press('Tab')
@@ -60,7 +65,7 @@ test.describe('Contribute modal', () => {
await expect.poll(async () => page.evaluate(() => (window as typeof window & { __tolariaOpenedUrls: string[] }).__tolariaOpenedUrls)).toContain('https://github.com/refactoringhq/tolaria/issues')
await page.keyboard.press('Tab')
await expect(page.getByRole('button', { name: 'Copy diagnostics' })).toBeFocused()
await expect(page.getByRole('button', { name: 'Copy sanitized diagnostics' })).toBeFocused()
await page.keyboard.press('Space')
await expect.poll(async () => page.evaluate(() => (window as typeof window & { __tolariaCopiedBundles: string[] }).__tolariaCopiedBundles.length)).toBe(1)