Merge branch 'main' into feat/summary-index

This commit is contained in:
zxhlyh
2026-01-22 14:27:23 +08:00
285 changed files with 2287 additions and 978 deletions

View File

@ -3,10 +3,8 @@ import {
RiClipboardFill,
RiClipboardLine,
} from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { debounce } from 'es-toolkit/compat'
import * as React from 'react'
import { useState } from 'react'
import { useClipboard } from 'foxact/use-clipboard'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
@ -21,32 +19,27 @@ const prefixEmbedded = 'overview.appInfo.embedded'
const CopyFeedback = ({ content }: Props) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)
const { copied, copy, reset } = useClipboard()
const onClickCopy = debounce(() => {
const handleCopy = useCallback(() => {
copy(content)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
}, [copy, content])
return (
<Tooltip
popupContent={
(isCopied
(copied
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
}
>
<ActionButton>
<div
onClick={onClickCopy}
onMouseLeave={onMouseLeave}
onClick={handleCopy}
onMouseLeave={reset}
>
{isCopied && <RiClipboardFill className="h-4 w-4" />}
{!isCopied && <RiClipboardLine className="h-4 w-4" />}
{copied && <RiClipboardFill className="h-4 w-4" />}
{!copied && <RiClipboardLine className="h-4 w-4" />}
</div>
</ActionButton>
</Tooltip>
@ -57,21 +50,16 @@ export default CopyFeedback
export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className' | 'content'>) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)
const { copied, copy, reset } = useClipboard()
const onClickCopy = debounce(() => {
const handleCopy = useCallback(() => {
copy(content)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
}, [copy, content])
return (
<Tooltip
popupContent={
(isCopied
(copied
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
}
@ -81,9 +69,9 @@ export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className'
}`}
>
<div
onClick={onClickCopy}
onMouseLeave={onMouseLeave}
className={`h-full w-full ${copyStyle.copyIcon} ${isCopied ? copyStyle.copied : ''
onClick={handleCopy}
onMouseLeave={reset}
className={`h-full w-full ${copyStyle.copyIcon} ${copied ? copyStyle.copied : ''
}`}
>
</div>

View File

@ -1,8 +1,6 @@
'use client'
import copy from 'copy-to-clipboard'
import { debounce } from 'es-toolkit/compat'
import * as React from 'react'
import { useState } from 'react'
import { useClipboard } from 'foxact/use-clipboard'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
Copy,
@ -18,29 +16,24 @@ const prefixEmbedded = 'overview.appInfo.embedded'
const CopyIcon = ({ content }: Props) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)
const { copied, copy, reset } = useClipboard()
const onClickCopy = debounce(() => {
const handleCopy = useCallback(() => {
copy(content)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
}, [copy, content])
return (
<Tooltip
popupContent={
(isCopied
(copied
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
}
>
<div onMouseLeave={onMouseLeave}>
{!isCopied
<div onMouseLeave={reset}>
{!copied
? (
<Copy className="mx-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary" onClick={onClickCopy} />
<Copy className="mx-1 h-3.5 w-3.5 cursor-pointer text-text-tertiary" onClick={handleCopy} />
)
: (
<CopyCheck className="mx-1 h-3.5 w-3.5 text-text-tertiary" />

View File

@ -2,7 +2,6 @@ import type { OnFeaturesChange } from '@/app/components/base/features/types'
import type { InputVar } from '@/app/components/workflow/types'
import type { PromptVariable } from '@/models/debug'
import { RiCloseLine, RiInformation2Fill } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import AnnotationReply from '@/app/components/base/features/new-feature-panel/annotation-reply'
@ -18,7 +17,6 @@ import SpeechToText from '@/app/components/base/features/new-feature-panel/speec
import TextToSpeech from '@/app/components/base/features/new-feature-panel/text-to-speech'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDocLink } from '@/context/i18n'
type Props = {
show: boolean
@ -46,7 +44,6 @@ const NewFeaturePanel = ({
onAutoAddPromptVariable,
}: Props) => {
const { t } = useTranslation()
const docLink = useDocLink()
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
@ -76,14 +73,6 @@ const NewFeaturePanel = ({
</div>
<div className="system-xs-medium p-1 text-text-primary">
<span>{isChatMode ? t('common.fileUploadTip', { ns: 'workflow' }) : t('common.ImageUploadLegacyTip', { ns: 'workflow' })}</span>
<a
className="text-text-accent"
href={docLink('/guides/workflow/bulletin')}
target="_blank"
rel="noopener noreferrer"
>
{t('common.featuresDocLink', { ns: 'workflow' })}
</a>
</div>
</div>
</div>

View File

@ -319,7 +319,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
<div className="flex h-9 items-center justify-between">
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
<a
href={docLink('/guides/extension/api-based-extension/README')}
href={docLink('/use-dify/workspace/api-extension/api-extension')}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center text-xs text-text-tertiary hover:text-primary-600"

View File

@ -3,13 +3,8 @@ import * as React from 'react'
import { createReactI18nextMock } from '@/test/i18n-mock'
import InputWithCopy from './index'
// Create a mock function that we can track using vi.hoisted
const mockCopyToClipboard = vi.hoisted(() => vi.fn(() => true))
// Mock the copy-to-clipboard library
vi.mock('copy-to-clipboard', () => ({
default: mockCopyToClipboard,
}))
// Mock navigator.clipboard for foxact/use-clipboard
const mockWriteText = vi.fn(() => Promise.resolve())
// Mock the i18n hook with custom translations for test assertions
vi.mock('react-i18next', () => createReactI18nextMock({
@ -19,15 +14,16 @@ vi.mock('react-i18next', () => createReactI18nextMock({
'overview.appInfo.embedded.copied': 'Copied',
}))
// Mock es-toolkit/compat debounce
vi.mock('es-toolkit/compat', () => ({
debounce: (fn: any) => fn,
}))
describe('InputWithCopy component', () => {
beforeEach(() => {
vi.clearAllMocks()
mockCopyToClipboard.mockClear()
mockWriteText.mockClear()
// Setup navigator.clipboard mock
Object.assign(navigator, {
clipboard: {
writeText: mockWriteText,
},
})
})
it('renders correctly with default props', () => {
@ -55,7 +51,9 @@ describe('InputWithCopy component', () => {
const copyButton = screen.getByRole('button')
fireEvent.click(copyButton)
expect(mockCopyToClipboard).toHaveBeenCalledWith('test value')
await waitFor(() => {
expect(mockWriteText).toHaveBeenCalledWith('test value')
})
})
it('copies custom value when copyValue prop is provided', async () => {
@ -65,7 +63,9 @@ describe('InputWithCopy component', () => {
const copyButton = screen.getByRole('button')
fireEvent.click(copyButton)
expect(mockCopyToClipboard).toHaveBeenCalledWith('custom copy value')
await waitFor(() => {
expect(mockWriteText).toHaveBeenCalledWith('custom copy value')
})
})
it('calls onCopy callback when copy button is clicked', async () => {
@ -76,7 +76,9 @@ describe('InputWithCopy component', () => {
const copyButton = screen.getByRole('button')
fireEvent.click(copyButton)
expect(onCopyMock).toHaveBeenCalledWith('test value')
await waitFor(() => {
expect(onCopyMock).toHaveBeenCalledWith('test value')
})
})
it('shows copied state after successful copy', async () => {
@ -115,17 +117,19 @@ describe('InputWithCopy component', () => {
expect(input).toHaveClass('custom-class')
})
it('handles empty value correctly', () => {
it('handles empty value correctly', async () => {
const mockOnChange = vi.fn()
render(<InputWithCopy value="" onChange={mockOnChange} />)
const input = screen.getByRole('textbox')
const input = screen.getByDisplayValue('')
const copyButton = screen.getByRole('button')
expect(input).toBeInTheDocument()
expect(copyButton).toBeInTheDocument()
fireEvent.click(copyButton)
expect(mockCopyToClipboard).toHaveBeenCalledWith('')
await waitFor(() => {
expect(mockWriteText).toHaveBeenCalledWith('')
})
})
it('maintains focus on input after copy', async () => {

View File

@ -1,10 +1,8 @@
'use client'
import type { InputProps } from '../input'
import { RiClipboardFill, RiClipboardLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { debounce } from 'es-toolkit/compat'
import { useClipboard } from 'foxact/use-clipboard'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import ActionButton from '../action-button'
@ -30,31 +28,16 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>((
ref,
) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = useState<boolean>(false)
// Determine what value to copy
const valueToString = typeof value === 'string' ? value : String(value || '')
const finalCopyValue = copyValue || valueToString
const onClickCopy = debounce(() => {
const { copied, copy, reset } = useClipboard()
const handleCopy = () => {
copy(finalCopyValue)
setIsCopied(true)
onCopy?.(finalCopyValue)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
useEffect(() => {
if (isCopied) {
const timeout = setTimeout(() => {
setIsCopied(false)
}, 2000)
return () => {
clearTimeout(timeout)
}
}
}, [isCopied])
}
return (
<div className={cn('relative w-full', wrapperClassName)}>
@ -73,21 +56,21 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>((
{showCopyButton && (
<div
className="absolute right-2 top-1/2 -translate-y-1/2"
onMouseLeave={onMouseLeave}
onMouseLeave={reset}
>
<Tooltip
popupContent={
(isCopied
(copied
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''
}
>
<ActionButton
size="xs"
onClick={onClickCopy}
onClick={handleCopy}
className="hover:bg-components-button-ghost-bg-hover"
>
{isCopied
{copied
? (
<RiClipboardFill className="h-3.5 w-3.5 text-text-tertiary" />
)