diff --git a/web/app/components/base/inline-delete-confirm/index.spec.tsx b/web/app/components/base/inline-delete-confirm/index.spec.tsx new file mode 100644 index 0000000000..c113c4ade9 --- /dev/null +++ b/web/app/components/base/inline-delete-confirm/index.spec.tsx @@ -0,0 +1,152 @@ +import React from 'react' +import { cleanup, fireEvent, render } from '@testing-library/react' +import InlineDeleteConfirm from './index' + +// Mock react-i18next +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'common.operation.deleteConfirmTitle': 'Delete?', + 'common.operation.yes': 'Yes', + 'common.operation.no': 'No', + 'common.operation.confirmAction': 'Please confirm your action.', + } + return translations[key] || key + }, + }), +})) + +afterEach(cleanup) + +describe('InlineDeleteConfirm', () => { + describe('Rendering', () => { + test('should render with default text', () => { + const onConfirm = jest.fn() + const onCancel = jest.fn() + const { getByText } = render( + , + ) + + expect(getByText('Delete?')).toBeInTheDocument() + expect(getByText('No')).toBeInTheDocument() + expect(getByText('Yes')).toBeInTheDocument() + }) + + test('should render with custom text', () => { + const onConfirm = jest.fn() + const onCancel = jest.fn() + const { getByText } = render( + , + ) + + expect(getByText('Remove?')).toBeInTheDocument() + expect(getByText('Cancel')).toBeInTheDocument() + expect(getByText('Confirm')).toBeInTheDocument() + }) + + test('should have proper ARIA attributes', () => { + const onConfirm = jest.fn() + const onCancel = jest.fn() + const { container } = render( + , + ) + + const wrapper = container.firstChild as HTMLElement + expect(wrapper).toHaveAttribute('aria-labelledby', 'inline-delete-confirm-title') + expect(wrapper).toHaveAttribute('aria-describedby', 'inline-delete-confirm-description') + }) + }) + + describe('Button interactions', () => { + test('should call onCancel when cancel button is clicked', () => { + const onConfirm = jest.fn() + const onCancel = jest.fn() + const { getByText } = render( + , + ) + + fireEvent.click(getByText('No')) + expect(onCancel).toHaveBeenCalledTimes(1) + expect(onConfirm).not.toHaveBeenCalled() + }) + + test('should call onConfirm when confirm button is clicked', () => { + const onConfirm = jest.fn() + const onCancel = jest.fn() + const { getByText } = render( + , + ) + + fireEvent.click(getByText('Yes')) + expect(onConfirm).toHaveBeenCalledTimes(1) + expect(onCancel).not.toHaveBeenCalled() + }) + }) + + describe('Variant prop', () => { + test('should render with delete variant by default', () => { + const onConfirm = jest.fn() + const onCancel = jest.fn() + const { getByText } = render( + , + ) + + const confirmButton = getByText('Yes').closest('button') + expect(confirmButton?.className).toContain('btn-destructive') + }) + + test('should render without destructive class for warning variant', () => { + const onConfirm = jest.fn() + const onCancel = jest.fn() + const { getByText } = render( + , + ) + + const confirmButton = getByText('Yes').closest('button') + expect(confirmButton?.className).not.toContain('btn-destructive') + }) + + test('should render without destructive class for info variant', () => { + const onConfirm = jest.fn() + const onCancel = jest.fn() + const { getByText } = render( + , + ) + + const confirmButton = getByText('Yes').closest('button') + expect(confirmButton?.className).not.toContain('btn-destructive') + }) + }) + + describe('Custom className', () => { + test('should apply custom className to wrapper', () => { + const onConfirm = jest.fn() + const onCancel = jest.fn() + const { container } = render( + , + ) + + const wrapper = container.firstChild as HTMLElement + expect(wrapper.className).toContain('custom-class') + }) + }) +}) diff --git a/web/app/components/base/inline-delete-confirm/index.tsx b/web/app/components/base/inline-delete-confirm/index.tsx new file mode 100644 index 0000000000..2a33e14701 --- /dev/null +++ b/web/app/components/base/inline-delete-confirm/index.tsx @@ -0,0 +1,90 @@ +'use client' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import cn from '@/utils/classnames' + +export type InlineDeleteConfirmProps = { + title?: string + confirmText?: string + cancelText?: string + onConfirm: () => void + onCancel: () => void + className?: string + variant?: 'delete' | 'warning' | 'info' +} + +const InlineDeleteConfirm: FC = ({ + title, + confirmText, + cancelText, + onConfirm, + onCancel, + className, + variant = 'delete', +}) => { + const { t } = useTranslation() + + const titleText = title || t('common.operation.deleteConfirmTitle', 'Delete?') + const confirmTxt = confirmText || t('common.operation.yes', 'Yes') + const cancelTxt = cancelText || t('common.operation.no', 'No') + + return ( +
+
+
+ {titleText} +
+ +
+ + +
+
+ + + {t('common.operation.confirmAction', 'Please confirm your action.')} + +
+ ) +} + +InlineDeleteConfirm.displayName = 'InlineDeleteConfirm' + +export default InlineDeleteConfirm diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index b9d315388f..4f177ca0c3 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -18,6 +18,10 @@ const translation = { cancel: 'Cancel', clear: 'Clear', save: 'Save', + yes: 'Yes', + no: 'No', + deleteConfirmTitle: 'Delete?', + confirmAction: 'Please confirm your action.', saveAndEnable: 'Save & Enable', edit: 'Edit', add: 'Add', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index 5526ac0441..52545c460b 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -67,6 +67,10 @@ const translation = { selectAll: 'すべて選択', deSelectAll: 'すべて選択解除', config: 'コンフィグ', + yes: 'はい', + no: 'いいえ', + deleteConfirmTitle: '削除しますか?', + confirmAction: '操作を確認してください。', }, errorMsg: { fieldRequired: '{{field}}は必要です', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 0ecdb20d5e..e73ac4cc6b 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -18,6 +18,10 @@ const translation = { cancel: '取消', clear: '清空', save: '保存', + yes: '是', + no: '否', + deleteConfirmTitle: '删除?', + confirmAction: '请确认您的操作。', saveAndEnable: '保存并启用', edit: '编辑', add: '添加',