mirror of
https://github.com/langgenius/dify.git
synced 2026-06-07 16:32:01 +08:00
feat(dify-ui): add Checkbox/CheckboxGroup primitives (#36271)
This commit is contained in:
parent
b96f372f45
commit
6649e4025e
@ -25,6 +25,14 @@
|
||||
"types": "./src/button/index.tsx",
|
||||
"import": "./src/button/index.tsx"
|
||||
},
|
||||
"./checkbox": {
|
||||
"types": "./src/checkbox/index.tsx",
|
||||
"import": "./src/checkbox/index.tsx"
|
||||
},
|
||||
"./checkbox-group": {
|
||||
"types": "./src/checkbox-group/index.tsx",
|
||||
"import": "./src/checkbox-group/index.tsx"
|
||||
},
|
||||
"./combobox": {
|
||||
"types": "./src/combobox/index.tsx",
|
||||
"import": "./src/combobox/index.tsx"
|
||||
|
||||
76
packages/dify-ui/src/checkbox-group/__tests__/index.spec.tsx
Normal file
76
packages/dify-ui/src/checkbox-group/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Field } from '@base-ui/react/field'
|
||||
import { Fieldset } from '@base-ui/react/fieldset'
|
||||
import { useState } from 'react'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { Checkbox } from '../../checkbox'
|
||||
import { CheckboxGroup } from '../index'
|
||||
|
||||
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
|
||||
|
||||
describe('CheckboxGroup', () => {
|
||||
it('should manage selected values and parent mixed state', async () => {
|
||||
function PermissionsDemo() {
|
||||
const [value, setValue] = useState(['read'])
|
||||
|
||||
return (
|
||||
<CheckboxGroup value={value} onValueChange={setValue} allValues={['read', 'write']}>
|
||||
<Checkbox parent aria-label="All permissions" />
|
||||
<label>
|
||||
<Checkbox value="read" />
|
||||
Read
|
||||
</label>
|
||||
<label>
|
||||
<Checkbox value="write" />
|
||||
Write
|
||||
</label>
|
||||
</CheckboxGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const screen = await render(<PermissionsDemo />)
|
||||
const parent = screen.getByRole('checkbox', { name: 'All permissions' })
|
||||
const write = screen.getByRole('checkbox', { name: 'Write' })
|
||||
|
||||
await expect.element(parent).toHaveAttribute('aria-checked', 'mixed')
|
||||
await expect.element(parent).toHaveAttribute('data-indeterminate', '')
|
||||
await expect.element(write).toHaveAttribute('aria-checked', 'false')
|
||||
|
||||
asHTMLElement(parent.element()).click()
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
await expect.element(parent).toHaveAttribute('aria-checked', 'true')
|
||||
await expect.element(write).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
it('should compose with Base UI Field and Fieldset without losing labels', async () => {
|
||||
const onValueChange = vi.fn()
|
||||
const screen = await render(
|
||||
<Field.Root name="features">
|
||||
<Fieldset.Root render={<CheckboxGroup value={['search']} onValueChange={onValueChange} />}>
|
||||
<Fieldset.Legend>Features</Fieldset.Legend>
|
||||
<Field.Item>
|
||||
<Field.Label>
|
||||
<Checkbox value="search" />
|
||||
Search
|
||||
</Field.Label>
|
||||
</Field.Item>
|
||||
<Field.Item>
|
||||
<Field.Label>
|
||||
<Checkbox value="analytics" />
|
||||
Analytics
|
||||
</Field.Label>
|
||||
</Field.Item>
|
||||
</Fieldset.Root>
|
||||
</Field.Root>,
|
||||
)
|
||||
|
||||
const analytics = screen.getByRole('checkbox', { name: 'Analytics' })
|
||||
await expect.element(analytics).toHaveAttribute('aria-checked', 'false')
|
||||
|
||||
asHTMLElement(analytics.element()).click()
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledTimes(1)
|
||||
expect(onValueChange.mock.calls[0]?.[0]).toEqual(['search', 'analytics'])
|
||||
})
|
||||
})
|
||||
116
packages/dify-ui/src/checkbox-group/index.stories.tsx
Normal file
116
packages/dify-ui/src/checkbox-group/index.stories.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { Field } from '@base-ui/react/field'
|
||||
import { Fieldset } from '@base-ui/react/fieldset'
|
||||
import { useId, useState } from 'react'
|
||||
import { CheckboxGroup } from '.'
|
||||
import { Checkbox } from '../checkbox'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/CheckboxGroup',
|
||||
component: CheckboxGroup,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'CheckboxGroup primitive built on Base UI. It owns multi-checkbox array state, allValues, and parent checkbox semantics. Import from `@langgenius/dify-ui/checkbox-group` and compose with `Checkbox` from `@langgenius/dify-ui/checkbox`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof CheckboxGroup>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
function DocumentSelectionDemo() {
|
||||
const documentIds = ['doc-1', 'doc-2', 'doc-3']
|
||||
const [selected, setSelected] = useState<string[]>(['doc-1'])
|
||||
const groupLabelId = useId()
|
||||
|
||||
return (
|
||||
<CheckboxGroup
|
||||
aria-labelledby={groupLabelId}
|
||||
value={selected}
|
||||
onValueChange={setSelected}
|
||||
allValues={documentIds}
|
||||
className="flex flex-col gap-3"
|
||||
>
|
||||
<label id={groupLabelId} className="flex items-center gap-2 system-sm-semibold-uppercase text-text-secondary">
|
||||
<Checkbox parent />
|
||||
Current page documents
|
||||
</label>
|
||||
<div className="flex flex-col gap-2 pl-6">
|
||||
{[
|
||||
{ id: 'doc-1', name: 'onboarding-guide.pdf' },
|
||||
{ id: 'doc-2', name: 'pricing-faq.md' },
|
||||
{ id: 'doc-3', name: 'release-notes.txt' },
|
||||
].map(document => (
|
||||
<label key={document.id} className="flex items-center gap-2 system-sm-medium text-text-secondary">
|
||||
<Checkbox value={document.id} />
|
||||
{document.name}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</CheckboxGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export const DocumentSelection: Story = {
|
||||
render: () => <DocumentSelectionDemo />,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Matches Dify table/list selection patterns such as documents, segments, annotations, and install bundle items: CheckboxGroup owns the selected ID array, allValues defines the current selectable page, and the parent checkbox provides select-all plus mixed state.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
function DynamicFormFieldDemo() {
|
||||
const options = [
|
||||
{ value: 'markdown', label: 'Markdown' },
|
||||
{ value: 'pdf', label: 'PDF' },
|
||||
{ value: 'html', label: 'HTML' },
|
||||
]
|
||||
const [selected, setSelected] = useState<string[]>(['markdown'])
|
||||
|
||||
return (
|
||||
<Field.Root name="allowed_file_types" className="flex w-80 flex-col gap-2">
|
||||
<Field.Description className="body-xs-regular text-text-tertiary">
|
||||
This mirrors Dify dynamic form fields where checkbox options are controlled by schema and persisted as a string array.
|
||||
</Field.Description>
|
||||
<Fieldset.Root
|
||||
render={(
|
||||
<CheckboxGroup
|
||||
value={selected}
|
||||
onValueChange={setSelected}
|
||||
className="flex flex-col gap-2 rounded-lg border border-components-panel-border bg-components-panel-bg p-3"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Fieldset.Legend className="system-sm-medium text-text-secondary">
|
||||
Allowed file types
|
||||
</Fieldset.Legend>
|
||||
{options.map(option => (
|
||||
<Field.Item key={option.value}>
|
||||
<Field.Label className="flex items-center gap-2 system-sm-medium text-text-secondary">
|
||||
<Checkbox value={option.value} />
|
||||
{option.label}
|
||||
</Field.Label>
|
||||
</Field.Item>
|
||||
))}
|
||||
</Fieldset.Root>
|
||||
</Field.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export const DynamicFormField: Story = {
|
||||
render: () => <DynamicFormFieldDemo />,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Matches Dify checkbox-list form usage in workflow node forms and base form rendering. Field and Fieldset provide group labeling; CheckboxGroup owns controlled array state.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
10
packages/dify-ui/src/checkbox-group/index.tsx
Normal file
10
packages/dify-ui/src/checkbox-group/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import type { CheckboxGroup as BaseCheckboxGroupNS } from '@base-ui/react/checkbox-group'
|
||||
import { CheckboxGroup as BaseCheckboxGroup } from '@base-ui/react/checkbox-group'
|
||||
|
||||
export type CheckboxGroupProps = BaseCheckboxGroupNS.Props
|
||||
|
||||
export function CheckboxGroup(props: CheckboxGroupProps) {
|
||||
return <BaseCheckboxGroup {...props} />
|
||||
}
|
||||
129
packages/dify-ui/src/checkbox/__tests__/index.spec.tsx
Normal file
129
packages/dify-ui/src/checkbox/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxIndicator,
|
||||
CheckboxRoot,
|
||||
CheckboxSkeleton,
|
||||
} from '../index'
|
||||
|
||||
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
|
||||
|
||||
describe('Checkbox', () => {
|
||||
it('should render an unchecked checkbox with Base UI semantics', async () => {
|
||||
const screen = await render(<Checkbox checked={false} aria-label="Accept terms" />)
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' })
|
||||
|
||||
await expect.element(checkbox).toHaveAttribute('aria-checked', 'false')
|
||||
await expect.element(checkbox).toHaveAttribute('data-unchecked', '')
|
||||
await expect.element(checkbox).not.toHaveAttribute('data-checked')
|
||||
await expect.element(checkbox).not.toHaveAttribute('data-indeterminate')
|
||||
})
|
||||
|
||||
it('should expose checked data attributes and icon styling hooks', async () => {
|
||||
const screen = await render(<Checkbox checked aria-label="Accept terms" />)
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' })
|
||||
|
||||
await expect.element(checkbox).toHaveAttribute('aria-checked', 'true')
|
||||
await expect.element(checkbox).toHaveAttribute('data-checked', '')
|
||||
await expect.element(checkbox).toHaveClass('data-checked:bg-components-checkbox-bg')
|
||||
expect(screen.container.querySelector('.i-ri-check-line')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose mixed state when indeterminate', async () => {
|
||||
const screen = await render(<Checkbox checked={false} indeterminate aria-label="Select all" />)
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Select all' })
|
||||
|
||||
await expect.element(checkbox).toHaveAttribute('aria-checked', 'mixed')
|
||||
await expect.element(checkbox).toHaveAttribute('data-indeterminate', '')
|
||||
expect(screen.container.querySelector('.i-ri-check-line')).not.toBeInTheDocument()
|
||||
expect(screen.container.querySelector('span span.rounded-full.bg-current')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onCheckedChange with the next checked value', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(
|
||||
<Checkbox checked={false} aria-label="Accept terms" onCheckedChange={onCheckedChange} />,
|
||||
)
|
||||
|
||||
asHTMLElement(screen.getByRole('checkbox', { name: 'Accept terms' }).element()).click()
|
||||
|
||||
expect(onCheckedChange).toHaveBeenCalledTimes(1)
|
||||
expect(onCheckedChange.mock.calls[0]?.[0]).toBe(true)
|
||||
})
|
||||
|
||||
it('should stay controlled until the checked prop changes', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(
|
||||
<Checkbox checked={false} aria-label="Accept terms" onCheckedChange={onCheckedChange} />,
|
||||
)
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' })
|
||||
|
||||
asHTMLElement(checkbox.element()).click()
|
||||
expect(onCheckedChange.mock.calls[0]?.[0]).toBe(true)
|
||||
await expect.element(checkbox).toHaveAttribute('aria-checked', 'false')
|
||||
|
||||
await screen.rerender(<Checkbox checked aria-label="Accept terms" onCheckedChange={onCheckedChange} />)
|
||||
await expect.element(screen.getByRole('checkbox', { name: 'Accept terms' })).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should ignore interaction when disabled', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(
|
||||
<Checkbox checked={false} disabled aria-label="Accept terms" onCheckedChange={onCheckedChange} />,
|
||||
)
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' })
|
||||
|
||||
await expect.element(checkbox).toHaveAttribute('data-disabled', '')
|
||||
await expect.element(checkbox).toHaveClass('data-disabled:cursor-not-allowed')
|
||||
|
||||
asHTMLElement(checkbox.element()).click()
|
||||
|
||||
expect(onCheckedChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should submit checked and unchecked form values through the hidden input', async () => {
|
||||
const screen = await render(
|
||||
<form>
|
||||
<Checkbox
|
||||
checked
|
||||
name="terms"
|
||||
value="accepted"
|
||||
uncheckedValue="declined"
|
||||
aria-label="Terms"
|
||||
/>
|
||||
<Checkbox
|
||||
checked={false}
|
||||
name="newsletter"
|
||||
value="yes"
|
||||
uncheckedValue="no"
|
||||
aria-label="Newsletter"
|
||||
/>
|
||||
</form>,
|
||||
)
|
||||
const form = screen.container.querySelector('form') as HTMLFormElement
|
||||
const data = new FormData(form)
|
||||
|
||||
expect(data.get('terms')).toBe('accepted')
|
||||
expect(data.get('newsletter')).toBe('no')
|
||||
})
|
||||
|
||||
it('should support custom compound composition with CheckboxRoot and CheckboxIndicator', async () => {
|
||||
const screen = await render(
|
||||
<CheckboxRoot checked aria-label="Custom checkbox" className="custom-root">
|
||||
<CheckboxIndicator className="custom-indicator" />
|
||||
</CheckboxRoot>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByRole('checkbox', { name: 'Custom checkbox' })).toHaveClass('custom-root')
|
||||
expect(screen.container.querySelector('.custom-indicator')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CheckboxSkeleton', () => {
|
||||
it('should render a visual placeholder without checkbox semantics', async () => {
|
||||
const screen = await render(<CheckboxSkeleton data-testid="checkbox-skeleton" />)
|
||||
|
||||
expect(screen.container.querySelector('[role="checkbox"]')).not.toBeInTheDocument()
|
||||
await expect.element(screen.getByTestId('checkbox-skeleton')).toHaveClass('bg-text-quaternary', 'opacity-20')
|
||||
})
|
||||
})
|
||||
145
packages/dify-ui/src/checkbox/index.stories.tsx
Normal file
145
packages/dify-ui/src/checkbox/index.stories.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxSkeleton,
|
||||
} from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/Checkbox',
|
||||
component: Checkbox,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Checkbox primitive built on Base UI. It preserves Base UI checked, indeterminate, disabled, and hidden input semantics while applying the Dify 16px checkbox design from Figma. Import from `@langgenius/dify-ui/checkbox`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
args: {
|
||||
checked: false,
|
||||
disabled: false,
|
||||
indeterminate: false,
|
||||
},
|
||||
argTypes: {
|
||||
checked: {
|
||||
control: 'boolean',
|
||||
description: 'Controlled checked state.',
|
||||
},
|
||||
indeterminate: {
|
||||
control: 'boolean',
|
||||
description: 'Mixed state used by parent or select-all checkboxes.',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Disables user interaction and exposes Base UI disabled state attributes.',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Checkbox>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
function CheckboxDemo(args: Partial<ComponentProps<typeof Checkbox>>) {
|
||||
const [checked, setChecked] = useState(args.checked ?? false)
|
||||
|
||||
return (
|
||||
<label className="flex items-center gap-2 system-sm-medium text-text-secondary">
|
||||
<Checkbox
|
||||
{...args}
|
||||
checked={checked}
|
||||
onCheckedChange={setChecked}
|
||||
/>
|
||||
Enable feature
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: args => <CheckboxDemo {...args} />,
|
||||
args: {
|
||||
checked: false,
|
||||
indeterminate: false,
|
||||
disabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const Checked: Story = {
|
||||
render: args => <CheckboxDemo {...args} />,
|
||||
args: {
|
||||
checked: true,
|
||||
indeterminate: false,
|
||||
disabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const Indeterminate: Story = {
|
||||
args: {
|
||||
'checked': false,
|
||||
'indeterminate': true,
|
||||
'disabled': false,
|
||||
'aria-label': 'Partial selection',
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="flex items-center gap-2 system-sm-medium text-text-secondary">
|
||||
<Checkbox checked={false} disabled />
|
||||
Disabled unchecked
|
||||
</label>
|
||||
<label className="flex items-center gap-2 system-sm-medium text-text-secondary">
|
||||
<Checkbox checked disabled />
|
||||
Disabled checked
|
||||
</label>
|
||||
<label className="flex items-center gap-2 system-sm-medium text-text-secondary">
|
||||
<Checkbox checked={false} indeterminate disabled />
|
||||
Disabled mixed
|
||||
</label>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
function StateMatrixDemo() {
|
||||
const states = [
|
||||
{ label: 'Unchecked', checked: false },
|
||||
{ label: 'Checked', checked: true },
|
||||
{ label: 'Indeterminate', checked: false, indeterminate: true },
|
||||
{ label: 'Disabled unchecked', checked: false, disabled: true },
|
||||
{ label: 'Disabled checked', checked: true, disabled: true },
|
||||
{ label: 'Disabled indeterminate', checked: false, indeterminate: true, disabled: true },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{states.map(state => (
|
||||
<label key={state.label} className="flex items-center gap-2 system-sm-medium text-text-secondary">
|
||||
<Checkbox
|
||||
checked={state.checked}
|
||||
indeterminate={state.indeterminate}
|
||||
disabled={state.disabled}
|
||||
/>
|
||||
{state.label}
|
||||
</label>
|
||||
))}
|
||||
<div className="flex items-center gap-2 system-sm-medium text-text-secondary">
|
||||
<CheckboxSkeleton aria-hidden="true" />
|
||||
Skeleton
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StateMatrix: Story = {
|
||||
render: () => <StateMatrixDemo />,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'The full visual matrix for Dify checkbox states. State styling comes from Base UI data attributes such as data-checked, data-indeterminate, and data-disabled.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
100
packages/dify-ui/src/checkbox/index.tsx
Normal file
100
packages/dify-ui/src/checkbox/index.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
'use client'
|
||||
|
||||
import type { Checkbox as BaseCheckboxNS } from '@base-ui/react/checkbox'
|
||||
import type { HTMLAttributes } from 'react'
|
||||
import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox'
|
||||
import { cn } from '../cn'
|
||||
|
||||
const checkboxRootClassName = cn(
|
||||
'inline-flex size-4 shrink-0 touch-manipulation items-center justify-center rounded-sm shadow-xs shadow-shadow-shadow-3 transition-colors motion-reduce:transition-none',
|
||||
'border border-components-checkbox-border bg-components-checkbox-bg-unchecked text-components-checkbox-icon',
|
||||
'hover:border-components-checkbox-border-hover hover:bg-components-checkbox-bg-unchecked-hover',
|
||||
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-components-checkbox-bg focus-visible:ring-offset-0',
|
||||
'data-checked:border-transparent data-checked:bg-components-checkbox-bg data-checked:hover:bg-components-checkbox-bg-hover',
|
||||
'data-indeterminate:border-transparent data-indeterminate:bg-components-checkbox-bg data-indeterminate:hover:bg-components-checkbox-bg-hover',
|
||||
'data-disabled:cursor-not-allowed data-disabled:border-components-checkbox-border-disabled data-disabled:bg-components-checkbox-bg-disabled',
|
||||
'data-disabled:hover:border-components-checkbox-border-disabled data-disabled:hover:bg-components-checkbox-bg-disabled',
|
||||
'data-disabled:data-checked:border-transparent data-disabled:data-checked:bg-components-checkbox-bg-disabled-checked data-disabled:data-checked:text-components-checkbox-icon-disabled',
|
||||
'data-disabled:data-checked:hover:bg-components-checkbox-bg-disabled-checked',
|
||||
'data-disabled:data-indeterminate:border-transparent data-disabled:data-indeterminate:bg-components-checkbox-bg-disabled-checked data-disabled:data-indeterminate:text-components-checkbox-icon-disabled',
|
||||
'data-disabled:data-indeterminate:hover:bg-components-checkbox-bg-disabled-checked',
|
||||
)
|
||||
|
||||
const checkboxIndicatorClassName = 'flex size-3 items-center justify-center text-current data-unchecked:hidden'
|
||||
|
||||
const checkboxSkeletonClassName = 'size-4 shrink-0 rounded-sm bg-text-quaternary opacity-20'
|
||||
|
||||
export type CheckboxRootProps
|
||||
= Omit<BaseCheckboxNS.Root.Props, 'className'>
|
||||
& {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CheckboxRoot({
|
||||
className,
|
||||
...props
|
||||
}: CheckboxRootProps) {
|
||||
return (
|
||||
<BaseCheckbox.Root
|
||||
className={cn(checkboxRootClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export type CheckboxIndicatorProps
|
||||
= Omit<BaseCheckboxNS.Indicator.Props, 'className' | 'children'>
|
||||
& {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CheckboxIndicator({
|
||||
className,
|
||||
render,
|
||||
...props
|
||||
}: CheckboxIndicatorProps) {
|
||||
return (
|
||||
<BaseCheckbox.Indicator
|
||||
className={cn(checkboxIndicatorClassName, className)}
|
||||
render={render ?? ((indicatorProps, state) => (
|
||||
<span {...indicatorProps}>
|
||||
{state.indeterminate
|
||||
? <span className="block h-[1.5px] w-1.75 rounded-full bg-current" />
|
||||
: <span className="i-ri-check-line block size-3 shrink-0" />}
|
||||
</span>
|
||||
))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export type CheckboxProps
|
||||
= Omit<CheckboxRootProps, 'children'>
|
||||
|
||||
export function Checkbox({
|
||||
...props
|
||||
}: CheckboxProps) {
|
||||
return (
|
||||
<CheckboxRoot {...props}>
|
||||
<CheckboxIndicator />
|
||||
</CheckboxRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export type CheckboxSkeletonProps
|
||||
= Omit<HTMLAttributes<HTMLDivElement>, 'className'>
|
||||
& {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function CheckboxSkeleton({
|
||||
className,
|
||||
...props
|
||||
}: CheckboxSkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(checkboxSkeletonClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import CheckboxList from '..'
|
||||
import { CheckboxList } from '..'
|
||||
|
||||
describe('checkbox list component', () => {
|
||||
const selectAllName = 'common.operation.selectAll'
|
||||
const options = [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
{ label: 'Option 2', value: 'option2' },
|
||||
@ -38,8 +39,7 @@ describe('checkbox list component', () => {
|
||||
|
||||
it('renders select-all checkbox', () => {
|
||||
render(<CheckboxList options={options} showSelectAll />)
|
||||
const checkboxes = screen.getByTestId('checkbox-selectAll')
|
||||
expect(checkboxes)!.toBeInTheDocument()
|
||||
expect(screen.getByRole('checkbox', { name: selectAllName })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('selects all options when select-all is clicked', async () => {
|
||||
@ -54,7 +54,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByTestId('checkbox-selectAll')
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
await userEvent.click(selectAll)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(['option1', 'option2', 'option3', 'apple'])
|
||||
@ -73,7 +73,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByTestId('checkbox-selectAll')
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
await userEvent.click(selectAll)
|
||||
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
@ -91,7 +91,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByTestId('checkbox-selectAll')
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
await userEvent.click(selectAll)
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
@ -109,14 +109,14 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectAll = screen.getByTestId('checkbox-selectAll')
|
||||
expect(selectAll.querySelector('[data-testid="check-icon-selectAll"]'))!.toBeInTheDocument()
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
expect(selectAll).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('hides select-all checkbox when searching', async () => {
|
||||
render(<CheckboxList options={options} />)
|
||||
await userEvent.type(screen.getByRole('textbox'), 'app')
|
||||
expect(screen.queryByTestId('checkbox-selectAll')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('checkbox', { name: selectAllName })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('selects options when checkbox is clicked', async () => {
|
||||
@ -131,7 +131,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectOption = screen.getByTestId('checkbox-option1')
|
||||
const selectOption = screen.getByRole('checkbox', { name: 'Option 1' })
|
||||
await userEvent.click(selectOption)
|
||||
expect(onChange).toHaveBeenCalledWith(['option1'])
|
||||
})
|
||||
@ -148,7 +148,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectOption = screen.getByTestId('checkbox-option1')
|
||||
const selectOption = screen.getByRole('checkbox', { name: 'Option 1' })
|
||||
await userEvent.click(selectOption)
|
||||
expect(onChange).toHaveBeenCalledWith([])
|
||||
})
|
||||
@ -165,7 +165,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const selectOption = screen.getByTestId('checkbox-option1')
|
||||
const selectOption = screen.getByRole('checkbox', { name: 'Option 1' })
|
||||
await userEvent.click(selectOption)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
@ -202,12 +202,12 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const disabledCheckbox = screen.getByTestId('checkbox-disabled')
|
||||
const disabledCheckbox = screen.getByRole('checkbox', { name: 'Disabled' })
|
||||
await userEvent.click(disabledCheckbox)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not toggle option when component is disabled and option is clicked via div', async () => {
|
||||
it('does not toggle option when component is disabled and option label is clicked', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
@ -219,11 +219,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Find option and click the div container
|
||||
const optionLabels = screen.getAllByText('Option 1')
|
||||
const optionDiv = optionLabels[0]!.closest('[data-testid="option-item"]')
|
||||
expect(optionDiv)!.toBeInTheDocument()
|
||||
await userEvent.click(optionDiv as HTMLElement)
|
||||
await userEvent.click(screen.getByText('Option 1'))
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -246,7 +242,7 @@ describe('checkbox list component', () => {
|
||||
showSearch={false}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByTestId('checkbox-selectAll')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('checkbox', { name: selectAllName })).not.toBeInTheDocument()
|
||||
options.forEach((option) => {
|
||||
expect(screen.getByText(option.label))!.toBeInTheDocument()
|
||||
})
|
||||
@ -284,7 +280,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
// When some but not all options are selected, clicking select-all should select all remaining options
|
||||
const selectAll = screen.getByTestId('checkbox-selectAll')
|
||||
const selectAll = screen.getByRole('checkbox', { name: selectAllName })
|
||||
expect(selectAll)!.toBeInTheDocument()
|
||||
expect(selectAll)!.toHaveAttribute('aria-checked', 'mixed')
|
||||
|
||||
@ -326,7 +322,7 @@ describe('checkbox list component', () => {
|
||||
)
|
||||
|
||||
const optionLabel = screen.getByText('Option 1')
|
||||
const optionRow = optionLabel.closest('div[data-testid="option-item"]')
|
||||
const optionRow = optionLabel.closest('label[data-testid="option-item"]')
|
||||
expect(optionRow)!.toBeInTheDocument()
|
||||
await userEvent.click(optionRow as HTMLElement)
|
||||
|
||||
@ -347,7 +343,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const optionRow = screen.getByText('Option 1').closest('div[data-testid="option-item"]')
|
||||
const optionRow = screen.getByText('Option 1').closest('label[data-testid="option-item"]')
|
||||
expect(optionRow)!.toBeInTheDocument()
|
||||
await userEvent.click(optionRow as HTMLElement)
|
||||
|
||||
@ -404,7 +400,7 @@ describe('checkbox list component', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const checkbox = screen.getByTestId('checkbox-option')
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'Option' })
|
||||
await userEvent.click(checkbox)
|
||||
expect(onChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useId, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import SearchInput from '@/app/components/base/search-input'
|
||||
import SearchMenu from '@/assets/search-menu.svg'
|
||||
|
||||
@ -30,7 +30,7 @@ type CheckboxListProps = {
|
||||
maxHeight?: string | number
|
||||
}
|
||||
|
||||
const CheckboxList: FC<CheckboxListProps> = ({
|
||||
export const CheckboxList = ({
|
||||
title = '',
|
||||
label,
|
||||
description,
|
||||
@ -43,8 +43,9 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
||||
showCount = true,
|
||||
showSearch = true,
|
||||
maxHeight,
|
||||
}) => {
|
||||
}: CheckboxListProps) => {
|
||||
const { t } = useTranslation()
|
||||
const groupLabelId = useId()
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
@ -59,48 +60,15 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
||||
|
||||
const selectedCount = value.length
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
const selectableOptions = options.filter(option => !option.disabled)
|
||||
return selectableOptions.length > 0 && selectableOptions.every(option => value.includes(option.value))
|
||||
}, [options, value])
|
||||
|
||||
const isIndeterminate = useMemo(() => {
|
||||
const selectableOptions = options.filter(option => !option.disabled)
|
||||
const selectedCount = selectableOptions.filter(option => value.includes(option.value)).length
|
||||
return selectedCount > 0 && selectedCount < selectableOptions.length
|
||||
}, [options, value])
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
if (disabled)
|
||||
return
|
||||
|
||||
if (isAllSelected) {
|
||||
// Deselect all
|
||||
onChange?.([])
|
||||
}
|
||||
else {
|
||||
// Select all non-disabled options
|
||||
const allValues = options
|
||||
.filter(option => !option.disabled)
|
||||
.map(option => option.value)
|
||||
onChange?.(allValues)
|
||||
}
|
||||
}, [isAllSelected, options, onChange, disabled])
|
||||
|
||||
const handleToggleOption = useCallback((optionValue: string) => {
|
||||
if (disabled)
|
||||
return
|
||||
|
||||
const newValue = value.includes(optionValue)
|
||||
? value.filter(v => v !== optionValue)
|
||||
: [...value, optionValue]
|
||||
onChange?.(newValue)
|
||||
}, [value, onChange, disabled])
|
||||
const selectableOptionValues = useMemo(
|
||||
() => options.filter(option => !option.disabled).map(option => option.value),
|
||||
[options],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn('flex w-full flex-col gap-1', containerClassName)}>
|
||||
{label && (
|
||||
<div className="system-sm-medium text-text-secondary">
|
||||
<div id={groupLabelId} className="system-sm-medium text-text-secondary">
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
@ -110,17 +78,24 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border border-components-panel-border bg-components-panel-bg">
|
||||
<CheckboxGroup
|
||||
aria-labelledby={label ? groupLabelId : undefined}
|
||||
value={value}
|
||||
onValueChange={nextValue => onChange?.(nextValue)}
|
||||
allValues={selectableOptionValues}
|
||||
disabled={disabled}
|
||||
className="rounded-lg border border-components-panel-border bg-components-panel-bg"
|
||||
>
|
||||
{(showSelectAll || title || showSearch) && (
|
||||
<div className="relative flex items-center gap-2 border-b border-divider-subtle px-3 py-2">
|
||||
{!searchQuery && showSelectAll && (
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
indeterminate={isIndeterminate}
|
||||
onCheck={handleSelectAll}
|
||||
disabled={disabled}
|
||||
id="selectAll"
|
||||
/>
|
||||
<label className={cn('flex shrink-0 items-center', !disabled && 'cursor-pointer')}>
|
||||
<Checkbox
|
||||
parent
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span className="sr-only">{t('operation.selectAll', { ns: 'common' })}</span>
|
||||
</label>
|
||||
)}
|
||||
{!searchQuery
|
||||
? (
|
||||
@ -177,45 +152,30 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
filteredOptions.map((option) => {
|
||||
const selected = value.includes(option.value)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
data-testid="option-item"
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-state-base-hover',
|
||||
option.disabled && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!option.disabled && !disabled)
|
||||
handleToggleOption(option.value)
|
||||
}}
|
||||
filteredOptions.map(option => (
|
||||
<label
|
||||
key={option.value}
|
||||
data-testid="option-item"
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-state-base-hover',
|
||||
(option.disabled || disabled) && 'cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
value={option.value}
|
||||
disabled={option.disabled || disabled}
|
||||
/>
|
||||
<span
|
||||
className="flex-1 truncate system-sm-medium text-text-secondary"
|
||||
title={option.label}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selected}
|
||||
onCheck={() => {
|
||||
if (!option.disabled && !disabled)
|
||||
handleToggleOption(option.value)
|
||||
}}
|
||||
disabled={option.disabled || disabled}
|
||||
id={option.value}
|
||||
/>
|
||||
<div
|
||||
className="flex-1 truncate system-sm-medium text-text-secondary"
|
||||
title={option.label}
|
||||
>
|
||||
{option.label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
{option.label}
|
||||
</span>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CheckboxGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CheckboxList
|
||||
|
||||
@ -394,8 +394,8 @@ describe('BaseField', () => {
|
||||
fireEvent.click(screen.getByText('Feature B'))
|
||||
})
|
||||
|
||||
const checkboxB = screen.getByTestId('checkbox-b')
|
||||
expect(checkboxB).toBeChecked()
|
||||
const checkboxB = screen.getByRole('checkbox', { name: 'Feature B' })
|
||||
expect(checkboxB).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should handle dynamic select error state', () => {
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CheckboxList from '@/app/components/base/checkbox-list'
|
||||
import { CheckboxList } from '@/app/components/base/checkbox-list'
|
||||
import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { MetadataItemInBatchEdit, MetadataItemWithEdit } from '../../types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { DataType, UpdateType } from '../../types'
|
||||
import EditMetadataBatchModal from '../modal'
|
||||
@ -212,6 +213,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
})
|
||||
|
||||
it('should toggle apply to all checkbox', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<EditMetadataBatchModal {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -219,7 +221,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
})
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'dataset.metadata.batchEditMetadata.applyToAllSelectDocument' })
|
||||
fireEvent.click(checkbox)
|
||||
await user.click(checkbox)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(checkbox).toHaveAttribute('aria-checked', 'true')
|
||||
@ -482,6 +484,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
})
|
||||
|
||||
it('should pass isApplyToAllSelectDocument as true when checked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSave = vi.fn()
|
||||
render(<EditMetadataBatchModal {...defaultProps} onSave={onSave} />)
|
||||
|
||||
@ -489,7 +492,7 @@ describe('EditMetadataBatchModal', () => {
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: 'dataset.metadata.batchEditMetadata.applyToAllSelectDocument' }))
|
||||
await user.click(screen.getByRole('checkbox', { name: 'dataset.metadata.batchEditMetadata.applyToAllSelectDocument' }))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { BuiltInMetadataItem, MetadataItemInBatchEdit, MetadataItemWithEdit } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { produce } from 'immer'
|
||||
@ -10,7 +11,6 @@ import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useCreateMetaData } from '@/service/knowledge/use-metadata'
|
||||
import Checkbox from '../../../base/checkbox'
|
||||
import { Infotip } from '../../../base/infotip'
|
||||
import useCheckMetadataName from '../hooks/use-check-metadata-name'
|
||||
import { DatasetMetadataPicker } from '../metadata-dataset/dataset-metadata-picker'
|
||||
@ -131,13 +131,15 @@ const EditMetadataBatchModal: FC<Props> = ({ datasetId, documentNum, list, onSav
|
||||
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div className="flex items-center select-none">
|
||||
<Checkbox
|
||||
checked={isApplyToAllSelectDocument}
|
||||
onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)}
|
||||
id="apply-to-all"
|
||||
ariaLabel={t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}
|
||||
/>
|
||||
<div className="mr-1 ml-2 system-xs-medium text-text-secondary">{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}</div>
|
||||
<label className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
checked={isApplyToAllSelectDocument}
|
||||
onCheckedChange={setIsApplyToAllSelectDocument}
|
||||
/>
|
||||
<span className="mr-1 ml-2 system-xs-medium text-text-secondary">
|
||||
{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}
|
||||
</span>
|
||||
</label>
|
||||
<Infotip
|
||||
aria-label={t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}
|
||||
className="p-px"
|
||||
|
||||
@ -8,7 +8,7 @@ import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/work
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import CheckboxList from '@/app/components/base/checkbox-list'
|
||||
import { CheckboxList } from '@/app/components/base/checkbox-list'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { AppSelector } from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user