mirror of
https://github.com/langgenius/dify.git
synced 2026-05-23 18:38:26 +08:00
Compare commits
1 Commits
codex/dify
...
codex/refi
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bf8758807 |
@ -1770,6 +1770,14 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/textarea/index.stories.tsx": {
|
||||
"no-console": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/voice-input/__tests__/index.spec.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 3
|
||||
|
||||
@ -33,7 +33,6 @@ import { Drawer, DrawerPopup, DrawerTrigger } from '@langgenius/dify-ui/drawer'
|
||||
import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { Form } from '@langgenius/dify-ui/form'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import '@langgenius/dify-ui/styles.css' // once, in the app root
|
||||
```
|
||||
|
||||
@ -41,16 +40,16 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
|
||||
|
||||
## Primitives
|
||||
|
||||
| Category | Subpath | Notes |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
|
||||
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
|
||||
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
|
||||
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
|
||||
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
|
||||
| Media | `./avatar` | Avatar root, image, and fallback primitives. |
|
||||
| Navigation | `./tabs`, `./toggle-group` | Tabs for panels; ToggleGroup for segmented modes. |
|
||||
| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
|
||||
| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
|
||||
| Category | Subpath | Notes |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
||||
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
|
||||
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
|
||||
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
|
||||
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
|
||||
| Media | `./avatar` | Avatar root, image, and fallback primitives. |
|
||||
| Navigation | `./tabs`, `./toggle-group` | Tabs for panels; ToggleGroup for segmented modes. |
|
||||
| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
|
||||
| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
|
||||
|
||||
Utilities:
|
||||
|
||||
@ -65,7 +64,7 @@ Use `Form` for the submit boundary. It renders a native `<form>`, preserves Ente
|
||||
|
||||
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.
|
||||
|
||||
Choose the label primitive by the control semantics. Text-like inputs, `Textarea`, 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.
|
||||
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:
|
||||
|
||||
|
||||
@ -113,10 +113,6 @@
|
||||
"types": "./src/tabs/index.tsx",
|
||||
"import": "./src/tabs/index.tsx"
|
||||
},
|
||||
"./textarea": {
|
||||
"types": "./src/textarea/index.tsx",
|
||||
"import": "./src/textarea/index.tsx"
|
||||
},
|
||||
"./toggle-group": {
|
||||
"types": "./src/toggle-group/index.tsx",
|
||||
"import": "./src/toggle-group/index.tsx"
|
||||
|
||||
@ -49,6 +49,19 @@ describe('Switch', () => {
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should work in uncontrolled mode with defaultChecked prop', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(<Switch defaultChecked={false} onCheckedChange={onCheckedChange} />)
|
||||
const switchElement = screen.getByRole('switch')
|
||||
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'false')
|
||||
|
||||
asHTMLElement(switchElement.element()).click()
|
||||
|
||||
expect(onCheckedChange).toHaveBeenCalledWith(true)
|
||||
await expect.element(switchElement).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should not call onCheckedChange when disabled', async () => {
|
||||
const onCheckedChange = vi.fn()
|
||||
const screen = await render(<Switch checked={false} disabled onCheckedChange={onCheckedChange} />)
|
||||
@ -142,6 +155,24 @@ describe('Switch', () => {
|
||||
expect(screen.container.querySelector('span[aria-hidden="true"] i')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use checked data attributes to position spinner', async () => {
|
||||
const screen = await render(<Switch checked={false} loading size="md" />)
|
||||
const spinner = screen.container.querySelector('span[aria-hidden="true"]')
|
||||
|
||||
expect(spinner).toHaveClass(
|
||||
'left-[calc(50%+6px)]',
|
||||
'group-data-checked:left-[calc(50%-6px)]',
|
||||
)
|
||||
|
||||
await screen.rerender(<Switch checked={true} loading size="md" />)
|
||||
|
||||
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-checked', '')
|
||||
expect(screen.container.querySelector('span[aria-hidden="true"]')).toHaveClass(
|
||||
'left-[calc(50%+6px)]',
|
||||
'group-data-checked:left-[calc(50%-6px)]',
|
||||
)
|
||||
})
|
||||
|
||||
it('should not show spinner for xs and sm sizes', async () => {
|
||||
const screen = await render(<Switch checked={false} loading size="xs" />)
|
||||
expect(screen.container.querySelector('span[aria-hidden="true"] i')).not.toBeInTheDocument()
|
||||
|
||||
@ -2,6 +2,11 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { ComponentProps } from 'react'
|
||||
import { useState, useTransition } from 'react'
|
||||
import { Switch, SwitchSkeleton } from '.'
|
||||
import {
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
FieldRoot,
|
||||
} from '../field'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Form/Switch',
|
||||
@ -10,7 +15,7 @@ const meta = {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Toggle switch built on Base UI with CVA variants, Figma-aligned design tokens, loading spinner, and skeleton placeholder. Import `Switch` and `SwitchSkeleton` from `@langgenius/dify-ui/switch`.',
|
||||
component: 'Toggle switch primitive with controlled and uncontrolled state support, loading state, and skeleton placeholder.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -42,20 +47,27 @@ const meta = {
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const SwitchDemo = (args: Partial<ComponentProps<typeof Switch>>) => {
|
||||
type SwitchDemoProps = Partial<Omit<ComponentProps<typeof Switch>, 'checked' | 'defaultChecked' | 'onCheckedChange'>> & {
|
||||
checked?: boolean
|
||||
}
|
||||
|
||||
const SwitchDemo = (args: SwitchDemoProps) => {
|
||||
const [enabled, setEnabled] = useState(args.checked ?? false)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Switch
|
||||
{...args}
|
||||
checked={enabled}
|
||||
onCheckedChange={setEnabled}
|
||||
/>
|
||||
<span className="text-sm text-gray-700">
|
||||
{enabled ? 'On' : 'Off'}
|
||||
</span>
|
||||
</div>
|
||||
<FieldRoot name="autoRetry" className="w-72">
|
||||
<FieldLabel className="flex items-center justify-between gap-3">
|
||||
<span>Enable auto retry</span>
|
||||
<Switch
|
||||
{...args}
|
||||
checked={enabled}
|
||||
onCheckedChange={setEnabled}
|
||||
/>
|
||||
</FieldLabel>
|
||||
<FieldDescription>
|
||||
{enabled ? 'Failures will retry automatically.' : 'Failures require manual retry.'}
|
||||
</FieldDescription>
|
||||
</FieldRoot>
|
||||
)
|
||||
}
|
||||
|
||||
@ -116,24 +128,24 @@ const AllStatesDemo = () => {
|
||||
<td className="py-3 font-medium text-gray-900">{size}</td>
|
||||
<td className="py-3">
|
||||
<div className="flex gap-2">
|
||||
<Switch size={size} checked={false} onCheckedChange={() => {}} />
|
||||
<Switch size={size} checked={true} onCheckedChange={() => {}} />
|
||||
<Switch size={size} checked={false} onCheckedChange={() => {}} aria-label={`${size} unchecked switch`} />
|
||||
<Switch size={size} checked={true} onCheckedChange={() => {}} aria-label={`${size} checked switch`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<div className="flex gap-2">
|
||||
<Switch size={size} checked={false} disabled />
|
||||
<Switch size={size} checked={true} disabled />
|
||||
<Switch size={size} checked={false} disabled aria-label={`${size} disabled unchecked switch`} />
|
||||
<Switch size={size} checked={true} disabled aria-label={`${size} disabled checked switch`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<div className="flex gap-2">
|
||||
<Switch size={size} checked={false} loading />
|
||||
<Switch size={size} checked={true} loading />
|
||||
<Switch size={size} checked={false} loading aria-label={`${size} loading unchecked switch`} />
|
||||
<Switch size={size} checked={true} loading aria-label={`${size} loading checked switch`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3">
|
||||
<SwitchSkeleton size={size} />
|
||||
<SwitchSkeleton size={size} aria-hidden="true" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@ -148,7 +160,7 @@ export const AllStates: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Complete variant matrix: all sizes × all states, matching Figma design spec (node 2144:1210).',
|
||||
story: 'Variant matrix for switch sizes and states.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -164,22 +176,30 @@ const SizeComparisonDemo = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="xs" checked={states.xs} onCheckedChange={v => setStates({ ...states, xs: v })} />
|
||||
<span className="text-sm text-gray-700">Extra Small (xs) — 14×10</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="sm" checked={states.sm} onCheckedChange={v => setStates({ ...states, sm: v })} />
|
||||
<span className="text-sm text-gray-700">Small (sm) — 20×12</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="md" checked={states.md} onCheckedChange={v => setStates({ ...states, md: v })} />
|
||||
<span className="text-sm text-gray-700">Regular (md) — 28×16</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={states.lg} onCheckedChange={v => setStates({ ...states, lg: v })} />
|
||||
<span className="text-sm text-gray-700">Large (lg) — 36×20</span>
|
||||
</div>
|
||||
<FieldRoot name="extraSmallSwitch">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="xs" checked={states.xs} onCheckedChange={v => setStates({ ...states, xs: v })} />
|
||||
Extra Small (xs) - 14x10
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="smallSwitch">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="sm" checked={states.sm} onCheckedChange={v => setStates({ ...states, sm: v })} />
|
||||
Small (sm) - 20x12
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="regularSwitch">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="md" checked={states.md} onCheckedChange={v => setStates({ ...states, md: v })} />
|
||||
Regular (md) - 28x16
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="largeSwitch">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={states.lg} onCheckedChange={v => setStates({ ...states, lg: v })} />
|
||||
Large (lg) - 36x20
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -200,30 +220,42 @@ const LoadingDemo = () => {
|
||||
{loading ? 'Stop Loading' : 'Start Loading'}
|
||||
</button>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={false} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Large unchecked</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={true} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Large checked</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="md" checked={false} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Regular unchecked</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="md" checked={true} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Regular checked</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="sm" checked={false} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Small (no spinner)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch size="xs" checked={false} loading={loading} />
|
||||
<span className="text-sm text-gray-700">Extra Small (no spinner)</span>
|
||||
</div>
|
||||
<FieldRoot name="largeUncheckedLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={false} loading={loading} />
|
||||
Large unchecked
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="largeCheckedLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="lg" checked={true} loading={loading} />
|
||||
Large checked
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="regularUncheckedLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="md" checked={false} loading={loading} />
|
||||
Regular unchecked
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="regularCheckedLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="md" checked={true} loading={loading} />
|
||||
Regular checked
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="smallLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="sm" checked={false} loading={loading} />
|
||||
Small
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="extraSmallLoading">
|
||||
<FieldLabel className="flex items-center gap-3">
|
||||
<Switch size="xs" checked={false} loading={loading} />
|
||||
Extra Small
|
||||
</FieldLabel>
|
||||
</FieldRoot>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -234,7 +266,7 @@ export const Loading: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Loading state disables interaction and shows a spinning icon (i-ri-loader-2-line) for md/lg sizes. Spinner position mirrors the knob: appears on the opposite side of the checked state.',
|
||||
story: 'Loading state disables interaction and shows a spinner for md and lg sizes.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -242,61 +274,76 @@ export const Loading: Story = {
|
||||
|
||||
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
const MutationLoadingDemo = () => {
|
||||
function useMockAutoRetrySettingQuery() {
|
||||
const [enabled, setEnabled] = useState(false)
|
||||
|
||||
return {
|
||||
data: {
|
||||
enabled,
|
||||
},
|
||||
setData: setEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
function useMockUpdateAutoRetrySettingMutation({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess: (enabled: boolean) => void
|
||||
}) {
|
||||
const [requestCount, setRequestCount] = useState(0)
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const handleChange = (nextValue: boolean) => {
|
||||
const mutate = (nextValue: boolean) => {
|
||||
if (isPending)
|
||||
return
|
||||
|
||||
startTransition(async () => {
|
||||
setRequestCount(current => current + 1)
|
||||
await wait(1200)
|
||||
setEnabled(nextValue)
|
||||
onSuccess(nextValue)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
requestCount,
|
||||
isPending,
|
||||
mutate,
|
||||
}
|
||||
}
|
||||
|
||||
const MutationLoadingDemo = () => {
|
||||
const autoRetrySetting = useMockAutoRetrySettingQuery()
|
||||
const updateAutoRetrySetting = useMockUpdateAutoRetrySettingMutation({
|
||||
onSuccess: autoRetrySetting.setData,
|
||||
})
|
||||
const statusText = updateAutoRetrySetting.isPending
|
||||
? 'Saving changes...'
|
||||
: autoRetrySetting.data.enabled
|
||||
? 'Auto retry is enabled.'
|
||||
: 'Auto retry is disabled.'
|
||||
|
||||
return (
|
||||
<div className="w-[340px] space-y-4 rounded-2xl border border-components-panel-border bg-components-panel-bg p-4 shadow-sm">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-text-primary">Mutation Loading Guard</p>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
Click once to start a simulated mutate call. While the request is pending, the switch enters
|
||||
{' '}
|
||||
<code className="rounded-sm bg-state-base-hover px-1 py-0.5 text-[11px]">loading</code>
|
||||
{' '}
|
||||
and rejects duplicate clicks.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid w-90 gap-3 rounded-lg border border-components-panel-border bg-components-panel-bg p-4 shadow-sm">
|
||||
<FieldRoot name="autoRetry">
|
||||
<FieldLabel className="flex items-center justify-between gap-4">
|
||||
<span className="system-sm-medium text-text-secondary">Enable auto retry</span>
|
||||
<Switch
|
||||
size="lg"
|
||||
checked={autoRetrySetting.data.enabled}
|
||||
loading={updateAutoRetrySetting.isPending}
|
||||
onCheckedChange={updateAutoRetrySetting.mutate}
|
||||
/>
|
||||
</FieldLabel>
|
||||
<FieldDescription>Retry failed workflow runs without manual intervention.</FieldDescription>
|
||||
</FieldRoot>
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border border-components-panel-border-subtle bg-background-default-dodge px-3 py-2 shadow-sm">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-text-primary">Enable Auto Retry</p>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
{isPending ? 'Saving…' : enabled ? 'Saved as on' : 'Saved as off'}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
size="lg"
|
||||
checked={enabled}
|
||||
loading={isPending}
|
||||
onCheckedChange={handleChange}
|
||||
aria-label="Enable Auto Retry"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-text-tertiary">
|
||||
<div className="rounded-lg bg-state-base-hover px-3 py-2">
|
||||
<div className="font-medium text-text-secondary">Committed Value</div>
|
||||
<div>{enabled ? 'On' : 'Off'}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-state-base-hover px-3 py-2">
|
||||
<div className="font-medium text-text-secondary">Mutate Count</div>
|
||||
<div>{requestCount}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-text-tertiary" aria-live="polite">
|
||||
{statusText}
|
||||
{' '}
|
||||
Save attempts:
|
||||
{' '}
|
||||
{updateAutoRetrySetting.requestCount}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -306,7 +353,7 @@ export const MutationLoadingGuard: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Simulates a controlled switch backed by an async mutate call. The component keeps its previous committed value, sets `loading` during the request, and blocks duplicate clicks until the mutation resolves.',
|
||||
story: 'Controlled switch that enters loading while the change is saved.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -315,19 +362,19 @@ export const MutationLoadingGuard: Story = {
|
||||
const SkeletonDemo = () => (
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<SwitchSkeleton size="xs" />
|
||||
<SwitchSkeleton size="xs" aria-hidden="true" />
|
||||
<span className="text-sm text-gray-700">Extra Small skeleton</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<SwitchSkeleton size="sm" />
|
||||
<SwitchSkeleton size="sm" aria-hidden="true" />
|
||||
<span className="text-sm text-gray-700">Small skeleton</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<SwitchSkeleton size="md" />
|
||||
<SwitchSkeleton size="md" aria-hidden="true" />
|
||||
<span className="text-sm text-gray-700">Regular skeleton</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<SwitchSkeleton size="lg" />
|
||||
<SwitchSkeleton size="lg" aria-hidden="true" />
|
||||
<span className="text-sm text-gray-700">Large skeleton</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -338,7 +385,7 @@ export const Skeleton: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: '`SwitchSkeleton` renders a non-interactive placeholder with `bg-text-quaternary opacity-20`. Exported from `@langgenius/dify-ui/switch` alongside `Switch`.',
|
||||
story: 'Non-interactive placeholders for switch loading layouts.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -45,26 +45,34 @@ const switchThumbVariants = cva(
|
||||
|
||||
export type SwitchSize = NonNullable<VariantProps<typeof switchRootVariants>['size']>
|
||||
|
||||
const spinnerSizeConfig: Partial<Record<SwitchSize, {
|
||||
icon: string
|
||||
uncheckedPosition: string
|
||||
checkedPosition: string
|
||||
}>> = {
|
||||
md: {
|
||||
icon: 'size-2',
|
||||
uncheckedPosition: 'left-[calc(50%+6px)]',
|
||||
checkedPosition: 'left-[calc(50%-6px)]',
|
||||
},
|
||||
lg: {
|
||||
icon: 'size-2.5',
|
||||
uncheckedPosition: 'left-[calc(50%+8px)]',
|
||||
checkedPosition: 'left-[calc(50%-8px)]',
|
||||
const switchSpinnerVariants = cva(
|
||||
'absolute top-1/2 -translate-x-1/2 -translate-y-1/2',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
md: 'size-2 left-[calc(50%+6px)] group-data-checked:left-[calc(50%-6px)]',
|
||||
lg: 'size-2.5 left-[calc(50%+8px)] group-data-checked:left-[calc(50%-8px)]',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
type ControlledSwitchProps = {
|
||||
checked: boolean
|
||||
defaultChecked?: never
|
||||
}
|
||||
|
||||
type UncontrolledSwitchProps = {
|
||||
checked?: never
|
||||
defaultChecked?: boolean
|
||||
}
|
||||
|
||||
type SwitchControlProps = ControlledSwitchProps | UncontrolledSwitchProps
|
||||
|
||||
export type SwitchProps
|
||||
= Omit<BaseSwitchNS.Root.Props, 'className' | 'size' | 'onCheckedChange'>
|
||||
= Omit<BaseSwitchNS.Root.Props, 'checked' | 'defaultChecked' | 'className' | 'size' | 'onCheckedChange'>
|
||||
& VariantProps<typeof switchRootVariants>
|
||||
& SwitchControlProps
|
||||
& {
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
loading?: boolean
|
||||
@ -81,7 +89,6 @@ export function Switch({
|
||||
...props
|
||||
}: SwitchProps) {
|
||||
const isDisabled = disabled || loading
|
||||
const spinner = loading && size ? spinnerSizeConfig[size] : undefined
|
||||
|
||||
return (
|
||||
<BaseSwitch.Root
|
||||
@ -95,14 +102,10 @@ export function Switch({
|
||||
<BaseSwitch.Thumb
|
||||
className={switchThumbVariants({ size })}
|
||||
/>
|
||||
{spinner
|
||||
{loading && (size === 'md' || size === 'lg')
|
||||
? (
|
||||
<span
|
||||
className={cn(
|
||||
'absolute top-1/2 -translate-x-1/2 -translate-y-1/2',
|
||||
spinner.icon,
|
||||
checked ? spinner.checkedPosition : spinner.uncheckedPosition,
|
||||
)}
|
||||
className={switchSpinnerVariants({ size })}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<i className="i-ri-loader-2-line size-full animate-spin text-text-tertiary motion-reduce:animate-none" />
|
||||
@ -131,11 +134,8 @@ const switchSkeletonVariants = cva(
|
||||
)
|
||||
|
||||
export type SwitchSkeletonProps
|
||||
= Omit<HTMLAttributes<HTMLDivElement>, 'className'>
|
||||
= HTMLAttributes<HTMLDivElement>
|
||||
& VariantProps<typeof switchSkeletonVariants>
|
||||
& {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SwitchSkeleton({
|
||||
size = 'md',
|
||||
|
||||
@ -1,133 +0,0 @@
|
||||
import { render } from 'vitest-browser-react'
|
||||
import {
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
FieldRoot,
|
||||
} from '../../field'
|
||||
import { Form } from '../../form'
|
||||
import { Textarea } from '../index'
|
||||
|
||||
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
|
||||
const setTextareaValue = (element: HTMLElement | SVGElement, value: string) => {
|
||||
const textarea = asHTMLElement(element) as HTMLTextAreaElement
|
||||
const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
|
||||
valueSetter?.call(textarea, value)
|
||||
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
|
||||
describe('Textarea', () => {
|
||||
it('should render a labelled textarea through Base UI Field.Control', async () => {
|
||||
const screen = await render(
|
||||
<FieldRoot name="description">
|
||||
<FieldLabel>Description</FieldLabel>
|
||||
<Textarea defaultValue="A workspace for support automation." />
|
||||
<FieldDescription>Shown to workspace members.</FieldDescription>
|
||||
</FieldRoot>,
|
||||
)
|
||||
|
||||
const textarea = screen.getByRole('textbox', { name: 'Description' })
|
||||
|
||||
await expect.element(textarea).toHaveValue('A workspace for support automation.')
|
||||
await expect.element(textarea).toHaveAccessibleDescription('Shown to workspace members.')
|
||||
await expect.element(textarea).toHaveClass('min-h-20', 'rounded-lg', 'system-sm-regular')
|
||||
expect(asHTMLElement(textarea.element()).tagName).toBe('TEXTAREA')
|
||||
})
|
||||
|
||||
it('should apply size variants and custom classes', async () => {
|
||||
const screen = await render(
|
||||
<label>
|
||||
Prompt
|
||||
<Textarea size="large" className="resize-none" />
|
||||
</label>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByRole('textbox', { name: 'Prompt' })).toHaveClass(
|
||||
'rounded-[10px]',
|
||||
'px-4',
|
||||
'py-2',
|
||||
'system-md-regular',
|
||||
'resize-none',
|
||||
)
|
||||
})
|
||||
|
||||
it('should call onValueChange and stay controlled until value changes', async () => {
|
||||
const onValueChange = vi.fn()
|
||||
const screen = await render(
|
||||
<label>
|
||||
Notes
|
||||
<Textarea value="" onValueChange={onValueChange} />
|
||||
</label>,
|
||||
)
|
||||
|
||||
const textarea = screen.getByRole('textbox', { name: 'Notes' })
|
||||
setTextareaValue(textarea.element(), 'a')
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledWith('a', expect.any(Object))
|
||||
await expect.element(textarea).toHaveValue('')
|
||||
|
||||
await screen.rerender(
|
||||
<label>
|
||||
Notes
|
||||
<Textarea value="a" onValueChange={onValueChange} />
|
||||
</label>,
|
||||
)
|
||||
await expect.element(screen.getByRole('textbox', { name: 'Notes' })).toHaveValue('a')
|
||||
})
|
||||
|
||||
it('should submit valid values and show validation errors through Base UI Form', async () => {
|
||||
const onFormSubmit = vi.fn()
|
||||
const screen = await render(
|
||||
<Form aria-label="dataset form" onFormSubmit={onFormSubmit}>
|
||||
<FieldRoot name="summary">
|
||||
<FieldLabel>Summary</FieldLabel>
|
||||
<Textarea required minLength={10} />
|
||||
<FieldError match="valueMissing">Summary is required.</FieldError>
|
||||
<FieldError match="tooShort">Summary is too short.</FieldError>
|
||||
</FieldRoot>
|
||||
<button type="submit">Save</button>
|
||||
</Form>,
|
||||
)
|
||||
|
||||
const saveButton = asHTMLElement(screen.getByRole('button', { name: 'Save' }).element())
|
||||
saveButton.click()
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
await expect.element(screen.getByText('Summary is required.')).toBeInTheDocument()
|
||||
await expect.element(screen.getByRole('textbox', { name: 'Summary' })).toHaveAttribute('aria-invalid', 'true')
|
||||
})
|
||||
expect(onFormSubmit).not.toHaveBeenCalled()
|
||||
|
||||
await screen.rerender(
|
||||
<Form aria-label="dataset form" onFormSubmit={onFormSubmit}>
|
||||
<FieldRoot name="summary">
|
||||
<FieldLabel>Summary</FieldLabel>
|
||||
<Textarea key="valid-summary" required minLength={10} defaultValue="Long enough summary" />
|
||||
<FieldError match="valueMissing">Summary is required.</FieldError>
|
||||
<FieldError match="tooShort">Summary is too short.</FieldError>
|
||||
</FieldRoot>
|
||||
<button type="submit">Save</button>
|
||||
</Form>,
|
||||
)
|
||||
|
||||
asHTMLElement(screen.getByRole('button', { name: 'Save' }).element()).click()
|
||||
expect(onFormSubmit).toHaveBeenCalledTimes(1)
|
||||
expect(onFormSubmit.mock.calls[0]?.[0]).toMatchObject({ summary: 'Long enough summary' })
|
||||
})
|
||||
|
||||
it('should render character count when maxLength is set', async () => {
|
||||
const screen = await render(
|
||||
<label>
|
||||
Release notes
|
||||
<Textarea defaultValue="Draft" maxLength={20} />
|
||||
</label>,
|
||||
)
|
||||
|
||||
await expect.element(screen.getByText('5/20')).toBeInTheDocument()
|
||||
const textarea = screen.getByRole('textbox', { name: 'Release notes' })
|
||||
await expect.element(textarea).toHaveClass('pb-7')
|
||||
|
||||
setTextareaValue(textarea.element(), 'Published')
|
||||
await expect.element(screen.getByText('9/20')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -1,141 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '../button'
|
||||
import {
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
FieldRoot,
|
||||
} from '../field'
|
||||
import { Form } from '../form'
|
||||
import { Textarea } from './index'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Form/Textarea',
|
||||
component: Textarea,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Multiline text control built on Base UI Field.Control. Use it with FieldRoot for labelled, described, and validated form fields.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Textarea>
|
||||
|
||||
export default meta
|
||||
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => (
|
||||
<div className="w-80">
|
||||
<label htmlFor="workspace-description" className="mb-1 block w-fit py-1 text-text-secondary system-sm-medium">
|
||||
Workspace description
|
||||
</label>
|
||||
<Textarea
|
||||
id="workspace-description"
|
||||
name="workspaceDescription"
|
||||
placeholder="Describe how this workspace is used..."
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-80 gap-3">
|
||||
<label className="grid gap-1 text-text-secondary system-sm-medium" htmlFor="small-textarea">
|
||||
Small
|
||||
<Textarea id="small-textarea" size="small" name="smallTextarea" placeholder="Short note..." rows={3} />
|
||||
</label>
|
||||
<label className="grid gap-1 text-text-secondary system-sm-medium" htmlFor="medium-textarea">
|
||||
Medium
|
||||
<Textarea id="medium-textarea" name="mediumTextarea" placeholder="Add context..." rows={3} />
|
||||
</label>
|
||||
<label className="grid gap-1 text-text-secondary system-sm-medium" htmlFor="large-textarea">
|
||||
Large
|
||||
<Textarea id="large-textarea" size="large" name="largeTextarea" placeholder="Write a longer instruction..." rows={3} />
|
||||
</label>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const States: Story = {
|
||||
render: () => (
|
||||
<div className="grid w-80 gap-3">
|
||||
<FieldRoot name="placeholderState">
|
||||
<FieldLabel>Placeholder</FieldLabel>
|
||||
<Textarea placeholder="Add a description..." rows={3} />
|
||||
</FieldRoot>
|
||||
<FieldRoot name="filledState">
|
||||
<FieldLabel>Filled</FieldLabel>
|
||||
<Textarea defaultValue="Use this dataset for support articles and product FAQs." rows={3} />
|
||||
</FieldRoot>
|
||||
<FieldRoot name="invalidState" invalid>
|
||||
<FieldLabel>Invalid</FieldLabel>
|
||||
<Textarea defaultValue="Too short" rows={3} />
|
||||
<FieldError match>Use at least 20 characters.</FieldError>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="disabledState">
|
||||
<FieldLabel>Disabled</FieldLabel>
|
||||
<Textarea disabled placeholder="Editing is unavailable..." rows={3} />
|
||||
</FieldRoot>
|
||||
<FieldRoot name="readonlyState">
|
||||
<FieldLabel>Read-only</FieldLabel>
|
||||
<Textarea readOnly defaultValue="Generated from the published workflow configuration." rows={3} />
|
||||
</FieldRoot>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithField: Story = {
|
||||
render: () => (
|
||||
<Form aria-label="Dataset settings" className="grid w-80 gap-4" onFormSubmit={() => undefined}>
|
||||
<FieldRoot name="description">
|
||||
<FieldLabel>Description</FieldLabel>
|
||||
<Textarea
|
||||
required
|
||||
minLength={20}
|
||||
maxLength={160}
|
||||
placeholder="Describe what this dataset contains..."
|
||||
rows={4}
|
||||
className="resize-y"
|
||||
/>
|
||||
<FieldDescription>Shown to teammates when they choose a knowledge source.</FieldDescription>
|
||||
<FieldError match="valueMissing">Description is required.</FieldError>
|
||||
<FieldError match="tooShort">Use at least 20 characters.</FieldError>
|
||||
</FieldRoot>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" variant="primary">Save Settings</Button>
|
||||
</div>
|
||||
</Form>
|
||||
),
|
||||
}
|
||||
|
||||
const ControlledDemo = () => {
|
||||
const [value, setValue] = useState('Summarize customer feedback into actionable product themes.')
|
||||
|
||||
return (
|
||||
<FieldRoot name="prompt">
|
||||
<FieldLabel>Prompt</FieldLabel>
|
||||
<Textarea
|
||||
value={value}
|
||||
onValueChange={setValue}
|
||||
maxLength={120}
|
||||
rows={4}
|
||||
className="resize-y"
|
||||
/>
|
||||
<FieldDescription>The saved value is updated from the controlled state.</FieldDescription>
|
||||
</FieldRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => (
|
||||
<div className="w-80">
|
||||
<ControlledDemo />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
@ -1,115 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { Field as BaseFieldNS } from '@base-ui/react/field'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import type { ComponentPropsWithRef } from 'react'
|
||||
import { Field as BaseField } from '@base-ui/react/field'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { useState } from 'react'
|
||||
import { cn } from '../cn'
|
||||
|
||||
const textareaVariants = cva(
|
||||
[
|
||||
'min-h-20 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]',
|
||||
'placeholder:text-components-input-text-placeholder',
|
||||
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
'focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs',
|
||||
'data-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
|
||||
'read-only:cursor-default read-only:shadow-none read-only:hover:border-transparent read-only:hover:bg-components-input-bg-normal read-only:focus:border-transparent read-only:focus:bg-components-input-bg-normal read-only:focus:shadow-none',
|
||||
'disabled:cursor-not-allowed disabled:border-transparent disabled:bg-components-input-bg-disabled disabled:text-components-input-text-filled-disabled',
|
||||
'disabled:hover:border-transparent disabled:hover:bg-components-input-bg-disabled',
|
||||
'motion-reduce:transition-none',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
small: 'rounded-md px-2 py-1 system-xs-regular',
|
||||
medium: 'rounded-lg px-3 py-2 system-sm-regular',
|
||||
large: 'rounded-[10px] px-4 py-2 system-md-regular',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'medium',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
type TextareaValue = string | number
|
||||
export type TextareaSize = NonNullable<VariantProps<typeof textareaVariants>['size']>
|
||||
export type TextareaChangeEventDetails = BaseFieldNS.Control.ChangeEventDetails
|
||||
type TextareaOnValueChange = (value: string, eventDetails: TextareaChangeEventDetails) => void
|
||||
|
||||
type ControlledTextareaProps = {
|
||||
value: TextareaValue
|
||||
defaultValue?: never
|
||||
onValueChange: TextareaOnValueChange
|
||||
}
|
||||
|
||||
type UncontrolledTextareaProps = {
|
||||
value?: never
|
||||
defaultValue?: TextareaValue
|
||||
onValueChange?: TextareaOnValueChange
|
||||
}
|
||||
|
||||
type NativeTextareaProps = Omit<
|
||||
ComponentPropsWithRef<'textarea'>,
|
||||
'children' | 'className' | 'defaultValue' | 'onChange' | 'size' | 'value'
|
||||
>
|
||||
|
||||
type TextareaControlProps = ControlledTextareaProps | UncontrolledTextareaProps
|
||||
type TextareaVariantProps = VariantProps<typeof textareaVariants>
|
||||
|
||||
export type TextareaProps
|
||||
= NativeTextareaProps
|
||||
& TextareaControlProps
|
||||
& TextareaVariantProps
|
||||
& {
|
||||
children?: never
|
||||
className?: string
|
||||
}
|
||||
|
||||
function getTextareaValueLength(value: TextareaValue | undefined) {
|
||||
return String(value ?? '').length
|
||||
}
|
||||
|
||||
export function Textarea({
|
||||
className,
|
||||
defaultValue,
|
||||
maxLength,
|
||||
onValueChange,
|
||||
ref,
|
||||
size = 'medium',
|
||||
value,
|
||||
...props
|
||||
}: TextareaProps) {
|
||||
const showCharacterCount = maxLength !== undefined
|
||||
const [uncontrolledValueLength, setUncontrolledValueLength] = useState(() => getTextareaValueLength(defaultValue))
|
||||
const valueLength = value === undefined ? uncontrolledValueLength : getTextareaValueLength(value)
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<BaseField.Control
|
||||
className={cn(textareaVariants({ size }), showCharacterCount && 'pb-7', className)}
|
||||
defaultValue={defaultValue}
|
||||
maxLength={maxLength}
|
||||
onValueChange={(nextValue, eventDetails) => {
|
||||
if (showCharacterCount && value === undefined)
|
||||
setUncontrolledValueLength(nextValue.length)
|
||||
|
||||
onValueChange?.(nextValue, eventDetails)
|
||||
}}
|
||||
render={<textarea {...props} ref={ref} />}
|
||||
value={value}
|
||||
/>
|
||||
{showCharacterCount
|
||||
? (
|
||||
<span className="pointer-events-none absolute right-2 bottom-2 rounded-sm bg-components-panel-bg px-1 py-0.5 text-text-tertiary system-2xs-medium">
|
||||
{valueLength}
|
||||
/
|
||||
{maxLength}
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useLogout } from '@/service/use-common'
|
||||
@ -63,12 +63,11 @@ export default function FeedBack(props: DeleteAccountProps) {
|
||||
</DialogTitle>
|
||||
<label className="mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label>
|
||||
<Textarea
|
||||
aria-label={t('account.feedbackLabel', { ns: 'common' }) as string}
|
||||
rows={6}
|
||||
value={userFeedback}
|
||||
placeholder={t('account.feedbackPlaceholder', { ns: 'common' }) as string}
|
||||
onValueChange={(value) => {
|
||||
setUserFeedback(value)
|
||||
onChange={(e) => {
|
||||
setUserFeedback(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<div className="mt-3 flex w-full flex-col gap-2">
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
export enum EditItemType {
|
||||
Query = 'query',
|
||||
@ -33,9 +33,8 @@ const EditItem: FC<Props> = ({
|
||||
<div className="grow">
|
||||
<div className="mb-1 system-xs-semibold text-text-primary">{name}</div>
|
||||
<Textarea
|
||||
aria-label={name}
|
||||
value={content}
|
||||
onValueChange={value => onChange(value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
import type { FC } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { RiDeleteBinLine, RiEditFill, RiEditLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
export enum EditItemType {
|
||||
Query = 'query',
|
||||
@ -130,9 +130,8 @@ const EditItem: FC<Props> = ({
|
||||
<div className="mt-3">
|
||||
<EditTitle title={editTitle} />
|
||||
<Textarea
|
||||
aria-label={editTitle}
|
||||
value={newContent}
|
||||
onValueChange={value => setNewContent(value)}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setNewContent(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@ -3,12 +3,12 @@ import type { VersionHistory } from '@/types/workflow'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from '../../base/textarea'
|
||||
|
||||
type VersionInfoModalProps = {
|
||||
isOpen: boolean
|
||||
@ -57,8 +57,8 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleDescriptionChange = useCallback((value: string) => {
|
||||
setReleaseNotes(value)
|
||||
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setReleaseNotes(e.target.value)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@ -95,16 +95,17 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
onValueChange={setTitle}
|
||||
/>
|
||||
</FieldRoot>
|
||||
<FieldRoot name="releaseNotes" invalid={releaseNotesError} className="gap-y-1">
|
||||
<FieldLabel className="flex h-6 items-center py-0 system-sm-semibold text-text-secondary">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex h-6 items-center system-sm-semibold text-text-secondary">
|
||||
{t('versionHistory.editField.releaseNotes', { ns: 'workflow' })}
|
||||
</FieldLabel>
|
||||
</div>
|
||||
<Textarea
|
||||
value={releaseNotes}
|
||||
placeholder={`${t('versionHistory.releaseNotesPlaceholder', { ns: 'workflow' })}${t('panel.optional', { ns: 'workflow' })}`}
|
||||
onValueChange={handleDescriptionChange}
|
||||
onChange={handleDescriptionChange}
|
||||
destructive={releaseNotesError}
|
||||
/>
|
||||
</FieldRoot>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end p-6 pt-5">
|
||||
<div className="flex items-center gap-x-3">
|
||||
|
||||
@ -13,12 +13,12 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
@ -121,9 +121,8 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
|
||||
{type === InputVarType.paragraph && (
|
||||
<Field title={t('variableConfig.defaultValue', { ns: 'appDebug' })}>
|
||||
<Textarea
|
||||
aria-label={t('variableConfig.defaultValue', { ns: 'appDebug' })}
|
||||
value={String(tempPayload.default ?? '')}
|
||||
onValueChange={value => onPayloadChange('default')(value || undefined)}
|
||||
onChange={e => onPayloadChange('default')(e.target.value || undefined)}
|
||||
placeholder={t('variableConfig.inputPlaceholder', { ns: 'appDebug' })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
const i18nPrefix = 'generate'
|
||||
|
||||
@ -40,11 +40,10 @@ const IdeaOutput: FC<Props> = ({
|
||||
</div>
|
||||
{!isFoldIdeaOutput && (
|
||||
<Textarea
|
||||
aria-label={t(`${i18nPrefix}.idealOutput`, { ns: 'appDebug' })}
|
||||
className="h-[80px]"
|
||||
placeholder={t(`${i18nPrefix}.idealOutputPlaceholder`, { ns: 'appDebug' })}
|
||||
value={value}
|
||||
onValueChange={value => onChange(value)}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -4,13 +4,13 @@ import type { DataSet } from '@/models/datasets'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
|
||||
import { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import IndexMethod from '@/app/components/datasets/settings/index-method'
|
||||
@ -224,9 +224,8 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Textarea
|
||||
aria-label={t('form.desc', { ns: 'datasetSettings' })}
|
||||
value={localeCurrentDataset.description || ''}
|
||||
onValueChange={value => handleValueChange('description', value)}
|
||||
onChange={e => handleValueChange('description', e.target.value)}
|
||||
className="resize-none"
|
||||
placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''}
|
||||
/>
|
||||
|
||||
@ -84,6 +84,25 @@ vi.mock('@langgenius/dify-ui/select', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/textarea', () => ({
|
||||
default: ({ value, onChange, placeholder, readOnly, className }: {
|
||||
value: string
|
||||
onChange: (e: { target: { value: string } }) => void
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
className?: string
|
||||
}) => (
|
||||
<textarea
|
||||
data-testid={`textarea-${placeholder}`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
className={className}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/before-run-form/bool-input', () => ({
|
||||
default: ({ name, value, required, onChange, readonly }: {
|
||||
name: string
|
||||
@ -204,7 +223,7 @@ describe('ChatUserInput', () => {
|
||||
}))
|
||||
|
||||
render(<ChatUserInput inputs={{}} />)
|
||||
expect(screen.getByRole('textbox', { name: 'Description' })).toBeInTheDocument()
|
||||
expect(screen.getByTestId('textarea-Description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render select input type', () => {
|
||||
@ -256,7 +275,7 @@ describe('ChatUserInput', () => {
|
||||
|
||||
render(<ChatUserInput inputs={{}} />)
|
||||
expect(screen.getByTestId('input-Name')).toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox', { name: 'Description' })).toBeInTheDocument()
|
||||
expect(screen.getByTestId('textarea-Description')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-input')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -315,7 +334,7 @@ describe('ChatUserInput', () => {
|
||||
}))
|
||||
|
||||
render(<ChatUserInput inputs={{ desc: 'Long text here' }} />)
|
||||
expect(screen.getByRole('textbox', { name: 'Description' })).toHaveValue('Long text here')
|
||||
expect(screen.getByTestId('textarea-Description')).toHaveValue('Long text here')
|
||||
})
|
||||
|
||||
it('should display existing input values for number type', () => {
|
||||
@ -399,7 +418,7 @@ describe('ChatUserInput', () => {
|
||||
}))
|
||||
|
||||
render(<ChatUserInput inputs={{}} />)
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'Description' }), { target: { value: 'New Description' } })
|
||||
fireEvent.change(screen.getByTestId('textarea-Description'), { target: { value: 'New Description' } })
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledWith({ desc: 'New Description' })
|
||||
})
|
||||
@ -507,7 +526,7 @@ describe('ChatUserInput', () => {
|
||||
}))
|
||||
|
||||
render(<ChatUserInput inputs={{}} />)
|
||||
expect(screen.getByRole('textbox', { name: 'Description' })).toHaveAttribute('readonly')
|
||||
expect(screen.getByTestId('textarea-Description')).toHaveAttribute('readonly')
|
||||
})
|
||||
|
||||
it('should disable select when readonly is true', () => {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import type { Inputs } from '@/models/debug'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
|
||||
@ -94,10 +94,9 @@ const ChatUserInput = ({
|
||||
{type === 'paragraph' && (
|
||||
<Textarea
|
||||
className="h-[120px] grow"
|
||||
aria-label={name || key}
|
||||
placeholder={name}
|
||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||
onValueChange={(value) => { handleInputValueChange(key, value) }}
|
||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -5,7 +5,6 @@ import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
@ -20,6 +19,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { AppModeEnum, ModelModeType } from '@/types/app'
|
||||
@ -151,11 +151,10 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
)}
|
||||
{type === 'paragraph' && (
|
||||
<Textarea
|
||||
aria-label={name}
|
||||
className="h-[120px] grow"
|
||||
placeholder={name}
|
||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||
onValueChange={(value) => { handleInputValueChange(key, value) }}
|
||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -4,7 +4,6 @@ import type { AppIconSelection } from '../../base/app-icon-picker'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
@ -14,6 +13,7 @@ import AppIcon from '@/app/components/base/app-icon'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -240,11 +240,10 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
</span>
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={t('newApp.captionDescription', { ns: 'app' })}
|
||||
className="resize-none"
|
||||
placeholder={t('newApp.appDescriptionPlaceholder', { ns: 'app' }) || ''}
|
||||
value={description}
|
||||
onValueChange={value => setDescription(value)}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,6 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import * as React from 'react'
|
||||
@ -19,6 +18,7 @@ import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { PremiumBadgeButton } from '@/app/components/base/premium-badge'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@ -289,10 +289,9 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
<div className="relative">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webDesc`, { ns: 'appOverview' })}</div>
|
||||
<Textarea
|
||||
aria-label={t(`${prefixSettings}.webDesc`, { ns: 'appOverview' })}
|
||||
className="mt-1"
|
||||
value={inputInfo.desc}
|
||||
onValueChange={onDesChange}
|
||||
onChange={e => onDesChange(e.target.value)}
|
||||
placeholder={t(`${prefixSettings}.webDescPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.webDescTip`, { ns: 'appOverview' })}</p>
|
||||
@ -465,10 +464,9 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}</p>
|
||||
<Textarea
|
||||
aria-label={t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}
|
||||
className="mt-1"
|
||||
value={inputInfo.customDisclaimer}
|
||||
onValueChange={value => setInputInfo(item => ({ ...item, customDisclaimer: value }))}
|
||||
onChange={onChange('customDisclaimer')}
|
||||
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -7,8 +7,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
|
||||
@ -72,10 +72,9 @@ const WorkflowHiddenInputFields = ({
|
||||
) {
|
||||
return (
|
||||
<Textarea
|
||||
aria-label={label}
|
||||
id={fieldId}
|
||||
value={typeof fieldValue === 'string' ? fieldValue : ''}
|
||||
onValueChange={value => onValueChange(variable.variable, value)}
|
||||
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => onValueChange(variable.variable, event.target.value)}
|
||||
placeholder={label}
|
||||
maxLength={variable.max_length}
|
||||
className="min-h-24"
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
@ -71,9 +71,8 @@ const InputsFormContent = ({ showTip }: Props) => {
|
||||
)}
|
||||
{form.type === InputVarType.paragraph && (
|
||||
<Textarea
|
||||
aria-label={form.label}
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onValueChange={value => handleFormChange(form.variable, value)}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
placeholder={form.label}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { ContentItemProps } from './type'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
const ContentItem = ({
|
||||
content,
|
||||
@ -42,10 +42,9 @@ const ContentItem = ({
|
||||
<div className="py-3">
|
||||
{formInputField.type === 'paragraph' && (
|
||||
<Textarea
|
||||
aria-label={fieldName}
|
||||
className="h-[104px] sm:text-xs"
|
||||
value={inputs[fieldName]!}
|
||||
onValueChange={(value) => { onInputChange(fieldName, value) }}
|
||||
onChange={(e) => { onInputChange(fieldName, e.target.value) }}
|
||||
data-testid="content-item-textarea"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -6,7 +6,6 @@ import type {
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import copy from 'copy-to-clipboard'
|
||||
@ -22,6 +21,7 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu
|
||||
import Log from '@/app/components/base/chat/chat/log'
|
||||
import AnnotationCtrlButton from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button'
|
||||
import NewAudioButton from '@/app/components/base/new-audio-button'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useChatContext } from '../context'
|
||||
|
||||
type OperationProps = {
|
||||
@ -391,11 +391,10 @@ function Operation({
|
||||
{t('feedback.content', { ns: 'common' }) || 'Feedback Content'}
|
||||
</label>
|
||||
<Textarea
|
||||
aria-label={t('feedback.title', { ns: 'common' }) || 'Feedback'}
|
||||
id={feedbackTextareaId}
|
||||
name="feedback-content"
|
||||
value={feedbackContent}
|
||||
onValueChange={value => setFeedbackContent(value)}
|
||||
onChange={e => setFeedbackContent(e.target.value)}
|
||||
placeholder={t('feedback.placeholder', { ns: 'common' }) || 'Please describe what went wrong or how we can improve…'}
|
||||
rows={4}
|
||||
className="w-full"
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
@ -71,9 +71,8 @@ const InputsFormContent = ({ showTip }: Props) => {
|
||||
)}
|
||||
{form.type === InputVarType.paragraph && (
|
||||
<Textarea
|
||||
aria-label={form.label}
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onValueChange={value => handleFormChange(form.variable, value)}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
placeholder={form.label}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -12,10 +12,10 @@ 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'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
@ -220,10 +220,9 @@ const FollowUpSettingModal = ({
|
||||
</div>
|
||||
{promptMode === PROMPT_MODE.custom && (
|
||||
<Textarea
|
||||
aria-label={t('feature.suggestedQuestionsAfterAnswer.modal.customPromptOption', { ns: 'appDebug' })}
|
||||
className="mt-3 min-h-32 resize-y border-components-input-border-active bg-components-input-bg-normal"
|
||||
value={prompt}
|
||||
onValueChange={value => setPrompt(value)}
|
||||
onChange={e => setPrompt(e.target.value)}
|
||||
maxLength={CUSTOM_FOLLOW_UP_PROMPT_MAX_LENGTH}
|
||||
placeholder={t('feature.suggestedQuestionsAfterAnswer.modal.promptPlaceholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
|
||||
@ -2,7 +2,7 @@ import type { FC } from 'react'
|
||||
import type { CodeBasedExtensionForm } from '@/models/common'
|
||||
import type { ModerationConfig } from '@/models/debug'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
|
||||
type FormGenerationProps = {
|
||||
@ -55,11 +55,10 @@ const FormGeneration: FC<FormGenerationProps> = ({
|
||||
form.type === 'paragraph' && (
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
aria-label={locale === 'zh-Hans' ? form.label['zh-Hans'] : form.label['en-US']}
|
||||
className="resize-none"
|
||||
value={value?.[form.variable] || ''}
|
||||
placeholder={form.placeholder}
|
||||
onValueChange={value => handleFormChange(form.variable, value)}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import type { TextareaProps } from '@langgenius/dify-ui/textarea'
|
||||
import type { TextareaProps } from '../../../textarea'
|
||||
import type { LabelProps } from '../label'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { useFieldContext } from '../..'
|
||||
import Textarea from '../../../textarea'
|
||||
import Label from '../label'
|
||||
|
||||
type TextAreaFieldProps = {
|
||||
label: string
|
||||
labelOptions?: Omit<LabelProps, 'htmlFor' | 'label'>
|
||||
className?: string
|
||||
} & Omit<TextareaProps, 'className' | 'defaultValue' | 'onBlur' | 'value' | 'id'>
|
||||
} & Omit<TextareaProps, 'className' | 'onChange' | 'onBlur' | 'value' | 'id'>
|
||||
|
||||
const TextAreaField = ({
|
||||
label,
|
||||
@ -28,10 +28,9 @@ const TextAreaField = ({
|
||||
{...(labelOptions ?? {})}
|
||||
/>
|
||||
<Textarea
|
||||
aria-label={label}
|
||||
id={field.name}
|
||||
value={field.state.value}
|
||||
onValueChange={value => field.handleChange(value)}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
{...inputProps}
|
||||
/>
|
||||
|
||||
@ -3,7 +3,6 @@ import type { Dayjs } from 'dayjs'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Checkbox } from '@langgenius/dify-ui/checkbox'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger, SelectValue } from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useChatContext } from '@/app/components/base/chat/chat/context'
|
||||
@ -11,6 +10,7 @@ import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
|
||||
import { formatDateForOutput, toDayjs } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
const DATA_FORMAT = {
|
||||
TEXT: 'text',
|
||||
@ -372,12 +372,11 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
|
||||
return null
|
||||
return (
|
||||
<Textarea
|
||||
aria-label={name}
|
||||
key={key}
|
||||
name={name}
|
||||
placeholder={str(child.properties.placeholder)}
|
||||
value={str(formValues[name])}
|
||||
onValueChange={value => updateValue(name, value)}
|
||||
onChange={e => updateValue(name, e.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import Textarea from '../../../textarea'
|
||||
import TagLabel from './tag-label'
|
||||
import TypeSwitch from './type-switch'
|
||||
|
||||
@ -72,7 +72,6 @@ const PrePopulate: FC<Props> = ({
|
||||
value,
|
||||
onValueChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [onPlaceholderClicked, setOnPlaceholderClicked] = useState(false)
|
||||
const handleTypeChange = useCallback((isVar: boolean) => {
|
||||
setOnPlaceholderClicked(true)
|
||||
@ -128,10 +127,9 @@ const PrePopulate: FC<Props> = ({
|
||||
return (
|
||||
<div className={cn('relative min-h-[80px] rounded-lg border border-transparent bg-components-input-bg-normal pb-1', isFocus && 'border-components-input-border-active bg-components-input-bg-active shadow-xs')}>
|
||||
<Textarea
|
||||
aria-label={t(`${i18nPrefix}.staticContent`, { ns: 'workflow' })}
|
||||
value={value || ''}
|
||||
className="h-[43px] min-h-[43px] rounded-none border-none bg-transparent px-3 hover:bg-transparent focus:bg-transparent focus:shadow-none"
|
||||
onValueChange={value => onValueChange?.(value)}
|
||||
onChange={e => onValueChange?.(e.target.value)}
|
||||
onFocus={() => {
|
||||
setOnPlaceholderClicked(true)
|
||||
setIsFocus(true)
|
||||
|
||||
77
web/app/components/base/textarea/__tests__/index.spec.tsx
Normal file
77
web/app/components/base/textarea/__tests__/index.spec.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import TextArea from '../index'
|
||||
|
||||
describe('TextArea', () => {
|
||||
it('should render correctly with default props', () => {
|
||||
render(<TextArea value="" onChange={vi.fn()} />)
|
||||
const textarea = screen.getByTestId('text-area')
|
||||
expect(textarea).toBeInTheDocument()
|
||||
expect(textarea).toHaveValue('')
|
||||
})
|
||||
|
||||
it('should handle value and onChange correctly', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleChange = vi.fn()
|
||||
const { rerender } = render(<TextArea value="initial" onChange={handleChange} />)
|
||||
const textarea = screen.getByTestId('text-area')
|
||||
expect(textarea).toHaveValue('initial')
|
||||
|
||||
await user.type(textarea, ' updated')
|
||||
expect(handleChange).toHaveBeenCalled()
|
||||
|
||||
rerender(<TextArea value="initial updated" onChange={handleChange} />)
|
||||
expect(textarea).toHaveValue('initial updated')
|
||||
})
|
||||
|
||||
it('should handle autoFocus correctly', () => {
|
||||
render(<TextArea value="" onChange={vi.fn()} autoFocus />)
|
||||
const textarea = screen.getByTestId('text-area')
|
||||
expect(textarea).toHaveFocus()
|
||||
})
|
||||
|
||||
it('should handle disabled state', () => {
|
||||
render(<TextArea value="" onChange={vi.fn()} disabled />)
|
||||
const textarea = screen.getByTestId('text-area')
|
||||
expect(textarea).toBeDisabled()
|
||||
expect(textarea).toHaveClass('cursor-not-allowed')
|
||||
})
|
||||
|
||||
it('should handle placeholder', () => {
|
||||
render(<TextArea value="" onChange={vi.fn()} placeholder="Enter text here" />)
|
||||
expect(screen.getByPlaceholderText('Enter text here')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle className', () => {
|
||||
render(<TextArea value="" onChange={vi.fn()} className="custom-class" />)
|
||||
expect(screen.getByTestId('text-area')).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should handle size variants', () => {
|
||||
const { rerender } = render(<TextArea value="" onChange={vi.fn()} size="small" />)
|
||||
expect(screen.getByTestId('text-area')).toHaveClass('py-1')
|
||||
|
||||
rerender(<TextArea value="" onChange={vi.fn()} size="large" />)
|
||||
expect(screen.getByTestId('text-area')).toHaveClass('px-4')
|
||||
})
|
||||
|
||||
it('should handle destructive state', () => {
|
||||
render(<TextArea value="" onChange={vi.fn()} destructive />)
|
||||
expect(screen.getByTestId('text-area')).toHaveClass('border-components-input-border-destructive')
|
||||
})
|
||||
|
||||
it('should handle onFocus and onBlur', async () => {
|
||||
const user = userEvent.setup()
|
||||
const handleFocus = vi.fn()
|
||||
const handleBlur = vi.fn()
|
||||
render(<TextArea value="" onChange={vi.fn()} onFocus={handleFocus} onBlur={handleBlur} />)
|
||||
const textarea = screen.getByTestId('text-area')
|
||||
|
||||
await user.click(textarea)
|
||||
expect(handleFocus).toHaveBeenCalled()
|
||||
|
||||
await user.tab()
|
||||
expect(handleBlur).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
562
web/app/components/base/textarea/index.stories.tsx
Normal file
562
web/app/components/base/textarea/index.stories.tsx
Normal file
@ -0,0 +1,562 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { useState } from 'react'
|
||||
import Textarea from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Data Entry/Textarea',
|
||||
component: Textarea,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Textarea component with multiple sizes (small, regular, large). Built with class-variance-authority for consistent styling.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['small', 'regular', 'large'],
|
||||
description: 'Textarea size',
|
||||
},
|
||||
value: {
|
||||
control: 'text',
|
||||
description: 'Textarea value',
|
||||
},
|
||||
placeholder: {
|
||||
control: 'text',
|
||||
description: 'Placeholder text',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Disabled state',
|
||||
},
|
||||
destructive: {
|
||||
control: 'boolean',
|
||||
description: 'Error/destructive state',
|
||||
},
|
||||
rows: {
|
||||
control: 'number',
|
||||
description: 'Number of visible text rows',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Textarea>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Interactive demo wrapper
|
||||
const TextareaDemo = (args: any) => {
|
||||
const [value, setValue] = useState(args.value || '')
|
||||
|
||||
return (
|
||||
<div style={{ width: '500px' }}>
|
||||
<Textarea
|
||||
{...args}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
console.log('Textarea changed:', e.target.value)
|
||||
}}
|
||||
/>
|
||||
{value && (
|
||||
<div className="mt-3 text-sm text-gray-600">
|
||||
Character count:
|
||||
{' '}
|
||||
<span className="font-semibold">{value.length}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Default state
|
||||
export const Default: Story = {
|
||||
render: args => <TextareaDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
placeholder: 'Enter text...',
|
||||
rows: 4,
|
||||
value: '',
|
||||
},
|
||||
}
|
||||
|
||||
// Small size
|
||||
export const SmallSize: Story = {
|
||||
render: args => <TextareaDemo {...args} />,
|
||||
args: {
|
||||
size: 'small',
|
||||
placeholder: 'Small textarea...',
|
||||
rows: 3,
|
||||
value: '',
|
||||
},
|
||||
}
|
||||
|
||||
// Large size
|
||||
export const LargeSize: Story = {
|
||||
render: args => <TextareaDemo {...args} />,
|
||||
args: {
|
||||
size: 'large',
|
||||
placeholder: 'Large textarea...',
|
||||
rows: 5,
|
||||
value: '',
|
||||
},
|
||||
}
|
||||
|
||||
// With initial value
|
||||
export const WithInitialValue: Story = {
|
||||
render: args => <TextareaDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
value: 'This is some initial text content.\n\nIt spans multiple lines.',
|
||||
rows: 4,
|
||||
},
|
||||
}
|
||||
|
||||
// Disabled state
|
||||
export const Disabled: Story = {
|
||||
render: args => <TextareaDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
value: 'This textarea is disabled and cannot be edited.',
|
||||
disabled: true,
|
||||
rows: 3,
|
||||
},
|
||||
}
|
||||
|
||||
// Destructive/error state
|
||||
export const DestructiveState: Story = {
|
||||
render: args => <TextareaDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
value: 'This content has an error.',
|
||||
destructive: true,
|
||||
rows: 3,
|
||||
},
|
||||
}
|
||||
|
||||
// Size comparison
|
||||
const SizeComparisonDemo = () => {
|
||||
const [small, setSmall] = useState('')
|
||||
const [regular, setRegular] = useState('')
|
||||
const [large, setLarge] = useState('')
|
||||
|
||||
return (
|
||||
<div style={{ width: '600px' }} className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-medium text-gray-600">Small</label>
|
||||
<Textarea
|
||||
size="small"
|
||||
value={small}
|
||||
onChange={e => setSmall(e.target.value)}
|
||||
placeholder="Small textarea..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-medium text-gray-600">Regular</label>
|
||||
<Textarea
|
||||
size="regular"
|
||||
value={regular}
|
||||
onChange={e => setRegular(e.target.value)}
|
||||
placeholder="Regular textarea..."
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-xs font-medium text-gray-600">Large</label>
|
||||
<Textarea
|
||||
size="large"
|
||||
value={large}
|
||||
onChange={e => setLarge(e.target.value)}
|
||||
placeholder="Large textarea..."
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SizeComparison: Story = {
|
||||
render: () => <SizeComparisonDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// State comparison
|
||||
const StateComparisonDemo = () => {
|
||||
const [normal, setNormal] = useState('Normal state')
|
||||
const [error, setError] = useState('Error state')
|
||||
|
||||
return (
|
||||
<div style={{ width: '500px' }} className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Normal</label>
|
||||
<Textarea
|
||||
value={normal}
|
||||
onChange={e => setNormal(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Destructive</label>
|
||||
<Textarea
|
||||
value={error}
|
||||
onChange={e => setError(e.target.value)}
|
||||
destructive
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Disabled</label>
|
||||
<Textarea
|
||||
value="Disabled state"
|
||||
onChange={() => undefined}
|
||||
disabled
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const StateComparison: Story = {
|
||||
render: () => <StateComparisonDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Comment form
|
||||
const CommentFormDemo = () => {
|
||||
const [comment, setComment] = useState('')
|
||||
const maxLength = 500
|
||||
|
||||
return (
|
||||
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold">Leave a Comment</h3>
|
||||
<Textarea
|
||||
value={comment}
|
||||
onChange={e => setComment(e.target.value)}
|
||||
placeholder="Share your thoughts..."
|
||||
rows={5}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
{comment.length}
|
||||
{' '}
|
||||
/
|
||||
{maxLength}
|
||||
{' '}
|
||||
characters
|
||||
</span>
|
||||
<button
|
||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={comment.trim().length === 0}
|
||||
>
|
||||
Post Comment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const CommentForm: Story = {
|
||||
render: () => <CommentFormDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Feedback form
|
||||
const FeedbackFormDemo = () => {
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
return (
|
||||
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-2 text-lg font-semibold">Send Feedback</h3>
|
||||
<p className="mb-4 text-sm text-gray-600">Help us improve our product</p>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Your Email</label>
|
||||
<input
|
||||
type="email"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Your Feedback</label>
|
||||
<Textarea
|
||||
value={feedback}
|
||||
onChange={e => setFeedback(e.target.value)}
|
||||
placeholder="Tell us what you think..."
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
<button className="w-full rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700">
|
||||
Submit Feedback
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const FeedbackForm: Story = {
|
||||
render: () => <FeedbackFormDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Code snippet
|
||||
const CodeSnippetDemo = () => {
|
||||
const [code, setCode] = useState(`function hello() {
|
||||
console.log("Hello, world!");
|
||||
}`)
|
||||
|
||||
return (
|
||||
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold">Code Editor</h3>
|
||||
<Textarea
|
||||
value={code}
|
||||
onChange={e => setCode(e.target.value)}
|
||||
className="font-mono"
|
||||
rows={8}
|
||||
/>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<button className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
|
||||
Run Code
|
||||
</button>
|
||||
<button className="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const CodeSnippet: Story = {
|
||||
render: () => <CodeSnippetDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Message composer
|
||||
const MessageComposerDemo = () => {
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
return (
|
||||
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold">Compose Message</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">To</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
placeholder="Recipient name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
placeholder="Message subject"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Message</label>
|
||||
<Textarea
|
||||
value={message}
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
placeholder="Type your message here..."
|
||||
rows={8}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
|
||||
Send Message
|
||||
</button>
|
||||
<button className="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300">
|
||||
Save Draft
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const MessageComposer: Story = {
|
||||
render: () => <MessageComposerDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Bio editor
|
||||
const BioEditorDemo = () => {
|
||||
const [bio, setBio] = useState('Software developer passionate about building great products.')
|
||||
const maxLength = 200
|
||||
|
||||
return (
|
||||
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold">Edit Your Bio</h3>
|
||||
<Textarea
|
||||
value={bio}
|
||||
onChange={e => setBio(e.target.value.slice(0, maxLength))}
|
||||
placeholder="Tell us about yourself..."
|
||||
rows={4}
|
||||
/>
|
||||
<div className="mt-2 flex items-center justify-between text-xs">
|
||||
<span className={bio.length > maxLength * 0.9 ? 'text-orange-600' : 'text-gray-500'}>
|
||||
{bio.length}
|
||||
{' '}
|
||||
/
|
||||
{maxLength}
|
||||
{' '}
|
||||
characters
|
||||
</span>
|
||||
{bio.length > maxLength * 0.9 && (
|
||||
<span className="text-orange-600">
|
||||
{maxLength - bio.length}
|
||||
{' '}
|
||||
characters remaining
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg bg-gray-50 p-4">
|
||||
<div className="mb-2 text-xs font-medium text-gray-600">Preview:</div>
|
||||
<p className="text-sm text-gray-800">{bio || 'Your bio will appear here...'}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const BioEditor: Story = {
|
||||
render: () => <BioEditorDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - JSON editor
|
||||
const JSONEditorDemo = () => {
|
||||
const [json, setJson] = useState(`{
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"email": "john@example.com"
|
||||
}`)
|
||||
const [isValid, setIsValid] = useState(true)
|
||||
|
||||
const validateJSON = (value: string) => {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
setIsValid(true)
|
||||
}
|
||||
catch {
|
||||
setIsValid(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">JSON Editor</h3>
|
||||
<span className={`rounded-sm px-2 py-1 text-xs ${isValid ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
|
||||
{isValid ? '✓ Valid' : '✗ Invalid'}
|
||||
</span>
|
||||
</div>
|
||||
<Textarea
|
||||
value={json}
|
||||
onChange={(e) => {
|
||||
setJson(e.target.value)
|
||||
validateJSON(e.target.value)
|
||||
}}
|
||||
className="font-mono"
|
||||
destructive={!isValid}
|
||||
rows={10}
|
||||
/>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<button className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50" disabled={!isValid}>
|
||||
Save JSON
|
||||
</button>
|
||||
<button
|
||||
className="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300"
|
||||
onClick={() => {
|
||||
try {
|
||||
const formatted = JSON.stringify(JSON.parse(json), null, 2)
|
||||
setJson(formatted)
|
||||
}
|
||||
catch {
|
||||
// Invalid JSON, do nothing
|
||||
}
|
||||
}}
|
||||
>
|
||||
Format
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const JSONEditor: Story = {
|
||||
render: () => <JSONEditorDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Real-world example - Task description
|
||||
const TaskDescriptionDemo = () => {
|
||||
const [title, setTitle] = useState('Implement user authentication')
|
||||
const [description, setDescription] = useState('Add login and registration functionality with JWT tokens.')
|
||||
|
||||
return (
|
||||
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold">Create New Task</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Task Title</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Description</label>
|
||||
<Textarea
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Describe the task in detail..."
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">Priority</label>
|
||||
<select className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
|
||||
<option>Low</option>
|
||||
<option>Medium</option>
|
||||
<option>High</option>
|
||||
<option>Urgent</option>
|
||||
</select>
|
||||
</div>
|
||||
<button className="w-full rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
|
||||
Create Task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const TaskDescription: Story = {
|
||||
render: () => <TaskDescriptionDemo />,
|
||||
parameters: { controls: { disable: true } },
|
||||
} as unknown as Story
|
||||
|
||||
// Interactive playground
|
||||
export const Playground: Story = {
|
||||
render: args => <TextareaDemo {...args} />,
|
||||
args: {
|
||||
size: 'regular',
|
||||
placeholder: 'Enter text...',
|
||||
rows: 4,
|
||||
disabled: false,
|
||||
destructive: false,
|
||||
value: '',
|
||||
},
|
||||
}
|
||||
60
web/app/components/base/textarea/index.tsx
Normal file
60
web/app/components/base/textarea/index.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import type { CSSProperties } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import * as React from 'react'
|
||||
|
||||
const textareaVariants = cva(
|
||||
'',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
small: 'rounded-md py-1 system-xs-regular',
|
||||
regular: 'rounded-md px-3 system-sm-regular',
|
||||
large: 'rounded-lg px-4 system-md-regular',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'regular',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type TextareaProps = {
|
||||
value: string | number
|
||||
disabled?: boolean
|
||||
destructive?: boolean
|
||||
styleCss?: CSSProperties
|
||||
ref?: React.Ref<HTMLTextAreaElement>
|
||||
onFocus?: React.FocusEventHandler<HTMLTextAreaElement>
|
||||
onBlur?: React.FocusEventHandler<HTMLTextAreaElement>
|
||||
} & React.TextareaHTMLAttributes<HTMLTextAreaElement> & VariantProps<typeof textareaVariants>
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, value, onChange, disabled, size, destructive, styleCss, onFocus, onBlur, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={styleCss}
|
||||
className={cn(
|
||||
'min-h-20 w-full appearance-none border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-hidden placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs',
|
||||
textareaVariants({ size }),
|
||||
disabled && 'cursor-not-allowed border-transparent bg-components-input-bg-disabled text-components-input-text-filled-disabled hover:border-transparent hover:bg-components-input-bg-disabled',
|
||||
destructive && 'border-components-input-border-destructive bg-components-input-bg-destructive text-components-input-text-filled hover:border-components-input-border-destructive hover:bg-components-input-bg-destructive focus:border-components-input-border-destructive focus:bg-components-input-bg-destructive',
|
||||
className,
|
||||
)}
|
||||
value={value ?? ''}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
data-testid="text-area"
|
||||
{...props}
|
||||
>
|
||||
</textarea>
|
||||
)
|
||||
},
|
||||
)
|
||||
Textarea.displayName = 'Textarea'
|
||||
|
||||
export default Textarea
|
||||
@ -1,7 +1,6 @@
|
||||
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import type { PipelineTemplate } from '@/models/pipeline'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
@ -10,6 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useInvalidCustomizedTemplateList, useUpdateTemplateInfo } from '@/service/use-pipeline'
|
||||
|
||||
type EditPipelineInfoProps = {
|
||||
@ -57,7 +57,8 @@ const EditPipelineInfo = ({
|
||||
setShowAppIconPicker(false)
|
||||
}, [])
|
||||
|
||||
const handleDescriptionChange = useCallback((value: string) => {
|
||||
const handleDescriptionChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = event.target.value
|
||||
setDescription(value)
|
||||
}, [])
|
||||
|
||||
@ -132,8 +133,7 @@ const EditPipelineInfo = ({
|
||||
{t('knowledgeDescription', { ns: 'datasetPipeline' })}
|
||||
</label>
|
||||
<Textarea
|
||||
aria-label={t('knowledgeDescription', { ns: 'datasetPipeline' })}
|
||||
onValueChange={handleDescriptionChange}
|
||||
onChange={handleDescriptionChange}
|
||||
value={description}
|
||||
placeholder={t('knowledgeDescriptionPlaceholder', { ns: 'datasetPipeline' })}
|
||||
/>
|
||||
|
||||
@ -5,12 +5,12 @@ import type { DataSet } from '@/models/datasets'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { updateDatasetSetting } from '@/service/datasets'
|
||||
import AppIcon from '../../base/app-icon'
|
||||
import AppIconPicker from '../../base/app-icon-picker'
|
||||
@ -117,7 +117,7 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
|
||||
{t('form.desc', { ns: 'datasetSettings' })}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Textarea aria-label={t('form.desc', { ns: 'datasetSettings' })} value={description} onValueChange={value => setDescription(value)} className="resize-none" placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''} />
|
||||
<Textarea value={description} onChange={e => setDescription(e.target.value)} className="resize-none" placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,11 +3,11 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import type { Member } from '@/models/common'
|
||||
import type { DataSet, DatasetPermission, IconInfo } from '@/models/datasets'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import PermissionSelector from '../../permission-selector'
|
||||
|
||||
const rowClass = 'flex gap-x-1'
|
||||
@ -85,12 +85,11 @@ const BasicInfoSection = ({
|
||||
</div>
|
||||
<div className="grow">
|
||||
<Textarea
|
||||
aria-label={t('form.desc', { ns: 'datasetSettings' })}
|
||||
disabled={!currentDataset?.embedding_available}
|
||||
className="resize-none"
|
||||
placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''}
|
||||
value={description}
|
||||
onValueChange={value => setDescription(value)}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ChangeEvent } from 'react'
|
||||
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
@ -52,9 +53,9 @@ const SummaryIndexSetting = ({
|
||||
})
|
||||
}, [onSummaryIndexSettingChange])
|
||||
|
||||
const handleSummaryIndexPromptChange = useCallback((value: string) => {
|
||||
const handleSummaryIndexPromptChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onSummaryIndexSettingChange?.({
|
||||
summary_prompt: value,
|
||||
summary_prompt: e.target.value,
|
||||
})
|
||||
}, [onSummaryIndexSettingChange])
|
||||
|
||||
@ -94,9 +95,8 @@ const SummaryIndexSetting = ({
|
||||
{t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
value={summaryIndexSetting?.summary_prompt ?? ''}
|
||||
onValueChange={handleSummaryIndexPromptChange}
|
||||
onChange={handleSummaryIndexPromptChange}
|
||||
disabled={readonly}
|
||||
placeholder={t('form.summaryInstructionsPlaceholder', { ns: 'datasetSettings' })}
|
||||
/>
|
||||
@ -166,9 +166,8 @@ const SummaryIndexSetting = ({
|
||||
</div>
|
||||
<div className="grow">
|
||||
<Textarea
|
||||
aria-label={t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
value={summaryIndexSetting?.summary_prompt ?? ''}
|
||||
onValueChange={handleSummaryIndexPromptChange}
|
||||
onChange={handleSummaryIndexPromptChange}
|
||||
disabled={readonly}
|
||||
placeholder={t('form.summaryInstructionsPlaceholder', { ns: 'datasetSettings' })}
|
||||
/>
|
||||
@ -215,9 +214,8 @@ const SummaryIndexSetting = ({
|
||||
{t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
value={summaryIndexSetting?.summary_prompt ?? ''}
|
||||
onValueChange={handleSummaryIndexPromptChange}
|
||||
onChange={handleSummaryIndexPromptChange}
|
||||
disabled={readonly}
|
||||
placeholder={t('form.summaryInstructionsPlaceholder', { ns: 'datasetSettings' })}
|
||||
/>
|
||||
|
||||
@ -3,7 +3,6 @@ import type { AppIconType } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
@ -11,6 +10,7 @@ import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@ -145,11 +145,10 @@ const CreateAppModal = ({
|
||||
<div className="pt-2">
|
||||
<div className="py-2 text-sm leading-[20px] font-medium text-text-primary">{t('newApp.captionDescription', { ns: 'app' })}</div>
|
||||
<Textarea
|
||||
aria-label={t('newApp.captionDescription', { ns: 'app' })}
|
||||
className="resize-none"
|
||||
placeholder={t('newApp.appDescriptionPlaceholder', { ns: 'app' }) || ''}
|
||||
value={description}
|
||||
onValueChange={value => setDescription(value)}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* answer icon */}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
@ -55,9 +55,8 @@ const AppInputsForm = ({
|
||||
if (form.type === InputVarType.paragraph) {
|
||||
return (
|
||||
<Textarea
|
||||
aria-label={label}
|
||||
value={inputs[variable] || ''}
|
||||
onValueChange={value => handleFormChange(variable, value)}
|
||||
onChange={e => handleFormChange(variable, e.target.value)}
|
||||
placeholder={label}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -523,7 +523,9 @@ describe('useToolSelectorState Hook', () => {
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.handleDescriptionChange('new description')
|
||||
result.current.handleDescriptionChange({
|
||||
target: { value: 'new description' },
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||
})
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(
|
||||
@ -1722,7 +1724,9 @@ describe('Edge Cases', () => {
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.handleDescriptionChange('')
|
||||
result.current.handleDescriptionChange({
|
||||
target: { value: '' },
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||
})
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(
|
||||
|
||||
@ -1,6 +1,24 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/app/components/base/textarea', () => ({
|
||||
default: ({ value, onChange, disabled, placeholder }: {
|
||||
value?: string
|
||||
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
}) => (
|
||||
<textarea
|
||||
data-testid="description-textarea"
|
||||
value={value || ''}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../../readme-panel/entrance', () => ({
|
||||
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
|
||||
}))
|
||||
@ -50,28 +68,28 @@ describe('ToolBaseForm', () => {
|
||||
it('should render description textarea', () => {
|
||||
render(<ToolBaseForm {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('description-textarea')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable textarea when no provider_name in value', () => {
|
||||
render(<ToolBaseForm {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeDisabled()
|
||||
expect(screen.getByTestId('description-textarea')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should enable textarea when value has provider_name', () => {
|
||||
const value = { provider_name: 'test-provider', tool_name: 'test', extra: { description: 'Hello' } } as never
|
||||
render(<ToolBaseForm {...defaultProps} value={value} />)
|
||||
|
||||
expect(screen.getByRole('textbox')).not.toBeDisabled()
|
||||
expect(screen.getByTestId('description-textarea')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should call onDescriptionChange when textarea content changes', () => {
|
||||
const value = { provider_name: 'test-provider', tool_name: 'test', extra: { description: 'Hello' } } as never
|
||||
render(<ToolBaseForm {...defaultProps} value={value} />)
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Updated' } })
|
||||
expect(mockOnDescriptionChange).toHaveBeenCalledWith('Updated', expect.any(Object))
|
||||
fireEvent.change(screen.getByTestId('description-textarea'), { target: { value: 'Updated' } })
|
||||
expect(mockOnDescriptionChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show ReadmeEntrance when provider has plugin_unique_identifier', () => {
|
||||
|
||||
@ -4,8 +4,8 @@ import type { FC } from 'react'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
|
||||
import { ReadmeEntrance } from '../../../readme-panel/entrance'
|
||||
import ToolTrigger from './tool-trigger'
|
||||
@ -23,7 +23,7 @@ type ToolBaseFormProps = {
|
||||
onPanelShowStateChange?: (state: boolean) => void
|
||||
onSelectTool: (tool: ToolDefaultValue) => void
|
||||
onSelectMultipleTool: (tools: ToolDefaultValue[]) => void
|
||||
onDescriptionChange: (value: string) => void
|
||||
onDescriptionChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
}
|
||||
|
||||
const ToolBaseForm: FC<ToolBaseFormProps> = ({
|
||||
@ -85,10 +85,9 @@ const ToolBaseForm: FC<ToolBaseFormProps> = ({
|
||||
</div>
|
||||
<Textarea
|
||||
className="resize-none"
|
||||
aria-label={t('detailPanel.toolSelector.descriptionLabel', { ns: 'plugin' })}
|
||||
placeholder={t('detailPanel.toolSelector.descriptionPlaceholder', { ns: 'plugin' })}
|
||||
value={value?.extra?.description || ''}
|
||||
onValueChange={onDescriptionChange}
|
||||
onChange={onDescriptionChange}
|
||||
disabled={!value?.provider_name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type * as React from 'react'
|
||||
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@ -160,8 +161,9 @@ describe('useToolSelectorState', () => {
|
||||
useToolSelectorState({ value: toolValue, onSelect: mockOnSelect }),
|
||||
)
|
||||
|
||||
const event = { target: { value: 'New description' } } as React.ChangeEvent<HTMLTextAreaElement>
|
||||
act(() => {
|
||||
result.current.handleDescriptionChange('New description')
|
||||
result.current.handleDescriptionChange(event)
|
||||
})
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(expect.objectContaining({
|
||||
|
||||
@ -144,14 +144,14 @@ export const useToolSelectorState = ({
|
||||
onSelectMultiple?.(toolValues)
|
||||
}, [getToolValue, onSelectMultiple])
|
||||
|
||||
const handleDescriptionChange = useCallback((description: string) => {
|
||||
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (!value)
|
||||
return
|
||||
onSelect({
|
||||
...value,
|
||||
extra: {
|
||||
...value.extra,
|
||||
description: description || '',
|
||||
description: e.target.value || '',
|
||||
},
|
||||
})
|
||||
}, [value, onSelect])
|
||||
|
||||
@ -44,6 +44,17 @@ vi.mock('@/app/components/base/input', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/textarea', () => ({
|
||||
default: ({ value, onChange, ...props }: Record<string, unknown>) => (
|
||||
<textarea
|
||||
data-testid="description-textarea"
|
||||
value={value as string}
|
||||
onChange={onChange as () => void}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/app-icon', () => ({
|
||||
default: ({ onClick }: { onClick?: () => void }) => (
|
||||
<div data-testid="app-icon" onClick={onClick} />
|
||||
@ -104,7 +115,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should initialize description as empty', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const textarea = screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.description' }) as HTMLTextAreaElement
|
||||
const textarea = screen.getByTestId('description-textarea') as HTMLTextAreaElement
|
||||
expect(textarea.value).toBe('')
|
||||
})
|
||||
|
||||
@ -148,7 +159,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should update description when textarea changes', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const textarea = screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.description' })
|
||||
const textarea = screen.getByTestId('description-textarea')
|
||||
fireEvent.change(textarea, { target: { value: 'My description' } })
|
||||
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe('My description')
|
||||
@ -222,7 +233,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
const nameInput = screen.getByTestId('name-input')
|
||||
fireEvent.change(nameInput, { target: { value: ' Trimmed Name ' } })
|
||||
|
||||
const textarea = screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.description' })
|
||||
const textarea = screen.getByTestId('description-textarea')
|
||||
fireEvent.change(textarea, { target: { value: ' Some desc ' } })
|
||||
|
||||
fireEvent.click(screen.getByText('workflow.common.publish'))
|
||||
|
||||
@ -3,13 +3,13 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import type { IconInfo } from '@/models/datasets'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
type PublishAsKnowledgePipelineModalProps = {
|
||||
@ -118,10 +118,9 @@ const PublishAsKnowledgePipelineModal = ({
|
||||
</div>
|
||||
<Textarea
|
||||
className="resize-none"
|
||||
aria-label={t('common.publishAsPipeline.description', { ns: 'pipeline' })}
|
||||
placeholder={t('common.publishAsPipeline.descriptionPlaceholder', { ns: 'pipeline' }) || ''}
|
||||
value={description}
|
||||
onValueChange={value => setDescription(value)}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7,7 +7,6 @@ import type { VisionFile, VisionSettings } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import {
|
||||
RiLoader2Line,
|
||||
RiPlayLargeLine,
|
||||
@ -19,6 +18,7 @@ import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uplo
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
@ -159,11 +159,10 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
)}
|
||||
{item.type === 'paragraph' && (
|
||||
<Textarea
|
||||
aria-label={item.name}
|
||||
className="h-[104px] sm:text-xs"
|
||||
placeholder={item.name}
|
||||
value={inputs[item.key] as string}
|
||||
onValueChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'number' && (
|
||||
|
||||
@ -13,7 +13,6 @@ import {
|
||||
DrawerTitle,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiSettings2Line } from '@remixicon/react'
|
||||
import { useDebounce, useGetState } from 'ahooks'
|
||||
@ -24,6 +23,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import LabelSelector from '@/app/components/tools/labels/selector'
|
||||
import { parseParamsSchema } from '@/service/tools'
|
||||
import { LinkExternal02 } from '../../base/icons/src/vender/line/general'
|
||||
@ -280,10 +280,9 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={t('createTool.schema', { ns: 'tools' })}
|
||||
className="h-[240px] resize-none"
|
||||
value={schema}
|
||||
onValueChange={value => setSchema(value)}
|
||||
onChange={e => setSchema(e.target.value)}
|
||||
placeholder={t('createTool.schemaPlaceHolder', { ns: 'tools' })!}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -4,11 +4,11 @@ import type {
|
||||
} from '@/app/components/tools/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item'
|
||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||
import {
|
||||
@ -154,12 +154,12 @@ const MCPServerModal = ({
|
||||
<div className="system-xs-regular text-text-destructive-secondary">*</div>
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={t('mcp.server.modal.description', { ns: 'tools' })}
|
||||
className="h-[96px] resize-none"
|
||||
value={description}
|
||||
placeholder={t('mcp.server.modal.descriptionPlaceholder', { ns: 'tools' })}
|
||||
onValueChange={value => setDescription(value)}
|
||||
/>
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
>
|
||||
</Textarea>
|
||||
</div>
|
||||
|
||||
{latestParams.length > 0 && (
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
type Props = {
|
||||
data?: any
|
||||
@ -25,12 +25,12 @@ const MCPServerParamItem = ({
|
||||
<div className="max-w-full min-w-0 system-xs-medium wrap-break-word text-text-tertiary">{data.type}</div>
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={data.label}
|
||||
className="h-8 resize-none"
|
||||
value={value}
|
||||
placeholder={t('mcp.server.modal.parametersPlaceholder', { ns: 'tools' })}
|
||||
onValueChange={value => onChange(value)}
|
||||
/>
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
</Textarea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
DrawerTitle,
|
||||
DrawerViewport,
|
||||
} from '@langgenius/dify-ui/drawer'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { produce } from 'immer'
|
||||
@ -26,6 +25,7 @@ import Divider from '@/app/components/base/divider'
|
||||
import EmojiPickerInner from '@/app/components/base/emoji-picker/Inner'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import LabelSelector from '@/app/components/tools/labels/selector'
|
||||
import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal'
|
||||
import MethodSelector from '@/app/components/tools/workflow-tool/method-selector'
|
||||
@ -303,10 +303,9 @@ export function WorkflowToolDrawer({
|
||||
<div>
|
||||
<div className="py-2 system-sm-medium text-text-primary">{t('createTool.description', { ns: 'tools' })}</div>
|
||||
<Textarea
|
||||
aria-label={t('createTool.description', { ns: 'tools' })}
|
||||
placeholder={t('createTool.descriptionPlaceholder', { ns: 'tools' }) || ''}
|
||||
value={description}
|
||||
onValueChange={value => setDescription(value)}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Tool Input */}
|
||||
|
||||
@ -4,7 +4,6 @@ import type { InputVar } from '../../../../types'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
@ -19,6 +18,7 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
|
||||
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
@ -170,9 +170,8 @@ const FormItem: FC<Props> = ({
|
||||
{
|
||||
type === InputVarType.paragraph && (
|
||||
<Textarea
|
||||
aria-label={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
value={value || ''}
|
||||
onValueChange={value => onChange(value)}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
import type { FC } from 'react'
|
||||
import type { AssignerNodeOperation } from '../../types'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { RiDeleteBinLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { produce } from 'immer'
|
||||
@ -11,6 +10,7 @@ import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import ListNoDataPlaceholder from '@/app/components/workflow/nodes/_base/components/list-no-data-placeholder'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
@ -190,9 +190,8 @@ const VarList: FC<Props> = ({
|
||||
)}
|
||||
{assignedVarType === 'string' && (
|
||||
<Textarea
|
||||
aria-label={item.variable_selector?.join('.') || t('nodes.assigner.setParameter', { ns: 'workflow' })}
|
||||
value={item.value as string}
|
||||
onValueChange={value => handleToAssignedVarChange(index)(value)}
|
||||
onChange={e => handleToAssignedVarChange(index)(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -3,11 +3,11 @@ import type { FC } from 'react'
|
||||
import type { HttpNodeType } from '../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { useNodesInteractions } from '@/app/components/workflow/hooks'
|
||||
import { parseCurl } from './curl-parser'
|
||||
|
||||
@ -56,10 +56,9 @@ const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
|
||||
|
||||
<div>
|
||||
<Textarea
|
||||
aria-label={t('nodes.http.curl.title', { ns: 'workflow' })}
|
||||
value={inputString}
|
||||
className="my-3 h-40 w-full grow"
|
||||
onValueChange={value => setInputString(value)}
|
||||
onChange={e => setInputString(e.target.value)}
|
||||
placeholder={t('nodes.http.curl.placeholder', { ns: 'workflow' })!}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -2,12 +2,12 @@ import type { FC } from 'react'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { Model } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { RiCloseLine, RiSparklingFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
|
||||
type ModelInfo = {
|
||||
@ -38,8 +38,8 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleInstructionChange = useCallback((value: string) => {
|
||||
onInstructionChange(value)
|
||||
const handleInstructionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onInstructionChange(e.target.value)
|
||||
}, [onInstructionChange])
|
||||
|
||||
return (
|
||||
@ -90,11 +90,10 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Textarea
|
||||
aria-label={t('nodes.llm.jsonSchema.instruction', { ns: 'workflow' })}
|
||||
className="h-[364px] resize-none px-2 py-1"
|
||||
value={instruction}
|
||||
placeholder={t('nodes.llm.jsonSchema.promptPlaceholder', { ns: 'workflow' })}
|
||||
onValueChange={handleInstructionChange}
|
||||
onChange={handleInstructionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { FC } from 'react'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
|
||||
export type AdvancedOptionsType = {
|
||||
enum: string
|
||||
@ -22,8 +22,8 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
|
||||
// const [showAdvancedOptions, setShowAdvancedOptions] = useState(false)
|
||||
const [enumValue, setEnumValue] = useState(options.enum)
|
||||
|
||||
const handleEnumChange = useCallback((value: string) => {
|
||||
setEnumValue(value)
|
||||
const handleEnumChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setEnumValue(e.target.value)
|
||||
}, [])
|
||||
|
||||
const handleEnumBlur = useCallback((e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||
@ -51,11 +51,10 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
|
||||
Enum
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label="Enum"
|
||||
size="small"
|
||||
className="min-h-6"
|
||||
value={enumValue}
|
||||
onValueChange={handleEnumChange}
|
||||
onChange={handleEnumChange}
|
||||
onBlur={handleEnumBlur}
|
||||
placeholder="abcd, 1, 1.5, etc."
|
||||
/>
|
||||
|
||||
@ -4,13 +4,13 @@ import type {
|
||||
import type {
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
@ -49,10 +49,6 @@ const FormItem = ({
|
||||
onChange(e.target.value)
|
||||
}, [onChange])
|
||||
|
||||
const handleValueChange = useCallback((value: string) => {
|
||||
onChange(value)
|
||||
}, [onChange])
|
||||
|
||||
const handleChange = useCallback((value: any) => {
|
||||
onChange(value)
|
||||
}, [onChange])
|
||||
@ -96,9 +92,8 @@ const FormItem = ({
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.string && (
|
||||
<Textarea
|
||||
aria-label={item.label}
|
||||
value={value}
|
||||
onValueChange={handleValueChange}
|
||||
onChange={handleInputChange}
|
||||
className="min-h-12 w-full"
|
||||
/>
|
||||
)
|
||||
|
||||
@ -6,7 +6,6 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
@ -15,6 +14,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Field from '@/app/components/app/configuration/config-var/config-modal/field'
|
||||
import ConfigSelect from '@/app/components/app/configuration/config-var/config-select'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { ChangeType } from '@/app/components/workflow/types'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import { ParamType } from '../../types'
|
||||
@ -175,9 +175,8 @@ const AddExtractParameter: FC<Props> = ({
|
||||
)}
|
||||
<Field title={t(`${i18nPrefix}.addExtractParameterContent.description`, { ns: 'workflow' })}>
|
||||
<Textarea
|
||||
aria-label={t(`${i18nPrefix}.addExtractParameterContent.description`, { ns: 'workflow' })}
|
||||
value={param.description}
|
||||
onValueChange={value => handleParamChange('description')(value)}
|
||||
onChange={e => handleParamChange('description')(e.target.value)}
|
||||
placeholder={t(`${i18nPrefix}.addExtractParameterContent.descriptionPlaceholder`, { ns: 'workflow' })!}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@ -2,11 +2,11 @@ import type { VarType } from '../types'
|
||||
import type { ChunkInfo } from '@/app/components/rag-pipeline/components/chunk-card-list/types'
|
||||
import type { ParentMode } from '@/models/datasets'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@langgenius/dify-ui/toggle-group'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { ChunkCardList } from '@/app/components/rag-pipeline/components/chunk-card-list'
|
||||
import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
@ -91,12 +91,11 @@ export function DisplayContent(props: DisplayContentProps) {
|
||||
previewType === PreviewType.Markdown
|
||||
? (
|
||||
<Textarea
|
||||
aria-label="Markdown content"
|
||||
readOnly={readonly}
|
||||
disabled={readonly}
|
||||
className="h-full border-none bg-transparent p-0 text-text-secondary hover:bg-transparent focus:bg-transparent focus:shadow-none"
|
||||
value={mdString as any}
|
||||
onValueChange={value => handleTextChange?.(value)}
|
||||
onChange={e => handleTextChange?.(e.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
|
||||
@ -2,9 +2,9 @@ import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { FileUploadConfigResponse } from '@/models/common'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message'
|
||||
import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
@ -46,12 +46,11 @@ export const TextEditorSection = ({
|
||||
)
|
||||
: (
|
||||
<Textarea
|
||||
aria-label="Value"
|
||||
readOnly={textEditorDisabled}
|
||||
disabled={textEditorDisabled || isTruncated}
|
||||
className={cn('h-full', isTruncated && 'pt-[48px]')}
|
||||
value={typeof value === 'number' ? value : String(value ?? '')}
|
||||
onValueChange={value => onTextChange(value)}
|
||||
onChange={e => onTextChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user