From befe7ec269e55720456b65bb9fda298cc6773b56 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Wed, 1 Jul 2026 17:58:11 +0800 Subject: [PATCH] fix(web): improve card grid responsiveness (#38263) --- .../app/create-app-dialog/app-list/index.tsx | 2 +- .../components/apps/__tests__/list.spec.tsx | 39 +++++++++++++++++-- web/app/components/apps/app-card.tsx | 17 ++++++-- .../apps/app-list-header-filters.tsx | 15 +++---- web/app/components/apps/app-sort-filter.tsx | 2 +- web/app/components/apps/app-type-filter.tsx | 2 +- web/app/components/apps/constants.ts | 2 +- web/app/components/apps/creators-filter.tsx | 2 +- .../apps/first-empty-state/index.tsx | 2 +- .../__tests__/built-in-pipeline-list.spec.tsx | 14 +------ .../list/__tests__/customized-list.spec.tsx | 3 +- .../list/built-in-pipeline-list.tsx | 2 +- .../list/customized-list.tsx | 2 +- .../__tests__/index.spec.tsx | 16 ++++++++ .../datasets/list/first-empty-state/index.tsx | 2 +- .../explore/app-list/loading-skeletons.tsx | 6 +-- .../explore/continue-work/index.tsx | 2 +- .../components/explore/learn-dify/index.tsx | 2 +- .../snippet-list/__tests__/index.spec.tsx | 12 ++++++ web/app/components/snippet-list/index.tsx | 2 +- .../__tests__/agent-roster-list.spec.tsx | 11 ++++++ .../roster/components/agent-roster-list.tsx | 4 +- .../__tests__/tag-filter.spec.tsx | 5 +++ .../tag-management/components/tag-filter.tsx | 5 ++- 24 files changed, 125 insertions(+), 46 deletions(-) diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index ac660cdb1fc..f8c0bcd6ada 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -222,7 +222,7 @@ const Apps = ({
{searchFilteredList.map(app => ( diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index e2d8056ba7b..4e9fa240af9 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -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 }] } diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 4242a7214ef..f22bc28e931 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -1058,11 +1058,20 @@ export function AppCard({ app, onlineUsers = [], onRefresh, onOpenTagManagement
-
+
-
{app.author_name}
-
·
-
{editTimeText}
+ {app.author_name && ( + <> +
{app.author_name}
+
·
+ + )} +
{editTimeText}
diff --git a/web/app/components/apps/app-list-header-filters.tsx b/web/app/components/apps/app-list-header-filters.tsx index 04379fd5c1d..f31c1052ecc 100644 --- a/web/app/components/apps/app-list-header-filters.tsx +++ b/web/app/components/apps/app-list-header-filters.tsx @@ -54,8 +54,8 @@ export function AppListHeaderFilters({ const { t } = useTranslation() return ( -
-
+
+
-
-
+
+ {t('studio.viewSnippets', { ns: 'app' })} @@ -87,7 +88,7 @@ export function AppListHeaderFilters({ )}
-
+
{visibleItems.map(item => ( { 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' diff --git a/web/app/components/snippet-list/index.tsx b/web/app/components/snippet-list/index.tsx index 8437a02a923..fadf48b86ef 100644 --- a/web/app/components/snippet-list/index.tsx +++ b/web/app/components/snippet-list/index.tsx @@ -189,7 +189,7 @@ const SnippetList = () => {
diff --git a/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx b/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx index f11e76d2960..dab0347d410 100644 --- a/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx +++ b/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx @@ -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', () => { diff --git a/web/features/agent-v2/roster/components/agent-roster-list.tsx b/web/features/agent-v2/roster/components/agent-roster-list.tsx index a7f98b81506..e29967f9ceb 100644 --- a/web/features/agent-v2/roster/components/agent-roster-list.tsx +++ b/web/features/agent-v2/roster/components/agent-roster-list.tsx @@ -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" > -
+
{emptyPlaceholderCardIds.map(id => (
))} @@ -262,7 +262,7 @@ export function AgentRosterList({ const { t } = useTranslation('agentV2') return ( -
+
{isPending && } {!isPending && isError && ( diff --git a/web/features/tag-management/__tests__/tag-filter.spec.tsx b/web/features/tag-management/__tests__/tag-filter.spec.tsx index 5daadf1715b..13fd2b8e64d 100644 --- a/web/features/tag-management/__tests__/tag-filter.spec.tsx +++ b/web/features/tag-management/__tests__/tag-filter.spec.tsx @@ -95,6 +95,11 @@ describe('TagFilter', () => { expect(container.querySelector('svg')).not.toBeInTheDocument() }) + it('should apply custom trigger class names', () => { + render() + expect(screen.getByRole('combobox', { name: i18n.placeholder })).toHaveClass('min-w-0') + }) + it('should filter tags by type prop', async () => { const user = userEvent.setup() render() diff --git a/web/features/tag-management/components/tag-filter.tsx b/web/features/tag-management/components/tag-filter.tsx index 56c28776341..c6d1766d3c8 100644 --- a/web/features/tag-management/components/tag-filter.tsx +++ b/web/features/tag-management/components/tag-filter.tsx @@ -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, )} >