{textAreaContent}
{/* footer */}
{!readonly && (
diff --git a/web/app/components/base/button/add-button.tsx b/web/app/components/base/button/add-button.tsx
index 420b668141..cecc9ec063 100644
--- a/web/app/components/base/button/add-button.tsx
+++ b/web/app/components/base/button/add-button.tsx
@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { RiAddLine } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type Props = {
className?: string
diff --git a/web/app/components/base/button/index.spec.tsx b/web/app/components/base/button/index.spec.tsx
index 9da2620cd4..1f3dbaf652 100644
--- a/web/app/components/base/button/index.spec.tsx
+++ b/web/app/components/base/button/index.spec.tsx
@@ -101,7 +101,7 @@ describe('Button', () => {
describe('Button events', () => {
test('onClick should been call after clicked', async () => {
- const onClick = jest.fn()
+ const onClick = vi.fn()
const { getByRole } = render(
)
fireEvent.click(getByRole('button'))
expect(onClick).toHaveBeenCalled()
diff --git a/web/app/components/base/button/index.tsx b/web/app/components/base/button/index.tsx
index 4f75aec5a5..cb0d0c1fd9 100644
--- a/web/app/components/base/button/index.tsx
+++ b/web/app/components/base/button/index.tsx
@@ -2,7 +2,7 @@ import type { CSSProperties } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import Spinner from '../spinner'
-import classNames from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
const buttonVariants = cva(
'btn disabled:btn-disabled',
@@ -42,16 +42,14 @@ const Button = ({ className, variant, size, destructive, loading, styleCss, chil
return (
)
}
diff --git a/web/app/components/base/button/sync-button.tsx b/web/app/components/base/button/sync-button.tsx
index 013c86889a..a9d4d1022f 100644
--- a/web/app/components/base/button/sync-button.tsx
+++ b/web/app/components/base/button/sync-button.tsx
@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { RiRefreshLine } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import TooltipPlus from '@/app/components/base/tooltip'
type Props = {
diff --git a/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap b/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap
index 4ffcfa31e9..5a61d9204d 100644
--- a/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap
+++ b/web/app/components/base/chat/__tests__/__snapshots__/utils.spec.ts.snap
@@ -1,6 +1,6 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`build chat item tree and get thread messages should get thread messages from tree6, using specified message as target 1`] = `
+exports[`build chat item tree and get thread messages > should get thread messages from tree6, using specified message as target 1`] = `
[
{
"children": [
@@ -834,7 +834,7 @@ exports[`build chat item tree and get thread messages should get thread messages
]
`;
-exports[`build chat item tree and get thread messages should get thread messages from tree6, using the last message as target 1`] = `
+exports[`build chat item tree and get thread messages > should get thread messages from tree6, using the last message as target 1`] = `
[
{
"children": [
@@ -1804,7 +1804,7 @@ exports[`build chat item tree and get thread messages should get thread messages
]
`;
-exports[`build chat item tree and get thread messages should work with partial messages 1 1`] = `
+exports[`build chat item tree and get thread messages > should work with partial messages 1 1`] = `
[
{
"children": [
@@ -2155,7 +2155,7 @@ exports[`build chat item tree and get thread messages should work with partial m
]
`;
-exports[`build chat item tree and get thread messages should work with partial messages 2 1`] = `
+exports[`build chat item tree and get thread messages > should work with partial messages 2 1`] = `
[
{
"children": [
@@ -2327,7 +2327,7 @@ exports[`build chat item tree and get thread messages should work with partial m
]
`;
-exports[`build chat item tree and get thread messages should work with real world messages 1`] = `
+exports[`build chat item tree and get thread messages > should work with real world messages 1`] = `
[
{
"children": [
diff --git a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
index ab133d67af..535d7e19bf 100644
--- a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
+++ b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
@@ -20,7 +20,7 @@ import AppIcon from '@/app/components/base/app-icon'
import AnswerIcon from '@/app/components/base/answer-icon'
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types'
import { formatBooleanInputs } from '@/utils/model-config'
import Avatar from '../../avatar'
diff --git a/web/app/components/base/chat/chat-with-history/header/index.tsx b/web/app/components/base/chat/chat-with-history/header/index.tsx
index b5c5bccec1..f63c97603b 100644
--- a/web/app/components/base/chat/chat-with-history/header/index.tsx
+++ b/web/app/components/base/chat/chat-with-history/header/index.tsx
@@ -16,7 +16,7 @@ import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/input
import Confirm from '@/app/components/base/confirm'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import type { ConversationItem } from '@/models/share'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
const Header = () => {
const {
diff --git a/web/app/components/base/chat/chat-with-history/header/operation.tsx b/web/app/components/base/chat/chat-with-history/header/operation.tsx
index 0923d712fa..9549e9da26 100644
--- a/web/app/components/base/chat/chat-with-history/header/operation.tsx
+++ b/web/app/components/base/chat/chat-with-history/header/operation.tsx
@@ -7,7 +7,7 @@ import {
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type Props = {
title: string
diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx
index 6953be4b3c..51ba88b049 100644
--- a/web/app/components/base/chat/chat-with-history/index.tsx
+++ b/web/app/components/base/chat/chat-with-history/index.tsx
@@ -17,7 +17,7 @@ import ChatWrapper from './chat-wrapper'
import type { InstalledApp } from '@/models/explore'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
type ChatWithHistoryProps = {
diff --git a/web/app/components/base/chat/chat-with-history/inputs-form/index.tsx b/web/app/components/base/chat/chat-with-history/inputs-form/index.tsx
index 3a1b92089c..643ca1a808 100644
--- a/web/app/components/base/chat/chat-with-history/inputs-form/index.tsx
+++ b/web/app/components/base/chat/chat-with-history/inputs-form/index.tsx
@@ -5,7 +5,7 @@ import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
import { useChatWithHistoryContext } from '../context'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type Props = {
collapsed: boolean
diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
index c6a7063d80..c5f2afd425 100644
--- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
+++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx
@@ -18,7 +18,7 @@ import Confirm from '@/app/components/base/confirm'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import type { ConversationItem } from '@/models/share'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = {
diff --git a/web/app/components/base/chat/chat-with-history/sidebar/item.tsx b/web/app/components/base/chat/chat-with-history/sidebar/item.tsx
index ea17f3f3ea..cd181fd7eb 100644
--- a/web/app/components/base/chat/chat-with-history/sidebar/item.tsx
+++ b/web/app/components/base/chat/chat-with-history/sidebar/item.tsx
@@ -6,7 +6,7 @@ import {
import { useHover } from 'ahooks'
import type { ConversationItem } from '@/models/share'
import Operation from '@/app/components/base/chat/chat-with-history/sidebar/operation'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type ItemProps = {
isPin?: boolean
diff --git a/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx b/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx
index 19d2aa2cbf..9c4ea6ffb1 100644
--- a/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx
+++ b/web/app/components/base/chat/chat-with-history/sidebar/operation.tsx
@@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type Props = {
isActive?: boolean
diff --git a/web/app/components/base/chat/chat/answer/basic-content.tsx b/web/app/components/base/chat/chat/answer/basic-content.tsx
index 6c8a44cf52..cb3791650a 100644
--- a/web/app/components/base/chat/chat/answer/basic-content.tsx
+++ b/web/app/components/base/chat/chat/answer/basic-content.tsx
@@ -2,7 +2,7 @@ import type { FC } from 'react'
import { memo } from 'react'
import type { ChatItem } from '../../types'
import { Markdown } from '@/app/components/base/markdown'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type BasicContentProps = {
item: ChatItem
diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx
index a1b458ba9a..fb5b91054f 100644
--- a/web/app/components/base/chat/chat/answer/index.tsx
+++ b/web/app/components/base/chat/chat/answer/index.tsx
@@ -19,7 +19,7 @@ import Citation from '@/app/components/base/chat/chat/citation'
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
import type { AppData } from '@/models/share'
import AnswerIcon from '@/app/components/base/answer-icon'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { FileList } from '@/app/components/base/file-uploader'
import ContentSwitch from '../content-switch'
diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx
index fca0ae5cae..d068d3e108 100644
--- a/web/app/components/base/chat/chat/answer/operation.tsx
+++ b/web/app/components/base/chat/chat/answer/operation.tsx
@@ -26,7 +26,7 @@ import NewAudioButton from '@/app/components/base/new-audio-button'
import Modal from '@/app/components/base/modal/modal'
import Textarea from '@/app/components/base/textarea'
import Tooltip from '@/app/components/base/tooltip'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type OperationProps = {
item: ChatItem
diff --git a/web/app/components/base/chat/chat/answer/tool-detail.tsx b/web/app/components/base/chat/chat/answer/tool-detail.tsx
index 26d1b3bbef..6e6710e053 100644
--- a/web/app/components/base/chat/chat/answer/tool-detail.tsx
+++ b/web/app/components/base/chat/chat/answer/tool-detail.tsx
@@ -7,7 +7,7 @@ import {
RiLoader2Line,
} from '@remixicon/react'
import type { ToolInfoInThought } from '../type'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type ToolDetailProps = {
payload: ToolInfoInThought
diff --git a/web/app/components/base/chat/chat/answer/workflow-process.tsx b/web/app/components/base/chat/chat/answer/workflow-process.tsx
index 0537d3c58b..c36f2b8f72 100644
--- a/web/app/components/base/chat/chat/answer/workflow-process.tsx
+++ b/web/app/components/base/chat/chat/answer/workflow-process.tsx
@@ -10,7 +10,7 @@ import {
import { useTranslation } from 'react-i18next'
import type { ChatItem, WorkflowProcess } from '../../types'
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
diff --git a/web/app/components/base/chat/chat/chat-input-area/index.tsx b/web/app/components/base/chat/chat/chat-input-area/index.tsx
index 5004bb2a92..bea1b3890b 100644
--- a/web/app/components/base/chat/chat/chat-input-area/index.tsx
+++ b/web/app/components/base/chat/chat/chat-input-area/index.tsx
@@ -16,7 +16,7 @@ import type { InputForm } from '../type'
import { useCheckInputsForms } from '../check-input-forms-hooks'
import { useTextAreaHeight } from './hooks'
import Operation from './operation'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { FileListInChatInput } from '@/app/components/base/file-uploader'
import { useFile } from '@/app/components/base/file-uploader/hooks'
import {
diff --git a/web/app/components/base/chat/chat/chat-input-area/operation.tsx b/web/app/components/base/chat/chat/chat-input-area/operation.tsx
index 014ca6651f..2c041be90b 100644
--- a/web/app/components/base/chat/chat/chat-input-area/operation.tsx
+++ b/web/app/components/base/chat/chat/chat-input-area/operation.tsx
@@ -12,7 +12,7 @@ import Button from '@/app/components/base/button'
import ActionButton from '@/app/components/base/action-button'
import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
import type { FileUpload } from '@/app/components/base/features/types'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type OperationProps = {
fileConfig?: FileUpload
diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx
index 0e947f8137..9864dda6ae 100644
--- a/web/app/components/base/chat/chat/index.tsx
+++ b/web/app/components/base/chat/chat/index.tsx
@@ -26,7 +26,7 @@ import ChatInputArea from './chat-input-area'
import TryToAsk from './try-to-ask'
import { ChatContextProvider } from './context'
import type { InputForm } from './type'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import type { Emoji } from '@/app/components/tools/types'
import Button from '@/app/components/base/button'
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
@@ -222,11 +222,16 @@ const Chat: FC
= ({
return () => container.removeEventListener('scroll', setUserScrolled)
}, [])
- // Reset user scroll state when a new chat starts (length <= 1)
+ // Reset user scroll state when conversation changes or a new chat starts
+ // Track the first message ID to detect conversation switches (fixes #29820)
+ const prevFirstMessageIdRef = useRef(undefined)
useEffect(() => {
- if (chatList.length <= 1)
+ const firstMessageId = chatList[0]?.id
+ // Reset when: new chat (length <= 1) OR conversation switched (first message ID changed)
+ if (chatList.length <= 1 || (firstMessageId && prevFirstMessageIdRef.current !== firstMessageId))
userScrolledRef.current = false
- }, [chatList.length])
+ prevFirstMessageIdRef.current = firstMessageId
+ }, [chatList])
useEffect(() => {
if (!sidebarCollapseState)
diff --git a/web/app/components/base/chat/chat/loading-anim/index.tsx b/web/app/components/base/chat/chat/loading-anim/index.tsx
index 801c89fce7..90cda3da2d 100644
--- a/web/app/components/base/chat/chat/loading-anim/index.tsx
+++ b/web/app/components/base/chat/chat/loading-anim/index.tsx
@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
export type ILoadingAnimProps = {
type: 'text' | 'avatar'
diff --git a/web/app/components/base/chat/chat/question.tsx b/web/app/components/base/chat/chat/question.tsx
index 21b604b969..a36e7ee160 100644
--- a/web/app/components/base/chat/chat/question.tsx
+++ b/web/app/components/base/chat/chat/question.tsx
@@ -21,7 +21,7 @@ import { RiClipboardLine, RiEditLine } from '@remixicon/react'
import Toast from '../../toast'
import copy from 'copy-to-clipboard'
import { useTranslation } from 'react-i18next'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import Textarea from 'react-textarea-autosize'
import Button from '../../button'
import { useChatContext } from './context'
diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
index a07e6217b0..ebd2e2de14 100644
--- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
@@ -22,7 +22,7 @@ import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
import AnswerIcon from '@/app/components/base/answer-icon'
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import type { FileEntity } from '../../file-uploader/types'
import Avatar from '../../avatar'
diff --git a/web/app/components/base/chat/embedded-chatbot/header/index.tsx b/web/app/components/base/chat/embedded-chatbot/header/index.tsx
index 48f6de5725..16e656171e 100644
--- a/web/app/components/base/chat/embedded-chatbot/header/index.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/header/index.tsx
@@ -12,7 +12,7 @@ import ActionButton from '@/app/components/base/action-button'
import Divider from '@/app/components/base/divider'
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
import DifyLogo from '@/app/components/base/logo/dify-logo'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type IHeaderProps = {
diff --git a/web/app/components/base/chat/embedded-chatbot/index.tsx b/web/app/components/base/chat/embedded-chatbot/index.tsx
index 1553d1f153..d908e39787 100644
--- a/web/app/components/base/chat/embedded-chatbot/index.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/index.tsx
@@ -17,7 +17,7 @@ import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
import Header from '@/app/components/base/chat/embedded-chatbot/header'
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
import DifyLogo from '@/app/components/base/logo/dify-logo'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx
index 88472b5d8f..ac1017c619 100644
--- a/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx
@@ -5,7 +5,7 @@ import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
import { useEmbeddedChatbotContext } from '../context'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type Props = {
collapsed: boolean
diff --git a/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx b/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx
index d6c89864d9..9d2a6d9824 100644
--- a/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx
@@ -7,7 +7,7 @@ import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigge
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type Props = {
iconColor?: string
diff --git a/web/app/components/base/checkbox-list/index.tsx b/web/app/components/base/checkbox-list/index.tsx
index ca8333a200..efb1b588d1 100644
--- a/web/app/components/base/checkbox-list/index.tsx
+++ b/web/app/components/base/checkbox-list/index.tsx
@@ -3,7 +3,7 @@ import Badge from '@/app/components/base/badge'
import Checkbox from '@/app/components/base/checkbox'
import SearchInput from '@/app/components/base/search-input'
import SearchMenu from '@/assets/search-menu.svg'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import Image from 'next/image'
import type { FC } from 'react'
import { useCallback, useMemo, useState } from 'react'
diff --git a/web/app/components/base/checkbox/index.spec.tsx b/web/app/components/base/checkbox/index.spec.tsx
index 7ef901aef5..e817f05afd 100644
--- a/web/app/components/base/checkbox/index.spec.tsx
+++ b/web/app/components/base/checkbox/index.spec.tsx
@@ -26,7 +26,7 @@ describe('Checkbox Component', () => {
})
it('handles click events when not disabled', () => {
- const onCheck = jest.fn()
+ const onCheck = vi.fn()
render()
const checkbox = screen.getByTestId('checkbox-test')
@@ -35,7 +35,7 @@ describe('Checkbox Component', () => {
})
it('does not handle click events when disabled', () => {
- const onCheck = jest.fn()
+ const onCheck = vi.fn()
render()
const checkbox = screen.getByTestId('checkbox-test')
diff --git a/web/app/components/base/checkbox/index.tsx b/web/app/components/base/checkbox/index.tsx
index 9495292ea6..5d222f5723 100644
--- a/web/app/components/base/checkbox/index.tsx
+++ b/web/app/components/base/checkbox/index.tsx
@@ -1,5 +1,5 @@
import { RiCheckLine } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import IndeterminateIcon from './assets/indeterminate-icon'
type CheckboxProps = {
diff --git a/web/app/components/base/chip/index.tsx b/web/app/components/base/chip/index.tsx
index eeaf2b19c6..919f2e1ab1 100644
--- a/web/app/components/base/chip/index.tsx
+++ b/web/app/components/base/chip/index.tsx
@@ -1,7 +1,7 @@
import type { FC } from 'react'
import { useMemo, useState } from 'react'
import { RiArrowDownSLine, RiCheckLine, RiCloseCircleFill, RiFilter3Line } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
diff --git a/web/app/components/base/content-dialog/index.tsx b/web/app/components/base/content-dialog/index.tsx
index 5efab57a40..4367744f4d 100644
--- a/web/app/components/base/content-dialog/index.tsx
+++ b/web/app/components/base/content-dialog/index.tsx
@@ -1,6 +1,6 @@
import type { ReactNode } from 'react'
import { Transition, TransitionChild } from '@headlessui/react'
-import classNames from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type ContentDialogProps = {
className?: string
@@ -23,24 +23,20 @@ const ContentDialog = ({
>
-
+ className)}>
{children}
diff --git a/web/app/components/base/corner-label/index.tsx b/web/app/components/base/corner-label/index.tsx
index 0807ed4659..25cd228ba5 100644
--- a/web/app/components/base/corner-label/index.tsx
+++ b/web/app/components/base/corner-label/index.tsx
@@ -1,5 +1,5 @@
import { Corner } from '../icons/src/vender/solid/shapes'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type CornerLabelProps = {
label: string
diff --git a/web/app/components/base/date-and-time-picker/calendar/item.tsx b/web/app/components/base/date-and-time-picker/calendar/item.tsx
index 7132d7bdfb..991ab84043 100644
--- a/web/app/components/base/date-and-time-picker/calendar/item.tsx
+++ b/web/app/components/base/date-and-time-picker/calendar/item.tsx
@@ -1,6 +1,6 @@
import React, { type FC } from 'react'
import type { CalendarItemProps } from '../types'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import dayjs from '../utils/dayjs'
const Item: FC = ({
diff --git a/web/app/components/base/date-and-time-picker/common/option-list-item.tsx b/web/app/components/base/date-and-time-picker/common/option-list-item.tsx
index 0144a7c6ec..fcb1e5299e 100644
--- a/web/app/components/base/date-and-time-picker/common/option-list-item.tsx
+++ b/web/app/components/base/date-and-time-picker/common/option-list-item.tsx
@@ -1,5 +1,5 @@
import React, { type FC, useEffect, useRef } from 'react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
type OptionListItemProps = {
isSelected: boolean
diff --git a/web/app/components/base/date-and-time-picker/date-picker/footer.tsx b/web/app/components/base/date-and-time-picker/date-picker/footer.tsx
index 6351a8235b..9c7136f67a 100644
--- a/web/app/components/base/date-and-time-picker/date-picker/footer.tsx
+++ b/web/app/components/base/date-and-time-picker/date-picker/footer.tsx
@@ -2,7 +2,7 @@ import React, { type FC } from 'react'
import Button from '../../button'
import { type DatePickerFooterProps, ViewType } from '../types'
import { RiTimeLine } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
const Footer: FC = ({
diff --git a/web/app/components/base/date-and-time-picker/date-picker/index.tsx b/web/app/components/base/date-and-time-picker/date-picker/index.tsx
index db089d10d0..a0ccfa153d 100644
--- a/web/app/components/base/date-and-time-picker/date-picker/index.tsx
+++ b/web/app/components/base/date-and-time-picker/date-picker/index.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { RiCalendarLine, RiCloseCircleFill } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import type { DatePickerProps, Period } from '../types'
import { ViewType } from '../types'
import type { Dayjs } from 'dayjs'
diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx
index 24c7fff52f..3c7226fb4b 100644
--- a/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx
+++ b/web/app/components/base/date-and-time-picker/time-picker/index.spec.tsx
@@ -5,7 +5,7 @@ import dayjs from '../utils/dayjs'
import { isDayjsObject } from '../utils/dayjs'
import type { TimePickerProps } from '../types'
-jest.mock('react-i18next', () => ({
+vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
if (key === 'time.defaultPlaceholder') return 'Pick a time...'
@@ -17,7 +17,7 @@ jest.mock('react-i18next', () => ({
}),
}))
-jest.mock('@/app/components/base/portal-to-follow-elem', () => ({
+vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
PortalToFollowElem: ({ children }: { children: React.ReactNode }) => {children}
,
PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick: (e: React.MouseEvent) => void }) => (
{children}
@@ -27,27 +27,22 @@ jest.mock('@/app/components/base/portal-to-follow-elem', () => ({
),
}))
-jest.mock('./options', () => () => )
-jest.mock('./header', () => () => )
-jest.mock('@/app/components/base/timezone-label', () => {
- return function MockTimezoneLabel({ timezone, inline, className }: { timezone: string, inline?: boolean, className?: string }) {
- return (
-
- UTC+8
-
- )
- }
-})
+vi.mock('./options', () => ({
+ default: () => ,
+}))
+vi.mock('./header', () => ({
+ default: () => ,
+}))
describe('TimePicker', () => {
const baseProps: Pick = {
- onChange: jest.fn(),
- onClear: jest.fn(),
+ onChange: vi.fn(),
+ onClear: vi.fn(),
value: undefined,
}
beforeEach(() => {
- jest.clearAllMocks()
+ vi.clearAllMocks()
})
test('renders formatted value for string input (Issue #26692 regression)', () => {
@@ -86,7 +81,7 @@ describe('TimePicker', () => {
})
test('selecting current time emits timezone-aware value', () => {
- const onChange = jest.fn()
+ const onChange = vi.fn()
render(
{
/>,
)
- expect(screen.queryByTestId('timezone-label')).not.toBeInTheDocument()
+ expect(screen.queryByTitle(/Timezone: Asia\/Shanghai/)).not.toBeInTheDocument()
})
test('should not display timezone label when showTimezone is false', () => {
@@ -127,7 +122,7 @@ describe('TimePicker', () => {
/>,
)
- expect(screen.queryByTestId('timezone-label')).not.toBeInTheDocument()
+ expect(screen.queryByTitle(/Timezone: Asia\/Shanghai/)).not.toBeInTheDocument()
})
test('should display timezone label when showTimezone is true', () => {
@@ -140,23 +135,9 @@ describe('TimePicker', () => {
/>,
)
- const timezoneLabel = screen.getByTestId('timezone-label')
+ const timezoneLabel = screen.getByTitle(/Timezone: Asia\/Shanghai/)
expect(timezoneLabel).toBeInTheDocument()
- expect(timezoneLabel).toHaveAttribute('data-timezone', 'Asia/Shanghai')
- })
-
- test('should pass inline prop to timezone label', () => {
- render(
- ,
- )
-
- const timezoneLabel = screen.getByTestId('timezone-label')
- expect(timezoneLabel).toHaveAttribute('data-inline', 'true')
+ expect(timezoneLabel).toHaveTextContent(/UTC[+-]\d+/)
})
test('should not display timezone label when showTimezone is true but timezone is not provided', () => {
@@ -168,21 +149,7 @@ describe('TimePicker', () => {
/>,
)
- expect(screen.queryByTestId('timezone-label')).not.toBeInTheDocument()
- })
-
- test('should apply shrink-0 and text-xs classes to timezone label', () => {
- render(
- ,
- )
-
- const timezoneLabel = screen.getByTestId('timezone-label')
- expect(timezoneLabel).toHaveClass('shrink-0', 'text-xs')
+ expect(screen.queryByTitle(/Timezone:/)).not.toBeInTheDocument()
})
})
})
diff --git a/web/app/components/base/date-and-time-picker/time-picker/index.tsx b/web/app/components/base/date-and-time-picker/time-picker/index.tsx
index 9577a107e5..316164bfac 100644
--- a/web/app/components/base/date-and-time-picker/time-picker/index.tsx
+++ b/web/app/components/base/date-and-time-picker/time-picker/index.tsx
@@ -18,7 +18,7 @@ import Options from './options'
import Header from './header'
import { useTranslation } from 'react-i18next'
import { RiCloseCircleFill, RiTimeLine } from '@remixicon/react'
-import cn from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
import TimezoneLabel from '@/app/components/base/timezone-label'
const to24Hour = (hour12: string, period: Period) => {
diff --git a/web/app/components/base/dialog/index.tsx b/web/app/components/base/dialog/index.tsx
index d4c0f10b40..3a56942537 100644
--- a/web/app/components/base/dialog/index.tsx
+++ b/web/app/components/base/dialog/index.tsx
@@ -1,7 +1,7 @@
import { Fragment, useCallback } from 'react'
import type { ElementType, ReactNode } from 'react'
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'
-import classNames from '@/utils/classnames'
+import { cn } from '@/utils/classnames'
// https://headlessui.com/react/dialog
@@ -35,37 +35,33 @@ const CustomDialog = ({