mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
Merge branch 'main' into 4-27-app-deploy
This commit is contained in:
commit
444c846480
2
.github/workflows/style.yml
vendored
2
.github/workflows/style.yml
vendored
@ -110,6 +110,8 @@ jobs:
|
||||
- name: Web tsslint
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ./web
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
run: vp run lint:tss
|
||||
|
||||
- name: Web type check
|
||||
|
||||
6
.github/workflows/web-tests.yml
vendored
6
.github/workflows/web-tests.yml
vendored
@ -16,7 +16,7 @@ concurrency:
|
||||
jobs:
|
||||
test:
|
||||
name: Web Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
env:
|
||||
VITEST_COVERAGE_SCOPE: app-components
|
||||
strategy:
|
||||
@ -54,7 +54,7 @@ jobs:
|
||||
name: Merge Test Reports
|
||||
if: ${{ !cancelled() }}
|
||||
needs: [test]
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
defaults:
|
||||
@ -92,7 +92,7 @@ jobs:
|
||||
|
||||
dify-ui-test:
|
||||
name: dify-ui Tests
|
||||
runs-on: depot-ubuntu-24.04
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
defaults:
|
||||
|
||||
@ -2059,7 +2059,7 @@
|
||||
},
|
||||
"web/app/components/base/text-generation/types.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 3
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/textarea/index.stories.tsx": {
|
||||
@ -2070,11 +2070,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/textarea/index.tsx": {
|
||||
"react-refresh/only-export-components": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/video-gallery/VideoPlayer.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
@ -2422,21 +2417,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/bucket.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -2525,11 +2510,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/detail/completed/common/summary-status.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/documents/detail/completed/components/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 3
|
||||
@ -2789,11 +2769,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/develop/secret-key/input-copy.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/develop/secret-key/secret-key-generate.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -3159,16 +3134,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/base/badges/icon-with-tooltip.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/base/key-value-item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/card/index.tsx": {
|
||||
"ts/no-non-null-asserted-optional-chain": {
|
||||
"count": 1
|
||||
@ -3328,24 +3293,11 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/detail-header/components/plugin-source-badge.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/detail-header/hooks/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -3544,11 +3496,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/plugins/readme-panel/index.tsx": {
|
||||
"react/unsupported-syntax": {
|
||||
"count": 1
|
||||
@ -3822,14 +3769,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/detail/content.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/tools/mcp/detail/tool-item.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
@ -5394,14 +5333,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/panel/chat-variable-panel/components/variable-type-select.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/panel/chat-variable-panel/type.ts": {
|
||||
"erasable-syntax-only/enums": {
|
||||
"count": 1
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"name": "dify",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"packageManager": "pnpm@10.33.2",
|
||||
"engines": {
|
||||
"node": "^22.22.1"
|
||||
},
|
||||
|
||||
@ -194,7 +194,6 @@ describe('Select wrappers', () => {
|
||||
})
|
||||
|
||||
it('should forward passthrough props to positioner popup and list when passthrough props are provided', async () => {
|
||||
const onPositionerMouseEnter = vi.fn()
|
||||
const onPopupClick = vi.fn()
|
||||
const onListFocus = vi.fn()
|
||||
|
||||
@ -208,7 +207,6 @@ describe('Select wrappers', () => {
|
||||
'role': 'group',
|
||||
'aria-label': 'select positioner',
|
||||
'id': 'select-positioner',
|
||||
'onMouseEnter': onPositionerMouseEnter,
|
||||
}}
|
||||
popupProps={{
|
||||
'role': 'dialog',
|
||||
@ -231,10 +229,7 @@ describe('Select wrappers', () => {
|
||||
</Select>,
|
||||
)
|
||||
|
||||
screen.getByRole('group', { name: 'select positioner' }).element().dispatchEvent(new MouseEvent('mouseover', {
|
||||
bubbles: true,
|
||||
}))
|
||||
asHTMLElement(screen.getByRole('dialog', { name: 'select popup' }).element()).click()
|
||||
await screen.getByRole('dialog', { name: 'select popup' }).click()
|
||||
screen.getByRole('listbox', { name: 'select list' }).element().dispatchEvent(new FocusEvent('focusin', {
|
||||
bubbles: true,
|
||||
}))
|
||||
@ -242,7 +237,6 @@ describe('Select wrappers', () => {
|
||||
await expect.element(screen.getByRole('group', { name: 'select positioner' })).toHaveAttribute('id', 'select-positioner')
|
||||
await expect.element(screen.getByRole('dialog', { name: 'select popup' })).toHaveAttribute('id', 'select-popup')
|
||||
await expect.element(screen.getByRole('listbox', { name: 'select list' })).toHaveAttribute('id', 'select-list')
|
||||
expect(onPositionerMouseEnter).toHaveBeenCalledTimes(1)
|
||||
expect(onPopupClick).toHaveBeenCalledTimes(1)
|
||||
expect(onListFocus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -125,21 +125,6 @@ describe('@langgenius/dify-ui/toast', () => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should respect the host timeout configuration', async () => {
|
||||
const screen = await render(<ToastHost timeout={3000} />)
|
||||
|
||||
toast('Configured timeout')
|
||||
await expect.element(screen.getByText('Configured timeout')).toBeInTheDocument()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2999)
|
||||
expect(document.body).toHaveTextContent('Configured timeout')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
await vi.waitFor(() => {
|
||||
expect(document.body).not.toHaveTextContent('Configured timeout')
|
||||
})
|
||||
})
|
||||
|
||||
it('should respect custom timeout values including zero', async () => {
|
||||
const screen = await render(<ToastHost />)
|
||||
|
||||
|
||||
850
pnpm-lock.yaml
generated
850
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -47,18 +47,18 @@ overrides:
|
||||
yaml@>=2.0.0 <2.8.3: 2.8.3
|
||||
yauzl@<3.2.1: 3.2.1
|
||||
catalog:
|
||||
'@amplitude/analytics-browser': 2.41.0
|
||||
'@amplitude/plugin-session-replay-browser': 1.27.10
|
||||
'@amplitude/analytics-browser': 2.41.1
|
||||
'@amplitude/plugin-session-replay-browser': 1.28.0
|
||||
'@antfu/eslint-config': 8.2.0
|
||||
'@base-ui/react': 1.4.1
|
||||
'@chromatic-com/storybook': 5.1.2
|
||||
'@cucumber/cucumber': 12.8.1
|
||||
'@cucumber/cucumber': 12.8.2
|
||||
'@egoist/tailwindcss-icons': 1.9.2
|
||||
'@emoji-mart/data': 1.2.1
|
||||
'@eslint-react/eslint-plugin': 3.0.0
|
||||
'@eslint/js': 10.0.1
|
||||
'@floating-ui/react': 0.27.19
|
||||
'@formatjs/intl-localematcher': 0.8.3
|
||||
'@formatjs/intl-localematcher': 0.8.4
|
||||
'@headlessui/react': 2.2.10
|
||||
'@heroicons/react': 2.2.0
|
||||
'@hono/node-server': 1.19.14
|
||||
@ -77,14 +77,14 @@ catalog:
|
||||
'@monaco-editor/react': 4.7.0
|
||||
'@next/eslint-plugin-next': 16.2.4
|
||||
'@next/mdx': 16.2.4
|
||||
'@orpc/client': 1.13.14
|
||||
'@orpc/contract': 1.13.14
|
||||
'@orpc/openapi-client': 1.13.14
|
||||
'@orpc/tanstack-query': 1.13.14
|
||||
'@orpc/client': 1.14.0
|
||||
'@orpc/contract': 1.14.0
|
||||
'@orpc/openapi-client': 1.14.0
|
||||
'@orpc/tanstack-query': 1.14.0
|
||||
'@playwright/test': 1.59.1
|
||||
'@remixicon/react': 4.9.0
|
||||
'@rgrove/parse-xml': 4.2.0
|
||||
'@sentry/react': 10.49.0
|
||||
'@sentry/react': 10.50.0
|
||||
'@storybook/addon-docs': 10.3.5
|
||||
'@storybook/addon-links': 10.3.5
|
||||
'@storybook/addon-onboarding': 10.3.5
|
||||
@ -98,12 +98,12 @@ catalog:
|
||||
'@tailwindcss/postcss': 4.2.4
|
||||
'@tailwindcss/typography': 0.5.19
|
||||
'@tailwindcss/vite': 4.2.4
|
||||
'@tanstack/eslint-plugin-query': 5.99.2
|
||||
'@tanstack/eslint-plugin-query': 5.100.5
|
||||
'@tanstack/react-devtools': 0.10.2
|
||||
'@tanstack/react-form': 1.29.1
|
||||
'@tanstack/react-form-devtools': 0.2.22
|
||||
'@tanstack/react-query': 5.99.2
|
||||
'@tanstack/react-query-devtools': 5.99.2
|
||||
'@tanstack/react-query': 5.100.5
|
||||
'@tanstack/react-query-devtools': 5.100.5
|
||||
'@tanstack/react-virtual': 3.13.24
|
||||
'@testing-library/dom': 10.4.1
|
||||
'@testing-library/jest-dom': 6.9.1
|
||||
@ -122,11 +122,11 @@ catalog:
|
||||
'@types/sortablejs': 1.15.9
|
||||
'@typescript-eslint/eslint-plugin': 8.59.0
|
||||
'@typescript-eslint/parser': 8.59.0
|
||||
'@typescript/native-preview': 7.0.0-dev.20260422.1
|
||||
'@typescript/native-preview': 7.0.0-dev.20260426.1
|
||||
'@vitejs/plugin-react': 6.0.1
|
||||
'@vitejs/plugin-rsc': 0.5.24
|
||||
'@vitejs/plugin-rsc': 0.5.25
|
||||
'@vitest/coverage-v8': 4.1.5
|
||||
abcjs: 6.6.2
|
||||
abcjs: 6.6.3
|
||||
agentation: 3.0.2
|
||||
ahooks: 3.9.7
|
||||
class-variance-authority: 0.7.1
|
||||
@ -158,7 +158,7 @@ catalog:
|
||||
fast-deep-equal: 3.1.3
|
||||
happy-dom: 20.9.0
|
||||
hast-util-to-jsx-runtime: 2.3.6
|
||||
hono: 4.12.14
|
||||
hono: 4.12.15
|
||||
html-entities: 2.6.0
|
||||
html-to-image: 1.11.13
|
||||
i18next: 26.0.6
|
||||
@ -171,11 +171,11 @@ catalog:
|
||||
js-yaml: 4.1.1
|
||||
jsonschema: 1.5.0
|
||||
katex: 0.16.45
|
||||
knip: 6.6.1
|
||||
knip: 6.7.0
|
||||
ky: 2.0.2
|
||||
lamejs: 1.2.1
|
||||
lexical: 0.43.0
|
||||
loro-crdt: 1.11.1
|
||||
loro-crdt: 1.12.0
|
||||
mermaid: 11.14.0
|
||||
mime: 4.1.0
|
||||
mitt: 3.0.1
|
||||
@ -185,7 +185,7 @@ catalog:
|
||||
nuqs: 2.8.9
|
||||
pinyin-pro: 3.28.1
|
||||
playwright: 1.59.1
|
||||
postcss: 8.5.10
|
||||
postcss: 8.5.12
|
||||
qrcode.react: 4.2.0
|
||||
qs: 6.15.1
|
||||
react: 19.2.5
|
||||
|
||||
@ -5,7 +5,7 @@ export type ModelAndParameter = {
|
||||
parameters: Record<string, any>
|
||||
}
|
||||
|
||||
export type MultipleAndConfigs = {
|
||||
type MultipleAndConfigs = {
|
||||
multiple: boolean
|
||||
configs: ModelAndParameter[]
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import type {
|
||||
HumanInputFormData,
|
||||
} from '@/types/workflow'
|
||||
|
||||
export type MessageMore = {
|
||||
type MessageMore = {
|
||||
time: string
|
||||
tokens: number
|
||||
latency: number | string
|
||||
|
||||
@ -2,7 +2,7 @@ import type { Features } from './types'
|
||||
import { createStore } from 'zustand'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
|
||||
export type FeaturesModal = {
|
||||
type FeaturesModal = {
|
||||
showFeaturesModal: boolean
|
||||
setShowFeaturesModal: (showFeaturesModal: boolean) => void
|
||||
}
|
||||
@ -11,7 +11,7 @@ export type FeaturesState = {
|
||||
features: Features
|
||||
}
|
||||
|
||||
export type FeaturesAction = {
|
||||
type FeaturesAction = {
|
||||
setFeatures: (features: Features) => void
|
||||
}
|
||||
|
||||
|
||||
@ -6,11 +6,11 @@ import type {
|
||||
TtsAutoPlay,
|
||||
} from '@/types/app'
|
||||
|
||||
export type EnabledOrDisabled = {
|
||||
type EnabledOrDisabled = {
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export type MoreLikeThis = EnabledOrDisabled
|
||||
type MoreLikeThis = EnabledOrDisabled
|
||||
|
||||
export type OpeningStatement = EnabledOrDisabled & {
|
||||
opening_statement?: string
|
||||
@ -75,7 +75,7 @@ export type FileUpload = {
|
||||
}
|
||||
} & EnabledOrDisabled
|
||||
|
||||
export type AnnotationReplyConfig = {
|
||||
type AnnotationReplyConfig = {
|
||||
enabled: boolean
|
||||
id?: string
|
||||
score_threshold?: number
|
||||
|
||||
@ -33,7 +33,7 @@ export type SelectConfiguration = {
|
||||
}
|
||||
}
|
||||
|
||||
export type FileConfiguration = {
|
||||
type FileConfiguration = {
|
||||
allowedFileTypes: string[]
|
||||
allowedFileExtensions: string[]
|
||||
allowedFileUploadMethods: TransferMethod[]
|
||||
|
||||
@ -13,11 +13,11 @@ export enum InputFieldType {
|
||||
fileTypes = 'fileTypes',
|
||||
}
|
||||
|
||||
export type InputTypeSelectConfiguration = {
|
||||
type InputTypeSelectConfiguration = {
|
||||
supportFile: boolean
|
||||
}
|
||||
|
||||
export type NumberSliderConfiguration = {
|
||||
type NumberSliderConfiguration = {
|
||||
description: string
|
||||
max?: number
|
||||
min?: number
|
||||
|
||||
@ -14,11 +14,11 @@ export enum InputFieldType {
|
||||
variableOrConstant = 'variableOrConstant',
|
||||
}
|
||||
|
||||
export type InputTypeSelectConfiguration = {
|
||||
type InputTypeSelectConfiguration = {
|
||||
supportFile: boolean
|
||||
}
|
||||
|
||||
export type NumberSliderConfiguration = {
|
||||
type NumberSliderConfiguration = {
|
||||
description: string
|
||||
max?: number
|
||||
min?: number
|
||||
|
||||
@ -14,7 +14,7 @@ export type TypeWithI18N<T = string> = {
|
||||
[key: string]: T
|
||||
}
|
||||
|
||||
export type FormShowOnObject = {
|
||||
type FormShowOnObject = {
|
||||
variable: string
|
||||
value: string
|
||||
}
|
||||
@ -43,7 +43,7 @@ export type FormOption = {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any, any, any>
|
||||
type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any, any, any>
|
||||
|
||||
export enum FormItemValidateStatusEnum {
|
||||
Success = 'success',
|
||||
|
||||
@ -3,11 +3,11 @@ import * as z from 'zod'
|
||||
const commonSchema = {
|
||||
className: z.string().min(1).optional(),
|
||||
}
|
||||
export const withIconCardListPropsSchema = z.object(commonSchema).strict()
|
||||
const withIconCardListPropsSchema = z.object(commonSchema).strict()
|
||||
|
||||
const HTTP_URL_REGEX = /^https?:\/\//i
|
||||
|
||||
export const withIconCardItemPropsSchema = z.object({
|
||||
const withIconCardItemPropsSchema = z.object({
|
||||
...commonSchema,
|
||||
icon: z.string().trim().url().refine(
|
||||
value => HTTP_URL_REGEX.test(value),
|
||||
|
||||
@ -4,7 +4,6 @@ import type {
|
||||
VisionFile,
|
||||
} from '@/types/app'
|
||||
|
||||
export type { VisionFile } from '@/types/app'
|
||||
export { TransferMethod } from '@/types/app'
|
||||
|
||||
export type TextGenerationConfig = Omit<ModelConfig, 'model'> & {
|
||||
|
||||
@ -58,4 +58,3 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
Textarea.displayName = 'Textarea'
|
||||
|
||||
export default Textarea
|
||||
export { textareaVariants }
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { UploadDropzoneProps } from '../upload-dropzone'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import UploadDropzone from '../upload-dropzone'
|
||||
|
||||
let mockEnableBilling = false
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContextSelector: <T,>(selector: (state: Pick<ProviderContextState, 'enableBilling'>) => T): T =>
|
||||
selector({ enableBilling: mockEnableBilling }),
|
||||
}))
|
||||
|
||||
// Helper to create mock ref objects for testing
|
||||
const createMockRef = <T,>(value: T | null = null): RefObject<T | null> => ({ current: value })
|
||||
|
||||
@ -27,6 +35,7 @@ describe('UploadDropzone', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEnableBilling = false
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
@ -46,7 +55,7 @@ describe('UploadDropzone', () => {
|
||||
|
||||
it('should render upload icon', () => {
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
const icon = document.querySelector('svg')
|
||||
const icon = document.querySelector('.i-ri-upload-cloud-2-line')
|
||||
expect(icon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -67,6 +76,51 @@ describe('UploadDropzone', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('tip rendering by billing state', () => {
|
||||
it('should render tip without total count limit when billing is disabled', () => {
|
||||
mockEnableBilling = false
|
||||
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
|
||||
const tipWithoutTotal = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip(?!WithTotalLimit)/)
|
||||
expect(tipWithoutTotal).toBeInTheDocument()
|
||||
expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tip with total count limit when billing is enabled', () => {
|
||||
mockEnableBilling = true
|
||||
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.tip(?!WithTotalLimit)/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass file size, batch count and supported types to tip when billing is disabled', () => {
|
||||
mockEnableBilling = false
|
||||
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
|
||||
const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip/).textContent ?? ''
|
||||
expect(tipText).toContain('"size":15')
|
||||
expect(tipText).toContain('"batchCount":5')
|
||||
expect(tipText).toContain('"supportTypes":"PDF, DOCX, TXT"')
|
||||
expect(tipText).not.toContain('"totalCount"')
|
||||
})
|
||||
|
||||
it('should additionally pass total count to tip when billing is enabled', () => {
|
||||
mockEnableBilling = true
|
||||
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
|
||||
const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/).textContent ?? ''
|
||||
expect(tipText).toContain('"size":15')
|
||||
expect(tipText).toContain('"batchCount":5')
|
||||
expect(tipText).toContain('"supportTypes":"PDF, DOCX, TXT"')
|
||||
expect(tipText).toContain('"totalCount":10')
|
||||
})
|
||||
})
|
||||
|
||||
describe('file input configuration', () => {
|
||||
it('should allow multiple files when supportBatchUpload is true', () => {
|
||||
render(<UploadDropzone {...defaultProps} supportBatchUpload={true} />)
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { FileUploadConfig } from '../hooks/use-file-upload'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiUploadCloud2Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
|
||||
export type UploadDropzoneProps = {
|
||||
dropRef: RefObject<HTMLDivElement | null>
|
||||
@ -31,6 +31,7 @@ const UploadDropzone = ({
|
||||
onFileChange,
|
||||
}: UploadDropzoneProps) => {
|
||||
const { t } = useTranslation()
|
||||
const enableBilling = useProviderContextSelector(state => state.enableBilling)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -51,7 +52,7 @@ const UploadDropzone = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex min-h-5 items-center justify-center text-sm leading-4 text-text-secondary">
|
||||
<RiUploadCloud2Line className="mr-2 size-5" />
|
||||
<span className="mr-2 i-ri-upload-cloud-2-line size-5" />
|
||||
<span>
|
||||
{supportBatchUpload
|
||||
? t('stepOne.uploader.button', { ns: 'datasetCreation' })
|
||||
@ -67,13 +68,20 @@ const UploadDropzone = ({
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{t('stepOne.uploader.tip', {
|
||||
ns: 'datasetCreation',
|
||||
size: fileUploadConfig.file_size_limit,
|
||||
supportTypes: supportTypesShowNames,
|
||||
batchCount: fileUploadConfig.batch_count_limit,
|
||||
totalCount: fileUploadConfig.file_upload_limit,
|
||||
})}
|
||||
{enableBilling
|
||||
? t('stepOne.uploader.tipWithTotalLimit', {
|
||||
ns: 'datasetCreation',
|
||||
size: fileUploadConfig.file_size_limit,
|
||||
supportTypes: supportTypesShowNames,
|
||||
batchCount: fileUploadConfig.batch_count_limit,
|
||||
totalCount: fileUploadConfig.file_upload_limit,
|
||||
})
|
||||
: t('stepOne.uploader.tip', {
|
||||
ns: 'datasetCreation',
|
||||
size: fileUploadConfig.file_size_limit,
|
||||
supportTypes: supportTypesShowNames,
|
||||
batchCount: fileUploadConfig.batch_count_limit,
|
||||
})}
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className="absolute top-0 left-0 h-full w-full" />}
|
||||
</div>
|
||||
|
||||
@ -2,18 +2,10 @@ import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Header from '../header'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({ children }: { children: React.ReactNode }) => <button>{children}</button>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/divider', () => ({
|
||||
default: () => <span data-testid="divider" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div data-testid="tooltip">{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../credential-selector', () => ({
|
||||
default: () => <div data-testid="credential-selector" />,
|
||||
}))
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import type { CredentialSelectorProps } from './credential-selector'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { RiBookOpenLine, RiEqualizer2Line } from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import CredentialSelector from './credential-selector'
|
||||
|
||||
type HeaderProps = {
|
||||
@ -22,6 +21,7 @@ const Header = ({
|
||||
...rest
|
||||
}: HeaderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const configurationTip = t('configurationTip', { ns: 'datasetPipeline', pluginName })
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-x-2">
|
||||
@ -30,20 +30,23 @@ const Header = ({
|
||||
{...rest}
|
||||
/>
|
||||
<Divider type="vertical" className="mx-1 h-3.5 shrink-0" />
|
||||
<Tooltip
|
||||
popupContent={t('configurationTip', { ns: 'datasetPipeline', pluginName })}
|
||||
position="top"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="size-6 shrink-0 px-1"
|
||||
>
|
||||
<RiEqualizer2Line
|
||||
className="h-4 w-4"
|
||||
onClick={onClickConfiguration}
|
||||
/>
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="size-6 shrink-0 px-1"
|
||||
aria-label={configurationTip}
|
||||
onClick={onClickConfiguration}
|
||||
>
|
||||
<span aria-hidden className="i-ri-equalizer-2-line h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{configurationTip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<a
|
||||
@ -52,7 +55,7 @@ const Header = ({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RiBookOpenLine className="size-3.5 shrink-0" />
|
||||
<span aria-hidden className="i-ri-book-open-line size-3.5 shrink-0" />
|
||||
<span title={docTitle}>{docTitle}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { UploadDropzoneProps } from '../upload-dropzone'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import UploadDropzone from '../upload-dropzone'
|
||||
|
||||
let mockEnableBilling = false
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContextSelector: <T,>(selector: (state: Pick<ProviderContextState, 'enableBilling'>) => T): T =>
|
||||
selector({ enableBilling: mockEnableBilling }),
|
||||
}))
|
||||
|
||||
// Helper to create mock ref objects for testing
|
||||
const createMockRef = <T,>(value: T | null = null): RefObject<T | null> => ({ current: value })
|
||||
|
||||
@ -28,6 +36,7 @@ describe('UploadDropzone', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEnableBilling = false
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
@ -50,7 +59,7 @@ describe('UploadDropzone', () => {
|
||||
it('should render upload icon', () => {
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
|
||||
const icon = document.querySelector('svg')
|
||||
const icon = document.querySelector('.i-ri-upload-cloud-2-line')
|
||||
expect(icon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -73,6 +82,51 @@ describe('UploadDropzone', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('tip rendering by billing state', () => {
|
||||
it('should render tip without total count limit when billing is disabled', () => {
|
||||
mockEnableBilling = false
|
||||
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
|
||||
const tipWithoutTotal = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip(?!WithTotalLimit)/)
|
||||
expect(tipWithoutTotal).toBeInTheDocument()
|
||||
expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tip with total count limit when billing is enabled', () => {
|
||||
mockEnableBilling = true
|
||||
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.tip(?!WithTotalLimit)/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass file size, batch count and supported types to tip when billing is disabled', () => {
|
||||
mockEnableBilling = false
|
||||
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
|
||||
const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip/).textContent ?? ''
|
||||
expect(tipText).toContain('"size":15')
|
||||
expect(tipText).toContain('"batchCount":5')
|
||||
expect(tipText).toContain('"supportTypes":"PDF, DOCX, TXT"')
|
||||
expect(tipText).not.toContain('"totalCount"')
|
||||
})
|
||||
|
||||
it('should additionally pass total count to tip when billing is enabled', () => {
|
||||
mockEnableBilling = true
|
||||
|
||||
render(<UploadDropzone {...defaultProps} />)
|
||||
|
||||
const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tipWithTotalLimit/).textContent ?? ''
|
||||
expect(tipText).toContain('"size":15')
|
||||
expect(tipText).toContain('"batchCount":5')
|
||||
expect(tipText).toContain('"supportTypes":"PDF, DOCX, TXT"')
|
||||
expect(tipText).toContain('"totalCount":10')
|
||||
})
|
||||
})
|
||||
|
||||
describe('file input configuration', () => {
|
||||
it('should allow multiple files when supportBatchUpload is true', () => {
|
||||
render(<UploadDropzone {...defaultProps} supportBatchUpload={true} />)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ChangeEvent, RefObject } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiUploadCloud2Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
|
||||
type FileUploadConfig = {
|
||||
file_size_limit: number
|
||||
@ -37,6 +37,7 @@ const UploadDropzone = ({
|
||||
allowedExtensions,
|
||||
}: UploadDropzoneProps) => {
|
||||
const { t } = useTranslation()
|
||||
const enableBilling = useProviderContextSelector(state => state.enableBilling)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -57,7 +58,7 @@ const UploadDropzone = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex min-h-5 items-center justify-center text-sm leading-4 text-text-secondary">
|
||||
<RiUploadCloud2Line className="mr-2 size-5" />
|
||||
<span className="mr-2 i-ri-upload-cloud-2-line size-5" />
|
||||
<span>
|
||||
{supportBatchUpload ? t('stepOne.uploader.button', { ns: 'datasetCreation' }) : t('stepOne.uploader.buttonSingleFile', { ns: 'datasetCreation' })}
|
||||
{allowedExtensions.length > 0 && (
|
||||
@ -66,13 +67,20 @@ const UploadDropzone = ({
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{t('stepOne.uploader.tip', {
|
||||
ns: 'datasetCreation',
|
||||
size: fileUploadConfig.file_size_limit,
|
||||
supportTypes: supportTypesShowNames,
|
||||
batchCount: fileUploadConfig.batch_count_limit,
|
||||
totalCount: fileUploadConfig.file_upload_limit,
|
||||
})}
|
||||
{enableBilling
|
||||
? t('stepOne.uploader.tipWithTotalLimit', {
|
||||
ns: 'datasetCreation',
|
||||
size: fileUploadConfig.file_size_limit,
|
||||
supportTypes: supportTypesShowNames,
|
||||
batchCount: fileUploadConfig.batch_count_limit,
|
||||
totalCount: fileUploadConfig.file_upload_limit,
|
||||
})
|
||||
: t('stepOne.uploader.tip', {
|
||||
ns: 'datasetCreation',
|
||||
size: fileUploadConfig.file_size_limit,
|
||||
supportTypes: supportTypesShowNames,
|
||||
batchCount: fileUploadConfig.batch_count_limit,
|
||||
})}
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className="absolute top-0 left-0 h-full w-full" />}
|
||||
</div>
|
||||
|
||||
@ -5,9 +5,6 @@ import Bucket from '../bucket'
|
||||
vi.mock('@/app/components/base/icons/src/public/knowledge/online-drive', () => ({
|
||||
BucketsGray: (props: React.SVGProps<SVGSVGElement>) => <svg data-testid="buckets-gray" {...props} />,
|
||||
}))
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
describe('Bucket', () => {
|
||||
const defaultProps = {
|
||||
@ -32,8 +29,7 @@ describe('Bucket', () => {
|
||||
|
||||
it('should call handleBackToBucketList on icon button click', () => {
|
||||
render(<Bucket {...defaultProps} />)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
fireEvent.click(buttons[0]!)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.onlineDrive.breadcrumbs.allBuckets' }))
|
||||
expect(defaultProps.handleBackToBucketList).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BucketsGray } from '@/app/components/base/icons/src/public/knowledge/online-drive'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type BucketProps = {
|
||||
bucketName: string
|
||||
@ -27,19 +28,28 @@ const Bucket = ({
|
||||
if (!disabled)
|
||||
handleClickBucketName()
|
||||
}, [disabled, handleClickBucketName])
|
||||
const allBucketsLabel = t('onlineDrive.breadcrumbs.allBuckets', { ns: 'datasetPipeline' })
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
popupContent={t('onlineDrive.breadcrumbs.allBuckets', { ns: 'datasetPipeline' })}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover"
|
||||
onClick={handleBackToBucketList}
|
||||
>
|
||||
<BucketsGray />
|
||||
</button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
aria-label={allBucketsLabel}
|
||||
className="size-6 shrink-0 rounded-md px-0 hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
onClick={handleBackToBucketList}
|
||||
>
|
||||
<BucketsGray aria-hidden />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{allBucketsLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="system-xs-regular text-divider-deep">/</span>
|
||||
<button
|
||||
|
||||
@ -9,9 +9,6 @@ vi.mock('@/app/components/base/badge', () => ({
|
||||
vi.mock('@/app/components/base/icons/src/vender/knowledge', () => ({
|
||||
SearchLinesSparkle: (props: React.SVGProps<SVGSVGElement>) => <svg data-testid="sparkle-icon" {...props} />,
|
||||
}))
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
describe('SummaryStatus', () => {
|
||||
it('should render badge for SUMMARIZING status', () => {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { SearchLinesSparkle } from '@/app/components/base/icons/src/vender/knowledge'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type SummaryStatusProps = {
|
||||
status: string
|
||||
@ -18,18 +18,22 @@ const SummaryStatus = ({ status }: SummaryStatusProps) => {
|
||||
return ''
|
||||
}, [status, t])
|
||||
|
||||
if (status !== 'SUMMARIZING')
|
||||
return null
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={tip}
|
||||
>
|
||||
{
|
||||
status === 'SUMMARIZING' && (
|
||||
<Badge className="border-text-accent-secondary text-text-accent-secondary">
|
||||
<SearchLinesSparkle className="mr-0.5 h-3 w-3" />
|
||||
<span>{t('list.summary.generating', { ns: 'datasetDocuments' })}</span>
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<span className="inline-flex">
|
||||
<Badge className="border-text-accent-secondary text-text-accent-secondary">
|
||||
<SearchLinesSparkle aria-hidden className="mr-0.5 h-3 w-3" />
|
||||
<span>{t('list.summary.generating', { ns: 'datasetDocuments' })}</span>
|
||||
</Badge>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>{tip}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,13 +2,13 @@ import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext, useContextSelector } from 'use-context-selector'
|
||||
|
||||
export type CurrSegmentType = {
|
||||
type CurrSegmentType = {
|
||||
segInfo?: SegmentDetailModel
|
||||
showModal: boolean
|
||||
isEditMode?: boolean
|
||||
}
|
||||
|
||||
export type CurrChildChunkType = {
|
||||
type CurrChildChunkType = {
|
||||
childChunkInfo?: ChildChunkDetail
|
||||
showModal: boolean
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ const parseAsDocSort = createParser<SortType>({
|
||||
|
||||
const parseAsKeyword = parseAsString.withDefault('')
|
||||
|
||||
export const documentListParsers = {
|
||||
const documentListParsers = {
|
||||
page: parseAsPage,
|
||||
limit: parseAsLimit,
|
||||
keyword: parseAsKeyword,
|
||||
|
||||
@ -35,7 +35,7 @@ describe('InputCopy', () => {
|
||||
|
||||
it('should render with empty value by default', async () => {
|
||||
await renderAndFlush(<InputCopy />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should render children when provided', async () => {
|
||||
@ -273,12 +273,12 @@ describe('InputCopy', () => {
|
||||
describe('edge cases', () => {
|
||||
it('should handle undefined value', async () => {
|
||||
await renderAndFlush(<InputCopy value={undefined} />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should handle empty string value', async () => {
|
||||
await renderAndFlush(<InputCopy value="" />)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should handle very long values', async () => {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { t } from 'i18next'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { writeTextToClipboard } from '@/utils/clipboard'
|
||||
|
||||
type IInputCopyProps = {
|
||||
@ -18,6 +18,12 @@ const InputCopy = ({
|
||||
children,
|
||||
}: IInputCopyProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const copyLabel = isCopied ? `${t('copied', { ns: 'appApi' })}` : `${t('copy', { ns: 'appApi' })}`
|
||||
const handleCopy = () => {
|
||||
writeTextToClipboard(value).then(() => {
|
||||
setIsCopied(true)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isCopied) {
|
||||
@ -38,17 +44,24 @@ const InputCopy = ({
|
||||
<div className="relative h-full grow text-[13px]">
|
||||
<div
|
||||
className="r-0 absolute top-0 left-0 w-full cursor-pointer truncate pr-2 pl-2"
|
||||
onClick={() => {
|
||||
writeTextToClipboard(value).then(() => {
|
||||
setIsCopied(true)
|
||||
})
|
||||
role="button"
|
||||
aria-label={copyLabel}
|
||||
tabIndex={0}
|
||||
onClick={handleCopy}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
handleCopy()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
popupContent={isCopied ? `${t('copied', { ns: 'appApi' })}` : `${t('copy', { ns: 'appApi' })}`}
|
||||
position="bottom"
|
||||
>
|
||||
<span className="text-text-secondary">{value}</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<span className="text-text-secondary">{value}</span>}
|
||||
/>
|
||||
<TooltipContent placement="bottom">
|
||||
{copyLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,9 +5,9 @@ import type { CommonNodeType } from '../../workflow/types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { App } from '@/types/app'
|
||||
|
||||
export type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node' | 'command' | 'recent'
|
||||
type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node' | 'command' | 'recent'
|
||||
|
||||
export type BaseSearchResult<T = any> = {
|
||||
type BaseSearchResult<T = any> = {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
@ -29,7 +29,7 @@ export type KnowledgeSearchResult = {
|
||||
type: 'knowledge'
|
||||
} & BaseSearchResult<DataSet>
|
||||
|
||||
export type WorkflowNodeSearchResult = {
|
||||
type WorkflowNodeSearchResult = {
|
||||
type: 'workflow-node'
|
||||
metadata?: {
|
||||
nodeId: string
|
||||
|
||||
@ -92,7 +92,7 @@ export enum CustomConfigurationStatusEnum {
|
||||
noConfigure = 'no-configure',
|
||||
}
|
||||
|
||||
export type FormShowOnObject = {
|
||||
type FormShowOnObject = {
|
||||
variable: string
|
||||
value: string
|
||||
}
|
||||
@ -155,7 +155,7 @@ export enum QuotaUnitEnum {
|
||||
times = 'times',
|
||||
}
|
||||
|
||||
export type QuotaConfiguration = {
|
||||
type QuotaConfiguration = {
|
||||
quota_type: CurrentSystemQuotaTypeEnum
|
||||
quota_unit: QuotaUnitEnum
|
||||
quota_limit: number
|
||||
|
||||
@ -6,15 +6,13 @@ vi.mock('../../../base/icons/src/vender/line/files', () => ({
|
||||
CopyCheck: () => <span data-testid="copy-check-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../../base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip" data-content={popupContent}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/action-button', () => ({
|
||||
default: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
|
||||
<button data-testid="action-button" onClick={onClick}>{children}</button>
|
||||
default: ({
|
||||
children,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||
<button data-testid="action-button" onClick={onClick} {...props}>{children}</button>
|
||||
),
|
||||
}))
|
||||
|
||||
@ -54,6 +52,6 @@ describe('KeyValueItem', () => {
|
||||
|
||||
it('renders copy tooltip', () => {
|
||||
render(<KeyValueItem label="ID" value="123" />)
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-content', 'common.operation.copy')
|
||||
expect(screen.getByRole('button', { name: 'common.operation.copy' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,24 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Theme } from '@/types/app'
|
||||
import IconWithTooltip from '../icon-with-tooltip'
|
||||
|
||||
// Mock Tooltip component
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({
|
||||
children,
|
||||
popupContent,
|
||||
popupClassName,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
popupContent?: string
|
||||
popupClassName?: string
|
||||
}) => (
|
||||
<div data-testid="tooltip" data-popup-content={popupContent} data-popup-classname={popupClassName}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock icon components
|
||||
const MockLightIcon = ({ className }: { className?: string }) => (
|
||||
<div data-testid="light-icon" className={className}>Light Icon</div>
|
||||
)
|
||||
@ -44,10 +26,10 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('light-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Tooltip wrapper', () => {
|
||||
it('should render tooltip trigger with accessible label when popupContent is provided', () => {
|
||||
render(
|
||||
<IconWithTooltip
|
||||
theme={Theme.light}
|
||||
@ -57,21 +39,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', 'Test tooltip')
|
||||
})
|
||||
|
||||
it('should apply correct popupClassName to Tooltip', () => {
|
||||
render(
|
||||
<IconWithTooltip
|
||||
theme={Theme.light}
|
||||
BadgeIconLight={MockLightIcon}
|
||||
BadgeIconDark={MockDarkIcon}
|
||||
/>,
|
||||
)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toHaveAttribute('data-popup-classname')
|
||||
expect(tooltip.getAttribute('data-popup-classname')).toContain('border-components-panel-border')
|
||||
expect(screen.getByLabelText('Test tooltip')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -171,10 +139,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-popup-content',
|
||||
'Custom tooltip content',
|
||||
)
|
||||
expect(screen.getByLabelText('Custom tooltip content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined popupContent', () => {
|
||||
@ -186,7 +151,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('light-icon')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -239,7 +204,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', longContent)
|
||||
expect(screen.getByLabelText(longContent)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in popupContent', () => {
|
||||
@ -253,7 +218,7 @@ describe('IconWithTooltip', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-popup-content', specialContent)
|
||||
expect(screen.getByLabelText(specialContent)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import * as React from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
type IconWithTooltipProps = {
|
||||
@ -22,15 +22,24 @@ const IconWithTooltip: FC<IconWithTooltipProps> = ({
|
||||
const isDark = theme === Theme.dark
|
||||
const iconClassName = cn('h-5 w-5', className)
|
||||
const Icon = isDark ? BadgeIconDark : BadgeIconLight
|
||||
const icon = (
|
||||
<span
|
||||
aria-label={popupContent}
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
>
|
||||
<Icon className={iconClassName} />
|
||||
</span>
|
||||
)
|
||||
|
||||
if (!popupContent)
|
||||
return icon
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupClassName="p-1.5 border-[0.5px] border-[0.5px] border-components-panel-border bg-components-tooltip-bg text-text-secondary system-xs-medium"
|
||||
popupContent={popupContent}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-center">
|
||||
<Icon className={iconClassName} />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={icon} />
|
||||
<TooltipContent className="border-[0.5px] border-components-panel-border bg-components-tooltip-bg p-1.5 system-xs-medium text-text-secondary">
|
||||
{popupContent}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiClipboardLine,
|
||||
} from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { CopyCheck } from '../../base/icons/src/vender/line/files'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
|
||||
type Props = {
|
||||
label: string
|
||||
@ -45,7 +42,7 @@ const KeyValueItem: FC<Props> = ({
|
||||
}
|
||||
}, [isCopied])
|
||||
|
||||
const CopyIcon = isCopied ? CopyCheck : RiClipboardLine
|
||||
const copyLabel = t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
@ -54,10 +51,19 @@ const KeyValueItem: FC<Props> = ({
|
||||
<span className={cn(valueMaxWidthClassName, 'truncate system-xs-medium text-text-secondary')}>
|
||||
{maskedValue || value}
|
||||
</span>
|
||||
<Tooltip popupContent={t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })} position="top">
|
||||
<ActionButton onClick={handleCopy}>
|
||||
<CopyIcon className="h-3.5 w-3.5 shrink-0 text-text-tertiary" />
|
||||
</ActionButton>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton aria-label={copyLabel} onClick={handleCopy}>
|
||||
{isCopied
|
||||
? <CopyCheck aria-hidden className="h-3.5 w-3.5 shrink-0 text-text-tertiary" />
|
||||
: <span aria-hidden className="i-ri-clipboard-line h-3.5 w-3.5 shrink-0 text-text-tertiary" />}
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent placement="top">
|
||||
{copyLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,14 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginSource } from '../../../../types'
|
||||
import PluginSourceBadge from '../plugin-source-badge'
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip" data-content={popupContent}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('PluginSourceBadge', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -20,33 +12,25 @@ describe('PluginSourceBadge', () => {
|
||||
it('should render marketplace source badge', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.marketplace} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.marketplace')
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.marketplace')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render github source badge', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.github} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.github')
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.github')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render local source badge', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.local} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.local')
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.local')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render debugging source badge', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.debugging} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toBeInTheDocument()
|
||||
expect(tooltip).toHaveAttribute('data-content', 'plugin.detailPanel.categoryTip.debugging')
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.debugging')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -86,71 +70,47 @@ describe('PluginSourceBadge', () => {
|
||||
it('should show marketplace tooltip', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.marketplace} />)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'plugin.detailPanel.categoryTip.marketplace',
|
||||
)
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.marketplace')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show github tooltip', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.github} />)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'plugin.detailPanel.categoryTip.github',
|
||||
)
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.github')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show local tooltip', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.local} />)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'plugin.detailPanel.categoryTip.local',
|
||||
)
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.local')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show debugging tooltip', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.debugging} />)
|
||||
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute(
|
||||
'data-content',
|
||||
'plugin.detailPanel.categoryTip.debugging',
|
||||
)
|
||||
expect(screen.getByLabelText('plugin.detailPanel.categoryTip.debugging')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Icon Element Structure', () => {
|
||||
it('should render icon inside tooltip for marketplace', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.marketplace} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
const iconWrapper = tooltip.querySelector('div')
|
||||
expect(iconWrapper).toBeInTheDocument()
|
||||
const { container } = render(<PluginSourceBadge source={PluginSource.marketplace} />)
|
||||
expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.marketplace"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render icon inside tooltip for github', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.github} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
const iconWrapper = tooltip.querySelector('div')
|
||||
expect(iconWrapper).toBeInTheDocument()
|
||||
const { container } = render(<PluginSourceBadge source={PluginSource.github} />)
|
||||
expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.github"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render icon inside tooltip for local', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.local} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
const iconWrapper = tooltip.querySelector('div')
|
||||
expect(iconWrapper).toBeInTheDocument()
|
||||
const { container } = render(<PluginSourceBadge source={PluginSource.local} />)
|
||||
expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.local"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render icon inside tooltip for debugging', () => {
|
||||
render(<PluginSourceBadge source={PluginSource.debugging} />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
const iconWrapper = tooltip.querySelector('div')
|
||||
expect(iconWrapper).toBeInTheDocument()
|
||||
const { container } = render(<PluginSourceBadge source={PluginSource.debugging} />)
|
||||
expect(container.querySelector('[aria-label="plugin.detailPanel.categoryTip.debugging"]')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -188,7 +148,7 @@ describe('PluginSourceBadge', () => {
|
||||
const invalidSource = '' as PluginSource
|
||||
render(<PluginSourceBadge source={invalidSource} />)
|
||||
|
||||
expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByLabelText(/^plugin\.detailPanel\.categoryTip\./)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import {
|
||||
RiBugLine,
|
||||
RiHardDrive3Line,
|
||||
} from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Github } from '@/app/components/base/icons/src/public/common'
|
||||
import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { PluginSource } from '../../../types'
|
||||
|
||||
type SourceConfig = {
|
||||
@ -30,11 +26,11 @@ const SOURCE_CONFIG_MAP: Record<PluginSource, SourceConfig | null> = {
|
||||
tipKey: 'detailPanel.categoryTip.github',
|
||||
},
|
||||
[PluginSource.local]: {
|
||||
icon: <RiHardDrive3Line className="h-3.5 w-3.5 text-text-tertiary" />,
|
||||
icon: <span aria-hidden className="i-ri-hard-drive-3-line h-3.5 w-3.5 text-text-tertiary" />,
|
||||
tipKey: 'detailPanel.categoryTip.local',
|
||||
},
|
||||
[PluginSource.debugging]: {
|
||||
icon: <RiBugLine className="h-3.5 w-3.5 text-text-tertiary hover:text-text-warning" />,
|
||||
icon: <span aria-hidden className="i-ri-bug-line h-3.5 w-3.5 text-text-tertiary hover:text-text-warning" />,
|
||||
tipKey: 'detailPanel.categoryTip.debugging',
|
||||
},
|
||||
}
|
||||
@ -45,12 +41,22 @@ const PluginSourceBadge: FC<PluginSourceBadgeProps> = ({ source }) => {
|
||||
const config = SOURCE_CONFIG_MAP[source]
|
||||
if (!config)
|
||||
return null
|
||||
const tip = t(config.tipKey as never, { ns: 'plugin' })
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mr-0.5 ml-1 system-xs-regular text-text-quaternary">·</div>
|
||||
<Tooltip popupContent={t(config.tipKey as never, { ns: 'plugin' })}>
|
||||
<div>{config.icon}</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<span aria-label={tip} className="inline-flex">
|
||||
{config.icon}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{tip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { EndpointListItem, PluginDetail } from '../types'
|
||||
import {
|
||||
AlertDialog,
|
||||
@ -9,7 +10,7 @@ import {
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiClipboardLine, RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
@ -17,7 +18,6 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import {
|
||||
@ -29,6 +29,8 @@ import {
|
||||
import EndpointModal from './endpoint-modal'
|
||||
import { NAME_FIELD } from './utils'
|
||||
|
||||
type EndpointModalFormSchemas = ComponentProps<typeof EndpointModal>['formSchemas']
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
data: EndpointListItem
|
||||
@ -118,7 +120,7 @@ const EndpointCard = ({
|
||||
toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
|
||||
},
|
||||
})
|
||||
const handleUpdate = (state: Record<string, any>) => updateEndpoint({
|
||||
const handleUpdate = (state: Record<string, unknown>) => updateEndpoint({
|
||||
endpointID,
|
||||
state,
|
||||
})
|
||||
@ -148,22 +150,22 @@ const EndpointCard = ({
|
||||
}
|
||||
}, [isCopied])
|
||||
|
||||
const CopyIcon = isCopied ? CopyCheck : RiClipboardLine
|
||||
const copyLabel = t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })
|
||||
|
||||
return (
|
||||
<div className="rounded-xl bg-background-section-burn p-0.5">
|
||||
<div className="group rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-2.5 pl-3">
|
||||
<div className="flex items-center">
|
||||
<div className="mb-1 flex h-6 grow items-center gap-1 system-md-semibold text-text-secondary">
|
||||
<RiLoginCircleLine className="h-4 w-4" />
|
||||
<span aria-hidden className="i-ri-login-circle-line h-4 w-4" />
|
||||
<div>{data.name}</div>
|
||||
</div>
|
||||
<div className="hidden items-center group-hover:flex">
|
||||
<ActionButton onClick={showEndpointModalConfirm}>
|
||||
<RiEditLine className="h-4 w-4" />
|
||||
<span aria-hidden className="i-ri-edit-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={showDeleteConfirm} className="text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive">
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
<span aria-hidden className="i-ri-delete-bin-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
@ -172,10 +174,23 @@ const EndpointCard = ({
|
||||
<div className="w-12 shrink-0 system-xs-regular text-text-tertiary">{endpoint.method}</div>
|
||||
<div className="group/item flex grow items-center truncate system-xs-regular text-text-secondary">
|
||||
<div title={`${data.url}${endpoint.path}`} className="truncate">{`${data.url}${endpoint.path}`}</div>
|
||||
<Tooltip popupContent={t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })} position="top">
|
||||
<ActionButton className="ml-2 hidden shrink-0 group-hover/item:flex" onClick={() => handleCopy(`${data.url}${endpoint.path}`)}>
|
||||
<CopyIcon className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
</ActionButton>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={copyLabel}
|
||||
className="ml-2 hidden shrink-0 group-hover/item:flex"
|
||||
onClick={() => handleCopy(`${data.url}${endpoint.path}`)}
|
||||
>
|
||||
{isCopied
|
||||
? <CopyCheck aria-hidden className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
: <span aria-hidden className="i-ri-clipboard-line h-3.5 w-3.5 text-text-tertiary" />}
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent placement="top">
|
||||
{copyLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@ -244,7 +259,7 @@ const EndpointCard = ({
|
||||
</AlertDialog>
|
||||
{isShowEndpointModal && (
|
||||
<EndpointModal
|
||||
formSchemas={formSchemas as any}
|
||||
formSchemas={formSchemas as EndpointModalFormSchemas}
|
||||
defaultValues={formValue}
|
||||
onCancel={hideEndpointModalConfirm}
|
||||
onSaved={handleUpdate}
|
||||
|
||||
@ -7,13 +7,13 @@ import { FormTypeEnum } from '@/app/components/header/account-setting/model-prov
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
export type ReasoningConfigInputValue = {
|
||||
type ReasoningConfigInputValue = {
|
||||
type?: VarKindType
|
||||
value?: unknown
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
|
||||
export type ReasoningConfigInput = {
|
||||
type ReasoningConfigInput = {
|
||||
value: ReasoningConfigInputValue
|
||||
auto?: 0 | 1
|
||||
}
|
||||
|
||||
@ -7,12 +7,6 @@ vi.mock('@/app/components/base/progress-bar/progress-circle', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip" data-tip={popupContent}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/plugins-nav/downloading-icon', () => ({
|
||||
default: () => <span data-testid="downloading-icon" />,
|
||||
}))
|
||||
@ -38,18 +32,17 @@ describe('TaskStatusIndicator', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<TaskStatusIndicator {...defaultProps} />)
|
||||
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Installing plugins' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass tip to tooltip', () => {
|
||||
it('should use tip as the trigger accessible name', () => {
|
||||
render(<TaskStatusIndicator {...defaultProps} tip="My tip" />)
|
||||
expect(screen.getByTestId('tooltip')).toHaveAttribute('data-tip', 'My tip')
|
||||
expect(screen.getByRole('button', { name: 'My tip' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render install icon by default', () => {
|
||||
const { container } = render(<TaskStatusIndicator {...defaultProps} />)
|
||||
// RiInstallLine renders as svg
|
||||
expect(container.querySelector('svg')).toBeInTheDocument()
|
||||
expect(container.querySelector('.i-ri-install-line')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('downloading-icon')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -127,7 +120,6 @@ describe('TaskStatusIndicator', () => {
|
||||
totalPluginsLength={3}
|
||||
/>,
|
||||
)
|
||||
// RiCheckboxCircleFill is rendered as svg with text-text-success
|
||||
const successIcon = container.querySelector('.text-text-success')
|
||||
expect(successIcon).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiInstallLine,
|
||||
} from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
|
||||
|
||||
type TaskStatusIndicatorProps = {
|
||||
@ -39,56 +35,61 @@ const TaskStatusIndicator: FC<TaskStatusIndicatorProps> = ({
|
||||
const showSuccessIcon = isSuccess || (successPluginsLength > 0 && runningPluginsLength === 0)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={tip}
|
||||
asChild
|
||||
offset={8}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
|
||||
showErrorStyle && 'cursor-pointer border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
|
||||
(isInstalling || isInstallingWithSuccess || isSuccess) && 'cursor-pointer hover:bg-components-button-secondary-bg-hover',
|
||||
)}
|
||||
id="plugin-task-trigger"
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Main Icon */}
|
||||
{showDownloadingIcon
|
||||
? <DownloadingIcon />
|
||||
: (
|
||||
<RiInstallLine
|
||||
className={cn(
|
||||
'h-4 w-4 text-components-button-secondary-text',
|
||||
showErrorStyle && 'text-components-button-destructive-secondary-text',
|
||||
)}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
aria-label={tip}
|
||||
className={cn(
|
||||
'relative h-8 w-8 rounded-lg px-0',
|
||||
'focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
showErrorStyle && 'cursor-pointer border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
|
||||
(isInstalling || isInstallingWithSuccess || isSuccess) && 'cursor-pointer hover:bg-components-button-secondary-bg-hover',
|
||||
)}
|
||||
id="plugin-task-trigger"
|
||||
onClick={onClick}
|
||||
>
|
||||
{showDownloadingIcon
|
||||
? <DownloadingIcon />
|
||||
: (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'i-ri-install-line h-4 w-4 text-components-button-secondary-text',
|
||||
showErrorStyle && 'text-components-button-destructive-secondary-text',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Status Indicator Badge */}
|
||||
<div className="absolute -top-1 -right-1">
|
||||
{(isInstalling || isInstallingWithSuccess) && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? successPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
/>
|
||||
)}
|
||||
{isInstallingWithError && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? runningPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
sectorFillColor="fill-components-progress-error-border"
|
||||
circleStrokeColor="stroke-components-progress-error-border"
|
||||
/>
|
||||
)}
|
||||
{showSuccessIcon && !isInstalling && !isInstallingWithSuccess && !isInstallingWithError && (
|
||||
<RiCheckboxCircleFill className="h-3.5 w-3.5 text-text-success" />
|
||||
)}
|
||||
{isFailed && (
|
||||
<RiErrorWarningFill className="h-3.5 w-3.5 text-text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute -top-1 -right-1">
|
||||
{(isInstalling || isInstallingWithSuccess) && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? successPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
/>
|
||||
)}
|
||||
{isInstallingWithError && (
|
||||
<ProgressCircle
|
||||
percentage={(totalPluginsLength > 0 ? runningPluginsLength / totalPluginsLength : 0) * 100}
|
||||
circleFillColor="fill-components-progress-brand-bg"
|
||||
sectorFillColor="fill-components-progress-error-border"
|
||||
circleStrokeColor="stroke-components-progress-error-border"
|
||||
/>
|
||||
)}
|
||||
{showSuccessIcon && !isInstalling && !isInstallingWithSuccess && !isInstallingWithError && (
|
||||
<span aria-hidden className="i-ri-checkbox-circle-fill h-3.5 w-3.5 text-text-success" />
|
||||
)}
|
||||
{isFailed && (
|
||||
<span aria-hidden className="i-ri-error-warning-fill h-3.5 w-3.5 text-text-destructive" />
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent sideOffset={8}>{tip}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export enum PluginSource {
|
||||
debugging = 'remote',
|
||||
}
|
||||
|
||||
export type PluginToolDeclaration = {
|
||||
type PluginToolDeclaration = {
|
||||
identity: {
|
||||
author: string
|
||||
name: string
|
||||
@ -34,12 +34,12 @@ export type PluginToolDeclaration = {
|
||||
credentials_schema: ToolCredential[] // TODO
|
||||
}
|
||||
|
||||
export type PluginEndpointDeclaration = {
|
||||
type PluginEndpointDeclaration = {
|
||||
settings: ToolCredential[]
|
||||
endpoints: EndpointItem[]
|
||||
}
|
||||
|
||||
export type EndpointItem = {
|
||||
type EndpointItem = {
|
||||
path: string
|
||||
method: string
|
||||
hidden?: boolean
|
||||
@ -60,7 +60,7 @@ export type EndpointListItem = {
|
||||
hook_id: string
|
||||
}
|
||||
|
||||
export type PluginDeclarationMeta = {
|
||||
type PluginDeclarationMeta = {
|
||||
version: string
|
||||
minimum_dify_version?: string
|
||||
}
|
||||
@ -96,14 +96,14 @@ export type PluginTriggerSubscriptionConstructor = {
|
||||
parameters: ParametersSchema[]
|
||||
}
|
||||
|
||||
export type PluginTriggerDefinition = {
|
||||
type PluginTriggerDefinition = {
|
||||
events: TriggerEvent[]
|
||||
identity: Identity
|
||||
subscription_constructor: PluginTriggerSubscriptionConstructor
|
||||
subscription_schema: ParametersSchema[]
|
||||
}
|
||||
|
||||
export type CredentialsSchema = {
|
||||
type CredentialsSchema = {
|
||||
name: string
|
||||
label: Record<Locale, string>
|
||||
description: Record<Locale, string>
|
||||
@ -117,7 +117,7 @@ export type CredentialsSchema = {
|
||||
placeholder: Record<Locale, string>
|
||||
}
|
||||
|
||||
export type OauthSchema = {
|
||||
type OauthSchema = {
|
||||
client_schema: CredentialsSchema[]
|
||||
credentials_schema: CredentialsSchema[]
|
||||
}
|
||||
@ -352,7 +352,7 @@ export enum InstallStep {
|
||||
installFailed = 'failed',
|
||||
}
|
||||
|
||||
export type GitHubAsset = {
|
||||
type GitHubAsset = {
|
||||
id: number
|
||||
name: string
|
||||
browser_download_url: string
|
||||
@ -496,7 +496,7 @@ export type PackageDependency = {
|
||||
|
||||
export type Dependency = GitHubItemAndMarketPlaceDependency | PackageDependency
|
||||
|
||||
export type Version = {
|
||||
type Version = {
|
||||
plugin_org: string
|
||||
plugin_name: string
|
||||
version: string
|
||||
@ -554,7 +554,7 @@ export type StrategyDetail = {
|
||||
features: AgentFeature[]
|
||||
}
|
||||
|
||||
export type Identity = {
|
||||
type Identity = {
|
||||
author: string
|
||||
name: string
|
||||
label: Record<Locale, string>
|
||||
@ -564,7 +564,7 @@ export type Identity = {
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export type StrategyDeclaration = {
|
||||
type StrategyDeclaration = {
|
||||
identity: Identity
|
||||
plugin_id: string
|
||||
strategies: StrategyDetail[]
|
||||
|
||||
@ -698,16 +698,9 @@ describe('MCPDetailContent', () => {
|
||||
const onHide = vi.fn()
|
||||
render(<MCPDetailContent {...defaultProps} onHide={onHide} />, { wrapper: createWrapper() })
|
||||
|
||||
// Find the close button (ActionButton with RiCloseLine)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const closeButton = buttons.find(btn =>
|
||||
btn.querySelector('svg.h-4.w-4'),
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
|
||||
if (closeButton) {
|
||||
fireEvent.click(closeButton)
|
||||
expect(onHide).toHaveBeenCalled()
|
||||
}
|
||||
expect(onHide).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ComponentProps, FC } from 'react'
|
||||
import type { ToolWithProvider } from '../../../workflow/types'
|
||||
import {
|
||||
AlertDialog,
|
||||
@ -12,18 +12,13 @@ import {
|
||||
} from '@langgenius/dify-ui/alert-dialog'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiLoader2Line,
|
||||
RiLoopLeftLine,
|
||||
} from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -49,6 +44,11 @@ type Props = {
|
||||
onFirstCreate: () => void
|
||||
}
|
||||
|
||||
type MCPModalConfirmPayload = Parameters<ComponentProps<typeof MCPModal>['onConfirm']>[0]
|
||||
type MutationResult = {
|
||||
result?: string
|
||||
}
|
||||
|
||||
const MCPDetailContent: FC<Props> = ({
|
||||
detail,
|
||||
onUpdate,
|
||||
@ -128,14 +128,14 @@ const MCPDetailContent: FC<Props> = ({
|
||||
}
|
||||
}, [onFirstCreate, isCurrentWorkspaceManager, detail, authorizeMcp, handleUpdateTools, handleOAuthCallback, onUpdate])
|
||||
|
||||
const handleUpdate = useCallback(async (data: any) => {
|
||||
const handleUpdate = useCallback(async (data: MCPModalConfirmPayload) => {
|
||||
if (!detail)
|
||||
return
|
||||
const res = await updateMCP({
|
||||
...data,
|
||||
provider_id: detail.id,
|
||||
})
|
||||
if ((res as any)?.result === 'success') {
|
||||
}) as MutationResult
|
||||
if (res.result === 'success') {
|
||||
hideUpdateModal()
|
||||
onUpdate()
|
||||
handleAuthorize()
|
||||
@ -146,9 +146,9 @@ const MCPDetailContent: FC<Props> = ({
|
||||
if (!detail)
|
||||
return
|
||||
showDeleting()
|
||||
const res = await deleteMCP(detail.id)
|
||||
const res = await deleteMCP(detail.id) as MutationResult
|
||||
hideDeleting()
|
||||
if ((res as any)?.result === 'success') {
|
||||
if (res.result === 'success') {
|
||||
hideDeleteConfirm()
|
||||
onUpdate(true)
|
||||
}
|
||||
@ -161,6 +161,8 @@ const MCPDetailContent: FC<Props> = ({
|
||||
|
||||
if (!detail)
|
||||
return null
|
||||
const identifierLabel = t('mcp.identifier', { ns: 'tools' })
|
||||
const serverUrlLabel = t('mcp.modal.serverUrl', { ns: 'tools' })
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -174,12 +176,37 @@ const MCPDetailContent: FC<Props> = ({
|
||||
<div className="truncate system-md-semibold text-text-primary" title={detail.name}>{detail.name}</div>
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-1">
|
||||
<Tooltip popupContent={t('mcp.identifier', { ns: 'tools' })}>
|
||||
<div className="shrink-0 cursor-pointer system-xs-regular text-text-secondary" onClick={() => copy(detail.server_identifier || '')}>{detail.server_identifier}</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
aria-label={identifierLabel}
|
||||
className="h-auto shrink-0 cursor-pointer rounded bg-transparent p-0 text-left system-xs-regular text-text-secondary hover:bg-transparent focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
onClick={() => copy(detail.server_identifier || '')}
|
||||
>
|
||||
{detail.server_identifier}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{identifierLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="shrink-0 system-xs-regular text-text-quaternary">·</div>
|
||||
<Tooltip popupContent={t('mcp.modal.serverUrl', { ns: 'tools' })}>
|
||||
<div className="truncate system-xs-regular text-text-secondary">{detail.server_url}</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div aria-label={serverUrlLabel} className="truncate system-xs-regular text-text-secondary">
|
||||
{detail.server_url}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{serverUrlLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@ -188,8 +215,8 @@ const MCPDetailContent: FC<Props> = ({
|
||||
onEdit={showUpdateModal}
|
||||
onRemove={showDeleteConfirm}
|
||||
/>
|
||||
<ActionButton onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
<ActionButton aria-label={t('operation.close', { ns: 'common' })} onClick={onHide}>
|
||||
<span aria-hidden className="i-ri-close-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
@ -221,7 +248,7 @@ const MCPDetailContent: FC<Props> = ({
|
||||
className="w-full"
|
||||
disabled
|
||||
>
|
||||
<RiLoader2Line className={cn('mr-1 h-4 w-4 animate-spin')} />
|
||||
<span aria-hidden className="mr-1 i-ri-loader-2-line h-4 w-4 animate-spin" />
|
||||
{t('mcp.authorizing', { ns: 'tools' })}
|
||||
</Button>
|
||||
)}
|
||||
@ -262,7 +289,7 @@ const MCPDetailContent: FC<Props> = ({
|
||||
</div>
|
||||
<div>
|
||||
<Button size="small" onClick={showUpdateConfirm}>
|
||||
<RiLoopLeftLine className="mr-1 h-3.5 w-3.5" />
|
||||
<span aria-hidden className="mr-1 i-ri-loop-left-line h-3.5 w-3.5" />
|
||||
{t('mcp.update', { ns: 'tools' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -100,7 +100,7 @@ export type ToolParameter = {
|
||||
max?: number
|
||||
}
|
||||
|
||||
export type TriggerParameter = {
|
||||
type TriggerParameter = {
|
||||
name: string
|
||||
label: LocalizedText
|
||||
human_description: LocalizedText
|
||||
@ -165,7 +165,7 @@ export type CustomCollectionBackend = {
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
export type ParamItem = {
|
||||
type ParamItem = {
|
||||
name: string
|
||||
label: LocalizedText
|
||||
human_description: LocalizedText
|
||||
|
||||
@ -10,7 +10,7 @@ import { handleStream, post } from '@/service/base'
|
||||
import { ContentType } from '@/service/fetch'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type HandleRunMode = TriggerType
|
||||
type HandleRunMode = TriggerType
|
||||
export type HandleRunOptions = {
|
||||
mode?: HandleRunMode
|
||||
scheduleNodeId?: string
|
||||
|
||||
@ -124,7 +124,7 @@ export type DataSourceItem = {
|
||||
is_authorized: boolean
|
||||
}
|
||||
|
||||
export type TriggerCredentialField = {
|
||||
type TriggerCredentialField = {
|
||||
type: 'secret-input' | 'text-input' | 'select' | 'boolean'
|
||||
| 'app-selector' | 'model-selector' | 'tools-selector'
|
||||
name: string
|
||||
@ -226,14 +226,14 @@ export type TriggerLogEntity = {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type LogRequest = {
|
||||
type LogRequest = {
|
||||
method: string
|
||||
url: string
|
||||
headers: LogRequestHeaders
|
||||
data: string
|
||||
}
|
||||
|
||||
export type LogRequestHeaders = {
|
||||
type LogRequestHeaders = {
|
||||
'Host': string
|
||||
'User-Agent': string
|
||||
'Content-Length': string
|
||||
@ -251,13 +251,13 @@ export type LogRequestHeaders = {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export type LogResponse = {
|
||||
type LogResponse = {
|
||||
status_code: number
|
||||
headers: LogResponseHeaders
|
||||
data: string
|
||||
}
|
||||
|
||||
export type LogResponseHeaders = {
|
||||
type LogResponseHeaders = {
|
||||
'Content-Type': string
|
||||
'Content-Length': string
|
||||
[key: string]: string
|
||||
|
||||
@ -22,7 +22,7 @@ export type NodePanelPresenceUser = {
|
||||
avatar?: string | null
|
||||
}
|
||||
|
||||
export type NodePanelPresenceInfo = NodePanelPresenceUser & {
|
||||
type NodePanelPresenceInfo = NodePanelPresenceUser & {
|
||||
clientId: string
|
||||
timestamp: number
|
||||
}
|
||||
@ -39,7 +39,7 @@ export type CollaborationState = {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type CollaborationEventType
|
||||
type CollaborationEventType
|
||||
= | 'mouse_move'
|
||||
| 'vars_and_features_update'
|
||||
| 'sync_request'
|
||||
|
||||
@ -4,7 +4,7 @@ export type WebSocketConfig = {
|
||||
withCredentials?: boolean
|
||||
}
|
||||
|
||||
export type ConnectionInfo = {
|
||||
type ConnectionInfo = {
|
||||
connected: boolean
|
||||
connecting: boolean
|
||||
socketId?: string
|
||||
|
||||
@ -27,7 +27,7 @@ export type SyncDraftCallback = {
|
||||
onError?: () => void
|
||||
onSettled?: () => void
|
||||
}
|
||||
export type CommonHooksFnMap = {
|
||||
type CommonHooksFnMap = {
|
||||
doSyncWorkflowDraft: (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: SyncDraftCallback,
|
||||
|
||||
@ -182,6 +182,16 @@ describe('var-reference-picker.helpers', () => {
|
||||
maxVarNameWidth: expect.any(Number),
|
||||
})
|
||||
|
||||
expect(getWidthAllocations(240, '', 'sys.user_id', 'String')).toEqual({
|
||||
maxNodeNameWidth: 0,
|
||||
maxTypeWidth: 64,
|
||||
maxVarNameWidth: 119,
|
||||
})
|
||||
|
||||
expect(getWidthAllocations(240, 'User Input', 'aa', 'String')).toMatchObject({
|
||||
maxVarNameWidth: 16,
|
||||
})
|
||||
|
||||
expect(getTooltipContent(true, true, true)).toBe('full-path')
|
||||
expect(getTooltipContent(true, false, false)).toBe('invalid-variable')
|
||||
expect(getTooltipContent(false, false, true)).toBeNull()
|
||||
|
||||
@ -168,11 +168,15 @@ export const getWidthAllocations = (
|
||||
) => {
|
||||
const availableWidth = triggerWidth - 56
|
||||
const totalTextLength = (nodeTitle + varName + type).length || 1
|
||||
const priorityWidth = 15
|
||||
const priorityWidth = nodeTitle ? 15 : 0
|
||||
const minVarNameWidth = varName ? 16 : 0
|
||||
return {
|
||||
maxNodeNameWidth: priorityWidth + Math.floor(nodeTitle.length / totalTextLength * availableWidth),
|
||||
maxTypeWidth: Math.floor(type.length / totalTextLength * availableWidth),
|
||||
maxVarNameWidth: -priorityWidth + Math.floor(varName.length / totalTextLength * availableWidth),
|
||||
maxVarNameWidth: Math.max(
|
||||
minVarNameWidth,
|
||||
-priorityWidth + Math.floor(varName.length / totalTextLength * availableWidth),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -279,13 +279,15 @@ const VarReferencePicker: FC<Props> = ({
|
||||
[outputVarNode?.type, varName],
|
||||
)
|
||||
const showErrorIcon = hasValue && !isValidVar
|
||||
const shouldShowNodeName = isShowNodeName && !isEnv && !isChatVar && !isGlobal && !isRagVar
|
||||
const visibleNodeTitle = shouldShowNodeName ? outputVarNode?.title || '' : ''
|
||||
|
||||
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
|
||||
const {
|
||||
maxNodeNameWidth,
|
||||
maxTypeWidth,
|
||||
maxVarNameWidth,
|
||||
} = getWidthAllocations(triggerWidth, outputVarNode?.title || '', varName || '', type || '')
|
||||
} = getWidthAllocations(triggerWidth, visibleNodeTitle, varName || '', type || '')
|
||||
|
||||
const hoverPopup = useMemo<HoverPopup | null>(() => {
|
||||
const tooltipType = getTooltipContent(hasValue, isShowAPart, isValidVar)
|
||||
@ -380,7 +382,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
isJustShowValue={isJustShowValue}
|
||||
isLoading={isLoading}
|
||||
isShowAPart={isShowAPart}
|
||||
isShowNodeName={isShowNodeName && !isEnv && !isChatVar && !isGlobal && !isRagVar}
|
||||
isShowNodeName={shouldShowNodeName}
|
||||
isSupportConstantValue={isSupportConstantValue}
|
||||
maxNodeNameWidth={maxNodeNameWidth}
|
||||
maxTypeWidth={maxTypeWidth}
|
||||
|
||||
@ -32,7 +32,7 @@ export type WeightedScore = {
|
||||
}
|
||||
}
|
||||
|
||||
export type RetrievalSetting = {
|
||||
type RetrievalSetting = {
|
||||
search_method?: RETRIEVE_METHOD
|
||||
reranking_enable?: boolean
|
||||
reranking_model?: RerankingModel
|
||||
|
||||
@ -83,8 +83,8 @@ export type LoopNodeType = CommonNodeType & {
|
||||
loop_variables?: LoopVariable[]
|
||||
}
|
||||
|
||||
export type HandleUpdateLoopVariable = (id: string, updateData: Partial<LoopVariable>) => void
|
||||
export type HandleRemoveLoopVariable = (id: string) => void
|
||||
type HandleUpdateLoopVariable = (id: string, updateData: Partial<LoopVariable>) => void
|
||||
type HandleRemoveLoopVariable = (id: string) => void
|
||||
|
||||
export type LoopVariablesComponentShape = {
|
||||
nodeId: string
|
||||
|
||||
@ -4,7 +4,7 @@ export type ScheduleMode = 'visual' | 'cron'
|
||||
|
||||
export type ScheduleFrequency = 'hourly' | 'daily' | 'weekly' | 'monthly'
|
||||
|
||||
export type VisualConfig = {
|
||||
type VisualConfig = {
|
||||
time?: string
|
||||
weekdays?: string[]
|
||||
on_minute?: number
|
||||
|
||||
@ -16,9 +16,9 @@ const isPresent = (v: unknown): boolean => {
|
||||
return !(v === '' || v === null || v === undefined || v === false)
|
||||
}
|
||||
// Column configuration types for table components
|
||||
export type ColumnType = 'input' | 'select' | 'switch' | 'custom'
|
||||
type ColumnType = 'input' | 'select' | 'switch' | 'custom'
|
||||
|
||||
export type SelectOption = {
|
||||
type SelectOption = {
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
@ -36,8 +36,9 @@ describe('VariableTypeSelector', () => {
|
||||
await user.keyboard('{Escape}')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('number')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('combobox')).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the custom popup class in in-cell mode', async () => {
|
||||
|
||||
@ -1,38 +1,47 @@
|
||||
'use client'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
type Props = {
|
||||
type Props<T extends string> = {
|
||||
inCell?: boolean
|
||||
value?: any
|
||||
list: any
|
||||
onSelect: (value: any) => void
|
||||
value?: T
|
||||
list: readonly T[]
|
||||
onSelect: (value: T) => void
|
||||
popupClassName?: string
|
||||
}
|
||||
|
||||
const VariableTypeSelector = ({
|
||||
const VariableTypeSelector = <T extends string, >({
|
||||
inCell = false,
|
||||
value,
|
||||
list,
|
||||
onSelect,
|
||||
popupClassName,
|
||||
}: Props) => {
|
||||
}: Props<T>) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleValueChange = (nextValue: string | null) => {
|
||||
if (!nextValue)
|
||||
return
|
||||
|
||||
const nextItem = list.find(item => item === nextValue)
|
||||
if (!nextItem)
|
||||
return
|
||||
|
||||
onSelect(nextItem)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
<Select
|
||||
value={value ?? null}
|
||||
open={open}
|
||||
onOpenChange={() => setOpen(v => !v)}
|
||||
placement="bottom"
|
||||
onOpenChange={setOpen}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger className="w-full" onClick={() => setOpen(v => !v)}>
|
||||
<SelectTrigger
|
||||
className="h-auto w-full max-w-none cursor-pointer rounded-none bg-transparent p-0 hover:bg-transparent focus-visible:bg-transparent data-popup-open:bg-transparent [&>*:last-child]:hidden"
|
||||
>
|
||||
<div className={cn(
|
||||
'flex w-full cursor-pointer items-center px-2',
|
||||
!inCell && 'rounded-lg bg-components-input-bg-normal py-1 hover:bg-state-base-hover-alt',
|
||||
@ -48,27 +57,22 @@ const VariableTypeSelector = ({
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
<RiArrowDownSLine className="ml-0.5 h-4 w-4 text-text-quaternary" />
|
||||
<span className="ml-0.5 i-ri-arrow-down-s-line h-4 w-4 text-text-quaternary" aria-hidden="true" />
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className={cn('z-11 w-full', popupClassName)}>
|
||||
<div className="rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
|
||||
{list.map((item: any) => (
|
||||
<div
|
||||
key={item}
|
||||
className="flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pr-2 pl-3 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
onSelect(item)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="grow truncate system-md-regular text-text-secondary">{item}</div>
|
||||
{value === item && <RiCheckLine className="h-4 w-4 text-text-accent" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</SelectTrigger>
|
||||
<SelectContent placement="bottom-start" popupClassName={cn('bg-components-panel-bg-blur', popupClassName)}>
|
||||
{list.map(item => (
|
||||
<SelectItem
|
||||
key={item}
|
||||
value={item}
|
||||
className="h-auto gap-2 py-[6px] pr-2 pl-3 system-md-regular font-normal"
|
||||
>
|
||||
<SelectItemText className="px-0 system-md-regular text-text-secondary">{item}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -498,7 +498,7 @@ export type ChildNodeTypeCount = {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
export const TRIGGER_NODE_TYPES = [
|
||||
const TRIGGER_NODE_TYPES = [
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
|
||||
@ -98,14 +98,14 @@ function createStore({
|
||||
return store
|
||||
}
|
||||
|
||||
export type WorkflowHistoryStore = {
|
||||
type WorkflowHistoryStore = {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
workflowHistoryEvent: WorkflowHistoryEventT | undefined
|
||||
workflowHistoryEventMeta?: WorkflowHistoryEventMeta
|
||||
}
|
||||
|
||||
export type WorkflowHistoryActions = {
|
||||
type WorkflowHistoryActions = {
|
||||
setNodes?: (nodes: Node[]) => void
|
||||
setEdges?: (edges: Edge[]) => void
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
@ -23,73 +22,6 @@ vi.mock('react-i18next', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/input', () => ({
|
||||
default: ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
}: {
|
||||
value?: string
|
||||
onChange: (event: { target: { value: string } }) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}) => (
|
||||
<input
|
||||
className={className}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={e => onChange({ target: { value: e.target.value } })}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/popover', async () => {
|
||||
const React = await import('react')
|
||||
const PopoverContext = React.createContext({
|
||||
open: false,
|
||||
setOpen: (_open: boolean) => {},
|
||||
})
|
||||
|
||||
const Popover = ({
|
||||
children,
|
||||
open: controlledOpen,
|
||||
onOpenChange,
|
||||
}: {
|
||||
children: ReactNode
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => {
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
|
||||
const isControlled = controlledOpen !== undefined
|
||||
const open = isControlled ? !!controlledOpen : uncontrolledOpen
|
||||
const setOpen = (nextOpen: boolean) => {
|
||||
if (!isControlled)
|
||||
setUncontrolledOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
|
||||
return (
|
||||
<PopoverContext.Provider value={{ open, setOpen }}>
|
||||
{children}
|
||||
</PopoverContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const PopoverTrigger = ({ render }: { render: ReactNode }) => <>{render}</>
|
||||
|
||||
const PopoverContent = ({ children }: { children: ReactNode }) => {
|
||||
const { open } = React.useContext(PopoverContext)
|
||||
return open ? <div data-testid="education-search-popover">{children}</div> : null
|
||||
}
|
||||
|
||||
return {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
}
|
||||
})
|
||||
|
||||
const ControlledSearchInput = () => {
|
||||
const [value, setValue] = useState('')
|
||||
return <SearchInput value={value} onChange={setValue} />
|
||||
@ -102,27 +34,38 @@ describe('education-apply/search-input', () => {
|
||||
educationMocks.hasNext = false
|
||||
})
|
||||
|
||||
it('opens the popover, queries schools, and closes after selection', async () => {
|
||||
it('keeps the search field editable when used as the popover trigger', async () => {
|
||||
const user = userEvent.setup()
|
||||
educationMocks.schools = []
|
||||
|
||||
render(<ControlledSearchInput />)
|
||||
|
||||
const input = screen.getByPlaceholderText('form.schoolName.placeholder') as HTMLInputElement
|
||||
expect(input.type).toBe('text')
|
||||
|
||||
await user.type(input, 'Alpha')
|
||||
|
||||
expect(input).toHaveValue('Alpha')
|
||||
expect(educationMocks.setSchools).toHaveBeenCalledWith([])
|
||||
expect(educationMocks.querySchoolsWithDebounced).toHaveBeenLastCalledWith({
|
||||
keywords: 'Alpha',
|
||||
page: 0,
|
||||
})
|
||||
})
|
||||
|
||||
it('closes the popover after selecting a school', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ControlledSearchInput />)
|
||||
|
||||
const input = screen.getByPlaceholderText('form.schoolName.placeholder')
|
||||
await user.type(input, 'A')
|
||||
await user.type(screen.getByPlaceholderText('form.schoolName.placeholder'), 'A')
|
||||
|
||||
expect(educationMocks.setSchools).toHaveBeenCalledWith([])
|
||||
expect(educationMocks.querySchoolsWithDebounced).toHaveBeenLastCalledWith({
|
||||
keywords: 'A',
|
||||
page: 0,
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('education-search-popover')).toBeInTheDocument()
|
||||
expect(screen.getByText('Alpha University')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByText('Beta College'))
|
||||
|
||||
expect(screen.getByDisplayValue('Beta College')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('education-search-popover')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Alpha University')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('loads the next page when the dropdown is scrolled to the bottom', async () => {
|
||||
|
||||
@ -77,6 +77,7 @@ const SearchInput = ({
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
nativeButton={false}
|
||||
render={(
|
||||
<Input
|
||||
className="w-full"
|
||||
@ -86,7 +87,7 @@ const SearchInput = ({
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{!!schools.length && !!value && (
|
||||
{open && !!schools.length && !!value && (
|
||||
<PopoverContent
|
||||
placement="bottom"
|
||||
sideOffset={4}
|
||||
|
||||
@ -7,7 +7,7 @@ import { createContext, useContext } from 'use-context-selector'
|
||||
* Typed event object emitted via the shared EventEmitter.
|
||||
* Covers workflow updates, prompt-editor commands, DSL export checks, etc.
|
||||
*/
|
||||
export type EventEmitterMessage = {
|
||||
type EventEmitterMessage = {
|
||||
type: string
|
||||
payload?: unknown
|
||||
instanceId?: string
|
||||
|
||||
@ -27,7 +27,7 @@ export type WorkflowCommentList = {
|
||||
participants: UserProfile[]
|
||||
}
|
||||
|
||||
export type WorkflowCommentDetailMention = {
|
||||
type WorkflowCommentDetailMention = {
|
||||
mentioned_user_id: string
|
||||
mentioned_user_account?: UserProfile | null
|
||||
reply_id: string | null
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "إلغاء",
|
||||
"stepOne.uploader.change": "تغيير",
|
||||
"stepOne.uploader.failed": "فشل التحميل",
|
||||
"stepOne.uploader.tip": "يدعم {{supportTypes}}. بحد أقصى {{batchCount}} في الدفعة الواحدة و {{size}} ميجابايت لكل منها. الحد الأقصى الإجمالي {{totalCount}} ملفات.",
|
||||
"stepOne.uploader.tip": "يدعم {{supportTypes}}. بحد أقصى {{batchCount}} في الدفعة الواحدة و {{size}} ميجابايت لكل منها.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "يدعم {{supportTypes}}. بحد أقصى {{batchCount}} في الدفعة الواحدة و {{size}} ميجابايت لكل منها. الحد الأقصى الإجمالي {{totalCount}} ملفات.",
|
||||
"stepOne.uploader.title": "تحميل ملف",
|
||||
"stepOne.uploader.validation.count": "ملفات متعددة غير مدعومة",
|
||||
"stepOne.uploader.validation.filesNumber": "لقد وصلت إلى حد تحميل الدفعة البالغ {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Abbrechen",
|
||||
"stepOne.uploader.change": "Ändern",
|
||||
"stepOne.uploader.failed": "Hochladen fehlgeschlagen",
|
||||
"stepOne.uploader.tip": "Unterstützt {{supportTypes}}. Maximal {{batchCount}} Dateien pro Batch und {{size}} MB pro Datei. Insgesamt maximal {{totalCount}} Dateien.",
|
||||
"stepOne.uploader.tip": "Unterstützt {{supportTypes}}. Maximal {{batchCount}} Dateien pro Batch und {{size}} MB pro Datei.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Unterstützt {{supportTypes}}. Maximal {{batchCount}} Dateien pro Batch und {{size}} MB pro Datei. Insgesamt maximal {{totalCount}} Dateien.",
|
||||
"stepOne.uploader.title": "Textdatei hochladen",
|
||||
"stepOne.uploader.validation.count": "Mehrere Dateien nicht unterstützt",
|
||||
"stepOne.uploader.validation.filesNumber": "Sie haben das Limit für die Stapelverarbeitung von {{filesNumber}} erreicht.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Cancel",
|
||||
"stepOne.uploader.change": "Change",
|
||||
"stepOne.uploader.failed": "Upload failed",
|
||||
"stepOne.uploader.tip": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each. Max total {{totalCount}} files.",
|
||||
"stepOne.uploader.tip": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each. Max total {{totalCount}} files.",
|
||||
"stepOne.uploader.title": "Upload file",
|
||||
"stepOne.uploader.validation.count": "Multiple files not supported",
|
||||
"stepOne.uploader.validation.filesNumber": "You have reached the batch upload limit of {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Cancelar",
|
||||
"stepOne.uploader.change": "Cambiar",
|
||||
"stepOne.uploader.failed": "Error al cargar",
|
||||
"stepOne.uploader.tip": "Soporta {{supportTypes}}. Máximo {{batchCount}} archivos por lote y {{size}} MB cada uno. Total máximo de {{totalCount}} archivos.",
|
||||
"stepOne.uploader.tip": "Soporta {{supportTypes}}. Máximo {{batchCount}} archivos por lote y {{size}} MB cada uno.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Soporta {{supportTypes}}. Máximo {{batchCount}} archivos por lote y {{size}} MB cada uno. Total máximo de {{totalCount}} archivos.",
|
||||
"stepOne.uploader.title": "Cargar archivo",
|
||||
"stepOne.uploader.validation.count": "No se admiten varios archivos",
|
||||
"stepOne.uploader.validation.filesNumber": "Has alcanzado el límite de carga por lotes de {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "لغو",
|
||||
"stepOne.uploader.change": "تغییر",
|
||||
"stepOne.uploader.failed": "بارگذاری ناموفق بود",
|
||||
"stepOne.uploader.tip": "پشتیبانی از {{supportTypes}}. حداکثر {{batchCount}} فایل در هر دسته و {{size}} مگابایت برای هر فایل. حداکثر کل {{totalCount}} فایل.",
|
||||
"stepOne.uploader.tip": "پشتیبانی از {{supportTypes}}. حداکثر {{batchCount}} فایل در هر دسته و {{size}} مگابایت برای هر فایل.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "پشتیبانی از {{supportTypes}}. حداکثر {{batchCount}} فایل در هر دسته و {{size}} مگابایت برای هر فایل. حداکثر کل {{totalCount}} فایل.",
|
||||
"stepOne.uploader.title": "بارگذاری فایل",
|
||||
"stepOne.uploader.validation.count": "چندین فایل پشتیبانی نمیشود",
|
||||
"stepOne.uploader.validation.filesNumber": "شما به حد مجاز بارگذاری دستهای {{filesNumber}} رسیدهاید.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Annuler",
|
||||
"stepOne.uploader.change": "Changer",
|
||||
"stepOne.uploader.failed": "Le téléchargement a échoué",
|
||||
"stepOne.uploader.tip": "Prend en charge {{supportTypes}}. Maximum {{batchCount}} fichiers par lot et {{size}} MB chacun. Maximum total de {{totalCount}} fichiers.",
|
||||
"stepOne.uploader.tip": "Prend en charge {{supportTypes}}. Maximum {{batchCount}} fichiers par lot et {{size}} MB chacun.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Prend en charge {{supportTypes}}. Maximum {{batchCount}} fichiers par lot et {{size}} MB chacun. Maximum total de {{totalCount}} fichiers.",
|
||||
"stepOne.uploader.title": "Télécharger le fichier texte",
|
||||
"stepOne.uploader.validation.count": "Plusieurs fichiers non pris en charge",
|
||||
"stepOne.uploader.validation.filesNumber": "Vous avez atteint la limite de téléchargement par lot de {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "रद्द करें",
|
||||
"stepOne.uploader.change": "बदलें",
|
||||
"stepOne.uploader.failed": "अपलोड विफल रहा",
|
||||
"stepOne.uploader.tip": "{{supportTypes}} समर्थित है। एक बैच में अधिकतम {{batchCount}} फ़ाइलें और प्रत्येक {{size}} MB। कुल अधिकतम {{totalCount}} फ़ाइलें।",
|
||||
"stepOne.uploader.tip": "{{supportTypes}} समर्थित है। एक बैच में अधिकतम {{batchCount}} फ़ाइलें और प्रत्येक {{size}} MB।",
|
||||
"stepOne.uploader.tipWithTotalLimit": "{{supportTypes}} समर्थित है। एक बैच में अधिकतम {{batchCount}} फ़ाइलें और प्रत्येक {{size}} MB। कुल अधिकतम {{totalCount}} फ़ाइलें।",
|
||||
"stepOne.uploader.title": "फ़ाइल अपलोड करें",
|
||||
"stepOne.uploader.validation.count": "एकाधिक फ़ाइलें समर्थित नहीं हैं",
|
||||
"stepOne.uploader.validation.filesNumber": "आपने {{filesNumber}} की बैच अपलोड सीमा तक पहुँच गए हैं।",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Membatalkan",
|
||||
"stepOne.uploader.change": "Ubah",
|
||||
"stepOne.uploader.failed": "Upload gagal",
|
||||
"stepOne.uploader.tip": "Mendukung {{supportTypes}}. Maksimal {{batchCount}} dalam satu batch dan {{size}} MB masing-masing. Total maksimal {{totalCount}} file.",
|
||||
"stepOne.uploader.tip": "Mendukung {{supportTypes}}. Maksimal {{batchCount}} dalam satu batch dan {{size}} MB masing-masing.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Mendukung {{supportTypes}}. Maksimal {{batchCount}} dalam satu batch dan {{size}} MB masing-masing. Total maksimal {{totalCount}} file.",
|
||||
"stepOne.uploader.title": "Unggah file",
|
||||
"stepOne.uploader.validation.count": "Beberapa file tidak didukung",
|
||||
"stepOne.uploader.validation.filesNumber": "Anda telah mencapai batas unggah batch sebanyak {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Annulla",
|
||||
"stepOne.uploader.change": "Cambia",
|
||||
"stepOne.uploader.failed": "Caricamento fallito",
|
||||
"stepOne.uploader.tip": "Supporta {{supportTypes}}. Massimo {{batchCount}} file per batch e {{size}} MB ciascuno. Totale massimo {{totalCount}} file.",
|
||||
"stepOne.uploader.tip": "Supporta {{supportTypes}}. Massimo {{batchCount}} file per batch e {{size}} MB ciascuno.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Supporta {{supportTypes}}. Massimo {{batchCount}} file per batch e {{size}} MB ciascuno. Totale massimo {{totalCount}} file.",
|
||||
"stepOne.uploader.title": "Carica file",
|
||||
"stepOne.uploader.validation.count": "Più file non supportati",
|
||||
"stepOne.uploader.validation.filesNumber": "Hai raggiunto il limite di caricamento batch di {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "キャンセル",
|
||||
"stepOne.uploader.change": "変更",
|
||||
"stepOne.uploader.failed": "アップロードに失敗しました",
|
||||
"stepOne.uploader.tip": "{{supportTypes}}をサポートしています。1バッチあたり最大{{batchCount}}ファイル、各ファイル{{size}}MB まで。合計最大{{totalCount}}ファイル。",
|
||||
"stepOne.uploader.tip": "{{supportTypes}}をサポートしています。1バッチあたり最大{{batchCount}}ファイル、各ファイル{{size}}MB まで。",
|
||||
"stepOne.uploader.tipWithTotalLimit": "{{supportTypes}}をサポートしています。1バッチあたり最大{{batchCount}}ファイル、各ファイル{{size}}MB まで。合計最大{{totalCount}}ファイル。",
|
||||
"stepOne.uploader.title": "テキストファイルをアップロード",
|
||||
"stepOne.uploader.validation.count": "複数のファイルはサポートされていません",
|
||||
"stepOne.uploader.validation.filesNumber": "バッチアップロードの制限({{filesNumber}}個)に達しました。",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "취소",
|
||||
"stepOne.uploader.change": "변경",
|
||||
"stepOne.uploader.failed": "업로드에 실패했습니다",
|
||||
"stepOne.uploader.tip": "{{supportTypes}}을(를) 지원합니다. 배치당 최대 {{batchCount}}개 파일, 각 파일당 {{size}}MB까지. 총 최대 {{totalCount}}개 파일.",
|
||||
"stepOne.uploader.tip": "{{supportTypes}}을(를) 지원합니다. 배치당 최대 {{batchCount}}개 파일, 각 파일당 {{size}}MB까지.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "{{supportTypes}}을(를) 지원합니다. 배치당 최대 {{batchCount}}개 파일, 각 파일당 {{size}}MB까지. 총 최대 {{totalCount}}개 파일.",
|
||||
"stepOne.uploader.title": "텍스트 파일 업로드",
|
||||
"stepOne.uploader.validation.count": "여러 파일은 지원되지 않습니다",
|
||||
"stepOne.uploader.validation.filesNumber": "일괄 업로드 제한 ({{filesNumber}}개) 에 도달했습니다.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Cancel",
|
||||
"stepOne.uploader.change": "Change",
|
||||
"stepOne.uploader.failed": "Upload failed",
|
||||
"stepOne.uploader.tip": "Supports {{supportTypes}}. Max {{batchCount}} in a batch and {{size}} MB each. Max total {{totalCount}} files.",
|
||||
"stepOne.uploader.tip": "Ondersteunt {{supportTypes}}. Maximaal {{batchCount}} per batch en {{size}} MB per bestand.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Ondersteunt {{supportTypes}}. Maximaal {{batchCount}} per batch en {{size}} MB per bestand. Maximaal {{totalCount}} bestanden in totaal.",
|
||||
"stepOne.uploader.title": "Upload file",
|
||||
"stepOne.uploader.validation.count": "Multiple files not supported",
|
||||
"stepOne.uploader.validation.filesNumber": "You have reached the batch upload limit of {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Anuluj",
|
||||
"stepOne.uploader.change": "Zmień",
|
||||
"stepOne.uploader.failed": "Przesyłanie nie powiodło się",
|
||||
"stepOne.uploader.tip": "Obsługuje {{supportTypes}}. Maksymalnie {{batchCount}} plików w partii, każdy do {{size}} MB. Łącznie maksymalnie {{totalCount}} plików.",
|
||||
"stepOne.uploader.tip": "Obsługuje {{supportTypes}}. Maksymalnie {{batchCount}} plików w partii, każdy do {{size}} MB.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Obsługuje {{supportTypes}}. Maksymalnie {{batchCount}} plików w partii, każdy do {{size}} MB. Łącznie maksymalnie {{totalCount}} plików.",
|
||||
"stepOne.uploader.title": "Prześlij plik tekstowy",
|
||||
"stepOne.uploader.validation.count": "Nieobsługiwane przesyłanie wielu plików",
|
||||
"stepOne.uploader.validation.filesNumber": "Osiągnąłeś limit przesłania partii {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Cancelar",
|
||||
"stepOne.uploader.change": "Alterar",
|
||||
"stepOne.uploader.failed": "Falha no envio",
|
||||
"stepOne.uploader.tip": "Suporta {{supportTypes}}. Máximo de {{batchCount}} arquivos por lote e {{size}} MB cada. Total máximo de {{totalCount}} arquivos.",
|
||||
"stepOne.uploader.tip": "Suporta {{supportTypes}}. Máximo de {{batchCount}} arquivos por lote e {{size}} MB cada.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Suporta {{supportTypes}}. Máximo de {{batchCount}} arquivos por lote e {{size}} MB cada. Total máximo de {{totalCount}} arquivos.",
|
||||
"stepOne.uploader.title": "Enviar arquivo de texto",
|
||||
"stepOne.uploader.validation.count": "Vários arquivos não suportados",
|
||||
"stepOne.uploader.validation.filesNumber": "Limite de upload em massa {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Anulează",
|
||||
"stepOne.uploader.change": "Schimbă",
|
||||
"stepOne.uploader.failed": "Încărcarea a eșuat",
|
||||
"stepOne.uploader.tip": "Acceptă {{supportTypes}}. Maxim {{batchCount}} fișiere pe lot și {{size}} MB fiecare. Total maxim {{totalCount}} fișiere.",
|
||||
"stepOne.uploader.tip": "Acceptă {{supportTypes}}. Maxim {{batchCount}} fișiere pe lot și {{size}} MB fiecare.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Acceptă {{supportTypes}}. Maxim {{batchCount}} fișiere pe lot și {{size}} MB fiecare. Total maxim {{totalCount}} fișiere.",
|
||||
"stepOne.uploader.title": "Încărcați fișier text",
|
||||
"stepOne.uploader.validation.count": "Nu se acceptă mai multe fișiere",
|
||||
"stepOne.uploader.validation.filesNumber": "Ați atins limita de încărcare în lot de {{filesNumber}} fișiere.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Отмена",
|
||||
"stepOne.uploader.change": "Изменить",
|
||||
"stepOne.uploader.failed": "Ошибка загрузки",
|
||||
"stepOne.uploader.tip": "Поддерживаются {{supportTypes}}. Максимум {{batchCount}} файлов за раз, каждый до {{size}} МБ. Всего максимум {{totalCount}} файлов.",
|
||||
"stepOne.uploader.tip": "Поддерживаются {{supportTypes}}. Максимум {{batchCount}} файлов за раз, каждый до {{size}} МБ.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Поддерживаются {{supportTypes}}. Максимум {{batchCount}} файлов за раз, каждый до {{size}} МБ. Всего максимум {{totalCount}} файлов.",
|
||||
"stepOne.uploader.title": "Загрузить файл",
|
||||
"stepOne.uploader.validation.count": "Несколько файлов не поддерживаются",
|
||||
"stepOne.uploader.validation.filesNumber": "Вы достигли лимита пакетной загрузки {{filesNumber}} файлов.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Prekliči",
|
||||
"stepOne.uploader.change": "Zamenjaj",
|
||||
"stepOne.uploader.failed": "Nalaganje ni uspelo",
|
||||
"stepOne.uploader.tip": "Podpira {{supportTypes}}. Največje število datotek v seriji: {{batchCount}}, vsaka do {{size}} MB. Skupaj največ {{totalCount}} datotek.",
|
||||
"stepOne.uploader.tip": "Podpira {{supportTypes}}. Največje število datotek v seriji: {{batchCount}}, vsaka do {{size}} MB.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Podpira {{supportTypes}}. Največje število datotek v seriji: {{batchCount}}, vsaka do {{size}} MB. Skupaj največ {{totalCount}} datotek.",
|
||||
"stepOne.uploader.title": "Naloži datoteko",
|
||||
"stepOne.uploader.validation.count": "Podprta je le ena datoteka",
|
||||
"stepOne.uploader.validation.filesNumber": "Dosegli ste omejitev za pošiljanje {{filesNumber}} datotek.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "ยกเลิก",
|
||||
"stepOne.uploader.change": "เปลี่ยน",
|
||||
"stepOne.uploader.failed": "อัปโหลดล้มเหลว",
|
||||
"stepOne.uploader.tip": "รองรับ {{supportTypes}} สูงสุด {{batchCount}} ไฟล์ต่อชุดและ {{size}} MB แต่ละไฟล์ รวมสูงสุด {{totalCount}} ไฟล์",
|
||||
"stepOne.uploader.tip": "รองรับ {{supportTypes}} สูงสุด {{batchCount}} ไฟล์ต่อชุดและ {{size}} MB แต่ละไฟล์",
|
||||
"stepOne.uploader.tipWithTotalLimit": "รองรับ {{supportTypes}} สูงสุด {{batchCount}} ไฟล์ต่อชุดและ {{size}} MB แต่ละไฟล์ รวมสูงสุด {{totalCount}} ไฟล์",
|
||||
"stepOne.uploader.title": "อัปโหลดไฟล์",
|
||||
"stepOne.uploader.validation.count": "ไม่รองรับหลายไฟล์",
|
||||
"stepOne.uploader.validation.filesNumber": "คุณถึงขีดจํากัดการอัปโหลดเป็นชุดของ {{filesNumber}} แล้ว",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "İptal",
|
||||
"stepOne.uploader.change": "Değiştir",
|
||||
"stepOne.uploader.failed": "Yükleme başarısız",
|
||||
"stepOne.uploader.tip": "{{supportTypes}} destekler. Parti başına en fazla {{batchCount}} dosya ve her biri {{size}} MB. Toplam en fazla {{totalCount}} dosya.",
|
||||
"stepOne.uploader.tip": "{{supportTypes}} destekler. Parti başına en fazla {{batchCount}} dosya ve her biri {{size}} MB.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "{{supportTypes}} destekler. Parti başına en fazla {{batchCount}} dosya ve her biri {{size}} MB. Toplam en fazla {{totalCount}} dosya.",
|
||||
"stepOne.uploader.title": "Dosya yükle",
|
||||
"stepOne.uploader.validation.count": "Birden fazla dosya desteklenmiyor",
|
||||
"stepOne.uploader.validation.filesNumber": "Toplu yükleme sınırına ulaştınız, {{filesNumber}} dosya.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Скасувати",
|
||||
"stepOne.uploader.change": "Змінити",
|
||||
"stepOne.uploader.failed": "Завантаження не вдалося",
|
||||
"stepOne.uploader.tip": "Підтримуються {{supportTypes}}. Максимум {{batchCount}} файлів за раз, кожен до {{size}} МБ. Загалом максимум {{totalCount}} файлів.",
|
||||
"stepOne.uploader.tip": "Підтримуються {{supportTypes}}. Максимум {{batchCount}} файлів за раз, кожен до {{size}} МБ.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Підтримуються {{supportTypes}}. Максимум {{batchCount}} файлів за раз, кожен до {{size}} МБ. Загалом максимум {{totalCount}} файлів.",
|
||||
"stepOne.uploader.title": "Завантажити текстовий файл",
|
||||
"stepOne.uploader.validation.count": "Не підтримується завантаження кількох файлів",
|
||||
"stepOne.uploader.validation.filesNumber": "Ліміт масового завантаження {{filesNumber}}.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "Hủy",
|
||||
"stepOne.uploader.change": "Thay đổi",
|
||||
"stepOne.uploader.failed": "Tải lên thất bại",
|
||||
"stepOne.uploader.tip": "Hỗ trợ {{supportTypes}}. Tối đa {{batchCount}} tệp trong một lô và {{size}} MB mỗi tệp. Tổng tối đa {{totalCount}} tệp.",
|
||||
"stepOne.uploader.tip": "Hỗ trợ {{supportTypes}}. Tối đa {{batchCount}} tệp trong một lô và {{size}} MB mỗi tệp.",
|
||||
"stepOne.uploader.tipWithTotalLimit": "Hỗ trợ {{supportTypes}}. Tối đa {{batchCount}} tệp trong một lô và {{size}} MB mỗi tệp. Tổng tối đa {{totalCount}} tệp.",
|
||||
"stepOne.uploader.title": "Tải lên tệp văn bản",
|
||||
"stepOne.uploader.validation.count": "Không hỗ trợ tải lên nhiều tệp",
|
||||
"stepOne.uploader.validation.filesNumber": "Bạn đã đạt đến giới hạn tải lên lô của {{filesNumber}} tệp.",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "取消",
|
||||
"stepOne.uploader.change": "更改文件",
|
||||
"stepOne.uploader.failed": "上传失败",
|
||||
"stepOne.uploader.tip": "已支持 {{supportTypes}},每批最多 {{batchCount}} 个文件,每个文件不超过 {{size}} MB ,总数不超过 {{totalCount}} 个文件。",
|
||||
"stepOne.uploader.tip": "已支持 {{supportTypes}},每批最多 {{batchCount}} 个文件,每个文件不超过 {{size}} MB。",
|
||||
"stepOne.uploader.tipWithTotalLimit": "已支持 {{supportTypes}},每批最多 {{batchCount}} 个文件,每个文件不超过 {{size}} MB,总数不超过 {{totalCount}} 个文件。",
|
||||
"stepOne.uploader.title": "上传文本文件",
|
||||
"stepOne.uploader.validation.count": "暂不支持多个文件",
|
||||
"stepOne.uploader.validation.filesNumber": "批量上传限制 {{filesNumber}}。",
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
"stepOne.uploader.cancel": "取消",
|
||||
"stepOne.uploader.change": "更改檔案",
|
||||
"stepOne.uploader.failed": "上傳失敗",
|
||||
"stepOne.uploader.tip": "支援 {{supportTypes}}。每批最多 {{batchCount}} 個檔案,每個檔案不超過 {{size}} MB,總數不超過 {{totalCount}} 個檔案。",
|
||||
"stepOne.uploader.tip": "支援 {{supportTypes}}。每批最多 {{batchCount}} 個檔案,每個檔案不超過 {{size}} MB。",
|
||||
"stepOne.uploader.tipWithTotalLimit": "支援 {{supportTypes}}。每批最多 {{batchCount}} 個檔案,每個檔案不超過 {{size}} MB,總數不超過 {{totalCount}} 個檔案。",
|
||||
"stepOne.uploader.title": "上傳文字檔案",
|
||||
"stepOne.uploader.validation.count": "暫不支援多個檔案",
|
||||
"stepOne.uploader.validation.filesNumber": "批次上傳限制 {{filesNumber}}。",
|
||||
|
||||
@ -75,7 +75,7 @@ export type AppTokenCostsResponse = {
|
||||
|
||||
export type UpdateAppModelConfigResponse = { result: string }
|
||||
|
||||
export type ApiKeyItemResponse = {
|
||||
type ApiKeyItemResponse = {
|
||||
id: string
|
||||
token: string
|
||||
last_used_at: string
|
||||
|
||||
@ -56,7 +56,7 @@ export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_l
|
||||
role: 'owner' | 'admin' | 'editor' | 'normal' | 'dataset_operator'
|
||||
}
|
||||
|
||||
export enum ProviderName {
|
||||
enum ProviderName {
|
||||
OPENAI = 'openai',
|
||||
AZURE_OPENAI = 'azure_openai',
|
||||
ANTHROPIC = 'anthropic',
|
||||
|
||||
@ -227,7 +227,7 @@ export type QA = {
|
||||
answer: string
|
||||
}
|
||||
|
||||
export type IndexingEstimateResponse = {
|
||||
type IndexingEstimateResponse = {
|
||||
tokens: number
|
||||
total_price: number
|
||||
currency: string
|
||||
@ -279,7 +279,7 @@ export type Rules = {
|
||||
subchunk_segmentation: Segmentation
|
||||
}
|
||||
|
||||
export type Limits = {
|
||||
type Limits = {
|
||||
indexing_max_segmentation_tokens_length: number
|
||||
}
|
||||
|
||||
@ -288,24 +288,21 @@ export type PreProcessingRule = {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type Segmentation = {
|
||||
type Segmentation = {
|
||||
separator: string
|
||||
max_tokens: number
|
||||
chunk_overlap?: number
|
||||
}
|
||||
|
||||
export const DocumentIndexingStatusList = [
|
||||
'waiting',
|
||||
'parsing',
|
||||
'cleaning',
|
||||
'splitting',
|
||||
'indexing',
|
||||
'paused',
|
||||
'error',
|
||||
'completed',
|
||||
] as const
|
||||
|
||||
export type DocumentIndexingStatus = typeof DocumentIndexingStatusList[number]
|
||||
export type DocumentIndexingStatus
|
||||
= | 'waiting'
|
||||
| 'parsing'
|
||||
| 'cleaning'
|
||||
| 'splitting'
|
||||
| 'indexing'
|
||||
| 'paused'
|
||||
| 'error'
|
||||
| 'completed'
|
||||
|
||||
export const DisplayStatusList = [
|
||||
'queuing',
|
||||
@ -386,7 +383,7 @@ export type UploadFileIdInfo = {
|
||||
|
||||
export type DataSourceInfo = LegacyDataSourceInfo | LocalFileInfo | OnlineDocumentInfo | WebsiteCrawlInfo | UploadFileIdInfo
|
||||
|
||||
export type InitialDocumentDetail = {
|
||||
type InitialDocumentDetail = {
|
||||
id: string
|
||||
batch: string
|
||||
position: number
|
||||
@ -432,7 +429,7 @@ export type DocumentListResponse = {
|
||||
limit: number
|
||||
}
|
||||
|
||||
export type DocumentReq = {
|
||||
type DocumentReq = {
|
||||
original_document_id?: string
|
||||
indexing_technique?: IndexingType
|
||||
doc_form: ChunkingMode
|
||||
@ -452,7 +449,7 @@ export type IndexingEstimateParams = DocumentReq & Partial<DataSource> & {
|
||||
dataset_id: string
|
||||
}
|
||||
|
||||
export type DataSource = {
|
||||
type DataSource = {
|
||||
type: DataSourceType
|
||||
info_list: {
|
||||
data_source_type: DataSourceType
|
||||
@ -513,7 +510,7 @@ export type FullDocumentDetail = SimpleDocumentDetail & {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type DocMetadata = {
|
||||
type DocMetadata = {
|
||||
title: string
|
||||
language: string
|
||||
author: string
|
||||
@ -534,16 +531,13 @@ export const CUSTOMIZABLE_DOC_TYPES = [
|
||||
'im_chat_log',
|
||||
] as const
|
||||
|
||||
export const FIXED_DOC_TYPES = ['synced_from_github', 'synced_from_notion', 'wikipedia_entry'] as const
|
||||
|
||||
export type CustomizableDocType = typeof CUSTOMIZABLE_DOC_TYPES[number]
|
||||
export type FixedDocType = typeof FIXED_DOC_TYPES[number]
|
||||
type CustomizableDocType = typeof CUSTOMIZABLE_DOC_TYPES[number]
|
||||
type FixedDocType = 'synced_from_github' | 'synced_from_notion' | 'wikipedia_entry'
|
||||
export type DocType = CustomizableDocType | FixedDocType
|
||||
|
||||
export type DocumentDetailResponse = FullDocumentDetail
|
||||
|
||||
export const SEGMENT_STATUS_LIST = ['waiting', 'completed', 'error', 'indexing']
|
||||
export type SegmentStatus = typeof SEGMENT_STATUS_LIST[number]
|
||||
type SegmentStatus = 'waiting' | 'completed' | 'error' | 'indexing'
|
||||
|
||||
export type Attachment = {
|
||||
id: string
|
||||
@ -634,7 +628,7 @@ export type ExternalKnowledgeBaseHitTesting = {
|
||||
}
|
||||
}
|
||||
|
||||
export type Segment = {
|
||||
type Segment = {
|
||||
id: string
|
||||
document: Document
|
||||
content: string
|
||||
@ -648,7 +642,7 @@ export type Segment = {
|
||||
answer: string
|
||||
}
|
||||
|
||||
export type Document = {
|
||||
type Document = {
|
||||
id: string
|
||||
data_source_type: string
|
||||
name: string
|
||||
@ -663,7 +657,7 @@ export type HitTestingRecordsResponse = {
|
||||
page: number
|
||||
}
|
||||
|
||||
export type TsnePosition = {
|
||||
type TsnePosition = {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
@ -750,7 +744,7 @@ export const DEFAULT_WEIGHTED_SCORE = {
|
||||
},
|
||||
}
|
||||
|
||||
export type ChildChunkType = 'automatic' | 'customized'
|
||||
type ChildChunkType = 'automatic' | 'customized'
|
||||
|
||||
export type ChildChunkDetail = {
|
||||
id: string
|
||||
|
||||
@ -114,7 +114,7 @@ export type ModerationConfig = MoreLikeThisConfig & {
|
||||
} & Partial<Record<string, any>>
|
||||
}
|
||||
|
||||
export type RetrieverResourceConfig = MoreLikeThisConfig
|
||||
type RetrieverResourceConfig = MoreLikeThisConfig
|
||||
export type AgentConfig = {
|
||||
enabled: boolean
|
||||
strategy: AgentStrategy
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { AppIconType, AppModeEnum } from '@/types/app'
|
||||
|
||||
export type AppBasicInfo = {
|
||||
type AppBasicInfo = {
|
||||
id: string
|
||||
mode: AppModeEnum
|
||||
icon_type: AppIconType | null
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { VisionFile } from '@/types/app'
|
||||
|
||||
export type CompletionParamsType = {
|
||||
type CompletionParamsType = {
|
||||
max_tokens: number
|
||||
temperature: number
|
||||
top_p: number
|
||||
@ -15,13 +15,13 @@ export type CompletionParamsType = {
|
||||
frequency_penalty: number
|
||||
}
|
||||
|
||||
export type LogModelConfig = {
|
||||
type LogModelConfig = {
|
||||
name: string
|
||||
provider: string
|
||||
completion_params: CompletionParamsType
|
||||
}
|
||||
|
||||
export type ModelConfigDetail = {
|
||||
type ModelConfigDetail = {
|
||||
introduction: string
|
||||
prompt_template: string
|
||||
prompt_variables: Array<{
|
||||
@ -53,7 +53,7 @@ export type Annotation = {
|
||||
created_at?: number
|
||||
}
|
||||
|
||||
export type MessageContent = {
|
||||
type MessageContent = {
|
||||
id: string
|
||||
conversation_id: string
|
||||
query: string
|
||||
@ -186,8 +186,7 @@ export type ChatMessagesResponse = {
|
||||
limit: number
|
||||
}
|
||||
|
||||
export const MessageRatings = ['like', 'dislike', null] as const
|
||||
export type MessageRating = typeof MessageRatings[number]
|
||||
export type MessageRating = 'like' | 'dislike' | null
|
||||
|
||||
export type LogMessageFeedbacksRequest = {
|
||||
message_id: string
|
||||
@ -229,7 +228,7 @@ export type TriggerMetadata = {
|
||||
icon_dark?: string | null
|
||||
}
|
||||
|
||||
export type WorkflowLogDetails = {
|
||||
type WorkflowLogDetails = {
|
||||
trigger_metadata?: TriggerMetadata
|
||||
}
|
||||
|
||||
@ -246,12 +245,12 @@ export type WorkflowRunDetail = {
|
||||
total_steps: number
|
||||
finished_at: number
|
||||
}
|
||||
export type AccountInfo = {
|
||||
type AccountInfo = {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
export type EndUserInfo = {
|
||||
type EndUserInfo = {
|
||||
id: string
|
||||
type: 'browser' | 'service_api'
|
||||
is_anonymous: boolean
|
||||
@ -303,7 +302,7 @@ export type WorkflowRunDetailResponse = {
|
||||
exceptions_count?: number
|
||||
}
|
||||
|
||||
export type AgentLogMeta = {
|
||||
type AgentLogMeta = {
|
||||
status: string
|
||||
executor: string
|
||||
start_time: string
|
||||
@ -338,7 +337,7 @@ export type AgentIteration = {
|
||||
}
|
||||
}
|
||||
|
||||
export type AgentLogFile = {
|
||||
type AgentLogFile = {
|
||||
id: string
|
||||
type: string
|
||||
url: string
|
||||
@ -357,7 +356,7 @@ export type AgentLogDetailResponse = {
|
||||
files: AgentLogFile[]
|
||||
}
|
||||
|
||||
export type PauseType = {
|
||||
type PauseType = {
|
||||
type: 'human_input'
|
||||
form_id: string
|
||||
backstage_input_url: string
|
||||
@ -365,7 +364,7 @@ export type PauseType = {
|
||||
type: 'breakpoint'
|
||||
}
|
||||
|
||||
export type PauseDetail = {
|
||||
type PauseDetail = {
|
||||
node_id: string
|
||||
node_title: string
|
||||
pause_type: PauseType
|
||||
|
||||
@ -275,7 +275,7 @@ function generateTypeDefinitions(
|
||||
typeNames.push(typeName)
|
||||
|
||||
lines.push(`// ${sectionToTypeName(section)} paths`)
|
||||
lines.push(`export type ${typeName} =`)
|
||||
lines.push(`type ${typeName} =`)
|
||||
|
||||
for (const p of paths) {
|
||||
lines.push(` | '/${p}'`)
|
||||
@ -297,7 +297,7 @@ function generateTypeDefinitions(
|
||||
if (apiReferencePaths.length > 0) {
|
||||
const sortedPaths = [...apiReferencePaths].sort()
|
||||
lines.push('// API Reference paths (English, use apiReferencePathTranslations for other languages)')
|
||||
lines.push('export type ApiReferencePath =')
|
||||
lines.push('type ApiReferencePath =')
|
||||
for (const p of sortedPaths) {
|
||||
lines.push(` | '${p}'`)
|
||||
}
|
||||
@ -307,7 +307,7 @@ function generateTypeDefinitions(
|
||||
|
||||
// Generate base combined type
|
||||
lines.push('// Base path without language prefix')
|
||||
lines.push('export type DocPathWithoutLangBase =')
|
||||
lines.push('type DocPathWithoutLangBase =')
|
||||
for (const typeName of typeNames) {
|
||||
lines.push(` | ${typeName}`)
|
||||
}
|
||||
|
||||
@ -47,39 +47,39 @@ export type IOnDataMoreInfo = {
|
||||
}
|
||||
|
||||
export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
|
||||
export type IOnThought = (though: ThoughtItem) => void
|
||||
export type IOnFile = (file: VisionFile) => void
|
||||
export type IOnMessageEnd = (messageEnd: MessageEnd) => void
|
||||
type IOnThought = (though: ThoughtItem) => void
|
||||
type IOnFile = (file: VisionFile) => void
|
||||
type IOnMessageEnd = (messageEnd: MessageEnd) => void
|
||||
export type IOnMessageReplace = (messageReplace: MessageReplace) => void
|
||||
export type IOnCompleted = (hasError?: boolean, errorMessage?: string) => void
|
||||
export type IOnError = (msg: string, code?: string) => void
|
||||
|
||||
export type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void
|
||||
export type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void
|
||||
export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void
|
||||
export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void
|
||||
export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void
|
||||
export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void
|
||||
export type IOnNodeRetry = (nodeFinished: NodeFinishedResponse) => void
|
||||
export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void
|
||||
export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void
|
||||
export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void
|
||||
export type IOnTextChunk = (textChunk: TextChunkResponse) => void
|
||||
export type IOnTTSChunk = (messageId: string, audioStr: string, audioType?: string) => void
|
||||
export type IOnTTSEnd = (messageId: string, audioStr: string, audioType?: string) => void
|
||||
export type IOnTextReplace = (textReplace: TextReplaceResponse) => void
|
||||
export type IOnLoopStarted = (workflowStarted: LoopStartedResponse) => void
|
||||
export type IOnLoopNext = (workflowStarted: LoopNextResponse) => void
|
||||
export type IOnLoopFinished = (workflowFinished: LoopFinishedResponse) => void
|
||||
export type IOnAgentLog = (agentLog: AgentLogResponse) => void
|
||||
type IOnWorkflowStarted = (workflowStarted: WorkflowStartedResponse) => void
|
||||
type IOnWorkflowFinished = (workflowFinished: WorkflowFinishedResponse) => void
|
||||
type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void
|
||||
type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void
|
||||
type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void
|
||||
type IOnIterationNext = (workflowStarted: IterationNextResponse) => void
|
||||
type IOnNodeRetry = (nodeFinished: NodeFinishedResponse) => void
|
||||
type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void
|
||||
type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void
|
||||
type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void
|
||||
type IOnTextChunk = (textChunk: TextChunkResponse) => void
|
||||
type IOnTTSChunk = (messageId: string, audioStr: string, audioType?: string) => void
|
||||
type IOnTTSEnd = (messageId: string, audioStr: string, audioType?: string) => void
|
||||
type IOnTextReplace = (textReplace: TextReplaceResponse) => void
|
||||
type IOnLoopStarted = (workflowStarted: LoopStartedResponse) => void
|
||||
type IOnLoopNext = (workflowStarted: LoopNextResponse) => void
|
||||
type IOnLoopFinished = (workflowFinished: LoopFinishedResponse) => void
|
||||
type IOnAgentLog = (agentLog: AgentLogResponse) => void
|
||||
|
||||
export type IOHumanInputRequired = (humanInputRequired: HumanInputRequiredResponse) => void
|
||||
export type IOnHumanInputFormFilled = (humanInputFormFilled: HumanInputFormFilledResponse) => void
|
||||
export type IOnHumanInputFormTimeout = (humanInputFormTimeout: HumanInputFormTimeoutResponse) => void
|
||||
export type IOWorkflowPaused = (workflowPaused: WorkflowPausedResponse) => void
|
||||
export type IOnDataSourceNodeProcessing = (dataSourceNodeProcessing: DataSourceNodeProcessingResponse) => void
|
||||
export type IOnDataSourceNodeCompleted = (dataSourceNodeCompleted: DataSourceNodeCompletedResponse) => void
|
||||
export type IOnDataSourceNodeError = (dataSourceNodeError: DataSourceNodeErrorResponse) => void
|
||||
type IOHumanInputRequired = (humanInputRequired: HumanInputRequiredResponse) => void
|
||||
type IOnHumanInputFormFilled = (humanInputFormFilled: HumanInputFormFilledResponse) => void
|
||||
type IOnHumanInputFormTimeout = (humanInputFormTimeout: HumanInputFormTimeoutResponse) => void
|
||||
type IOWorkflowPaused = (workflowPaused: WorkflowPausedResponse) => void
|
||||
type IOnDataSourceNodeProcessing = (dataSourceNodeProcessing: DataSourceNodeProcessingResponse) => void
|
||||
type IOnDataSourceNodeCompleted = (dataSourceNodeCompleted: DataSourceNodeCompletedResponse) => void
|
||||
type IOnDataSourceNodeError = (dataSourceNodeError: DataSourceNodeErrorResponse) => void
|
||||
|
||||
export type IOtherOptions = {
|
||||
isPublicAPI?: boolean
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user