mirror of
https://github.com/langgenius/dify.git
synced 2026-05-29 22:01:00 -04:00
feat(dify-ui): add textarea primitive (#36547)
This commit is contained in:
@@ -1757,14 +1757,6 @@
|
||||
"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
|
||||
|
||||
@@ -34,6 +34,7 @@ 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 { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import '@langgenius/dify-ui/styles.css' // once, in the app root
|
||||
```
|
||||
|
||||
@@ -41,17 +42,17 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
|
||||
|
||||
## Primitives
|
||||
|
||||
| Category | Subpath | Notes |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
|
||||
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
|
||||
| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. |
|
||||
| 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 | `./pagination`, `./tabs` | Pagination for page navigation; Tabs for panels. |
|
||||
| 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. |
|
||||
| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. |
|
||||
| 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 | `./pagination`, `./tabs` | Pagination for page navigation; Tabs for panels. |
|
||||
| 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:
|
||||
|
||||
@@ -72,7 +73,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, 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, `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.
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -129,6 +129,10 @@
|
||||
"types": "./src/tabs/index.tsx",
|
||||
"import": "./src/tabs/index.tsx"
|
||||
},
|
||||
"./textarea": {
|
||||
"types": "./src/textarea/index.tsx",
|
||||
"import": "./src/textarea/index.tsx"
|
||||
},
|
||||
"./toast": {
|
||||
"types": "./src/toast/index.tsx",
|
||||
"import": "./src/toast/index.tsx"
|
||||
|
||||
187
packages/dify-ui/src/textarea/__tests__/index.spec.tsx
Normal file
187
packages/dify-ui/src/textarea/__tests__/index.spec.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { FocusEvent } from 'react'
|
||||
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', 'overflow-auto', '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 pass maxLength to the textarea without rendering a counter', async () => {
|
||||
const screen = await render(
|
||||
<label>
|
||||
Release notes
|
||||
<Textarea defaultValue="Draft" maxLength={20} />
|
||||
</label>,
|
||||
)
|
||||
|
||||
const textarea = screen.getByRole('textbox', { name: 'Release notes' })
|
||||
await expect.element(textarea).toHaveAttribute('maxLength', '20')
|
||||
expect(screen.container.textContent).not.toContain('5/20')
|
||||
})
|
||||
|
||||
it('should route field props through Base UI Field.Control and textarea-only props to textarea', async () => {
|
||||
const onFormSubmit = vi.fn()
|
||||
const onBlur = vi.fn((event: FocusEvent<HTMLTextAreaElement>) => {
|
||||
expect(event.currentTarget.tagName).toBe('TEXTAREA')
|
||||
})
|
||||
const screen = await render(
|
||||
<Form aria-label="profile form" onFormSubmit={onFormSubmit}>
|
||||
<FieldRoot name="profileSummary">
|
||||
<FieldLabel>Profile summary</FieldLabel>
|
||||
<Textarea
|
||||
id="profile-summary"
|
||||
name="ignoredControlName"
|
||||
defaultValue="Long enough summary"
|
||||
rows={6}
|
||||
cols={40}
|
||||
wrap="soft"
|
||||
maxLength={80}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</FieldRoot>
|
||||
<FieldRoot disabled>
|
||||
<FieldLabel>Disabled note</FieldLabel>
|
||||
<Textarea name="disabledNote" defaultValue="Disabled value" />
|
||||
</FieldRoot>
|
||||
<button type="submit">Save</button>
|
||||
</Form>,
|
||||
)
|
||||
|
||||
const profileSummary = screen.getByRole('textbox', { name: 'Profile summary' })
|
||||
expect(
|
||||
asHTMLElement(screen.getByText('Profile summary').element()).getAttribute('for'),
|
||||
).toBe('profile-summary')
|
||||
await expect.element(profileSummary).toHaveAttribute('id', 'profile-summary')
|
||||
await expect.element(profileSummary).toHaveAttribute('name', 'profileSummary')
|
||||
await expect.element(profileSummary).toHaveAttribute('rows', '6')
|
||||
await expect.element(profileSummary).toHaveAttribute('cols', '40')
|
||||
await expect.element(profileSummary).toHaveAttribute('wrap', 'soft')
|
||||
await expect.element(profileSummary).toHaveAttribute('maxLength', '80')
|
||||
|
||||
await expect.element(screen.getByRole('textbox', { name: 'Disabled note' })).toBeDisabled()
|
||||
|
||||
asHTMLElement(profileSummary.element()).focus()
|
||||
const saveButton = asHTMLElement(screen.getByRole('button', { name: 'Save' }).element())
|
||||
saveButton.focus()
|
||||
expect(onBlur).toHaveBeenCalledTimes(1)
|
||||
|
||||
saveButton.click()
|
||||
|
||||
expect(onFormSubmit).toHaveBeenCalledTimes(1)
|
||||
expect(onFormSubmit.mock.calls[0]?.[0]).toMatchObject({
|
||||
profileSummary: 'Long enough summary',
|
||||
})
|
||||
expect(onFormSubmit.mock.calls[0]?.[0]).not.toHaveProperty('ignoredControlName')
|
||||
expect(onFormSubmit.mock.calls[0]?.[0]).not.toHaveProperty('disabledNote')
|
||||
})
|
||||
})
|
||||
193
packages/dify-ui/src/textarea/index.stories.tsx
Normal file
193
packages/dify-ui/src/textarea/index.stories.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
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>
|
||||
),
|
||||
}
|
||||
|
||||
const FormDemo = () => {
|
||||
const [savedDescription, setSavedDescription] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<Form
|
||||
aria-label="Dataset settings"
|
||||
className="grid w-80 gap-4"
|
||||
onFormSubmit={(values) => {
|
||||
setSavedDescription(String(values.description ?? ''))
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{savedDescription && (
|
||||
<div className="rounded-lg bg-background-section px-3 py-2 text-text-secondary system-xs-regular">
|
||||
Saved:
|
||||
{' '}
|
||||
{savedDescription}
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithField: Story = {
|
||||
render: () => <FormDemo />,
|
||||
}
|
||||
|
||||
const ControlledDemo = () => {
|
||||
const [value, setValue] = useState('Summarize customer feedback into actionable product themes.')
|
||||
|
||||
return (
|
||||
<FieldRoot name="prompt">
|
||||
<FieldLabel>Prompt</FieldLabel>
|
||||
<Textarea
|
||||
value={value}
|
||||
onValueChange={nextValue => setValue(nextValue)}
|
||||
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>
|
||||
),
|
||||
}
|
||||
|
||||
const CharacterCounterDemo = () => {
|
||||
const maxLength = 120
|
||||
const [value, setValue] = useState('Summarize customer feedback into actionable product themes.')
|
||||
|
||||
return (
|
||||
<FieldRoot name="limitedPrompt">
|
||||
<FieldLabel>Prompt</FieldLabel>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
value={value}
|
||||
onValueChange={nextValue => setValue(nextValue)}
|
||||
maxLength={maxLength}
|
||||
rows={4}
|
||||
className="resize-y pb-8"
|
||||
/>
|
||||
<div className="pointer-events-none absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-text-quaternary system-xs-medium">
|
||||
<span>{value.length}</span>
|
||||
/
|
||||
<span className="text-text-tertiary">{maxLength}</span>
|
||||
</div>
|
||||
</div>
|
||||
<FieldDescription>Character counters are composed at the usage site when the workflow needs one.</FieldDescription>
|
||||
</FieldRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithCharacterCounter: Story = {
|
||||
render: () => (
|
||||
<div className="w-80">
|
||||
<CharacterCounterDemo />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
103
packages/dify-ui/src/textarea/index.tsx
Normal file
103
packages/dify-ui/src/textarea/index.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'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 { cn } from '../cn'
|
||||
|
||||
const textareaVariants = cva(
|
||||
[
|
||||
'min-h-20 w-full appearance-none overflow-auto 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 TextareaNativeProps = ComponentPropsWithRef<'textarea'>
|
||||
type TextareaOnlyProps = Pick<TextareaNativeProps, 'cols' | 'rows' | 'wrap'>
|
||||
type TextareaElementProps = Omit<
|
||||
TextareaNativeProps,
|
||||
'children' | 'className' | 'cols' | 'defaultValue' | 'onChange' | 'rows' | 'size' | 'value' | 'wrap'
|
||||
>
|
||||
|
||||
type TextareaControlProps = ControlledTextareaProps | UncontrolledTextareaProps
|
||||
type TextareaVariantProps = VariantProps<typeof textareaVariants>
|
||||
type FieldControlTextareaProps = Omit<
|
||||
BaseFieldNS.Control.Props,
|
||||
'className' | 'defaultValue' | 'onValueChange' | 'render' | 'value'
|
||||
>
|
||||
|
||||
export type TextareaProps
|
||||
= TextareaElementProps
|
||||
& TextareaOnlyProps
|
||||
& TextareaControlProps
|
||||
& TextareaVariantProps
|
||||
& {
|
||||
children?: never
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function Textarea({
|
||||
className,
|
||||
cols,
|
||||
defaultValue,
|
||||
onValueChange,
|
||||
ref,
|
||||
rows,
|
||||
size = 'medium',
|
||||
value,
|
||||
wrap,
|
||||
...controlProps
|
||||
}: TextareaProps) {
|
||||
// Base UI types Field.Control as an input even when render replaces it with a textarea.
|
||||
const fieldControlProps = controlProps as FieldControlTextareaProps
|
||||
|
||||
return (
|
||||
<BaseField.Control
|
||||
{...fieldControlProps}
|
||||
className={cn(textareaVariants({ size }), className)}
|
||||
defaultValue={defaultValue}
|
||||
onValueChange={onValueChange}
|
||||
ref={ref}
|
||||
render={<textarea cols={cols} rows={rows} wrap={wrap} />}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -493,8 +493,8 @@ describe('Capacity Full Components Integration', () => {
|
||||
expect(screen.getByText(/upgradeBtn\.encourageShort/i)).toBeInTheDocument()
|
||||
// Should show usage/total fraction "5/5"
|
||||
expect(screen.getByText(/5\/5/)).toBeInTheDocument()
|
||||
// Should have a meter rendered
|
||||
expect(screen.getByRole('meter')).toBeInTheDocument()
|
||||
// Should have an accessible meter rendered
|
||||
expect(screen.getByRole('meter', { name: /usagePage\.buildApps/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display upgrade tip and upgrade button for professional plan', () => {
|
||||
|
||||
@@ -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,11 +63,12 @@ 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}
|
||||
onChange={(e) => {
|
||||
setUserFeedback(e.target.value)
|
||||
onValueChange={(value) => {
|
||||
setUserFeedback(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,8 +33,9 @@ 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}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
|
||||
onValueChange={value => onChange(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,8 +130,9 @@ const EditItem: FC<Props> = ({
|
||||
<div className="mt-3">
|
||||
<EditTitle title={editTitle} />
|
||||
<Textarea
|
||||
aria-label={editTitle}
|
||||
value={newContent}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setNewContent(e.target.value)}
|
||||
onValueChange={value => setNewContent(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((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setReleaseNotes(e.target.value)
|
||||
const handleDescriptionChange = useCallback((value: string) => {
|
||||
setReleaseNotes(value)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -95,17 +95,16 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
onValueChange={setTitle}
|
||||
/>
|
||||
</FieldRoot>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<div className="flex h-6 items-center system-sm-semibold text-text-secondary">
|
||||
<FieldRoot name="releaseNotes" invalid={releaseNotesError} className="gap-y-1">
|
||||
<FieldLabel className="flex h-6 items-center py-0 system-sm-semibold text-text-secondary">
|
||||
{t('versionHistory.editField.releaseNotes', { ns: 'workflow' })}
|
||||
</div>
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
value={releaseNotes}
|
||||
placeholder={`${t('versionHistory.releaseNotesPlaceholder', { ns: 'workflow' })}${t('panel.optional', { ns: 'workflow' })}`}
|
||||
onChange={handleDescriptionChange}
|
||||
destructive={releaseNotesError}
|
||||
onValueChange={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</FieldRoot>
|
||||
</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,8 +121,9 @@ 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 ?? '')}
|
||||
onChange={e => onPayloadChange('default')(e.target.value || undefined)}
|
||||
onValueChange={value => onPayloadChange('default')(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,10 +40,11 @@ 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}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onValueChange={value => onChange(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,8 +224,9 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Textarea
|
||||
aria-label={t('form.desc', { ns: 'datasetSettings' })}
|
||||
value={localeCurrentDataset.description || ''}
|
||||
onChange={e => handleValueChange('description', e.target.value)}
|
||||
onValueChange={value => handleValueChange('description', value)}
|
||||
className="resize-none"
|
||||
placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''}
|
||||
/>
|
||||
|
||||
@@ -84,25 +84,6 @@ 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
|
||||
@@ -223,7 +204,7 @@ describe('ChatUserInput', () => {
|
||||
}))
|
||||
|
||||
render(<ChatUserInput inputs={{}} />)
|
||||
expect(screen.getByTestId('textarea-Description')).toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox', { name: 'Description' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render select input type', () => {
|
||||
@@ -275,7 +256,7 @@ describe('ChatUserInput', () => {
|
||||
|
||||
render(<ChatUserInput inputs={{}} />)
|
||||
expect(screen.getByTestId('input-Name')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('textarea-Description')).toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox', { name: 'Description' })).toBeInTheDocument()
|
||||
expect(screen.getByTestId('select-input')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -334,7 +315,7 @@ describe('ChatUserInput', () => {
|
||||
}))
|
||||
|
||||
render(<ChatUserInput inputs={{ desc: 'Long text here' }} />)
|
||||
expect(screen.getByTestId('textarea-Description')).toHaveValue('Long text here')
|
||||
expect(screen.getByRole('textbox', { name: 'Description' })).toHaveValue('Long text here')
|
||||
})
|
||||
|
||||
it('should display existing input values for number type', () => {
|
||||
@@ -418,7 +399,7 @@ describe('ChatUserInput', () => {
|
||||
}))
|
||||
|
||||
render(<ChatUserInput inputs={{}} />)
|
||||
fireEvent.change(screen.getByTestId('textarea-Description'), { target: { value: 'New Description' } })
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'Description' }), { target: { value: 'New Description' } })
|
||||
|
||||
expect(mockSetInputs).toHaveBeenCalledWith({ desc: 'New Description' })
|
||||
})
|
||||
@@ -526,7 +507,7 @@ describe('ChatUserInput', () => {
|
||||
}))
|
||||
|
||||
render(<ChatUserInput inputs={{}} />)
|
||||
expect(screen.getByTestId('textarea-Description')).toHaveAttribute('readonly')
|
||||
expect(screen.getByRole('textbox', { name: '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,9 +94,10 @@ const ChatUserInput = ({
|
||||
{type === 'paragraph' && (
|
||||
<Textarea
|
||||
className="h-[120px] grow"
|
||||
aria-label={name || key}
|
||||
placeholder={name}
|
||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||
onValueChange={(value) => { handleInputValueChange(key, value) }}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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,
|
||||
@@ -19,7 +20,6 @@ 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,10 +151,11 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
)}
|
||||
{type === 'paragraph' && (
|
||||
<Textarea
|
||||
aria-label={name}
|
||||
className="h-[120px] grow"
|
||||
placeholder={name}
|
||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||
onValueChange={(value) => { handleInputValueChange(key, value) }}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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'
|
||||
@@ -13,7 +14,6 @@ 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'
|
||||
@@ -241,10 +241,11 @@ 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}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
onValueChange={value => setDescription(value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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'
|
||||
@@ -18,7 +19,6 @@ 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,9 +289,10 @@ 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}
|
||||
onChange={e => onDesChange(e.target.value)}
|
||||
onValueChange={onDesChange}
|
||||
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>
|
||||
@@ -464,9 +465,10 @@ 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}
|
||||
onChange={onChange('customDisclaimer')}
|
||||
onValueChange={value => setInputInfo(item => ({ ...item, customDisclaimer: value }))}
|
||||
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'
|
||||
|
||||
@@ -74,7 +74,7 @@ const WorkflowHiddenInputFields = ({
|
||||
<Textarea
|
||||
id={fieldId}
|
||||
value={typeof fieldValue === 'string' ? fieldValue : ''}
|
||||
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => onValueChange(variable.variable, event.target.value)}
|
||||
onValueChange={value => onValueChange(variable.variable, 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,8 +71,9 @@ const InputsFormContent = ({ showTip }: Props) => {
|
||||
)}
|
||||
{form.type === InputVarType.paragraph && (
|
||||
<Textarea
|
||||
aria-label={form.label}
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
onValueChange={value => handleFormChange(form.variable, 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,9 +42,10 @@ const ContentItem = ({
|
||||
<div className="py-3">
|
||||
{formInputField.type === 'paragraph' && (
|
||||
<Textarea
|
||||
aria-label={fieldName}
|
||||
className="h-[104px] sm:text-xs"
|
||||
value={inputs[fieldName]!}
|
||||
onChange={(e) => { onInputChange(fieldName, e.target.value) }}
|
||||
onValueChange={(value) => { onInputChange(fieldName, value) }}
|
||||
data-testid="content-item-textarea"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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'
|
||||
@@ -21,7 +22,6 @@ 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 = {
|
||||
@@ -394,7 +394,7 @@ function Operation({
|
||||
id={feedbackTextareaId}
|
||||
name="feedback-content"
|
||||
value={feedbackContent}
|
||||
onChange={e => setFeedbackContent(e.target.value)}
|
||||
onValueChange={value => setFeedbackContent(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,8 +71,9 @@ const InputsFormContent = ({ showTip }: Props) => {
|
||||
)}
|
||||
{form.type === InputVarType.paragraph && (
|
||||
<Textarea
|
||||
aria-label={form.label}
|
||||
value={inputsFormValue?.[form.variable] || ''}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
onValueChange={value => handleFormChange(form.variable, 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,9 +220,10 @@ 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}
|
||||
onChange={e => setPrompt(e.target.value)}
|
||||
onValueChange={value => setPrompt(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 '@/app/components/base/textarea'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
|
||||
type FormGenerationProps = {
|
||||
@@ -55,10 +55,11 @@ 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}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
onValueChange={value => handleFormChange(form.variable, value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { ModerationContentConfig } from '@/models/debug'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ModerationContentProps = {
|
||||
@@ -50,12 +51,14 @@ const ModerationContent: FC<ModerationContentProps> = ({
|
||||
{t('feature.moderation.modal.content.preset', { ns: 'appDebug' })}
|
||||
<span className="text-xs font-normal text-text-tertiary">{t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })}</span>
|
||||
</div>
|
||||
<div className="relative h-20 rounded-lg bg-components-input-bg-normal px-3 py-2">
|
||||
<textarea
|
||||
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
|
||||
<div className="relative h-20">
|
||||
<Textarea
|
||||
aria-label={t('feature.moderation.modal.content.preset', { ns: 'appDebug' }) as string}
|
||||
value={config.preset_response || ''}
|
||||
className="block size-full resize-none appearance-none bg-transparent text-sm text-text-secondary outline-hidden"
|
||||
className="size-full resize-none pb-8"
|
||||
placeholder={t('feature.moderation.modal.content.placeholder', { ns: 'appDebug' }) || ''}
|
||||
onChange={e => handleConfigChange('preset_response', e.target.value)}
|
||||
onValueChange={value => handleConfigChange('preset_response', value)}
|
||||
/>
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
|
||||
<span>{(config.preset_response || '').length}</span>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { ChangeEvent, FC } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import type { CodeBasedExtensionItem } from '@/models/common'
|
||||
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
|
||||
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 { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -103,9 +104,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const handleDataKeywordsChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value
|
||||
|
||||
const handleDataKeywordsChange = (value: string) => {
|
||||
const arr = value.split('\n').reduce((prev: string[], next: string) => {
|
||||
if (next !== '')
|
||||
prev.push(next.slice(0, 100))
|
||||
@@ -292,11 +291,13 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
<div className="py-2">
|
||||
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
|
||||
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
|
||||
<div className="relative h-[88px] rounded-lg bg-components-input-bg-normal px-3 py-2">
|
||||
<textarea
|
||||
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
|
||||
<div className="relative h-[88px]">
|
||||
<Textarea
|
||||
aria-label={t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' }) as string}
|
||||
value={localeData.config?.keywords || ''}
|
||||
onChange={handleDataKeywordsChange}
|
||||
className="block size-full resize-none appearance-none bg-transparent text-sm text-text-secondary outline-hidden"
|
||||
onValueChange={handleDataKeywordsChange}
|
||||
className="size-full resize-none pb-8"
|
||||
placeholder={t('feature.moderation.modal.keywords.placeholder', { ns: 'appDebug' }) || ''}
|
||||
/>
|
||||
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import TextAreaField from '../text-area'
|
||||
|
||||
@@ -30,4 +31,20 @@ describe('TextAreaField', () => {
|
||||
fireEvent.change(screen.getByLabelText('Note'), { target: { value: 'Updated note' } })
|
||||
expect(mockField.handleChange).toHaveBeenCalledWith('Updated note')
|
||||
})
|
||||
|
||||
it('should keep form writeback when external props contain onValueChange', () => {
|
||||
const externalOnValueChange = vi.fn()
|
||||
|
||||
render(
|
||||
<TextAreaField
|
||||
label="Note"
|
||||
{...({ onValueChange: externalOnValueChange } as Partial<ComponentProps<typeof TextAreaField>>)}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Note'), { target: { value: 'Updated note' } })
|
||||
|
||||
expect(mockField.handleChange).toHaveBeenCalledWith('Updated note')
|
||||
expect(externalOnValueChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { TextareaProps } from '../../../textarea'
|
||||
import type { TextareaProps } from '@langgenius/dify-ui/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' | 'onChange' | 'onBlur' | 'value' | 'id'>
|
||||
} & Omit<TextareaProps, 'className' | 'defaultValue' | 'onBlur' | 'onValueChange' | 'value' | 'id'>
|
||||
|
||||
const TextAreaField = ({
|
||||
label,
|
||||
@@ -28,11 +28,11 @@ const TextAreaField = ({
|
||||
{...(labelOptions ?? {})}
|
||||
/>
|
||||
<Textarea
|
||||
{...inputProps}
|
||||
id={field.name}
|
||||
value={field.state.value}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onValueChange={value => field.handleChange(value)}
|
||||
onBlur={field.handleBlur}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ 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'
|
||||
@@ -10,7 +11,6 @@ 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,11 +372,12 @@ 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])}
|
||||
onChange={e => updateValue(name, e.target.value)}
|
||||
onValueChange={value => updateValue(name, 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,6 +72,7 @@ const PrePopulate: FC<Props> = ({
|
||||
value,
|
||||
onValueChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [onPlaceholderClicked, setOnPlaceholderClicked] = useState(false)
|
||||
const handleTypeChange = useCallback((isVar: boolean) => {
|
||||
setOnPlaceholderClicked(true)
|
||||
@@ -127,9 +128,10 @@ 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"
|
||||
onChange={e => onValueChange?.(e.target.value)}
|
||||
onValueChange={value => onValueChange?.(value)}
|
||||
onFocus={() => {
|
||||
setOnPlaceholderClicked(true)
|
||||
setIsFocus(true)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -1,562 +0,0 @@
|
||||
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: '',
|
||||
},
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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
|
||||
@@ -25,6 +25,7 @@ const AppsFull: FC<{ loc: string, className?: string }> = ({
|
||||
const total = plan.total.buildApps
|
||||
const percent = total > 0 ? (usage / total) * 100 : 0
|
||||
const tone: MeterTone = percent >= 80 ? 'error' : percent >= 50 ? 'warning' : 'neutral'
|
||||
const buildAppsLabel = t('usagePage.buildApps', { ns: 'billing' })
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex flex-col gap-3 rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg p-4 shadow-xs backdrop-blur-xs',
|
||||
@@ -61,14 +62,14 @@ const AppsFull: FC<{ loc: string, className?: string }> = ({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between system-xs-medium text-text-secondary">
|
||||
<div>{t('usagePage.buildApps', { ns: 'billing' })}</div>
|
||||
<div>{buildAppsLabel}</div>
|
||||
<div>
|
||||
{usage}
|
||||
/
|
||||
{total}
|
||||
</div>
|
||||
</div>
|
||||
<MeterRoot value={Math.min(percent, 100)} max={100}>
|
||||
<MeterRoot value={Math.min(percent, 100)} max={100} aria-label={buildAppsLabel}>
|
||||
<MeterTrack>
|
||||
<MeterIndicator tone={tone} />
|
||||
</MeterTrack>
|
||||
|
||||
@@ -229,7 +229,7 @@ describe('UsageInfo', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('meter')).toBeInTheDocument()
|
||||
expect(screen.getByRole('meter', { name: 'Storage' })).toBeInTheDocument()
|
||||
expect(container.querySelector('[aria-hidden="true"]')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -270,7 +270,7 @@ describe('UsageInfo', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('meter')).toBeInTheDocument()
|
||||
expect(screen.getByRole('meter', { name: 'Storage' })).toBeInTheDocument()
|
||||
expect(container.querySelector('[aria-hidden="true"]')).toBeNull()
|
||||
})
|
||||
|
||||
|
||||
@@ -144,13 +144,13 @@ const UsageInfo: FC<Props> = ({
|
||||
<div
|
||||
className={cn(
|
||||
'h-1 rounded-md bg-progress-bar-indeterminate-stripe',
|
||||
isSandboxPlan ? 'w-full' : 'w-[30px]',
|
||||
isSandboxPlan ? 'w-full' : 'w-7.5',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<MeterRoot value={effectivePercent} max={100}>
|
||||
<MeterRoot value={effectivePercent} max={100} aria-label={name}>
|
||||
<MeterTrack>
|
||||
<MeterIndicator tone={tone} />
|
||||
</MeterTrack>
|
||||
@@ -162,7 +162,7 @@ const UsageInfo: FC<Props> = ({
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div className="cursor-default">{children}</div>} />
|
||||
<TooltipContent className="w-[200px] max-w-[200px]">
|
||||
<TooltipContent className="w-50 max-w-50">
|
||||
{storageTooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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'
|
||||
@@ -9,7 +10,6 @@ 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 = {
|
||||
@@ -45,8 +45,7 @@ const EditPipelineInfo = ({
|
||||
setAppIcon(icon)
|
||||
}, [])
|
||||
|
||||
const handleDescriptionChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = event.target.value
|
||||
const handleDescriptionChange = useCallback((value: string) => {
|
||||
setDescription(value)
|
||||
}, [])
|
||||
|
||||
@@ -121,7 +120,8 @@ const EditPipelineInfo = ({
|
||||
{t('knowledgeDescription', { ns: 'datasetPipeline' })}
|
||||
</label>
|
||||
<Textarea
|
||||
onChange={handleDescriptionChange}
|
||||
aria-label={t('knowledgeDescription', { ns: 'datasetPipeline' })}
|
||||
onValueChange={handleDescriptionChange}
|
||||
value={description}
|
||||
placeholder={t('knowledgeDescriptionPlaceholder', { ns: 'datasetPipeline' })}
|
||||
/>
|
||||
|
||||
@@ -244,6 +244,15 @@ describe('DatasetCard Component', () => {
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-1/documents')
|
||||
})
|
||||
|
||||
it('should not change background color on hover', () => {
|
||||
const dataset = createMockDataset()
|
||||
render(<DatasetCard dataset={dataset} />)
|
||||
const card = screen.getByText('Test Dataset').closest('[data-disable-nprogress]')
|
||||
|
||||
expect(card).toHaveClass('bg-components-card-bg')
|
||||
expect(card).not.toHaveClass('hover:bg-components-card-bg-alt')
|
||||
})
|
||||
|
||||
it('should navigate to hitTesting for external provider', () => {
|
||||
const dataset = createMockDataset({ provider: 'external' })
|
||||
render(<DatasetCard dataset={dataset} />)
|
||||
|
||||
@@ -63,7 +63,7 @@ const DatasetCard = ({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="group relative col-span-1 flex h-[190px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-all duration-200 ease-in-out hover:bg-components-card-bg-alt hover:shadow-md hover:shadow-shadow-shadow-5"
|
||||
className="group relative col-span-1 flex h-47.5 cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-all duration-200 ease-in-out hover:shadow-md hover:shadow-shadow-shadow-5"
|
||||
data-disable-nprogress={true}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
|
||||
@@ -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, 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'
|
||||
@@ -108,7 +108,7 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
|
||||
{t('form.desc', { ns: 'datasetSettings' })}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Textarea value={description} onChange={e => setDescription(e.target.value)} className="resize-none" placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''} />
|
||||
<Textarea aria-label={t('form.desc', { ns: 'datasetSettings' })} value={description} onValueChange={value => setDescription(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,11 +85,12 @@ 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}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
onValueChange={value => setDescription(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,7 +9,6 @@ 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'
|
||||
@@ -53,9 +52,9 @@ const SummaryIndexSetting = ({
|
||||
})
|
||||
}, [onSummaryIndexSettingChange])
|
||||
|
||||
const handleSummaryIndexPromptChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const handleSummaryIndexPromptChange = useCallback((value: string) => {
|
||||
onSummaryIndexSettingChange?.({
|
||||
summary_prompt: e.target.value,
|
||||
summary_prompt: value,
|
||||
})
|
||||
}, [onSummaryIndexSettingChange])
|
||||
|
||||
@@ -95,8 +94,9 @@ const SummaryIndexSetting = ({
|
||||
{t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
value={summaryIndexSetting?.summary_prompt ?? ''}
|
||||
onChange={handleSummaryIndexPromptChange}
|
||||
onValueChange={handleSummaryIndexPromptChange}
|
||||
disabled={readonly}
|
||||
placeholder={t('form.summaryInstructionsPlaceholder', { ns: 'datasetSettings' })}
|
||||
/>
|
||||
@@ -166,8 +166,9 @@ const SummaryIndexSetting = ({
|
||||
</div>
|
||||
<div className="grow">
|
||||
<Textarea
|
||||
aria-label={t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
value={summaryIndexSetting?.summary_prompt ?? ''}
|
||||
onChange={handleSummaryIndexPromptChange}
|
||||
onValueChange={handleSummaryIndexPromptChange}
|
||||
disabled={readonly}
|
||||
placeholder={t('form.summaryInstructionsPlaceholder', { ns: 'datasetSettings' })}
|
||||
/>
|
||||
@@ -214,8 +215,9 @@ const SummaryIndexSetting = ({
|
||||
{t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={t('form.summaryInstructions', { ns: 'datasetSettings' })}
|
||||
value={summaryIndexSetting?.summary_prompt ?? ''}
|
||||
onChange={handleSummaryIndexPromptChange}
|
||||
onValueChange={handleSummaryIndexPromptChange}
|
||||
disabled={readonly}
|
||||
placeholder={t('form.summaryInstructionsPlaceholder', { ns: 'datasetSettings' })}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ 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'
|
||||
@@ -10,7 +11,6 @@ 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,10 +145,11 @@ 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}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
onValueChange={value => setDescription(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,8 +55,9 @@ const AppInputsForm = ({
|
||||
if (form.type === InputVarType.paragraph) {
|
||||
return (
|
||||
<Textarea
|
||||
aria-label={label}
|
||||
value={inputs[variable] || ''}
|
||||
onChange={e => handleFormChange(variable, e.target.value)}
|
||||
onValueChange={value => handleFormChange(variable, value)}
|
||||
placeholder={label}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -523,9 +523,7 @@ describe('useToolSelectorState Hook', () => {
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.handleDescriptionChange({
|
||||
target: { value: 'new description' },
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||
result.current.handleDescriptionChange('new description')
|
||||
})
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(
|
||||
@@ -1724,9 +1722,7 @@ describe('Edge Cases', () => {
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.handleDescriptionChange({
|
||||
target: { value: '' },
|
||||
} as React.ChangeEvent<HTMLTextAreaElement>)
|
||||
result.current.handleDescriptionChange('')
|
||||
})
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith(
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
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" />,
|
||||
}))
|
||||
@@ -68,28 +50,28 @@ describe('ToolBaseForm', () => {
|
||||
it('should render description textarea', () => {
|
||||
render(<ToolBaseForm {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('description-textarea')).toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable textarea when no provider_name in value', () => {
|
||||
render(<ToolBaseForm {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('description-textarea')).toBeDisabled()
|
||||
expect(screen.getByRole('textbox')).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.getByTestId('description-textarea')).not.toBeDisabled()
|
||||
expect(screen.getByRole('textbox')).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.getByTestId('description-textarea'), { target: { value: 'Updated' } })
|
||||
expect(mockOnDescriptionChange).toHaveBeenCalled()
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Updated' } })
|
||||
expect(mockOnDescriptionChange).toHaveBeenCalledWith('Updated', expect.any(Object))
|
||||
})
|
||||
|
||||
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: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
onDescriptionChange: (value: string) => void
|
||||
}
|
||||
|
||||
const ToolBaseForm: FC<ToolBaseFormProps> = ({
|
||||
@@ -85,9 +85,10 @@ 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 || ''}
|
||||
onChange={onDescriptionChange}
|
||||
onValueChange={onDescriptionChange}
|
||||
disabled={!value?.provider_name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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'
|
||||
@@ -161,9 +160,8 @@ describe('useToolSelectorState', () => {
|
||||
useToolSelectorState({ value: toolValue, onSelect: mockOnSelect }),
|
||||
)
|
||||
|
||||
const event = { target: { value: 'New description' } } as React.ChangeEvent<HTMLTextAreaElement>
|
||||
act(() => {
|
||||
result.current.handleDescriptionChange(event)
|
||||
result.current.handleDescriptionChange('New description')
|
||||
})
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(expect.objectContaining({
|
||||
|
||||
@@ -144,14 +144,14 @@ export const useToolSelectorState = ({
|
||||
onSelectMultiple?.(toolValues)
|
||||
}, [getToolValue, onSelectMultiple])
|
||||
|
||||
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const handleDescriptionChange = useCallback((description: string) => {
|
||||
if (!value)
|
||||
return
|
||||
onSelect({
|
||||
...value,
|
||||
extra: {
|
||||
...value.extra,
|
||||
description: e.target.value || '',
|
||||
description: description || '',
|
||||
},
|
||||
})
|
||||
}, [value, onSelect])
|
||||
|
||||
@@ -47,17 +47,6 @@ 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} />
|
||||
@@ -102,7 +91,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should initialize description as empty', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const textarea = screen.getByTestId('description-textarea') as HTMLTextAreaElement
|
||||
const textarea = screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.description' }) as HTMLTextAreaElement
|
||||
expect(textarea.value).toBe('')
|
||||
})
|
||||
|
||||
@@ -146,7 +135,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should update description when textarea changes', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
const textarea = screen.getByTestId('description-textarea')
|
||||
const textarea = screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.description' })
|
||||
fireEvent.change(textarea, { target: { value: 'My description' } })
|
||||
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe('My description')
|
||||
@@ -225,7 +214,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
const nameInput = screen.getByTestId('name-input')
|
||||
fireEvent.change(nameInput, { target: { value: ' Trimmed Name ' } })
|
||||
|
||||
const textarea = screen.getByTestId('description-textarea')
|
||||
const textarea = screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.description' })
|
||||
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 = {
|
||||
@@ -108,9 +108,10 @@ const PublishAsKnowledgePipelineModal = ({
|
||||
</div>
|
||||
<Textarea
|
||||
className="resize-none"
|
||||
aria-label={t('common.publishAsPipeline.description', { ns: 'pipeline' })}
|
||||
placeholder={t('common.publishAsPipeline.descriptionPlaceholder', { ns: 'pipeline' }) || ''}
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
onValueChange={value => setDescription(value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ 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,
|
||||
@@ -18,7 +19,6 @@ 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,10 +159,11 @@ 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}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
|
||||
onValueChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'number' && (
|
||||
|
||||
@@ -13,6 +13,7 @@ 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'
|
||||
@@ -23,7 +24,6 @@ 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,9 +280,10 @@ const EditCustomCollectionModal: FC<Props> = ({
|
||||
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={t('createTool.schema', { ns: 'tools' })}
|
||||
className="h-[240px] resize-none"
|
||||
value={schema}
|
||||
onChange={e => setSchema(e.target.value)}
|
||||
onValueChange={value => setSchema(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' })}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
>
|
||||
</Textarea>
|
||||
onValueChange={value => setDescription(value)}
|
||||
/>
|
||||
</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' })}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
>
|
||||
</Textarea>
|
||||
onValueChange={value => onChange(value)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ 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'
|
||||
@@ -23,7 +24,6 @@ import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
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'
|
||||
@@ -256,9 +256,10 @@ 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}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
onValueChange={value => setDescription(value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Tool Input */}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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'
|
||||
@@ -18,7 +19,6 @@ 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,8 +170,9 @@ const FormItem: FC<Props> = ({
|
||||
{
|
||||
type === InputVarType.paragraph && (
|
||||
<Textarea
|
||||
aria-label={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
value={value || ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onValueChange={value => onChange(value)}
|
||||
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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'
|
||||
@@ -10,7 +11,6 @@ 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,8 +190,9 @@ const VarList: FC<Props> = ({
|
||||
)}
|
||||
{assignedVarType === 'string' && (
|
||||
<Textarea
|
||||
aria-label={item.variable_selector?.join('.') || t('nodes.assigner.setParameter', { ns: 'workflow' })}
|
||||
value={item.value as string}
|
||||
onChange={e => handleToAssignedVarChange(index)(e.target.value)}
|
||||
onValueChange={value => handleToAssignedVarChange(index)(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,9 +56,10 @@ 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"
|
||||
onChange={e => setInputString(e.target.value)}
|
||||
onValueChange={value => setInputString(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((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onInstructionChange(e.target.value)
|
||||
const handleInstructionChange = useCallback((value: string) => {
|
||||
onInstructionChange(value)
|
||||
}, [onInstructionChange])
|
||||
|
||||
return (
|
||||
@@ -90,10 +90,11 @@ 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' })}
|
||||
onChange={handleInstructionChange}
|
||||
onValueChange={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((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setEnumValue(e.target.value)
|
||||
const handleEnumChange = useCallback((value: string) => {
|
||||
setEnumValue(value)
|
||||
}, [])
|
||||
|
||||
const handleEnumBlur = useCallback((e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||
@@ -48,13 +48,14 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex h-6 items-center system-xs-medium text-text-secondary">
|
||||
Enum
|
||||
{t('nodes.llm.jsonSchema.enum', { ns: 'workflow' })}
|
||||
</div>
|
||||
<Textarea
|
||||
aria-label={t('nodes.llm.jsonSchema.enum', { ns: 'workflow' })}
|
||||
size="small"
|
||||
className="min-h-6"
|
||||
value={enumValue}
|
||||
onChange={handleEnumChange}
|
||||
onValueChange={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,6 +49,10 @@ const FormItem = ({
|
||||
onChange(e.target.value)
|
||||
}, [onChange])
|
||||
|
||||
const handleValueChange = useCallback((value: string) => {
|
||||
onChange(value)
|
||||
}, [onChange])
|
||||
|
||||
const handleChange = useCallback((value: any) => {
|
||||
onChange(value)
|
||||
}, [onChange])
|
||||
@@ -92,8 +96,9 @@ const FormItem = ({
|
||||
{
|
||||
value_type === ValueType.constant && var_type === VarType.string && (
|
||||
<Textarea
|
||||
aria-label={item.label}
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
onValueChange={handleValueChange}
|
||||
className="min-h-12 w-full"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ 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'
|
||||
@@ -14,7 +15,6 @@ 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,8 +175,9 @@ const AddExtractParameter: FC<Props> = ({
|
||||
)}
|
||||
<Field title={t(`${i18nPrefix}.addExtractParameterContent.description`, { ns: 'workflow' })}>
|
||||
<Textarea
|
||||
aria-label={t(`${i18nPrefix}.addExtractParameterContent.description`, { ns: 'workflow' })}
|
||||
value={param.description}
|
||||
onChange={e => handleParamChange('description')(e.target.value)}
|
||||
onValueChange={value => handleParamChange('description')(value)}
|
||||
placeholder={t(`${i18nPrefix}.addExtractParameterContent.descriptionPlaceholder`, { ns: 'workflow' })!}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { ChunkInfo } from '@/app/components/rag-pipeline/components/chunk-c
|
||||
import type { ParentMode } from '@/models/datasets'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
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'
|
||||
@@ -98,11 +98,12 @@ export function DisplayContent(props: DisplayContentProps) {
|
||||
previewType === PreviewType.Markdown
|
||||
? (
|
||||
<Textarea
|
||||
aria-label={t('debug.variableInspect.markdownContent', { ns: 'workflow' })}
|
||||
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}
|
||||
onChange={e => handleTextChange?.(e.target.value)}
|
||||
onValueChange={value => handleTextChange?.(value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
/>
|
||||
|
||||
@@ -2,9 +2,10 @@ 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 { useTranslation } from 'react-i18next'
|
||||
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'
|
||||
@@ -30,6 +31,8 @@ export const TextEditorSection = ({
|
||||
isTruncated,
|
||||
onTextChange,
|
||||
}: TextEditorSectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
{isTruncated && <LargeDataAlert className="absolute inset-x-3 top-1" />}
|
||||
@@ -46,11 +49,12 @@ export const TextEditorSection = ({
|
||||
)
|
||||
: (
|
||||
<Textarea
|
||||
aria-label={t('errorMsg.fields.variableValue', { ns: 'workflow' })}
|
||||
readOnly={textEditorDisabled}
|
||||
disabled={textEditorDisabled || isTruncated}
|
||||
className={cn('h-full', isTruncated && 'pt-[48px]')}
|
||||
value={typeof value === 'number' ? value : String(value ?? '')}
|
||||
onChange={e => onTextChange(e.target.value)}
|
||||
onValueChange={value => onTextChange(value)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -108,13 +108,14 @@ describe('DatasetCardTags', () => {
|
||||
expect(wrapper).not.toHaveClass('opacity-30')
|
||||
})
|
||||
|
||||
it('should hide mask with CSS when the tag area is hovered', () => {
|
||||
it('should keep the overflow mask independent from dataset card hover', () => {
|
||||
const { container } = render(<DatasetCardTags {...defaultProps} />)
|
||||
const maskDiv = container.querySelector('.bg-tag-selector-mask-bg')
|
||||
expect(maskDiv).toBeInTheDocument()
|
||||
expect(maskDiv).toHaveClass('group-hover/tag-area:hidden')
|
||||
expect(maskDiv).toHaveClass('group-focus-within/tag-area:hidden')
|
||||
expect(maskDiv).toHaveClass('group-hover:bg-tag-selector-mask-hover-bg')
|
||||
expect(maskDiv).not.toHaveClass('group-hover:bg-tag-selector-mask-hover-bg')
|
||||
expect(maskDiv?.parentElement).toHaveClass('relative', 'w-full', 'overflow-hidden')
|
||||
})
|
||||
|
||||
it('should keep TagSelector visible when tags are empty', () => {
|
||||
|
||||
@@ -49,6 +49,16 @@ describe('AppCardTags', () => {
|
||||
value: tags,
|
||||
}))
|
||||
})
|
||||
|
||||
it('should keep the overflow mask independent from app card hover', () => {
|
||||
const { container } = render(<AppCardTags appId="app-1" tags={tags} />)
|
||||
const mask = container.querySelector('.bg-tag-selector-mask-bg')
|
||||
|
||||
expect(mask).toBeInTheDocument()
|
||||
expect(mask).toHaveClass('group-hover/tag-area:hidden')
|
||||
expect(mask).toHaveClass('group-focus-within/tag-area:hidden')
|
||||
expect(mask).not.toHaveClass('group-hover:bg-tag-selector-mask-hover-bg')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Callbacks', () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const AppCardTags = ({
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
onTagsChange={onTagsChange}
|
||||
/>
|
||||
<div className="pointer-events-none absolute top-0 right-0 h-full w-20 bg-tag-selector-mask-bg group-focus-within/tag-area:hidden group-hover:bg-tag-selector-mask-hover-bg group-hover/tag-area:hidden" />
|
||||
<div className="pointer-events-none absolute top-0 right-0 h-full w-20 bg-tag-selector-mask-bg group-focus-within/tag-area:hidden group-hover/tag-area:hidden" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ export const DatasetCardTags = ({
|
||||
onTagsChange,
|
||||
}: DatasetCardTagsProps) => (
|
||||
<div
|
||||
className={cn('group/tag-area relative w-full px-3', !embeddingAvailable && 'opacity-30')}
|
||||
className={cn('group/tag-area w-full px-3', !embeddingAvailable && 'opacity-30')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="w-full">
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<TagSelector
|
||||
placement="bottom-start"
|
||||
type="knowledge"
|
||||
@@ -33,9 +33,9 @@ export const DatasetCardTags = ({
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
onTagsChange={onTagsChange}
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute top-0 right-0 h-full w-20 bg-tag-selector-mask-bg group-focus-within/tag-area:hidden group-hover/tag-area:hidden"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="absolute top-0 right-0 h-full w-20 bg-tag-selector-mask-bg group-focus-within/tag-area:hidden group-hover:bg-tag-selector-mask-hover-bg group-hover/tag-area:hidden"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "الآن يمكنك إنشاء أحداث في {{- pluginName}}، واسترجاع المخرجات من هذه الأحداث في فحص المتغير.",
|
||||
"debug.variableInspect.listening.tipSchedule": "الاستماع للأحداث من مشغلات الجدول.\nالتشغيل المجدول التالي: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "الاستماع للأحداث من المشغلات...",
|
||||
"debug.variableInspect.markdownContent": "محتوى Markdown",
|
||||
"debug.variableInspect.reset": "إعادة تعيين إلى قيمة آخر تشغيل",
|
||||
"debug.variableInspect.resetConversationVar": "إعادة تعيين متغير المحادثة إلى القيمة الافتراضية",
|
||||
"debug.variableInspect.systemNode": "النظام",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "رجوع",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "إضافة وصف",
|
||||
"nodes.llm.jsonSchema.doc": "معرفة المزيد عن الإخراج المنظم",
|
||||
"nodes.llm.jsonSchema.enum": "تعداد",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "اسم الخاصية موجود بالفعل",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "اسم الحقل",
|
||||
"nodes.llm.jsonSchema.generate": "توليد",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Jetzt können Sie in {{- pluginName}} Ereignisse erstellen und Ausgaben dieser Ereignisse im Variableninspektor abrufen.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Hört auf Ereignisse von Zeitplan-Auslösern. Nächster geplanter Lauf: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Höre auf Ereignisse von Triggern...",
|
||||
"debug.variableInspect.markdownContent": "Markdown-Inhalt",
|
||||
"debug.variableInspect.reset": "Auf den letzten Ausführungswert zurücksetzen",
|
||||
"debug.variableInspect.resetConversationVar": "Setze die Gesprächsvariable auf den Standardwert zurück",
|
||||
"debug.variableInspect.systemNode": "System",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Zurück",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Fügen Sie eine Beschreibung hinzu.",
|
||||
"nodes.llm.jsonSchema.doc": "Erfahren Sie mehr über strukturierten Output.",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Eigenschaftsname existiert bereits",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Feldname",
|
||||
"nodes.llm.jsonSchema.generate": "Generieren",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Now you can create events in {{- pluginName}}, and retrieve outputs from these events in the Variable Inspector.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Listening for events from schedule triggers.\nNext scheduled run: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Listening for events from triggers...",
|
||||
"debug.variableInspect.markdownContent": "Markdown content",
|
||||
"debug.variableInspect.reset": "Reset to last run value",
|
||||
"debug.variableInspect.resetConversationVar": "Reset conversation variable to default value",
|
||||
"debug.variableInspect.systemNode": "System",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Back",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Add description",
|
||||
"nodes.llm.jsonSchema.doc": "Learn more about structured output",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Property name already exists",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Field Name",
|
||||
"nodes.llm.jsonSchema.generate": "Generate",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Ahora puedes crear eventos en {{- pluginName}} y obtener los resultados de estos eventos en el Inspector de Variables.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Escuchando eventos de los desencadenadores de programación.\nPróxima ejecución programada: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Escuchando eventos desde los activadores...",
|
||||
"debug.variableInspect.markdownContent": "Contenido Markdown",
|
||||
"debug.variableInspect.reset": "Restablecer al último valor ejecutado",
|
||||
"debug.variableInspect.resetConversationVar": "Restablecer la variable de conversación al valor predeterminado",
|
||||
"debug.variableInspect.systemNode": "Sistema",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Atrás",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Agregar descripción",
|
||||
"nodes.llm.jsonSchema.doc": "Aprender más sobre la salida estructurada",
|
||||
"nodes.llm.jsonSchema.enum": "Enumeración",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "El nombre de la propiedad ya existe",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Nombre del campo",
|
||||
"nodes.llm.jsonSchema.generate": "Generar",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "اکنون میتوانید در {{- pluginName}} رویداد ایجاد کنید و خروجیها را در بازرسی متغیر مشاهده کنید.",
|
||||
"debug.variableInspect.listening.tipSchedule": "گوش دادن به رویدادها از تریگرهای زمانبندیشده.\nزمان اجرای بعدی: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "در انتظار رویدادها از تریگرها...",
|
||||
"debug.variableInspect.markdownContent": "محتوای Markdown",
|
||||
"debug.variableInspect.reset": "بازنشانی به آخرین مقدار اجراشده",
|
||||
"debug.variableInspect.resetConversationVar": "بازنشانی متغیر مکالمه به مقدار پیشفرض",
|
||||
"debug.variableInspect.systemNode": "سیستم",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "بازگشت",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "افزودن توضیحات",
|
||||
"nodes.llm.jsonSchema.doc": "درباره خروجی ساختاریافته بیشتر بدانید",
|
||||
"nodes.llm.jsonSchema.enum": "شمارش",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "نام ویژگی از قبل وجود دارد",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "نام فیلد",
|
||||
"nodes.llm.jsonSchema.generate": "تولید",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Vous pouvez maintenant créer des événements dans {{- pluginName}} et récupérer les résultats de ces événements dans l'Inspecteur de Variables.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Écoute des événements des déclencheurs de planification.\nProchaine exécution planifiée : {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "En attente d'événements provenant des déclencheurs...",
|
||||
"debug.variableInspect.markdownContent": "Contenu Markdown",
|
||||
"debug.variableInspect.reset": "Réinitialiser à la dernière valeur d'exécution",
|
||||
"debug.variableInspect.resetConversationVar": "Réinitialiser la variable de conversation à la valeur par défaut",
|
||||
"debug.variableInspect.systemNode": "Système",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Retour",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Ajouter une description",
|
||||
"nodes.llm.jsonSchema.doc": "En savoir plus sur la sortie structurée",
|
||||
"nodes.llm.jsonSchema.enum": "Énumération",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Le nom de la propriété existe déjà",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Nom du champ",
|
||||
"nodes.llm.jsonSchema.generate": "Générer",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "अब आप {{- pluginName}} में ईवेंट बना सकते हैं, और वैरिएबल इंस्पेक्टर में इन ईवेंट्स के आउटपुट प्राप्त कर सकते हैं।",
|
||||
"debug.variableInspect.listening.tipSchedule": "अनुसूची ट्रिगर्स से घटनाओं के लिए सुनना।\nअगली निर्धारित रन: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "ट्रिगर से घटनाओं को सुनना...",
|
||||
"debug.variableInspect.markdownContent": "Markdown सामग्री",
|
||||
"debug.variableInspect.reset": "अंतिम रन मान पर रीसेट करें",
|
||||
"debug.variableInspect.resetConversationVar": "संवाद चर को डिफ़ॉल्ट मान पर रीसेट करें",
|
||||
"debug.variableInspect.systemNode": "प्रणाली",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "पीछे",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "विवरण जोड़ें",
|
||||
"nodes.llm.jsonSchema.doc": "संरचित आउटपुट के बारे में अधिक जानें",
|
||||
"nodes.llm.jsonSchema.enum": "एनम",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "प्रॉपर्टी नाम पहले से मौजूद है",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "क्षेत्र नाम",
|
||||
"nodes.llm.jsonSchema.generate": "जनरेट करें",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Sekarang Anda dapat membuat acara di {{- pluginName}}, dan mengambil hasil dari acara ini di Inspektur Variabel.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Mendengarkan acara dari pemicu jadwal.\nJalankan berikutnya yang dijadwalkan: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Mendengarkan peristiwa dari pemicu...",
|
||||
"debug.variableInspect.markdownContent": "Konten Markdown",
|
||||
"debug.variableInspect.reset": "Atur ulang ke nilai eksekusi terakhir",
|
||||
"debug.variableInspect.resetConversationVar": "Mengatur ulang variabel percakapan ke nilai default",
|
||||
"debug.variableInspect.systemNode": "Sistem",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Belakang",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Tambahkan deskripsi",
|
||||
"nodes.llm.jsonSchema.doc": "Pelajari output terstruktur lebih lanjut",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Nama properti sudah ada",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Nama Bidang",
|
||||
"nodes.llm.jsonSchema.generate": "Menghasilkan",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Ora puoi creare eventi in {{- pluginName}} e recuperare i risultati di questi eventi nell'Ispettore Variabili.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Ascolto degli eventi dai trigger del programma.\nProssima esecuzione programmata: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "In ascolto degli eventi dai trigger...",
|
||||
"debug.variableInspect.markdownContent": "Contenuto Markdown",
|
||||
"debug.variableInspect.reset": "Ripristina il valore dell'ultima esecuzione",
|
||||
"debug.variableInspect.resetConversationVar": "Reimposta la variabile della conversazione al valore predefinito",
|
||||
"debug.variableInspect.systemNode": "Sistema",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Indietro",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Aggiungi descrizione",
|
||||
"nodes.llm.jsonSchema.doc": "Scopri di più sull'output strutturato",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Il nome della proprietà esiste già",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Nome del campo",
|
||||
"nodes.llm.jsonSchema.generate": "Genera",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "{{- pluginName}} でイベントを作成し、これらのイベントの出力を Variable Inspector で取得できます。",
|
||||
"debug.variableInspect.listening.tipSchedule": "スケジュールトリガーからのイベントを待機しています。\n次回の予定実行: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "トリガーからのイベントを待機中…",
|
||||
"debug.variableInspect.markdownContent": "Markdown コンテンツ",
|
||||
"debug.variableInspect.reset": "最後の実行値にリセットする",
|
||||
"debug.variableInspect.resetConversationVar": "会話の変数をデフォルト値にリセットする",
|
||||
"debug.variableInspect.systemNode": "システム",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "前に戻る",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "説明を入力",
|
||||
"nodes.llm.jsonSchema.doc": "構造化出力の詳細を見る",
|
||||
"nodes.llm.jsonSchema.enum": "列挙型",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "プロパティ名はすでに存在します",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "フィールド名",
|
||||
"nodes.llm.jsonSchema.generate": "生成",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "이제 {{- pluginName}}에서 이벤트를 생성하고, 변수 검사기에서 이러한 이벤트의 출력을 확인할 수 있습니다.",
|
||||
"debug.variableInspect.listening.tipSchedule": "스케줄 트리거의 이벤트를 수신 대기 중입니다.\n다음 예약 실행: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "트리거 이벤트 수신 대기 중...",
|
||||
"debug.variableInspect.markdownContent": "Markdown 콘텐츠",
|
||||
"debug.variableInspect.reset": "마지막 실행 값으로 재설정",
|
||||
"debug.variableInspect.resetConversationVar": "대화 변수를 기본 값으로 재설정합니다.",
|
||||
"debug.variableInspect.systemNode": "시스템",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "뒤",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "설명을 추가하세요.",
|
||||
"nodes.llm.jsonSchema.doc": "구조화된 출력에 대해 더 알아보세요.",
|
||||
"nodes.llm.jsonSchema.enum": "열거형",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "속성 이름이 이미 존재합니다",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "필드 이름",
|
||||
"nodes.llm.jsonSchema.generate": "생성",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Now you can create events in {{- pluginName}}, and retrieve outputs from these events in the Variable Inspector.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Listening for events from schedule triggers.\nNext scheduled run: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Listening for events from triggers...",
|
||||
"debug.variableInspect.markdownContent": "Markdown-inhoud",
|
||||
"debug.variableInspect.reset": "Reset to last run value",
|
||||
"debug.variableInspect.resetConversationVar": "Reset conversation variable to default value",
|
||||
"debug.variableInspect.systemNode": "System",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Back",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Add description",
|
||||
"nodes.llm.jsonSchema.doc": "Learn more about structured output",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Eigenschapsnaam bestaat al",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Field Name",
|
||||
"nodes.llm.jsonSchema.generate": "Generate",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Teraz możesz tworzyć zdarzenia w {{- pluginName}} i pobierać wyniki z tych zdarzeń w Inspektorze Zmiennych.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Nasłuchiwanie zdarzeń z wyzwalaczy harmonogramu.\nNastępne zaplanowane uruchomienie: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Oczekiwanie na zdarzenia wywoływane przez wyzwalacze...",
|
||||
"debug.variableInspect.markdownContent": "Treść Markdown",
|
||||
"debug.variableInspect.reset": "Zresetuj do ostatniej wartości run",
|
||||
"debug.variableInspect.resetConversationVar": "Zresetuj zmienną rozmowy do wartości domyślnej",
|
||||
"debug.variableInspect.systemNode": "System",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Tył",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Dodaj opis",
|
||||
"nodes.llm.jsonSchema.doc": "Dowiedz się więcej o zorganizowanym wyjściu",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Nazwa właściwości już istnieje",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Nazwa pola",
|
||||
"nodes.llm.jsonSchema.generate": "Generować",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Agora você pode criar eventos em {{- pluginName}} e recuperar resultados desses eventos no Inspetor de Variáveis.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Ouvindo eventos de gatilhos de agendamento.\nPróxima execução agendada: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Aguardando eventos dos gatilhos...",
|
||||
"debug.variableInspect.markdownContent": "Conteúdo Markdown",
|
||||
"debug.variableInspect.reset": "Redefinir para o último valor de execução",
|
||||
"debug.variableInspect.resetConversationVar": "Redefinir a variável da conversa para o valor padrão",
|
||||
"debug.variableInspect.systemNode": "Sistema",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Voltar",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Adicionar descrição",
|
||||
"nodes.llm.jsonSchema.doc": "Saiba mais sobre saída estruturada",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "O nome da propriedade já existe",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Nome do Campo",
|
||||
"nodes.llm.jsonSchema.generate": "Gerar",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Acum poți crea evenimente în {{- pluginName}} și poți prelua rezultatele acestor evenimente în Inspectorul de Variabile.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Ascultarea evenimentelor de la declanșatoarele de programare.\nUrmătoarea rulare programată: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Ascult pentru evenimente de la declanșatoare...",
|
||||
"debug.variableInspect.markdownContent": "Conținut Markdown",
|
||||
"debug.variableInspect.reset": "Resetează la ultima valoare rulată",
|
||||
"debug.variableInspect.resetConversationVar": "Resetați variabila de conversație la valoarea implicită",
|
||||
"debug.variableInspect.systemNode": "Sistem",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Înapoi",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Adăugați o descriere",
|
||||
"nodes.llm.jsonSchema.doc": "Aflați mai multe despre ieșirea structurată",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Numele proprietății există deja",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Numele câmpului",
|
||||
"nodes.llm.jsonSchema.generate": "Generează",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Теперь вы можете создавать события в {{- pluginName}} и получать данные этих событий в Инспекторе переменных.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Прослушивание событий от триггеров расписания.\nСледующий запланированный запуск: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Ожидание событий от триггеров...",
|
||||
"debug.variableInspect.markdownContent": "Содержимое Markdown",
|
||||
"debug.variableInspect.reset": "Сбросить до последнего значения выполнения",
|
||||
"debug.variableInspect.resetConversationVar": "Сбросить переменную разговора до значения по умолчанию",
|
||||
"debug.variableInspect.systemNode": "Система",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Спина",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Добавить описание",
|
||||
"nodes.llm.jsonSchema.doc": "Узнайте больше о структурированном выводе",
|
||||
"nodes.llm.jsonSchema.enum": "Перечисление",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Имя свойства уже существует",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Название поля",
|
||||
"nodes.llm.jsonSchema.generate": "Сгенерировать",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Zdaj lahko ustvarjate dogodke v {{- pluginName}} in pridobivate izhode iz teh dogodkov v Inšpektorju spremenljivk.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Poslušanje dogodkov iz sprožilcev urnika.\nNaslednje načrtovano izvajanje: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Poslušanje dogodkov iz sprožilcev...",
|
||||
"debug.variableInspect.markdownContent": "Vsebina Markdown",
|
||||
"debug.variableInspect.reset": "Ponastavi na zadnjo vrednost izvajanja",
|
||||
"debug.variableInspect.resetConversationVar": "Ponastavi spremenljivko pogovora na privzeto vrednost",
|
||||
"debug.variableInspect.systemNode": "Sistem",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Nazaj",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Dodajte opis",
|
||||
"nodes.llm.jsonSchema.doc": "Izvedite več o strukturiranem izhodu",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Ime lastnosti že obstaja",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Ime polja",
|
||||
"nodes.llm.jsonSchema.generate": "Generirati",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "ตอนนี้คุณสามารถสร้างกิจกรรมใน {{- pluginName}} และดึงผลลัพธ์จากกิจกรรมเหล่านี้ในตัวตรวจสอบตัวแปรได้แล้ว",
|
||||
"debug.variableInspect.listening.tipSchedule": "กำลังรอฟังเหตุการณ์จากตัวเรียกใช้งานตามตาราง การทำงานครั้งถัดไปตามตาราง: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "กำลังรอฟังเหตุการณ์จากทริกเกอร์...",
|
||||
"debug.variableInspect.markdownContent": "เนื้อหา Markdown",
|
||||
"debug.variableInspect.reset": "รีเซ็ตกลับไปยังค่าครั้งล่าสุด",
|
||||
"debug.variableInspect.resetConversationVar": "รีเซ็ตตัวแปรการสนทนาไปยังค่าตั้งต้น",
|
||||
"debug.variableInspect.systemNode": "ระบบ",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "กลับ",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "เพิ่มคำอธิบาย",
|
||||
"nodes.llm.jsonSchema.doc": "เรียนรู้เพิ่มเติมเกี่ยวกับผลลัพธ์ที่มีโครงสร้าง",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "ชื่อคุณสมบัติมีอยู่แล้ว",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "ชื่อฟิลด์",
|
||||
"nodes.llm.jsonSchema.generate": "สร้าง",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Artık {{- pluginName}} içinde etkinlikler oluşturabilir ve bu etkinliklerden elde edilen çıktıları Değişken Denetleyicisinde görebilirsiniz.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Zamanlayıcı tetikleyicilerinden etkinlikleri dinleme. Bir sonraki planlanan çalıştırma: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Tetikleyicilerden olaylar dinleniyor...",
|
||||
"debug.variableInspect.markdownContent": "Markdown içeriği",
|
||||
"debug.variableInspect.reset": "Son çalıştırma değerine sıfırla",
|
||||
"debug.variableInspect.resetConversationVar": "Konuşma değişkenini varsayılan değere sıfırla",
|
||||
"debug.variableInspect.systemNode": "Sistem",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Geri",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Açıklama ekleyin",
|
||||
"nodes.llm.jsonSchema.doc": "Yapılandırılmış çıktı hakkında daha fazla bilgi edinin",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Özellik adı zaten mevcut",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Alan Adı",
|
||||
"nodes.llm.jsonSchema.generate": "Oluştur",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Тепер ви можете створювати події в {{- pluginName}} та отримувати результати цих подій у Інспекторі змінних.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Прослуховування подій від тригерів розкладу. Наступний запланований запуск: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Очікування подій від тригерів...",
|
||||
"debug.variableInspect.markdownContent": "Вміст Markdown",
|
||||
"debug.variableInspect.reset": "Скинути до значення останнього запуску",
|
||||
"debug.variableInspect.resetConversationVar": "Скинути змінну розмови на значення за замовчуванням",
|
||||
"debug.variableInspect.systemNode": "Система",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Назад",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Додати опис",
|
||||
"nodes.llm.jsonSchema.doc": "Дізнайтеся більше про структурований вихід",
|
||||
"nodes.llm.jsonSchema.enum": "Перелік",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Ім'я властивості вже існує",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Назва поля",
|
||||
"nodes.llm.jsonSchema.generate": "Генерувати",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "Bây giờ bạn có thể tạo các sự kiện trong {{- pluginName}} và lấy kết quả từ các sự kiện này trong Trình kiểm tra Biến.",
|
||||
"debug.variableInspect.listening.tipSchedule": "Lắng nghe sự kiện từ các tác nhân kích hoạt theo lịch. Chạy theo lịch tiếp theo: {{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "Đang lắng nghe các sự kiện từ các kích hoạt...",
|
||||
"debug.variableInspect.markdownContent": "Nội dung Markdown",
|
||||
"debug.variableInspect.reset": "Đặt lại thành giá trị của lần chạy cuối cùng",
|
||||
"debug.variableInspect.resetConversationVar": "Đặt lại biến cuộc trò chuyện về giá trị mặc định",
|
||||
"debug.variableInspect.systemNode": "Hệ thống",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "Quay lại",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "Thêm mô tả",
|
||||
"nodes.llm.jsonSchema.doc": "Tìm hiểu thêm về đầu ra có cấu trúc",
|
||||
"nodes.llm.jsonSchema.enum": "Enum",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "Tên thuộc tính đã tồn tại",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "Tên trường",
|
||||
"nodes.llm.jsonSchema.generate": "Tạo ra",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "现在您可以在 {{- pluginName}} 中创建事件,并在变量检查器中查看这些事件的输出。",
|
||||
"debug.variableInspect.listening.tipSchedule": "正在监听计划触发器事件。\n下一次计划运行时间:{{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "正在监听触发器事件…",
|
||||
"debug.variableInspect.markdownContent": "Markdown 内容",
|
||||
"debug.variableInspect.reset": "还原至上一次运行",
|
||||
"debug.variableInspect.resetConversationVar": "重置会话变量为默认值",
|
||||
"debug.variableInspect.systemNode": "系统变量",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "返回",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "添加描述",
|
||||
"nodes.llm.jsonSchema.doc": "了解有关结构化输出的更多信息",
|
||||
"nodes.llm.jsonSchema.enum": "枚举",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "属性名称已存在",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "字段名",
|
||||
"nodes.llm.jsonSchema.generate": "生成",
|
||||
|
||||
@@ -302,6 +302,7 @@
|
||||
"debug.variableInspect.listening.tipPlugin": "您現在可以在 {{- pluginName}} 中建立事件,並在變數檢視器中檢視這些事件的輸出。",
|
||||
"debug.variableInspect.listening.tipSchedule": "正在監聽排程觸發器事件。\n下一次排程執行時間:{{nextTriggerTime}}",
|
||||
"debug.variableInspect.listening.title": "正在監聽觸發器事件…",
|
||||
"debug.variableInspect.markdownContent": "Markdown 內容",
|
||||
"debug.variableInspect.reset": "重置為上次運行值",
|
||||
"debug.variableInspect.resetConversationVar": "將對話變數重置為默認值",
|
||||
"debug.variableInspect.systemNode": "系統",
|
||||
@@ -788,6 +789,7 @@
|
||||
"nodes.llm.jsonSchema.back": "返回",
|
||||
"nodes.llm.jsonSchema.descriptionPlaceholder": "新增描述",
|
||||
"nodes.llm.jsonSchema.doc": "了解更多有關結構化輸出的資訊",
|
||||
"nodes.llm.jsonSchema.enum": "列舉",
|
||||
"nodes.llm.jsonSchema.fieldNameAlreadyExists": "屬性名稱已存在",
|
||||
"nodes.llm.jsonSchema.fieldNamePlaceholder": "欄位名稱",
|
||||
"nodes.llm.jsonSchema.generate": "生成",
|
||||
|
||||
Reference in New Issue
Block a user