mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
## Problem Our `<Button>` component breaks the default `button` contract by redefining the `type` prop to set its variant (`primary`, `default`, etc) instead of the button type (`submit`, `button`, etc). This is confusing and forces to write more code when using it with shadcn components that expect/inject the standard button props. ## Solution - rename the `type` prop to `variant` - rename the `htmlType` prop to `type` - propagate the changes where necessary - format code ## How to test As this is just prop renaming, if it builds it's ok --------- Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
214 lines
5.7 KiB
TypeScript
214 lines
5.7 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { Pencil, ThumbsDown, ThumbsUp, Trash2 } from 'lucide-react'
|
|
import { useEffect, useState, type PropsWithChildren } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import {
|
|
Button,
|
|
cn,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
TextArea,
|
|
} from 'ui'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import * as z from 'zod'
|
|
|
|
import { ButtonTooltip } from '../ButtonTooltip'
|
|
|
|
export function MessageActions({
|
|
children,
|
|
alwaysShow = false,
|
|
}: PropsWithChildren<{ alwaysShow?: boolean }>) {
|
|
return (
|
|
<div className="flex items-center gap-4 mt-2 mb-1">
|
|
<span className="h-0.5 w-5 bg-muted" />
|
|
<div className={cn('group-hover:opacity-100 transition-opacity', !alwaysShow && 'opacity-0')}>
|
|
{children}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
function MessageActionsEdit({ onClick, tooltip }: { onClick: () => void; tooltip: string }) {
|
|
return (
|
|
<ButtonTooltip
|
|
variant="text"
|
|
icon={<Pencil size={14} strokeWidth={1.5} />}
|
|
onClick={onClick}
|
|
className="text-foreground-light hover:text-foreground p-1 rounded-sm"
|
|
aria-label={tooltip}
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
text: tooltip,
|
|
},
|
|
}}
|
|
/>
|
|
)
|
|
}
|
|
MessageActions.Edit = MessageActionsEdit
|
|
|
|
function MessageActionsDelete({ onClick }: { onClick: () => void }) {
|
|
return (
|
|
<ButtonTooltip
|
|
variant="text"
|
|
icon={<Trash2 size={14} strokeWidth={1.5} />}
|
|
tooltip={{ content: { side: 'bottom', text: 'Delete message' } }}
|
|
onClick={onClick}
|
|
className="text-foreground-light hover:text-foreground p-1 rounded-sm"
|
|
title="Delete message"
|
|
aria-label="Delete message"
|
|
/>
|
|
)
|
|
}
|
|
MessageActions.Delete = MessageActionsDelete
|
|
|
|
function MessageActionsThumbsUp({
|
|
onClick,
|
|
isActive,
|
|
disabled,
|
|
}: {
|
|
onClick: () => void
|
|
isActive?: boolean
|
|
disabled?: boolean
|
|
}) {
|
|
return (
|
|
<Button
|
|
variant="text"
|
|
disabled={disabled}
|
|
icon={
|
|
<ThumbsUp
|
|
size={14}
|
|
strokeWidth={1.5}
|
|
className={cn(
|
|
isActive
|
|
? 'text-brand hover:text-brand-700'
|
|
: 'text-foreground-light hover:text-foreground'
|
|
)}
|
|
/>
|
|
}
|
|
onClick={onClick}
|
|
className={cn(
|
|
'p-1 rounded-sm transition-colors',
|
|
disabled && 'opacity-50 pointer-events-none'
|
|
)}
|
|
title="Good response"
|
|
aria-label="Good response"
|
|
/>
|
|
)
|
|
}
|
|
MessageActions.ThumbsUp = MessageActionsThumbsUp
|
|
|
|
const feedbackSchema = z.object({
|
|
reason: z.string().optional(),
|
|
})
|
|
|
|
type FeedbackFormValues = z.infer<typeof feedbackSchema>
|
|
|
|
function MessageActionsThumbsDown({
|
|
onClick,
|
|
isActive,
|
|
disabled,
|
|
}: {
|
|
onClick: (reason?: string) => void
|
|
isActive?: boolean
|
|
disabled?: boolean
|
|
}) {
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const form = useForm<FeedbackFormValues>({
|
|
resolver: zodResolver(feedbackSchema),
|
|
defaultValues: { reason: '' },
|
|
mode: 'onSubmit',
|
|
})
|
|
|
|
const handleOpenChange = (newOpen: boolean) => {
|
|
if (disabled) return
|
|
// When popover closes, submit the rating if not already submitted
|
|
if (!newOpen && open && !form.formState.isSubmitSuccessful) {
|
|
onClick()
|
|
}
|
|
setOpen(newOpen)
|
|
if (!newOpen) {
|
|
form.reset()
|
|
}
|
|
}
|
|
|
|
const onSubmit = (values: FeedbackFormValues) => {
|
|
onClick(values.reason || undefined)
|
|
}
|
|
|
|
// Auto-close popover after showing thank you message
|
|
useEffect(() => {
|
|
if (form.formState.isSubmitSuccessful) {
|
|
const timer = setTimeout(() => {
|
|
setOpen(false)
|
|
}, 2000)
|
|
return () => clearTimeout(timer)
|
|
}
|
|
}, [form.formState.isSubmitSuccessful])
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={handleOpenChange}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="text"
|
|
disabled={disabled}
|
|
onClick={() => !disabled && setOpen(true)}
|
|
className={cn(
|
|
'p-1 rounded-sm transition-colors',
|
|
disabled && 'opacity-50 pointer-events-none'
|
|
)}
|
|
title="Bad response"
|
|
aria-label="Bad response"
|
|
>
|
|
<ThumbsDown
|
|
size={14}
|
|
strokeWidth={1.5}
|
|
className={cn(
|
|
isActive
|
|
? 'text-warning hover:text-warning-700'
|
|
: 'text-foreground-light hover:text-foreground'
|
|
)}
|
|
/>
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-80" align="start">
|
|
{form.formState.isSubmitSuccessful ? (
|
|
<p className="text-sm">We appreciate your feedback!</p>
|
|
) : (
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
|
|
<FormField
|
|
control={form.control}
|
|
name="reason"
|
|
render={({ field }) => (
|
|
<FormItemLayout label="What went wrong?" labelOptional="optional">
|
|
<FormControl>
|
|
<TextArea
|
|
placeholder="Describe why the response was not helpful..."
|
|
autoComplete="off"
|
|
rows={4}
|
|
autoFocus
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
<div className="flex justify-end">
|
|
<Button variant="primary" type="submit" size="tiny">
|
|
Submit feedback
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
)}
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|
|
MessageActions.ThumbsDown = MessageActionsThumbsDown
|