mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
Merge commit '9c339239' into sandboxed-agent-rebase
Made-with: Cursor # Conflicts: # api/README.md # api/controllers/console/app/workflow_draft_variable.py # api/core/agent/cot_agent_runner.py # api/core/agent/fc_agent_runner.py # api/core/app/apps/advanced_chat/app_runner.py # api/core/plugin/backwards_invocation/model.py # api/core/prompt/advanced_prompt_transform.py # api/core/workflow/nodes/base/node.py # api/core/workflow/nodes/llm/llm_utils.py # api/core/workflow/nodes/llm/node.py # api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py # api/core/workflow/nodes/question_classifier/question_classifier_node.py # api/core/workflow/runtime/graph_runtime_state.py # api/extensions/storage/base_storage.py # api/factories/variable_factory.py # api/pyproject.toml # api/services/variable_truncator.py # api/uv.lock # web/app/account/oauth/authorize/page.tsx # web/app/components/app/configuration/config-var/config-modal/field.tsx # web/app/components/base/alert.tsx # web/app/components/base/chat/chat/answer/human-input-content/executed-action.tsx # web/app/components/base/chat/chat/answer/more.tsx # web/app/components/base/chat/chat/answer/operation.tsx # web/app/components/base/chat/chat/answer/workflow-process.tsx # web/app/components/base/chat/chat/citation/index.tsx # web/app/components/base/chat/chat/citation/popup.tsx # web/app/components/base/chat/chat/citation/progress-tooltip.tsx # web/app/components/base/chat/chat/citation/tooltip.tsx # web/app/components/base/chat/chat/question.tsx # web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx # web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx # web/app/components/base/markdown-blocks/form.tsx # web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx # web/app/components/base/tag-management/panel.tsx # web/app/components/base/tag-management/trigger.tsx # web/app/components/header/account-setting/index.tsx # web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx # web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx # web/app/signin/utils/post-login-redirect.ts # web/eslint-suppressions.json # web/package.json # web/pnpm-lock.yaml
This commit is contained in:
@ -0,0 +1,40 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ParamItem from '..'
|
||||
|
||||
describe('ParamItem Slider onChange', () => {
|
||||
const defaultProps = {
|
||||
id: 'test_param',
|
||||
name: 'Test Param',
|
||||
enable: true,
|
||||
onChange: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should divide slider value by 100 when max < 5', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ParamItem {...defaultProps} value={0.5} min={0} max={1} />)
|
||||
const slider = screen.getByRole('slider')
|
||||
|
||||
await user.click(slider)
|
||||
await user.keyboard('{ArrowRight}')
|
||||
|
||||
// max=1 < 5, so slider value change (50->51) becomes 0.51
|
||||
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 0.51)
|
||||
})
|
||||
|
||||
it('should not divide slider value when max >= 5', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ParamItem {...defaultProps} value={5} min={1} max={10} />)
|
||||
const slider = screen.getByRole('slider')
|
||||
|
||||
await user.click(slider)
|
||||
await user.keyboard('{ArrowRight}')
|
||||
|
||||
// max=10 >= 5, so value remains raw (5->6)
|
||||
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 6)
|
||||
})
|
||||
})
|
||||
179
web/app/components/base/param-item/__tests__/index.spec.tsx
Normal file
179
web/app/components/base/param-item/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import ParamItem from '..'
|
||||
|
||||
describe('ParamItem', () => {
|
||||
const defaultProps = {
|
||||
id: 'test_param',
|
||||
name: 'Test Param',
|
||||
value: 0.5,
|
||||
enable: true,
|
||||
max: 1,
|
||||
onChange: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the parameter name', () => {
|
||||
render(<ParamItem {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('Test Param')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render a tooltip trigger by default', () => {
|
||||
const { container } = render(<ParamItem {...defaultProps} tip="Some tip text" />)
|
||||
|
||||
// Tooltip trigger icon should be rendered (the data-state div)
|
||||
expect(container.querySelector('[data-state]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render tooltip trigger when noTooltip is true', () => {
|
||||
const { container } = render(<ParamItem {...defaultProps} noTooltip tip="Hidden tip" />)
|
||||
|
||||
// No tooltip trigger icon should be rendered
|
||||
expect(container.querySelector('[data-state]')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render a switch when hasSwitch is true', () => {
|
||||
render(<ParamItem {...defaultProps} hasSwitch />)
|
||||
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render a switch by default', () => {
|
||||
render(<ParamItem {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render InputNumber and Slider', () => {
|
||||
render(<ParamItem {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('spinbutton')).toBeInTheDocument()
|
||||
expect(screen.getByRole('slider')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<ParamItem {...defaultProps} className="my-custom-class" />)
|
||||
|
||||
expect(container.firstChild).toHaveClass('my-custom-class')
|
||||
})
|
||||
|
||||
it('should disable InputNumber when enable is false', () => {
|
||||
render(<ParamItem {...defaultProps} enable={false} />)
|
||||
|
||||
expect(screen.getByRole('spinbutton')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable Slider when enable is false', () => {
|
||||
render(<ParamItem {...defaultProps} enable={false} />)
|
||||
|
||||
expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true')
|
||||
})
|
||||
|
||||
it('should set switch value based on enable prop', () => {
|
||||
render(<ParamItem {...defaultProps} hasSwitch enable={true} />)
|
||||
|
||||
const toggle = screen.getByRole('switch')
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onChange with id and value when InputNumber changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
const StatefulParamItem = () => {
|
||||
const [value, setValue] = useState(defaultProps.value)
|
||||
|
||||
return (
|
||||
<ParamItem
|
||||
{...defaultProps}
|
||||
value={value}
|
||||
onChange={(key: string, nextValue: number) => {
|
||||
defaultProps.onChange(key, nextValue)
|
||||
setValue(nextValue)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render(<StatefulParamItem />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
await user.clear(input)
|
||||
await user.type(input, '0.8')
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 0.8)
|
||||
})
|
||||
|
||||
it('should pass scaled value to slider when max < 5', () => {
|
||||
render(<ParamItem {...defaultProps} value={0.5} />)
|
||||
const slider = screen.getByRole('slider')
|
||||
|
||||
// When max < 5, slider value = value * 100 = 50
|
||||
expect(slider).toHaveAttribute('aria-valuenow', '50')
|
||||
})
|
||||
|
||||
it('should pass raw value to slider when max >= 5', () => {
|
||||
render(<ParamItem {...defaultProps} value={5} max={10} />)
|
||||
const slider = screen.getByRole('slider')
|
||||
|
||||
// When max >= 5, slider value = value = 5
|
||||
expect(slider).toHaveAttribute('aria-valuenow', '5')
|
||||
})
|
||||
|
||||
it('should call onSwitchChange with id and value when switch is toggled', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSwitchChange = vi.fn()
|
||||
render(<ParamItem {...defaultProps} hasSwitch onSwitchChange={onSwitchChange} />)
|
||||
|
||||
await user.click(screen.getByRole('switch'))
|
||||
|
||||
expect(onSwitchChange).toHaveBeenCalledWith('test_param', expect.any(Boolean))
|
||||
})
|
||||
|
||||
it('should call onChange with id when increment button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ParamItem {...defaultProps} value={0.5} step={0.1} />)
|
||||
const incrementBtn = screen.getByRole('button', { name: /increment/i })
|
||||
|
||||
await user.click(incrementBtn)
|
||||
|
||||
// step=0.1, so 0.5 + 0.1 = 0.6, clamped to [0,1] → 0.6
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith('test_param', 0.6)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should correctly scale slider value when max < 5', () => {
|
||||
render(<ParamItem {...defaultProps} value={0.5} min={0} />)
|
||||
|
||||
// Slider should get value * 100 = 50, min * 100 = 0, max * 100 = 100
|
||||
const slider = screen.getByRole('slider')
|
||||
expect(slider).toHaveAttribute('aria-valuemax', '100')
|
||||
})
|
||||
|
||||
it('should not scale slider value when max >= 5', () => {
|
||||
render(<ParamItem {...defaultProps} value={5} min={1} max={10} />)
|
||||
|
||||
const slider = screen.getByRole('slider')
|
||||
expect(slider).toHaveAttribute('aria-valuemax', '10')
|
||||
})
|
||||
|
||||
it('should use default step of 0.1 and min of 0 when not provided', () => {
|
||||
render(<ParamItem {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
// Component renders without error with default step/min
|
||||
expect(screen.getByRole('spinbutton')).toBeInTheDocument()
|
||||
expect(input).toHaveAttribute('step', '0.1')
|
||||
expect(input).toHaveAttribute('min', '0')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,145 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import ScoreThresholdItem from '../score-threshold-item'
|
||||
|
||||
describe('ScoreThresholdItem', () => {
|
||||
const defaultProps = {
|
||||
value: 0.7,
|
||||
enable: true,
|
||||
onChange: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the translated parameter name', () => {
|
||||
render(<ScoreThresholdItem {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('appDebug.datasetConfig.score_threshold')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tooltip trigger', () => {
|
||||
const { container } = render(<ScoreThresholdItem {...defaultProps} />)
|
||||
|
||||
// Tooltip trigger icon should be rendered
|
||||
expect(container.querySelector('[data-state]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render InputNumber and Slider', () => {
|
||||
render(<ScoreThresholdItem {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('spinbutton')).toBeInTheDocument()
|
||||
expect(screen.getByRole('slider')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<ScoreThresholdItem {...defaultProps} className="custom-cls" />)
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-cls')
|
||||
})
|
||||
|
||||
it('should render switch when hasSwitch is true', () => {
|
||||
render(<ScoreThresholdItem {...defaultProps} hasSwitch />)
|
||||
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should forward onSwitchChange to ParamItem', async () => {
|
||||
const onSwitchChange = vi.fn()
|
||||
render(<ScoreThresholdItem {...defaultProps} hasSwitch onSwitchChange={onSwitchChange} />)
|
||||
|
||||
// Verify the switch rendered (onSwitchChange forwarded internally)
|
||||
expect(screen.getByRole('switch')).toBeInTheDocument()
|
||||
await userEvent.click(screen.getByRole('switch'))
|
||||
expect(onSwitchChange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should disable controls when enable is false', () => {
|
||||
render(<ScoreThresholdItem {...defaultProps} enable={false} />)
|
||||
|
||||
expect(screen.getByRole('spinbutton')).toBeDisabled()
|
||||
expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Value Clamping', () => {
|
||||
it('should clamp values to minimum of 0', () => {
|
||||
render(<ScoreThresholdItem {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
expect(input).toHaveAttribute('min', '0')
|
||||
})
|
||||
|
||||
it('should clamp values to maximum of 1', () => {
|
||||
render(<ScoreThresholdItem {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
expect(input).toHaveAttribute('max', '1')
|
||||
})
|
||||
|
||||
it('should use step of 0.01', () => {
|
||||
render(<ScoreThresholdItem {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
expect(input).toHaveAttribute('step', '0.01')
|
||||
})
|
||||
|
||||
it('should call onChange with rounded value when input changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
const StatefulScoreThresholdItem = () => {
|
||||
const [value, setValue] = useState(defaultProps.value)
|
||||
|
||||
return (
|
||||
<ScoreThresholdItem
|
||||
{...defaultProps}
|
||||
value={value}
|
||||
onChange={(key, nextValue) => {
|
||||
defaultProps.onChange(key, nextValue)
|
||||
setValue(nextValue)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render(<StatefulScoreThresholdItem />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
await user.clear(input)
|
||||
await user.type(input, '0.55')
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenLastCalledWith('score_threshold', 0.55)
|
||||
})
|
||||
|
||||
it('should call onChange with clamped value via increment button', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ScoreThresholdItem {...defaultProps} value={0.5} />)
|
||||
const incrementBtn = screen.getByRole('button', { name: /increment/i })
|
||||
|
||||
await user.click(incrementBtn)
|
||||
|
||||
// step=0.01, so 0.5 + 0.01 = 0.51, clamped to [0,1] → 0.51
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith('score_threshold', 0.51)
|
||||
})
|
||||
|
||||
it('should call onChange with clamped value via decrement button', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ScoreThresholdItem {...defaultProps} value={0.5} />)
|
||||
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
|
||||
|
||||
await user.click(decrementBtn)
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith('score_threshold', 0.49)
|
||||
})
|
||||
|
||||
it('should clamp to max=1 when value exceeds maximum', () => {
|
||||
render(<ScoreThresholdItem {...defaultProps} value={1.5} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
expect(input).toHaveValue(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
130
web/app/components/base/param-item/__tests__/top-k-item.spec.tsx
Normal file
130
web/app/components/base/param-item/__tests__/top-k-item.spec.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import TopKItem from '../top-k-item'
|
||||
|
||||
vi.mock('@/env', () => ({
|
||||
env: {
|
||||
NEXT_PUBLIC_TOP_K_MAX_VALUE: 10,
|
||||
},
|
||||
}))
|
||||
|
||||
describe('TopKItem', () => {
|
||||
const defaultProps = {
|
||||
value: 2,
|
||||
enable: true,
|
||||
onChange: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the translated parameter name', () => {
|
||||
render(<TopKItem {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('appDebug.datasetConfig.top_k')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tooltip trigger', () => {
|
||||
const { container } = render(<TopKItem {...defaultProps} />)
|
||||
|
||||
// Tooltip trigger icon should be rendered
|
||||
expect(container.querySelector('[data-state]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render InputNumber and Slider', () => {
|
||||
render(<TopKItem {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('spinbutton')).toBeInTheDocument()
|
||||
expect(screen.getByRole('slider')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<TopKItem {...defaultProps} className="custom-cls" />)
|
||||
|
||||
expect(container.firstChild).toHaveClass('custom-cls')
|
||||
})
|
||||
|
||||
it('should disable controls when enable is false', () => {
|
||||
render(<TopKItem {...defaultProps} enable={false} />)
|
||||
|
||||
expect(screen.getByRole('spinbutton')).toBeDisabled()
|
||||
expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Value Limits', () => {
|
||||
it('should use step of 1', () => {
|
||||
render(<TopKItem {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
expect(input).toHaveAttribute('step', '1')
|
||||
})
|
||||
|
||||
it('should use minimum of 1', () => {
|
||||
render(<TopKItem {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
expect(input).toHaveAttribute('min', '1')
|
||||
})
|
||||
|
||||
it('should use maximum from env (10)', () => {
|
||||
render(<TopKItem {...defaultProps} />)
|
||||
const input = screen.getByRole('spinbutton')
|
||||
|
||||
expect(input).toHaveAttribute('max', '10')
|
||||
})
|
||||
|
||||
it('should render slider with max >= 5 so no scaling is applied', () => {
|
||||
render(<TopKItem {...defaultProps} />)
|
||||
const slider = screen.getByRole('slider')
|
||||
|
||||
// max=10 >= 5 so slider shows raw values
|
||||
expect(slider).toHaveAttribute('aria-valuemax', '10')
|
||||
})
|
||||
|
||||
it('should not render a switch (no hasSwitch prop)', () => {
|
||||
render(<TopKItem {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call onChange with clamped integer value via increment button', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<TopKItem {...defaultProps} value={5} />)
|
||||
const incrementBtn = screen.getByRole('button', { name: /increment/i })
|
||||
|
||||
await user.click(incrementBtn)
|
||||
|
||||
// step=1, so 5 + 1 = 6, clamped to [1,10] → 6
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith('top_k', 6)
|
||||
})
|
||||
|
||||
it('should call onChange with clamped integer value via decrement button', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<TopKItem {...defaultProps} value={5} />)
|
||||
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
|
||||
|
||||
await user.click(decrementBtn)
|
||||
|
||||
// step=1, so 5 - 1 = 4, clamped to [1,10] → 4
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith('top_k', 4)
|
||||
})
|
||||
|
||||
it('should call onChange with integer value when slider changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<TopKItem {...defaultProps} value={2} />)
|
||||
const slider = screen.getByRole('slider')
|
||||
|
||||
await user.click(slider)
|
||||
await user.keyboard('{ArrowRight}')
|
||||
|
||||
expect(defaultProps.onChange).toHaveBeenLastCalledWith('top_k', 3)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -35,6 +35,11 @@ const ScoreThresholdItem: FC<Props> = ({
|
||||
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
|
||||
onChange(key, notOutRangeValue)
|
||||
}
|
||||
const safeValue = Math.min(
|
||||
VALUE_LIMIT.max,
|
||||
Math.max(VALUE_LIMIT.min, Number.parseFloat(value.toFixed(2))),
|
||||
)
|
||||
|
||||
return (
|
||||
<ParamItem
|
||||
className={className}
|
||||
@ -42,7 +47,7 @@ const ScoreThresholdItem: FC<Props> = ({
|
||||
name={t('datasetConfig.score_threshold', { ns: 'appDebug' })}
|
||||
tip={t('datasetConfig.score_thresholdTip', { ns: 'appDebug' }) as string}
|
||||
{...VALUE_LIMIT}
|
||||
value={value}
|
||||
value={safeValue}
|
||||
enable={enable}
|
||||
onChange={handleParamChange}
|
||||
hasSwitch={hasSwitch}
|
||||
|
||||
Reference in New Issue
Block a user