mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
fix(web): improve card grid responsiveness (#38263)
This commit is contained in:
parent
76391b0383
commit
befe7ec269
@ -222,7 +222,7 @@ const Apps = ({
|
||||
</div>
|
||||
<div
|
||||
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 => (
|
||||
|
||||
@ -429,17 +429,19 @@ describe('List', () => {
|
||||
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()
|
||||
|
||||
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 sortButton = screen.getByRole('button', { name: 'Sort by Last modified' })
|
||||
const snippetsLink = screen.getByRole('link', { name: 'app.studio.viewSnippets' })
|
||||
const createButton = screen.getByRole('button', { name: 'common.operation.create' })
|
||||
|
||||
expect(snippetsLink).toHaveAttribute('href', '/snippets')
|
||||
expect(sortButton.compareDocumentPosition(searchInput) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(searchInput.compareDocumentPosition(snippetsLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(creatorsButton.compareDocumentPosition(searchInput) & 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()
|
||||
})
|
||||
|
||||
@ -450,6 +452,17 @@ describe('List', () => {
|
||||
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', () => {
|
||||
renderList()
|
||||
|
||||
@ -522,6 +535,24 @@ describe('List', () => {
|
||||
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', () => {
|
||||
mockAppData = { pages: [{ data: [], total: 0 }] }
|
||||
|
||||
|
||||
@ -1058,11 +1058,20 @@ export function AppCard({ app, onlineUsers = [], onRefresh, onOpenTagManagement
|
||||
</div>
|
||||
</div>
|
||||
<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="truncate">{app.author_name}</div>
|
||||
<div className="shrink-0">·</div>
|
||||
<div className="truncate">{editTimeText}</div>
|
||||
{app.author_name && (
|
||||
<>
|
||||
<div className="min-w-0 truncate">{app.author_name}</div>
|
||||
<div className="shrink-0">·</div>
|
||||
</>
|
||||
)}
|
||||
<div className="shrink-0">{editTimeText}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -54,8 +54,8 @@ export function AppListHeaderFilters({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
||||
<AppTypeFilter value={category} onChange={onCategoryChange} />
|
||||
<TagFilter
|
||||
type="app"
|
||||
@ -63,20 +63,21 @@ export function AppListHeaderFilters({
|
||||
onChange={onTagIDsChange}
|
||||
onOpenTagManagement={onOpenTagManagement}
|
||||
showLeadingIcon={false}
|
||||
triggerClassName="min-w-0"
|
||||
/>
|
||||
<CreatorsFilter value={creatorIDs} onChange={onCreatorIDsChange} />
|
||||
<AppSortFilter value={sortBy} onChange={onSortByChange} />
|
||||
<SearchInput
|
||||
className="w-50"
|
||||
className="w-50 max-w-full"
|
||||
value={keywords}
|
||||
onValueChange={onKeywordsChange}
|
||||
aria-label={t('gotoAnything.actions.searchApplications', { ns: 'app' })}
|
||||
/>
|
||||
</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
|
||||
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' })}
|
||||
</Link>
|
||||
@ -87,7 +88,7 @@ export function AppListHeaderFilters({
|
||||
<Button
|
||||
variant="primary"
|
||||
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 className="pl-1">{t('operation.create', { ns: 'common' })}</span>
|
||||
|
||||
@ -45,7 +45,7 @@ export function AppSortFilter({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
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="font-normal text-text-tertiary">{sortByLabel}</span>
|
||||
|
||||
@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
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 = {
|
||||
value: AppListCategory
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
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'
|
||||
|
||||
@ -26,7 +26,7 @@ type CreatorOption = {
|
||||
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 = ({
|
||||
value,
|
||||
|
||||
@ -57,7 +57,7 @@ function FirstEmptyState({
|
||||
return (
|
||||
<div className="flex grow flex-col 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 => (
|
||||
<div key={id} className="rounded-xl bg-background-default-lighter opacity-75" />
|
||||
))}
|
||||
|
||||
@ -149,18 +149,8 @@ describe('BuiltInPipelineList', () => {
|
||||
|
||||
const { container } = render(<BuiltInPipelineList />)
|
||||
const grid = container.querySelector('.grid')
|
||||
expect(grid).toHaveClass('grid-cols-1', 'gap-3', 'py-2')
|
||||
})
|
||||
|
||||
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')
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -132,7 +132,8 @@ describe('CustomizedList', () => {
|
||||
|
||||
const { container } = render(<CustomizedList />)
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -22,7 +22,7 @@ const BuiltInPipelineList = () => {
|
||||
const list = pipelineList?.pipeline_templates || []
|
||||
|
||||
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 />
|
||||
{!isLoading && list.map((pipeline, index) => (
|
||||
<TemplateCard
|
||||
|
||||
@ -13,7 +13,7 @@ const CustomizedList = () => {
|
||||
return (
|
||||
<>
|
||||
<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) => (
|
||||
<TemplateCard
|
||||
key={index}
|
||||
|
||||
@ -17,6 +17,22 @@ describe('DatasetFirstEmptyState', () => {
|
||||
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', () => {
|
||||
render(<DatasetFirstEmptyState canConnectExternalDataset canCreateDataset={false} />)
|
||||
|
||||
|
||||
@ -60,7 +60,7 @@ function DatasetFirstEmptyState({
|
||||
return (
|
||||
<div className="flex grow flex-col 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 => (
|
||||
<div key={id} className="rounded-xl bg-background-default-lighter opacity-75" />
|
||||
))}
|
||||
|
||||
@ -45,7 +45,7 @@ function RecommendationSectionSkeletonBody({
|
||||
</div>
|
||||
<SkeletonRectangle className="size-8 shrink-0 animate-pulse rounded-lg" />
|
||||
</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) => (
|
||||
<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">
|
||||
@ -70,7 +70,7 @@ function RecommendationSectionSkeletonBody({
|
||||
<SkeletonRectangle className="h-5 w-48 animate-pulse" />
|
||||
</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) => (
|
||||
<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>
|
||||
@ -110,7 +110,7 @@ function ExploreHeaderSkeletonBody() {
|
||||
|
||||
function ExploreAppListSkeletonBody() {
|
||||
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) => (
|
||||
<ExploreAppCardSkeleton key={index} />
|
||||
))}
|
||||
|
||||
@ -35,7 +35,7 @@ const ContinueWork = ({
|
||||
<span className="i-ri-arrow-right-line size-3 shrink-0" aria-hidden="true" />
|
||||
</Link>
|
||||
</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 => (
|
||||
<ContinueWorkItem key={app.id} app={app} />
|
||||
))}
|
||||
|
||||
@ -117,7 +117,7 @@ const LearnDifyContent = ({
|
||||
</button>
|
||||
)}
|
||||
</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 => (
|
||||
<LearnDifyItem
|
||||
key={item.app_id}
|
||||
|
||||
@ -270,6 +270,18 @@ describe('SnippetList', () => {
|
||||
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', () => {
|
||||
mockQueryState.tagIDs = ['tag-1', 'tag-2']
|
||||
mockQueryState.keywords = 'sales'
|
||||
|
||||
@ -189,7 +189,7 @@ const SnippetList = () => {
|
||||
</div>
|
||||
</StudioListHeader>
|
||||
<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',
|
||||
)}
|
||||
>
|
||||
|
||||
@ -165,11 +165,22 @@ describe('AgentRosterList', () => {
|
||||
|
||||
it('renders the Figma-aligned empty roster overlay', () => {
|
||||
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(container.querySelectorAll('.bg-background-default-lighter')).toHaveLength(16)
|
||||
expect(container.querySelector('.bg-linear-to-b')).toBeInTheDocument()
|
||||
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', () => {
|
||||
|
||||
@ -71,7 +71,7 @@ function AgentRosterPlaceholderState({ title }: { title: string }) {
|
||||
aria-labelledby="agent-roster-placeholder-title"
|
||||
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 => (
|
||||
<div key={id} className="rounded-xl bg-background-default-lighter opacity-75" />
|
||||
))}
|
||||
@ -262,7 +262,7 @@ export function AgentRosterList({
|
||||
const { t } = useTranslation('agentV2')
|
||||
|
||||
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 && isError && (
|
||||
<AgentRosterPlaceholderState title={t('roster.loadingError')} />
|
||||
|
||||
@ -95,6 +95,11 @@ describe('TagFilter', () => {
|
||||
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 () => {
|
||||
const user = userEvent.setup()
|
||||
render(<TagFilter {...defaultProps} type="knowledge" />)
|
||||
|
||||
@ -19,6 +19,7 @@ type TagFilterProps = {
|
||||
onChange: (v: string[]) => void
|
||||
onOpenTagManagement?: () => void
|
||||
showLeadingIcon?: boolean
|
||||
triggerClassName?: string
|
||||
}
|
||||
export const TagFilter = ({
|
||||
type,
|
||||
@ -26,6 +27,7 @@ export const TagFilter = ({
|
||||
onChange,
|
||||
onOpenTagManagement = () => {},
|
||||
showLeadingIcon = true,
|
||||
triggerClassName,
|
||||
}: TagFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
@ -76,8 +78,9 @@ export const TagFilter = ({
|
||||
aria-label={triggerLabel}
|
||||
icon={false}
|
||||
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',
|
||||
triggerClassName,
|
||||
)}
|
||||
>
|
||||
<span className="flex w-full min-w-0 items-center gap-1">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user