mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 14:14:17 +08:00
fix(web): form content style issues
This commit is contained in:
parent
1a3c1a9b32
commit
7c348e994c
@ -1,22 +1,16 @@
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import TypeSelector from '../type-select'
|
||||
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
PortalToFollowElemTrigger: ({ children, onClick }: { children: React.ReactNode, onClick?: () => void }) => (
|
||||
<button type="button" onClick={onClick}>{children}</button>
|
||||
),
|
||||
PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/input-var-type-icon', () => ({
|
||||
default: ({ type }: { type: string }) => <span>{type}</span>,
|
||||
}))
|
||||
|
||||
describe('TypeSelector', () => {
|
||||
it('should toggle open state and select a new variable type', () => {
|
||||
it('should select a new variable type when an option is clicked', async () => {
|
||||
const onSelect = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TypeSelector
|
||||
@ -29,9 +23,32 @@ describe('TypeSelector', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
fireEvent.click(screen.getByText('Number'))
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
const [, numberOption] = await screen.findAllByRole('option')
|
||||
await user.click(numberOption)
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith({ value: 'number', name: 'Number' })
|
||||
})
|
||||
|
||||
it('should size popup content to match the trigger width', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TypeSelector
|
||||
value="text-input"
|
||||
onSelect={vi.fn()}
|
||||
items={[
|
||||
{ value: 'text-input' as any, name: 'Text' },
|
||||
{ value: 'number' as any, name: 'Number' },
|
||||
]}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('combobox'))
|
||||
|
||||
const [, numberOption] = await screen.findAllByRole('option')
|
||||
const popup = numberOption.closest('[data-side]')
|
||||
|
||||
expect(popup).toHaveClass('w-(--anchor-width)')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,16 +1,10 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { InputVarType } from '@/app/components/workflow/types'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Select, SelectContent, SelectItem, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
|
||||
import { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
@ -32,65 +26,68 @@ const TypeSelector: FC<Props> = ({
|
||||
value,
|
||||
onSelect,
|
||||
items,
|
||||
popupClassName,
|
||||
popupInnerClassName,
|
||||
readonly,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const selectedItem = value ? items.find(item => item.value === value) : undefined
|
||||
const selectedItem = value ? items.find(item => `${item.value}` === `${value}`) : undefined
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement="bottom-start"
|
||||
offset={4}
|
||||
<Select
|
||||
value={selectedItem ? `${selectedItem.value}` : null}
|
||||
readOnly={readonly}
|
||||
onValueChange={(nextValue) => {
|
||||
if (!nextValue)
|
||||
return
|
||||
|
||||
const nextItem = items.find(item => `${item.value}` === nextValue)
|
||||
if (nextItem)
|
||||
onSelect(nextItem)
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => !readonly && setOpen(v => !v)} className="w-full">
|
||||
<div
|
||||
className={cn(`group flex h-9 items-center justify-between rounded-lg border-0 bg-components-input-bg-normal px-2 text-sm hover:bg-state-base-hover-alt ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'}`)}
|
||||
title={selectedItem?.name}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<SelectTrigger className="h-9 rounded-lg px-2 text-sm" title={selectedItem?.name}>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<InputVarTypeIcon type={selectedItem?.value as InputVarType} className="size-4 shrink-0 text-text-secondary" />
|
||||
<span
|
||||
className={`
|
||||
ml-1.5 text-components-input-text-filled ${!selectedItem?.name && 'text-components-input-text-placeholder'}
|
||||
`}
|
||||
className={cn(
|
||||
'ml-1.5 truncate text-components-input-text-filled',
|
||||
!selectedItem?.name && 'text-components-input-text-placeholder',
|
||||
)}
|
||||
>
|
||||
{selectedItem?.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className="flex shrink-0 items-center">
|
||||
<Badge uppercase={false}>{inputVarTypeToVarType(selectedItem?.value as InputVarType)}</Badge>
|
||||
<ChevronDownIcon className={cn('h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-61">
|
||||
<div
|
||||
className={cn('w-[432px] rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg px-1 py-1 text-base shadow-lg focus:outline-hidden sm:text-sm', popupInnerClassName)}
|
||||
>
|
||||
{items.map((item: Item) => (
|
||||
<div
|
||||
key={item.value}
|
||||
className="flex h-9 cursor-pointer items-center justify-between rounded-lg px-2 text-text-secondary hover:bg-state-base-hover"
|
||||
title={item.name}
|
||||
onClick={() => {
|
||||
onSelect(item)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
className={popupClassName}
|
||||
popupClassName={cn('w-(--anchor-width) text-base sm:text-sm', popupInnerClassName)}
|
||||
listClassName="p-1"
|
||||
>
|
||||
{items.map((item: Item) => (
|
||||
<SelectItem
|
||||
key={item.value}
|
||||
value={`${item.value}`}
|
||||
className="h-9 justify-between px-2"
|
||||
title={item.name}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center space-x-2">
|
||||
<InputVarTypeIcon type={item.value} className="size-4 shrink-0 text-text-secondary" />
|
||||
<span title={item.name}>{item.name}</span>
|
||||
<SelectItemText title={item.name} className="mr-0 px-0">{item.name}</SelectItemText>
|
||||
</div>
|
||||
<Badge uppercase={false}>{inputVarTypeToVarType(item.value)}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -129,6 +129,25 @@ describe('HITLInputComponentUI', () => {
|
||||
expect(getByText('alpha, beta')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render input type label after the summary content', () => {
|
||||
const { getByText } = renderComponent({
|
||||
formInput: {
|
||||
type: InputVarType.select,
|
||||
output_variable_name: 'customer_name',
|
||||
option_source: {
|
||||
type: 'constant',
|
||||
selector: [],
|
||||
value: ['alpha', 'beta'],
|
||||
},
|
||||
} satisfies FormInputItem,
|
||||
})
|
||||
|
||||
const summary = getByText('alpha, beta')
|
||||
const typeLabel = getByText('appDebug.variableConfig.select')
|
||||
|
||||
expect(summary.compareDocumentPosition(typeLabel) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should render file-list summary with max uploads', () => {
|
||||
const { getByText } = renderComponent({
|
||||
formInput: {
|
||||
|
||||
@ -147,11 +147,8 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center gap-x-0.5 pr-5">
|
||||
<div className="flex w-full items-center gap-x-2 pr-5">
|
||||
<div className="min-w-0 grow">
|
||||
<div className="max-w-full truncate system-2xs-medium text-text-tertiary uppercase">
|
||||
{inputTypeLabel}
|
||||
</div>
|
||||
{variableSelector
|
||||
? (
|
||||
<VariableBlock
|
||||
@ -169,6 +166,9 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="shrink-0 system-2xs-medium text-text-tertiary uppercase">
|
||||
{inputTypeLabel}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
{!readonly && (
|
||||
|
||||
@ -203,6 +203,7 @@ const InputField: React.FC<InputFieldProps> = ({
|
||||
<TypeSelector
|
||||
value={tempPayload.type}
|
||||
items={fieldTypeItems}
|
||||
popupClassName="z-[1000000]"
|
||||
onSelect={handleTypeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -158,7 +158,7 @@ export default function ShortcutsPopupPlugin({
|
||||
apply({ availableWidth, availableHeight, elements }) {
|
||||
Object.assign(elements.floating.style, {
|
||||
maxWidth: `${Math.min(400, availableWidth)}px`,
|
||||
maxHeight: `${Math.min(300, availableHeight)}px`,
|
||||
maxHeight: `${Math.min(560, availableHeight)}px`,
|
||||
overflow: 'auto',
|
||||
})
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user