mirror of
https://github.com/langgenius/dify.git
synced 2026-08-31 01:36:38 +08:00
fix: align main navigation card grids (#41167)
This commit is contained in:
@@ -899,29 +899,6 @@ describe('List', () => {
|
||||
)
|
||||
})
|
||||
|
||||
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 }] }
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-qu
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InfiniteScrollSentinel } from '@/app/components/base/infinite-scroll-sentinel'
|
||||
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
|
||||
import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
@@ -217,7 +218,7 @@ function AppListCatalogContent({
|
||||
preloadDistance={getPreloadDistance}
|
||||
scrollContainerRef={scrollViewportRef}
|
||||
/>
|
||||
<div className="relative grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5">
|
||||
<div className={cn('relative gap-2.5', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
|
||||
<AppCardSkeleton count={3} />
|
||||
{isFetchNextPageError && (
|
||||
<div
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
|
||||
|
||||
export const APP_LIST_SEARCH_DEBOUNCE_MS = 500
|
||||
export const APP_LIST_GRID_CLASS_NAME =
|
||||
'grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5 px-8 pt-2'
|
||||
export const APP_LIST_GRID_CLASS_NAME = cn('gap-2.5 px-8 pt-2', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { App } from '@/models/explore'
|
||||
import type { TryAppSelection } from '@/types/try-app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import LearnDify from '@/app/components/explore/learn-dify'
|
||||
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
|
||||
import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry'
|
||||
import FirstEmptyActionCard from './action-card'
|
||||
|
||||
@@ -71,7 +73,12 @@ function FirstEmptyState({
|
||||
return (
|
||||
<div className="flex grow flex-col overflow-hidden">
|
||||
<div className="relative min-h-107.5 flex-1 overflow-hidden">
|
||||
<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">
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none absolute inset-x-8 inset-y-2 grid-rows-4 gap-3',
|
||||
MAIN_NAV_APP_CARD_GRID_CLASS_NAME,
|
||||
)}
|
||||
>
|
||||
{EMPTY_PLACEHOLDER_CARD_IDS.map((id) => (
|
||||
<div key={id} className="rounded-xl bg-background-default-lighter opacity-75" />
|
||||
))}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useLearnDifyAppList } from '@/service/use-explore'
|
||||
import LearnDifyItem from './item'
|
||||
@@ -127,7 +128,7 @@ const LearnDifyContent = ({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5">
|
||||
<div className={cn('gap-2.5', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
|
||||
{visibleItems.map((item) => (
|
||||
<LearnDifyItem
|
||||
key={item.app_id}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { APP_LIST_GRID_CLASS_NAME } from '@/app/components/apps/constants'
|
||||
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '../app-card-grid'
|
||||
|
||||
const cardIds = ['first', 'second', 'third', 'fourth']
|
||||
|
||||
function CardGrid({
|
||||
className,
|
||||
label,
|
||||
width,
|
||||
}: {
|
||||
className: string
|
||||
label: string
|
||||
width: number
|
||||
}) {
|
||||
return (
|
||||
<section aria-label={label} className={className} style={{ width }}>
|
||||
{cardIds.map((id) => (
|
||||
<article key={id} aria-label={id} className="h-10" />
|
||||
))}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
describe('Main navigation card grid', () => {
|
||||
it('aligns Studio starred and app cards and remains overflow-free in a narrow container', async () => {
|
||||
// happy-dom does not resolve CSS Grid tracks or browser layout geometry.
|
||||
const screen = await render(
|
||||
<>
|
||||
<CardGrid label="Starred apps" className={APP_LIST_GRID_CLASS_NAME} width={1264} />
|
||||
<CardGrid label="All apps" className={APP_LIST_GRID_CLASS_NAME} width={1264} />
|
||||
<CardGrid
|
||||
label="Narrow card grid"
|
||||
className={cn('gap-2.5', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}
|
||||
width={280}
|
||||
/>
|
||||
</>,
|
||||
)
|
||||
|
||||
const starredGrid = screen.getByRole('region', { name: 'Starred apps' })
|
||||
const starredGridRect = starredGrid.element().getBoundingClientRect()
|
||||
const starredCardRects = starredGrid
|
||||
.getByRole('article')
|
||||
.elements()
|
||||
.map((element) => element.getBoundingClientRect())
|
||||
const appCardRects = screen
|
||||
.getByRole('region', { name: 'All apps' })
|
||||
.getByRole('article')
|
||||
.elements()
|
||||
.map((element) => element.getBoundingClientRect())
|
||||
|
||||
expect(new Set(starredCardRects.map((rect) => rect.top)).size).toBe(1)
|
||||
expect(starredCardRects[0]!.left - starredGridRect.left).toBeCloseTo(32)
|
||||
expect(starredCardRects[0]!.width).toBeCloseTo(292.5)
|
||||
expect(starredGridRect.right - starredCardRects.at(-1)!.right).toBeCloseTo(32)
|
||||
expect(appCardRects.map(({ left, width }) => ({ left, width }))).toEqual(
|
||||
starredCardRects.map(({ left, width }) => ({ left, width })),
|
||||
)
|
||||
|
||||
const narrowGrid = screen.getByRole('region', { name: 'Narrow card grid' })
|
||||
const narrowGridElement = narrowGrid.element()
|
||||
const narrowCardRects = narrowGrid
|
||||
.getByRole('article')
|
||||
.elements()
|
||||
.map((element) => element.getBoundingClientRect())
|
||||
|
||||
expect(new Set(narrowCardRects.map((rect) => rect.top)).size).toBe(4)
|
||||
expect(narrowCardRects.every((rect) => Math.abs(rect.width - 280) < 0.1)).toBe(true)
|
||||
expect(narrowGridElement.scrollWidth).toBe(narrowGridElement.clientWidth)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,2 @@
|
||||
export const MAIN_NAV_APP_CARD_GRID_CLASS_NAME =
|
||||
'grid grid-cols-[repeat(auto-fill,minmax(min(100%,288px),1fr))]'
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { RecentAppResponse } from '@dify/contracts/api/console/apps/types.gen'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
|
||||
import Link from '@/next/link'
|
||||
import { ContinueWorkItem } from './item'
|
||||
|
||||
@@ -33,7 +34,7 @@ export function ContinueWork({ apps, className }: ContinueWorkProps) {
|
||||
<span className="i-ri-arrow-right-line size-3 shrink-0" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5 pt-2">
|
||||
<div className={cn('gap-2.5 pt-2', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
|
||||
{apps.map((app) => (
|
||||
<ContinueWorkItem key={app.id} app={app} />
|
||||
))}
|
||||
|
||||
@@ -698,7 +698,7 @@ describe('HomeContent', () => {
|
||||
|
||||
expect(screen.getByText('Alpha')).toBeInTheDocument()
|
||||
expect(screen.getByText('Beta')).toBeInTheDocument()
|
||||
expect(screen.getByText('explore.apps.title')).toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'explore.apps.title' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render continue work with the first eight workspace apps', () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
|
||||
import {
|
||||
getStepByStepTourPermissionVariant,
|
||||
trackStepByStepTourEvent,
|
||||
@@ -40,7 +41,6 @@ import { HomeIntro } from '../home-intro'
|
||||
import { HomeShell } from '../home-shell'
|
||||
import { TemplateCard } from '../template-card'
|
||||
import { HomeRecommendations } from './recommendations'
|
||||
import s from './style.module.css'
|
||||
import { HomeTemplatesHeader } from './templates-header'
|
||||
|
||||
const TryApp = dynamic(() => import('@/app/components/explore/try-app'), { ssr: false })
|
||||
@@ -431,9 +431,9 @@ export function HomeContent() {
|
||||
/>
|
||||
|
||||
<div className={cn('relative flex flex-1 shrink-0 grow flex-col pb-6')}>
|
||||
<nav
|
||||
<section
|
||||
aria-labelledby="home-templates-title"
|
||||
className={cn(s.templateGrid, 'grid shrink-0 content-start gap-3 px-8')}
|
||||
className={cn('shrink-0 content-start gap-2.5 px-8', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}
|
||||
>
|
||||
{searchFilteredList.map((app) => (
|
||||
<TemplateCard
|
||||
@@ -444,7 +444,7 @@ export function HomeContent() {
|
||||
onTry={handleTryApp}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{isShowCreateModal && (
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
.textGradient {
|
||||
background: linear-gradient(to right, rgba(16, 74, 225, 1) 0, rgba(0, 152, 238, 1) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.templateGrid {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.templateGrid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 640px) and (max-width: 1279px) {
|
||||
.templateGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SkeletonContainer, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
|
||||
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
|
||||
import { HomeIntroSkeleton } from './home-intro'
|
||||
|
||||
function HomeTemplateCardSkeleton() {
|
||||
@@ -39,7 +41,7 @@ function HomeRecommendationsSkeleton() {
|
||||
<SkeletonRectangle className="my-0 h-6 w-48 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5 pt-2">
|
||||
<div className={cn('gap-2.5 pt-2', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
|
||||
{Array.from({ length: 4 }, (_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
@@ -82,7 +84,7 @@ function HomeTemplatesHeaderSkeletonBody() {
|
||||
|
||||
function HomeTemplatesSkeletonBody() {
|
||||
return (
|
||||
<div className="grid shrink-0 grid-cols-[repeat(auto-fill,minmax(296px,1fr))] content-start gap-3 px-8">
|
||||
<div className={cn('shrink-0 content-start gap-2.5 px-8', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
|
||||
{Array.from({ length: 8 }, (_, index) => (
|
||||
<HomeTemplateCardSkeleton key={index} />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user