mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
feat: hide learn dify anim effect
This commit is contained in:
parent
374eca2504
commit
d340b6d168
@ -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', () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
39
web/app/components/explore/learn-dify/storage.ts
Normal file
39
web/app/components/explore/learn-dify/storage.ts
Normal 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
|
||||
}
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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!" />
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -221,6 +221,7 @@
|
||||
"license.unlimited": "无限制",
|
||||
"loading": "加载中",
|
||||
"mainNav.help.docs": "文档",
|
||||
"mainNav.help.learnDify": "学习 Dify",
|
||||
"mainNav.help.openMenu": "打开帮助菜单",
|
||||
"mainNav.home": "首页",
|
||||
"mainNav.integrations": "集成",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user