refactor(dify-ui): govern public component APIs (#39806)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh
2026-07-30 10:12:34 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent 3aa937def4
commit 563cf2602f
115 changed files with 2378 additions and 975 deletions
@@ -1,4 +1,3 @@
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { FormData } from './form'
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
@@ -99,7 +98,7 @@ const LoadedFormContent = ({
<Button
key={action.id}
disabled={isActionDisabled}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
variant={getButtonStyle(action.button_style)}
onClick={() => submit(action.id)}
>
{action.title}
@@ -163,10 +163,8 @@ export default function AddMemberOrGroupDialog() {
)}
{hasResults ? (
<>
<ComboboxList className="max-h-none p-1">
{(subject: Subject) => (
<SubjectItem key={getSubjectValue(subject)} subject={subject} />
)}
<ComboboxList<Subject> className="max-h-none p-1">
{(subject) => <SubjectItem key={getSubjectValue(subject)} subject={subject} />}
</ComboboxList>
{isFetchingNextPage && <Loading />}
<div ref={anchorRef} className="h-0" />
+7 -13
View File
@@ -20,16 +20,6 @@ type AppSortFilterProps = {
onChange: (value: AppListSortBy) => void
}
const appListSortByValues: AppListSortBy[] = [
'last_modified',
'recently_created',
'earliest_created',
]
function isAppListSortBy(value: string): value is AppListSortBy {
return appListSortByValues.includes(value as AppListSortBy)
}
export function AppSortFilter({ value, onChange }: AppSortFilterProps) {
const { t } = useTranslation()
@@ -65,12 +55,16 @@ export function AppSortFilter({ value, onChange }: AppSortFilterProps) {
<span aria-hidden className="i-ri-arrow-down-s-line size-4 shrink-0 text-text-tertiary" />
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-start" sideOffset={4} popupClassName="w-[220px]">
<DropdownMenuRadioGroup
<DropdownMenuRadioGroup<AppListSortBy>
value={value}
onValueChange={(nextValue) => isAppListSortBy(nextValue) && onChange(nextValue)}
onValueChange={(nextValue) => onChange(nextValue)}
>
{options.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value} closeOnClick>
<DropdownMenuRadioItem<AppListSortBy>
key={option.value}
value={option.value}
closeOnClick
>
<span className="min-w-0 flex-1 truncate">{option.text}</span>
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
@@ -2,14 +2,7 @@ import { parseAsStringLiteral } from 'nuqs'
import { AppModes } from '@/types/app'
const APP_LIST_CATEGORY_VALUES = ['all', ...AppModes] as const
type AppListCategory = (typeof APP_LIST_CATEGORY_VALUES)[number]
export type { AppListCategory }
const appListCategorySet = new Set<string>(APP_LIST_CATEGORY_VALUES)
export const isAppListCategory = (value: string): value is AppListCategory => {
return appListCategorySet.has(value)
}
export type AppListCategory = (typeof APP_LIST_CATEGORY_VALUES)[number]
export const parseAsAppListCategory = parseAsStringLiteral(APP_LIST_CATEGORY_VALUES)
.withDefault('all')
+36 -36
View File
@@ -13,7 +13,6 @@ import {
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { AppModeEnum } from '@/types/app'
import { isAppListCategory } from './app-type-filter-shared'
const chipClassName =
'flex h-8 items-center whitespace-nowrap rounded-lg border-[0.5px] px-2 text-[13px] leading-4 outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-state-accent-solid'
@@ -27,38 +26,39 @@ export function AppTypeFilter({ value, onChange }: AppTypeFilterProps) {
const { t } = useTranslation()
const options = useMemo(
() => [
{
value: 'all',
text: t(($) => $['types.all'], { ns: 'app' }),
iconClassName: 'i-ri-apps-2-line',
},
{
value: AppModeEnum.WORKFLOW,
text: t(($) => $['types.workflow'], { ns: 'app' }),
iconClassName: 'i-ri-exchange-2-line',
},
{
value: AppModeEnum.ADVANCED_CHAT,
text: t(($) => $['types.advanced'], { ns: 'app' }),
iconClassName: 'i-ri-message-3-line',
},
{
value: AppModeEnum.CHAT,
text: t(($) => $['types.chatbot'], { ns: 'app' }),
iconClassName: 'i-ri-message-3-line',
},
{
value: AppModeEnum.AGENT_CHAT,
text: t(($) => $['types.agent'], { ns: 'app' }),
iconClassName: 'i-ri-robot-3-line',
},
{
value: AppModeEnum.COMPLETION,
text: t(($) => $['newApp.completeApp'], { ns: 'app' }),
iconClassName: 'i-ri-file-4-line',
},
],
() =>
[
{
value: 'all',
text: t(($) => $['types.all'], { ns: 'app' }),
iconClassName: 'i-ri-apps-2-line',
},
{
value: AppModeEnum.WORKFLOW,
text: t(($) => $['types.workflow'], { ns: 'app' }),
iconClassName: 'i-ri-exchange-2-line',
},
{
value: AppModeEnum.ADVANCED_CHAT,
text: t(($) => $['types.advanced'], { ns: 'app' }),
iconClassName: 'i-ri-message-3-line',
},
{
value: AppModeEnum.CHAT,
text: t(($) => $['types.chatbot'], { ns: 'app' }),
iconClassName: 'i-ri-message-3-line',
},
{
value: AppModeEnum.AGENT_CHAT,
text: t(($) => $['types.agent'], { ns: 'app' }),
iconClassName: 'i-ri-robot-3-line',
},
{
value: AppModeEnum.COMPLETION,
text: t(($) => $['newApp.completeApp'], { ns: 'app' }),
iconClassName: 'i-ri-file-4-line',
},
] satisfies Array<{ value: AppListCategory; text: string; iconClassName: string }>,
[t],
)
@@ -87,12 +87,12 @@ export function AppTypeFilter({ value, onChange }: AppTypeFilterProps) {
<span aria-hidden className="i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-tertiary" />
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-start" popupClassName="w-[220px]">
<DropdownMenuRadioGroup
<DropdownMenuRadioGroup<AppListCategory>
value={value}
onValueChange={(nextValue) => isAppListCategory(nextValue) && onChange(nextValue)}
onValueChange={(nextValue) => onChange(nextValue)}
>
{options.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value}>
<DropdownMenuRadioItem<AppListCategory> key={option.value} value={option.value}>
<span
aria-hidden
className={cn('h-4 w-4 shrink-0 text-text-tertiary', option.iconClassName)}
@@ -103,7 +103,7 @@ const InputsFormContent = ({ showTip }: Props) => {
/>
)}
{form.type === InputVarType.select && (
<Select
<Select<string>
value={(inputsFormValue?.[form.variable] ?? form.default ?? '') || null}
onValueChange={(value) => value && handleFormChange(form.variable, value)}
>
@@ -1,5 +1,4 @@
'use client'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { HumanInputFieldValue } from './field-renderer'
import type { HumanInputFormProps } from './type'
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
@@ -63,7 +62,7 @@ const HumanInputForm = ({ formData, onSubmit }: HumanInputFormProps) => {
<Button
key={action.id}
disabled={isActionDisabled}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
variant={getButtonStyle(action.button_style)}
onClick={() => formToken && submit(formToken, action.id, inputs)}
>
{action.title}
@@ -1,3 +1,4 @@
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { HumanInputFieldValue } from './field-renderer'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
@@ -24,7 +25,7 @@ dayjs.extend(utc)
dayjs.extend(relativeTime)
dayjs.extend(isSameOrAfter)
export const getButtonStyle = (style: UserActionButtonType) => {
export const getButtonStyle = (style: UserActionButtonType): ButtonProps['variant'] => {
if (style === UserActionButtonType.Primary) return 'primary'
if (style === UserActionButtonType.Default) return 'secondary'
if (style === UserActionButtonType.Accent) return 'secondary-accent'
@@ -107,7 +107,7 @@ const InputsFormContent = ({ showTip }: Props) => {
/>
)}
{form.type === InputVarType.select && (
<Select
<Select<string>
value={(inputsFormValue?.[form.variable] ?? form.default ?? '') || null}
onValueChange={(value) => value && handleFormChange(form.variable, value)}
>
@@ -13,7 +13,7 @@ import { useStore } from '../store'
type FileFromLinkOrLocalProps = {
showFromLink?: boolean
showFromLocal?: boolean
trigger: (open: boolean) => React.ReactNode
trigger: (open: boolean) => React.ReactElement
fileConfig: FileUpload
}
const FileFromLinkOrLocal = ({
@@ -49,7 +49,7 @@ const FileFromLinkOrLocal = ({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger render={trigger(open) as React.ReactElement} />
<PopoverTrigger render={trigger(open)} />
<PopoverContent
placement="top"
sideOffset={4}
@@ -277,7 +277,7 @@ const BaseField = ({
)}
{formItemType === FormTypeEnum.select &&
(multiple ? (
<Select
<Select<string, true>
multiple
items={memorizedOptions}
value={Array.isArray(value) ? value : []}
@@ -289,9 +289,9 @@ const BaseField = ({
aria-label={translatedLabel || field.name}
className="px-2"
>
<SelectValue placeholder={translatedPlaceholder}>
{(selectedValue: string[]) =>
selectedValue.length
<SelectValue<string, true> placeholder={translatedPlaceholder}>
{(selectedValue) =>
selectedValue?.length
? t(($) => $['dynamicSelect.selected'], {
ns: 'common',
count: selectedValue.length,
@@ -310,7 +310,7 @@ const BaseField = ({
</SelectContent>
</Select>
) : (
<Select
<Select<string>
items={memorizedOptions}
value={getSingleSelectValue(value, memorizedOptions)}
disabled={disabled}
@@ -324,7 +324,7 @@ const BaseField = ({
aria-label={translatedLabel || field.name}
className="px-2"
>
<SelectValue placeholder={translatedPlaceholder}>
<SelectValue<string> placeholder={translatedPlaceholder}>
{(nextValue) =>
getSingleSelectLabel(nextValue, memorizedOptions, translatedPlaceholder)
}
@@ -352,7 +352,7 @@ const BaseField = ({
)}
{formItemType === FormTypeEnum.dynamicSelect &&
(multiple ? (
<Select
<Select<string, true>
multiple
items={dynamicOptions}
value={Array.isArray(value) ? value : []}
@@ -364,9 +364,9 @@ const BaseField = ({
aria-label={translatedLabel || field.name}
className="px-2"
>
<SelectValue placeholder={dynamicPlaceholder}>
{(selectedValue: string[]) =>
selectedValue.length
<SelectValue<string, true> placeholder={dynamicPlaceholder}>
{(selectedValue) =>
selectedValue?.length
? t(($) => $['dynamicSelect.selected'], {
ns: 'common',
count: selectedValue.length,
@@ -395,7 +395,7 @@ const BaseField = ({
</SelectContent>
</Select>
) : (
<Select
<Select<string>
items={dynamicOptions}
value={getSingleSelectValue(value, dynamicOptions)}
disabled={disabled || isDynamicOptionsLoading}
@@ -409,7 +409,7 @@ const BaseField = ({
aria-label={translatedLabel || field.name}
className="px-2"
>
<SelectValue placeholder={dynamicPlaceholder}>
<SelectValue<string> placeholder={dynamicPlaceholder}>
{(nextValue) =>
getSingleSelectLabel(nextValue, dynamicOptions, dynamicPlaceholder)
}
@@ -71,8 +71,8 @@ const SelectField = ({
}}
>
<SelectTrigger id={field.name} className="px-2">
<SelectValue placeholder={placeholderText}>
{(nextValue: string | null) => getDisplayLabel(nextValue, options, placeholderText)}
<SelectValue<string> placeholder={placeholderText}>
{(nextValue) => getDisplayLabel(nextValue, options, placeholderText)}
</SelectValue>
</SelectTrigger>
<SelectContent popupClassName={cn('bg-components-panel-bg-blur', popupProps?.className)}>
@@ -1,3 +1,4 @@
import type { ExtraProps } from 'streamdown'
import type { ChatContextValue } from '@/app/components/base/chat/chat/context'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@@ -13,14 +14,15 @@ vi.mock('../utils', () => ({
isValidUrl: (u: string) => isValidUrlSpy(u),
})) // test subject
type TestNode = {
properties?: {
dataVariant?: string
dataMessage?: string
dataLink?: string
dataSize?: string
type TestNode = NonNullable<ExtraProps['node']>
function createButtonNode(properties: TestNode['properties'], label?: string): TestNode {
return {
type: 'element',
tagName: 'button',
properties,
children: label === undefined ? [] : [{ type: 'text', value: label }],
}
children?: Array<{ value?: string }>
}
describe('MarkdownButton (integration)', () => {
@@ -39,13 +41,13 @@ describe('MarkdownButton (integration)', () => {
return render(
<ChatContextProvider {...ctx}>
<MarkdownButton node={node as unknown as Record<string, unknown>} />
<MarkdownButton node={node} />
</ChatContextProvider>,
)
}
it('renders button text from node children', () => {
const node: TestNode = { children: [{ value: 'Click me' }], properties: {} }
const node = createButtonNode({}, 'Click me')
renderWithCtx(node)
expect(screen.getByRole('button')).toHaveTextContent('Click me')
})
@@ -55,10 +57,7 @@ describe('MarkdownButton (integration)', () => {
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
const user = userEvent.setup()
const node: TestNode = {
properties: { dataLink: 'https://example.com' },
children: [{ value: 'Go' }],
}
const node = createButtonNode({ dataLink: 'https://example.com' }, 'Go')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
@@ -74,10 +73,7 @@ describe('MarkdownButton (integration)', () => {
isValidUrlSpy.mockReturnValue(false)
const user = userEvent.setup()
const node: TestNode = {
properties: { dataLink: 'not-a-url', dataMessage: 'hello!' },
children: [{ value: 'Send' }],
}
const node = createButtonNode({ dataLink: 'not-a-url', dataMessage: 'hello!' }, 'Send')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
@@ -91,7 +87,7 @@ describe('MarkdownButton (integration)', () => {
isValidUrlSpy.mockReturnValue(false)
const user = userEvent.setup()
const node: TestNode = { properties: {}, children: [{ value: 'Empty' }] }
const node = createButtonNode({}, 'Empty')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
@@ -101,10 +97,7 @@ describe('MarkdownButton (integration)', () => {
it('calls onSend when message present and no link', async () => {
const user = userEvent.setup()
const node: TestNode = {
properties: { dataMessage: 'msg-only' },
children: [{ value: 'Msg' }],
}
const node = createButtonNode({ dataMessage: 'msg-only' }, 'Msg')
renderWithCtx(node)
await user.click(screen.getByRole('button'))
@@ -113,9 +106,17 @@ describe('MarkdownButton (integration)', () => {
})
it('falls back to empty label when first child value is missing', () => {
const node: TestNode = { properties: {}, children: [{}] }
const node = createButtonNode({})
renderWithCtx(node)
expect(screen.getByRole('button')).toHaveTextContent('')
})
it('maps the legacy warning variant to a destructive primary button', () => {
renderWithCtx(createButtonNode({ dataVariant: 'warning' }, 'Delete'))
expect(screen.getByRole('button', { name: 'Delete' })).toHaveClass(
'bg-components-button-destructive-primary-bg',
)
})
})
@@ -704,6 +704,18 @@ describe('MarkdownForm', () => {
expect(button)!.toBeInTheDocument()
})
it('should map the legacy warning variant to a destructive primary button', () => {
const node = createRootNode([
createElementNode('button', { dataVariant: 'warning' }, [createTextNode('Delete')]),
])
render(<MarkdownForm node={node} />)
expect(screen.getByRole('button', { name: 'Delete' })).toHaveClass(
'bg-components-button-destructive-primary-bg',
)
})
it('should ignore invalid variant and size values', () => {
const node = createRootNode([
createElementNode('button', { dataVariant: 'danger', dataSize: 'xl' }, [
@@ -0,0 +1,46 @@
import type { ButtonProps } from '@langgenius/dify-ui/button'
type MarkdownButtonAppearance = Pick<ButtonProps, 'size' | 'tone' | 'variant'>
const MARKDOWN_BUTTON_APPEARANCES = {
primary: { variant: 'primary' },
warning: { tone: 'destructive', variant: 'primary' },
secondary: { variant: 'secondary' },
'secondary-accent': { variant: 'secondary-accent' },
ghost: { variant: 'ghost' },
'ghost-accent': { variant: 'ghost-accent' },
tertiary: { variant: 'tertiary' },
} satisfies Record<string, MarkdownButtonAppearance>
const VALID_BUTTON_SIZES: ReadonlySet<string> = new Set([
'small',
'medium',
'large',
] satisfies Array<NonNullable<ButtonProps['size']>>)
function isMarkdownButtonVariant(value: string): value is keyof typeof MARKDOWN_BUTTON_APPEARANCES {
return Object.hasOwn(MARKDOWN_BUTTON_APPEARANCES, value)
}
function isButtonSize(value: string): value is NonNullable<ButtonProps['size']> {
return VALID_BUTTON_SIZES.has(value)
}
function normalizeAttribute(value: unknown) {
return value == null ? '' : String(value)
}
function getMarkdownButtonAppearance(
dataVariant: unknown,
dataSize: unknown,
): MarkdownButtonAppearance {
const variant = normalizeAttribute(dataVariant)
const size = normalizeAttribute(dataSize)
return {
...(isMarkdownButtonVariant(variant) ? MARKDOWN_BUTTON_APPEARANCES[variant] : undefined),
size: isButtonSize(size) ? size : undefined,
}
}
export { getMarkdownButtonAppearance }
@@ -1,19 +1,31 @@
import type { ComponentProps } from 'react'
import type { ExtraProps } from 'streamdown'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useChatContext } from '@/app/components/base/chat/chat/context'
import { getMarkdownButtonAppearance } from './button-appearance'
import { isValidUrl } from './utils'
const MarkdownButton = ({ node }: any) => {
type MarkdownButtonProps = ComponentProps<'button'> & ExtraProps
function getStringProperty(value: unknown) {
return typeof value === 'string' ? value : undefined
}
const MarkdownButton = ({ node }: MarkdownButtonProps) => {
const { onSend } = useChatContext()
const variant = node.properties.dataVariant
const message = node.properties.dataMessage
const link = node.properties.dataLink
const size = node.properties.dataSize
const appearance = getMarkdownButtonAppearance(
node?.properties.dataVariant,
node?.properties.dataSize,
)
const message = getStringProperty(node?.properties.dataMessage)
const link = getStringProperty(node?.properties.dataLink)
const firstChild = node?.children[0]
const label = firstChild?.type === 'text' ? firstChild.value : ''
return (
<Button
variant={variant}
size={size}
{...appearance}
className={cn('h-auto! min-h-8 px-3! whitespace-normal select-none')}
onClick={() => {
if (link && isValidUrl(link)) {
@@ -24,7 +36,7 @@ const MarkdownButton = ({ node }: any) => {
onSend?.(message)
}}
>
<span className="text-[13px]">{node.children[0]?.value || ''}</span>
<span className="text-[13px]">{label}</span>
</Button>
)
}
@@ -1,4 +1,3 @@
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { Dayjs } from 'dayjs'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
@@ -22,6 +21,7 @@ import {
toDayjs,
} from '@/app/components/base/date-and-time-picker/utils/dayjs'
import Input from '@/app/components/base/input'
import { getMarkdownButtonAppearance } from './button-appearance'
const DATA_FORMAT = {
TEXT: 'text',
@@ -72,17 +72,6 @@ function isSafeName(name: unknown): name is string {
)
}
const VALID_BUTTON_VARIANTS = new Set<string>([
'primary',
'warning',
'secondary',
'secondary-accent',
'ghost',
'ghost-accent',
'tertiary',
])
const VALID_BUTTON_SIZES = new Set<string>(['small', 'medium', 'large'])
type HastText = {
type: 'text'
value: string
@@ -391,21 +380,16 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
}
if (child.tagName === SUPPORTED_TAGS.BUTTON) {
const rawVariant = str(child.properties.dataVariant)
const rawSize = str(child.properties.dataSize)
const variant = VALID_BUTTON_VARIANTS.has(rawVariant)
? (rawVariant as ButtonProps['variant'])
: undefined
const size = VALID_BUTTON_SIZES.has(rawSize)
? (rawSize as ButtonProps['size'])
: undefined
const appearance = getMarkdownButtonAppearance(
child.properties.dataVariant,
child.properties.dataSize,
)
return (
<Button
variant={variant}
size={size}
className="mt-4"
key={key}
{...appearance}
className="mt-4"
disabled={isSubmitting}
onClick={onSubmit}
>
+8 -12
View File
@@ -22,14 +22,7 @@ const isTheme = (value: string): value is Theme => {
export default function ThemeSelector() {
const { t } = useTranslation()
const { theme, setTheme } = useTheme()
const handleThemeChange = (newTheme: Theme) => {
setTheme(newTheme)
}
const handleThemeValueChange = (value: string) => {
if (isTheme(value)) handleThemeChange(value)
}
const currentTheme: Theme = theme && isTheme(theme) ? theme : 'system'
const getCurrentIcon = () => {
switch (theme) {
@@ -55,22 +48,25 @@ export default function ThemeSelector() {
{getCurrentIcon()}
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-end" sideOffset={6} popupClassName="w-[144px]">
<DropdownMenuRadioGroup value={theme || 'system'} onValueChange={handleThemeValueChange}>
<DropdownMenuRadioItem value="light" closeOnClick>
<DropdownMenuRadioGroup<Theme>
value={currentTheme}
onValueChange={(nextTheme) => setTheme(nextTheme)}
>
<DropdownMenuRadioItem<Theme> value="light" closeOnClick>
<span className="i-ri-sun-line size-4 text-text-tertiary" />
<span className="grow px-1 system-md-regular">
{t(($) => $['theme.light'], { ns: 'common' })}
</span>
<DropdownMenuRadioItemIndicator data-testid="light-icon" />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark" closeOnClick>
<DropdownMenuRadioItem<Theme> value="dark" closeOnClick>
<span className="i-ri-moon-line size-4 text-text-tertiary" />
<span className="grow px-1 system-md-regular">
{t(($) => $['theme.dark'], { ns: 'common' })}
</span>
<DropdownMenuRadioItemIndicator data-testid="dark-icon" />
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system" closeOnClick>
<DropdownMenuRadioItem<Theme> value="system" closeOnClick>
<span className="i-ri-computer-line size-4 text-text-tertiary" />
<span className="grow px-1 system-md-regular">
{t(($) => $['theme.auto'], { ns: 'common' })}
@@ -20,8 +20,8 @@ function getDocumentExtension(document: SimpleDocumentDetail) {
export default function DocumentList({ className }: Props) {
return (
<ComboboxList className={cn('max-h-[calc(100vh-120px)] p-0', className)}>
{(item: SimpleDocumentDetail) => {
<ComboboxList<SimpleDocumentDetail> className={cn('max-h-[calc(100vh-120px)] p-0', className)}>
{(item) => {
const extension = getDocumentExtension(item)
return (
<ComboboxItem
@@ -148,10 +148,8 @@ export function DocumentPicker({ datasetId, value, parentMode, onChange }: Props
'ml-1 flex size-auto rounded-lg border-0 bg-transparent px-2 py-1 hover:bg-state-base-hover focus-visible:bg-state-base-hover data-popup-open:bg-state-base-hover',
)}
>
<ComboboxValue>
{(document: SimpleDocumentDetail | null) => (
<DocumentPickerTriggerValue document={document} parentMode={parentMode} />
)}
<ComboboxValue<SimpleDocumentDetail>>
{(document) => <DocumentPickerTriggerValue document={document} parentMode={parentMode} />}
</ComboboxValue>
</ComboboxTrigger>
<ComboboxContent
@@ -27,7 +27,7 @@ export function CreateMetadataModal({
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger render={triggerElement as React.ReactElement} />
<PopoverTrigger render={triggerElement} />
<PopoverContent
placement="left-start"
sideOffset={popupLeft}
@@ -202,8 +202,8 @@ function MetadataPickerSelectPanel({
{query && <ComboboxClear aria-label={t(($) => $['operation.clear'], { ns: 'common' })} />}
</ComboboxInputGroup>
</div>
<ComboboxList>
{(metadata: MetadataItem) => <MetadataOption key={metadata.id} metadata={metadata} />}
<ComboboxList<MetadataItem>>
{(metadata) => <MetadataOption key={metadata.id} metadata={metadata} />}
</ComboboxList>
<ComboboxEmpty>{t(($) => $.noData, { ns: 'common' })}</ComboboxEmpty>
<ComboboxSeparator />
+4 -4
View File
@@ -477,8 +477,8 @@ function GotoAnythingDialog() {
? t(($) => $['gotoAnything.groups.commands'], { ns: 'app' })
: t(($) => $['gotoAnything.selectSearchType'], { ns: 'app' })}
</AutocompleteGroupLabel>
<AutocompleteCollection>
{(option: CommandOption) => (
<AutocompleteCollection<CommandOption>>
{(option) => (
<AutocompleteItem
key={option.shortcut}
value={option}
@@ -542,8 +542,8 @@ function GotoAnythingDialog() {
{ ns: 'app' },
)}
</AutocompleteGroupLabel>
<AutocompleteCollection>
{(result: SearchResult) => (
<AutocompleteCollection<SearchResult>>
{(result) => (
<AutocompleteItem
key={`${result.type}-${result.id}`}
value={result}
@@ -57,6 +57,8 @@ function MainNavRadioItemContent({ iconClassName, label }: MainNavRadioItemConte
function AppearanceSubmenu() {
const { t } = useTranslation()
const { theme, setTheme } = useTheme()
const currentTheme: Theme =
theme === 'light' || theme === 'dark' || theme === 'system' ? theme : 'system'
return (
<DropdownMenuSub>
@@ -71,23 +73,35 @@ function AppearanceSubmenu() {
sideOffset={6}
popupClassName="w-[139px] max-h-[360px] bg-components-panel-bg-blur p-1 backdrop-blur-[5px]"
>
<DropdownMenuRadioGroup
value={theme || 'system'}
onValueChange={(value) => setTheme(value as Theme)}
<DropdownMenuRadioGroup<Theme>
value={currentTheme}
onValueChange={(nextTheme) => setTheme(nextTheme)}
>
<DropdownMenuRadioItem value="light" closeOnClick className="mx-0 h-8 gap-1 px-2 py-1">
<DropdownMenuRadioItem<Theme>
value="light"
closeOnClick
className="mx-0 h-8 gap-1 px-2 py-1"
>
<MainNavRadioItemContent
iconClassName="i-ri-sun-line"
label={t(($) => $['account.appearanceLight'], { ns: 'common' })}
/>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark" closeOnClick className="mx-0 h-8 gap-1 px-2 py-1">
<DropdownMenuRadioItem<Theme>
value="dark"
closeOnClick
className="mx-0 h-8 gap-1 px-2 py-1"
>
<MainNavRadioItemContent
iconClassName="i-ri-moon-line"
label={t(($) => $['account.appearanceDark'], { ns: 'common' })}
/>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system" closeOnClick className="mx-0 h-8 gap-1 px-2 py-1">
<DropdownMenuRadioItem<Theme>
value="system"
closeOnClick
className="mx-0 h-8 gap-1 px-2 py-1"
>
<MainNavRadioItemContent
iconClassName="i-ri-computer-line"
label={t(($) => $['account.appearanceSystem'], { ns: 'common' })}
@@ -1,5 +1,6 @@
import type { ReactElement, ReactNode } from 'react'
import type { DefaultModel, Model, ModelItem } from '../../declarations'
import type { ModelSelectorPreviewPayload } from '../popup-item'
import { Combobox } from '@langgenius/dify-ui/combobox'
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import { fireEvent, screen } from '@testing-library/react'
@@ -138,7 +139,7 @@ const makeProvider = (overrides: Record<string, unknown> = {}) => ({
})
const previewCardProps = () => ({
previewCardHandle: createPreviewCardHandle(),
previewCardHandle: createPreviewCardHandle<ModelSelectorPreviewPayload>(),
onPreviewCardClose: vi.fn(),
})
@@ -206,7 +207,7 @@ describe('PopupItem', () => {
const onPreviewCardClose = vi.fn()
renderWithCombobox(
<PopupItem
previewCardHandle={createPreviewCardHandle()}
previewCardHandle={createPreviewCardHandle<ModelSelectorPreviewPayload>()}
onPreviewCardClose={onPreviewCardClose}
model={makeModel()}
onHide={vi.fn()}
@@ -1,4 +1,4 @@
import type { ComponentProps } from 'react'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { DefaultModel, Model, ModelItem } from '../declarations'
import type { ModelSelectorModelPredicate } from './types'
import { cn } from '@langgenius/dify-ui/cn'
@@ -26,14 +26,12 @@ export type ModelSelectorPreviewPayload = {
modelItem: ModelItem
}
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
type PopupItemProps = {
defaultModel?: DefaultModel
model: Model
modelPredicate?: ModelSelectorModelPredicate
modelSuggestionPredicate?: ModelSelectorModelPredicate
previewCardHandle: PreviewCardHandle
previewCardHandle: PreviewCardHandle<ModelSelectorPreviewPayload>
onPreviewCardClose: () => void
onHide: () => void
}
@@ -325,7 +325,7 @@ function Popup({
<ModelSelectorPreviewCard
capabilitiesLabel={t(($) => $['model.capabilities'], { ns: 'common' })}
language={language}
payload={payload as ModelSelectorPreviewPayload | undefined}
payload={payload}
/>
)}
</PreviewCard>
@@ -89,15 +89,15 @@ function WorkspaceSwitchControls({
sideOffset={4}
popupClassName="w-40 bg-components-panel-bg-blur! p-1! backdrop-blur-[5px]"
>
<DropdownMenuRadioGroup
<DropdownMenuRadioGroup<WorkspaceSort>
value={sort}
onValueChange={(value) => {
onSortChange(value as WorkspaceSort)
onSortChange(value)
setSortMenuOpen(false)
}}
>
{sortOptions.map((option) => (
<DropdownMenuRadioItem
<DropdownMenuRadioItem<WorkspaceSort>
key={option.value}
value={option.value}
className="mx-0 h-8 gap-1 px-2 py-1"
@@ -168,8 +168,8 @@ export function AppPicker({
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-1">
{isLoading && <ComboboxStatus>{t(($) => $.loading, { ns: 'common' })}</ComboboxStatus>}
<ComboboxList className="max-h-none p-0">
{(app: App) => <AppPickerOption key={app.id} app={app} />}
<ComboboxList<App> className="max-h-none p-0">
{(app) => <AppPickerOption key={app.id} app={app} />}
</ComboboxList>
<ComboboxEmpty>{t(($) => $.noData, { ns: 'common' })}</ComboboxEmpty>
{hasMore && (
@@ -1,6 +1,7 @@
'use client'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { Placement } from '@langgenius/dify-ui/popover'
import type { ComponentProps, ReactNode } from 'react'
import type { ReactNode } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
@@ -16,7 +17,7 @@ type DebugInfoProps = {
popupPlacement?: Placement
triggerClassName?: string
triggerContent?: ReactNode
triggerVariant?: ComponentProps<typeof Button>['variant']
triggerVariant?: ButtonProps['variant']
}
function DebugInfo({
@@ -21,11 +21,6 @@ type SnippetPublishStatusFilterProps = {
const chipClassName =
'flex h-8 items-center rounded-lg border-[0.5px] px-2 text-[13px] leading-4 outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-state-accent-solid'
const snippetPublishStatusValues: SnippetPublishStatus[] = ['all', 'published', 'draft']
const isSnippetPublishStatus = (value: string): value is SnippetPublishStatus => {
return snippetPublishStatusValues.includes(value as SnippetPublishStatus)
}
const SnippetPublishStatusFilter = ({ value, onChange }: SnippetPublishStatusFilterProps) => {
const { t } = useTranslation()
@@ -64,12 +59,16 @@ const SnippetPublishStatusFilter = ({ value, onChange }: SnippetPublishStatusFil
<span aria-hidden className="i-ri-arrow-down-s-line h-4 w-4 shrink-0 text-text-tertiary" />
</DropdownMenuTrigger>
<DropdownMenuContent placement="bottom-start" popupClassName="w-[220px]">
<DropdownMenuRadioGroup
<DropdownMenuRadioGroup<SnippetPublishStatus>
value={value}
onValueChange={(nextValue) => isSnippetPublishStatus(nextValue) && onChange(nextValue)}
onValueChange={(nextValue) => onChange(nextValue)}
>
{options.map((option) => (
<DropdownMenuRadioItem key={option.value} value={option.value} closeOnClick>
<DropdownMenuRadioItem<SnippetPublishStatus>
key={option.value}
value={option.value}
closeOnClick
>
<span>{option.text}</span>
<DropdownMenuRadioItemIndicator />
</DropdownMenuRadioItem>
@@ -1,5 +1,5 @@
import type { DropdownMenuContent } from '@langgenius/dify-ui/dropdown-menu'
import type { ComponentProps, SyntheticEvent } from 'react'
import type { DropdownMenuContentProps } from '@langgenius/dify-ui/dropdown-menu'
import type { SyntheticEvent } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useCallback, useState } from 'react'
@@ -8,7 +8,6 @@ const STEP_BY_STEP_TOUR_MENU_POPUP_NO_MOTION_CLASS_NAME =
'transition-none data-starting-style:scale-100 data-starting-style:opacity-100 data-ending-style:scale-100 data-ending-style:opacity-100'
const STEP_BY_STEP_TOUR_MENU_PRESENTATION_CLASS_NAME = 'pointer-events-none cursor-default'
type DropdownMenuContentProps = ComponentProps<typeof DropdownMenuContent>
type DropdownMenuPositionerProps = DropdownMenuContentProps['positionerProps']
type DropdownMenuPopupProps = DropdownMenuContentProps['popupProps']
@@ -194,7 +194,7 @@ const Blocks = ({
)}
{!isEmpty && BLOCK_CLASSIFICATIONS.map(renderGroup)}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => <BlockPreviewCard payload={payload as BlockPreviewPayload | undefined} />}
{({ payload }) => <BlockPreviewCard payload={payload} />}
</PreviewCard>
</div>
)
@@ -1,4 +1,5 @@
'use client'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { TFunction } from 'i18next'
import type { ToolWithProvider } from '../types'
import type { ToolDefaultValue, ToolValue } from './types'
@@ -227,9 +228,7 @@ const FeaturedTools = ({
)}
</CollapsiblePanel>
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<FeaturedToolPreviewCard payload={payload as FeaturedToolPreviewPayload | undefined} />
)}
{({ payload }) => <FeaturedToolPreviewCard payload={payload} />}
</PreviewCard>
</Collapsible>
)
@@ -238,7 +237,7 @@ const FeaturedTools = ({
type FeaturedToolUninstalledItemProps = {
plugin: Plugin
language: Locale
previewCardHandle: ReturnType<typeof createPreviewCardHandle<FeaturedToolPreviewPayload>>
previewCardHandle: PreviewCardHandle<FeaturedToolPreviewPayload>
onInstallSuccess?: () => Promise<void> | void
t: TFunction
}
@@ -1,4 +1,5 @@
'use client'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { TFunction } from 'i18next'
import type { TriggerPluginActionPreviewPayload } from './trigger-plugin/action-item'
import type { TriggerDefaultValue, TriggerWithProvider } from './types'
@@ -227,18 +228,10 @@ const FeaturedTriggers = ({
)}
</CollapsiblePanel>
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<FeaturedTriggerPreviewCard
payload={payload as FeaturedTriggerPreviewPayload | undefined}
/>
)}
{({ payload }) => <FeaturedTriggerPreviewCard payload={payload} />}
</PreviewCard>
<PreviewCard handle={triggerActionPreviewCardHandle}>
{({ payload }) => (
<TriggerPluginActionPreviewCard
payload={payload as TriggerPluginActionPreviewPayload | undefined}
/>
)}
{({ payload }) => <TriggerPluginActionPreviewCard payload={payload} />}
</PreviewCard>
</Collapsible>
)
@@ -247,7 +240,7 @@ const FeaturedTriggers = ({
type FeaturedTriggerUninstalledItemProps = {
plugin: Plugin
language: Locale
previewCardHandle: ReturnType<typeof createPreviewCardHandle<FeaturedTriggerPreviewPayload>>
previewCardHandle: PreviewCardHandle<FeaturedTriggerPreviewPayload>
onInstallSuccess?: () => Promise<void> | void
t: TFunction
}
@@ -66,9 +66,7 @@ const List = ({ onSelect, tools, viewType, unInstalledPlugins, className }: List
/>
))}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<ToolActionPreviewCard payload={payload as ToolActionPreviewPayload | undefined} />
)}
{({ payload }) => <ToolActionPreviewCard payload={payload} />}
</PreviewCard>
{unInstalledPlugins.map((item) => {
return <UninstalledItem key={item.plugin_id} payload={item} />
@@ -196,7 +196,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }:
return (
<BlockSelectorPreviewCardContent>
<SnippetDetailCard snippet={payload as SnippetListItemData} />
<SnippetDetailCard snippet={payload} />
</BlockSelectorPreviewCardContent>
)
}}
@@ -214,9 +214,7 @@ const StartBlocks = ({
))}
</div>
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<StartBlockPreviewCard payload={payload as StartBlockPreviewPayload | undefined} t={t} />
)}
{({ payload }) => <StartBlockPreviewCard payload={payload} t={t} />}
</PreviewCard>
</div>
)
@@ -1,5 +1,6 @@
'use client'
import type { ComponentProps, FC } from 'react'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { FC } from 'react'
import type { ToolWithProvider } from '../../types'
import type { ToolDefaultValue } from '../types'
import type { Tool } from '@/app/components/tools/types'
@@ -30,23 +31,22 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
return icon
}
type Props = Readonly<{
provider: ToolWithProvider
payload: Tool
previewCardHandle: PreviewCardHandle
disabled?: boolean
isAdded?: boolean
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
}>
export type ToolActionPreviewPayload = {
providerIcon: ToolWithProvider['icon']
payload: Tool
language: ReturnType<typeof useGetLanguage>
}
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
export type ToolActionPreviewCardHandle = PreviewCardHandle
export type ToolActionPreviewCardHandle = PreviewCardHandle<ToolActionPreviewPayload>
type Props = Readonly<{
provider: ToolWithProvider
payload: Tool
previewCardHandle: ToolActionPreviewCardHandle
disabled?: boolean
isAdded?: boolean
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
}>
const ToolItem: FC<Props> = ({
provider,
@@ -91,9 +91,7 @@ const Tools = ({
/>
))}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<ToolActionPreviewCard payload={payload as ToolActionPreviewPayload | undefined} />
)}
{({ payload }) => <ToolActionPreviewCard payload={payload} />}
</PreviewCard>
</div>
)
@@ -1,5 +1,6 @@
'use client'
import type { ComponentProps, FC } from 'react'
import type { PreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import type { FC } from 'react'
import type { TriggerDefaultValue, TriggerWithProvider } from '../types'
import type { Event } from '@/app/components/tools/types'
import { cn } from '@langgenius/dify-ui/cn'
@@ -26,8 +27,8 @@ export type TriggerPluginActionPreviewPayload = {
language: ReturnType<typeof useGetLanguage>
}
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
export type TriggerPluginActionPreviewCardHandle = PreviewCardHandle
export type TriggerPluginActionPreviewCardHandle =
PreviewCardHandle<TriggerPluginActionPreviewPayload>
const TriggerPluginActionItem: FC<Props> = ({
provider,
@@ -102,11 +102,7 @@ const TriggerPluginList = ({
/>
))}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<TriggerPluginActionPreviewCard
payload={payload as TriggerPluginActionPreviewPayload | undefined}
/>
)}
{({ payload }) => <TriggerPluginActionPreviewCard payload={payload} />}
</PreviewCard>
</div>
)
@@ -190,7 +190,7 @@ const FormItem: FC<Props> = ({
)}
{type === InputVarType.select && (
<Select
<Select<string>
value={value || payload.default || null}
onValueChange={(nextValue) => {
if (!nextValue) return
@@ -1,8 +1,13 @@
import type { ComponentProps, ReactNode } from 'react'
import type {
CollapsiblePanelProps,
CollapsibleProps,
CollapsibleTriggerProps,
} from '@langgenius/dify-ui/collapsible'
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
type CollapseProps = Omit<ComponentProps<typeof Collapsible>, 'open' | 'onOpenChange'> & {
type CollapseProps = Omit<CollapsibleProps, 'open' | 'onOpenChange'> & {
collapsed?: boolean
onCollapse?: (collapsed: boolean) => void
}
@@ -33,9 +38,7 @@ export function CollapseActions({ children }: CollapseActionsProps) {
return <div className="ml-auto shrink-0">{children}</div>
}
type CollapseTriggerProps = ComponentProps<typeof CollapsibleTrigger>
export function CollapseTrigger({ className, ...props }: CollapseTriggerProps) {
export function CollapseTrigger({ className, ...props }: CollapsibleTriggerProps) {
return (
<CollapsibleTrigger
className={cn(
@@ -71,9 +74,7 @@ export function CollapseIndicator() {
)
}
type CollapseContentProps = ComponentProps<typeof CollapsiblePanel>
export function CollapseContent({ className, ...props }: CollapseContentProps) {
export function CollapseContent({ className, ...props }: CollapsiblePanelProps) {
return <CollapsiblePanel className={cn(className)} {...props} />
}
@@ -1,4 +1,5 @@
import type { ComponentProps, MouseEventHandler } from 'react'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { MouseEventHandler } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useState } from 'react'
@@ -8,7 +9,7 @@ import useWorkspacePluginInstallPermission from '@/app/components/plugins/instal
import { TaskStatus } from '@/app/components/plugins/types'
import { useCheckInstalled, useInstallPackageFromMarketPlace } from '@/service/use-plugins'
type InstallPluginButtonProps = Omit<ComponentProps<typeof Button>, 'children' | 'loading'> & {
type InstallPluginButtonProps = Omit<ButtonProps, 'children' | 'loading'> & {
uniqueIdentifier: string
extraIdentifiers?: string[]
onSuccess?: () => void
@@ -1,5 +1,4 @@
'use client'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { FC } from 'react'
import type { FormInputItem, UserAction } from '../types'
import { Button } from '@langgenius/dify-ui/button'
@@ -94,10 +93,7 @@ const FormContentPreview: FC<FormContentPreviewProps> = ({
/>
<div className="mt-3 flex flex-wrap gap-1 py-1">
{userActions.map((action: UserAction) => (
<Button
key={action.id}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
>
<Button key={action.id} variant={getButtonStyle(action.button_style)}>
{action.title}
</Button>
))}
@@ -1,5 +1,4 @@
'use client'
import type { ButtonProps } from '@langgenius/dify-ui/button'
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
import type { UserAction } from '@/app/components/workflow/nodes/human-input/types'
import type { HumanInputFormData } from '@/types/workflow'
@@ -85,7 +84,7 @@ const FormContent = ({ nodeName, data, showBackButton, handleBack, onSubmit }: P
<Button
key={action.id}
disabled={isSubmitting || hasEmptySelectOrFileInput}
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
variant={getButtonStyle(action.button_style)}
onClick={() => submit(action.id)}
>
{action.title}
@@ -39,7 +39,7 @@ const Operator = ({
onMouseDown={(event) => {
event.preventDefault()
event.stopPropagation()
;(event as typeof event & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.()
event.preventBaseUIHandler()
setOpen((prev) => !prev)
}}
onClick={(event) => event.stopPropagation()}
@@ -120,7 +120,7 @@ export function DisplayContent(props: DisplayContentProps) {
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}
value={mdString}
onValueChange={(value) => handleTextChange?.(value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
+5 -6
View File
@@ -24,10 +24,6 @@ export default function LocaleMenu<T extends string>({
onChange,
}: LocaleMenuProps<T>) {
const selectedItem = items.find((item) => item.value === value)
const handleValueChange = (nextValue: string) => {
const nextItem = items.find((item) => item.value === nextValue)
if (nextItem) onChange?.(nextItem.value)
}
return (
<div className="w-56 text-right">
@@ -48,9 +44,12 @@ export default function LocaleMenu<T extends string>({
</div>
</div>
<DropdownMenuContent placement="bottom-end" sideOffset={8} popupClassName="w-[200px]">
<DropdownMenuRadioGroup value={value} onValueChange={handleValueChange}>
<DropdownMenuRadioGroup<T>
value={value}
onValueChange={(nextValue) => onChange?.(nextValue)}
>
{items.map((item) => (
<DropdownMenuRadioItem
<DropdownMenuRadioItem<T>
key={item.value}
value={item.value}
closeOnClick
@@ -101,12 +101,12 @@ function EnvEditorScope({
}
return (
<Select
<Select<EnvScope>
value={scope}
onValueChange={(nextValue) => {
if (!nextValue) return
onChange?.(nextValue as EnvScope)
onChange?.(nextValue)
}}
>
<SelectTrigger
@@ -117,7 +117,7 @@ function EnvEditorScope({
</SelectTrigger>
<SelectContent placement="bottom-start" popupClassName="min-w-24">
{envScopeOptions.map((option) => (
<SelectItem key={option} value={option} className="h-7 system-xs-regular">
<SelectItem<EnvScope> key={option} value={option} className="h-7 system-xs-regular">
<SelectItemText>{t(($) => $[scopeLabelKeys[option]])}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
@@ -31,7 +31,7 @@ const imageFileExtensions = new Set([
'svg',
'webp',
])
const driveFileIconTypes = new Set<FileTreeIconType>([
const driveFileIconTypes: ReadonlySet<string> = new Set([
'archive',
'code',
'database',
@@ -43,7 +43,11 @@ const driveFileIconTypes = new Set<FileTreeIconType>([
'pdf',
'table',
'text',
])
] satisfies FileTreeIconType[])
function isFileTreeIconType(value: string): value is FileTreeIconType {
return driveFileIconTypes.has(value)
}
function getFileExtension(fileName: string) {
return fileName.split('.').pop()?.toLowerCase() ?? ''
@@ -77,8 +81,7 @@ export function getDriveFileIconType({
if (normalizedFileKind === 'directory') return 'folder'
if (normalizedFileKind && driveFileIconTypes.has(normalizedFileKind as FileTreeIconType))
return normalizedFileKind as FileTreeIconType
if (normalizedFileKind && isFileTreeIconType(normalizedFileKind)) return normalizedFileKind
return getFileIconType(fileName, mimeType)
}
@@ -73,10 +73,11 @@ export function AgentLogSourcePicker({
aria-label={t(($) => $['agentDetail.logs.filters.source.label'])}
className="mt-0 w-fit max-w-full min-w-22"
>
<ComboboxValue placeholder={t(($) => $['agentDetail.logs.filters.source.all'])}>
{(selectedValue: AgentLogSourceResponse[]) => {
if (selectedValue.length === 0)
return t(($) => $['agentDetail.logs.filters.source.all'])
<ComboboxValue<AgentLogSourceResponse, true>
placeholder={t(($) => $['agentDetail.logs.filters.source.all'])}
>
{(selectedValue) => {
if (!selectedValue?.length) return t(($) => $['agentDetail.logs.filters.source.all'])
if (selectedValue.length === 1) return selectedValue[0]!.app_name
return tCommon(($) => $['dynamicSelect.selected'], { count: selectedValue.length })
}}
@@ -117,8 +118,8 @@ export function AgentLogSourcePicker({
<ComboboxGroupLabel className="px-1 pt-2 pb-1">
{getSourceGroupLabel(group, t)}
</ComboboxGroupLabel>
<ComboboxCollection>
{(source: AgentLogSourceResponse) => (
<ComboboxCollection<AgentLogSourceResponse>>
{(source) => (
<ComboboxItem
key={source.id}
value={source}
@@ -220,8 +220,8 @@ export function SourceAppPicker({
{(sourceAppsIsLoading || sourceAppsIsFetchingNextPage) && apps.length === 0 && (
<SourceAppPickerSkeleton />
)}
<ComboboxList className="max-h-none p-0">
{(app: App) => <SourceAppOption key={app.id} app={app} />}
<ComboboxList<App> className="max-h-none p-0">
{(app) => <SourceAppOption key={app.id} app={app} />}
</ComboboxList>
{!(sourceAppsIsLoading || sourceAppsIsFetchingNextPage) && (
<ComboboxEmpty>{t(($) => $['createModal.appSearchEmpty'])}</ComboboxEmpty>
@@ -1,6 +1,6 @@
'use client'
import type { ComponentProps } from 'react'
import type { DropdownMenuContentProps } from '@langgenius/dify-ui/dropdown-menu'
import type { DeploymentActionAppInstance } from './types'
import { cn } from '@langgenius/dify-ui/cn'
import {
@@ -32,8 +32,8 @@ type DeploymentActionsMenuProps = {
appInstance: DeploymentActionAppInstance
className?: string
triggerClassName?: string
placement: ComponentProps<typeof DropdownMenuContent>['placement']
sideOffset?: ComponentProps<typeof DropdownMenuContent>['sideOffset']
placement: DropdownMenuContentProps['placement']
sideOffset?: DropdownMenuContentProps['sideOffset']
}
function DeploymentActionsMenuContent({
@@ -176,8 +176,8 @@ export function AccessSubjectAddButton({
)}
{hasResults ? (
<>
<ComboboxList className="max-h-none p-1">
{(subject: Subject) => (
<ComboboxList<Subject> className="max-h-none p-1">
{(subject) => (
<SubjectItem
key={getSubjectValue(subject)}
subject={subject}
@@ -70,8 +70,8 @@ export const TagSearchContent = ({
)}
</ComboboxInputGroup>
</div>
<ComboboxList className="max-h-58">
{(tag: TagComboboxItem) => {
<ComboboxList<TagComboboxItem> className="max-h-58">
{(tag) => {
if (isCreateTagOption(tag) && canManageTags) {
return (
<Fragment key={tag.id}>
@@ -1,6 +1,5 @@
import type { TagResponse as Tag, TagType } from '@dify/contracts/api/console/tags/types.gen'
import type { ComboboxProps } from '@langgenius/dify-ui/combobox'
import type { ComponentProps } from 'react'
import type { ComboboxContentProps, ComboboxProps } from '@langgenius/dify-ui/combobox'
import type { TagComboboxItem } from './tag-combobox-item'
import { cn } from '@langgenius/dify-ui/cn'
import { Combobox, ComboboxContent, ComboboxTrigger } from '@langgenius/dify-ui/combobox'
@@ -45,7 +44,7 @@ type TagSelectorRootProps = Omit<
| 'children'
>
type TagSelectorContentProps = Pick<
ComponentProps<typeof ComboboxContent>,
ComboboxContentProps,
| 'placement'
| 'sideOffset'
| 'alignOffset'