mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +08:00
fix(dify-ui): align form label guidance (#36510)
This commit is contained in:
parent
157e6244dd
commit
93b7a81071
@ -2255,9 +2255,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
|
||||
@ -58,13 +58,15 @@ Utilities:
|
||||
|
||||
## Form contract
|
||||
|
||||
Dify UI's form primitives are a Base UI composition layer for native form semantics, field accessibility, and design-system styling. They are intentionally not a form state-management framework. See the upstream [Base UI Form], [Base UI Field], and [Base UI Fieldset] docs for the underlying component contracts.
|
||||
Dify UI's form primitives are a Base UI composition layer for native form semantics, field accessibility, and design-system styling. They are intentionally not a form state-management framework. See the upstream [Base UI forms handbook], [Base UI Form], [Base UI Field], and [Base UI Fieldset] docs for the underlying component contracts.
|
||||
|
||||
Use `Form` for the submit boundary. It renders a native `<form>`, preserves Enter-to-submit and submit-button behavior, and adds Base UI's `onFormSubmit`, `errors`, `actionsRef`, and `validationMode` APIs for structured values and consolidated field validation. Prefer it over a bare `<form>` when the form is composed with Dify UI fields.
|
||||
|
||||
Use `FieldRoot` for each named field. A field must have a stable `name`, a visible `FieldLabel`, and either a `FieldControl` or another control that participates in the same Base UI field context. `FieldLabel`, `FieldDescription`, and `FieldError` provide the label and message relationships that screen readers need, while the Dify wrapper adds the default Form Input Set styling from the design system.
|
||||
Use `FieldRoot` for each standalone named field. A field must have a stable `name`, a label relationship, and either a `FieldControl` or another control that participates in the same Base UI field context. Prefer a visible label for normal form rows; when the surrounding UI already supplies the visible text, use the matching label primitive visually hidden or put `aria-label` on the actual interactive control. `FieldDescription` and `FieldError` provide the message relationships that screen readers need, while the Dify wrapper adds the default Form Input Set styling from the design system.
|
||||
|
||||
Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group of related controls, such as checkbox groups, radio groups, or multi-thumb sliders. Compose group controls with the Base UI pattern:
|
||||
Choose the label primitive by the control semantics. Text-like inputs, input-based `Combobox` / `Autocomplete`, single `Checkbox` / `Radio`, `Switch`, and `NumberField` use `FieldLabel`. Trigger-based `Select` fields use `SelectLabel`; `Slider` fields use `SliderLabel`, with per-thumb `aria-label` only when the thumbs need distinct names. `SelectGroupLabel` and `AutocompleteGroupLabel` only label grouped options inside their popup content; they are not field labels.
|
||||
|
||||
Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group of related controls, such as checkbox groups, radio groups, multi-thumb sliders, or a section that combines several inputs. For checkbox and radio groups, wrap each option with `FieldItem` and give each option its own label:
|
||||
|
||||
```tsx
|
||||
<FieldRoot name="allowedNetworkProtocols">
|
||||
@ -82,9 +84,9 @@ Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group
|
||||
|
||||
`FieldsetRoot` provides the group semantics and legend relationship. It does not own the interactive state of the grouped control. Pass `disabled`, `value`, `defaultValue`, and change handlers to the actual group primitive (`CheckboxGroup`, radio group, slider root, etc.) instead of relying on the fieldset wrapper to manage them.
|
||||
|
||||
For complex business forms, keep state ownership outside these primitives. TanStack Form, zod, server validation, dialog reset behavior, and schema-driven rendering belong to the feature layer in `web/`; they should pass `name`, `invalid`, `dirty`, `touched`, `value`, `onValueChange`, and errors into these primitives rather than replacing the field semantics.
|
||||
For complex business forms, keep state ownership outside these primitives. TanStack Form, zod, server validation, dialog reset behavior, and schema-driven rendering belong to the feature layer in `web/`; they should pass `name`, `invalid`, `dirty`, `touched`, `value`, `onValueChange`, and errors into these primitives rather than replacing the field semantics. In this repo, `web/app/components/base/form` is the TanStack/schema runtime adapter; `packages/dify-ui` remains the primitive layer.
|
||||
|
||||
Migration rule for `web/`: if a UI has a save/submit action, do not leave it as unrelated `Input` and `Button` pieces. Give it a real submit boundary with `Form` or a native `<form>`, attach visible field names through `FieldLabel`, expose helper/error text through `FieldDescription` / `FieldError`, and keep non-submit buttons as `type="button"`.
|
||||
Migration rule for `web/`: if a UI has a save/submit action, do not leave it as unrelated `Input` and `Button` pieces. Give it a real submit boundary with `Form` or a native `<form>`, attach visible field names through the appropriate label primitive (`FieldLabel`, `SelectLabel`, `SliderLabel`, or `FieldsetLegend`), expose helper/error text through `FieldDescription` / `FieldError`, and keep non-submit buttons as `type="button"`.
|
||||
|
||||
## Tailwind CSS v4 integration
|
||||
|
||||
@ -180,5 +182,6 @@ See `[AGENTS.md](./AGENTS.md)` for:
|
||||
[Base UI Form]: https://base-ui.com/react/components/form
|
||||
[Base UI Portal]: https://base-ui.com/react/overview/quick-start#portals
|
||||
[Base UI docs index]: https://base-ui.com/llms.txt
|
||||
[Base UI forms handbook]: https://base-ui.com/react/handbook/forms
|
||||
[Base UI]: https://base-ui.com/react
|
||||
[Overlay & portal contract]: #overlay--portal-contract
|
||||
|
||||
@ -6,12 +6,12 @@ import {
|
||||
AutocompleteContent,
|
||||
AutocompleteEmpty,
|
||||
AutocompleteGroup,
|
||||
AutocompleteGroupLabel,
|
||||
AutocompleteInput,
|
||||
AutocompleteInputGroup,
|
||||
AutocompleteItem,
|
||||
AutocompleteItemIndicator,
|
||||
AutocompleteItemText,
|
||||
AutocompleteLabel,
|
||||
AutocompleteList,
|
||||
AutocompleteSeparator,
|
||||
AutocompleteStatus,
|
||||
@ -230,7 +230,7 @@ describe('Autocomplete wrappers', () => {
|
||||
<AutocompleteContent popupProps={{ 'role': 'dialog', 'aria-label': 'autocomplete popup' }}>
|
||||
<AutocompleteList role="listbox" aria-label="autocomplete list">
|
||||
<AutocompleteGroup items={['workflow']}>
|
||||
<AutocompleteLabel className="custom-label">Resources</AutocompleteLabel>
|
||||
<AutocompleteGroupLabel className="custom-label">Resources</AutocompleteGroupLabel>
|
||||
<AutocompleteSeparator className="custom-separator" data-testid="separator" />
|
||||
<AutocompleteItem value="workflow" className="custom-item">
|
||||
<AutocompleteItemText className="custom-text">Workflow</AutocompleteItemText>
|
||||
|
||||
@ -10,11 +10,11 @@ import {
|
||||
AutocompleteContent,
|
||||
AutocompleteEmpty,
|
||||
AutocompleteGroup,
|
||||
AutocompleteGroupLabel,
|
||||
AutocompleteInput,
|
||||
AutocompleteInputGroup,
|
||||
AutocompleteItem,
|
||||
AutocompleteItemText,
|
||||
AutocompleteLabel,
|
||||
AutocompleteList,
|
||||
AutocompleteSeparator,
|
||||
AutocompleteStatus,
|
||||
@ -232,7 +232,7 @@ const GroupedSuggestionList = () => {
|
||||
{groups.map((group, groupIndex) => (
|
||||
<AutocompleteGroup key={group.label} items={group.items}>
|
||||
{groupIndex > 0 && <AutocompleteSeparator />}
|
||||
<AutocompleteLabel>{group.label}</AutocompleteLabel>
|
||||
<AutocompleteGroupLabel>{group.label}</AutocompleteGroupLabel>
|
||||
<AutocompleteCollection>
|
||||
{(item: Suggestion) => (
|
||||
<SuggestionItem key={item.value} item={item} />
|
||||
@ -252,7 +252,7 @@ const CommandPaletteList = () => {
|
||||
{groups.map((group, groupIndex) => (
|
||||
<AutocompleteGroup key={group.label} items={group.items}>
|
||||
{groupIndex > 0 && <AutocompleteSeparator />}
|
||||
<AutocompleteLabel>{group.label}</AutocompleteLabel>
|
||||
<AutocompleteGroupLabel>{group.label}</AutocompleteGroupLabel>
|
||||
<AutocompleteCollection>
|
||||
{(item: Suggestion) => (
|
||||
<AutocompleteItem key={item.value} value={item} className="grid grid-cols-[1fr_auto]">
|
||||
@ -475,7 +475,7 @@ const FuzzyMatchingDemo = () => {
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/Autocomplete',
|
||||
title: 'Base/Form/Autocomplete',
|
||||
component: Autocomplete,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
@ -599,7 +599,7 @@ export const LimitResults: Story = {
|
||||
|
||||
export const CommandPalette: Story = {
|
||||
render: () => (
|
||||
<div className="w-[440px] rounded-xl border border-divider-subtle bg-components-panel-bg-alt p-2 shadow-xs">
|
||||
<div className="w-110 rounded-xl border border-divider-subtle bg-components-panel-bg-alt p-2 shadow-xs">
|
||||
<Autocomplete
|
||||
open
|
||||
inline
|
||||
|
||||
@ -316,7 +316,7 @@ export function AutocompleteItemText({
|
||||
)
|
||||
}
|
||||
|
||||
export function AutocompleteLabel({
|
||||
export function AutocompleteGroupLabel({
|
||||
className,
|
||||
...props
|
||||
}: BaseAutocomplete.GroupLabel.Props) {
|
||||
|
||||
@ -6,6 +6,7 @@ import type { Placement } from '../placement'
|
||||
import { Combobox as BaseCombobox } from '@base-ui/react/combobox'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { cn } from '../cn'
|
||||
import { formLabelClassName } from '../form-control-shared'
|
||||
import {
|
||||
overlayIndicatorClassName,
|
||||
overlayLabelClassName,
|
||||
@ -399,7 +400,7 @@ export function ComboboxLabel({
|
||||
}: BaseCombobox.Label.Props) {
|
||||
return (
|
||||
<BaseCombobox.Label
|
||||
className={cn('mb-1 block text-text-secondary system-sm-medium', className)}
|
||||
className={cn(formLabelClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -4,7 +4,7 @@ import type { Field as BaseFieldNS } from '@base-ui/react/field'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { Field as BaseField } from '@base-ui/react/field'
|
||||
import { cn } from '../cn'
|
||||
import { textControlVariants } from '../text-control-variants'
|
||||
import { formLabelClassName, textControlVariants } from '../form-control-shared'
|
||||
|
||||
export type FieldRootProps
|
||||
= Omit<BaseFieldNS.Root.Props, 'className'>
|
||||
@ -56,7 +56,7 @@ export function FieldLabel({
|
||||
}: FieldLabelProps) {
|
||||
return (
|
||||
<BaseField.Label
|
||||
className={cn('w-fit py-1 text-text-secondary system-sm-medium data-disabled:cursor-not-allowed', className)}
|
||||
className={cn(formLabelClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export const formLabelClassName = 'w-fit py-1 text-text-secondary system-sm-medium data-disabled:cursor-not-allowed'
|
||||
|
||||
export const textControlVariants = cva(
|
||||
[
|
||||
'w-full appearance-none border border-transparent bg-components-input-bg-normal text-components-input-text-filled caret-primary-600 outline-hidden transition-[background-color,border-color,box-shadow]',
|
||||
@ -4,7 +4,7 @@ import type { Input as BaseInputNS } from '@base-ui/react/input'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import { Input as BaseInput } from '@base-ui/react/input'
|
||||
import { cn } from '../cn'
|
||||
import { textControlVariants } from '../text-control-variants'
|
||||
import { textControlVariants } from '../form-control-shared'
|
||||
|
||||
export type InputSize = NonNullable<VariantProps<typeof textControlVariants>['size']>
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ const meta = {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'RadioGroup primitive built on Base UI. For normal form rows, compose FieldRoot, FieldsetRoot, FieldLabel, RadioGroup, and Radio. For option cards, make the card itself a RadioRoot with variant="unstyled" and render RadioControl inside it.',
|
||||
component: 'RadioGroup primitive built on Base UI. For normal form rows, compose FieldRoot, FieldsetRoot, FieldLabel, RadioGroup, and Radio. For option cards, wrap each option in FieldItem and make the card itself a RadioRoot with variant="unstyled".',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -130,26 +130,27 @@ function OptionCardsDemo() {
|
||||
description: 'Write a prompt for this app and keep full control.',
|
||||
},
|
||||
].map(option => (
|
||||
<RadioRoot
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
variant="unstyled"
|
||||
nativeButton
|
||||
render={<button type="button" />}
|
||||
className="w-full rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-4 text-left transition-colors hover:bg-state-base-hover data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="system-sm-semibold text-text-primary">
|
||||
{option.title}
|
||||
</div>
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">
|
||||
{option.description}
|
||||
<FieldItem key={option.value}>
|
||||
<RadioRoot
|
||||
value={option.value}
|
||||
variant="unstyled"
|
||||
nativeButton
|
||||
render={<button type="button" />}
|
||||
className="w-full rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-4 text-left transition-colors hover:bg-state-base-hover data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="system-sm-semibold text-text-primary">
|
||||
{option.title}
|
||||
</div>
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">
|
||||
{option.description}
|
||||
</div>
|
||||
</div>
|
||||
<RadioControl aria-hidden="true" />
|
||||
</div>
|
||||
<RadioControl aria-hidden="true" />
|
||||
</div>
|
||||
</RadioRoot>
|
||||
</RadioRoot>
|
||||
</FieldItem>
|
||||
))}
|
||||
</FieldsetRoot>
|
||||
</FieldRoot>
|
||||
@ -161,7 +162,7 @@ export const OptionCards: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Use RadioRoot with variant="unstyled" when the entire option card is the radio. RadioControl renders the visual dot inside the card.',
|
||||
story: 'Wrap each option card in FieldItem, then use RadioRoot with variant="unstyled" when the entire card is the radio. RadioControl renders the visual dot inside the card.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger, SelectValue } from '../index'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectGroupLabel,
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '../index'
|
||||
|
||||
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
|
||||
const renderWithSafeViewport = (ui: import('react').ReactNode) => render(
|
||||
@ -84,6 +95,26 @@ describe('Select wrappers', () => {
|
||||
})
|
||||
|
||||
describe('SelectTrigger', () => {
|
||||
it('should use SelectLabel as the trigger accessible name', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<Select defaultValue="seattle">
|
||||
<SelectLabel>City</SelectLabel>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="seattle">
|
||||
<SelectItemText>Seattle</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByRole('combobox', { name: 'City' })).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('City')).toHaveClass('py-1', 'system-sm-medium', 'text-text-secondary')
|
||||
})
|
||||
|
||||
it('should forward native trigger props when trigger props are provided', async () => {
|
||||
const screen = await renderOpenSelect({
|
||||
triggerProps: {
|
||||
@ -179,6 +210,28 @@ describe('Select wrappers', () => {
|
||||
})
|
||||
|
||||
describe('SelectContent', () => {
|
||||
it('should render SelectGroupLabel for grouped options without naming the trigger', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<Select open defaultValue="seattle">
|
||||
<SelectTrigger aria-label="city select">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent listProps={{ 'role': 'listbox', 'aria-label': 'select list' }}>
|
||||
<SelectGroup>
|
||||
<SelectGroupLabel className="custom-label">Popular cities</SelectGroupLabel>
|
||||
<SelectItem value="seattle">
|
||||
<SelectItemText>Seattle</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByRole('combobox', { name: 'city select' })).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('Popular cities')).toHaveClass('custom-label')
|
||||
})
|
||||
|
||||
it('should use positioning attributes when placement is not provided', async () => {
|
||||
const screen = await renderOpenSelect()
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectGroupLabel,
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
@ -62,6 +63,29 @@ export const Default: Story = {
|
||||
),
|
||||
}
|
||||
|
||||
export const WithVisibleLabel: Story = {
|
||||
render: () => (
|
||||
<div className={triggerWidth}>
|
||||
<Select defaultValue="seattle">
|
||||
<SelectLabel>City</SelectLabel>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a city" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="seattle">
|
||||
<SelectItemText>Seattle</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="new-york">
|
||||
<SelectItemText>New York</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithPlaceholder: Story = {
|
||||
render: () => (
|
||||
<div className={triggerWidth}>
|
||||
@ -123,7 +147,7 @@ export const WithGroupsAndSeparator: Story = {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>OpenAI</SelectLabel>
|
||||
<SelectGroupLabel>OpenAI</SelectGroupLabel>
|
||||
<SelectItem value="gpt-5">
|
||||
<SelectItemText>GPT-5</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
@ -135,7 +159,7 @@ export const WithGroupsAndSeparator: Story = {
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Anthropic</SelectLabel>
|
||||
<SelectGroupLabel>Anthropic</SelectGroupLabel>
|
||||
<SelectItem value="claude-opus">
|
||||
<SelectItemText>Claude Opus</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
@ -147,7 +171,7 @@ export const WithGroupsAndSeparator: Story = {
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Google</SelectLabel>
|
||||
<SelectGroupLabel>Google</SelectGroupLabel>
|
||||
<SelectItem value="gemini-25">
|
||||
<SelectItemText>Gemini 2.5</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
|
||||
@ -6,6 +6,7 @@ import type { Placement } from '../placement'
|
||||
import { Select as BaseSelect } from '@base-ui/react/select'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { cn } from '../cn'
|
||||
import { formLabelClassName } from '../form-control-shared'
|
||||
import {
|
||||
overlayLabelClassName,
|
||||
overlayPopupAnimationClassName,
|
||||
@ -71,6 +72,18 @@ export function SelectTrigger({
|
||||
export function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: BaseSelect.Label.Props) {
|
||||
return (
|
||||
<BaseSelect.Label
|
||||
className={cn(formLabelClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function SelectGroupLabel({
|
||||
className,
|
||||
...props
|
||||
}: BaseSelect.GroupLabel.Props) {
|
||||
return (
|
||||
<BaseSelect.GroupLabel
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { Slider } from '../index'
|
||||
import {
|
||||
Slider,
|
||||
SliderControl,
|
||||
SliderIndicator,
|
||||
SliderLabel,
|
||||
SliderRoot,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
} from '../index'
|
||||
|
||||
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
|
||||
|
||||
@ -77,4 +85,21 @@ describe('Slider', () => {
|
||||
|
||||
expect(screen.container.querySelector('script')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose SliderLabel for composed slider fields', async () => {
|
||||
const screen = await render(
|
||||
<SliderRoot defaultValue={50}>
|
||||
<SliderLabel>Temperature</SliderLabel>
|
||||
<SliderControl>
|
||||
<SliderTrack>
|
||||
<SliderIndicator />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</SliderControl>
|
||||
</SliderRoot>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByRole('slider', { name: 'Temperature' })).toHaveAttribute('aria-valuenow', '50')
|
||||
await expect.element(screen.getByText('Temperature')).toHaveClass('py-1', 'system-sm-medium', 'text-text-secondary')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Slider } from '.'
|
||||
import {
|
||||
Slider,
|
||||
SliderControl,
|
||||
SliderIndicator,
|
||||
SliderLabel,
|
||||
SliderRoot,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
} from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Form/Slider',
|
||||
@ -90,3 +98,17 @@ export const Disabled: Story = {
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const ComposedWithLabel: Story = {
|
||||
render: () => (
|
||||
<SliderRoot defaultValue={50} className="group/slider relative inline-flex w-[320px] flex-col gap-1 data-disabled:opacity-30">
|
||||
<SliderLabel>Temperature</SliderLabel>
|
||||
<SliderControl>
|
||||
<SliderTrack>
|
||||
<SliderIndicator />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</SliderControl>
|
||||
</SliderRoot>
|
||||
),
|
||||
}
|
||||
|
||||
@ -2,9 +2,22 @@
|
||||
|
||||
import { Slider as BaseSlider } from '@base-ui/react/slider'
|
||||
import { cn } from '../cn'
|
||||
import { formLabelClassName } from '../form-control-shared'
|
||||
|
||||
export const SliderRoot = BaseSlider.Root
|
||||
|
||||
export function SliderLabel({
|
||||
className,
|
||||
...props
|
||||
}: BaseSlider.Label.Props) {
|
||||
return (
|
||||
<BaseSlider.Label
|
||||
className={cn(formLabelClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type SliderRootProps = BaseSlider.Root.Props<number>
|
||||
|
||||
const sliderControlClassName = cn(
|
||||
|
||||
@ -61,5 +61,6 @@ export const SelectItem = ({
|
||||
export const SelectItemText = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
export const SelectItemIndicator = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
export const SelectGroup = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
export const SelectLabel = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
export const SelectLabel = () => null
|
||||
export const SelectGroupLabel = ({ children }: { children?: ReactNode }) => <>{children}</>
|
||||
export const SelectSeparator = (props: React.HTMLAttributes<HTMLDivElement>) => <div role="separator" {...props} />
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { AgentConfig } from '@/models/debug'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import { Slider } from '@langgenius/dify-ui/slider'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
@ -34,6 +35,7 @@ const AgentSetting: FC<Props> = ({
|
||||
const [tempPayload, setTempPayload] = useState(payload)
|
||||
const ref = useRef(null)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const maximumIterationsLabel = t('agent.setting.maximumIterations.name', { ns: 'appDebug' })
|
||||
|
||||
useClickAway(() => {
|
||||
if (mounted)
|
||||
@ -96,10 +98,11 @@ const AgentSetting: FC<Props> = ({
|
||||
icon={
|
||||
<Unblur className="h-4 w-4 text-[#FB6514]" />
|
||||
}
|
||||
name={t('agent.setting.maximumIterations.name', { ns: 'appDebug' })}
|
||||
name={maximumIterationsLabel}
|
||||
description={t('agent.setting.maximumIterations.description', { ns: 'appDebug' })}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<FieldsetRoot className="flex items-center">
|
||||
<FieldsetLegend className="sr-only">{maximumIterationsLabel}</FieldsetLegend>
|
||||
<Slider
|
||||
className="mr-3 w-[156px]"
|
||||
min={maxIterationsMin}
|
||||
@ -111,10 +114,11 @@ const AgentSetting: FC<Props> = ({
|
||||
max_iteration: value,
|
||||
})
|
||||
}}
|
||||
aria-label={t('agent.setting.maximumIterations.name', { ns: 'appDebug' })}
|
||||
aria-label={maximumIterationsLabel}
|
||||
/>
|
||||
|
||||
<input
|
||||
aria-label={maximumIterationsLabel}
|
||||
type="number"
|
||||
min={maxIterationsMin}
|
||||
max={MAX_ITERATIONS_NUM}
|
||||
@ -134,7 +138,7 @@ const AgentSetting: FC<Props> = ({
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FieldsetRoot>
|
||||
</ItemPanel>
|
||||
|
||||
{!isFunctionCall && (
|
||||
|
||||
@ -8,7 +8,7 @@ import type {
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { FieldItem, FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio'
|
||||
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
|
||||
@ -161,70 +161,74 @@ const FollowUpSettingModal = ({
|
||||
<FieldsetLegend className="mb-1.5 py-0 system-sm-semibold-uppercase text-text-secondary">
|
||||
{t('feature.suggestedQuestionsAfterAnswer.modal.promptLabel', { ns: 'appDebug' })}
|
||||
</FieldsetLegend>
|
||||
<RadioRoot
|
||||
value={PROMPT_MODE.default}
|
||||
variant="unstyled"
|
||||
nativeButton
|
||||
render={<button type="button" />}
|
||||
className={cn(
|
||||
'w-full rounded-xl border p-4 text-left transition-colors',
|
||||
promptMode === PROMPT_MODE.default
|
||||
? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg'
|
||||
: 'border-components-option-card-option-border bg-components-option-card-option-bg hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="system-sm-semibold text-text-primary">
|
||||
{t('feature.suggestedQuestionsAfterAnswer.modal.defaultPromptOption', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">
|
||||
{t('feature.suggestedQuestionsAfterAnswer.modal.defaultPromptOptionDescription', { ns: 'appDebug' })}
|
||||
<FieldItem>
|
||||
<RadioRoot
|
||||
value={PROMPT_MODE.default}
|
||||
variant="unstyled"
|
||||
nativeButton
|
||||
render={<button type="button" />}
|
||||
className={cn(
|
||||
'w-full rounded-xl border p-4 text-left transition-colors',
|
||||
promptMode === PROMPT_MODE.default
|
||||
? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg'
|
||||
: 'border-components-option-card-option-border bg-components-option-card-option-bg hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="system-sm-semibold text-text-primary">
|
||||
{t('feature.suggestedQuestionsAfterAnswer.modal.defaultPromptOption', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">
|
||||
{t('feature.suggestedQuestionsAfterAnswer.modal.defaultPromptOptionDescription', { ns: 'appDebug' })}
|
||||
</div>
|
||||
</div>
|
||||
<RadioControl aria-hidden="true" />
|
||||
</div>
|
||||
<RadioControl aria-hidden="true" />
|
||||
</div>
|
||||
{promptMode === PROMPT_MODE.default && (
|
||||
<div className="mt-3 rounded-lg border border-components-input-border-active bg-components-input-bg-normal px-3 py-2">
|
||||
<div className="system-sm-regular wrap-break-word whitespace-pre-wrap text-text-secondary">
|
||||
{DEFAULT_FOLLOW_UP_PROMPT}
|
||||
{promptMode === PROMPT_MODE.default && (
|
||||
<div className="mt-3 rounded-lg border border-components-input-border-active bg-components-input-bg-normal px-3 py-2">
|
||||
<div className="system-sm-regular wrap-break-word whitespace-pre-wrap text-text-secondary">
|
||||
{DEFAULT_FOLLOW_UP_PROMPT}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</RadioRoot>
|
||||
</FieldItem>
|
||||
<FieldItem>
|
||||
<RadioRoot
|
||||
value={PROMPT_MODE.custom}
|
||||
variant="unstyled"
|
||||
nativeButton
|
||||
render={<button type="button" />}
|
||||
className={cn(
|
||||
'w-full rounded-xl border p-4 text-left transition-colors',
|
||||
promptMode === PROMPT_MODE.custom
|
||||
? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg'
|
||||
: 'border-components-option-card-option-border bg-components-option-card-option-bg hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="system-sm-semibold text-text-primary">
|
||||
{t('feature.suggestedQuestionsAfterAnswer.modal.customPromptOption', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">
|
||||
{t('feature.suggestedQuestionsAfterAnswer.modal.customPromptOptionDescription', { ns: 'appDebug' })}
|
||||
</div>
|
||||
</div>
|
||||
<RadioControl aria-hidden="true" />
|
||||
</div>
|
||||
)}
|
||||
</RadioRoot>
|
||||
<RadioRoot
|
||||
value={PROMPT_MODE.custom}
|
||||
variant="unstyled"
|
||||
nativeButton
|
||||
render={<button type="button" />}
|
||||
className={cn(
|
||||
'w-full rounded-xl border p-4 text-left transition-colors',
|
||||
promptMode === PROMPT_MODE.custom
|
||||
? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg'
|
||||
: 'border-components-option-card-option-border bg-components-option-card-option-bg hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="system-sm-semibold text-text-primary">
|
||||
{t('feature.suggestedQuestionsAfterAnswer.modal.customPromptOption', { ns: 'appDebug' })}
|
||||
</div>
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">
|
||||
{t('feature.suggestedQuestionsAfterAnswer.modal.customPromptOptionDescription', { ns: 'appDebug' })}
|
||||
</div>
|
||||
</div>
|
||||
<RadioControl aria-hidden="true" />
|
||||
</div>
|
||||
{promptMode === PROMPT_MODE.custom && (
|
||||
<Textarea
|
||||
className="mt-3 min-h-32 resize-y border-components-input-border-active bg-components-input-bg-normal"
|
||||
value={prompt}
|
||||
onChange={e => setPrompt(e.target.value)}
|
||||
maxLength={CUSTOM_FOLLOW_UP_PROMPT_MAX_LENGTH}
|
||||
placeholder={t('feature.suggestedQuestionsAfterAnswer.modal.promptPlaceholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
)}
|
||||
</RadioRoot>
|
||||
{promptMode === PROMPT_MODE.custom && (
|
||||
<Textarea
|
||||
className="mt-3 min-h-32 resize-y border-components-input-border-active bg-components-input-bg-normal"
|
||||
value={prompt}
|
||||
onChange={e => setPrompt(e.target.value)}
|
||||
maxLength={CUSTOM_FOLLOW_UP_PROMPT_MAX_LENGTH}
|
||||
placeholder={t('feature.suggestedQuestionsAfterAnswer.modal.promptPlaceholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
)}
|
||||
</RadioRoot>
|
||||
</FieldItem>
|
||||
</FieldsetRoot>
|
||||
</FieldRoot>
|
||||
</div>
|
||||
|
||||
@ -22,7 +22,7 @@ describe('NumberInputField', () => {
|
||||
|
||||
it('should render current number value', () => {
|
||||
render(<NumberInputField label="Count" />)
|
||||
expect(screen.getByRole('textbox')).toHaveValue('2')
|
||||
expect(screen.getByLabelText('Count')).toHaveValue('2')
|
||||
})
|
||||
|
||||
it('should update value when users click increment', () => {
|
||||
@ -33,14 +33,14 @@ describe('NumberInputField', () => {
|
||||
|
||||
it('should reset field value when users clear the input', () => {
|
||||
render(<NumberInputField label="Count" />)
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: '' } })
|
||||
fireEvent.change(screen.getByLabelText('Count'), { target: { value: '' } })
|
||||
expect(mockField.handleChange).toHaveBeenCalledWith(0)
|
||||
})
|
||||
|
||||
it('should clamp out-of-range edits before updating field state', () => {
|
||||
render(<NumberInputField label="Count" min={0} max={10} />)
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: '12' } })
|
||||
fireEvent.change(screen.getByLabelText('Count'), { target: { value: '12' } })
|
||||
|
||||
expect(mockField.handleChange).toHaveBeenLastCalledWith(10)
|
||||
})
|
||||
|
||||
@ -15,14 +15,16 @@ vi.mock('../../..', () => ({
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/input-number-with-slider', () => ({
|
||||
default: ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
}) => (
|
||||
<button onClick={() => onChange(value + 1)}>
|
||||
{`slider-value-${value}`}
|
||||
{`${label}-slider-value-${value}`}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
@ -40,7 +42,7 @@ describe('NumberSliderField', () => {
|
||||
|
||||
it('should update value when users interact with slider', () => {
|
||||
render(<NumberSliderField label="Threshold" />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'slider-value-2' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Threshold-slider-value-2' }))
|
||||
expect(mockField.handleChange).toHaveBeenCalledWith(3)
|
||||
})
|
||||
})
|
||||
|
||||
@ -56,7 +56,6 @@ const NumberInputField = ({
|
||||
{...(labelOptions ?? {})}
|
||||
/>
|
||||
<NumberField
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
min={min}
|
||||
@ -69,6 +68,7 @@ const NumberInputField = ({
|
||||
>
|
||||
<NumberFieldGroup size={size}>
|
||||
<NumberFieldInput
|
||||
id={field.name}
|
||||
{...inputProps}
|
||||
size={size}
|
||||
className={inputClassName}
|
||||
|
||||
@ -36,6 +36,7 @@ const NumberSliderField = ({
|
||||
)}
|
||||
</div>
|
||||
<InputNumberWithSlider
|
||||
label={label}
|
||||
value={field.state.value}
|
||||
onChange={value => field.handleChange(value)}
|
||||
{...InputNumberWithSliderProps}
|
||||
|
||||
@ -29,6 +29,12 @@ const FieldHarness = ({ config, initialData = {} }: FieldHarnessProps) => {
|
||||
return <Component form={form} />
|
||||
}
|
||||
|
||||
const getVisibleText = (text: string) => {
|
||||
const element = screen.getAllByText(text).find(element => !element.classList.contains('sr-only'))
|
||||
expect(element).toBeDefined()
|
||||
return element!
|
||||
}
|
||||
|
||||
describe('InputField', () => {
|
||||
it('should render text input field by default', () => {
|
||||
render(<FieldHarness config={createConfig({ label: 'Prompt' })} initialData={{ fieldA: '' }} />)
|
||||
@ -51,7 +57,7 @@ describe('InputField', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Temperature')).toBeInTheDocument()
|
||||
expect(getVisibleText('Temperature')).toBeInTheDocument()
|
||||
expect(screen.getByText('Control randomness')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -54,6 +54,12 @@ const NodePanelWrapper = ({ children }: { children: ReactNode }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const getVisibleText = (text: string) => {
|
||||
const element = screen.getAllByText(text).find(element => !element.classList.contains('sr-only'))
|
||||
expect(element).toBeDefined()
|
||||
return element!
|
||||
}
|
||||
|
||||
describe('NodePanelField', () => {
|
||||
it('should render text input field', () => {
|
||||
render(<FieldHarness config={createConfig({ label: 'Node Name' })} initialData={{ fieldA: '' }} />)
|
||||
@ -130,7 +136,7 @@ describe('NodePanelField', () => {
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
const { unmount } = render(<FieldHarness config={scenario.config} initialData={scenario.initialData} />)
|
||||
expect(screen.getByText(scenario.config.label)).toBeInTheDocument()
|
||||
expect(getVisibleText(scenario.config.label)).toBeInTheDocument()
|
||||
unmount()
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,9 @@ describe('ParamItem Slider onChange', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const getSlider = () => screen.getByLabelText('Test Param')
|
||||
const getSlider = () => screen.getByLabelText('Test Param', {
|
||||
selector: 'input[type="range"]',
|
||||
})
|
||||
|
||||
it('should divide slider value by 100 when max < 5', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
@ -17,13 +17,15 @@ describe('ParamItem', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const getSlider = () => screen.getByLabelText('Test Param')
|
||||
const getSlider = () => screen.getByLabelText('Test Param', {
|
||||
selector: 'input[type="range"]',
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the parameter name', () => {
|
||||
render(<ParamItem {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('Test Param')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Param', { selector: 'span' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render a tooltip trigger by default', () => {
|
||||
|
||||
@ -14,13 +14,15 @@ describe('ScoreThresholdItem', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const getSlider = () => screen.getByLabelText('appDebug.datasetConfig.score_threshold')
|
||||
const getSlider = () => screen.getByLabelText('appDebug.datasetConfig.score_threshold', {
|
||||
selector: 'input[type="range"]',
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the translated parameter name', () => {
|
||||
render(<ScoreThresholdItem {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('appDebug.datasetConfig.score_threshold')).toBeInTheDocument()
|
||||
expect(screen.getByText('appDebug.datasetConfig.score_threshold', { selector: 'span' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tooltip trigger', () => {
|
||||
|
||||
@ -19,13 +19,15 @@ describe('TopKItem', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const getSlider = () => screen.getByLabelText('appDebug.datasetConfig.top_k')
|
||||
const getSlider = () => screen.getByLabelText('appDebug.datasetConfig.top_k', {
|
||||
selector: 'input[type="range"]',
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the translated parameter name', () => {
|
||||
render(<TopKItem {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('appDebug.datasetConfig.top_k')).toBeInTheDocument()
|
||||
expect(screen.getByText('appDebug.datasetConfig.top_k', { selector: 'span' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tooltip trigger', () => {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldControls,
|
||||
@ -30,7 +31,8 @@ type Props = {
|
||||
|
||||
const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1, min = 0, max, value, enable, onChange, hasSwitch, onSwitchChange }) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<FieldsetRoot className={className}>
|
||||
<FieldsetLegend className="sr-only">{name}</FieldsetLegend>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex h-6 items-center">
|
||||
{hasSwitch && (
|
||||
@ -62,7 +64,7 @@ const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1,
|
||||
onValueChange={nextValue => onChange(id, nextValue ?? min)}
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput className="w-[72px]" />
|
||||
<NumberFieldInput aria-label={name} className="w-18" />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
@ -82,7 +84,7 @@ const ParamItem: FC<Props> = ({ className, id, name, noTooltip, tip, step = 0.1,
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FieldsetRoot>
|
||||
)
|
||||
}
|
||||
export default ParamItem
|
||||
|
||||
@ -72,12 +72,14 @@ export const DelimiterInput: FC<InputProps & { tooltip?: string }> = ({ tooltip,
|
||||
}
|
||||
|
||||
type CompoundNumberInputProps = Omit<NumberFieldRootProps, 'children' | 'className' | 'onValueChange'> & Omit<NumberFieldInputProps, 'children' | 'size' | 'onChange'> & {
|
||||
label: string
|
||||
unit?: ReactNode
|
||||
size?: NumberFieldSize
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
function CompoundNumberInput({
|
||||
label,
|
||||
onChange,
|
||||
unit,
|
||||
size = 'large',
|
||||
@ -104,6 +106,7 @@ function CompoundNumberInput({
|
||||
<NumberFieldGroup size={size}>
|
||||
<NumberFieldInput
|
||||
{...inputProps}
|
||||
aria-label={label}
|
||||
size={size}
|
||||
className={className}
|
||||
onBlur={onBlur}
|
||||
@ -122,18 +125,22 @@ function CompoundNumberInput({
|
||||
)
|
||||
}
|
||||
|
||||
export const MaxLengthInput: FC<CompoundNumberInputProps> = (props) => {
|
||||
type LabeledCompoundNumberInputProps = Omit<CompoundNumberInputProps, 'label'>
|
||||
|
||||
export const MaxLengthInput: FC<LabeledCompoundNumberInputProps> = (props) => {
|
||||
const maxValue = env.NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH
|
||||
|
||||
const { t } = useTranslation()
|
||||
const label = t('stepTwo.maxLength', { ns: 'datasetCreation' })
|
||||
return (
|
||||
<FormField label={(
|
||||
<div className="mb-1 system-sm-semibold">
|
||||
{t('stepTwo.maxLength', { ns: 'datasetCreation' })}
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<CompoundNumberInput
|
||||
label={label}
|
||||
size="large"
|
||||
placeholder={`≤ ${maxValue}`}
|
||||
max={maxValue}
|
||||
@ -144,12 +151,13 @@ export const MaxLengthInput: FC<CompoundNumberInputProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const OverlapInput: FC<CompoundNumberInputProps> = (props) => {
|
||||
export const OverlapInput: FC<LabeledCompoundNumberInputProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const label = t('stepTwo.overlap', { ns: 'datasetCreation' })
|
||||
return (
|
||||
<FormField label={(
|
||||
<div className="mb-1 flex items-center">
|
||||
<span className="system-sm-semibold">{t('stepTwo.overlap', { ns: 'datasetCreation' })}</span>
|
||||
<span className="system-sm-semibold">{label}</span>
|
||||
<Infotip aria-label={t('stepTwo.overlapTip', { ns: 'datasetCreation' })} popupClassName="max-w-[200px]">
|
||||
{t('stepTwo.overlapTip', { ns: 'datasetCreation' })}
|
||||
</Infotip>
|
||||
@ -157,8 +165,9 @@ export const OverlapInput: FC<CompoundNumberInputProps> = (props) => {
|
||||
)}
|
||||
>
|
||||
<CompoundNumberInput
|
||||
label={label}
|
||||
size="large"
|
||||
placeholder={t('stepTwo.overlap', { ns: 'datasetCreation' }) || ''}
|
||||
placeholder={label || ''}
|
||||
min={1}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@ -78,6 +78,12 @@ const renderComponent = (props: Partial<React.ComponentProps<typeof ExternalKnow
|
||||
return render(<ExternalKnowledgeBaseCreate {...defaultProps} {...props} />)
|
||||
}
|
||||
|
||||
const getVisibleText = (text: string) => {
|
||||
const element = screen.getAllByText(text).find(element => !element.classList.contains('sr-only'))
|
||||
expect(element).toBeDefined()
|
||||
return element!
|
||||
}
|
||||
|
||||
describe('ExternalKnowledgeBaseCreate', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -878,8 +884,8 @@ describe('ExternalKnowledgeBaseCreate', () => {
|
||||
expect(screen.getByText('dataset.retrievalSettings'))!.toBeInTheDocument()
|
||||
// Should show Top K and Score Threshold labels
|
||||
// Should show Top K and Score Threshold labels
|
||||
expect(screen.getByText('appDebug.datasetConfig.top_k'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('appDebug.datasetConfig.score_threshold'))!.toBeInTheDocument()
|
||||
expect(getVisibleText('appDebug.datasetConfig.top_k')).toBeInTheDocument()
|
||||
expect(getVisibleText('appDebug.datasetConfig.score_threshold')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -92,6 +92,21 @@ describe('WrappedDatePicker', () => {
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'common.operation.clear' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should include the field label in the trigger accessible name', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(<WrappedDatePicker label="Metadata field" onChange={handleChange} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Metadata field: dataset.metadata.chooseTime' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should include the field label in the clear button accessible name', () => {
|
||||
const handleChange = vi.fn()
|
||||
const timestamp = 1609459200
|
||||
render(<WrappedDatePicker label="Metadata field" value={timestamp} onChange={handleChange} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Metadata field: common.operation.clear' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props', () => {
|
||||
|
||||
@ -13,11 +13,13 @@ import useTimestamp from '@/hooks/use-timestamp'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
label?: string
|
||||
value?: number
|
||||
onChange: (date: number | null) => void
|
||||
}
|
||||
const WrappedDatePicker = ({
|
||||
className,
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
@ -37,11 +39,13 @@ const WrappedDatePicker = ({
|
||||
}: TriggerProps) => {
|
||||
const hasValue = Boolean(value)
|
||||
const triggerText = value ? formatTimestamp(value, t('metadata.dateTimeFormat', { ns: 'datasetDocuments' })) : t('metadata.chooseTime', { ns: 'dataset' })
|
||||
const clearLabel = t('operation.clear', { ns: 'common' })
|
||||
|
||||
return (
|
||||
<div className={cn('group flex items-center rounded-md bg-components-input-bg-normal', className)}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label ? `${label}: ${triggerText}` : undefined}
|
||||
className="flex min-w-0 grow items-center border-none bg-transparent p-0 text-left focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={handleClickTrigger}
|
||||
>
|
||||
@ -65,7 +69,7 @@ const WrappedDatePicker = ({
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
aria-label={label ? `${label}: ${clearLabel}` : clearLabel}
|
||||
className={cn(
|
||||
'hidden size-4 cursor-pointer rounded-full border-none bg-transparent p-0 text-text-quaternary group-hover:block hover:text-components-input-text-filled focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden',
|
||||
)}
|
||||
@ -80,7 +84,7 @@ const WrappedDatePicker = ({
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}, [className, value, formatTimestamp, t, handleDateChange])
|
||||
}, [className, label, value, formatTimestamp, t, handleDateChange])
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
|
||||
@ -7,14 +7,15 @@ type DatePickerProps = {
|
||||
value: number | null
|
||||
onChange: (value: number) => void
|
||||
className?: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
// Mock the base date-picker component
|
||||
vi.mock('../../base/date-picker', () => ({
|
||||
default: ({ value, onChange, className }: DatePickerProps) => (
|
||||
<div data-testid="date-picker" className={className} onClick={() => onChange(Date.now())}>
|
||||
default: ({ value, onChange, className, label }: DatePickerProps) => (
|
||||
<button type="button" aria-label={label} data-testid="date-picker" className={className} onClick={() => onChange(Date.now())}>
|
||||
{value || 'Pick date'}
|
||||
</div>
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
@ -23,7 +24,7 @@ describe('InputCombined', () => {
|
||||
it('should render without crashing', () => {
|
||||
const handleChange = vi.fn()
|
||||
const { container } = render(
|
||||
<InputCombined type={DataType.string} value="" onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value="" onChange={handleChange} />,
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
@ -31,7 +32,7 @@ describe('InputCombined', () => {
|
||||
it('should render text input for string type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.string} value="test" onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value="test" onChange={handleChange} />,
|
||||
)
|
||||
const input = screen.getByDisplayValue('test')
|
||||
expect(input).toBeInTheDocument()
|
||||
@ -41,9 +42,9 @@ describe('InputCombined', () => {
|
||||
it('should render number input for number type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.number} value={42} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={42} onChange={handleChange} />,
|
||||
)
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByRole('textbox', { name: 'Metadata field' })
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input).toHaveValue('42')
|
||||
})
|
||||
@ -51,7 +52,7 @@ describe('InputCombined', () => {
|
||||
it('should render date picker for time type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.time} value={Date.now()} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.time} value={Date.now()} onChange={handleChange} />,
|
||||
)
|
||||
expect(screen.getByTestId('date-picker')).toBeInTheDocument()
|
||||
})
|
||||
@ -61,7 +62,7 @@ describe('InputCombined', () => {
|
||||
it('should call onChange with input value for string type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.string} value="" onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value="" onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -73,7 +74,7 @@ describe('InputCombined', () => {
|
||||
it('should display current value for string type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.string} value="existing value" onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value="existing value" onChange={handleChange} />,
|
||||
)
|
||||
|
||||
expect(screen.getByDisplayValue('existing value')).toBeInTheDocument()
|
||||
@ -82,7 +83,7 @@ describe('InputCombined', () => {
|
||||
it('should apply readOnly prop to string input', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.string} value="test" onChange={handleChange} readOnly />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value="test" onChange={handleChange} readOnly />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -94,7 +95,7 @@ describe('InputCombined', () => {
|
||||
it('should call onChange with number value for number type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.number} value={0} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={0} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -106,7 +107,7 @@ describe('InputCombined', () => {
|
||||
it('should reset cleared number input to 0', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.number} value={42} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={42} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -118,7 +119,7 @@ describe('InputCombined', () => {
|
||||
it('should display current value for number type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.number} value={999} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={999} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('textbox')).toHaveValue('999')
|
||||
@ -127,7 +128,7 @@ describe('InputCombined', () => {
|
||||
it('should apply readOnly prop to number input', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.number} value={42} onChange={handleChange} readOnly />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={42} onChange={handleChange} readOnly />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -139,16 +140,25 @@ describe('InputCombined', () => {
|
||||
it('should render date picker for time type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.time} value={1234567890} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.time} value={1234567890} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('date-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should label the date picker trigger with the metadata field name', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined label="Metadata field" type={DataType.time} value={1234567890} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Metadata field' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onChange when date is selected', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.time} value={null} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.time} value={null} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('date-picker'))
|
||||
@ -161,6 +171,7 @@ describe('InputCombined', () => {
|
||||
const handleChange = vi.fn()
|
||||
const { container } = render(
|
||||
<InputCombined
|
||||
label="Metadata field"
|
||||
type={DataType.string}
|
||||
value=""
|
||||
onChange={handleChange}
|
||||
@ -176,7 +187,7 @@ describe('InputCombined', () => {
|
||||
it('should handle null value for string type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.string} value={null} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value={null} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -186,7 +197,7 @@ describe('InputCombined', () => {
|
||||
it('should handle undefined value for string type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.string} value={undefined as unknown as string} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value={undefined as unknown as string} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -196,7 +207,7 @@ describe('InputCombined', () => {
|
||||
it('should handle null value for number type', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.number} value={null} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={null} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -208,7 +219,7 @@ describe('InputCombined', () => {
|
||||
it('should have correct base styling for string input', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.string} value="" onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value="" onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -218,7 +229,7 @@ describe('InputCombined', () => {
|
||||
it('should have correct styling for number input', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.number} value={0} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={0} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -230,7 +241,7 @@ describe('InputCombined', () => {
|
||||
it('should handle empty string value', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.string} value="" onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value="" onChange={handleChange} />,
|
||||
)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -240,7 +251,7 @@ describe('InputCombined', () => {
|
||||
it('should handle zero value for number', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.number} value={0} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={0} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('textbox')).toHaveValue('0')
|
||||
@ -249,7 +260,7 @@ describe('InputCombined', () => {
|
||||
it('should handle negative number', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.number} value={-100} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={-100} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('textbox')).toHaveValue('-100')
|
||||
@ -258,7 +269,7 @@ describe('InputCombined', () => {
|
||||
it('should handle special characters in string', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(
|
||||
<InputCombined type={DataType.string} value={'<script>alert("xss")</script>'} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value={'<script>alert("xss")</script>'} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
expect(screen.getByDisplayValue('<script>alert("xss")</script>')).toBeInTheDocument()
|
||||
@ -267,13 +278,13 @@ describe('InputCombined', () => {
|
||||
it('should handle switching between types', () => {
|
||||
const handleChange = vi.fn()
|
||||
const { rerender } = render(
|
||||
<InputCombined type={DataType.string} value="test" onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.string} value="test" onChange={handleChange} />,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<InputCombined type={DataType.number} value={42} onChange={handleChange} />,
|
||||
<InputCombined label="Metadata field" type={DataType.number} value={42} onChange={handleChange} />,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
|
||||
@ -24,6 +24,7 @@ const AddRow: FC<Props> = ({
|
||||
<div className={cn('flex h-6 items-center space-x-0.5', className)}>
|
||||
<Label text={payload.name} />
|
||||
<InputCombined
|
||||
label={payload.name}
|
||||
type={payload.type}
|
||||
value={payload.value}
|
||||
onChange={value => onChange({ ...payload, value })}
|
||||
|
||||
@ -38,6 +38,7 @@ const EditMetadatabatchItem: FC<Props> = ({
|
||||
)
|
||||
: (
|
||||
<InputCombined
|
||||
label={payload.name}
|
||||
type={payload.type}
|
||||
value={payload.value}
|
||||
onChange={v => onChange({ ...payload, value: v as string })}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldControls,
|
||||
@ -10,12 +11,12 @@ import {
|
||||
NumberFieldInput,
|
||||
} from '@langgenius/dify-ui/number-field'
|
||||
import * as React from 'react'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Datepicker from '../base/date-picker'
|
||||
import { DataType } from '../types'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
label: string
|
||||
type: DataType
|
||||
value: any
|
||||
onChange: (value: any) => void
|
||||
@ -24,6 +25,7 @@ type Props = {
|
||||
|
||||
const InputCombined: FC<Props> = ({
|
||||
className: configClassName,
|
||||
label,
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
@ -33,6 +35,7 @@ const InputCombined: FC<Props> = ({
|
||||
if (type === DataType.time) {
|
||||
return (
|
||||
<Datepicker
|
||||
label={label}
|
||||
className={className}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
@ -51,6 +54,7 @@ const InputCombined: FC<Props> = ({
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput
|
||||
aria-label={label}
|
||||
className={cn(className, 'rounded-l-md')}
|
||||
/>
|
||||
<NumberFieldControls className="overflow-hidden">
|
||||
@ -64,8 +68,8 @@ const InputCombined: FC<Props> = ({
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
wrapperClassName={configClassName}
|
||||
className={cn(className, 'rounded-md')}
|
||||
aria-label={label}
|
||||
className={cn(configClassName, className, 'rounded-md')}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
readOnly={readOnly}
|
||||
|
||||
@ -5,6 +5,7 @@ import { DataType } from '../../types'
|
||||
import InfoGroup from '../info-group'
|
||||
|
||||
type InputCombinedProps = {
|
||||
label: string
|
||||
value: string | number | null
|
||||
onChange: (value: string | number) => void
|
||||
type: DataType
|
||||
@ -43,9 +44,9 @@ vi.mock('@/hooks/use-timestamp', () => ({
|
||||
|
||||
// Mock InputCombined
|
||||
vi.mock('../../edit-metadata-batch/input-combined', () => ({
|
||||
default: ({ value, onChange, type }: InputCombinedProps) => (
|
||||
default: ({ label, value, onChange, type }: InputCombinedProps) => (
|
||||
<input
|
||||
aria-label={`Metadata ${type} value`}
|
||||
aria-label={label}
|
||||
data-type={type}
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
|
||||
@ -91,6 +91,7 @@ const InfoGroup: FC<Props> = ({
|
||||
<div className="flex items-center space-x-0.5">
|
||||
<InputCombined
|
||||
className="h-6"
|
||||
label={item.name}
|
||||
type={item.type}
|
||||
value={item.value}
|
||||
onChange={value => onChange?.({ ...item, value })}
|
||||
|
||||
@ -18,12 +18,12 @@ describe('KeyWordNumber', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<KeyWordNumber {...defaultProps} />)
|
||||
expect(screen.getByText(/form\.numberOfKeywords/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/form\.numberOfKeywords/, { selector: '.truncate' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render label text', () => {
|
||||
render(<KeyWordNumber {...defaultProps} />)
|
||||
expect(screen.getByText(/form\.numberOfKeywords/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/form\.numberOfKeywords/, { selector: '.truncate' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render infotip with question icon', () => {
|
||||
@ -48,7 +48,7 @@ describe('KeyWordNumber', () => {
|
||||
describe('Props', () => {
|
||||
it('should display correct keywordNumber value in input', () => {
|
||||
render(<KeyWordNumber {...defaultProps} keywordNumber={25} />)
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByRole('textbox', { name: 'datasetSettings.form.numberOfKeywords' })
|
||||
expect(input).toHaveValue('25')
|
||||
})
|
||||
|
||||
@ -84,7 +84,7 @@ describe('KeyWordNumber', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(<KeyWordNumber {...defaultProps} onKeywordNumberChange={handleChange} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByRole('textbox', { name: 'datasetSettings.form.numberOfKeywords' })
|
||||
fireEvent.change(input, { target: { value: '30' } })
|
||||
|
||||
expect(handleChange).toHaveBeenCalled()
|
||||
@ -94,7 +94,7 @@ describe('KeyWordNumber', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(<KeyWordNumber {...defaultProps} onKeywordNumberChange={handleChange} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByRole('textbox', { name: 'datasetSettings.form.numberOfKeywords' })
|
||||
fireEvent.change(input, { target: { value: '' } })
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith(0)
|
||||
@ -126,13 +126,13 @@ describe('KeyWordNumber', () => {
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle minimum value (0)', () => {
|
||||
render(<KeyWordNumber {...defaultProps} keywordNumber={0} />)
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByRole('textbox', { name: 'datasetSettings.form.numberOfKeywords' })
|
||||
expect(input).toHaveValue('0')
|
||||
})
|
||||
|
||||
it('should handle maximum value (50)', () => {
|
||||
render(<KeyWordNumber {...defaultProps} keywordNumber={50} />)
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByRole('textbox', { name: 'datasetSettings.form.numberOfKeywords' })
|
||||
expect(input).toHaveValue('50')
|
||||
})
|
||||
|
||||
@ -151,7 +151,7 @@ describe('KeyWordNumber', () => {
|
||||
const handleChange = vi.fn()
|
||||
render(<KeyWordNumber {...defaultProps} onKeywordNumberChange={handleChange} />)
|
||||
|
||||
const input = screen.getByRole('textbox')
|
||||
const input = screen.getByRole('textbox', { name: 'datasetSettings.form.numberOfKeywords' })
|
||||
|
||||
// Simulate rapid changes via input with different values
|
||||
fireEvent.change(input, { target: { value: '15' } })
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldControls,
|
||||
@ -25,22 +26,24 @@ const KeyWordNumber = ({
|
||||
onKeywordNumberChange,
|
||||
}: KeyWordNumberProps) => {
|
||||
const { t } = useTranslation()
|
||||
const label = t('form.numberOfKeywords', { ns: 'datasetSettings' })
|
||||
|
||||
const handleInputChange = useCallback((value: number | null) => {
|
||||
onKeywordNumberChange(value ?? MIN_KEYWORD_NUMBER)
|
||||
}, [onKeywordNumberChange])
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<FieldsetRoot className="flex items-center gap-x-1">
|
||||
<FieldsetLegend className="sr-only">{label}</FieldsetLegend>
|
||||
<div className="flex grow items-center gap-x-0.5">
|
||||
<div className="truncate system-xs-medium text-text-secondary">
|
||||
{t('form.numberOfKeywords', { ns: 'datasetSettings' })}
|
||||
{label}
|
||||
</div>
|
||||
<Infotip
|
||||
aria-label={t('form.numberOfKeywords', { ns: 'datasetSettings' })}
|
||||
aria-label={label}
|
||||
className="size-3.5"
|
||||
>
|
||||
{t('form.numberOfKeywords', { ns: 'datasetSettings' })}
|
||||
{label}
|
||||
</Infotip>
|
||||
</div>
|
||||
<Slider
|
||||
@ -49,7 +52,7 @@ const KeyWordNumber = ({
|
||||
min={MIN_KEYWORD_NUMBER}
|
||||
max={MAX_KEYWORD_NUMBER}
|
||||
onValueChange={onKeywordNumberChange}
|
||||
aria-label={t('form.numberOfKeywords', { ns: 'datasetSettings' })}
|
||||
aria-label={label}
|
||||
/>
|
||||
<NumberField
|
||||
className="w-[74px] shrink-0"
|
||||
@ -59,14 +62,14 @@ const KeyWordNumber = ({
|
||||
onValueChange={handleInputChange}
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput className="w-12 flex-none px-2 text-center" />
|
||||
<NumberFieldInput aria-label={label} className="w-12 flex-none px-2 text-center" />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</div>
|
||||
</FieldsetRoot>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -72,6 +72,7 @@ vi.mock('@langgenius/dify-ui/select', async (importOriginal) => {
|
||||
),
|
||||
SelectGroup: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectLabel: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectGroupLabel: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({
|
||||
children,
|
||||
value,
|
||||
|
||||
@ -4,9 +4,9 @@ import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectGroupLabel,
|
||||
SelectItem,
|
||||
SelectItemText,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@ -50,9 +50,9 @@ export const WorkplaceSelectorContent = memo(({
|
||||
return (
|
||||
<SelectContent popupClassName={popupClassName}>
|
||||
<SelectGroup>
|
||||
<SelectLabel>
|
||||
<SelectGroupLabel>
|
||||
{t('userProfile.workspace', { ns: 'common' })}
|
||||
</SelectLabel>
|
||||
</SelectGroupLabel>
|
||||
{workspaces.map(workspace => (
|
||||
<WorkplaceSelectorItem key={workspace.id} workspace={workspace} />
|
||||
))}
|
||||
|
||||
@ -17,7 +17,7 @@ import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import { Radio } from '@langgenius/dify-ui/radio'
|
||||
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import { AppSelector } from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
@ -293,11 +293,12 @@ function Form<
|
||||
? formSchema.default
|
||||
: value[variable]
|
||||
const selectedOption = filteredOptions.find(option => option.value === currentValue)
|
||||
const translatedLabel = label[language] || label.en_US
|
||||
|
||||
return (
|
||||
<div key={variable} className={cn(itemClassName, 'py-3')}>
|
||||
<div className={cn(fieldLabelClassName, 'flex items-center py-2 system-sm-semibold text-text-secondary')}>
|
||||
{label[language] || label.en_US}
|
||||
{translatedLabel}
|
||||
|
||||
{required && (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
@ -313,6 +314,7 @@ function Form<
|
||||
handleFormChange(variable, nextValue)
|
||||
}}
|
||||
>
|
||||
<SelectLabel className="sr-only">{translatedLabel}</SelectLabel>
|
||||
<SelectTrigger size="medium" className={cn(inputClassName)}>
|
||||
{selectedOption?.name ?? placeholder?.[language] ?? placeholder?.en_US}
|
||||
</SelectTrigger>
|
||||
|
||||
@ -1234,7 +1234,7 @@ describe('Form', () => {
|
||||
|
||||
expect(screen.getByText('API Key'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('Region'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('Model'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('combobox', { name: 'Model' }))!.toBeInTheDocument()
|
||||
expect(screen.getByText('Agree'))!.toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Enter your API key here'))!.toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Select region'))!.toBeInTheDocument()
|
||||
@ -1546,7 +1546,7 @@ describe('Form', () => {
|
||||
|
||||
expect(screen.getByText('API Key Fallback'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('Region Fallback'))!.toBeInTheDocument()
|
||||
expect(screen.getByText('Model Fallback'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('combobox', { name: 'Model Fallback' }))!.toBeInTheDocument()
|
||||
expect(screen.getByText('Agree Fallback'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ vi.mock('@langgenius/dify-ui/select', async (importOriginal) => {
|
||||
),
|
||||
SelectContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectLabel: () => null,
|
||||
SelectTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||
SelectValue: () => <div>SelectValue</div>,
|
||||
SelectItemText: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
||||
|
||||
@ -8,7 +8,7 @@ import { FieldItem, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import { Radio } from '@langgenius/dify-ui/radio'
|
||||
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger, SelectValue } from '@langgenius/dify-ui/select'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger, SelectValue } from '@langgenius/dify-ui/select'
|
||||
import { Slider } from '@langgenius/dify-ui/slider'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
@ -165,58 +165,90 @@ function ParameterItem({
|
||||
step = 10
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{numberInputWithSlide && (
|
||||
<Slider
|
||||
className="w-[120px]"
|
||||
value={renderValue as number}
|
||||
min={parameterRule.min}
|
||||
max={parameterRule.max}
|
||||
step={step}
|
||||
onValueChange={handleSlideChange}
|
||||
aria-label={sliderLabel}
|
||||
/>
|
||||
)}
|
||||
if (!numberInputWithSlide) {
|
||||
return (
|
||||
<input
|
||||
aria-label={sliderLabel}
|
||||
ref={numberInputRef}
|
||||
className="ml-4 block h-8 w-16 shrink-0 appearance-none rounded-lg bg-components-input-bg-normal pl-3 system-sm-regular text-components-input-text-filled outline-hidden"
|
||||
type="number"
|
||||
max={parameterRule.max}
|
||||
min={parameterRule.min}
|
||||
step={numberInputWithSlide ? step : +`0.${parameterRule.precision || 0}`}
|
||||
step={+`0.${parameterRule.precision || 0}`}
|
||||
onChange={handleNumberInputChange}
|
||||
onBlur={handleNumberInputBlur}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldsetRoot className="flex items-center">
|
||||
<FieldsetLegend className="sr-only">{sliderLabel}</FieldsetLegend>
|
||||
<Slider
|
||||
className="w-[120px]"
|
||||
value={renderValue as number}
|
||||
min={parameterRule.min}
|
||||
max={parameterRule.max}
|
||||
step={step}
|
||||
onValueChange={handleSlideChange}
|
||||
aria-label={sliderLabel}
|
||||
/>
|
||||
<input
|
||||
aria-label={sliderLabel}
|
||||
ref={numberInputRef}
|
||||
className="ml-4 block h-8 w-16 shrink-0 appearance-none rounded-lg bg-components-input-bg-normal pl-3 system-sm-regular text-components-input-text-filled outline-hidden"
|
||||
type="number"
|
||||
max={parameterRule.max}
|
||||
min={parameterRule.min}
|
||||
step={step}
|
||||
onChange={handleNumberInputChange}
|
||||
onBlur={handleNumberInputBlur}
|
||||
/>
|
||||
</FieldsetRoot>
|
||||
)
|
||||
}
|
||||
|
||||
if (parameterRule.type === 'float') {
|
||||
return (
|
||||
<>
|
||||
{numberInputWithSlide && (
|
||||
<Slider
|
||||
className="w-[120px]"
|
||||
value={renderValue as number}
|
||||
min={parameterRule.min}
|
||||
max={parameterRule.max}
|
||||
step={0.1}
|
||||
onValueChange={handleSlideChange}
|
||||
aria-label={sliderLabel}
|
||||
/>
|
||||
)}
|
||||
if (!numberInputWithSlide) {
|
||||
return (
|
||||
<input
|
||||
aria-label={sliderLabel}
|
||||
ref={numberInputRef}
|
||||
className="ml-4 block h-8 w-16 shrink-0 appearance-none rounded-lg bg-components-input-bg-normal pl-3 system-sm-regular text-components-input-text-filled outline-hidden"
|
||||
type="number"
|
||||
max={parameterRule.max}
|
||||
min={parameterRule.min}
|
||||
step={numberInputWithSlide ? 0.1 : +`0.${parameterRule.precision || 0}`}
|
||||
step={+`0.${parameterRule.precision || 0}`}
|
||||
onChange={handleNumberInputChange}
|
||||
onBlur={handleNumberInputBlur}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldsetRoot className="flex items-center">
|
||||
<FieldsetLegend className="sr-only">{sliderLabel}</FieldsetLegend>
|
||||
<Slider
|
||||
className="w-[120px]"
|
||||
value={renderValue as number}
|
||||
min={parameterRule.min}
|
||||
max={parameterRule.max}
|
||||
step={0.1}
|
||||
onValueChange={handleSlideChange}
|
||||
aria-label={sliderLabel}
|
||||
/>
|
||||
<input
|
||||
aria-label={sliderLabel}
|
||||
ref={numberInputRef}
|
||||
className="ml-4 block h-8 w-16 shrink-0 appearance-none rounded-lg bg-components-input-bg-normal pl-3 system-sm-regular text-components-input-text-filled outline-hidden"
|
||||
type="number"
|
||||
max={parameterRule.max}
|
||||
min={parameterRule.min}
|
||||
step={0.1}
|
||||
onChange={handleNumberInputChange}
|
||||
onBlur={handleNumberInputBlur}
|
||||
/>
|
||||
</FieldsetRoot>
|
||||
)
|
||||
}
|
||||
|
||||
@ -317,6 +349,7 @@ function ParameterItem({
|
||||
value={renderValue as string}
|
||||
onValueChange={v => handleInputChange(v ?? undefined)}
|
||||
>
|
||||
<SelectLabel className="sr-only">{sliderLabel}</SelectLabel>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
@ -38,6 +38,7 @@ vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
),
|
||||
SelectLabel: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
@ -149,7 +150,8 @@ describe('SelectPackage', () => {
|
||||
|
||||
const getSection = (label: string): HTMLElement => {
|
||||
const labelElement = screen.getByText(label)
|
||||
const section = labelElement.closest('label')?.nextElementSibling
|
||||
const labelContainer = labelElement.closest('label') ?? labelElement.parentElement
|
||||
const section = labelContainer?.parentElement
|
||||
if (!(section instanceof HTMLElement))
|
||||
throw new Error(`Missing section for ${label}`)
|
||||
return section
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
@ -79,9 +79,6 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
return (
|
||||
<>
|
||||
<FieldRoot name="version" className="gap-4 self-stretch">
|
||||
<FieldLabel className="flex w-full flex-col items-start justify-center p-0 text-text-secondary">
|
||||
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectVersion`, { ns: 'plugin' })}</span>
|
||||
</FieldLabel>
|
||||
<Select
|
||||
value={selectedVersionOption ? String(selectedVersionOption.value) : null}
|
||||
onValueChange={(value) => {
|
||||
@ -92,6 +89,9 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
onSelectVersion(selectedItem)
|
||||
}}
|
||||
>
|
||||
<SelectLabel className="flex w-full flex-col items-start justify-center p-0 text-text-secondary">
|
||||
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectVersion`, { ns: 'plugin' })}</span>
|
||||
</SelectLabel>
|
||||
<SelectTrigger className="h-9 text-components-input-text-filled">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="truncate">
|
||||
@ -122,9 +122,6 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
</Select>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="package" className="gap-4 self-stretch">
|
||||
<FieldLabel className="flex w-full flex-col items-start justify-center p-0 text-text-secondary">
|
||||
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectPackage`, { ns: 'plugin' })}</span>
|
||||
</FieldLabel>
|
||||
<Select
|
||||
value={selectedPackageOption ? String(selectedPackageOption.value) : null}
|
||||
readOnly={!selectedVersion}
|
||||
@ -136,6 +133,9 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
|
||||
onSelectPackage(selectedItem)
|
||||
}}
|
||||
>
|
||||
<SelectLabel className="flex w-full flex-col items-start justify-center p-0 text-text-secondary">
|
||||
<span className="system-sm-semibold">{t(`${i18nPrefix}.selectPackage`, { ns: 'plugin' })}</span>
|
||||
</SelectLabel>
|
||||
<SelectTrigger className="h-9 text-components-input-text-filled">
|
||||
{selectedPackageOption?.name ?? t(`${i18nPrefix}.selectPackagePlaceholder`, { ns: 'plugin' }) ?? ''}
|
||||
</SelectTrigger>
|
||||
|
||||
@ -145,8 +145,7 @@ describe('AgentStrategy', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByLabelText('Count')).toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox', { name: 'Count' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should skip text-number schemas when min is missing', () => {
|
||||
|
||||
@ -4,6 +4,7 @@ import type { NodeOutPutVar } from '../../../types'
|
||||
import type { ToolVarInputs } from '../../tool/types'
|
||||
import type { CredentialFormSchema, CredentialFormSchemaNumberInput, CredentialFormSchemaTextInput } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { PluginMeta } from '@/app/components/plugins/types'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import {
|
||||
NumberField,
|
||||
NumberFieldControls,
|
||||
@ -128,6 +129,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
|
||||
const defaultValue = schema.default ? Number.parseInt(schema.default) : 1
|
||||
const value = props.value[schema.variable] ?? defaultValue
|
||||
const label = renderI18nObject(def.label)
|
||||
const onChange = (value: number) => {
|
||||
props.onChange({ ...props.value, [schema.variable]: value })
|
||||
}
|
||||
@ -135,7 +137,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
<Field
|
||||
title={(
|
||||
<>
|
||||
{renderI18nObject(def.label)}
|
||||
{label}
|
||||
{' '}
|
||||
{def.required && <span className="text-red-500">*</span>}
|
||||
</>
|
||||
@ -144,14 +146,15 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
tooltip={def.tooltip && renderI18nObject(def.tooltip)}
|
||||
inline
|
||||
>
|
||||
<div className="flex w-[200px] items-center gap-3">
|
||||
<FieldsetRoot className="flex w-[200px] items-center gap-3">
|
||||
<FieldsetLegend className="sr-only">{label}</FieldsetLegend>
|
||||
<Slider
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full"
|
||||
min={def.min}
|
||||
max={def.max}
|
||||
aria-label={renderI18nObject(def.label)}
|
||||
aria-label={label}
|
||||
/>
|
||||
<NumberField
|
||||
value={value}
|
||||
@ -160,14 +163,14 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
|
||||
onValueChange={nextValue => onChange(nextValue ?? defaultValue)}
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput className="w-12" />
|
||||
<NumberFieldInput aria-label={label} className="w-12" />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
</NumberFieldControls>
|
||||
</NumberFieldGroup>
|
||||
</NumberField>
|
||||
</div>
|
||||
</FieldsetRoot>
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
@ -161,6 +161,7 @@ const FileUploadSetting: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
<InputNumberWithSlider
|
||||
label={t('variableConfig.maxNumberOfUploads', { ns: 'appDebug' })!}
|
||||
value={max_length}
|
||||
min={1}
|
||||
max={maxFileUploadLimit}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import { Slider } from '@langgenius/dify-ui/slider'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export type InputNumberWithSliderProps = {
|
||||
label: string
|
||||
value: number
|
||||
defaultValue?: number
|
||||
min?: number
|
||||
@ -13,14 +14,15 @@ export type InputNumberWithSliderProps = {
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const InputNumberWithSlider: FC<InputNumberWithSliderProps> = ({
|
||||
function InputNumberWithSlider({
|
||||
label,
|
||||
value,
|
||||
defaultValue = 0,
|
||||
min,
|
||||
max,
|
||||
readonly,
|
||||
onChange,
|
||||
}) => {
|
||||
}: InputNumberWithSliderProps) {
|
||||
const handleBlur = useCallback(() => {
|
||||
if (value === undefined || value === null || Number.isNaN(value)) {
|
||||
onChange(defaultValue)
|
||||
@ -39,29 +41,33 @@ const InputNumberWithSlider: FC<InputNumberWithSliderProps> = ({
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<div className="flex h-8 items-center justify-between space-x-2">
|
||||
<input
|
||||
value={value}
|
||||
className="block h-8 w-12 shrink-0 appearance-none rounded-lg bg-components-input-bg-normal pl-3 text-[13px] text-components-input-text-filled outline-hidden"
|
||||
type="number"
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<Slider
|
||||
className="grow"
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onValueChange={onChange}
|
||||
disabled={readonly}
|
||||
aria-label="Number input slider"
|
||||
/>
|
||||
</div>
|
||||
<FieldsetRoot>
|
||||
<FieldsetLegend className="sr-only">{label}</FieldsetLegend>
|
||||
<div className="flex h-8 items-center justify-between space-x-2">
|
||||
<input
|
||||
aria-label={label}
|
||||
value={value}
|
||||
className="block h-8 w-12 shrink-0 appearance-none rounded-lg bg-components-input-bg-normal pl-3 text-[13px] text-components-input-text-filled outline-hidden"
|
||||
type="number"
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<Slider
|
||||
className="grow"
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
onValueChange={onChange}
|
||||
disabled={readonly}
|
||||
aria-label={label}
|
||||
/>
|
||||
</div>
|
||||
</FieldsetRoot>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputNumberWithSlider)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { Memory } from '../../../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import { Slider } from '@langgenius/dify-ui/slider'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { produce } from 'immer'
|
||||
@ -67,6 +68,7 @@ const MemoryConfig: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const payload = config.data
|
||||
const windowSizeLabel = t(`${i18nPrefix}.windowSize`, { ns: 'workflow' })
|
||||
const handleMemoryEnabledChange = useCallback((enabled: boolean) => {
|
||||
onChange(enabled ? MEMORY_DEFAULT : undefined)
|
||||
}, [onChange])
|
||||
@ -154,9 +156,10 @@ const MemoryConfig: FC<Props> = ({
|
||||
size="md"
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{t(`${i18nPrefix}.windowSize`, { ns: 'workflow' })}</div>
|
||||
<div className="system-xs-medium-uppercase text-text-tertiary">{windowSizeLabel}</div>
|
||||
</div>
|
||||
<div className="flex h-8 items-center space-x-2">
|
||||
<FieldsetRoot className="flex h-8 items-center space-x-2">
|
||||
<FieldsetLegend className="sr-only">{windowSizeLabel}</FieldsetLegend>
|
||||
<Slider
|
||||
className="w-[144px]"
|
||||
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
|
||||
@ -165,9 +168,10 @@ const MemoryConfig: FC<Props> = ({
|
||||
step={1}
|
||||
onValueChange={handleWindowSizeChange}
|
||||
disabled={readonly || !payload.window?.enabled}
|
||||
aria-label={t(`${i18nPrefix}.windowSize`, { ns: 'workflow' })}
|
||||
aria-label={windowSizeLabel}
|
||||
/>
|
||||
<Input
|
||||
aria-label={windowSizeLabel}
|
||||
value={(payload.window?.size || WINDOW_SIZE_DEFAULT) as number}
|
||||
wrapperClassName="w-12"
|
||||
className="appearance-none pr-0"
|
||||
@ -179,7 +183,7 @@ const MemoryConfig: FC<Props> = ({
|
||||
onBlur={handleBlur}
|
||||
disabled={readonly || !payload.window?.enabled}
|
||||
/>
|
||||
</div>
|
||||
</FieldsetRoot>
|
||||
</div>
|
||||
{canSetRoleName && (
|
||||
<div className="mt-4">
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type {
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import { Slider } from '@langgenius/dify-ui/slider'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -17,6 +18,8 @@ const RetryOnPanel = ({
|
||||
const { t } = useTranslation()
|
||||
const { handleRetryConfigChange } = useRetryConfig(id)
|
||||
const { retry_config } = data
|
||||
const maxRetriesLabel = t('nodes.common.retry.maxRetries', { ns: 'workflow' })
|
||||
const retryIntervalLabel = t('nodes.common.retry.retryInterval', { ns: 'workflow' })
|
||||
|
||||
const handleRetryEnabledChange = (value: boolean) => {
|
||||
handleRetryConfigChange({
|
||||
@ -65,17 +68,19 @@ const RetryOnPanel = ({
|
||||
{
|
||||
retry_config?.retry_enabled && (
|
||||
<div className="px-4 pb-2">
|
||||
<div className="mb-1 flex w-full items-center">
|
||||
<div className="mr-2 grow system-xs-medium-uppercase text-text-secondary">{t('nodes.common.retry.maxRetries', { ns: 'workflow' })}</div>
|
||||
<FieldsetRoot className="mb-1 flex w-full items-center">
|
||||
<FieldsetLegend className="sr-only">{maxRetriesLabel}</FieldsetLegend>
|
||||
<div className="mr-2 grow system-xs-medium-uppercase text-text-secondary">{maxRetriesLabel}</div>
|
||||
<Slider
|
||||
className="mr-3 w-[108px]"
|
||||
value={retry_config?.max_retries || 3}
|
||||
onValueChange={handleMaxRetriesChange}
|
||||
min={1}
|
||||
max={10}
|
||||
aria-label={t('nodes.common.retry.maxRetries', { ns: 'workflow' })}
|
||||
aria-label={maxRetriesLabel}
|
||||
/>
|
||||
<Input
|
||||
aria-label={maxRetriesLabel}
|
||||
type="number"
|
||||
wrapperClassName="w-[100px]"
|
||||
value={retry_config?.max_retries || 3}
|
||||
@ -86,18 +91,20 @@ const RetryOnPanel = ({
|
||||
unit={t('nodes.common.retry.times', { ns: 'workflow' }) || ''}
|
||||
className={s.input}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2 grow system-xs-medium-uppercase text-text-secondary">{t('nodes.common.retry.retryInterval', { ns: 'workflow' })}</div>
|
||||
</FieldsetRoot>
|
||||
<FieldsetRoot className="flex items-center">
|
||||
<FieldsetLegend className="sr-only">{retryIntervalLabel}</FieldsetLegend>
|
||||
<div className="mr-2 grow system-xs-medium-uppercase text-text-secondary">{retryIntervalLabel}</div>
|
||||
<Slider
|
||||
className="mr-3 w-[108px]"
|
||||
value={retry_config?.retry_interval || 1000}
|
||||
onValueChange={handleRetryIntervalChange}
|
||||
min={100}
|
||||
max={5000}
|
||||
aria-label={t('nodes.common.retry.retryInterval', { ns: 'workflow' })}
|
||||
aria-label={retryIntervalLabel}
|
||||
/>
|
||||
<Input
|
||||
aria-label={retryIntervalLabel}
|
||||
type="number"
|
||||
wrapperClassName="w-[100px]"
|
||||
value={retry_config?.retry_interval || 1000}
|
||||
@ -108,7 +115,7 @@ const RetryOnPanel = ({
|
||||
unit={t('nodes.common.retry.ms', { ns: 'workflow' }) || ''}
|
||||
className={s.input}
|
||||
/>
|
||||
</div>
|
||||
</FieldsetRoot>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { FC } from 'react'
|
||||
import type { IterationNodeType } from './types'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Slider } from '@langgenius/dify-ui/slider'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import * as React from 'react'
|
||||
@ -22,6 +23,8 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const maxParallelismLabel = t(`${i18nPrefix}.MaxParallelismTitle`, { ns: 'workflow' })
|
||||
const errorResponseMethodLabel = t(`${i18nPrefix}.errorResponseMethod`, { ns: 'workflow' })
|
||||
const responseMethod = [
|
||||
{
|
||||
value: ErrorHandleMode.Terminated,
|
||||
@ -99,18 +102,19 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
{
|
||||
inputs.is_parallel && (
|
||||
<div className="px-4 pb-2">
|
||||
<Field title={t(`${i18nPrefix}.MaxParallelismTitle`, { ns: 'workflow' })} isSubTitle tooltip={<div className="w-[230px]">{t(`${i18nPrefix}.MaxParallelismDesc`, { ns: 'workflow' })}</div>}>
|
||||
<div className="row flex">
|
||||
<Input type="number" wrapperClassName="w-18 mr-4" max={MAX_PARALLEL_LIMIT} min={MIN_ITERATION_PARALLEL_NUM} value={inputs.parallel_nums} onChange={(e) => { changeParallelNums(Number(e.target.value)) }} />
|
||||
<Field title={maxParallelismLabel} isSubTitle tooltip={<div className="w-[230px]">{t(`${i18nPrefix}.MaxParallelismDesc`, { ns: 'workflow' })}</div>}>
|
||||
<FieldsetRoot className="row flex">
|
||||
<FieldsetLegend className="sr-only">{maxParallelismLabel}</FieldsetLegend>
|
||||
<Input aria-label={maxParallelismLabel} type="number" wrapperClassName="w-18 mr-4" max={MAX_PARALLEL_LIMIT} min={MIN_ITERATION_PARALLEL_NUM} value={inputs.parallel_nums} onChange={(e) => { changeParallelNums(Number(e.target.value)) }} />
|
||||
<Slider
|
||||
value={inputs.parallel_nums}
|
||||
onValueChange={changeParallelNums}
|
||||
max={MAX_PARALLEL_LIMIT}
|
||||
min={MIN_ITERATION_PARALLEL_NUM}
|
||||
className="mt-4 flex-1 shrink-0"
|
||||
aria-label={t(`${i18nPrefix}.MaxParallelismTitle`, { ns: 'workflow' })}
|
||||
aria-label={maxParallelismLabel}
|
||||
/>
|
||||
</div>
|
||||
</FieldsetRoot>
|
||||
|
||||
</Field>
|
||||
</div>
|
||||
@ -119,7 +123,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
<Split />
|
||||
|
||||
<div className="px-4 py-2">
|
||||
<Field title={t(`${i18nPrefix}.errorResponseMethod`, { ns: 'workflow' })}>
|
||||
<Field title={errorResponseMethodLabel}>
|
||||
<Select
|
||||
value={selectedResponseMethod ? String(selectedResponseMethod.value) : null}
|
||||
onValueChange={(nextValue) => {
|
||||
@ -130,6 +134,7 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
changeErrorResponseMode(nextItem)
|
||||
}}
|
||||
>
|
||||
<SelectLabel className="sr-only">{errorResponseMethodLabel}</SelectLabel>
|
||||
<SelectTrigger className="w-full">
|
||||
{selectedResponseMethod?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
|
||||
import { Slider } from '@langgenius/dify-ui/slider'
|
||||
import {
|
||||
memo,
|
||||
@ -35,6 +36,7 @@ const IndexMethod = ({
|
||||
readonly = false,
|
||||
}: IndexMethodProps) => {
|
||||
const { t } = useTranslation()
|
||||
const keywordNumberLabel = t('form.numberOfKeywords', { ns: 'datasetSettings' })
|
||||
const isHighQuality = indexMethod === IndexMethodEnum.QUALIFIED
|
||||
const isEconomy = indexMethod === IndexMethodEnum.ECONOMICAL
|
||||
|
||||
@ -91,16 +93,17 @@ const IndexMethod = ({
|
||||
onClick={handleIndexMethodChange}
|
||||
effectColor="blue"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<FieldsetRoot className="flex items-center">
|
||||
<FieldsetLegend className="sr-only">{keywordNumberLabel}</FieldsetLegend>
|
||||
<div className="flex grow items-center">
|
||||
<div className="truncate system-xs-medium text-text-secondary">
|
||||
{t('form.numberOfKeywords', { ns: 'datasetSettings' })}
|
||||
{keywordNumberLabel}
|
||||
</div>
|
||||
<Infotip
|
||||
aria-label={t('form.numberOfKeywords', { ns: 'datasetSettings' })}
|
||||
aria-label={keywordNumberLabel}
|
||||
className="ml-0.5 size-3.5"
|
||||
>
|
||||
{t('form.numberOfKeywords', { ns: 'datasetSettings' })}
|
||||
{keywordNumberLabel}
|
||||
</Infotip>
|
||||
</div>
|
||||
<Slider
|
||||
@ -108,9 +111,10 @@ const IndexMethod = ({
|
||||
className="mr-3 w-24 shrink-0"
|
||||
value={keywordNumber}
|
||||
onValueChange={onKeywordNumberChange}
|
||||
aria-label={t('form.numberOfKeywords', { ns: 'datasetSettings' })}
|
||||
aria-label={keywordNumberLabel}
|
||||
/>
|
||||
<Input
|
||||
aria-label={keywordNumberLabel}
|
||||
disabled={readonly}
|
||||
className="shrink-0"
|
||||
wrapperClassName="shrink-0 w-[72px]"
|
||||
@ -118,7 +122,7 @@ const IndexMethod = ({
|
||||
value={keywordNumber}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</FieldsetRoot>
|
||||
</OptionCard>
|
||||
)
|
||||
}
|
||||
|
||||
@ -46,6 +46,8 @@ const TopKAndScoreThreshold = ({
|
||||
hiddenScoreThreshold,
|
||||
}: TopKAndScoreThresholdProps) => {
|
||||
const { t } = useTranslation()
|
||||
const topKLabel = t('datasetConfig.top_k', { ns: 'appDebug' })
|
||||
const scoreThresholdLabel = t('datasetConfig.score_threshold', { ns: 'appDebug' })
|
||||
const handleTopKChange = useCallback((value: number) => {
|
||||
onTopKChange?.(Number.parseInt(value.toFixed(0)))
|
||||
}, [onTopKChange])
|
||||
@ -58,7 +60,7 @@ const TopKAndScoreThreshold = ({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="mb-0.5 flex h-6 items-center system-xs-medium text-text-secondary">
|
||||
{t('datasetConfig.top_k', { ns: 'appDebug' })}
|
||||
{topKLabel}
|
||||
<Infotip
|
||||
aria-label={t('datasetConfig.top_kTip', { ns: 'appDebug' })}
|
||||
className="ml-0.5 size-3.5"
|
||||
@ -76,7 +78,7 @@ const TopKAndScoreThreshold = ({
|
||||
onValueChange={value => handleTopKChange(value ?? 0)}
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput />
|
||||
<NumberFieldInput aria-label={topKLabel} />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
@ -95,7 +97,7 @@ const TopKAndScoreThreshold = ({
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className="grow truncate system-sm-medium text-text-secondary">
|
||||
{t('datasetConfig.score_threshold', { ns: 'appDebug' })}
|
||||
{scoreThresholdLabel}
|
||||
</div>
|
||||
<Infotip
|
||||
aria-label={t('datasetConfig.score_thresholdTip', { ns: 'appDebug' })}
|
||||
@ -114,7 +116,7 @@ const TopKAndScoreThreshold = ({
|
||||
onValueChange={value => handleScoreThresholdChange(value ?? 0)}
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput />
|
||||
<NumberFieldInput aria-label={scoreThresholdLabel} />
|
||||
<NumberFieldControls>
|
||||
<NumberFieldIncrement />
|
||||
<NumberFieldDecrement />
|
||||
|
||||
@ -9,6 +9,7 @@ type MockSwitchProps = {
|
||||
}
|
||||
|
||||
type MockSliderProps = {
|
||||
label: string
|
||||
value: number
|
||||
min: number
|
||||
max: number
|
||||
@ -108,6 +109,7 @@ describe('list-operator/limit-config', () => {
|
||||
min: 1,
|
||||
max: 20,
|
||||
readonly: true,
|
||||
label: 'workflow.nodes.listFilter.limit',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'slider:6:true' }))
|
||||
|
||||
@ -66,6 +66,7 @@ const LimitConfig: FC<Props> = ({
|
||||
{payload?.enabled
|
||||
? (
|
||||
<InputNumberWithSlider
|
||||
label={t(`${i18nPrefix}.limit`, { ns: 'workflow' })}
|
||||
value={payload?.size || LIMIT_SIZE_DEFAULT}
|
||||
min={LIMIT_SIZE_MIN}
|
||||
max={LIMIT_SIZE_MAX}
|
||||
|
||||
@ -92,6 +92,7 @@ const Panel: FC<NodePanelProps<LoopNodeType>> = ({
|
||||
>
|
||||
<div className="px-3 py-2">
|
||||
<InputNumberWithSlider
|
||||
label={t(`${i18nPrefix}.loopMaxCount`, { ns: 'workflow' })}
|
||||
min={1}
|
||||
max={LOOP_NODE_MAX_COUNT}
|
||||
value={inputs.loop_count}
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectGroupLabel,
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
@ -24,6 +25,7 @@ type FrequencySelectorProps = {
|
||||
const FrequencySelector = ({ frequency, onChange }: FrequencySelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const groupLabel = t('nodes.triggerSchedule.frequency.label', { ns: 'workflow' })
|
||||
const fieldLabel = t('nodes.triggerSchedule.frequencyLabel', { ns: 'workflow' })
|
||||
|
||||
const frequencies: FrequencyOption[] = [
|
||||
{ value: 'hourly', name: t('nodes.triggerSchedule.frequency.hourly', { ns: 'workflow' }) },
|
||||
@ -45,12 +47,13 @@ const FrequencySelector = ({ frequency, onChange }: FrequencySelectorProps) => {
|
||||
value={frequency}
|
||||
onValueChange={handleFrequencyChange}
|
||||
>
|
||||
<SelectLabel className="sr-only">{fieldLabel}</SelectLabel>
|
||||
<SelectTrigger className="w-full py-2">
|
||||
{selectedFrequency?.name ?? t('nodes.triggerSchedule.selectFrequency', { ns: 'workflow' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{groupLabel}</SelectLabel>
|
||||
<SelectGroupLabel>{groupLabel}</SelectGroupLabel>
|
||||
{frequencies.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
|
||||
@ -48,6 +48,7 @@ vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
<div>{children}</div>
|
||||
</SelectContext.Provider>
|
||||
),
|
||||
SelectLabel: () => null,
|
||||
SelectTrigger: ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
const context = React.useContext(SelectContext)
|
||||
return (
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
NumberFieldIncrement,
|
||||
NumberFieldInput,
|
||||
} from '@langgenius/dify-ui/number-field'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import copy from 'copy-to-clipboard'
|
||||
@ -203,6 +203,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
disabled={readOnly}
|
||||
onValueChange={value => value && handleContentTypeChange(value)}
|
||||
>
|
||||
<SelectLabel className="sr-only">{t(`${i18nPrefix}.contentType`, { ns: 'workflow' })}</SelectLabel>
|
||||
<SelectTrigger className="h-8 w-full text-sm">
|
||||
{selectedContentType?.name}
|
||||
</SelectTrigger>
|
||||
@ -267,6 +268,7 @@ const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
||||
>
|
||||
<NumberFieldGroup>
|
||||
<NumberFieldInput
|
||||
aria-label={t(`${i18nPrefix}.statusCode`, { ns: 'workflow' })}
|
||||
className="h-8"
|
||||
/>
|
||||
<NumberFieldControls>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user