mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
refactor(web): align Base UI composition contracts (#39820)
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import type {
|
||||
AgentLogSourceGroupResponse,
|
||||
AgentLogSourceResponse,
|
||||
} from '@dify/contracts/api/console/agent/types.gen'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { AgentLogSourcePicker } from '../components/source-picker'
|
||||
|
||||
const sources = {
|
||||
webapp: {
|
||||
app_id: 'webapp-app-id',
|
||||
app_name: 'Book Translation',
|
||||
id: 'webapp:webapp-app-id',
|
||||
type: 'webapp',
|
||||
},
|
||||
workflow: {
|
||||
app_id: 'workflow-app-id',
|
||||
app_name: 'SVG Logo Design',
|
||||
id: 'workflow:workflow-app-id:workflow-id:v3:agent-node-id',
|
||||
node_id: 'agent-node-id',
|
||||
type: 'workflow',
|
||||
workflow_id: 'workflow-id',
|
||||
workflow_version: 'v3',
|
||||
},
|
||||
} satisfies Record<string, AgentLogSourceResponse>
|
||||
|
||||
const groups: AgentLogSourceGroupResponse[] = [
|
||||
{
|
||||
label: 'Webapp',
|
||||
type: 'webapp',
|
||||
sources: [sources.webapp],
|
||||
},
|
||||
{
|
||||
label: 'Workflow',
|
||||
type: 'workflow',
|
||||
sources: [sources.workflow],
|
||||
},
|
||||
]
|
||||
|
||||
describe('AgentLogSourcePicker', () => {
|
||||
it('should filter sources across groups and only show empty when no source matches', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<AgentLogSourcePicker
|
||||
value={[]}
|
||||
groups={groups}
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
onRetry={vi.fn()}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('combobox', {
|
||||
name: 'agentV2.agentDetail.logs.filters.source.label',
|
||||
}),
|
||||
)
|
||||
const searchInput = screen.getByRole('combobox', {
|
||||
name: 'agentV2.agentDetail.logs.filters.source.searchLabel',
|
||||
})
|
||||
await user.type(searchInput, 'Book')
|
||||
|
||||
expect(screen.getByRole('option', { name: /Book Translation/ })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('option', { name: /SVG Logo Design/ })).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('agentV2.agentDetail.logs.filters.source.empty'),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
await user.clear(searchInput)
|
||||
await user.type(searchInput, 'Missing source')
|
||||
|
||||
expect(screen.queryByRole('option')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('agentV2.agentDetail.logs.filters.source.empty')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
} from '@langgenius/dify-ui/combobox'
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LogSourceIcon } from './source-icon'
|
||||
|
||||
@@ -35,6 +35,10 @@ const getSourceGroupLabel = (group: AgentLogSourceGroupResponse, t: TFunction<'a
|
||||
|
||||
const getSourceLabel = (source: AgentLogSourceResponse) => source.app_name
|
||||
|
||||
type AgentLogSourceComboboxGroup = Omit<AgentLogSourceGroupResponse, 'sources'> & {
|
||||
items: AgentLogSourceResponse[]
|
||||
}
|
||||
|
||||
export function AgentLogSourcePicker({
|
||||
value,
|
||||
groups,
|
||||
@@ -53,13 +57,17 @@ export function AgentLogSourcePicker({
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const sources = groups.flatMap((group) => group.sources ?? [])
|
||||
const sourceGroups = useMemo<AgentLogSourceComboboxGroup[]>(
|
||||
() => groups.map(({ sources, ...group }) => ({ ...group, items: sources ?? [] })),
|
||||
[groups],
|
||||
)
|
||||
const sources = sourceGroups.flatMap((group) => group.items)
|
||||
const selectedSources = sources.filter((source) => value.includes(source.id))
|
||||
|
||||
return (
|
||||
<Combobox<AgentLogSourceResponse, true>
|
||||
multiple
|
||||
items={groups}
|
||||
items={sourceGroups}
|
||||
value={selectedSources}
|
||||
itemToStringLabel={getSourceLabel}
|
||||
onValueChange={(nextSources) => {
|
||||
@@ -112,9 +120,9 @@ export function AgentLogSourcePicker({
|
||||
)}
|
||||
{!isLoading && !isError && (
|
||||
<>
|
||||
<ComboboxList className="max-h-69 p-2 pt-1">
|
||||
{groups.map((group) => (
|
||||
<ComboboxGroup key={group.type} items={group.sources ?? []}>
|
||||
<ComboboxList<AgentLogSourceComboboxGroup> className="max-h-69 p-2 pt-1">
|
||||
{(group) => (
|
||||
<ComboboxGroup key={group.type} items={group.items}>
|
||||
<ComboboxGroupLabel className="px-1 pt-2 pb-1">
|
||||
{getSourceGroupLabel(group, t)}
|
||||
</ComboboxGroupLabel>
|
||||
@@ -134,7 +142,7 @@ export function AgentLogSourcePicker({
|
||||
)}
|
||||
</ComboboxCollection>
|
||||
</ComboboxGroup>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
<ComboboxEmpty className="px-3 py-3 text-center system-xs-regular">
|
||||
{t(($) => $['agentDetail.logs.filters.source.empty'])}
|
||||
|
||||
@@ -77,6 +77,7 @@ function SourceMethodSection() {
|
||||
hideHeader
|
||||
>
|
||||
<RadioGroup<GuideMethod>
|
||||
aria-label={t(($) => $['createGuide.steps.method'])}
|
||||
value={method}
|
||||
onValueChange={selectMethod}
|
||||
className="flex flex-col items-stretch gap-2 sm:flex-row"
|
||||
|
||||
@@ -90,6 +90,7 @@ function TargetEnvironmentSection() {
|
||||
</div>
|
||||
{hasEnvironmentOptions ? (
|
||||
<RadioGroup<string>
|
||||
aria-label={t(($) => $['createGuide.target.environment'])}
|
||||
value={effectiveSelectedEnvironmentId}
|
||||
onValueChange={selectEnvironment}
|
||||
className="grid grid-cols-1 items-stretch gap-3 lg:grid-cols-2"
|
||||
|
||||
@@ -155,4 +155,17 @@ describe('SourceAppPicker', () => {
|
||||
screen.queryByRole('button', { name: /createModal\.loadMoreApps/ }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should restore the selected app by business identity', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderSourceAppPicker(false)
|
||||
|
||||
await user.click(screen.getByRole('combobox', { name: 'deployments.versions.sourceAppOption' }))
|
||||
|
||||
expect(screen.getByRole('option', { name: /Workflow App/ })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,6 +41,10 @@ function sourceAppSearchText(app: App) {
|
||||
return `${app.name} ${app.id}`.toLowerCase()
|
||||
}
|
||||
|
||||
function isSameApp(app: App, selectedApp: App) {
|
||||
return app.id === selectedApp.id
|
||||
}
|
||||
|
||||
function SourceAppTrigger({ app }: { app?: SourceAppPickerValue }) {
|
||||
const { t } = useTranslation('deployments')
|
||||
|
||||
@@ -144,6 +148,7 @@ export function SourceAppPicker({
|
||||
const sourceAppsIsFetching = useAtomValue(createReleaseSourceAppsIsFetchingAtom)
|
||||
const sourceAppsIsFetchingNextPage = useAtomValue(createReleaseSourceAppsIsFetchingNextPageAtom)
|
||||
const sourceAppsIsLoading = useAtomValue(createReleaseSourceAppsIsLoadingAtom)
|
||||
const selectedApp = apps.find((app) => app.id === value?.id) ?? null
|
||||
const { rootRef, sentinelRef } = useInfiniteScroll<HTMLDivElement>(
|
||||
{
|
||||
error: sourceAppsError,
|
||||
@@ -163,8 +168,10 @@ export function SourceAppPicker({
|
||||
return (
|
||||
<Combobox<App>
|
||||
items={apps}
|
||||
value={selectedApp}
|
||||
open={!disabled && isShow}
|
||||
inputValue={searchText}
|
||||
isItemEqualToValue={isSameApp}
|
||||
onOpenChange={(open) => {
|
||||
setIsShow(disabled ? false : open)
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user