mirror of
https://github.com/langgenius/dify.git
synced 2026-04-20 18:57:19 +08:00
refactor(web): tighten button tone typing
This commit is contained in:
@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import type { ButtonProps } from '@/app/components/base/ui/button'
|
||||
import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import type { HumanInputFormError } from '@/service/use-share'
|
||||
@ -263,7 +262,7 @@ const FormContent = () => {
|
||||
<Button
|
||||
key={action.id}
|
||||
disabled={isSubmitting}
|
||||
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
|
||||
variant={getButtonStyle(action.button_style)}
|
||||
onClick={() => submit(action.id)}
|
||||
>
|
||||
{action.title}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import type { HumanInputFormProps } from './type'
|
||||
import type { ButtonProps } from '@/app/components/base/ui/button'
|
||||
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
@ -47,7 +46,7 @@ const HumanInputForm = ({
|
||||
<Button
|
||||
key={action.id}
|
||||
disabled={isSubmitting}
|
||||
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
|
||||
variant={getButtonStyle(action.button_style)}
|
||||
onClick={() => submit(formToken, action.id, inputs)}
|
||||
data-testid="action-button"
|
||||
>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ButtonVariant } from '@/app/components/base/ui/button'
|
||||
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import dayjs from 'dayjs'
|
||||
@ -14,7 +15,7 @@ dayjs.extend(utc)
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(isSameOrAfter)
|
||||
|
||||
export const getButtonStyle = (style: UserActionButtonType) => {
|
||||
export const getButtonStyle = (style: UserActionButtonType): ButtonVariant | undefined => {
|
||||
if (style === UserActionButtonType.Primary)
|
||||
return 'primary'
|
||||
if (style === UserActionButtonType.Default)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import type { ButtonProps } from '@/app/components/base/ui/button'
|
||||
import type { ButtonSize, ButtonVariant } from '@/app/components/base/ui/button'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useChatContext } from '@/app/components/base/chat/chat/context'
|
||||
@ -9,7 +9,7 @@ import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
|
||||
import { formatDateForOutput, toDayjs } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { Button } from '@/app/components/base/ui/button'
|
||||
import { Button, isButtonSize, isButtonVariant } from '@/app/components/base/ui/button'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select'
|
||||
|
||||
enum DATA_FORMAT {
|
||||
@ -48,17 +48,6 @@ function isSafeName(name: unknown): name is string {
|
||||
&& !PROTOTYPE_POISON_KEYS.has(name)
|
||||
}
|
||||
|
||||
const VALID_BUTTON_VARIANTS = new Set<string>([
|
||||
'primary',
|
||||
'warning',
|
||||
'secondary',
|
||||
'secondary-accent',
|
||||
'ghost',
|
||||
'ghost-accent',
|
||||
'tertiary',
|
||||
])
|
||||
const VALID_BUTTON_SIZES = new Set<string>(['small', 'medium', 'large'])
|
||||
|
||||
type HastText = {
|
||||
type: 'text'
|
||||
value: string
|
||||
@ -364,12 +353,8 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
|
||||
if (child.tagName === SUPPORTED_TAGS.BUTTON) {
|
||||
const rawVariant = str(child.properties.dataVariant)
|
||||
const rawSize = str(child.properties.dataSize)
|
||||
const variant = VALID_BUTTON_VARIANTS.has(rawVariant)
|
||||
? rawVariant as ButtonProps['variant']
|
||||
: undefined
|
||||
const size = VALID_BUTTON_SIZES.has(rawSize)
|
||||
? rawSize as ButtonProps['size']
|
||||
: undefined
|
||||
const variant: ButtonVariant | undefined = isButtonVariant(rawVariant) ? rawVariant : undefined
|
||||
const size: ButtonSize | undefined = isButtonSize(rawSize) ? rawSize : undefined
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@ -62,8 +62,9 @@ export function AlertDialogActions({ className, ...props }: AlertDialogActionsPr
|
||||
)
|
||||
}
|
||||
|
||||
type AlertDialogCancelButtonProps = Omit<ButtonProps, 'children'> & {
|
||||
type AlertDialogCancelButtonProps = Omit<ButtonProps, 'children' | 'tone'> & {
|
||||
children: React.ReactNode
|
||||
tone?: 'default'
|
||||
closeProps?: Omit<React.ComponentPropsWithoutRef<typeof BaseAlertDialog.Close>, 'children' | 'render'>
|
||||
}
|
||||
|
||||
@ -85,14 +86,17 @@ export function AlertDialogCancelButton({
|
||||
type AlertDialogConfirmButtonProps = ButtonProps
|
||||
|
||||
export function AlertDialogConfirmButton({
|
||||
variant = 'primary',
|
||||
tone = 'destructive',
|
||||
variant,
|
||||
tone,
|
||||
...props
|
||||
}: AlertDialogConfirmButtonProps) {
|
||||
if (variant === 'secondary-accent' || variant === 'ghost-accent')
|
||||
return <Button variant={variant} tone="default" {...props} />
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={variant}
|
||||
tone={tone}
|
||||
tone={tone ?? 'destructive'}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ButtonProps } from '../index'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { Button } from '../index'
|
||||
|
||||
@ -60,6 +61,12 @@ describe('Button', () => {
|
||||
render(<Button variant="primary" tone="destructive">Click me</Button>)
|
||||
expect(screen.getByRole('button').className).toContain('btn-destructive-primary')
|
||||
})
|
||||
|
||||
it('keeps accent variants on the default tone path', () => {
|
||||
render(<Button variant="secondary-accent">Click me</Button>)
|
||||
expect(screen.getByRole('button').className).toContain('btn-secondary-accent')
|
||||
expect(screen.getByRole('button').className).not.toContain('btn-destructive')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sizes', () => {
|
||||
@ -152,3 +159,9 @@ describe('Button', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// @ts-expect-error accent variants do not support destructive tone
|
||||
const _invalidSecondaryAccentTone: ButtonProps = { variant: 'secondary-accent', tone: 'destructive' }
|
||||
|
||||
// @ts-expect-error accent variants do not support destructive tone
|
||||
const _invalidGhostAccentTone: ButtonProps = { variant: 'ghost-accent', tone: 'destructive' }
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
|
||||
import { Button } from '.'
|
||||
import { Button, buttonTones, buttonVariantsList } from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/General/Button',
|
||||
@ -13,12 +13,13 @@ const meta = {
|
||||
loading: { control: 'boolean' },
|
||||
tone: {
|
||||
control: 'select',
|
||||
options: ['default', 'destructive'],
|
||||
options: buttonTones,
|
||||
description: 'Destructive tone is only supported by primary, secondary, tertiary, and ghost variants.',
|
||||
},
|
||||
disabled: { control: 'boolean' },
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'secondary-accent', 'ghost', 'ghost-accent', 'tertiary'],
|
||||
options: buttonVariantsList,
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
@ -53,6 +54,13 @@ export const SecondaryAccent: Story = {
|
||||
variant: 'secondary-accent',
|
||||
children: 'Secondary Accent Button',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Accent variants only support the default tone.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Ghost: Story = {
|
||||
@ -67,6 +75,13 @@ export const GhostAccent: Story = {
|
||||
variant: 'ghost-accent',
|
||||
children: 'Ghost Accent Button',
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Accent variants only support the default tone.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Tertiary: Story = {
|
||||
|
||||
@ -1,9 +1,29 @@
|
||||
import type { Button as BaseButtonNS } from '@base-ui/react/button'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { Button as BaseButton } from '@base-ui/react/button'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const destructiveToneVariants = ['primary', 'secondary', 'tertiary', 'ghost'] as const
|
||||
const accentVariants = ['secondary-accent', 'ghost-accent'] as const
|
||||
export const buttonVariantsList = [...destructiveToneVariants, ...accentVariants] as const
|
||||
export const buttonTones = ['default', 'destructive'] as const
|
||||
const buttonVariantSet = new Set<string>(buttonVariantsList)
|
||||
const buttonSizeSet = new Set<string>(['small', 'medium', 'large'])
|
||||
|
||||
type DestructiveToneVariant = typeof destructiveToneVariants[number]
|
||||
type AccentVariant = typeof accentVariants[number]
|
||||
export type ButtonVariant = typeof buttonVariantsList[number]
|
||||
export type ButtonSize = 'small' | 'medium' | 'large'
|
||||
type ButtonTone = typeof buttonTones[number]
|
||||
|
||||
export const isButtonVariant = (value: unknown): value is ButtonVariant => {
|
||||
return typeof value === 'string' && buttonVariantSet.has(value)
|
||||
}
|
||||
|
||||
export const isButtonSize = (value: unknown): value is ButtonSize => {
|
||||
return typeof value === 'string' && buttonSizeSet.has(value)
|
||||
}
|
||||
|
||||
const buttonVariants = cva(
|
||||
'btn',
|
||||
{
|
||||
@ -40,12 +60,30 @@ const buttonVariants = cva(
|
||||
},
|
||||
)
|
||||
|
||||
export type ButtonProps
|
||||
= Omit<BaseButtonNS.Props, 'className'>
|
||||
& VariantProps<typeof buttonVariants> & {
|
||||
loading?: boolean
|
||||
className?: string
|
||||
}
|
||||
type BaseButtonProps = Omit<BaseButtonNS.Props, 'className'> & {
|
||||
size?: ButtonSize
|
||||
className?: string
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
type DestructiveToneButtonProps = BaseButtonProps & {
|
||||
variant?: DestructiveToneVariant
|
||||
tone?: ButtonTone
|
||||
}
|
||||
|
||||
type AccentButtonProps = BaseButtonProps & {
|
||||
variant: AccentVariant
|
||||
tone?: 'default'
|
||||
}
|
||||
|
||||
export type ButtonProps = DestructiveToneButtonProps | AccentButtonProps
|
||||
|
||||
const resolveTone = (variant: ButtonVariant | undefined, tone: ButtonTone | undefined) => {
|
||||
if (variant === 'secondary-accent' || variant === 'ghost-accent')
|
||||
return 'default'
|
||||
|
||||
return tone
|
||||
}
|
||||
|
||||
export function Button({
|
||||
className,
|
||||
@ -61,7 +99,7 @@ export function Button({
|
||||
return (
|
||||
<BaseButton
|
||||
type={type}
|
||||
className={cn(buttonVariants({ variant, size, tone, className }))}
|
||||
className={cn(buttonVariants({ variant, size, tone: resolveTone(variant, tone), className }))}
|
||||
disabled={disabled || loading}
|
||||
aria-busy={loading || undefined}
|
||||
{...props}
|
||||
|
||||
@ -8,7 +8,7 @@ import { TaskStatus } from '@/app/components/plugins/types'
|
||||
import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & {
|
||||
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading' | 'variant' | 'tone'> & {
|
||||
uniqueIdentifier: string
|
||||
extraIdentifiers?: string[]
|
||||
onSuccess?: () => void
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { FormInputItem, UserAction } from '../types'
|
||||
import type { ButtonProps } from '@/app/components/base/ui/button'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
@ -86,7 +85,7 @@ const FormContentPreview: FC<FormContentPreviewProps> = ({
|
||||
{userActions.map((action: UserAction) => (
|
||||
<Button
|
||||
key={action.id}
|
||||
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
|
||||
variant={getButtonStyle(action.button_style)}
|
||||
>
|
||||
{action.title}
|
||||
</Button>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
import type { ButtonProps } from '@/app/components/base/ui/button'
|
||||
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { HumanInputFormData } from '@/types/workflow'
|
||||
import { RiArrowLeftLine } from '@remixicon/react'
|
||||
@ -72,7 +71,7 @@ const FormContent = ({
|
||||
<Button
|
||||
key={action.id}
|
||||
disabled={isSubmitting}
|
||||
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
|
||||
variant={getButtonStyle(action.button_style)}
|
||||
onClick={() => submit(action.id)}
|
||||
>
|
||||
{action.title}
|
||||
|
||||
Reference in New Issue
Block a user