mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 08:58:09 +08:00
feat(workflow): add selection context menu helpers and integrate with context menu component (#34013)
Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: lif <1835304752@qq.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com> Co-authored-by: Desel72 <pedroluiscolmenares722@gmail.com> Co-authored-by: Renzo <170978465+RenzoMXD@users.noreply.github.com> Co-authored-by: Krishna Chaitanya <krishnabkc15@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,143 @@
|
||||
import type { FileUploadConfigResponse } from '@/models/common'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import {
|
||||
BoolArraySection,
|
||||
ErrorMessages,
|
||||
FileEditorSection,
|
||||
JsonEditorSection,
|
||||
TextEditorSection,
|
||||
} from '../value-content-sections'
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor', () => ({
|
||||
default: ({ schema, onUpdate }: { schema: string, onUpdate: (value: string) => void }) => (
|
||||
<textarea data-testid="schema-editor" value={schema} onChange={event => onUpdate(event.target.value)} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useParams: () => ({ token: '' }),
|
||||
}))
|
||||
|
||||
describe('value-content sections', () => {
|
||||
const createFileUploadConfig = (): FileUploadConfigResponse => ({
|
||||
batch_count_limit: 10,
|
||||
image_file_batch_limit: 10,
|
||||
single_chunk_attachment_limit: 10,
|
||||
attachment_image_file_size_limit: 2,
|
||||
file_size_limit: 15,
|
||||
file_upload_limit: 5,
|
||||
workflow_file_upload_limit: 5,
|
||||
})
|
||||
|
||||
const createVar = (overrides: Partial<VarInInspect>): VarInInspect => ({
|
||||
id: 'var-1',
|
||||
name: 'query',
|
||||
type: VarInInspectType.node,
|
||||
value_type: VarType.string,
|
||||
value: '',
|
||||
...overrides,
|
||||
} as VarInInspect)
|
||||
|
||||
it('should render the text editor section and forward text changes', () => {
|
||||
const handleTextChange = vi.fn()
|
||||
|
||||
render(
|
||||
<TextEditorSection
|
||||
currentVar={createVar({ value_type: VarType.string })}
|
||||
value="hello"
|
||||
textEditorDisabled={false}
|
||||
isTruncated={false}
|
||||
onTextChange={handleTextChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'updated' } })
|
||||
expect(handleTextChange).toHaveBeenCalledWith('updated')
|
||||
})
|
||||
|
||||
it('should render the textarea editor for non-string values', () => {
|
||||
const handleTextChange = vi.fn()
|
||||
|
||||
render(
|
||||
<TextEditorSection
|
||||
currentVar={createVar({ name: 'count', value_type: VarType.number })}
|
||||
value="12"
|
||||
textEditorDisabled={false}
|
||||
isTruncated={false}
|
||||
onTextChange={handleTextChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: '24' } })
|
||||
expect(handleTextChange).toHaveBeenCalledWith('24')
|
||||
})
|
||||
|
||||
it('should update a boolean array item by index', () => {
|
||||
const onChange = vi.fn()
|
||||
render(<BoolArraySection values={[true, false]} onChange={onChange} />)
|
||||
|
||||
fireEvent.click(screen.getAllByText('True')[1])
|
||||
expect(onChange).toHaveBeenCalledWith([true, true])
|
||||
})
|
||||
|
||||
it('should render schema editor and error messages', () => {
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<>
|
||||
<JsonEditorSection
|
||||
hasChunks={false}
|
||||
valueType={VarType.object}
|
||||
json="{}"
|
||||
readonly={false}
|
||||
isTruncated={false}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<ErrorMessages
|
||||
parseError={new Error('Broken JSON')}
|
||||
validationError="Too deep"
|
||||
/>
|
||||
</>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByTestId('schema-editor'), { target: { value: '{"foo":1}' } })
|
||||
expect(onChange).toHaveBeenCalledWith('{"foo":1}')
|
||||
expect(screen.getByText('Broken JSON')).toBeInTheDocument()
|
||||
expect(screen.getByText('Too deep')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render chunk preview when the json editor has chunks', () => {
|
||||
render(
|
||||
<JsonEditorSection
|
||||
hasChunks
|
||||
schemaType="general_structure"
|
||||
valueType={VarType.object}
|
||||
json="{}"
|
||||
readonly={false}
|
||||
isTruncated={false}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('schema-editor')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the file editor section', () => {
|
||||
render(
|
||||
<ToastContext.Provider value={{ notify: vi.fn(), close: vi.fn() }}>
|
||||
<FileEditorSection
|
||||
currentVar={createVar({ name: 'files', value_type: VarType.file })}
|
||||
fileValue={[]}
|
||||
fileUploadConfig={createFileUploadConfig()}
|
||||
textEditorDisabled={false}
|
||||
onChange={vi.fn()}
|
||||
/>
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
|
||||
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,48 @@
|
||||
describe('value-content helpers branch coverage', () => {
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return validation errors for invalid schemas, over-deep schemas, and draft7 violations', async () => {
|
||||
const validateSchemaAgainstDraft7 = vi.fn()
|
||||
const getValidationErrorMessage = vi.fn(() => 'draft7 error')
|
||||
|
||||
vi.doMock('@/app/components/workflow/nodes/llm/utils', () => ({
|
||||
checkJsonSchemaDepth: (schema: Record<string, unknown>) => schema.depth as number,
|
||||
getValidationErrorMessage,
|
||||
validateSchemaAgainstDraft7,
|
||||
}))
|
||||
|
||||
vi.doMock('../utils', () => ({
|
||||
validateJSONSchema: (schema: Record<string, unknown>) => {
|
||||
if (schema.kind === 'invalid')
|
||||
return { success: false, error: new Error('schema invalid') }
|
||||
return { success: true }
|
||||
},
|
||||
}))
|
||||
|
||||
const { validateInspectJsonValue } = await import('../value-content.helpers')
|
||||
|
||||
expect(validateInspectJsonValue('{"kind":"invalid"}', 'object')).toMatchObject({
|
||||
success: false,
|
||||
validationError: 'schema invalid',
|
||||
parseError: null,
|
||||
})
|
||||
|
||||
expect(validateInspectJsonValue('{"depth":99}', 'object')).toMatchObject({
|
||||
success: false,
|
||||
validationError: expect.stringContaining('Schema exceeds maximum depth'),
|
||||
parseError: null,
|
||||
})
|
||||
|
||||
validateSchemaAgainstDraft7.mockReturnValueOnce([{ message: 'broken' }])
|
||||
|
||||
expect(validateInspectJsonValue('{"depth":1}', 'object')).toMatchObject({
|
||||
success: false,
|
||||
validationError: 'draft7 error',
|
||||
parseError: null,
|
||||
})
|
||||
expect(getValidationErrorMessage).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,80 @@
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import {
|
||||
formatInspectFileValue,
|
||||
getValueEditorState,
|
||||
isFileValueUploaded,
|
||||
validateInspectJsonValue,
|
||||
} from '../value-content.helpers'
|
||||
|
||||
describe('value-content helpers', () => {
|
||||
const createVar = (overrides: Partial<VarInInspect>): VarInInspect => ({
|
||||
id: 'var-1',
|
||||
name: 'query',
|
||||
type: VarInInspectType.node,
|
||||
value_type: VarType.string,
|
||||
value: '',
|
||||
...overrides,
|
||||
} as VarInInspect)
|
||||
|
||||
it('should derive editor modes from the variable shape', () => {
|
||||
expect(getValueEditorState(createVar({
|
||||
type: VarInInspectType.environment,
|
||||
name: 'api_key',
|
||||
value_type: VarType.string,
|
||||
value: 'secret',
|
||||
}))).toMatchObject({
|
||||
showTextEditor: true,
|
||||
textEditorDisabled: true,
|
||||
showJSONEditor: false,
|
||||
})
|
||||
|
||||
expect(getValueEditorState(createVar({
|
||||
name: 'payload',
|
||||
value_type: VarType.object,
|
||||
value: { foo: 1 },
|
||||
schemaType: 'general_structure',
|
||||
}))).toMatchObject({
|
||||
showJSONEditor: true,
|
||||
hasChunks: true,
|
||||
})
|
||||
|
||||
expect(getValueEditorState(createVar({
|
||||
type: VarInInspectType.system,
|
||||
name: 'files',
|
||||
value_type: VarType.arrayFile,
|
||||
value: [],
|
||||
}))).toMatchObject({
|
||||
isSysFiles: true,
|
||||
showFileEditor: true,
|
||||
showJSONEditor: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should format file values and detect upload completion', () => {
|
||||
expect(formatInspectFileValue(createVar({
|
||||
name: 'file',
|
||||
value_type: VarType.file,
|
||||
value: { id: 'file-1' },
|
||||
}))).toHaveLength(1)
|
||||
|
||||
expect(isFileValueUploaded([{ upload_file_id: 'file-1' }])).toBe(true)
|
||||
expect(isFileValueUploaded([{ upload_file_id: '' }])).toBe(false)
|
||||
expect(formatInspectFileValue(createVar({
|
||||
type: VarInInspectType.system,
|
||||
name: 'files',
|
||||
value_type: VarType.arrayFile,
|
||||
value: [{ id: 'file-2' }],
|
||||
}))).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should validate json input and surface parse errors', () => {
|
||||
expect(validateInspectJsonValue('{"foo":1}', 'object').success).toBe(true)
|
||||
expect(validateInspectJsonValue('[]', 'array[any]')).toMatchObject({ success: true })
|
||||
expect(validateInspectJsonValue('{', 'object')).toMatchObject({
|
||||
success: false,
|
||||
parseError: expect.any(Error),
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,410 @@
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import ValueContent from '../value-content'
|
||||
|
||||
vi.mock('@/app/components/base/file-uploader/utils', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/base/file-uploader/utils')>()
|
||||
return {
|
||||
...actual,
|
||||
getProcessedFiles: (files: unknown[]) => files,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor', () => ({
|
||||
default: ({ schema, onUpdate }: { schema: string, onUpdate: (value: string) => void }) => (
|
||||
<textarea data-testid="json-editor" value={schema} onChange={event => onUpdate(event.target.value)} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../value-content-sections', () => ({
|
||||
TextEditorSection: ({
|
||||
value,
|
||||
onTextChange,
|
||||
}: {
|
||||
value: string
|
||||
onTextChange: (value: string) => void
|
||||
}) => <textarea aria-label="value-text-editor" value={value ?? ''} onChange={event => onTextChange(event.target.value)} />,
|
||||
BoolArraySection: ({
|
||||
onChange,
|
||||
}: {
|
||||
onChange: (value: boolean[]) => void
|
||||
}) => <button onClick={() => onChange([true, true])}>bool-array-editor</button>,
|
||||
JsonEditorSection: ({
|
||||
json,
|
||||
onChange,
|
||||
}: {
|
||||
json: string
|
||||
onChange: (value: string) => void
|
||||
}) => <textarea data-testid="json-editor" value={json} onChange={event => onChange(event.target.value)} />,
|
||||
FileEditorSection: ({
|
||||
onChange,
|
||||
}: {
|
||||
onChange: (files: Array<Record<string, unknown>>) => void
|
||||
}) => (
|
||||
<div>
|
||||
<button onClick={() => onChange([{ upload_file_id: '' }])}>file-pending</button>
|
||||
<button onClick={() => onChange([{ upload_file_id: 'file-1', name: 'report.pdf' }])}>file-uploaded</button>
|
||||
<button onClick={() => onChange([
|
||||
{ upload_file_id: 'file-1', name: 'a.pdf' },
|
||||
{ upload_file_id: 'file-2', name: 'b.pdf' },
|
||||
])}
|
||||
>
|
||||
file-array-uploaded
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
ErrorMessages: ({
|
||||
parseError,
|
||||
validationError,
|
||||
}: {
|
||||
parseError: Error | null
|
||||
validationError: string
|
||||
}) => (
|
||||
<div>
|
||||
{parseError && <div>{parseError.message}</div>}
|
||||
{validationError && <div>{validationError}</div>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useParams: () => ({ token: '' }),
|
||||
}))
|
||||
|
||||
describe('ValueContent', () => {
|
||||
const createVar = (overrides: Partial<VarInInspect>): VarInInspect => ({
|
||||
id: 'var-default',
|
||||
name: 'query',
|
||||
type: VarInInspectType.node,
|
||||
value_type: VarType.string,
|
||||
value: '',
|
||||
...overrides,
|
||||
} as VarInInspect)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should debounce text changes for string variables', async () => {
|
||||
const handleValueChange = vi.fn()
|
||||
|
||||
renderWorkflowComponent(
|
||||
<ValueContent
|
||||
currentVar={createVar({
|
||||
id: 'var-1',
|
||||
value_type: VarType.string,
|
||||
value: 'hello',
|
||||
})}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated={false}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
fileUploadConfig: {
|
||||
workflow_file_upload_limit: 5,
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByLabelText('value-text-editor'), { target: { value: 'updated' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleValueChange).toHaveBeenCalledWith('var-1', 'updated')
|
||||
})
|
||||
})
|
||||
|
||||
it('should surface parse errors from invalid json input', async () => {
|
||||
renderWorkflowComponent(
|
||||
<ValueContent
|
||||
currentVar={createVar({
|
||||
id: 'var-2',
|
||||
name: 'payload',
|
||||
value_type: VarType.object,
|
||||
value: { foo: 1 },
|
||||
})}
|
||||
handleValueChange={vi.fn()}
|
||||
isTruncated={false}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
fileUploadConfig: {
|
||||
workflow_file_upload_limit: 5,
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByTestId('json-editor'), { target: { value: '{' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/json/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should debounce numeric changes', async () => {
|
||||
const handleValueChange = vi.fn()
|
||||
|
||||
renderWorkflowComponent(
|
||||
<ValueContent
|
||||
currentVar={createVar({
|
||||
id: 'var-3',
|
||||
name: 'count',
|
||||
value_type: VarType.number,
|
||||
value: 1,
|
||||
})}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated={false}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
fileUploadConfig: {
|
||||
workflow_file_upload_limit: 5,
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByLabelText('value-text-editor'), { target: { value: '24.5' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleValueChange).toHaveBeenCalledWith('var-3', 24.5)
|
||||
})
|
||||
expect(handleValueChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should update boolean values', async () => {
|
||||
const handleValueChange = vi.fn()
|
||||
|
||||
renderWorkflowComponent(
|
||||
<ValueContent
|
||||
currentVar={createVar({
|
||||
id: 'var-4',
|
||||
name: 'enabled',
|
||||
value_type: VarType.boolean,
|
||||
value: false,
|
||||
})}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated={false}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
fileUploadConfig: {
|
||||
workflow_file_upload_limit: 5,
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('True'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleValueChange).toHaveBeenCalledWith('var-4', true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not emit changes when the content is truncated', async () => {
|
||||
const handleValueChange = vi.fn()
|
||||
|
||||
renderWorkflowComponent(
|
||||
<ValueContent
|
||||
currentVar={createVar({
|
||||
id: 'var-5',
|
||||
value_type: VarType.string,
|
||||
value: 'hello',
|
||||
})}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
fileUploadConfig: {
|
||||
workflow_file_upload_limit: 5,
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByLabelText('value-text-editor'), { target: { value: 'updated' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleValueChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update boolean array values', async () => {
|
||||
const handleValueChange = vi.fn()
|
||||
|
||||
renderWorkflowComponent(
|
||||
<ValueContent
|
||||
currentVar={createVar({
|
||||
id: 'var-6',
|
||||
name: 'flags',
|
||||
value_type: VarType.arrayBoolean,
|
||||
value: [true, false],
|
||||
})}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated={false}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
fileUploadConfig: {
|
||||
workflow_file_upload_limit: 5,
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('bool-array-editor'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleValueChange).toHaveBeenCalledWith('var-6', [true, true])
|
||||
})
|
||||
})
|
||||
|
||||
it('should parse valid json values', async () => {
|
||||
const handleValueChange = vi.fn()
|
||||
|
||||
renderWorkflowComponent(
|
||||
<ValueContent
|
||||
currentVar={createVar({
|
||||
id: 'var-7',
|
||||
name: 'payload',
|
||||
value_type: VarType.object,
|
||||
value: { foo: 1 },
|
||||
})}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated={false}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
fileUploadConfig: {
|
||||
workflow_file_upload_limit: 5,
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByTestId('json-editor'), { target: { value: '{"foo":2}' } })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleValueChange).toHaveBeenCalledWith('var-7', { foo: 2 })
|
||||
})
|
||||
})
|
||||
|
||||
it('should update uploaded single file values and ignore pending uploads', async () => {
|
||||
const handleValueChange = vi.fn()
|
||||
|
||||
renderWorkflowComponent(
|
||||
<ValueContent
|
||||
currentVar={createVar({
|
||||
id: 'var-8',
|
||||
name: 'files',
|
||||
value_type: VarType.file,
|
||||
value: null,
|
||||
})}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated={false}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
fileUploadConfig: {
|
||||
workflow_file_upload_limit: 5,
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('file-pending'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleValueChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('file-uploaded'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleValueChange).toHaveBeenCalledWith('var-8', expect.objectContaining({ upload_file_id: 'file-1' }))
|
||||
})
|
||||
})
|
||||
|
||||
it('should update uploaded file arrays and react to resize observer changes', async () => {
|
||||
const handleValueChange = vi.fn()
|
||||
const observe = vi.fn()
|
||||
const disconnect = vi.fn()
|
||||
const originalResizeObserver = globalThis.ResizeObserver
|
||||
const originalClientHeight = Object.getOwnPropertyDescriptor(HTMLDivElement.prototype, 'clientHeight')
|
||||
|
||||
Object.defineProperty(HTMLDivElement.prototype, 'clientHeight', {
|
||||
configurable: true,
|
||||
get: () => 120,
|
||||
})
|
||||
|
||||
class MockResizeObserver {
|
||||
callback: ResizeObserverCallback
|
||||
|
||||
constructor(callback: ResizeObserverCallback) {
|
||||
this.callback = callback
|
||||
}
|
||||
|
||||
observe = (target: Element) => {
|
||||
observe(target)
|
||||
this.callback([{
|
||||
borderBoxSize: [{ inlineSize: 20 }],
|
||||
} as unknown as ResizeObserverEntry], this as unknown as ResizeObserver)
|
||||
}
|
||||
|
||||
disconnect = disconnect
|
||||
}
|
||||
|
||||
vi.stubGlobal('ResizeObserver', MockResizeObserver as unknown as typeof ResizeObserver)
|
||||
|
||||
renderWorkflowComponent(
|
||||
<ValueContent
|
||||
currentVar={createVar({
|
||||
id: 'var-9',
|
||||
name: 'files',
|
||||
type: VarInInspectType.system,
|
||||
value_type: VarType.arrayFile,
|
||||
value: [],
|
||||
})}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated={false}
|
||||
/>,
|
||||
{
|
||||
initialStoreState: {
|
||||
fileUploadConfig: {
|
||||
workflow_file_upload_limit: 5,
|
||||
} as never,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('file-array-uploaded'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleValueChange).toHaveBeenCalledWith('var-9', expect.arrayContaining([
|
||||
expect.objectContaining({ upload_file_id: 'file-1' }),
|
||||
expect.objectContaining({ upload_file_id: 'file-2' }),
|
||||
]))
|
||||
})
|
||||
|
||||
expect(observe).toHaveBeenCalled()
|
||||
expect(document.querySelector('[style="height: 100px;"]')).toBeInTheDocument()
|
||||
|
||||
if (originalClientHeight)
|
||||
Object.defineProperty(HTMLDivElement.prototype, 'clientHeight', originalClientHeight)
|
||||
else
|
||||
delete (HTMLDivElement.prototype as { clientHeight?: number }).clientHeight
|
||||
|
||||
if (originalResizeObserver)
|
||||
vi.stubGlobal('ResizeObserver', originalResizeObserver)
|
||||
else
|
||||
vi.unstubAllGlobals()
|
||||
|
||||
expect(disconnect).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,190 @@
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { FileUploadConfigResponse } from '@/models/common'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message'
|
||||
import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { PreviewMode } from '../../base/features/types'
|
||||
import BoolValue from '../panel/chat-variable-panel/components/bool-value'
|
||||
import DisplayContent from './display-content'
|
||||
import LargeDataAlert from './large-data-alert'
|
||||
import { PreviewType } from './types'
|
||||
|
||||
type TextEditorSectionProps = {
|
||||
currentVar: VarInInspect
|
||||
value: unknown
|
||||
textEditorDisabled: boolean
|
||||
isTruncated: boolean
|
||||
onTextChange: (value: string) => void
|
||||
}
|
||||
|
||||
export const TextEditorSection = ({
|
||||
currentVar,
|
||||
value,
|
||||
textEditorDisabled,
|
||||
isTruncated,
|
||||
onTextChange,
|
||||
}: TextEditorSectionProps) => {
|
||||
return (
|
||||
<>
|
||||
{isTruncated && <LargeDataAlert className="absolute left-3 right-3 top-1" />}
|
||||
{currentVar.value_type === 'string'
|
||||
? (
|
||||
<DisplayContent
|
||||
previewType={PreviewType.Markdown}
|
||||
varType={currentVar.value_type}
|
||||
mdString={typeof value === 'string' ? value : String(value ?? '')}
|
||||
readonly={textEditorDisabled}
|
||||
handleTextChange={onTextChange}
|
||||
className={cn(isTruncated && 'pt-[36px]')}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Textarea
|
||||
readOnly={textEditorDisabled}
|
||||
disabled={textEditorDisabled || isTruncated}
|
||||
className={cn('h-full', isTruncated && 'pt-[48px]')}
|
||||
value={typeof value === 'number' ? value : String(value ?? '')}
|
||||
onChange={e => onTextChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type BoolArraySectionProps = {
|
||||
values: boolean[]
|
||||
onChange: (nextValue: boolean[]) => void
|
||||
}
|
||||
|
||||
export const BoolArraySection = ({
|
||||
values,
|
||||
onChange,
|
||||
}: BoolArraySectionProps) => {
|
||||
return (
|
||||
<div className="w-[295px] space-y-1">
|
||||
{values.map((value, index) => (
|
||||
<BoolValue
|
||||
key={`${index}-${String(value)}`}
|
||||
value={value}
|
||||
onChange={(newValue) => {
|
||||
const nextValue = [...values]
|
||||
nextValue[index] = newValue
|
||||
onChange(nextValue)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type JsonEditorSectionProps = {
|
||||
hasChunks: boolean
|
||||
schemaType?: string
|
||||
valueType: VarInInspect['value_type']
|
||||
json: string
|
||||
readonly: boolean
|
||||
isTruncated: boolean
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export const JsonEditorSection = ({
|
||||
hasChunks,
|
||||
schemaType,
|
||||
valueType,
|
||||
json,
|
||||
readonly,
|
||||
isTruncated,
|
||||
onChange,
|
||||
}: JsonEditorSectionProps) => {
|
||||
if (hasChunks) {
|
||||
return (
|
||||
<DisplayContent
|
||||
previewType={PreviewType.Chunks}
|
||||
varType={valueType}
|
||||
schemaType={schemaType ?? ''}
|
||||
jsonString={json ?? '{}'}
|
||||
readonly={readonly}
|
||||
handleEditorChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SchemaEditor
|
||||
readonly={readonly || isTruncated}
|
||||
className="overflow-y-auto"
|
||||
hideTopMenu
|
||||
schema={json}
|
||||
onUpdate={onChange}
|
||||
isTruncated={isTruncated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type FileEditorSectionProps = {
|
||||
currentVar: VarInInspect
|
||||
fileValue: FileEntity[]
|
||||
fileUploadConfig?: FileUploadConfigResponse
|
||||
textEditorDisabled: boolean
|
||||
onChange: (files: FileEntity[]) => void
|
||||
}
|
||||
|
||||
export const FileEditorSection = ({
|
||||
currentVar,
|
||||
fileValue,
|
||||
fileUploadConfig,
|
||||
textEditorDisabled,
|
||||
onChange,
|
||||
}: FileEditorSectionProps) => {
|
||||
return (
|
||||
<div className="max-w-[460px]">
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={fileValue}
|
||||
onChange={onChange}
|
||||
fileConfig={{
|
||||
allowed_file_types: [
|
||||
SupportUploadFileTypes.image,
|
||||
SupportUploadFileTypes.document,
|
||||
SupportUploadFileTypes.audio,
|
||||
SupportUploadFileTypes.video,
|
||||
],
|
||||
allowed_file_extensions: [
|
||||
...FILE_EXTS[SupportUploadFileTypes.image],
|
||||
...FILE_EXTS[SupportUploadFileTypes.document],
|
||||
...FILE_EXTS[SupportUploadFileTypes.audio],
|
||||
...FILE_EXTS[SupportUploadFileTypes.video],
|
||||
],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
number_limits: currentVar.value_type === 'file' ? 1 : fileUploadConfig?.workflow_file_upload_limit || 5,
|
||||
fileUploadConfig,
|
||||
preview_config: {
|
||||
mode: PreviewMode.NewPage,
|
||||
file_type_list: ['application/pdf'],
|
||||
},
|
||||
}}
|
||||
isDisabled={textEditorDisabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ErrorMessages = ({
|
||||
parseError,
|
||||
validationError,
|
||||
}: {
|
||||
parseError: Error | null
|
||||
validationError: string
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{parseError && <ErrorMessage className="mt-1" message={parseError.message} />}
|
||||
{validationError && <ErrorMessage className="mt-1" message={validationError} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import {
|
||||
checkJsonSchemaDepth,
|
||||
getValidationErrorMessage,
|
||||
validateSchemaAgainstDraft7,
|
||||
} from '@/app/components/workflow/nodes/llm/utils'
|
||||
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import { CHUNK_SCHEMA_TYPES } from './types'
|
||||
import { validateJSONSchema } from './utils'
|
||||
|
||||
type UploadedFileLike = {
|
||||
upload_file_id?: string
|
||||
}
|
||||
|
||||
export const getValueEditorState = (currentVar: VarInInspect) => {
|
||||
const showTextEditor = currentVar.value_type === 'secret' || currentVar.value_type === 'string' || currentVar.value_type === 'number'
|
||||
const showBoolEditor = typeof currentVar.value === 'boolean'
|
||||
const showBoolArrayEditor = Array.isArray(currentVar.value) && currentVar.value.every(v => typeof v === 'boolean')
|
||||
const isSysFiles = currentVar.type === VarInInspectType.system && currentVar.name === 'files'
|
||||
const showJSONEditor = !isSysFiles && ['object', 'array[string]', 'array[number]', 'array[object]', 'array[any]'].includes(currentVar.value_type)
|
||||
const showFileEditor = isSysFiles || currentVar.value_type === 'file' || currentVar.value_type === 'array[file]'
|
||||
const textEditorDisabled = currentVar.type === VarInInspectType.environment || (currentVar.type === VarInInspectType.system && currentVar.name !== 'query' && currentVar.name !== 'files')
|
||||
const JSONEditorDisabled = currentVar.value_type === 'array[any]'
|
||||
const hasChunks = !!currentVar.schemaType && CHUNK_SCHEMA_TYPES.includes(currentVar.schemaType)
|
||||
|
||||
return {
|
||||
showTextEditor,
|
||||
showBoolEditor,
|
||||
showBoolArrayEditor,
|
||||
isSysFiles,
|
||||
showJSONEditor,
|
||||
showFileEditor,
|
||||
textEditorDisabled,
|
||||
JSONEditorDisabled,
|
||||
hasChunks,
|
||||
}
|
||||
}
|
||||
|
||||
export const formatInspectFileValue = (currentVar: VarInInspect) => {
|
||||
if (currentVar.value_type === 'file')
|
||||
return currentVar.value ? getProcessedFilesFromResponse([currentVar.value]) : []
|
||||
if (currentVar.value_type === 'array[file]' || (currentVar.type === VarInInspectType.system && currentVar.name === 'files'))
|
||||
return currentVar.value && currentVar.value.length > 0 ? getProcessedFilesFromResponse(currentVar.value) : []
|
||||
return []
|
||||
}
|
||||
|
||||
export const validateInspectJsonValue = (value: string, type: string) => {
|
||||
try {
|
||||
const newJSONSchema = JSON.parse(value)
|
||||
const result = validateJSONSchema(newJSONSchema, type)
|
||||
if (!result.success)
|
||||
return { success: false, validationError: result.error.message, parseError: null }
|
||||
|
||||
if (type === 'object' || type === 'array[object]') {
|
||||
const schemaDepth = checkJsonSchemaDepth(newJSONSchema)
|
||||
if (schemaDepth > JSON_SCHEMA_MAX_DEPTH)
|
||||
return { success: false, validationError: `Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`, parseError: null }
|
||||
|
||||
const validationErrors = validateSchemaAgainstDraft7(newJSONSchema)
|
||||
if (validationErrors.length > 0)
|
||||
return { success: false, validationError: getValidationErrorMessage(validationErrors), parseError: null }
|
||||
}
|
||||
|
||||
return { success: true, validationError: '', parseError: null }
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
validationError: '',
|
||||
parseError: error instanceof Error ? error : new Error('Invalid JSON'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isFileValueUploaded = (fileList: UploadedFileLike[]) => fileList.every(file => file.upload_file_id)
|
||||
@ -2,31 +2,23 @@ import type { VarInInspect } from '@/types/workflow'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message'
|
||||
import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
|
||||
import {
|
||||
checkJsonSchemaDepth,
|
||||
getValidationErrorMessage,
|
||||
validateSchemaAgainstDraft7,
|
||||
} from '@/app/components/workflow/nodes/llm/utils'
|
||||
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import {
|
||||
validateJSONSchema,
|
||||
} from '@/app/components/workflow/variable-inspect/utils'
|
||||
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { PreviewMode } from '../../base/features/types'
|
||||
import BoolValue from '../panel/chat-variable-panel/components/bool-value'
|
||||
import DisplayContent from './display-content'
|
||||
import LargeDataAlert from './large-data-alert'
|
||||
import { CHUNK_SCHEMA_TYPES, PreviewType } from './types'
|
||||
import {
|
||||
BoolArraySection,
|
||||
ErrorMessages,
|
||||
FileEditorSection,
|
||||
JsonEditorSection,
|
||||
TextEditorSection,
|
||||
} from './value-content-sections'
|
||||
import {
|
||||
formatInspectFileValue,
|
||||
getValueEditorState,
|
||||
isFileValueUploaded,
|
||||
validateInspectJsonValue,
|
||||
} from './value-content.helpers'
|
||||
|
||||
type Props = {
|
||||
currentVar: VarInInspect
|
||||
@ -42,35 +34,24 @@ const ValueContent = ({
|
||||
const contentContainerRef = useRef<HTMLDivElement>(null)
|
||||
const errorMessageRef = useRef<HTMLDivElement>(null)
|
||||
const [editorHeight, setEditorHeight] = useState(0)
|
||||
const showTextEditor = currentVar.value_type === 'secret' || currentVar.value_type === 'string' || currentVar.value_type === 'number'
|
||||
const showBoolEditor = typeof currentVar.value === 'boolean'
|
||||
const showBoolArrayEditor = Array.isArray(currentVar.value) && currentVar.value.every(v => typeof v === 'boolean')
|
||||
const isSysFiles = currentVar.type === VarInInspectType.system && currentVar.name === 'files'
|
||||
const showJSONEditor = !isSysFiles && (currentVar.value_type === 'object' || currentVar.value_type === 'array[string]' || currentVar.value_type === 'array[number]' || currentVar.value_type === 'array[object]' || currentVar.value_type === 'array[any]')
|
||||
const showFileEditor = isSysFiles || currentVar.value_type === 'file' || currentVar.value_type === 'array[file]'
|
||||
const textEditorDisabled = currentVar.type === VarInInspectType.environment || (currentVar.type === VarInInspectType.system && currentVar.name !== 'query' && currentVar.name !== 'files')
|
||||
const JSONEditorDisabled = currentVar.value_type === 'array[any]'
|
||||
const {
|
||||
showTextEditor,
|
||||
showBoolEditor,
|
||||
showBoolArrayEditor,
|
||||
isSysFiles,
|
||||
showJSONEditor,
|
||||
showFileEditor,
|
||||
textEditorDisabled,
|
||||
JSONEditorDisabled,
|
||||
hasChunks,
|
||||
} = useMemo(() => getValueEditorState(currentVar), [currentVar])
|
||||
const fileUploadConfig = useStore(s => s.fileUploadConfig)
|
||||
|
||||
const hasChunks = useMemo(() => {
|
||||
if (!currentVar.schemaType)
|
||||
return false
|
||||
return CHUNK_SCHEMA_TYPES.includes(currentVar.schemaType)
|
||||
}, [currentVar.schemaType])
|
||||
|
||||
const formatFileValue = (value: VarInInspect) => {
|
||||
if (value.value_type === 'file')
|
||||
return value.value ? getProcessedFilesFromResponse([value.value]) : []
|
||||
if (value.value_type === 'array[file]' || (value.type === VarInInspectType.system && currentVar.name === 'files'))
|
||||
return value.value && value.value.length > 0 ? getProcessedFilesFromResponse(value.value) : []
|
||||
return []
|
||||
}
|
||||
|
||||
const [value, setValue] = useState<any>()
|
||||
const [json, setJson] = useState('')
|
||||
const [parseError, setParseError] = useState<Error | null>(null)
|
||||
const [validationError, setValidationError] = useState<string>('')
|
||||
const [fileValue, setFileValue] = useState<any>(() => formatFileValue(currentVar))
|
||||
const [fileValue, setFileValue] = useState<any>(() => formatInspectFileValue(currentVar))
|
||||
|
||||
const { run: debounceValueChange } = useDebounceFn(handleValueChange, { wait: 500 })
|
||||
|
||||
@ -87,7 +68,7 @@ const ValueContent = ({
|
||||
setJson(currentVar.value != null ? JSON.stringify(currentVar.value, null, 2) : '')
|
||||
|
||||
if (showFileEditor)
|
||||
setFileValue(formatFileValue(currentVar))
|
||||
setFileValue(formatInspectFileValue(currentVar))
|
||||
}, [currentVar.id, currentVar.value])
|
||||
|
||||
const handleTextChange = (value: string) => {
|
||||
@ -105,40 +86,10 @@ const ValueContent = ({
|
||||
}
|
||||
|
||||
const jsonValueValidate = (value: string, type: string) => {
|
||||
try {
|
||||
const newJSONSchema = JSON.parse(value)
|
||||
setParseError(null)
|
||||
const result = validateJSONSchema(newJSONSchema, type)
|
||||
if (!result.success) {
|
||||
setValidationError(result.error.message)
|
||||
return false
|
||||
}
|
||||
if (type === 'object' || type === 'array[object]') {
|
||||
const schemaDepth = checkJsonSchemaDepth(newJSONSchema)
|
||||
if (schemaDepth > JSON_SCHEMA_MAX_DEPTH) {
|
||||
setValidationError(`Schema exceeds maximum depth of ${JSON_SCHEMA_MAX_DEPTH}.`)
|
||||
return false
|
||||
}
|
||||
const validationErrors = validateSchemaAgainstDraft7(newJSONSchema)
|
||||
if (validationErrors.length > 0) {
|
||||
setValidationError(getValidationErrorMessage(validationErrors))
|
||||
return false
|
||||
}
|
||||
}
|
||||
setValidationError('')
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
setValidationError('')
|
||||
if (error instanceof Error) {
|
||||
setParseError(error)
|
||||
return false
|
||||
}
|
||||
else {
|
||||
setParseError(new Error('Invalid JSON'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
const result = validateInspectJsonValue(value, type)
|
||||
setParseError(result.parseError)
|
||||
setValidationError(result.validationError)
|
||||
return result.success
|
||||
}
|
||||
|
||||
const handleEditorChange = (value: string) => {
|
||||
@ -151,13 +102,11 @@ const ValueContent = ({
|
||||
}
|
||||
}
|
||||
|
||||
const fileValueValidate = (fileList: any[]) => fileList.every(file => file.upload_file_id)
|
||||
|
||||
const handleFileChange = (value: any[]) => {
|
||||
setFileValue(value)
|
||||
// check every file upload progress
|
||||
// invoke update api after every file uploaded
|
||||
if (!fileValueValidate(value))
|
||||
if (!isFileValueUploaded(value))
|
||||
return
|
||||
if (currentVar.value_type === 'file')
|
||||
debounceValueChange(currentVar.id, value[0])
|
||||
@ -189,31 +138,13 @@ const ValueContent = ({
|
||||
>
|
||||
<div className={cn('relative grow')} style={{ height: `${editorHeight}px` }}>
|
||||
{showTextEditor && (
|
||||
<>
|
||||
{isTruncated && <LargeDataAlert className="absolute left-3 right-3 top-1" />}
|
||||
{
|
||||
currentVar.value_type === 'string'
|
||||
? (
|
||||
<DisplayContent
|
||||
previewType={PreviewType.Markdown}
|
||||
varType={currentVar.value_type}
|
||||
mdString={value as any}
|
||||
readonly={textEditorDisabled}
|
||||
handleTextChange={handleTextChange}
|
||||
className={cn(isTruncated && 'pt-[36px]')}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Textarea
|
||||
readOnly={textEditorDisabled}
|
||||
disabled={textEditorDisabled || isTruncated}
|
||||
className={cn('h-full', isTruncated && 'pt-[48px]')}
|
||||
value={value as any}
|
||||
onChange={e => handleTextChange(e.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
<TextEditorSection
|
||||
currentVar={currentVar}
|
||||
value={value}
|
||||
textEditorDisabled={textEditorDisabled}
|
||||
isTruncated={isTruncated}
|
||||
onTextChange={handleTextChange}
|
||||
/>
|
||||
)}
|
||||
{showBoolEditor && (
|
||||
<div className="w-[295px]">
|
||||
@ -228,79 +159,41 @@ const ValueContent = ({
|
||||
)}
|
||||
{
|
||||
showBoolArrayEditor && (
|
||||
<div className="w-[295px] space-y-1">
|
||||
{currentVar.value.map((v: boolean, i: number) => (
|
||||
<BoolValue
|
||||
key={i}
|
||||
value={v}
|
||||
onChange={(newValue) => {
|
||||
const newArray = [...(currentVar.value as boolean[])]
|
||||
newArray[i] = newValue
|
||||
setValue(newArray)
|
||||
debounceValueChange(currentVar.id, newArray)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<BoolArraySection
|
||||
values={currentVar.value as boolean[]}
|
||||
onChange={(newArray) => {
|
||||
setValue(newArray)
|
||||
debounceValueChange(currentVar.id, newArray)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{showJSONEditor && (
|
||||
hasChunks
|
||||
? (
|
||||
<DisplayContent
|
||||
previewType={PreviewType.Chunks}
|
||||
varType={currentVar.value_type}
|
||||
schemaType={currentVar.schemaType ?? ''}
|
||||
jsonString={json ?? '{}'}
|
||||
readonly={JSONEditorDisabled}
|
||||
handleEditorChange={handleEditorChange}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<SchemaEditor
|
||||
readonly={JSONEditorDisabled || isTruncated}
|
||||
className="overflow-y-auto"
|
||||
hideTopMenu
|
||||
schema={json}
|
||||
onUpdate={handleEditorChange}
|
||||
isTruncated={isTruncated}
|
||||
/>
|
||||
)
|
||||
<JsonEditorSection
|
||||
hasChunks={hasChunks}
|
||||
schemaType={currentVar.schemaType}
|
||||
valueType={currentVar.value_type}
|
||||
json={json}
|
||||
readonly={JSONEditorDisabled}
|
||||
isTruncated={isTruncated}
|
||||
onChange={handleEditorChange}
|
||||
/>
|
||||
)}
|
||||
{showFileEditor && (
|
||||
<div className="max-w-[460px]">
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={fileValue}
|
||||
onChange={files => handleFileChange(getProcessedFiles(files))}
|
||||
fileConfig={{
|
||||
allowed_file_types: [
|
||||
SupportUploadFileTypes.image,
|
||||
SupportUploadFileTypes.document,
|
||||
SupportUploadFileTypes.audio,
|
||||
SupportUploadFileTypes.video,
|
||||
],
|
||||
allowed_file_extensions: [
|
||||
...FILE_EXTS[SupportUploadFileTypes.image],
|
||||
...FILE_EXTS[SupportUploadFileTypes.document],
|
||||
...FILE_EXTS[SupportUploadFileTypes.audio],
|
||||
...FILE_EXTS[SupportUploadFileTypes.video],
|
||||
],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
number_limits: currentVar.value_type === 'file' ? 1 : fileUploadConfig?.workflow_file_upload_limit || 5,
|
||||
fileUploadConfig,
|
||||
preview_config: {
|
||||
mode: PreviewMode.NewPage,
|
||||
file_type_list: ['application/pdf'],
|
||||
},
|
||||
}}
|
||||
isDisabled={textEditorDisabled}
|
||||
/>
|
||||
</div>
|
||||
<FileEditorSection
|
||||
currentVar={currentVar}
|
||||
fileValue={fileValue}
|
||||
fileUploadConfig={fileUploadConfig}
|
||||
textEditorDisabled={textEditorDisabled}
|
||||
onChange={files => handleFileChange(getProcessedFiles(files))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={errorMessageRef} className="shrink-0">
|
||||
{parseError && <ErrorMessage className="mt-1" message={parseError.message} />}
|
||||
{validationError && <ErrorMessage className="mt-1" message={validationError} />}
|
||||
<ErrorMessages
|
||||
parseError={parseError}
|
||||
validationError={validationError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user