fix(web): form content style issues

This commit is contained in:
JzoNg 2026-04-23 17:43:58 +08:00
parent 1a3c1a9b32
commit 7c348e994c
6 changed files with 98 additions and 64 deletions

View File

@ -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)')
})
})

View File

@ -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>
)
}

View File

@ -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: {

View File

@ -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 && (

View File

@ -203,6 +203,7 @@ const InputField: React.FC<InputFieldProps> = ({
<TypeSelector
value={tempPayload.type}
items={fieldTypeItems}
popupClassName="z-[1000000]"
onSelect={handleTypeChange}
/>
</div>

View File

@ -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',
})
},