mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 17:08:03 +08:00
Merge branch 'main' into feat/grouping-branching
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
RiAddLine,
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@ -4,7 +4,7 @@ import type { ExternalDataTool } from '@/models/common'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
@ -7,19 +7,24 @@ export const jsonObjectWrap = {
|
||||
|
||||
export const jsonConfigPlaceHolder = JSON.stringify(
|
||||
{
|
||||
foo: {
|
||||
type: 'string',
|
||||
},
|
||||
bar: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sub: {
|
||||
type: 'number',
|
||||
},
|
||||
type: 'object',
|
||||
properties: {
|
||||
foo: {
|
||||
type: 'string',
|
||||
},
|
||||
bar: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sub: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: true,
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: true,
|
||||
},
|
||||
required: [],
|
||||
additionalProperties: true,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
||||
@ -28,7 +28,7 @@ import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscoreInVarNameInpu
|
||||
import ConfigSelect from '../config-select'
|
||||
import ConfigString from '../config-string'
|
||||
import ModalFoot from '../modal-foot'
|
||||
import { jsonConfigPlaceHolder, jsonObjectWrap } from './config'
|
||||
import { jsonConfigPlaceHolder } from './config'
|
||||
import Field from './field'
|
||||
import TypeSelector from './type-select'
|
||||
|
||||
@ -78,13 +78,12 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const isBasicApp = appDetail?.mode !== AppModeEnum.ADVANCED_CHAT && appDetail?.mode !== AppModeEnum.WORKFLOW
|
||||
const isSupportJSON = false
|
||||
const jsonSchemaStr = useMemo(() => {
|
||||
const isJsonObject = type === InputVarType.jsonObject
|
||||
if (!isJsonObject || !tempPayload.json_schema)
|
||||
return ''
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tempPayload.json_schema).properties, null, 2)
|
||||
return JSON.stringify(JSON.parse(tempPayload.json_schema), null, 2)
|
||||
}
|
||||
catch {
|
||||
return ''
|
||||
@ -129,13 +128,14 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
}, [])
|
||||
|
||||
const handleJSONSchemaChange = useCallback((value: string) => {
|
||||
const isEmpty = value == null || value.trim() === ''
|
||||
if (isEmpty) {
|
||||
handlePayloadChange('json_schema')(undefined)
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const v = JSON.parse(value)
|
||||
const res = {
|
||||
...jsonObjectWrap,
|
||||
properties: v,
|
||||
}
|
||||
handlePayloadChange('json_schema')(JSON.stringify(res, null, 2))
|
||||
handlePayloadChange('json_schema')(JSON.stringify(v, null, 2))
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
@ -175,7 +175,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...((!isBasicApp && isSupportJSON)
|
||||
...((!isBasicApp)
|
||||
? [{
|
||||
name: t('variableConfig.json', { ns: 'appDebug' }),
|
||||
value: InputVarType.jsonObject,
|
||||
@ -233,7 +233,28 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
|
||||
const checkboxDefaultSelectValue = useMemo(() => getCheckboxDefaultSelectValue(tempPayload.default), [tempPayload.default])
|
||||
|
||||
const isJsonSchemaEmpty = (value: InputVar['json_schema']) => {
|
||||
if (value === null || value === undefined) {
|
||||
return true
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return false
|
||||
}
|
||||
const trimmed = value.trim()
|
||||
return trimmed === ''
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
const jsonSchemaValue = tempPayload.json_schema
|
||||
const isSchemaEmpty = isJsonSchemaEmpty(jsonSchemaValue)
|
||||
const normalizedJsonSchema = isSchemaEmpty ? undefined : jsonSchemaValue
|
||||
|
||||
// if the input type is jsonObject and the schema is empty as determined by `isJsonSchemaEmpty`,
|
||||
// remove the `json_schema` field from the payload by setting its value to `undefined`.
|
||||
const payloadToSave = tempPayload.type === InputVarType.jsonObject && isSchemaEmpty
|
||||
? { ...tempPayload, json_schema: undefined }
|
||||
: tempPayload
|
||||
|
||||
const moreInfo = tempPayload.variable === payload?.variable
|
||||
? undefined
|
||||
: {
|
||||
@ -250,7 +271,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
return
|
||||
}
|
||||
if (isStringInput || type === InputVarType.number) {
|
||||
onConfirm(tempPayload, moreInfo)
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
else if (type === InputVarType.select) {
|
||||
if (options?.length === 0) {
|
||||
@ -270,7 +291,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }) })
|
||||
return
|
||||
}
|
||||
onConfirm(tempPayload, moreInfo)
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
else if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
|
||||
if (tempPayload.allowed_file_types?.length === 0) {
|
||||
@ -283,10 +304,26 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
Toast.notify({ type: 'error', message: errorMessages })
|
||||
return
|
||||
}
|
||||
onConfirm(tempPayload, moreInfo)
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
else if (type === InputVarType.jsonObject) {
|
||||
if (!isSchemaEmpty && typeof normalizedJsonSchema === 'string') {
|
||||
try {
|
||||
const schema = JSON.parse(normalizedJsonSchema)
|
||||
if (schema?.type !== 'object') {
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' }) })
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' }) })
|
||||
return
|
||||
}
|
||||
}
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
else {
|
||||
onConfirm(tempPayload, moreInfo)
|
||||
onConfirm(payloadToSave, moreInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,15 +5,6 @@ import * as React from 'react'
|
||||
import { AgentStrategy } from '@/types/app'
|
||||
import AgentSettingButton from './agent-setting-button'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
let latestAgentSettingProps: any
|
||||
vi.mock('./agent/agent-setting', () => ({
|
||||
default: (props: any) => {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
|
||||
@ -15,15 +15,6 @@ vi.mock('use-context-selector', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUseFeatures = vi.fn()
|
||||
const mockUseFeaturesStore = vi.fn()
|
||||
vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
|
||||
@ -3,7 +3,7 @@ import type { Member } from '@/models/common'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { isEqual } from 'es-toolkit/compat'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { ModelAndParameter } from '../types'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
|
||||
export type DebugWithMultipleModelContextType = {
|
||||
|
||||
@ -4,7 +4,8 @@ import type {
|
||||
OnSend,
|
||||
TextGenerationConfig,
|
||||
} from '@/app/components/base/text-generation/types'
|
||||
import { cloneDeep, noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import { memo } from 'react'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import { TransferMethod } from '@/app/components/base/chat/types'
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
} from '@/app/components/base/chat/types'
|
||||
import { cloneDeep } from 'es-toolkit/compat'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
|
||||
@ -11,7 +11,8 @@ import {
|
||||
RiSparklingFill,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { cloneDeep, noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import { produce, setAutoFreeze } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { ChatPromptConfig, CompletionPromptConfig, ConversationHistoriesRole, PromptItem } from '@/models/debug'
|
||||
import { clone } from 'es-toolkit/compat'
|
||||
import { clone } from 'es-toolkit/object'
|
||||
import { produce } from 'immer'
|
||||
import { useState } from 'react'
|
||||
import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock, PRE_PROMPT_PLACEHOLDER_TEXT } from '@/app/components/base/prompt-editor/constants'
|
||||
|
||||
@ -20,7 +20,8 @@ import type {
|
||||
import type { ModelConfig as BackendModelConfig, UserInputFormItem, VisionSettings } from '@/types/app'
|
||||
import { CodeBracketIcon } from '@heroicons/react/20/solid'
|
||||
import { useBoolean, useGetState } from 'ahooks'
|
||||
import { clone, isEqual } from 'es-toolkit/compat'
|
||||
import { clone } from 'es-toolkit/object'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
import { produce } from 'immer'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
CodeBasedExtensionItem,
|
||||
ExternalDataTool,
|
||||
} from '@/models/common'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
@ -14,13 +14,6 @@ vi.mock('ahooks', () => ({
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({ isCurrentWorkspaceEditor: true }),
|
||||
}))
|
||||
vi.mock('use-context-selector', async () => {
|
||||
const actual = await vi.importActual<typeof import('use-context-selector')>('use-context-selector')
|
||||
return {
|
||||
...actual,
|
||||
useContext: () => ({ hasEditPermission: true }),
|
||||
}
|
||||
})
|
||||
vi.mock('nuqs', () => ({
|
||||
useQueryState: () => ['Recommended', vi.fn()],
|
||||
}))
|
||||
@ -119,6 +112,7 @@ describe('Apps', () => {
|
||||
fireEvent.click(screen.getAllByTestId('app-card')[0])
|
||||
expect(screen.getByTestId('create-from-template-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows no template message when list is empty', () => {
|
||||
mockUseExploreAppList.mockReturnValueOnce({
|
||||
data: { allList: [], categories: [] },
|
||||
|
||||
@ -8,7 +8,6 @@ import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppTypeSelector from '@/app/components/app/type-selector'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
@ -19,7 +18,6 @@ import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import { importDSL } from '@/service/apps'
|
||||
import { fetchAppDetail } from '@/service/explore'
|
||||
@ -47,7 +45,6 @@ const Apps = ({
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { push } = useRouter()
|
||||
const { hasEditPermission } = useContext(ExploreContext)
|
||||
const allCategoriesEn = AppCategories.RECOMMENDED
|
||||
|
||||
const [keywords, setKeywords] = useState('')
|
||||
@ -214,7 +211,7 @@ const Apps = ({
|
||||
<AppCard
|
||||
key={app.app_id}
|
||||
app={app}
|
||||
canCreate={hasEditPermission}
|
||||
canCreate={isCurrentWorkspaceEditor}
|
||||
onCreate={() => {
|
||||
setCurrApp(app)
|
||||
setIsShowCreateModal(true)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import type { App } from '@/types/app'
|
||||
import { useDebounce } from 'ahooks'
|
||||
import dayjs from 'dayjs'
|
||||
import { omit } from 'es-toolkit/compat'
|
||||
import { omit } from 'es-toolkit/object'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
@ -12,7 +12,8 @@ import { RiCloseLine, RiEditFill } from '@remixicon/react'
|
||||
import dayjs from 'dayjs'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { get, noop } from 'es-toolkit/compat'
|
||||
import { get } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
@ -2,7 +2,7 @@ import type { RenderOptions } from '@testing-library/react'
|
||||
import type { Mock, MockedFunction } from 'vitest'
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import { fireEvent, render } from '@testing-library/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import { useModalContext as actualUseModalContext } from '@/context/modal-context'
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import type { App } from '@/types/app'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -5,7 +5,7 @@ import { useDebounce } from 'ahooks'
|
||||
import dayjs from 'dayjs'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { omit } from 'es-toolkit/compat'
|
||||
import { omit } from 'es-toolkit/object'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
import type { FC } from 'react'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { AgentIteration, AgentLogDetailResponse } from '@/models/log'
|
||||
import { flatten, uniq } from 'es-toolkit/compat'
|
||||
import { uniq } from 'es-toolkit/array'
|
||||
import { flatten } from 'es-toolkit/compat'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -3,7 +3,7 @@ import type { Area } from 'react-easy-crop'
|
||||
import type { OnImageInput } from './ImageInput'
|
||||
import type { AppIconType, ImageFile } from '@/types/app'
|
||||
import { RiImageCircleAiLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
|
||||
|
||||
@ -14,7 +14,7 @@ import type {
|
||||
AppMeta,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
|
||||
export type ChatWithHistoryContextValue = {
|
||||
|
||||
@ -10,7 +10,7 @@ import type {
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
useCallback,
|
||||
|
||||
@ -8,7 +8,8 @@ import type { InputForm } from './type'
|
||||
import type AudioPlayer from '@/app/components/base/audio-btn/audio'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { Annotation } from '@/models/log'
|
||||
import { noop, uniqBy } from 'es-toolkit/compat'
|
||||
import { uniqBy } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce, setAutoFreeze } from 'immer'
|
||||
import { useParams, usePathname } from 'next/navigation'
|
||||
import {
|
||||
|
||||
@ -13,7 +13,7 @@ import type {
|
||||
AppMeta,
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
|
||||
export type EmbeddedChatbotContextValue = {
|
||||
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
ConversationItem,
|
||||
} from '@/models/share'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
useCallback,
|
||||
|
||||
@ -3,6 +3,7 @@ import type { Day } from '../types'
|
||||
import dayjs from 'dayjs'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { IS_PROD } from '@/config'
|
||||
import tz from '@/utils/timezone.json'
|
||||
|
||||
dayjs.extend(utc)
|
||||
@ -131,7 +132,7 @@ export type ToDayjsOptions = {
|
||||
}
|
||||
|
||||
const warnParseFailure = (value: string) => {
|
||||
if (process.env.NODE_ENV !== 'production')
|
||||
if (!IS_PROD)
|
||||
console.warn('[TimePicker] Failed to parse time value', value)
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -4,6 +4,7 @@ import { RiAlertLine, RiBugLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { IS_DEV } from '@/config'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type ErrorBoundaryState = {
|
||||
@ -54,7 +55,7 @@ class ErrorBoundaryInner extends React.Component<
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (IS_DEV) {
|
||||
console.error('ErrorBoundary caught an error:', error)
|
||||
console.error('Error Info:', errorInfo)
|
||||
}
|
||||
@ -262,13 +263,13 @@ export function withErrorBoundary<P extends object>(
|
||||
// Simple error fallback component
|
||||
export const ErrorFallback: React.FC<{
|
||||
error: Error
|
||||
resetErrorBoundary: () => void
|
||||
}> = ({ error, resetErrorBoundary }) => {
|
||||
resetErrorBoundaryAction: () => void
|
||||
}> = ({ error, resetErrorBoundaryAction }) => {
|
||||
return (
|
||||
<div className="flex min-h-[200px] flex-col items-center justify-center rounded-lg border border-red-200 bg-red-50 p-8">
|
||||
<h2 className="mb-2 text-lg font-semibold text-red-800">Oops! Something went wrong</h2>
|
||||
<p className="mb-4 text-center text-red-600">{error.message}</p>
|
||||
<Button onClick={resetErrorBoundary} size="small">
|
||||
<Button onClick={resetErrorBoundaryAction} size="small">
|
||||
Try again
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -3,7 +3,7 @@ import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import { RiAddLine, RiAsterisk, RiCloseLine, RiDeleteBinLine, RiDraggable } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
@ -2,7 +2,7 @@ import type { ChangeEvent, FC } from 'react'
|
||||
import type { CodeBasedExtensionItem } from '@/models/common'
|
||||
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
@ -2,7 +2,7 @@ import type { ClipboardEvent } from 'react'
|
||||
import type { FileEntity } from './types'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import type { FileUploadConfigResponse } from '@/models/common'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiCloseLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { t } from 'i18next'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type {
|
||||
FileEntity,
|
||||
} from './types'
|
||||
import { isEqual } from 'es-toolkit/compat'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react'
|
||||
import { RiCloseLargeLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type IModal = {
|
||||
|
||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import { headers } from 'next/headers'
|
||||
import Script from 'next/script'
|
||||
import * as React from 'react'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { IS_CE_EDITION, IS_PROD } from '@/config'
|
||||
|
||||
export enum GaType {
|
||||
admin = 'admin',
|
||||
@ -32,7 +32,7 @@ const GA: FC<IGAProps> = ({
|
||||
if (IS_CE_EDITION)
|
||||
return null
|
||||
|
||||
const cspHeader = process.env.NODE_ENV === 'production'
|
||||
const cspHeader = IS_PROD
|
||||
? (headers() as unknown as UnsafeUnwrappedHeaders).get('content-security-policy')
|
||||
: null
|
||||
const nonce = extractNonceFromCSP(cspHeader)
|
||||
|
||||
@ -1,174 +0,0 @@
|
||||
import { access, appendFile, mkdir, open, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { parseXml } from '@rgrove/parse-xml'
|
||||
import { camelCase, template } from 'es-toolkit/compat'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const generateDir = async (currentPath) => {
|
||||
try {
|
||||
await mkdir(currentPath, { recursive: true })
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err.message)
|
||||
}
|
||||
}
|
||||
const processSvgStructure = (svgStructure, replaceFillOrStrokeColor) => {
|
||||
if (svgStructure?.children.length) {
|
||||
svgStructure.children = svgStructure.children.filter(c => c.type !== 'text')
|
||||
|
||||
svgStructure.children.forEach((child) => {
|
||||
if (child?.name === 'path' && replaceFillOrStrokeColor) {
|
||||
if (child?.attributes?.stroke)
|
||||
child.attributes.stroke = 'currentColor'
|
||||
|
||||
if (child?.attributes.fill)
|
||||
child.attributes.fill = 'currentColor'
|
||||
}
|
||||
if (child?.children.length)
|
||||
processSvgStructure(child, replaceFillOrStrokeColor)
|
||||
})
|
||||
}
|
||||
}
|
||||
const generateSvgComponent = async (fileHandle, entry, pathList, replaceFillOrStrokeColor) => {
|
||||
const currentPath = path.resolve(__dirname, 'src', ...pathList.slice(2))
|
||||
|
||||
try {
|
||||
await access(currentPath)
|
||||
}
|
||||
catch {
|
||||
await generateDir(currentPath)
|
||||
}
|
||||
|
||||
const svgString = await fileHandle.readFile({ encoding: 'utf8' })
|
||||
const svgJson = parseXml(svgString).toJSON()
|
||||
const svgStructure = svgJson.children[0]
|
||||
processSvgStructure(svgStructure, replaceFillOrStrokeColor)
|
||||
const prefixFileName = camelCase(entry.split('.')[0])
|
||||
const fileName = prefixFileName.charAt(0).toUpperCase() + prefixFileName.slice(1)
|
||||
const svgData = {
|
||||
icon: svgStructure,
|
||||
name: fileName,
|
||||
}
|
||||
|
||||
const componentRender = template(`
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './<%= svgName %>.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>;
|
||||
},
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = '<%= svgName %>'
|
||||
|
||||
export default Icon
|
||||
`.trim())
|
||||
|
||||
await writeFile(path.resolve(currentPath, `${fileName}.json`), `${JSON.stringify(svgData, '', '\t')}\n`)
|
||||
await writeFile(path.resolve(currentPath, `${fileName}.tsx`), `${componentRender({ svgName: fileName })}\n`)
|
||||
|
||||
const indexingRender = template(`
|
||||
export { default as <%= svgName %> } from './<%= svgName %>'
|
||||
`.trim())
|
||||
|
||||
await appendFile(path.resolve(currentPath, 'index.ts'), `${indexingRender({ svgName: fileName })}\n`)
|
||||
}
|
||||
|
||||
const generateImageComponent = async (entry, pathList) => {
|
||||
const currentPath = path.resolve(__dirname, 'src', ...pathList.slice(2))
|
||||
|
||||
try {
|
||||
await access(currentPath)
|
||||
}
|
||||
catch {
|
||||
await generateDir(currentPath)
|
||||
}
|
||||
|
||||
const prefixFileName = camelCase(entry.split('.')[0])
|
||||
const fileName = prefixFileName.charAt(0).toUpperCase() + prefixFileName.slice(1)
|
||||
|
||||
const componentCSSRender = template(`
|
||||
.wrapper {
|
||||
display: inline-flex;
|
||||
background: url(<%= assetPath %>) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
`.trim())
|
||||
|
||||
await writeFile(path.resolve(currentPath, `${fileName}.module.css`), `${componentCSSRender({ assetPath: path.posix.join('~@/app/components/base/icons/assets', ...pathList.slice(2), entry) })}\n`)
|
||||
|
||||
const componentRender = template(`
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import s from './<%= fileName %>.module.css'
|
||||
|
||||
const Icon = (
|
||||
{
|
||||
ref,
|
||||
className,
|
||||
...restProps
|
||||
}: React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> & {
|
||||
ref?: React.RefObject<HTMLSpanElement>;
|
||||
},
|
||||
) => <span className={cn(s.wrapper, className)} {...restProps} ref={ref} />
|
||||
|
||||
Icon.displayName = '<%= fileName %>'
|
||||
|
||||
export default Icon
|
||||
`.trim())
|
||||
|
||||
await writeFile(path.resolve(currentPath, `${fileName}.tsx`), `${componentRender({ fileName })}\n`)
|
||||
|
||||
const indexingRender = template(`
|
||||
export { default as <%= fileName %> } from './<%= fileName %>'
|
||||
`.trim())
|
||||
|
||||
await appendFile(path.resolve(currentPath, 'index.ts'), `${indexingRender({ fileName })}\n`)
|
||||
}
|
||||
|
||||
const walk = async (entry, pathList, replaceFillOrStrokeColor) => {
|
||||
const currentPath = path.resolve(...pathList, entry)
|
||||
let fileHandle
|
||||
|
||||
try {
|
||||
fileHandle = await open(currentPath)
|
||||
const stat = await fileHandle.stat()
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
const files = await readdir(currentPath)
|
||||
|
||||
for (const file of files)
|
||||
await walk(file, [...pathList, entry], replaceFillOrStrokeColor)
|
||||
}
|
||||
|
||||
if (stat.isFile() && /.+\.svg$/.test(entry))
|
||||
await generateSvgComponent(fileHandle, entry, pathList, replaceFillOrStrokeColor)
|
||||
|
||||
if (stat.isFile() && /.+\.png$/.test(entry))
|
||||
await generateImageComponent(entry, pathList)
|
||||
}
|
||||
finally {
|
||||
fileHandle?.close()
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await rm(path.resolve(__dirname, 'src'), { recursive: true, force: true })
|
||||
await walk('public', [__dirname, 'assets'])
|
||||
await walk('vender', [__dirname, 'assets'], true)
|
||||
await walk('image', [__dirname, 'assets'])
|
||||
})()
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiAddBoxLine, RiCloseLine, RiDownloadCloud2Line, RiFileCopyLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { t } from 'i18next'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
@ -1,26 +1,14 @@
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||
import InlineDeleteConfirm from './index'
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, defaultValueOrOptions?: string | { ns?: string }) => {
|
||||
const translations: Record<string, string> = {
|
||||
'operation.deleteConfirmTitle': 'Delete?',
|
||||
'operation.yes': 'Yes',
|
||||
'operation.no': 'No',
|
||||
'operation.confirmAction': 'Please confirm your action.',
|
||||
}
|
||||
if (translations[key])
|
||||
return translations[key]
|
||||
// Handle case where second arg is default value string
|
||||
if (typeof defaultValueOrOptions === 'string')
|
||||
return defaultValueOrOptions
|
||||
const prefix = defaultValueOrOptions?.ns ? `${defaultValueOrOptions.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
// Mock react-i18next with custom translations for test assertions
|
||||
vi.mock('react-i18next', () => createReactI18nextMock({
|
||||
'operation.deleteConfirmTitle': 'Delete?',
|
||||
'operation.yes': 'Yes',
|
||||
'operation.no': 'No',
|
||||
'operation.confirmAction': 'Please confirm your action.',
|
||||
}))
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
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
|
||||
@ -10,22 +11,12 @@ vi.mock('copy-to-clipboard', () => ({
|
||||
default: mockCopyToClipboard,
|
||||
}))
|
||||
|
||||
// Mock the i18n hook
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const translations: Record<string, string> = {
|
||||
'operation.copy': 'Copy',
|
||||
'operation.copied': 'Copied',
|
||||
'overview.appInfo.embedded.copy': 'Copy',
|
||||
'overview.appInfo.embedded.copied': 'Copied',
|
||||
}
|
||||
if (translations[key])
|
||||
return translations[key]
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
// Mock the i18n hook with custom translations for test assertions
|
||||
vi.mock('react-i18next', () => createReactI18nextMock({
|
||||
'operation.copy': 'Copy',
|
||||
'operation.copied': 'Copied',
|
||||
'overview.appInfo.embedded.copy': 'Copy',
|
||||
'overview.appInfo.embedded.copied': 'Copied',
|
||||
}))
|
||||
|
||||
// Mock es-toolkit/compat debounce
|
||||
|
||||
@ -1,21 +1,12 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { createReactI18nextMock } from '@/test/i18n-mock'
|
||||
import Input, { inputVariants } from './index'
|
||||
|
||||
// Mock the i18n hook
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const translations: Record<string, string> = {
|
||||
'operation.search': 'Search',
|
||||
'placeholder.input': 'Please input',
|
||||
}
|
||||
if (translations[key])
|
||||
return translations[key]
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
// Mock the i18n hook with custom translations for test assertions
|
||||
vi.mock('react-i18next', () => createReactI18nextMock({
|
||||
'operation.search': 'Search',
|
||||
'placeholder.input': 'Please input',
|
||||
}))
|
||||
|
||||
describe('Input component', () => {
|
||||
|
||||
@ -2,7 +2,7 @@ import type { VariantProps } from 'class-variance-authority'
|
||||
import type { ChangeEventHandler, CSSProperties, FocusEventHandler } from 'react'
|
||||
import { RiCloseCircleFill, RiErrorWarningLine, RiSearchLine } from '@remixicon/react'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
248
web/app/components/base/markdown-blocks/think-block.spec.tsx
Normal file
248
web/app/components/base/markdown-blocks/think-block.spec.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChatContextProvider } from '@/app/components/base/chat/chat/context'
|
||||
import ThinkBlock from './think-block'
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'chat.thinking': 'Thinking...',
|
||||
'chat.thought': 'Thought',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Helper to wrap component with ChatContextProvider
|
||||
const renderWithContext = (
|
||||
children: React.ReactNode,
|
||||
isResponding: boolean = true,
|
||||
) => {
|
||||
return render(
|
||||
<ChatContextProvider
|
||||
config={undefined}
|
||||
isResponding={isResponding}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
onRegenerate={undefined}
|
||||
onAnnotationEdited={undefined}
|
||||
onAnnotationAdded={undefined}
|
||||
onAnnotationRemoved={undefined}
|
||||
onFeedback={undefined}
|
||||
>
|
||||
{children}
|
||||
</ChatContextProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ThinkBlock', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render regular details element when data-think is false', () => {
|
||||
render(
|
||||
<ThinkBlock data-think={false}>
|
||||
<p>Regular content</p>
|
||||
</ThinkBlock>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Regular content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render think block with thinking state when data-think is true', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Thinking content</p>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
expect(screen.getByText('Thinking content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render thought state when content has ENDTHINKFLAG', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Completed thinking[ENDTHINKFLAG]</p>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thought/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Timer behavior', () => {
|
||||
it('should update elapsed time while thinking', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Thinking...</p>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
// Initial state should show 0.0s
|
||||
expect(screen.getByText(/\(0\.0s\)/)).toBeInTheDocument()
|
||||
|
||||
// Advance timer by 500ms and run pending timers
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500)
|
||||
})
|
||||
|
||||
// Should show approximately 0.5s
|
||||
expect(screen.getByText(/\(0\.5s\)/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should stop timer when isResponding becomes false', () => {
|
||||
const { rerender } = render(
|
||||
<ChatContextProvider
|
||||
config={undefined}
|
||||
isResponding={true}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
onRegenerate={undefined}
|
||||
onAnnotationEdited={undefined}
|
||||
onAnnotationAdded={undefined}
|
||||
onAnnotationRemoved={undefined}
|
||||
onFeedback={undefined}
|
||||
>
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Thinking content</p>
|
||||
</ThinkBlock>
|
||||
</ChatContextProvider>,
|
||||
)
|
||||
|
||||
// Verify initial thinking state
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
|
||||
// Advance timer
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000)
|
||||
})
|
||||
|
||||
// Simulate user clicking stop (isResponding becomes false)
|
||||
rerender(
|
||||
<ChatContextProvider
|
||||
config={undefined}
|
||||
isResponding={false}
|
||||
chatList={[]}
|
||||
showPromptLog={false}
|
||||
questionIcon={undefined}
|
||||
answerIcon={undefined}
|
||||
onSend={undefined}
|
||||
onRegenerate={undefined}
|
||||
onAnnotationEdited={undefined}
|
||||
onAnnotationAdded={undefined}
|
||||
onAnnotationRemoved={undefined}
|
||||
onFeedback={undefined}
|
||||
>
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Thinking content</p>
|
||||
</ThinkBlock>
|
||||
</ChatContextProvider>,
|
||||
)
|
||||
|
||||
// Should now show "Thought" instead of "Thinking..."
|
||||
expect(screen.getByText(/Thought/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should NOT stop timer when isResponding is undefined (outside ChatContextProvider)', () => {
|
||||
// Render without ChatContextProvider
|
||||
render(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Content without ENDTHINKFLAG</p>
|
||||
</ThinkBlock>,
|
||||
)
|
||||
|
||||
// Initial state should show "Thinking..."
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
|
||||
// Advance timer
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(2000)
|
||||
})
|
||||
|
||||
// Timer should still be running (showing "Thinking..." not "Thought")
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
expect(screen.getByText(/\(2\.0s\)/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ENDTHINKFLAG handling', () => {
|
||||
it('should remove ENDTHINKFLAG from displayed content', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<p>Content[ENDTHINKFLAG]</p>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Content')).toBeInTheDocument()
|
||||
expect(screen.queryByText('[ENDTHINKFLAG]')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should detect ENDTHINKFLAG in nested children', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
<div>
|
||||
<span>Nested content[ENDTHINKFLAG]</span>
|
||||
</div>
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
// Should show "Thought" since ENDTHINKFLAG is present
|
||||
expect(screen.getByText(/Thought/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should detect ENDTHINKFLAG in array children', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
{['Part 1', 'Part 2[ENDTHINKFLAG]']}
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thought/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle empty children', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}></ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle null children gracefully', () => {
|
||||
renderWithContext(
|
||||
<ThinkBlock data-think={true}>
|
||||
{null}
|
||||
</ThinkBlock>,
|
||||
true,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -59,7 +59,11 @@ const useThinkTimer = (children: any) => {
|
||||
}, [startTime, isComplete])
|
||||
|
||||
useEffect(() => {
|
||||
if (hasEndThink(children) || !isResponding)
|
||||
// Stop timer when:
|
||||
// 1. Content has [ENDTHINKFLAG] marker (normal completion)
|
||||
// 2. isResponding is explicitly false (user clicked stop button)
|
||||
// Note: Don't stop when isResponding is undefined (component used outside ChatContextProvider)
|
||||
if (hasEndThink(children) || isResponding === false)
|
||||
setIsComplete(true)
|
||||
}, [children, isResponding])
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { Fragment } from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
// https://headlessui.com/react/dialog
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ButtonProps } from '@/app/components/base/button'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
@ -4,7 +4,7 @@ import type {
|
||||
IPaginationProps,
|
||||
PageButtonProps,
|
||||
} from './type'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import usePagination from './hook'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ContextBlockType } from '../../types'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import {
|
||||
memo,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ContextBlockType } from '../../types'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { HistoryBlockType } from '../../types'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { $applyNodeReplacement } from 'lexical'
|
||||
import {
|
||||
useCallback,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { HistoryBlockType } from '../../types'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import {
|
||||
$insertNodes,
|
||||
COMMAND_PRIORITY_EDITOR,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import type { HtmlContentProps } from '@/app/components/base/popover'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import { RiAddLine, RiPriceTag3Line } from '@remixicon/react'
|
||||
import { useUnmount } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import Toast, { ToastProvider, useToastContext } from '.'
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
RiErrorWarningFill,
|
||||
RiInformation2Fill,
|
||||
} from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { z } from 'zod'
|
||||
import withValidation from '.'
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { headers } from 'next/headers'
|
||||
import Script from 'next/script'
|
||||
import { memo } from 'react'
|
||||
import { IS_CE_EDITION, ZENDESK_WIDGET_KEY } from '@/config'
|
||||
import { IS_CE_EDITION, IS_PROD, ZENDESK_WIDGET_KEY } from '@/config'
|
||||
|
||||
const Zendesk = async () => {
|
||||
if (IS_CE_EDITION || !ZENDESK_WIDGET_KEY)
|
||||
return null
|
||||
|
||||
const nonce = process.env.NODE_ENV === 'production' ? (await headers()).get('x-nonce') ?? '' : ''
|
||||
const nonce = IS_PROD ? (await headers()).get('x-nonce') ?? '' : ''
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -3,8 +3,6 @@ import * as React from 'react'
|
||||
import { CategoryEnum } from '.'
|
||||
import Footer from './footer'
|
||||
|
||||
let mockTranslations: Record<string, string> = {}
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href, className, target }: { children: React.ReactNode, href: string, className?: string, target?: string }) => (
|
||||
<a href={href} className={className} target={target} data-testid="pricing-link">
|
||||
@ -13,25 +11,9 @@ vi.mock('next/link', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('react-i18next')>()
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
if (mockTranslations[key])
|
||||
return mockTranslations[key]
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('Footer', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockTranslations = {}
|
||||
})
|
||||
|
||||
// Rendering behavior
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { IndexingStatusResponse } from '@/models/datasets'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useReducer } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -18,16 +18,6 @@ const IndexingTypeValues = {
|
||||
// Mock External Dependencies
|
||||
// ==========================================
|
||||
|
||||
// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages)
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock next/link
|
||||
vi.mock('next/link', () => {
|
||||
return function MockLink({ children, href }: { children: React.ReactNode, href: string }) {
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
RiArrowLeftLine,
|
||||
RiSearchEyeLine,
|
||||
} from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
@ -9,16 +9,6 @@ import Processing from './index'
|
||||
// Mock External Dependencies
|
||||
// ==========================================
|
||||
|
||||
// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages)
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const prefix = options?.ns ? `${options.ns}.` : ''
|
||||
return `${prefix}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useDocLink - returns a function that generates doc URLs
|
||||
// Strips leading slash from path to match actual implementation behavior
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ChunkingMode, FileItem } from '@/models/datasets'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Drawer from './drawer'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useCountDown } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -4,7 +4,7 @@ import type { Item } from '@/app/components/base/select'
|
||||
import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types'
|
||||
import type { ChildChunkDetail, SegmentDetailModel, SegmentUpdater } from '@/models/datasets'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { CrawlResultItem, CustomFile, FileIndexingEstimateResponse } from '@/models/datasets'
|
||||
import type { OnlineDriveFile, PublishedPipelineRunPreviewResponse } from '@/models/pipeline'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -9,7 +9,8 @@ import {
|
||||
RiGlobalLine,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { pick, uniq } from 'es-toolkit/compat'
|
||||
import { uniq } from 'es-toolkit/array'
|
||||
import { pick } from 'es-toolkit/object'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
RiPlayCircleLine,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean, useDebounceFn } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { RiArrowLeftLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -4,7 +4,7 @@ import type { MouseEventHandler } from 'react'
|
||||
import type { AppIconSelection } from '../../base/app-icon-picker'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ApiBasedExtension } from '@/models/common'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { DataSourceNotion as TDataSourceNotion } from '@/models/common'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
|
||||
@ -3,7 +3,7 @@ import type { RoleKey } from './role-selector'
|
||||
import type { InvitationResult } from '@/models/common'
|
||||
import { RiCloseLine, RiErrorWarningFill } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReactMultiEmail } from 'react-multi-email'
|
||||
|
||||
@ -2,7 +2,7 @@ import type { InvitationResult } from '@/models/common'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { CheckCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { Fragment, useCallback, useEffect } from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon, PlusIcon } from '@heroicons/react/24/solid'
|
||||
import { noop } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -3,6 +3,7 @@ import type { FC } from 'react'
|
||||
import type { GithubRepo } from '@/models/common'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { IS_DEV } from '@/config'
|
||||
|
||||
const defaultData = {
|
||||
stargazers_count: 110918,
|
||||
@ -21,7 +22,7 @@ const GithubStar: FC<{ className: string }> = (props) => {
|
||||
const { isFetching, isError, data } = useQuery<GithubRepo>({
|
||||
queryKey: ['github-star'],
|
||||
queryFn: getStar,
|
||||
enabled: process.env.NODE_ENV !== 'development',
|
||||
enabled: !IS_DEV,
|
||||
retry: false,
|
||||
placeholderData: defaultData,
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import { camelCase } from 'es-toolkit/compat'
|
||||
import { camelCase } from 'es-toolkit/string'
|
||||
import Link from 'next/link'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@ -21,33 +21,6 @@ import Card from './index'
|
||||
// Mock External Dependencies Only
|
||||
// ================================
|
||||
|
||||
// Mock react-i18next (translation hook)
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useMixedTranslation hook
|
||||
vi.mock('../marketplace/hooks', () => ({
|
||||
useMixedTranslation: (_locale?: string) => ({
|
||||
t: (key: string, options?: { ns?: string }) => {
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
const translations: Record<string, string> = {
|
||||
'plugin.marketplace.partnerTip': 'Partner plugin',
|
||||
'plugin.marketplace.verifiedTip': 'Verified plugin',
|
||||
'plugin.installModal.installWarning': 'Install warning message',
|
||||
}
|
||||
return translations[fullKey] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useGetLanguage context
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en-US',
|
||||
}))
|
||||
|
||||
// Mock useTheme hook
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: 'light' }),
|
||||
|
||||
@ -64,26 +64,20 @@ vi.mock('@/context/app-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
// Handle interpolation params (excluding ns)
|
||||
const { ns: _ns, ...params } = options || {}
|
||||
if (Object.keys(params).length > 0) {
|
||||
return `${fullKey}:${JSON.stringify(params)}`
|
||||
}
|
||||
return fullKey
|
||||
},
|
||||
}),
|
||||
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
|
||||
<span data-testid="trans">
|
||||
{i18nKey}
|
||||
{components?.trustSource}
|
||||
</span>
|
||||
),
|
||||
}))
|
||||
vi.mock('react-i18next', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('react-i18next')>()
|
||||
const { createReactI18nextMock } = await import('@/test/i18n-mock')
|
||||
return {
|
||||
...actual,
|
||||
...createReactI18nextMock(),
|
||||
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
|
||||
<span data-testid="trans">
|
||||
{i18nKey}
|
||||
{components?.trustSource}
|
||||
</span>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ payload, titleLeft }: {
|
||||
|
||||
@ -48,21 +48,6 @@ vi.mock('@/service/plugins', () => ({
|
||||
uploadFile: (...args: unknown[]) => mockUploadFile(...args),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
// Handle interpolation params (excluding ns)
|
||||
const { ns: _ns, ...params } = options || {}
|
||||
if (Object.keys(params).length > 0) {
|
||||
return `${fullKey}:${JSON.stringify(params)}`
|
||||
}
|
||||
return fullKey
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../card', () => ({
|
||||
default: ({ payload, isLoading, loadingFileName }: {
|
||||
payload: { name: string }
|
||||
|
||||
@ -11,7 +11,8 @@ import type {
|
||||
SearchParams,
|
||||
SearchParamsFromCollection,
|
||||
} from './types'
|
||||
import { debounce, noop } from 'es-toolkit/compat'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
|
||||
@ -27,17 +27,17 @@ import {
|
||||
// Mock External Dependencies Only
|
||||
// ================================
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock i18next-config
|
||||
vi.mock('@/i18n-config/i18next-config', () => ({
|
||||
default: {
|
||||
getFixedT: (_locale: string) => (key: string) => key,
|
||||
getFixedT: (_locale: string) => (key: string, options?: Record<string, unknown>) => {
|
||||
if (options && options.ns) {
|
||||
return `${options.ns}.${key}`
|
||||
}
|
||||
else {
|
||||
return key
|
||||
}
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@ -617,8 +617,8 @@ describe('hooks', () => {
|
||||
it('should return translation key when no translation found', () => {
|
||||
const { result } = renderHook(() => useMixedTranslation())
|
||||
|
||||
// The mock returns key as-is
|
||||
expect(result.current.t('category.all', { ns: 'plugin' })).toBe('category.all')
|
||||
// The global mock returns key with namespace prefix
|
||||
expect(result.current.t('category.all', { ns: 'plugin' })).toBe('plugin.category.all')
|
||||
})
|
||||
|
||||
it('should use locale from outer when provided', () => {
|
||||
@ -638,8 +638,8 @@ describe('hooks', () => {
|
||||
|
||||
it('should use getFixedT when localeFromOuter is provided', () => {
|
||||
const { result } = renderHook(() => useMixedTranslation('fr-FR'))
|
||||
// Should still return a function
|
||||
expect(result.current.t('search', { ns: 'plugin' })).toBe('search')
|
||||
// The global mock returns key with namespace prefix
|
||||
expect(result.current.t('search', { ns: 'plugin' })).toBe('plugin.search')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2756,15 +2756,15 @@ describe('PluginTypeSwitch Component', () => {
|
||||
</MarketplaceContextProvider>,
|
||||
)
|
||||
|
||||
// Note: The mock returns the key without namespace prefix
|
||||
expect(screen.getByText('category.all')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.models')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.tools')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.datasources')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.triggers')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.agents')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.extensions')).toBeInTheDocument()
|
||||
expect(screen.getByText('category.bundles')).toBeInTheDocument()
|
||||
// Note: The global mock returns the key with namespace prefix (plugin.)
|
||||
expect(screen.getByText('plugin.category.all')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.models')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.tools')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.datasources')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.triggers')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.agents')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.extensions')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.category.bundles')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply className prop', () => {
|
||||
@ -2794,7 +2794,7 @@ describe('PluginTypeSwitch Component', () => {
|
||||
</MarketplaceContextProvider>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('category.tools'))
|
||||
fireEvent.click(screen.getByText('plugin.category.tools'))
|
||||
expect(screen.getByTestId('active-type-display')).toHaveTextContent('tool')
|
||||
})
|
||||
|
||||
@ -2816,7 +2816,7 @@ describe('PluginTypeSwitch Component', () => {
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('set-model'))
|
||||
const modelOption = screen.getByText('category.models').closest('div')
|
||||
const modelOption = screen.getByText('plugin.category.models').closest('div')
|
||||
expect(modelOption).toHaveClass('shadow-xs')
|
||||
})
|
||||
})
|
||||
|
||||
@ -78,17 +78,6 @@ function createMockLogData(logs: TriggerLogEntity[] = []): { logs: TriggerLogEnt
|
||||
// Mock Setup
|
||||
// ============================================================================
|
||||
|
||||
const mockTranslate = vi.fn((key: string, options?: { ns?: string }) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
return fullKey
|
||||
})
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: mockTranslate,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock plugin store
|
||||
const mockPluginDetail = createMockPluginDetail()
|
||||
const mockUsePluginStore = vi.fn(() => mockPluginDetail)
|
||||
|
||||
@ -68,17 +68,6 @@ function createMockSubscriptionBuilder(overrides: Partial<TriggerSubscriptionBui
|
||||
// Mock Setup
|
||||
// ============================================================================
|
||||
|
||||
const mockTranslate = vi.fn((key: string, options?: { ns?: string }) => {
|
||||
// Build full key with namespace prefix if provided
|
||||
const fullKey = options?.ns ? `${options.ns}.${key}` : key
|
||||
return fullKey
|
||||
})
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: mockTranslate,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock plugin store
|
||||
const mockPluginDetail = createMockPluginDetail()
|
||||
const mockUsePluginStore = vi.fn(() => mockPluginDetail)
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
|
||||
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { isEqual } from 'es-toolkit/compat'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user