fix(web): improve card grid responsiveness (#38263)

This commit is contained in:
Stephen Zhou 2026-07-01 17:58:11 +08:00 committed by GitHub
parent 76391b0383
commit befe7ec269
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 125 additions and 46 deletions

View File

@ -222,7 +222,7 @@ const Apps = ({
</div> </div>
<div <div
className={cn( className={cn(
'grid shrink-0 grid-cols-1 content-start gap-3 2k:grid-cols-6 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5', 'grid shrink-0 grid-cols-[repeat(auto-fill,minmax(296px,1fr))] content-start gap-3',
)} )}
> >
{searchFilteredList.map(app => ( {searchFilteredList.map(app => (

View File

@ -429,17 +429,19 @@ describe('List', () => {
expect(screen.getByRole('button', { name: 'common.operation.create' }))!.toBeInTheDocument() expect(screen.getByRole('button', { name: 'common.operation.create' }))!.toBeInTheDocument()
}) })
it('should render sort filter before search and the snippets link', () => { it('should render search before the right aligned actions', () => {
renderList() renderList()
const sortButton = screen.getByRole('button', { name: 'Sort by Last modified' }) const creatorsButton = screen.getByRole('button', { name: 'Creators' })
const searchInput = screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' }) const searchInput = screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' })
const sortButton = screen.getByRole('button', { name: 'Sort by Last modified' })
const snippetsLink = screen.getByRole('link', { name: 'app.studio.viewSnippets' }) const snippetsLink = screen.getByRole('link', { name: 'app.studio.viewSnippets' })
const createButton = screen.getByRole('button', { name: 'common.operation.create' }) const createButton = screen.getByRole('button', { name: 'common.operation.create' })
expect(snippetsLink).toHaveAttribute('href', '/snippets') expect(snippetsLink).toHaveAttribute('href', '/snippets')
expect(sortButton.compareDocumentPosition(searchInput) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() expect(creatorsButton.compareDocumentPosition(searchInput) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(searchInput.compareDocumentPosition(snippetsLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() expect(searchInput.compareDocumentPosition(sortButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(sortButton.compareDocumentPosition(snippetsLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
expect(snippetsLink.compareDocumentPosition(createButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() expect(snippetsLink.compareDocumentPosition(createButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
}) })
@ -450,6 +452,17 @@ describe('List', () => {
expect(screen.getByTestId('app-card-app-2'))!.toBeInTheDocument() expect(screen.getByTestId('app-card-app-2'))!.toBeInTheDocument()
}) })
it('should lay out app cards with auto-fill grid columns', () => {
renderList()
const grid = screen.getByTestId('app-card-app-1').parentElement
expect(grid).toHaveClass(
'grid',
'grid-cols-[repeat(auto-fill,minmax(296px,1fr))]',
)
})
it('should hide starred section when there are no starred apps', () => { it('should hide starred section when there are no starred apps', () => {
renderList() renderList()
@ -522,6 +535,24 @@ describe('List', () => {
expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument() expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument()
}) })
it('should lay out first empty state placeholder cards with auto-fill grid columns', () => {
mockAppData = { pages: [{ data: [], total: 0 }] }
const { container } = renderList()
const placeholderGrid = Array.from(container.querySelectorAll('.pointer-events-none'))
.find(element => element.className.includes('grid-rows-4'))
if (!placeholderGrid)
throw new Error('Expected first empty state placeholder grid to render')
expect(placeholderGrid).toHaveClass(
'grid',
'grid-cols-[repeat(auto-fill,minmax(296px,1fr))]',
'grid-rows-4',
)
expect(placeholderGrid).not.toHaveClass('grid-cols-1', 'sm:grid-cols-2', 'lg:grid-cols-3', 'xl:grid-cols-4')
})
it('should hide learn dify in first empty state when learn app is disabled', () => { it('should hide learn dify in first empty state when learn app is disabled', () => {
mockAppData = { pages: [{ data: [], total: 0 }] } mockAppData = { pages: [{ data: [], total: 0 }] }

View File

@ -1058,11 +1058,20 @@ export function AppCard({ app, onlineUsers = [], onRefresh, onOpenTagManagement
</div> </div>
</div> </div>
<div className="flex h-[26px] shrink-0 items-start px-3" /> <div className="flex h-[26px] shrink-0 items-start px-3" />
<div className="flex min-w-0 shrink-0 items-center pt-2 pr-4 pb-3 pl-4 system-xs-regular text-text-tertiary"> <div
className={cn(
'flex min-w-0 shrink-0 items-center pt-2 pb-3 pl-4 system-xs-regular text-text-tertiary',
app.access_mode ? 'pr-9' : 'pr-4',
)}
>
<div className="flex min-w-0 flex-1 items-center gap-1 whitespace-nowrap"> <div className="flex min-w-0 flex-1 items-center gap-1 whitespace-nowrap">
<div className="truncate">{app.author_name}</div> {app.author_name && (
<div className="shrink-0">·</div> <>
<div className="truncate">{editTimeText}</div> <div className="min-w-0 truncate">{app.author_name}</div>
<div className="shrink-0">·</div>
</>
)}
<div className="shrink-0">{editTimeText}</div>
</div> </div>
</div> </div>
</> </>

View File

@ -54,8 +54,8 @@ export function AppListHeaderFilters({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="flex flex-wrap items-center justify-between gap-2"> <div className="flex flex-wrap items-start justify-between gap-2">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<AppTypeFilter value={category} onChange={onCategoryChange} /> <AppTypeFilter value={category} onChange={onCategoryChange} />
<TagFilter <TagFilter
type="app" type="app"
@ -63,20 +63,21 @@ export function AppListHeaderFilters({
onChange={onTagIDsChange} onChange={onTagIDsChange}
onOpenTagManagement={onOpenTagManagement} onOpenTagManagement={onOpenTagManagement}
showLeadingIcon={false} showLeadingIcon={false}
triggerClassName="min-w-0"
/> />
<CreatorsFilter value={creatorIDs} onChange={onCreatorIDsChange} /> <CreatorsFilter value={creatorIDs} onChange={onCreatorIDsChange} />
<AppSortFilter value={sortBy} onChange={onSortByChange} />
<SearchInput <SearchInput
className="w-50" className="w-50 max-w-full"
value={keywords} value={keywords}
onValueChange={onKeywordsChange} onValueChange={onKeywordsChange}
aria-label={t('gotoAnything.actions.searchApplications', { ns: 'app' })} aria-label={t('gotoAnything.actions.searchApplications', { ns: 'app' })}
/> />
</div> </div>
<div className="flex items-center gap-2"> <div className="ml-auto flex max-w-full min-w-0 flex-wrap items-center justify-end gap-2">
<AppSortFilter value={sortBy} onChange={onSortByChange} />
<Link <Link
href="/snippets" href="/snippets"
className="flex h-8 items-center rounded-lg px-3 text-sm font-semibold text-text-secondary outline-hidden hover:bg-state-base-hover hover:text-text-primary focus-visible:ring-2 focus-visible:ring-state-accent-solid" className="flex h-8 items-center rounded-lg px-3 text-sm font-semibold whitespace-nowrap text-text-secondary outline-hidden hover:bg-state-base-hover hover:text-text-primary focus-visible:ring-2 focus-visible:ring-state-accent-solid"
> >
{t('studio.viewSnippets', { ns: 'app' })} {t('studio.viewSnippets', { ns: 'app' })}
</Link> </Link>
@ -87,7 +88,7 @@ export function AppListHeaderFilters({
<Button <Button
variant="primary" variant="primary"
size="medium" size="medium"
className="gap-0.5 px-2 shadow-xs shadow-shadow-shadow-3" className="gap-0.5 px-2 whitespace-nowrap shadow-xs shadow-shadow-shadow-3"
> >
<span aria-hidden className="i-ri-add-line size-4 shrink-0" /> <span aria-hidden className="i-ri-add-line size-4 shrink-0" />
<span className="pl-1">{t('operation.create', { ns: 'common' })}</span> <span className="pl-1">{t('operation.create', { ns: 'common' })}</span>

View File

@ -45,7 +45,7 @@ export function AppSortFilter({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
aria-label={`${sortByLabel} ${activeOption.text}`} aria-label={`${sortByLabel} ${activeOption.text}`}
className="flex h-8 cursor-pointer items-center rounded-lg border-none bg-components-input-bg-normal py-1 pr-2.5 pl-2 text-left outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover" className="flex h-8 cursor-pointer items-center rounded-lg border-none bg-components-input-bg-normal py-1 pr-2.5 pl-2 text-left whitespace-nowrap outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover"
> >
<span className="flex items-center gap-1 p-1 text-[13px] leading-4 whitespace-nowrap"> <span className="flex items-center gap-1 p-1 text-[13px] leading-4 whitespace-nowrap">
<span className="font-normal text-text-tertiary">{sortByLabel}</span> <span className="font-normal text-text-tertiary">{sortByLabel}</span>

View File

@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next'
import { AppModeEnum } from '@/types/app' import { AppModeEnum } from '@/types/app'
import { isAppListCategory } from './app-type-filter-shared' import { isAppListCategory } from './app-type-filter-shared'
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 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'
type AppTypeFilterProps = { type AppTypeFilterProps = {
value: AppListCategory value: AppListCategory

View File

@ -1,2 +1,2 @@
export const APP_LIST_SEARCH_DEBOUNCE_MS = 500 export const APP_LIST_SEARCH_DEBOUNCE_MS = 500
export const APP_LIST_GRID_CLASS_NAME = 'grid grid-cols-1 gap-2.5 px-8 pt-2 2k:grid-cols-6 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5' export const APP_LIST_GRID_CLASS_NAME = 'grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5 px-8 pt-2'

View File

@ -26,7 +26,7 @@ type CreatorOption = {
isYou: boolean isYou: boolean
} }
const baseChipClassName = '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 baseChipClassName = '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'
const CreatorsFilter = ({ const CreatorsFilter = ({
value, value,

View File

@ -57,7 +57,7 @@ function FirstEmptyState({
return ( return (
<div className="flex grow flex-col overflow-hidden"> <div className="flex grow flex-col overflow-hidden">
<div className="relative min-h-[430px] flex-1 overflow-hidden"> <div className="relative min-h-[430px] flex-1 overflow-hidden">
<div className="pointer-events-none absolute inset-x-8 inset-y-2 grid grid-cols-1 grid-rows-4 gap-3 sm:grid-cols-2 xl:grid-cols-4"> <div className="pointer-events-none absolute inset-x-8 inset-y-2 grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] grid-rows-4 gap-3">
{EMPTY_PLACEHOLDER_CARD_IDS.map(id => ( {EMPTY_PLACEHOLDER_CARD_IDS.map(id => (
<div key={id} className="rounded-xl bg-background-default-lighter opacity-75" /> <div key={id} className="rounded-xl bg-background-default-lighter opacity-75" />
))} ))}

View File

@ -149,18 +149,8 @@ describe('BuiltInPipelineList', () => {
const { container } = render(<BuiltInPipelineList />) const { container } = render(<BuiltInPipelineList />)
const grid = container.querySelector('.grid') const grid = container.querySelector('.grid')
expect(grid).toHaveClass('grid-cols-1', 'gap-3', 'py-2') expect(grid).toHaveClass('grid-cols-[repeat(auto-fill,minmax(296px,1fr))]', 'gap-3', 'py-2')
}) expect(grid).not.toHaveClass('grid-cols-1', 'sm:grid-cols-2', 'md:grid-cols-3', 'lg:grid-cols-4')
it('should have responsive grid columns', () => {
mockUsePipelineTemplateList.mockReturnValue({
data: { pipeline_templates: [] },
isLoading: false,
})
const { container } = render(<BuiltInPipelineList />)
const grid = container.querySelector('.grid')
expect(grid).toHaveClass('sm:grid-cols-2', 'md:grid-cols-3', 'lg:grid-cols-4')
}) })
}) })

View File

@ -132,7 +132,8 @@ describe('CustomizedList', () => {
const { container } = render(<CustomizedList />) const { container } = render(<CustomizedList />)
const grid = container.querySelector('.grid') const grid = container.querySelector('.grid')
expect(grid).toHaveClass('grid-cols-1', 'gap-3', 'py-2') expect(grid).toHaveClass('grid-cols-[repeat(auto-fill,minmax(296px,1fr))]', 'gap-3', 'py-2')
expect(grid).not.toHaveClass('grid-cols-1', 'sm:grid-cols-2', 'md:grid-cols-3', 'lg:grid-cols-4')
}) })
}) })
}) })

View File

@ -22,7 +22,7 @@ const BuiltInPipelineList = () => {
const list = pipelineList?.pipeline_templates || [] const list = pipelineList?.pipeline_templates || []
return ( return (
<div className="grid grid-cols-1 gap-3 py-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> <div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-3 py-2">
<CreateCard /> <CreateCard />
{!isLoading && list.map((pipeline, index) => ( {!isLoading && list.map((pipeline, index) => (
<TemplateCard <TemplateCard

View File

@ -13,7 +13,7 @@ const CustomizedList = () => {
return ( return (
<> <>
<div className="pt-2 system-sm-semibold-uppercase text-text-tertiary">{t('templates.customized', { ns: 'datasetPipeline' })}</div> <div className="pt-2 system-sm-semibold-uppercase text-text-tertiary">{t('templates.customized', { ns: 'datasetPipeline' })}</div>
<div className="grid grid-cols-1 gap-3 py-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> <div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-3 py-2">
{list.map((pipeline, index) => ( {list.map((pipeline, index) => (
<TemplateCard <TemplateCard
key={index} key={index}

View File

@ -17,6 +17,22 @@ describe('DatasetFirstEmptyState', () => {
expect(pipelineLink.querySelector('.i-custom-vender-pipeline-pipeline-line')).toBeInTheDocument() expect(pipelineLink.querySelector('.i-custom-vender-pipeline-pipeline-line')).toBeInTheDocument()
}) })
it('lays out placeholder cards with auto-fill grid columns', () => {
const { container } = render(<DatasetFirstEmptyState canConnectExternalDataset canCreateDataset />)
const placeholderGrid = Array.from(container.querySelectorAll('.pointer-events-none'))
.find(element => element.className.includes('grid-rows-4'))
if (!placeholderGrid)
throw new Error('Expected dataset first empty state placeholder grid to render')
expect(placeholderGrid).toHaveClass(
'grid',
'grid-cols-[repeat(auto-fill,minmax(296px,1fr))]',
'grid-rows-4',
)
expect(placeholderGrid).not.toHaveClass('grid-cols-1', 'sm:grid-cols-2', 'lg:grid-cols-3', 'xl:grid-cols-4')
})
it('should hide dataset creation actions when dataset.create_and_management is unavailable', () => { it('should hide dataset creation actions when dataset.create_and_management is unavailable', () => {
render(<DatasetFirstEmptyState canConnectExternalDataset canCreateDataset={false} />) render(<DatasetFirstEmptyState canConnectExternalDataset canCreateDataset={false} />)

View File

@ -60,7 +60,7 @@ function DatasetFirstEmptyState({
return ( return (
<div className="flex grow flex-col overflow-hidden"> <div className="flex grow flex-col overflow-hidden">
<div className="relative min-h-[520px] flex-1 overflow-hidden"> <div className="relative min-h-[520px] flex-1 overflow-hidden">
<div className="pointer-events-none absolute inset-x-8 inset-y-2 grid grid-cols-1 grid-rows-4 gap-3 sm:grid-cols-2 xl:grid-cols-4"> <div className="pointer-events-none absolute inset-x-8 inset-y-2 grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] grid-rows-4 gap-3">
{EMPTY_PLACEHOLDER_CARD_IDS.map(id => ( {EMPTY_PLACEHOLDER_CARD_IDS.map(id => (
<div key={id} className="rounded-xl bg-background-default-lighter opacity-75" /> <div key={id} className="rounded-xl bg-background-default-lighter opacity-75" />
))} ))}

View File

@ -45,7 +45,7 @@ function RecommendationSectionSkeletonBody({
</div> </div>
<SkeletonRectangle className="size-8 shrink-0 animate-pulse rounded-lg" /> <SkeletonRectangle className="size-8 shrink-0 animate-pulse rounded-lg" />
</div> </div>
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 xl:grid-cols-4"> <div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5">
{Array.from({ length: 4 }, (_, index) => ( {Array.from({ length: 4 }, (_, index) => (
<div key={index} className="rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg px-4 pt-4 pb-4 shadow-xs"> <div key={index} className="rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg px-4 pt-4 pb-4 shadow-xs">
<div className="flex flex-col items-start gap-2 pb-1"> <div className="flex flex-col items-start gap-2 pb-1">
@ -70,7 +70,7 @@ function RecommendationSectionSkeletonBody({
<SkeletonRectangle className="h-5 w-48 animate-pulse" /> <SkeletonRectangle className="h-5 w-48 animate-pulse" />
</div> </div>
</div> </div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4"> <div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-3">
{Array.from({ length: 4 }, (_, index) => ( {Array.from({ length: 4 }, (_, index) => (
<div key={index} className="rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-4 py-3 shadow-md"> <div key={index} className="rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-4 py-3 shadow-md">
<SkeletonRow> <SkeletonRow>
@ -110,7 +110,7 @@ function ExploreHeaderSkeletonBody() {
function ExploreAppListSkeletonBody() { function ExploreAppListSkeletonBody() {
return ( return (
<div className="grid shrink-0 grid-cols-1 content-start gap-3 px-8 sm:grid-cols-2 xl:grid-cols-4"> <div className="grid shrink-0 grid-cols-[repeat(auto-fill,minmax(296px,1fr))] content-start gap-3 px-8">
{Array.from({ length: 8 }, (_, index) => ( {Array.from({ length: 8 }, (_, index) => (
<ExploreAppCardSkeleton key={index} /> <ExploreAppCardSkeleton key={index} />
))} ))}

View File

@ -35,7 +35,7 @@ const ContinueWork = ({
<span className="i-ri-arrow-right-line size-3 shrink-0" aria-hidden="true" /> <span className="i-ri-arrow-right-line size-3 shrink-0" aria-hidden="true" />
</Link> </Link>
</div> </div>
<div className="grid grid-cols-1 gap-2.5 pt-2 sm:grid-cols-2 xl:grid-cols-4"> <div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5 pt-2">
{apps.map(app => ( {apps.map(app => (
<ContinueWorkItem key={app.id} app={app} /> <ContinueWorkItem key={app.id} app={app} />
))} ))}

View File

@ -117,7 +117,7 @@ const LearnDifyContent = ({
</button> </button>
)} )}
</div> </div>
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2 xl:grid-cols-4"> <div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5">
{visibleItems.map(item => ( {visibleItems.map(item => (
<LearnDifyItem <LearnDifyItem
key={item.app_id} key={item.app_id}

View File

@ -270,6 +270,18 @@ describe('SnippetList', () => {
expect(screen.getByTestId('tag-management-modal')).toBeInTheDocument() expect(screen.getByTestId('tag-management-modal')).toBeInTheDocument()
}) })
it('lays out snippet cards with auto-fill grid columns', () => {
renderList()
const card = screen.getByRole('link', { name: /Sales Snippet/ }).closest('article')
const grid = card?.parentElement
expect(grid).toHaveClass(
'grid',
'grid-cols-[repeat(auto-fill,minmax(296px,1fr))]',
)
})
it('passes creator, tag, and search filters to the snippets list query', () => { it('passes creator, tag, and search filters to the snippets list query', () => {
mockQueryState.tagIDs = ['tag-1', 'tag-2'] mockQueryState.tagIDs = ['tag-1', 'tag-2']
mockQueryState.keywords = 'sales' mockQueryState.keywords = 'sales'

View File

@ -189,7 +189,7 @@ const SnippetList = () => {
</div> </div>
</StudioListHeader> </StudioListHeader>
<div className={cn( <div className={cn(
'relative grid grow grid-cols-1 content-start gap-4 px-8 pt-2 2k:grid-cols-6 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5', 'relative grid grow grid-cols-[repeat(auto-fill,minmax(296px,1fr))] content-start gap-4 px-8 pt-2',
!hasAnySnippet && 'overflow-hidden', !hasAnySnippet && 'overflow-hidden',
)} )}
> >

View File

@ -165,11 +165,22 @@ describe('AgentRosterList', () => {
it('renders the Figma-aligned empty roster overlay', () => { it('renders the Figma-aligned empty roster overlay', () => {
const { container } = renderList([]) const { container } = renderList([])
const placeholderGrid = Array.from(container.querySelectorAll('.pointer-events-none'))
.find(element => element.className.includes('grid-rows-4'))
if (!placeholderGrid)
throw new Error('Expected agent roster placeholder grid to render')
expect(screen.getByRole('heading', { name: 'agentV2.roster.empty' })).toHaveClass('system-sm-regular', 'text-text-tertiary') expect(screen.getByRole('heading', { name: 'agentV2.roster.empty' })).toHaveClass('system-sm-regular', 'text-text-tertiary')
expect(container.querySelectorAll('.bg-background-default-lighter')).toHaveLength(16) expect(container.querySelectorAll('.bg-background-default-lighter')).toHaveLength(16)
expect(container.querySelector('.bg-linear-to-b')).toBeInTheDocument() expect(container.querySelector('.bg-linear-to-b')).toBeInTheDocument()
expect(container.querySelector('.i-ri-robot-2-line')).toHaveClass('size-6', 'text-text-tertiary') expect(container.querySelector('.i-ri-robot-2-line')).toHaveClass('size-6', 'text-text-tertiary')
expect(placeholderGrid).toHaveClass(
'grid',
'grid-cols-[repeat(auto-fill,minmax(296px,1fr))]',
'grid-rows-4',
)
expect(placeholderGrid).not.toHaveClass('grid-cols-1', 'sm:grid-cols-2', 'lg:grid-cols-3', 'xl:grid-cols-4')
}) })
it('uses the same overlay treatment for empty search results', () => { it('uses the same overlay treatment for empty search results', () => {

View File

@ -71,7 +71,7 @@ function AgentRosterPlaceholderState({ title }: { title: string }) {
aria-labelledby="agent-roster-placeholder-title" aria-labelledby="agent-roster-placeholder-title"
className="relative col-span-full min-h-[calc(100vh-142px)] overflow-hidden" className="relative col-span-full min-h-[calc(100vh-142px)] overflow-hidden"
> >
<div className="pointer-events-none absolute inset-0 grid grid-cols-1 grid-rows-4 gap-3 sm:grid-cols-2 xl:grid-cols-4"> <div className="pointer-events-none absolute inset-0 grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] grid-rows-4 gap-3">
{emptyPlaceholderCardIds.map(id => ( {emptyPlaceholderCardIds.map(id => (
<div key={id} className="rounded-xl bg-background-default-lighter opacity-75" /> <div key={id} className="rounded-xl bg-background-default-lighter opacity-75" />
))} ))}
@ -262,7 +262,7 @@ export function AgentRosterList({
const { t } = useTranslation('agentV2') const { t } = useTranslation('agentV2')
return ( return (
<section aria-label={label} className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,294px),1fr))] gap-2.5" aria-busy={isFetching || undefined}> <section aria-label={label} className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5" aria-busy={isFetching || undefined}>
{isPending && <AgentRosterSkeleton />} {isPending && <AgentRosterSkeleton />}
{!isPending && isError && ( {!isPending && isError && (
<AgentRosterPlaceholderState title={t('roster.loadingError')} /> <AgentRosterPlaceholderState title={t('roster.loadingError')} />

View File

@ -95,6 +95,11 @@ describe('TagFilter', () => {
expect(container.querySelector('svg')).not.toBeInTheDocument() expect(container.querySelector('svg')).not.toBeInTheDocument()
}) })
it('should apply custom trigger class names', () => {
render(<TagFilter {...defaultProps} triggerClassName="min-w-0" />)
expect(screen.getByRole('combobox', { name: i18n.placeholder })).toHaveClass('min-w-0')
})
it('should filter tags by type prop', async () => { it('should filter tags by type prop', async () => {
const user = userEvent.setup() const user = userEvent.setup()
render(<TagFilter {...defaultProps} type="knowledge" />) render(<TagFilter {...defaultProps} type="knowledge" />)

View File

@ -19,6 +19,7 @@ type TagFilterProps = {
onChange: (v: string[]) => void onChange: (v: string[]) => void
onOpenTagManagement?: () => void onOpenTagManagement?: () => void
showLeadingIcon?: boolean showLeadingIcon?: boolean
triggerClassName?: string
} }
export const TagFilter = ({ export const TagFilter = ({
type, type,
@ -26,6 +27,7 @@ export const TagFilter = ({
onChange, onChange,
onOpenTagManagement = () => {}, onOpenTagManagement = () => {},
showLeadingIcon = true, showLeadingIcon = true,
triggerClassName,
}: TagFilterProps) => { }: TagFilterProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@ -76,8 +78,9 @@ export const TagFilter = ({
aria-label={triggerLabel} aria-label={triggerLabel}
icon={false} icon={false}
className={cn( className={cn(
'flex h-8 max-w-60 min-w-28 cursor-pointer items-center gap-1 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2 py-0 text-left select-none hover:bg-components-input-bg-normal focus-visible:bg-components-input-bg-normal focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-components-input-bg-normal', 'flex h-8 max-w-60 min-w-28 cursor-pointer items-center gap-1 rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2 py-0 text-left whitespace-nowrap select-none hover:bg-components-input-bg-normal focus-visible:bg-components-input-bg-normal focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-components-input-bg-normal',
!!value.length && 'pr-6 shadow-xs', !!value.length && 'pr-6 shadow-xs',
triggerClassName,
)} )}
> >
<span className="flex w-full min-w-0 items-center gap-1"> <span className="flex w-full min-w-0 items-center gap-1">