dify
This commit is contained in:
216
dify/web/app/components/base/confirm/index.stories.tsx
Normal file
216
dify/web/app/components/base/confirm/index.stories.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs'
|
||||
import { useState } from 'react'
|
||||
import Confirm from '.'
|
||||
import Button from '../button'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Feedback/Confirm',
|
||||
component: Confirm,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Confirmation dialog component that supports warning and info types, with customizable button text and behavior.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['info', 'warning'],
|
||||
description: 'Dialog type',
|
||||
},
|
||||
isShow: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show the dialog',
|
||||
},
|
||||
title: {
|
||||
control: 'text',
|
||||
description: 'Dialog title',
|
||||
},
|
||||
content: {
|
||||
control: 'text',
|
||||
description: 'Dialog content',
|
||||
},
|
||||
confirmText: {
|
||||
control: 'text',
|
||||
description: 'Confirm button text',
|
||||
},
|
||||
cancelText: {
|
||||
control: 'text',
|
||||
description: 'Cancel button text',
|
||||
},
|
||||
isLoading: {
|
||||
control: 'boolean',
|
||||
description: 'Confirm button loading state',
|
||||
},
|
||||
isDisabled: {
|
||||
control: 'boolean',
|
||||
description: 'Confirm button disabled state',
|
||||
},
|
||||
showConfirm: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show confirm button',
|
||||
},
|
||||
showCancel: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to show cancel button',
|
||||
},
|
||||
maskClosable: {
|
||||
control: 'boolean',
|
||||
description: 'Whether clicking mask closes dialog',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
onConfirm: () => {
|
||||
console.log('✅ User clicked confirm')
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('❌ User clicked cancel')
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Confirm>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Interactive demo wrapper
|
||||
const ConfirmDemo = (args: any) => {
|
||||
const [isShow, setIsShow] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button variant="primary" onClick={() => setIsShow(true)}>
|
||||
Open Dialog
|
||||
</Button>
|
||||
<Confirm
|
||||
{...args}
|
||||
isShow={isShow}
|
||||
onConfirm={() => {
|
||||
console.log('✅ User clicked confirm')
|
||||
setIsShow(false)
|
||||
}}
|
||||
onCancel={() => {
|
||||
console.log('❌ User clicked cancel')
|
||||
setIsShow(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Basic warning dialog - Delete action
|
||||
export const WarningDialog: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Delete Confirmation',
|
||||
content: 'Are you sure you want to delete this project? This action cannot be undone.',
|
||||
isShow: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Info dialog
|
||||
export const InfoDialog: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'info',
|
||||
title: 'Notice',
|
||||
content: 'Your changes have been saved. Do you want to proceed to the next step?',
|
||||
isShow: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Custom button text
|
||||
export const CustomButtonText: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Exit Editor',
|
||||
content: 'You have unsaved changes. Are you sure you want to exit?',
|
||||
confirmText: 'Discard Changes',
|
||||
cancelText: 'Continue Editing',
|
||||
isShow: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Loading state
|
||||
export const LoadingState: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Deleting...',
|
||||
content: 'Please wait while we delete the file...',
|
||||
isLoading: true,
|
||||
isShow: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Disabled state
|
||||
export const DisabledState: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'info',
|
||||
title: 'Verification Required',
|
||||
content: 'Please complete email verification before proceeding.',
|
||||
isDisabled: true,
|
||||
isShow: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Alert style - Confirm button only
|
||||
export const AlertStyle: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'info',
|
||||
title: 'Success',
|
||||
content: 'Your settings have been updated!',
|
||||
showCancel: false,
|
||||
confirmText: 'Got it',
|
||||
isShow: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Dangerous action - Long content
|
||||
export const DangerousAction: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Permanently Delete Account',
|
||||
content: 'This action will permanently delete your account and all associated data, including: all projects and files, collaboration history, and personal settings. This action cannot be reversed!',
|
||||
confirmText: 'Delete My Account',
|
||||
cancelText: 'Keep My Account',
|
||||
isShow: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Non-closable mask
|
||||
export const NotMaskClosable: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'Important Action',
|
||||
content: 'This action requires your explicit choice. Clicking outside will not close this dialog.',
|
||||
maskClosable: false,
|
||||
isShow: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Full feature demo - Playground
|
||||
export const Playground: Story = {
|
||||
render: args => <ConfirmDemo {...args} />,
|
||||
args: {
|
||||
type: 'warning',
|
||||
title: 'This is a title',
|
||||
content: 'This is the dialog content text...',
|
||||
confirmText: undefined,
|
||||
cancelText: undefined,
|
||||
isLoading: false,
|
||||
isDisabled: false,
|
||||
showConfirm: true,
|
||||
showCancel: true,
|
||||
maskClosable: true,
|
||||
isShow: false,
|
||||
},
|
||||
}
|
||||
128
dify/web/app/components/base/confirm/index.tsx
Normal file
128
dify/web/app/components/base/confirm/index.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../button'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
export type IConfirm = {
|
||||
className?: string
|
||||
isShow: boolean
|
||||
type?: 'info' | 'warning' | 'danger'
|
||||
title: string
|
||||
content?: React.ReactNode
|
||||
confirmText?: string | null
|
||||
onConfirm: () => void
|
||||
cancelText?: string
|
||||
onCancel: () => void
|
||||
isLoading?: boolean
|
||||
isDisabled?: boolean
|
||||
showConfirm?: boolean
|
||||
showCancel?: boolean
|
||||
maskClosable?: boolean
|
||||
}
|
||||
|
||||
function Confirm({
|
||||
isShow,
|
||||
type = 'warning',
|
||||
title,
|
||||
content,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
showConfirm = true,
|
||||
showCancel = true,
|
||||
isLoading = false,
|
||||
isDisabled = false,
|
||||
maskClosable = true,
|
||||
}: IConfirm) {
|
||||
const { t } = useTranslation()
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
const titleRef = useRef<HTMLDivElement>(null)
|
||||
const [isVisible, setIsVisible] = useState(isShow)
|
||||
const [isTitleTruncated, setIsTitleTruncated] = useState(false)
|
||||
|
||||
const confirmTxt = confirmText || `${t('common.operation.confirm')}`
|
||||
const cancelTxt = cancelText || `${t('common.operation.cancel')}`
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape')
|
||||
onCancel()
|
||||
if (event.key === 'Enter' && isShow) {
|
||||
event.preventDefault()
|
||||
onConfirm()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [onCancel, onConfirm, isShow])
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (maskClosable && dialogRef.current && !dialogRef.current.contains(event.target as Node))
|
||||
onCancel()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [maskClosable])
|
||||
|
||||
useEffect(() => {
|
||||
if (isShow) {
|
||||
setIsVisible(true)
|
||||
}
|
||||
else {
|
||||
const timer = setTimeout(() => setIsVisible(false), 200)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isShow])
|
||||
|
||||
useEffect(() => {
|
||||
if (titleRef.current) {
|
||||
const isOverflowing = titleRef.current.scrollWidth > titleRef.current.clientWidth
|
||||
setIsTitleTruncated(isOverflowing)
|
||||
}
|
||||
}, [title, isVisible])
|
||||
|
||||
if (!isVisible)
|
||||
return null
|
||||
|
||||
return createPortal(
|
||||
<div className={'fixed inset-0 z-[10000000] flex items-center justify-center bg-background-overlay'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}>
|
||||
<div ref={dialogRef} className={'relative w-full max-w-[480px] overflow-hidden'}>
|
||||
<div className='shadows-shadow-lg flex max-w-full flex-col items-start rounded-2xl border-[0.5px] border-solid border-components-panel-border bg-components-panel-bg'>
|
||||
<div className='flex flex-col items-start gap-2 self-stretch pb-4 pl-6 pr-6 pt-6'>
|
||||
<Tooltip
|
||||
popupContent={title}
|
||||
disabled={!isTitleTruncated}
|
||||
portalContentClassName='!z-[10000001]'
|
||||
asChild={false}
|
||||
triggerClassName='w-full'
|
||||
>
|
||||
<div ref={titleRef} className='title-2xl-semi-bold w-full truncate text-text-primary'>
|
||||
{title}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className='system-md-regular w-full whitespace-pre-wrap break-words text-text-tertiary'>{content}</div>
|
||||
</div>
|
||||
<div className='flex items-start justify-end gap-2 self-stretch p-6'>
|
||||
{showCancel && <Button onClick={onCancel}>{cancelTxt}</Button>}
|
||||
{showConfirm && <Button variant={'primary'} destructive={type !== 'info'} loading={isLoading} disabled={isDisabled} onClick={onConfirm}>{confirmTxt}</Button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>, document.body,
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Confirm)
|
||||
Reference in New Issue
Block a user