mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 18:08:07 +08:00
Merge branch 'main' into feat/mcp-authentication
This commit is contained in:
@ -13,39 +13,60 @@ import { ThemeProvider } from 'next-themes'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const DARK_MODE_MEDIA_QUERY = /prefers-color-scheme:\s*dark/i
|
||||
|
||||
// Setup browser environment for testing
|
||||
const setupMockEnvironment = (storedTheme: string | null, systemPrefersDark = false) => {
|
||||
// Mock localStorage
|
||||
const mockStorage = {
|
||||
getItem: jest.fn((key: string) => {
|
||||
if (key === 'theme') return storedTheme
|
||||
return null
|
||||
}),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
if (typeof window === 'undefined')
|
||||
return
|
||||
|
||||
try {
|
||||
window.localStorage.clear()
|
||||
}
|
||||
catch {
|
||||
// ignore if localStorage has been replaced by a throwing stub
|
||||
}
|
||||
|
||||
// Mock system theme preference
|
||||
const mockMatchMedia = jest.fn((query: string) => ({
|
||||
matches: query.includes('dark') && systemPrefersDark,
|
||||
media: query,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
}))
|
||||
if (storedTheme === null)
|
||||
window.localStorage.removeItem('theme')
|
||||
else
|
||||
window.localStorage.setItem('theme', storedTheme)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockStorage,
|
||||
configurable: true,
|
||||
})
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
value: mockMatchMedia,
|
||||
configurable: true,
|
||||
})
|
||||
const mockMatchMedia: typeof window.matchMedia = (query: string) => {
|
||||
const listeners = new Set<(event: MediaQueryListEvent) => void>()
|
||||
const isDarkQuery = DARK_MODE_MEDIA_QUERY.test(query)
|
||||
const matches = isDarkQuery ? systemPrefersDark : false
|
||||
|
||||
const mediaQueryList: MediaQueryList = {
|
||||
matches,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: (listener: MediaQueryListListener) => {
|
||||
listeners.add(listener)
|
||||
},
|
||||
removeListener: (listener: MediaQueryListListener) => {
|
||||
listeners.delete(listener)
|
||||
},
|
||||
addEventListener: (_event, listener: EventListener) => {
|
||||
if (typeof listener === 'function')
|
||||
listeners.add(listener as MediaQueryListListener)
|
||||
},
|
||||
removeEventListener: (_event, listener: EventListener) => {
|
||||
if (typeof listener === 'function')
|
||||
listeners.delete(listener as MediaQueryListListener)
|
||||
},
|
||||
dispatchEvent: (event: Event) => {
|
||||
listeners.forEach(listener => listener(event as MediaQueryListEvent))
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
return mediaQueryList
|
||||
}
|
||||
|
||||
return { mockStorage, mockMatchMedia }
|
||||
jest.spyOn(window, 'matchMedia').mockImplementation(mockMatchMedia)
|
||||
}
|
||||
|
||||
// Simulate real page component based on Dify's actual theme usage
|
||||
@ -94,7 +115,17 @@ const TestThemeProvider = ({ children }: { children: React.ReactNode }) => (
|
||||
|
||||
describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
jest.clearAllMocks()
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
window.localStorage.clear()
|
||||
}
|
||||
catch {
|
||||
// ignore when localStorage is replaced with an error-throwing stub
|
||||
}
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
}
|
||||
})
|
||||
|
||||
describe('Page Refresh Scenario Simulation', () => {
|
||||
@ -323,35 +354,40 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
test('handles localStorage access errors gracefully', async () => {
|
||||
// Mock localStorage to throw an error
|
||||
setupMockEnvironment(null)
|
||||
|
||||
const mockStorage = {
|
||||
getItem: jest.fn(() => {
|
||||
throw new Error('LocalStorage access denied')
|
||||
}),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockStorage,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
render(
|
||||
<TestThemeProvider>
|
||||
<PageComponent />
|
||||
</TestThemeProvider>,
|
||||
)
|
||||
|
||||
// Should fallback gracefully without crashing
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('theme-indicator')).toBeInTheDocument()
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockStorage,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
// Should default to light theme when localStorage fails
|
||||
expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light')
|
||||
try {
|
||||
render(
|
||||
<TestThemeProvider>
|
||||
<PageComponent />
|
||||
</TestThemeProvider>,
|
||||
)
|
||||
|
||||
// Should fallback gracefully without crashing
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('theme-indicator')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Should default to light theme when localStorage fails
|
||||
expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light')
|
||||
}
|
||||
finally {
|
||||
Reflect.deleteProperty(window, 'localStorage')
|
||||
}
|
||||
})
|
||||
|
||||
test('handles invalid theme values in localStorage', async () => {
|
||||
@ -403,6 +439,8 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => {
|
||||
|
||||
setupMockEnvironment('dark')
|
||||
|
||||
expect(window.localStorage.getItem('theme')).toBe('dark')
|
||||
|
||||
render(
|
||||
<TestThemeProvider>
|
||||
<PerformanceTestComponent />
|
||||
|
||||
@ -17,12 +17,9 @@ import type {
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
export type EmbeddedChatbotContextValue = {
|
||||
userCanAccess?: boolean
|
||||
appInfoError?: any
|
||||
appInfoLoading?: boolean
|
||||
appMeta?: AppMeta
|
||||
appData?: AppData
|
||||
appParams?: ChatConfig
|
||||
appMeta: AppMeta | null
|
||||
appData: AppData | null
|
||||
appParams: ChatConfig | null
|
||||
appChatListDataLoading?: boolean
|
||||
currentConversationId: string
|
||||
currentConversationItem?: ConversationItem
|
||||
@ -59,7 +56,10 @@ export type EmbeddedChatbotContextValue = {
|
||||
}
|
||||
|
||||
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
|
||||
userCanAccess: false,
|
||||
appData: null,
|
||||
appMeta: null,
|
||||
appParams: null,
|
||||
appChatListDataLoading: false,
|
||||
currentConversationId: '',
|
||||
appPrevChatList: [],
|
||||
pinnedConversationList: [],
|
||||
|
||||
@ -18,9 +18,6 @@ import { CONVERSATION_ID_INFO } from '../constants'
|
||||
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
|
||||
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
|
||||
import {
|
||||
fetchAppInfo,
|
||||
fetchAppMeta,
|
||||
fetchAppParams,
|
||||
fetchChatList,
|
||||
fetchConversations,
|
||||
generationConversationName,
|
||||
@ -36,8 +33,7 @@ import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
|
||||
function getFormattedChatList(messages: any[]) {
|
||||
const newChatList: ChatItem[] = []
|
||||
@ -67,18 +63,10 @@ function getFormattedChatList(messages: any[]) {
|
||||
|
||||
export const useEmbeddedChatbot = () => {
|
||||
const isInstalledApp = false
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo)
|
||||
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
|
||||
appId: appInfo?.app_id,
|
||||
isInstalledApp,
|
||||
enabled: systemFeatures.webapp_auth.enabled,
|
||||
})
|
||||
|
||||
const appData = useMemo(() => {
|
||||
return appInfo
|
||||
}, [appInfo])
|
||||
const appId = useMemo(() => appData?.app_id, [appData])
|
||||
const appInfo = useWebAppStore(s => s.appInfo)
|
||||
const appMeta = useWebAppStore(s => s.appMeta)
|
||||
const appParams = useWebAppStore(s => s.appParams)
|
||||
const appId = useMemo(() => appInfo?.app_id, [appInfo])
|
||||
|
||||
const [userId, setUserId] = useState<string>()
|
||||
const [conversationId, setConversationId] = useState<string>()
|
||||
@ -145,8 +133,6 @@ export const useEmbeddedChatbot = () => {
|
||||
return currentConversationId
|
||||
}, [currentConversationId, newConversationId])
|
||||
|
||||
const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId))
|
||||
const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId))
|
||||
const { data: appPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100))
|
||||
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
|
||||
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
|
||||
@ -398,16 +384,13 @@ export const useEmbeddedChatbot = () => {
|
||||
}, [isInstalledApp, appId, t, notify])
|
||||
|
||||
return {
|
||||
appInfoError,
|
||||
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
|
||||
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
|
||||
isInstalledApp,
|
||||
allowResetChat,
|
||||
appId,
|
||||
currentConversationId,
|
||||
currentConversationItem,
|
||||
handleConversationIdInfoChange,
|
||||
appData,
|
||||
appData: appInfo,
|
||||
appParams: appParams || {} as ChatConfig,
|
||||
appMeta,
|
||||
appPinnedConversationData,
|
||||
|
||||
@ -101,7 +101,6 @@ const EmbeddedChatbotWrapper = () => {
|
||||
|
||||
const {
|
||||
appData,
|
||||
userCanAccess,
|
||||
appParams,
|
||||
appMeta,
|
||||
appChatListDataLoading,
|
||||
@ -135,7 +134,6 @@ const EmbeddedChatbotWrapper = () => {
|
||||
} = useEmbeddedChatbot()
|
||||
|
||||
return <EmbeddedChatbotContext.Provider value={{
|
||||
userCanAccess,
|
||||
appData,
|
||||
appParams,
|
||||
appMeta,
|
||||
|
||||
152
web/app/components/base/inline-delete-confirm/index.spec.tsx
Normal file
152
web/app/components/base/inline-delete-confirm/index.spec.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import React from 'react'
|
||||
import { cleanup, fireEvent, render } from '@testing-library/react'
|
||||
import InlineDeleteConfirm from './index'
|
||||
|
||||
// Mock react-i18next
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'common.operation.deleteConfirmTitle': 'Delete?',
|
||||
'common.operation.yes': 'Yes',
|
||||
'common.operation.no': 'No',
|
||||
'common.operation.confirmAction': 'Please confirm your action.',
|
||||
}
|
||||
return translations[key] || key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('InlineDeleteConfirm', () => {
|
||||
describe('Rendering', () => {
|
||||
test('should render with default text', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
expect(getByText('Delete?')).toBeInTheDocument()
|
||||
expect(getByText('No')).toBeInTheDocument()
|
||||
expect(getByText('Yes')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should render with custom text', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm
|
||||
title="Remove?"
|
||||
confirmText="Confirm"
|
||||
cancelText="Cancel"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(getByText('Remove?')).toBeInTheDocument()
|
||||
expect(getByText('Cancel')).toBeInTheDocument()
|
||||
expect(getByText('Confirm')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('should have proper ARIA attributes', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { container } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveAttribute('aria-labelledby', 'inline-delete-confirm-title')
|
||||
expect(wrapper).toHaveAttribute('aria-describedby', 'inline-delete-confirm-description')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Button interactions', () => {
|
||||
test('should call onCancel when cancel button is clicked', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('No'))
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
expect(onConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should call onConfirm when confirm button is clicked', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
fireEvent.click(getByText('Yes'))
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
expect(onCancel).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Variant prop', () => {
|
||||
test('should render with delete variant by default', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm onConfirm={onConfirm} onCancel={onCancel} />,
|
||||
)
|
||||
|
||||
const confirmButton = getByText('Yes').closest('button')
|
||||
expect(confirmButton?.className).toContain('btn-destructive')
|
||||
})
|
||||
|
||||
test('should render without destructive class for warning variant', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm
|
||||
variant="warning"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>,
|
||||
)
|
||||
|
||||
const confirmButton = getByText('Yes').closest('button')
|
||||
expect(confirmButton?.className).not.toContain('btn-destructive')
|
||||
})
|
||||
|
||||
test('should render without destructive class for info variant', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { getByText } = render(
|
||||
<InlineDeleteConfirm
|
||||
variant="info"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>,
|
||||
)
|
||||
|
||||
const confirmButton = getByText('Yes').closest('button')
|
||||
expect(confirmButton?.className).not.toContain('btn-destructive')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom className', () => {
|
||||
test('should apply custom className to wrapper', () => {
|
||||
const onConfirm = jest.fn()
|
||||
const onCancel = jest.fn()
|
||||
const { container } = render(
|
||||
<InlineDeleteConfirm
|
||||
className="custom-class"
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>,
|
||||
)
|
||||
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper.className).toContain('custom-class')
|
||||
})
|
||||
})
|
||||
})
|
||||
83
web/app/components/base/inline-delete-confirm/index.tsx
Normal file
83
web/app/components/base/inline-delete-confirm/index.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type InlineDeleteConfirmProps = {
|
||||
title?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
className?: string
|
||||
variant?: 'delete' | 'warning' | 'info'
|
||||
}
|
||||
|
||||
const InlineDeleteConfirm: FC<InlineDeleteConfirmProps> = ({
|
||||
title,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
className,
|
||||
variant = 'delete',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const titleText = title || t('common.operation.deleteConfirmTitle', 'Delete?')
|
||||
const confirmTxt = confirmText || t('common.operation.yes', 'Yes')
|
||||
const cancelTxt = cancelText || t('common.operation.no', 'No')
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-labelledby="inline-delete-confirm-title"
|
||||
aria-describedby="inline-delete-confirm-description"
|
||||
className={cn(
|
||||
'flex w-[120px] flex-col justify-center gap-1.5',
|
||||
'rounded-[10px] border-[0.5px] border-components-panel-border-subtle',
|
||||
'bg-components-panel-bg-blur px-2 pb-2 pt-1.5',
|
||||
'backdrop-blur-[10px]',
|
||||
'shadow-lg',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
id="inline-delete-confirm-title"
|
||||
className="system-xs-semibold text-text-primary"
|
||||
>
|
||||
{titleText}
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center justify-center gap-1">
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
aria-label={cancelTxt}
|
||||
className="flex-1"
|
||||
>
|
||||
{cancelTxt}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="primary"
|
||||
destructive={variant === 'delete'}
|
||||
onClick={onConfirm}
|
||||
aria-label={confirmTxt}
|
||||
className="flex-1"
|
||||
>
|
||||
{confirmTxt}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<span id="inline-delete-confirm-description" className="sr-only">
|
||||
{t('common.operation.confirmAction', 'Please confirm your action.')}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
InlineDeleteConfirm.displayName = 'InlineDeleteConfirm'
|
||||
|
||||
export default InlineDeleteConfirm
|
||||
@ -7,6 +7,7 @@ import { useInvalidateStrategyProviders } from '@/service/use-strategy'
|
||||
import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types'
|
||||
import { PluginType } from '../../types'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import { useInvalidDataSourceListAuth } from '@/service/use-datasource'
|
||||
|
||||
const useRefreshPluginList = () => {
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
@ -19,6 +20,8 @@ const useRefreshPluginList = () => {
|
||||
const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()
|
||||
const invalidateAllDataSources = useInvalidDataSourceList()
|
||||
|
||||
const invalidateDataSourceListAuth = useInvalidDataSourceListAuth()
|
||||
|
||||
const invalidateStrategyProviders = useInvalidateStrategyProviders()
|
||||
return {
|
||||
refreshPluginList: (manifest?: PluginManifestInMarket | Plugin | PluginDeclaration | null, refreshAllType?: boolean) => {
|
||||
@ -32,8 +35,10 @@ const useRefreshPluginList = () => {
|
||||
// TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins
|
||||
}
|
||||
|
||||
if ((manifest && PluginType.datasource.includes(manifest.category)) || refreshAllType)
|
||||
if ((manifest && PluginType.datasource.includes(manifest.category)) || refreshAllType) {
|
||||
invalidateAllDataSources()
|
||||
invalidateDataSourceListAuth()
|
||||
}
|
||||
|
||||
// model select
|
||||
if ((manifest && PluginType.model.includes(manifest.category)) || refreshAllType) {
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { BlockEnum } from '../types'
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
@ -14,10 +13,12 @@ import type { DataSourceDefaultValue, ToolDefaultValue } from './types'
|
||||
import Tools from './tools'
|
||||
import { ViewType } from './view-type-select'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import PluginList, { type ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE } from './constants'
|
||||
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
||||
import { PluginType } from '../../plugins/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
|
||||
type AllToolsProps = {
|
||||
className?: string
|
||||
@ -34,9 +35,26 @@ const DataSources = ({
|
||||
onSelect,
|
||||
dataSources,
|
||||
}: AllToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const pluginRef = useRef<ListRef>(null)
|
||||
const wrapElemRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isMatchingKeywords = (text: string, keywords: string) => {
|
||||
return text.toLowerCase().includes(keywords.toLowerCase())
|
||||
}
|
||||
|
||||
const filteredDatasources = useMemo(() => {
|
||||
const hasFilter = searchText
|
||||
if (!hasFilter)
|
||||
return dataSources.filter(toolWithProvider => toolWithProvider.tools.length > 0)
|
||||
|
||||
return dataSources.filter((toolWithProvider) => {
|
||||
return isMatchingKeywords(toolWithProvider.name, searchText) || toolWithProvider.tools.some((tool) => {
|
||||
return tool.label[language].toLowerCase().includes(searchText.toLowerCase()) || tool.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
})
|
||||
})
|
||||
}, [searchText, dataSources, language])
|
||||
|
||||
const handleSelect = useCallback((_: any, toolDefaultValue: ToolDefaultValue) => {
|
||||
let defaultValue: DataSourceDefaultValue = {
|
||||
plugin_id: toolDefaultValue?.provider_id,
|
||||
@ -55,8 +73,24 @@ const DataSources = ({
|
||||
}
|
||||
onSelect(BlockEnum.DataSource, toolDefaultValue && defaultValue)
|
||||
}, [onSelect])
|
||||
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
const {
|
||||
queryPluginsWithDebounced: fetchPlugins,
|
||||
plugins: notInstalledPlugins = [],
|
||||
} = useMarketplacePlugins()
|
||||
|
||||
useEffect(() => {
|
||||
if (!enable_marketplace) return
|
||||
if (searchText) {
|
||||
fetchPlugins({
|
||||
query: searchText,
|
||||
category: PluginType.datasource,
|
||||
})
|
||||
}
|
||||
}, [searchText, enable_marketplace])
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<div
|
||||
@ -66,24 +100,23 @@ const DataSources = ({
|
||||
>
|
||||
<Tools
|
||||
className={toolContentClassName}
|
||||
tools={dataSources}
|
||||
tools={filteredDatasources}
|
||||
onSelect={handleSelect as OnSelectBlock}
|
||||
viewType={ViewType.flat}
|
||||
hasSearchText={!!searchText}
|
||||
canNotSelectMultiple
|
||||
/>
|
||||
{
|
||||
enable_marketplace && (
|
||||
<Link
|
||||
className='system-sm-medium sticky bottom-0 z-10 flex h-8 cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg'
|
||||
href={getMarketplaceUrl('')}
|
||||
target='_blank'
|
||||
>
|
||||
<span>{t('plugin.findMoreInMarketplace')}</span>
|
||||
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
{/* Plugins from marketplace */}
|
||||
{enable_marketplace && (
|
||||
<PluginList
|
||||
ref={pluginRef}
|
||||
wrapElemRef={wrapElemRef}
|
||||
list={notInstalledPlugins}
|
||||
tags={[]}
|
||||
searchText={searchText}
|
||||
toolContentClassName={toolContentClassName}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
94
web/app/components/workflow/nodes/components.ts
Normal file
94
web/app/components/workflow/nodes/components.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import { BlockEnum } from '../types'
|
||||
import StartNode from './start/node'
|
||||
import StartPanel from './start/panel'
|
||||
import EndNode from './end/node'
|
||||
import EndPanel from './end/panel'
|
||||
import AnswerNode from './answer/node'
|
||||
import AnswerPanel from './answer/panel'
|
||||
import LLMNode from './llm/node'
|
||||
import LLMPanel from './llm/panel'
|
||||
import KnowledgeRetrievalNode from './knowledge-retrieval/node'
|
||||
import KnowledgeRetrievalPanel from './knowledge-retrieval/panel'
|
||||
import QuestionClassifierNode from './question-classifier/node'
|
||||
import QuestionClassifierPanel from './question-classifier/panel'
|
||||
import IfElseNode from './if-else/node'
|
||||
import IfElsePanel from './if-else/panel'
|
||||
import CodeNode from './code/node'
|
||||
import CodePanel from './code/panel'
|
||||
import TemplateTransformNode from './template-transform/node'
|
||||
import TemplateTransformPanel from './template-transform/panel'
|
||||
import HttpNode from './http/node'
|
||||
import HttpPanel from './http/panel'
|
||||
import ToolNode from './tool/node'
|
||||
import ToolPanel from './tool/panel'
|
||||
import VariableAssignerNode from './variable-assigner/node'
|
||||
import VariableAssignerPanel from './variable-assigner/panel'
|
||||
import AssignerNode from './assigner/node'
|
||||
import AssignerPanel from './assigner/panel'
|
||||
import ParameterExtractorNode from './parameter-extractor/node'
|
||||
import ParameterExtractorPanel from './parameter-extractor/panel'
|
||||
import IterationNode from './iteration/node'
|
||||
import IterationPanel from './iteration/panel'
|
||||
import LoopNode from './loop/node'
|
||||
import LoopPanel from './loop/panel'
|
||||
import DocExtractorNode from './document-extractor/node'
|
||||
import DocExtractorPanel from './document-extractor/panel'
|
||||
import ListFilterNode from './list-operator/node'
|
||||
import ListFilterPanel from './list-operator/panel'
|
||||
import AgentNode from './agent/node'
|
||||
import AgentPanel from './agent/panel'
|
||||
import DataSourceNode from './data-source/node'
|
||||
import DataSourcePanel from './data-source/panel'
|
||||
import KnowledgeBaseNode from './knowledge-base/node'
|
||||
import KnowledgeBasePanel from './knowledge-base/panel'
|
||||
|
||||
export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartNode,
|
||||
[BlockEnum.End]: EndNode,
|
||||
[BlockEnum.Answer]: AnswerNode,
|
||||
[BlockEnum.LLM]: LLMNode,
|
||||
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalNode,
|
||||
[BlockEnum.QuestionClassifier]: QuestionClassifierNode,
|
||||
[BlockEnum.IfElse]: IfElseNode,
|
||||
[BlockEnum.Code]: CodeNode,
|
||||
[BlockEnum.TemplateTransform]: TemplateTransformNode,
|
||||
[BlockEnum.HttpRequest]: HttpNode,
|
||||
[BlockEnum.Tool]: ToolNode,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerNode,
|
||||
[BlockEnum.Assigner]: AssignerNode,
|
||||
[BlockEnum.VariableAggregator]: VariableAssignerNode,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
|
||||
[BlockEnum.Iteration]: IterationNode,
|
||||
[BlockEnum.Loop]: LoopNode,
|
||||
[BlockEnum.DocExtractor]: DocExtractorNode,
|
||||
[BlockEnum.ListFilter]: ListFilterNode,
|
||||
[BlockEnum.Agent]: AgentNode,
|
||||
[BlockEnum.DataSource]: DataSourceNode,
|
||||
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
|
||||
}
|
||||
|
||||
export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartPanel,
|
||||
[BlockEnum.End]: EndPanel,
|
||||
[BlockEnum.Answer]: AnswerPanel,
|
||||
[BlockEnum.LLM]: LLMPanel,
|
||||
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalPanel,
|
||||
[BlockEnum.QuestionClassifier]: QuestionClassifierPanel,
|
||||
[BlockEnum.IfElse]: IfElsePanel,
|
||||
[BlockEnum.Code]: CodePanel,
|
||||
[BlockEnum.TemplateTransform]: TemplateTransformPanel,
|
||||
[BlockEnum.HttpRequest]: HttpPanel,
|
||||
[BlockEnum.Tool]: ToolPanel,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
|
||||
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
|
||||
[BlockEnum.Assigner]: AssignerPanel,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
|
||||
[BlockEnum.Iteration]: IterationPanel,
|
||||
[BlockEnum.Loop]: LoopPanel,
|
||||
[BlockEnum.DocExtractor]: DocExtractorPanel,
|
||||
[BlockEnum.ListFilter]: ListFilterPanel,
|
||||
[BlockEnum.Agent]: AgentPanel,
|
||||
[BlockEnum.DataSource]: DataSourcePanel,
|
||||
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
|
||||
}
|
||||
@ -1,101 +1,5 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import { BlockEnum } from '../types'
|
||||
import StartNode from './start/node'
|
||||
import StartPanel from './start/panel'
|
||||
import EndNode from './end/node'
|
||||
import EndPanel from './end/panel'
|
||||
import AnswerNode from './answer/node'
|
||||
import AnswerPanel from './answer/panel'
|
||||
import LLMNode from './llm/node'
|
||||
import LLMPanel from './llm/panel'
|
||||
import KnowledgeRetrievalNode from './knowledge-retrieval/node'
|
||||
import KnowledgeRetrievalPanel from './knowledge-retrieval/panel'
|
||||
import QuestionClassifierNode from './question-classifier/node'
|
||||
import QuestionClassifierPanel from './question-classifier/panel'
|
||||
import IfElseNode from './if-else/node'
|
||||
import IfElsePanel from './if-else/panel'
|
||||
import CodeNode from './code/node'
|
||||
import CodePanel from './code/panel'
|
||||
import TemplateTransformNode from './template-transform/node'
|
||||
import TemplateTransformPanel from './template-transform/panel'
|
||||
import HttpNode from './http/node'
|
||||
import HttpPanel from './http/panel'
|
||||
import ToolNode from './tool/node'
|
||||
import ToolPanel from './tool/panel'
|
||||
import VariableAssignerNode from './variable-assigner/node'
|
||||
import VariableAssignerPanel from './variable-assigner/panel'
|
||||
import AssignerNode from './assigner/node'
|
||||
import AssignerPanel from './assigner/panel'
|
||||
import ParameterExtractorNode from './parameter-extractor/node'
|
||||
import ParameterExtractorPanel from './parameter-extractor/panel'
|
||||
import IterationNode from './iteration/node'
|
||||
import IterationPanel from './iteration/panel'
|
||||
import LoopNode from './loop/node'
|
||||
import LoopPanel from './loop/panel'
|
||||
import DocExtractorNode from './document-extractor/node'
|
||||
import DocExtractorPanel from './document-extractor/panel'
|
||||
import ListFilterNode from './list-operator/node'
|
||||
import ListFilterPanel from './list-operator/panel'
|
||||
import AgentNode from './agent/node'
|
||||
import AgentPanel from './agent/panel'
|
||||
import DataSourceNode from './data-source/node'
|
||||
import DataSourcePanel from './data-source/panel'
|
||||
import KnowledgeBaseNode from './knowledge-base/node'
|
||||
import KnowledgeBasePanel from './knowledge-base/panel'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartNode,
|
||||
[BlockEnum.End]: EndNode,
|
||||
[BlockEnum.Answer]: AnswerNode,
|
||||
[BlockEnum.LLM]: LLMNode,
|
||||
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalNode,
|
||||
[BlockEnum.QuestionClassifier]: QuestionClassifierNode,
|
||||
[BlockEnum.IfElse]: IfElseNode,
|
||||
[BlockEnum.Code]: CodeNode,
|
||||
[BlockEnum.TemplateTransform]: TemplateTransformNode,
|
||||
[BlockEnum.HttpRequest]: HttpNode,
|
||||
[BlockEnum.Tool]: ToolNode,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerNode,
|
||||
[BlockEnum.Assigner]: AssignerNode,
|
||||
[BlockEnum.VariableAggregator]: VariableAssignerNode,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorNode,
|
||||
[BlockEnum.Iteration]: IterationNode,
|
||||
[BlockEnum.Loop]: LoopNode,
|
||||
[BlockEnum.DocExtractor]: DocExtractorNode,
|
||||
[BlockEnum.ListFilter]: ListFilterNode,
|
||||
[BlockEnum.Agent]: AgentNode,
|
||||
[BlockEnum.DataSource]: DataSourceNode,
|
||||
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
|
||||
}
|
||||
|
||||
export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
||||
[BlockEnum.Start]: StartPanel,
|
||||
[BlockEnum.End]: EndPanel,
|
||||
[BlockEnum.Answer]: AnswerPanel,
|
||||
[BlockEnum.LLM]: LLMPanel,
|
||||
[BlockEnum.KnowledgeRetrieval]: KnowledgeRetrievalPanel,
|
||||
[BlockEnum.QuestionClassifier]: QuestionClassifierPanel,
|
||||
[BlockEnum.IfElse]: IfElsePanel,
|
||||
[BlockEnum.Code]: CodePanel,
|
||||
[BlockEnum.TemplateTransform]: TemplateTransformPanel,
|
||||
[BlockEnum.HttpRequest]: HttpPanel,
|
||||
[BlockEnum.Tool]: ToolPanel,
|
||||
[BlockEnum.VariableAssigner]: VariableAssignerPanel,
|
||||
[BlockEnum.VariableAggregator]: VariableAssignerPanel,
|
||||
[BlockEnum.Assigner]: AssignerPanel,
|
||||
[BlockEnum.ParameterExtractor]: ParameterExtractorPanel,
|
||||
[BlockEnum.Iteration]: IterationPanel,
|
||||
[BlockEnum.Loop]: LoopPanel,
|
||||
[BlockEnum.DocExtractor]: DocExtractorPanel,
|
||||
[BlockEnum.ListFilter]: ListFilterPanel,
|
||||
[BlockEnum.Agent]: AgentPanel,
|
||||
[BlockEnum.DataSource]: DataSourcePanel,
|
||||
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
|
||||
}
|
||||
|
||||
export const CUSTOM_NODE_TYPE = 'custom'
|
||||
|
||||
export const FILE_TYPE_OPTIONS = [
|
||||
{ value: 'image', i18nKey: 'image' },
|
||||
{ value: 'document', i18nKey: 'doc' },
|
||||
|
||||
@ -8,7 +8,7 @@ import { CUSTOM_NODE } from '../constants'
|
||||
import {
|
||||
NodeComponentMap,
|
||||
PanelComponentMap,
|
||||
} from './constants'
|
||||
} from './components'
|
||||
import BaseNode from './_base/node'
|
||||
import BasePanel from './_base/components/workflow-panel'
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ const ChunkStructure = ({
|
||||
<Field
|
||||
fieldTitleProps={{
|
||||
title: t('workflow.nodes.knowledgeBase.chunkStructure'),
|
||||
tooltip: t('workflow.nodes.knowledgeBase.chunkStructure'),
|
||||
tooltip: t('workflow.nodes.knowledgeBase.chunkStructureTip.message'),
|
||||
operation: chunkStructure && (
|
||||
<Selector
|
||||
options={options}
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
selectAll: 'Alles auswählen',
|
||||
deSelectAll: 'Alle abwählen',
|
||||
config: 'Konfiguration',
|
||||
yes: 'Ja',
|
||||
deleteConfirmTitle: 'Löschen?',
|
||||
no: 'Nein',
|
||||
confirmAction: 'Bitte bestätigen Sie Ihre Aktion.',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Bitte eingeben',
|
||||
|
||||
@ -18,6 +18,10 @@ const translation = {
|
||||
cancel: 'Cancel',
|
||||
clear: 'Clear',
|
||||
save: 'Save',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
deleteConfirmTitle: 'Delete?',
|
||||
confirmAction: 'Please confirm your action.',
|
||||
saveAndEnable: 'Save & Enable',
|
||||
edit: 'Edit',
|
||||
add: 'Add',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
deSelectAll: 'Deseleccionar todo',
|
||||
selectAll: 'Seleccionar todo',
|
||||
config: 'Config',
|
||||
confirmAction: 'Por favor, confirme su acción.',
|
||||
deleteConfirmTitle: '¿Eliminar?',
|
||||
yes: 'Sí',
|
||||
no: 'No',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} es requerido',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
selectAll: 'انتخاب همه',
|
||||
deSelectAll: 'همه را انتخاب نکنید',
|
||||
config: 'تنظیمات',
|
||||
no: 'نه',
|
||||
deleteConfirmTitle: 'حذف شود؟',
|
||||
yes: 'بله',
|
||||
confirmAction: 'لطفاً اقدام خود را تأیید کنید.',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} الزامی است',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
deSelectAll: 'Désélectionner tout',
|
||||
selectAll: 'Sélectionner tout',
|
||||
config: 'Config',
|
||||
no: 'Non',
|
||||
confirmAction: 'Veuillez confirmer votre action.',
|
||||
deleteConfirmTitle: 'Supprimer ?',
|
||||
yes: 'Oui',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Veuillez entrer',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
selectAll: 'सभी चुनें',
|
||||
deSelectAll: 'सभी चयन हटाएँ',
|
||||
config: 'कॉन्फ़िगरेशन',
|
||||
no: 'नहीं',
|
||||
yes: 'हाँ',
|
||||
deleteConfirmTitle: 'हटाएं?',
|
||||
confirmAction: 'कृपया अपनी क्रिया की पुष्टि करें।',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} आवश्यक है',
|
||||
|
||||
@ -67,6 +67,10 @@ const translation = {
|
||||
sure: 'Saya yakin',
|
||||
imageCopied: 'Gambar yang disalin',
|
||||
config: 'Konfigurasi',
|
||||
deleteConfirmTitle: 'Hapus?',
|
||||
confirmAction: 'Silakan konfirmasi tindakan Anda.',
|
||||
yes: 'Ya',
|
||||
no: 'Tidak',
|
||||
},
|
||||
errorMsg: {
|
||||
urlError: 'URL harus dimulai dengan http:// atau https://',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
selectAll: 'Seleziona tutto',
|
||||
deSelectAll: 'Deseleziona tutto',
|
||||
config: 'Config',
|
||||
no: 'No',
|
||||
yes: 'Sì',
|
||||
confirmAction: 'Per favore conferma la tua azione.',
|
||||
deleteConfirmTitle: 'Eliminare?',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} è obbligatorio',
|
||||
|
||||
@ -67,6 +67,10 @@ const translation = {
|
||||
selectAll: 'すべて選択',
|
||||
deSelectAll: 'すべて選択解除',
|
||||
config: 'コンフィグ',
|
||||
yes: 'はい',
|
||||
no: 'いいえ',
|
||||
deleteConfirmTitle: '削除しますか?',
|
||||
confirmAction: '操作を確認してください。',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}}は必要です',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
selectAll: '모두 선택',
|
||||
deSelectAll: '모두 선택 해제',
|
||||
config: '구성',
|
||||
no: '아니요',
|
||||
yes: '네',
|
||||
deleteConfirmTitle: '삭제하시겠습니까?',
|
||||
confirmAction: '귀하의 행동을 확인해 주세요.',
|
||||
},
|
||||
placeholder: {
|
||||
input: '입력해주세요',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
deSelectAll: 'Odznacz wszystkie',
|
||||
selectAll: 'Zaznacz wszystkie',
|
||||
config: 'Konfiguracja',
|
||||
yes: 'Tak',
|
||||
no: 'Nie',
|
||||
deleteConfirmTitle: 'Usunąć?',
|
||||
confirmAction: 'Proszę potwierdzić swoją akcję.',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Proszę wprowadzić',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
deSelectAll: 'Desmarcar tudo',
|
||||
selectAll: 'Selecionar tudo',
|
||||
config: 'Configuração',
|
||||
no: 'Não',
|
||||
yes: 'Sim',
|
||||
deleteConfirmTitle: 'Excluir?',
|
||||
confirmAction: 'Por favor, confirme sua ação.',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Por favor, insira',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
deSelectAll: 'Deselectați tot',
|
||||
selectAll: 'Selectați tot',
|
||||
config: 'Configurație',
|
||||
yes: 'Da',
|
||||
deleteConfirmTitle: 'Ștergere?',
|
||||
no: 'Nu',
|
||||
confirmAction: 'Vă rugăm să confirmați acțiunea dumneavoastră.',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Vă rugăm să introduceți',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
selectAll: 'Выбрать все',
|
||||
deSelectAll: 'Снять выделение со всех',
|
||||
config: 'Конфигурация',
|
||||
yes: 'Да',
|
||||
no: 'Нет',
|
||||
deleteConfirmTitle: 'Удалить?',
|
||||
confirmAction: 'Пожалуйста, подтвердите ваше действие.',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} обязательно',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
selectAll: 'Izberi vse',
|
||||
deSelectAll: 'Odberi vse',
|
||||
config: 'Konfiguracija',
|
||||
no: 'Ne',
|
||||
confirmAction: 'Prosimo, potrdite svoje dejanje.',
|
||||
deleteConfirmTitle: 'Izbrisati?',
|
||||
yes: 'Da',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} je obvezno',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
selectAll: 'เลือกทั้งหมด',
|
||||
deSelectAll: 'ยกเลิกการเลือกทั้งหมด',
|
||||
config: 'การตั้งค่า',
|
||||
no: 'ไม่',
|
||||
deleteConfirmTitle: 'ลบหรือไม่?',
|
||||
confirmAction: 'กรุณายืนยันการกระทำของคุณ',
|
||||
yes: 'ใช่',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} เป็นสิ่งจําเป็น',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
selectAll: 'Hepsini Seç',
|
||||
deSelectAll: 'Hepsini Seçme',
|
||||
config: 'Konfigürasyon',
|
||||
no: 'Hayır',
|
||||
yes: 'Evet',
|
||||
deleteConfirmTitle: 'Silinsin mi?',
|
||||
confirmAction: 'Lütfen işleminizi onaylayın.',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} gereklidir',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
deSelectAll: 'Вимкнути все',
|
||||
selectAll: 'Вибрати все',
|
||||
config: 'Конфігурація',
|
||||
yes: 'Так',
|
||||
no: 'Ні',
|
||||
deleteConfirmTitle: 'Видалити?',
|
||||
confirmAction: 'Будь ласка, підтвердіть свої дії.',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Будь ласка, введіть текст',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
deSelectAll: 'Bỏ chọn tất cả',
|
||||
selectAll: 'Chọn Tất Cả',
|
||||
config: 'Cấu hình',
|
||||
no: 'Không',
|
||||
yes: 'Vâng',
|
||||
deleteConfirmTitle: 'Xóa?',
|
||||
confirmAction: 'Vui lòng xác nhận hành động của bạn.',
|
||||
},
|
||||
placeholder: {
|
||||
input: 'Vui lòng nhập',
|
||||
|
||||
@ -18,6 +18,10 @@ const translation = {
|
||||
cancel: '取消',
|
||||
clear: '清空',
|
||||
save: '保存',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
deleteConfirmTitle: '删除?',
|
||||
confirmAction: '请确认您的操作。',
|
||||
saveAndEnable: '保存并启用',
|
||||
edit: '编辑',
|
||||
add: '添加',
|
||||
|
||||
@ -61,6 +61,10 @@ const translation = {
|
||||
deSelectAll: '全不選',
|
||||
selectAll: '全選',
|
||||
config: '配置',
|
||||
yes: '是',
|
||||
confirmAction: '請確認您的操作。',
|
||||
deleteConfirmTitle: '刪除?',
|
||||
no: '不',
|
||||
},
|
||||
placeholder: {
|
||||
input: '請輸入',
|
||||
|
||||
@ -160,7 +160,11 @@ const config: Config = {
|
||||
testEnvironment: '@happy-dom/jest-environment',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
testEnvironmentOptions: {
|
||||
// Match happy-dom's default to ensure Node.js environment resolution
|
||||
// This prevents ESM packages like uuid from using browser exports
|
||||
customExportConditions: ['node', 'node-addons'],
|
||||
},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
@ -189,10 +193,10 @@ const config: Config = {
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
// For pnpm: allow transforming uuid ESM package
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(.pnpm|uuid))',
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
"@lexical/react": "^0.36.2",
|
||||
"@lexical/selection": "^0.36.2",
|
||||
"@lexical/text": "^0.36.2",
|
||||
"@lexical/utils": "^0.36.2",
|
||||
"@lexical/utils": "^0.37.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@octokit/core": "^6.1.2",
|
||||
"@octokit/request-error": "^6.1.5",
|
||||
@ -143,7 +143,7 @@
|
||||
"@babel/core": "^7.28.3",
|
||||
"@chromatic-com/storybook": "^3.1.0",
|
||||
"@eslint-react/eslint-plugin": "^1.15.0",
|
||||
"@happy-dom/jest-environment": "^17.4.4",
|
||||
"@happy-dom/jest-environment": "^20.0.0",
|
||||
"@mdx-js/loader": "^3.1.0",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@next/bundle-analyzer": "15.5.4",
|
||||
@ -190,7 +190,7 @@
|
||||
"globals": "^15.11.0",
|
||||
"husky": "^9.1.6",
|
||||
"jest": "^29.7.0",
|
||||
"knip": "^5.64.1",
|
||||
"knip": "^5.64.3",
|
||||
"lint-staged": "^15.2.10",
|
||||
"lodash": "^4.17.21",
|
||||
"magicast": "^0.3.4",
|
||||
|
||||
797
web/pnpm-lock.yaml
generated
797
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user