diff --git a/web/app/components/explore/app-list/__tests__/index.spec.tsx b/web/app/components/explore/app-list/__tests__/index.spec.tsx index 7598a2c2ca..dd1b5e3dce 100644 --- a/web/app/components/explore/app-list/__tests__/index.spec.tsx +++ b/web/app/components/explore/app-list/__tests__/index.spec.tsx @@ -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', () => { diff --git a/web/app/components/explore/learn-dify/index.tsx b/web/app/components/explore/learn-dify/index.tsx index 83184664bc..e30a1703d4 100644 --- a/web/app/components/explore/learn-dify/index.tsx +++ b/web/app/components/explore/learn-dify/index.tsx @@ -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() + const hideTimerRef = useRef | null>(null) + const sectionRef = useRef(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 ( -
+

{t('learnDify.title', { ns: 'explore' })}

-
diff --git a/web/app/components/explore/learn-dify/storage.ts b/web/app/components/explore/learn-dify/storage.ts new file mode 100644 index 0000000000..9f37c31615 --- /dev/null +++ b/web/app/components/explore/learn-dify/storage.ts @@ -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(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 +} diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index f594f2f7ca..eca37f00cf 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -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 }) => 1,234, +})) + vi.mock('@/context/i18n', () => ({ useLocale: () => 'en-US', useDocLink: () => (path: string) => `https://docs.dify.ai${path}`, @@ -156,6 +161,7 @@ const renderMainNav = () => renderWithSystemFeatures(, { 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() diff --git a/web/app/components/main-nav/components/help-menu.tsx b/web/app/components/main-nav/components/help-menu.tsx index b9e54c2ce7..18e399644b 100644 --- a/web/app/components/main-nav/components/help-menu.tsx +++ b/web/app/components/main-nav/components/help-menu.tsx @@ -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 = () => { @@ -61,6 +66,21 @@ const HelpMenu = () => { /> setOpen(false)} /> + {learnDifyHidden && ( + { + setLearnDifyHidden(false) + setOpen(false) + router.push('/explore/apps') + }} + > + + + )} {IS_CLOUD_EDITION && isCurrentWorkspaceOwner && } diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index 9d363366af..5442dab625 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -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", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index b0d1c23332..bcc6857c82 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -221,6 +221,7 @@ "license.unlimited": "无限制", "loading": "加载中", "mainNav.help.docs": "文档", + "mainNav.help.learnDify": "学习 Dify", "mainNav.help.openMenu": "打开帮助菜单", "mainNav.home": "首页", "mainNav.integrations": "集成",