feat: add feedback modal flow
This commit is contained in:
53
src/components/FeedbackDialog.test.tsx
Normal file
53
src/components/FeedbackDialog.test.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { FeedbackDialog } from './FeedbackDialog'
|
||||
import { LAPUTA_GITHUB_ISSUES_URL } from '../constants/feedback'
|
||||
|
||||
vi.mock('../utils/url', () => ({
|
||||
openExternalUrl: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
const { openExternalUrl } = await import('../utils/url') as typeof import('../utils/url') & {
|
||||
openExternalUrl: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
describe('FeedbackDialog', () => {
|
||||
it('renders the instructional copy when open', () => {
|
||||
render(<FeedbackDialog open={true} onClose={vi.fn()} />)
|
||||
expect(screen.getByTestId('feedback-dialog')).toBeInTheDocument()
|
||||
expect(screen.getByText('Share feedback')).toBeInTheDocument()
|
||||
expect(screen.getByText(/best way to share product feedback/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/check whether a similar one already exists/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('focuses the primary CTA when opened', async () => {
|
||||
render(<FeedbackDialog open={true} onClose={vi.fn()} />)
|
||||
const cta = screen.getByRole('button', { name: 'Go to Issues' })
|
||||
await waitFor(() => expect(cta).toHaveFocus())
|
||||
})
|
||||
|
||||
it('opens GitHub Issues without closing the modal', async () => {
|
||||
const onClose = vi.fn()
|
||||
render(<FeedbackDialog open={true} onClose={onClose} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Go to Issues' }))
|
||||
|
||||
await waitFor(() => expect(openExternalUrl).toHaveBeenCalledWith(LAPUTA_GITHUB_ISSUES_URL))
|
||||
expect(onClose).not.toHaveBeenCalled()
|
||||
expect(screen.getByTestId('feedback-dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('closes when pressing Escape', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<FeedbackDialog open={true} onClose={onClose} />)
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
expect(onClose).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('closes when clicking Close', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<FeedbackDialog open={true} onClose={onClose} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
|
||||
expect(onClose).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
59
src/components/FeedbackDialog.tsx
Normal file
59
src/components/FeedbackDialog.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Megaphone } from '@phosphor-icons/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { LAPUTA_GITHUB_ISSUES_URL } from '../constants/feedback'
|
||||
import { openExternalUrl } from '../utils/url'
|
||||
|
||||
interface FeedbackDialogProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function FeedbackDialog({ open, onClose }: FeedbackDialogProps) {
|
||||
const handleOpenIssues = () => {
|
||||
void openExternalUrl(LAPUTA_GITHUB_ISSUES_URL)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(next) => { if (!next) onClose() }}>
|
||||
<DialogContent showCloseButton={false} className="sm:max-w-[460px]" data-testid="feedback-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Megaphone size={18} weight="duotone" />
|
||||
Share feedback
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
The best way to share product feedback is through a GitHub Issue.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 text-sm leading-6 text-muted-foreground">
|
||||
<p>
|
||||
Before opening a new issue, please check whether a similar one already exists.
|
||||
If it does, add an upvote or comment there instead of opening a duplicate.
|
||||
</p>
|
||||
<p>
|
||||
When you do open a new issue, include the steps to reproduce, what you expected,
|
||||
and what actually happened so it is easier to triage.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="sm:justify-between">
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button type="button" autoFocus onClick={handleOpenIssues}>
|
||||
Go to Issues
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -51,6 +51,19 @@ describe('StatusBar', () => {
|
||||
expect(screen.queryByText('main')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows Feedback button when callback is provided', () => {
|
||||
render(<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} onOpenFeedback={vi.fn()} />)
|
||||
expect(screen.getByTestId('status-feedback')).toBeInTheDocument()
|
||||
expect(screen.getByText('Feedback')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onOpenFeedback when Feedback is clicked', () => {
|
||||
const onOpenFeedback = vi.fn()
|
||||
render(<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} onOpenFeedback={onOpenFeedback} />)
|
||||
fireEvent.click(screen.getByTestId('status-feedback'))
|
||||
expect(onOpenFeedback).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows clickable commit hash that opens URL via openExternalUrl', () => {
|
||||
render(
|
||||
<StatusBar
|
||||
|
||||
@@ -34,6 +34,7 @@ interface StatusBarProps {
|
||||
onOpenConflictResolver?: () => void
|
||||
zoomLevel?: number
|
||||
onZoomReset?: () => void
|
||||
onOpenFeedback?: () => void
|
||||
buildNumber?: string
|
||||
onCheckForUpdates?: () => void
|
||||
onRemoveVault?: (path: string) => void
|
||||
@@ -67,6 +68,7 @@ export function StatusBar({
|
||||
onOpenConflictResolver,
|
||||
zoomLevel = 100,
|
||||
onZoomReset,
|
||||
onOpenFeedback,
|
||||
buildNumber,
|
||||
onCheckForUpdates,
|
||||
onRemoveVault,
|
||||
@@ -131,6 +133,7 @@ export function StatusBar({
|
||||
noteCount={noteCount}
|
||||
zoomLevel={zoomLevel}
|
||||
onZoomReset={onZoomReset}
|
||||
onOpenFeedback={onOpenFeedback}
|
||||
onOpenSettings={onOpenSettings}
|
||||
/>
|
||||
</footer>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Bell, FileText, Package, Settings } from 'lucide-react'
|
||||
import { Megaphone } from '@phosphor-icons/react'
|
||||
import type { ClaudeCodeStatus } from '../../hooks/useClaudeCodeStatus'
|
||||
import type { McpStatus } from '../../hooks/useMcpStatus'
|
||||
import type { GitRemoteStatus, LastCommitInfo, SyncStatus } from '../../types'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
ClaudeCodeBadge,
|
||||
CommitBadge,
|
||||
@@ -49,6 +51,7 @@ interface StatusBarSecondarySectionProps {
|
||||
noteCount: number
|
||||
zoomLevel: number
|
||||
onZoomReset?: () => void
|
||||
onOpenFeedback?: () => void
|
||||
onOpenSettings?: () => void
|
||||
}
|
||||
|
||||
@@ -127,6 +130,7 @@ export function StatusBarSecondarySection({
|
||||
noteCount,
|
||||
zoomLevel,
|
||||
onZoomReset,
|
||||
onOpenFeedback,
|
||||
onOpenSettings,
|
||||
}: StatusBarSecondarySectionProps) {
|
||||
return (
|
||||
@@ -148,6 +152,20 @@ export function StatusBarSecondarySection({
|
||||
{zoomLevel}%
|
||||
</span>
|
||||
)}
|
||||
{onOpenFeedback && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="h-6 px-2 text-[11px] font-medium text-muted-foreground hover:text-foreground"
|
||||
onClick={onOpenFeedback}
|
||||
title="Share feedback"
|
||||
data-testid="status-feedback"
|
||||
>
|
||||
<Megaphone size={14} />
|
||||
Feedback
|
||||
</Button>
|
||||
)}
|
||||
<span style={DISABLED_STYLE} title="Coming soon">
|
||||
<Bell size={14} />
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user