feat: hide learn dify anim effect

This commit is contained in:
Joel 2026-05-09 18:30:44 +08:00
parent 374eca2504
commit d340b6d168
7 changed files with 156 additions and 3 deletions

View File

@ -9,6 +9,7 @@ import { fetchAppDetail } from '@/service/explore'
import { useMembers } from '@/service/use-common'
import { renderWithNuqs } from '@/test/nuqs-testing'
import { AppModeEnum } from '@/types/app'
import { LEARN_DIFY_HIDDEN_STORAGE_KEY } from '../../learn-dify/storage'
import AppList from '../index'
let mockExploreData: { categories: string[], allList: App[] } | undefined = { categories: [], allList: [] }
@ -170,6 +171,7 @@ describe('AppList', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
localStorage.clear()
mockExploreData = { categories: [], allList: [] }
mockIsLoading = false
mockIsError = false
@ -234,6 +236,28 @@ describe('AppList', () => {
expect(screen.queryByText('workflow')).not.toBeInTheDocument()
expect(screen.queryByText('3 min')).not.toBeInTheDocument()
})
it('should collapse learn dify and persist hidden state when hide is clicked', async () => {
mockExploreData = {
categories: ['Writing'],
allList: [createApp()],
}
renderAppList()
fireEvent.click(screen.getByRole('button', { name: 'explore.learnDify.hide' }))
const learnDifySection = screen.getByRole('heading', { name: 'explore.learnDify.title' }).closest('section')
expect(learnDifySection).toHaveClass('z-50', 'opacity-20')
expect(learnDifySection).toHaveStyle({ transform: 'scale(0.08)' })
await act(async () => {
await vi.advanceTimersByTimeAsync(800)
})
expect(screen.queryByRole('heading', { name: 'explore.learnDify.title' })).not.toBeInTheDocument()
expect(localStorage.getItem(LEARN_DIFY_HIDDEN_STORAGE_KEY)).toBe('true')
})
})
describe('Props', () => {

View File

@ -2,10 +2,12 @@
import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Link from '@/next/link'
import { learnDifyItems } from './data'
import LearnDifyItem from './item'
import { useLearnDifyHiddenState } from './storage'
type LearnDifyProps = {
className?: string
@ -15,16 +17,62 @@ const LearnDify = ({
className,
}: LearnDifyProps) => {
const { t } = useTranslation()
const [hidden, setHidden] = useLearnDifyHiddenState()
const [isClosing, setIsClosing] = useState(false)
const [collapseTransform, setCollapseTransform] = useState<string>()
const hideTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const sectionRef = useRef<HTMLElement>(null)
useEffect(() => {
return () => {
if (hideTimerRef.current)
clearTimeout(hideTimerRef.current)
}
}, [])
const handleHide = () => {
const sectionRect = sectionRef.current?.getBoundingClientRect()
const helpTargetRect = document.querySelector('[data-learn-dify-help-target]')?.getBoundingClientRect()
if (sectionRect && helpTargetRect) {
const sectionCenterX = sectionRect.left + sectionRect.width / 2
const sectionCenterY = sectionRect.top + sectionRect.height / 2
const helpCenterX = helpTargetRect.left + helpTargetRect.width / 2
const helpCenterY = helpTargetRect.top + helpTargetRect.height / 2
setCollapseTransform(`translate3d(${helpCenterX - sectionCenterX}px, ${helpCenterY - sectionCenterY}px, 0) scale(0.08)`)
}
else {
setCollapseTransform('scale(0.08)')
}
setIsClosing(true)
hideTimerRef.current = setTimeout(() => {
setHidden(true)
setIsClosing(false)
setCollapseTransform(undefined)
}, 800)
}
if (hidden)
return null
return (
<section className={cn('px-12 pb-6', className)} aria-labelledby="learn-dify-title">
<section
ref={sectionRef}
className={cn(
'px-12 pb-6 transition-all duration-800 ease-in-out',
isClosing && 'pointer-events-none relative z-50 opacity-20',
className,
)}
style={isClosing ? { transform: collapseTransform, transformOrigin: 'center center' } : undefined}
aria-labelledby="learn-dify-title"
>
<div className="flex min-h-12 items-end justify-between gap-4 pb-2">
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-4">
<h2 id="learn-dify-title" className="min-w-0 truncate system-xl-semibold text-text-primary">
{t('learnDify.title', { ns: 'explore' })}
</h2>
<button type="button" className="shrink-0 system-sm-medium text-text-primary">
<button type="button" className="shrink-0 system-sm-medium text-text-primary" onClick={handleHide}>
{t('learnDify.hide', { ns: 'explore' })}
</button>
</div>

View File

@ -0,0 +1,39 @@
'use client'
import { useLocalStorageState } from 'ahooks'
import { useCallback, useEffect } from 'react'
export const LEARN_DIFY_HIDDEN_STORAGE_KEY = 'explore-learn-dify-hidden'
export const LEARN_DIFY_VISIBILITY_CHANGE_EVENT = 'explore-learn-dify-visibility-change'
type LearnDifyVisibilityChangeEvent = CustomEvent<{ hidden: boolean }>
export const dispatchLearnDifyVisibilityChange = (hidden: boolean) => {
window.dispatchEvent(new CustomEvent(LEARN_DIFY_VISIBILITY_CHANGE_EVENT, {
detail: { hidden },
}))
}
export const useLearnDifyHiddenState = () => {
const [rawHidden, setRawHidden] = useLocalStorageState<boolean>(LEARN_DIFY_HIDDEN_STORAGE_KEY, {
defaultValue: false,
})
useEffect(() => {
const handleVisibilityChange = (event: Event) => {
setRawHidden((event as LearnDifyVisibilityChangeEvent).detail.hidden)
}
window.addEventListener(LEARN_DIFY_VISIBILITY_CHANGE_EVENT, handleVisibilityChange)
return () => {
window.removeEventListener(LEARN_DIFY_VISIBILITY_CHANGE_EVENT, handleVisibilityChange)
}
}, [setRawHidden])
const setHidden = useCallback((nextHidden: boolean) => {
setRawHidden(nextHidden)
dispatchLearnDifyVisibilityChange(nextHidden)
}, [setRawHidden])
return [rawHidden ?? false, setHidden] as const
}

View File

@ -6,6 +6,7 @@ import type { InstalledApp } from '@/models/explore'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { Plan } from '@/app/components/billing/type'
import { LEARN_DIFY_HIDDEN_STORAGE_KEY } from '@/app/components/explore/learn-dify/storage'
import { GOTO_ANYTHING_OPEN_EVENT } from '@/app/components/goto-anything/hooks'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useAppContext } from '@/context/app-context'
@ -70,6 +71,10 @@ vi.mock('@langgenius/dify-ui/toast', async (importOriginal) => {
}
})
vi.mock('@/app/components/header/github-star', () => ({
default: ({ className }: { className?: string }) => <span className={className}>1,234</span>,
}))
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
@ -156,6 +161,7 @@ const renderMainNav = () => renderWithSystemFeatures(<MainNav />, {
describe('MainNav', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
mockPathname = '/apps'
mockInstalledApps = []
@ -335,6 +341,20 @@ describe('MainNav', () => {
window.removeEventListener(GOTO_ANYTHING_OPEN_EVENT, handleOpen)
})
it('shows hidden Learn Dify in help menu and restores it from localStorage', async () => {
localStorage.setItem(LEARN_DIFY_HIDDEN_STORAGE_KEY, 'true')
renderMainNav()
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' }))
fireEvent.click(await screen.findByText('common.mainNav.help.learnDify'))
await waitFor(() => {
expect(localStorage.getItem(LEARN_DIFY_HIDDEN_STORAGE_KEY)).toBe('false')
})
expect(mockPush).toHaveBeenCalledWith('/explore/apps')
})
it('opens workspace settings, members, provider credits, upgrade, and workspace switching actions', async () => {
renderMainNav()

View File

@ -13,6 +13,7 @@ import {
import { useSuspenseQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLearnDifyHiddenState } from '@/app/components/explore/learn-dify/storage'
import AccountAbout from '@/app/components/header/account-about'
import Compliance from '@/app/components/header/account-dropdown/compliance'
import { ExternalLinkIndicator, MenuItemContent } from '@/app/components/header/account-dropdown/menu-item-content'
@ -23,6 +24,7 @@ import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useDocLink } from '@/context/i18n'
import { env } from '@/env'
import { useRouter } from '@/next/navigation'
import { systemFeaturesQueryOptions } from '@/service/system-features'
const HelpMenu = () => {
@ -30,6 +32,8 @@ const HelpMenu = () => {
const docLink = useDocLink()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const { langGeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
const router = useRouter()
const [learnDifyHidden, setLearnDifyHidden] = useLearnDifyHiddenState()
const [aboutVisible, setAboutVisible] = useState(false)
const [open, setOpen] = useState(false)
@ -38,8 +42,9 @@ const HelpMenu = () => {
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
aria-label={t('mainNav.help.openMenu', { ns: 'common' })}
data-learn-dify-help-target
className={cn(
'flex items-center justify-center overflow-hidden rounded-full border border-components-card-border bg-components-card-bg p-0.5 text-components-main-nav-text shadow-[0px_0px_0px_1px_var(--color-components-button-button-seam)] transition-colors hover:bg-components-card-bg-alt hover:text-text-accent hover:shadow-[0px_0px_0px_1px_var(--color-components-button-button-seam),0px_1px_2px_0px_var(--color-shadow-shadow-3)]',
'text-components-main-nav-text flex items-center justify-center overflow-hidden rounded-full border border-components-card-border bg-components-card-bg p-0.5 shadow-[0px_0px_0px_1px_var(--color-components-button-button-seam)] transition-colors hover:bg-components-card-bg-alt hover:text-text-accent hover:shadow-[0px_0px_0px_1px_var(--color-components-button-button-seam),0px_1px_2px_0px_var(--color-shadow-shadow-3)]',
open && 'bg-components-card-bg-alt text-text-accent shadow-[0px_0px_0px_1px_var(--color-components-button-button-seam),0px_1px_2px_0px_var(--color-shadow-shadow-3)]',
)}
>
@ -61,6 +66,21 @@ const HelpMenu = () => {
/>
</DropdownMenuLinkItem>
<Support closeAccountDropdown={() => setOpen(false)} />
{learnDifyHidden && (
<DropdownMenuItem
className="mx-0 h-8 gap-1 px-3 py-1.5"
onClick={() => {
setLearnDifyHidden(false)
setOpen(false)
router.push('/explore/apps')
}}
>
<MenuItemContent
iconClassName="i-ri-graduation-cap-line"
label={t('mainNav.help.learnDify', { ns: 'common' })}
/>
</DropdownMenuItem>
)}
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
</DropdownMenuGroup>
<DropdownMenuSeparator className="my-0!" />

View File

@ -221,6 +221,7 @@
"license.unlimited": "Unlimited",
"loading": "Loading",
"mainNav.help.docs": "Docs",
"mainNav.help.learnDify": "Learn Dify",
"mainNav.help.openMenu": "Open help menu",
"mainNav.home": "Home",
"mainNav.integrations": "Integrations",

View File

@ -221,6 +221,7 @@
"license.unlimited": "无限制",
"loading": "加载中",
"mainNav.help.docs": "文档",
"mainNav.help.learnDify": "学习 Dify",
"mainNav.help.openMenu": "打开帮助菜单",
"mainNav.home": "首页",
"mainNav.integrations": "集成",