mirror of
https://github.com/langgenius/dify.git
synced 2026-10-29 18:07:39 +08:00
fix(web): improve main navigation responsiveness and tour focus (#39940)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1493,7 +1493,7 @@
|
||||
},
|
||||
"web/app/components/base/icons/src/vender/solid/development/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 4
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/components/base/icons/src/vender/solid/editor/index.ts": {
|
||||
@@ -1503,7 +1503,7 @@
|
||||
},
|
||||
"web/app/components/base/icons/src/vender/solid/education/index.ts": {
|
||||
"no-barrel-files/no-barrel-files": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/base/icons/src/vender/solid/files/index.ts": {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "terminal-square"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Solid",
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M8.91927 1H3.08073C2.81716 0.999992 2.58977 0.999984 2.40249 1.01529C2.20481 1.03144 2.00821 1.06709 1.81902 1.16349C1.53677 1.3073 1.3073 1.53677 1.16349 1.81902C1.06709 2.00821 1.03144 2.20481 1.01529 2.40249C0.999984 2.58977 0.999992 2.81714 1 3.08071V8.91927C0.999992 9.18284 0.999984 9.41023 1.01529 9.59752C1.03144 9.79519 1.06709 9.9918 1.16349 10.181C1.3073 10.4632 1.53677 10.6927 1.81902 10.8365C2.00821 10.9329 2.20481 10.9686 2.40249 10.9847C2.58977 11 2.81715 11 3.08072 11H8.91928C9.18285 11 9.41023 11 9.59752 10.9847C9.79519 10.9686 9.9918 10.9329 10.181 10.8365C10.4632 10.6927 10.6927 10.4632 10.8365 10.181C10.9329 9.9918 10.9686 9.79519 10.9847 9.59752C11 9.41023 11 9.18285 11 8.91928V3.08072C11 2.81715 11 2.58977 10.9847 2.40249C10.9686 2.20481 10.9329 2.00821 10.8365 1.81902C10.6927 1.53677 10.4632 1.3073 10.181 1.16349C9.9918 1.06709 9.79519 1.03144 9.59752 1.01529C9.41023 0.999984 9.18284 0.999992 8.91927 1ZM3.85355 4.14645C3.65829 3.95118 3.34171 3.95118 3.14645 4.14645C2.95118 4.34171 2.95118 4.65829 3.14645 4.85355L4.29289 6L3.14645 7.14645C2.95118 7.34171 2.95118 7.65829 3.14645 7.85355C3.34171 8.04882 3.65829 8.04882 3.85355 7.85355L5.35355 6.35355C5.54882 6.15829 5.54882 5.84171 5.35355 5.64645L3.85355 4.14645ZM6.5 7C6.22386 7 6 7.22386 6 7.5C6 7.77614 6.22386 8 6.5 8H8.5C8.77614 8 9 7.77614 9 7.5C9 7.22386 8.77614 7 8.5 7H6.5Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "TerminalSquare"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
import * as React from 'react'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import data from './TerminalSquare.json'
|
||||
|
||||
const Icon = ({
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
|
||||
}) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'TerminalSquare'
|
||||
|
||||
export default Icon
|
||||
@@ -1,5 +1,4 @@
|
||||
export { default as ApiConnection } from './ApiConnection'
|
||||
export { default as ApiConnectionMod } from './ApiConnectionMod'
|
||||
|
||||
export { default as TerminalSquare } from './TerminalSquare'
|
||||
export { default as Variable02 } from './Variable02'
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "beaker-02"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Solid",
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M4.13856 0.500003H7.8617C7.92126 0.49998 7.99238 0.499953 8.05504 0.505073C8.12765 0.511005 8.23165 0.526227 8.34062 0.581751C8.48174 0.653656 8.59648 0.768392 8.66838 0.909513C8.72391 1.01849 8.73913 1.12248 8.74506 1.19509C8.75018 1.25775 8.75015 1.32888 8.75013 1.38844V2.61157C8.75015 2.67113 8.75018 2.74226 8.74506 2.80492C8.73913 2.87753 8.72391 2.98153 8.66838 3.0905C8.59648 3.23162 8.48174 3.34636 8.34062 3.41826C8.23165 3.47379 8.12765 3.48901 8.05504 3.49494C8.03725 3.49639 8.01877 3.49743 8.00006 3.49817V5.2506C8.00006 5.55312 8.00408 5.61265 8.01723 5.66153C8.03245 5.71807 8.05747 5.7715 8.09117 5.81939C8.1203 5.86078 8.16346 5.90197 8.39586 6.09564L10.2807 7.66627C10.4566 7.81255 10.6116 7.94145 10.7267 8.10509C10.8278 8.24875 10.9029 8.40904 10.9486 8.57867C11.0005 8.7719 11.0003 8.97351 11.0001 9.2023C11.0001 9.39886 11.0002 9.59542 11.0002 9.79198C11.0003 9.98232 11.0005 10.1463 10.9713 10.2927C10.853 10.8877 10.3878 11.3529 9.7928 11.4712C9.64637 11.5003 9.48246 11.5002 9.29211 11.5001H2.70822C2.51787 11.5002 2.35396 11.5003 2.20753 11.4712C1.98473 11.4269 1.78014 11.334 1.60515 11.2038C1.42854 11.0725 1.28221 10.9034 1.17753 10.7077C1.10892 10.5796 1.05831 10.4401 1.02899 10.2927C0.999862 10.1463 0.999992 9.98233 1.00014 9.79199C1.00014 9.59542 1.00006 9.39886 1.00003 9.20229C0.999794 8.97351 0.999584 8.7719 1.05157 8.57867C1.09721 8.40904 1.17229 8.24875 1.27338 8.10509C1.38855 7.94145 1.54356 7.81255 1.71947 7.66627L3.60427 6.09564C3.83667 5.90197 3.87983 5.86078 3.90896 5.81939C3.94266 5.7715 3.96768 5.71807 3.9829 5.66153C3.99605 5.61265 4.00006 5.55312 4.00006 5.2506V3.49817C3.9814 3.49743 3.96297 3.49639 3.94521 3.49494C3.8726 3.48901 3.76861 3.47379 3.65964 3.41826C3.51851 3.34636 3.40378 3.23162 3.33187 3.0905C3.27635 2.98153 3.26113 2.87753 3.25519 2.80492C3.25008 2.74226 3.2501 2.67113 3.25013 2.61158V1.38844C3.2501 1.32888 3.25008 1.25775 3.25519 1.19509C3.26113 1.12248 3.27635 1.01849 3.33187 0.909513C3.40378 0.768392 3.51851 0.653656 3.65964 0.581751C3.76861 0.526227 3.8726 0.511005 3.94521 0.505073C4.00787 0.499953 4.079 0.49998 4.13856 0.500003ZM9.11909 8.00004H2.88104L4.28066 6.83373C4.45657 6.68745 4.61158 6.55855 4.72675 6.39491C4.82784 6.25125 4.90292 6.09096 4.94856 5.92133C5.00054 5.7281 5.00033 5.52649 5.0001 5.29771L5.00006 3.50001H7.00006L7.00003 5.29771C6.99979 5.52649 6.99958 5.7281 7.05157 5.92133C7.09721 6.09096 7.17229 6.25125 7.27338 6.39491C7.38855 6.55855 7.54356 6.68745 7.71947 6.83373L9.11909 8.00004Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Beaker02"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import type { IconData } from '@/app/components/base/icons/IconBase'
|
||||
import * as React from 'react'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import data from './Beaker02.json'
|
||||
|
||||
const Icon = ({
|
||||
ref,
|
||||
...props
|
||||
}: React.SVGProps<SVGSVGElement> & {
|
||||
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
|
||||
}) => <IconBase {...props} ref={ref} data={data as IconData} />
|
||||
|
||||
Icon.displayName = 'Beaker02'
|
||||
|
||||
export default Icon
|
||||
@@ -1,3 +1 @@
|
||||
export { default as Beaker02 } from './Beaker02'
|
||||
|
||||
export { default as Unblur } from './Unblur'
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TerminalSquare } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { Beaker02 } from '@/app/components/base/icons/src/vender/solid/education'
|
||||
import { langGeniusVersionInfoAtom } from '@/context/version-state'
|
||||
|
||||
const headerEnvClassName: { [k: string]: string } = {
|
||||
@@ -26,18 +24,14 @@ const EnvNav = () => {
|
||||
>
|
||||
{langGeniusVersionInfo.current_env === 'TESTING' && (
|
||||
<>
|
||||
<Beaker02 className="size-3" />
|
||||
<div className="ml-1 max-[1280px]:hidden">
|
||||
{t(($) => $['environment.testing'], { ns: 'common' })}
|
||||
</div>
|
||||
<span aria-hidden className="i-custom-vender-solid-education-beaker-02 size-3" />
|
||||
<div className="ml-1">{t(($) => $['environment.testing'], { ns: 'common' })}</div>
|
||||
</>
|
||||
)}
|
||||
{langGeniusVersionInfo.current_env === 'DEVELOPMENT' && (
|
||||
<>
|
||||
<TerminalSquare className="size-3" />
|
||||
<div className="ml-1 max-[1280px]:hidden">
|
||||
{t(($) => $['environment.development'], { ns: 'common' })}
|
||||
</div>
|
||||
<span aria-hidden className="i-custom-vender-solid-development-terminal-square size-3" />
|
||||
<div className="ml-1">{t(($) => $['environment.development'], { ns: 'common' })}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -715,22 +715,6 @@ describe('MainNav', () => {
|
||||
expect(accountButton).not.toHaveClass('justify-center', 'p-1')
|
||||
})
|
||||
|
||||
it('renders the desktop environment tag from the old header contract', () => {
|
||||
mockConsoleState.current = {
|
||||
...consoleState,
|
||||
langGeniusVersionInfo: {
|
||||
...consoleState.langGeniusVersionInfo,
|
||||
current_env: 'TESTING',
|
||||
},
|
||||
}
|
||||
|
||||
renderMainNav()
|
||||
|
||||
const environmentTag = screen.getByText('common.environment.testing')
|
||||
expect(environmentTag).toBeInTheDocument()
|
||||
expect(environmentTag.closest('.relative.z-30')).toHaveClass('mt-auto', 'shrink-0')
|
||||
})
|
||||
|
||||
it('does not reserve environment tag space when the environment is not shown', () => {
|
||||
const { container } = renderMainNav()
|
||||
|
||||
@@ -993,6 +977,7 @@ describe('MainNav', () => {
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem(LEARN_DIFY_HIDDEN_STORAGE_KEY)).toBe('false')
|
||||
})
|
||||
expect(screen.getByRole('menu')).toBeInTheDocument()
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -1036,7 +1021,7 @@ describe('MainNav', () => {
|
||||
).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('lets existing accounts enable Step-by-step Tour from the help menu', async () => {
|
||||
it('closes the help menu and opens Step-by-step Tour when enabling it', async () => {
|
||||
const user = userEvent.setup()
|
||||
localStorage.setItem(STEP_BY_STEP_TOUR_SHELL_MODE_STORAGE_KEY, 'collapsed')
|
||||
mockStepByStepTour.setState({
|
||||
@@ -1062,6 +1047,10 @@ describe('MainNav', () => {
|
||||
expect(localStorage.getItem(STEP_BY_STEP_TOUR_SHELL_MODE_STORAGE_KEY)).toBe('expanded')
|
||||
})
|
||||
expect(mockTrackEvent).toHaveBeenCalledWith('step_tour', { action: 'tour_enabled' })
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(await screen.findByRole('dialog', { name: 'Get to know Dify' })).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'common.mainNav.help.openMenu' }))
|
||||
expect(
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('SupportMenu', () => {
|
||||
;(mailToSupport as Mock).mockReturnValue('mailto:support@example.com')
|
||||
})
|
||||
|
||||
const renderSupportMenu = (onContactUsClick = vi.fn()) => {
|
||||
const renderSupportMenu = () => {
|
||||
const { wrapper } = createConsoleQueryWrapper({
|
||||
systemFeatures: { deployment_edition: deploymentEdition },
|
||||
})
|
||||
@@ -99,7 +99,7 @@ describe('SupportMenu', () => {
|
||||
<DropdownMenu open={true} onOpenChange={() => {}}>
|
||||
<DropdownMenuTrigger>open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<SupportMenu onContactUsClick={onContactUsClick} />
|
||||
<SupportMenu />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
{ wrapper },
|
||||
@@ -107,8 +107,7 @@ describe('SupportMenu', () => {
|
||||
}
|
||||
|
||||
it('renders contact us before community support entries when Zendesk is configured', () => {
|
||||
const onContactUsClick = vi.fn()
|
||||
renderSupportMenu(onContactUsClick)
|
||||
renderSupportMenu()
|
||||
|
||||
expect(screen.getByText('common.userProfile.contactUs')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.userProfile.forum')).toBeInTheDocument()
|
||||
@@ -126,7 +125,6 @@ describe('SupportMenu', () => {
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'common.userProfile.contactUs' }))
|
||||
|
||||
expect(openZendeskWindow).toHaveBeenCalledWith('CLOUD')
|
||||
expect(onContactUsClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders contact us with upgrade badge for Cloud sandbox plan without dedicated support', () => {
|
||||
@@ -135,8 +133,7 @@ describe('SupportMenu', () => {
|
||||
plan: { type: Plan.sandbox },
|
||||
})
|
||||
|
||||
const onContactUsClick = vi.fn()
|
||||
renderSupportMenu(onContactUsClick)
|
||||
renderSupportMenu()
|
||||
|
||||
expect(screen.getByText('common.userProfile.contactUs')).toHaveClass('text-text-disabled')
|
||||
expect(screen.getByText('billing.upgradeBtn.encourageShort')).toHaveClass(
|
||||
@@ -156,7 +153,6 @@ describe('SupportMenu', () => {
|
||||
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
||||
expect(openZendeskWindow).not.toHaveBeenCalled()
|
||||
expect(onContactUsClick).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hides upgrade contact for Cloud sandbox plan when billing is disabled', () => {
|
||||
|
||||
@@ -100,7 +100,6 @@ const HelpMenu = ({ triggerIcon = defaultTriggerIcon, triggerClassName }: HelpMe
|
||||
const disableStepByStepTour = useSetAtom(disableStepByStepTourForCurrentWorkspaceAtom)
|
||||
const setStepByStepTourShellMode = useSetStepByStepTourShellMode()
|
||||
const [aboutOpen, setAboutOpen] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const shouldShowLearnDifySwitch = systemFeatures.enable_learn_app
|
||||
const shouldShowStepByStepTourSwitch = systemFeatures.enable_step_by_step_tour
|
||||
const canToggleStepByStepTour =
|
||||
@@ -123,13 +122,9 @@ const HelpMenu = ({ triggerIcon = defaultTriggerIcon, triggerClassName }: HelpMe
|
||||
onSuccess: trackVisibilityToggled,
|
||||
})
|
||||
}
|
||||
|
||||
if (checked) setOpen(false)
|
||||
}
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
setOpen(nextOpen)
|
||||
|
||||
if (nextOpen) setSkipRecoveryVisible(false)
|
||||
}
|
||||
|
||||
@@ -137,7 +132,7 @@ const HelpMenu = ({ triggerIcon = defaultTriggerIcon, triggerClassName }: HelpMe
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenu onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t(($) => $['mainNav.help.openMenu'], { ns: 'common' })}
|
||||
data-learn-dify-help-target
|
||||
@@ -201,7 +196,7 @@ const HelpMenu = ({ triggerIcon = defaultTriggerIcon, triggerClassName }: HelpMe
|
||||
{systemFeatures.deployment_edition === 'CLOUD' && shouldShowStepByStepTourSwitch && (
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={stepByStepTourEnabled}
|
||||
closeOnClick={false}
|
||||
closeOnClick={!stepByStepTourEnabled}
|
||||
className="mx-0 h-8 gap-1 px-0 py-1 pr-2 pl-3"
|
||||
disabled={!canToggleStepByStepTour}
|
||||
onCheckedChange={handleStepByStepTourCheckedChange}
|
||||
@@ -222,7 +217,7 @@ const HelpMenu = ({ triggerIcon = defaultTriggerIcon, triggerClassName }: HelpMe
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0!" />
|
||||
<DropdownMenuGroup className="p-1">
|
||||
<SupportMenu onContactUsClick={() => setOpen(false)} />
|
||||
<SupportMenu />
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0!" />
|
||||
<DropdownMenuGroup className="p-1">
|
||||
@@ -249,10 +244,7 @@ const HelpMenu = ({ triggerIcon = defaultTriggerIcon, triggerClassName }: HelpMe
|
||||
{env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
|
||||
<DropdownMenuItem
|
||||
className="mx-0 h-8 gap-1 px-3 py-1.5"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setAboutOpen(true)
|
||||
}}
|
||||
onClick={() => setAboutOpen(true)}
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-information-2-line"
|
||||
|
||||
@@ -16,11 +16,7 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import { langGeniusVersionInfoAtom } from '@/context/version-state'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
|
||||
type SupportMenuProps = {
|
||||
onContactUsClick?: () => void
|
||||
}
|
||||
|
||||
export default function SupportMenu({ onContactUsClick }: SupportMenuProps) {
|
||||
export default function SupportMenu() {
|
||||
const { t } = useTranslation()
|
||||
const { data: deploymentEdition } = useSuspenseQuery({
|
||||
...systemFeaturesQueryOptions(),
|
||||
@@ -46,7 +42,6 @@ export default function SupportMenu({ onContactUsClick }: SupportMenuProps) {
|
||||
className="mx-0 h-8 gap-1 px-3 py-1"
|
||||
onClick={() => {
|
||||
setShowPricingModal()
|
||||
onContactUsClick?.()
|
||||
}}
|
||||
>
|
||||
<MenuItemContent
|
||||
@@ -72,7 +67,6 @@ export default function SupportMenu({ onContactUsClick }: SupportMenuProps) {
|
||||
className="mx-0 h-8 gap-1 px-3 py-1"
|
||||
onClick={() => {
|
||||
openZendeskWindow(deploymentEdition)
|
||||
onContactUsClick?.()
|
||||
}}
|
||||
>
|
||||
<MenuItemContent
|
||||
|
||||
@@ -127,13 +127,13 @@ export function MainNav({ className }: MainNavProps) {
|
||||
</nav>
|
||||
{!isCurrentWorkspaceDatasetOperator && <WebAppsSection />}
|
||||
{showEnvTag && (
|
||||
<div className="relative z-30 mt-auto shrink-0 px-3 pb-2">
|
||||
<div className="mt-auto shrink-0 px-3 pb-2">
|
||||
<EnvNav />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative w-60 shrink-0">
|
||||
<StepByStepTourMount className="absolute -top-7 left-2.5 h-8 w-45.75 overflow-visible" />
|
||||
<div className="isolate w-60 shrink-0">
|
||||
<StepByStepTourMount className="relative z-1 -mb-1 ml-2.5 h-8 w-45.75 overflow-visible" />
|
||||
<div className="flex w-60 items-center justify-between bg-linear-to-b from-background-body-transparent to-background-body to-50% py-3 pr-1 pl-3 backdrop-blur-[2px]">
|
||||
<div className="flex min-w-0 items-center gap-1 overflow-hidden">
|
||||
<AccountSection />
|
||||
|
||||
@@ -672,6 +672,9 @@ describe('StepByStepTourMount', () => {
|
||||
).toBeInTheDocument()
|
||||
|
||||
const dismissButton = screen.getByRole('button', { name: 'Dismiss' })
|
||||
await waitFor(() => {
|
||||
expect(dismissButton).toHaveFocus()
|
||||
})
|
||||
await user.click(dismissButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -761,6 +764,71 @@ describe('StepByStepTourMount', () => {
|
||||
expect(await screen.findByRole('region', { name: 'Get to know Dify' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('exposes dialog semantics and returns focus after minimizing with the keyboard', async () => {
|
||||
setStepByStepTourTestState({
|
||||
manuallyEnabledWorkspaceIds: ['workspace-1'],
|
||||
manuallyDisabledWorkspaceIds: [],
|
||||
minimized: false,
|
||||
completedTaskIds: [],
|
||||
skipped: false,
|
||||
})
|
||||
|
||||
renderStepByStepTourMount()
|
||||
|
||||
expect(
|
||||
await screen.findByRole('dialog', {
|
||||
name: 'Get to know Dify',
|
||||
description: 'A quick tour — about 5 minutes',
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
|
||||
const minimizeButton = await screen.findByRole('button', { name: 'Minimize tour' })
|
||||
minimizeButton.focus()
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
const restoreButton = await screen.findByRole('button', { name: 'Open step-by-step tour' })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(restoreButton).toHaveFocus()
|
||||
})
|
||||
expect(screen.getAllByRole('button', { name: 'Open step-by-step tour' })).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('focuses the minimize button when the checklist opens', async () => {
|
||||
setStepByStepTourTestState({
|
||||
manuallyEnabledWorkspaceIds: ['workspace-1'],
|
||||
manuallyDisabledWorkspaceIds: [],
|
||||
minimized: false,
|
||||
completedTaskIds: ['home'],
|
||||
skipped: false,
|
||||
})
|
||||
|
||||
renderStepByStepTourMount()
|
||||
|
||||
const minimizeButton = await screen.findByRole('button', { name: 'Minimize tour' })
|
||||
await waitFor(() => {
|
||||
expect(minimizeButton).toHaveFocus()
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the expanded checklist open after an outside pointer interaction', async () => {
|
||||
setStepByStepTourTestState({
|
||||
manuallyEnabledWorkspaceIds: ['workspace-1'],
|
||||
manuallyDisabledWorkspaceIds: [],
|
||||
minimized: false,
|
||||
completedTaskIds: [],
|
||||
skipped: false,
|
||||
})
|
||||
|
||||
renderStepByStepTourMount()
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: 'Get to know Dify' })
|
||||
await user.click(document.body)
|
||||
|
||||
expect(dialog).toBeInTheDocument()
|
||||
expect(localStorage.getItem(STEP_BY_STEP_TOUR_SHELL_MODE_STORAGE_KEY)).toBe('expanded')
|
||||
})
|
||||
|
||||
it('shows the completion prompt expanded even when the saved shell mode is collapsed', async () => {
|
||||
localStorage.setItem(STEP_BY_STEP_TOUR_SHELL_MODE_STORAGE_KEY, 'collapsed')
|
||||
setStepByStepTourTestState({
|
||||
@@ -942,13 +1010,13 @@ describe('StepByStepTourMount', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('completes Knowledge directly when the workspace has no Knowledge walkthrough permissions', async () => {
|
||||
it('focuses Dismiss after completing Knowledge as the final task without permission', async () => {
|
||||
mockWorkspacePermissionKeys.value = ['app.create_and_management']
|
||||
setStepByStepTourTestState({
|
||||
manuallyEnabledWorkspaceIds: ['workspace-1'],
|
||||
manuallyDisabledWorkspaceIds: [],
|
||||
minimized: false,
|
||||
completedTaskIds: ['home', 'studio'],
|
||||
completedTaskIds: ['home', 'studio', 'integration'],
|
||||
skipped: false,
|
||||
})
|
||||
|
||||
@@ -973,6 +1041,10 @@ describe('StepByStepTourMount', () => {
|
||||
await user.click(screen.getByRole('button', { name: 'Got it' }))
|
||||
|
||||
await expectStepByStepTourPatch({ action: 'complete_task', task_id: 'knowledge' })
|
||||
const dismissButton = await screen.findByRole('button', { name: 'Dismiss' })
|
||||
await waitFor(() => {
|
||||
expect(dismissButton).toHaveFocus()
|
||||
})
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import type { ComponentProps, RefObject } from 'react'
|
||||
import type { StepByStepTourTaskId, StepByStepTourTaskView } from './types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { PopoverDescription, PopoverTitle } from '@langgenius/dify-ui/popover'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export type FloatingChecklistProps = {
|
||||
className?: string
|
||||
title: string
|
||||
duration: string
|
||||
minimized: boolean
|
||||
progress: {
|
||||
ariaValueText: string
|
||||
completed: number
|
||||
@@ -23,13 +24,12 @@ export type FloatingChecklistProps = {
|
||||
title: string
|
||||
}
|
||||
tasks: StepByStepTourTaskView[]
|
||||
closeButtonRef: RefObject<HTMLButtonElement | null>
|
||||
skipLabel: string
|
||||
minimizeLabel: string
|
||||
restoreLabel: string
|
||||
getTaskCompleteLabel: (taskTitle: string) => string
|
||||
getTaskIncompleteLabel: (taskTitle: string) => string
|
||||
onMinimize: () => void
|
||||
onRestore: () => void
|
||||
onSkip: () => void
|
||||
onCompleteTask: (taskId: StepByStepTourTaskId) => void
|
||||
onStartTask: (taskId: StepByStepTourTaskId) => void
|
||||
@@ -40,34 +40,20 @@ export function FloatingChecklist({
|
||||
className,
|
||||
title,
|
||||
duration,
|
||||
minimized,
|
||||
progress,
|
||||
completionPrompt,
|
||||
tasks,
|
||||
closeButtonRef,
|
||||
skipLabel,
|
||||
minimizeLabel,
|
||||
restoreLabel,
|
||||
getTaskCompleteLabel,
|
||||
getTaskIncompleteLabel,
|
||||
onMinimize,
|
||||
onRestore,
|
||||
onSkip,
|
||||
onCompleteTask,
|
||||
onStartTask,
|
||||
onUncompleteTask,
|
||||
}: FloatingChecklistProps) {
|
||||
if (minimized && !completionPrompt) {
|
||||
return (
|
||||
<MinimizedTourPill
|
||||
title={title}
|
||||
progress={progress}
|
||||
restoreLabel={restoreLabel}
|
||||
onRestore={onRestore}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
aria-label={title}
|
||||
@@ -79,8 +65,10 @@ export function FloatingChecklist({
|
||||
<div className="flex w-full shrink-0 flex-col gap-2 px-4 pt-4 pb-1">
|
||||
<div className="flex w-full items-start gap-1">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<h2 className="system-xl-semibold text-text-secondary">{title}</h2>
|
||||
<p className="system-xs-regular text-text-tertiary">{duration}</p>
|
||||
<PopoverTitle className="system-xl-semibold text-text-secondary">{title}</PopoverTitle>
|
||||
<PopoverDescription className="system-xs-regular text-text-tertiary">
|
||||
{duration}
|
||||
</PopoverDescription>
|
||||
</div>
|
||||
{!completionPrompt && (
|
||||
<>
|
||||
@@ -93,6 +81,7 @@ export function FloatingChecklist({
|
||||
{skipLabel}
|
||||
</Button>
|
||||
<Button
|
||||
ref={closeButtonRef}
|
||||
variant="ghost"
|
||||
size="small"
|
||||
className="size-6 px-0 text-text-tertiary hover:text-text-secondary"
|
||||
@@ -122,7 +111,9 @@ export function FloatingChecklist({
|
||||
onUncompleteTask={onUncompleteTask}
|
||||
/>
|
||||
))}
|
||||
{completionPrompt && <TourCompletionPrompt {...completionPrompt} />}
|
||||
{completionPrompt && (
|
||||
<TourCompletionPrompt {...completionPrompt} closeButtonRef={closeButtonRef} />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@@ -134,12 +125,13 @@ function TourCompletionPrompt({
|
||||
label,
|
||||
onDismiss,
|
||||
title,
|
||||
}: NonNullable<FloatingChecklistProps['completionPrompt']>) {
|
||||
const dismissRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
closeButtonRef,
|
||||
}: NonNullable<FloatingChecklistProps['completionPrompt']> & {
|
||||
closeButtonRef: RefObject<HTMLButtonElement | null>
|
||||
}) {
|
||||
useEffect(() => {
|
||||
dismissRef.current?.focus({ preventScroll: true })
|
||||
}, [])
|
||||
closeButtonRef.current?.focus({ preventScroll: true })
|
||||
}, [closeButtonRef])
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -156,7 +148,7 @@ function TourCompletionPrompt({
|
||||
<h3 className="system-md-semibold text-text-primary">{title}</h3>
|
||||
<p className="mt-1 system-sm-regular text-text-tertiary">{description}</p>
|
||||
<Button
|
||||
ref={dismissRef}
|
||||
ref={closeButtonRef}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
className="mt-4 min-w-20"
|
||||
@@ -168,28 +160,23 @@ function TourCompletionPrompt({
|
||||
)
|
||||
}
|
||||
|
||||
function MinimizedTourPill({
|
||||
export function MinimizedTourPill({
|
||||
title,
|
||||
progress,
|
||||
restoreLabel,
|
||||
onRestore,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
title: string
|
||||
progress: FloatingChecklistProps['progress']
|
||||
restoreLabel: string
|
||||
onRestore: () => void
|
||||
className?: string
|
||||
}) {
|
||||
} & ComponentProps<'button'>) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type="button"
|
||||
aria-label={restoreLabel}
|
||||
className={cn(
|
||||
'inline-flex h-8 w-45.75 max-w-[calc(100vw-16px)] items-center gap-2 overflow-hidden rounded-full border-[0.5px] border-components-panel-border bg-background-section px-3 py-2 text-saas-dify-blue-inverted outline-hidden transition-colors hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
className,
|
||||
)}
|
||||
onClick={onRestore}
|
||||
>
|
||||
<span aria-hidden className="i-custom-vender-line-education-lesson-open-01 size-4 shrink-0" />
|
||||
<span className="w-26 shrink-0 truncate system-sm-medium">{title}</span>
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
} from './types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent } from '@langgenius/dify-ui/popover'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useQueryState } from 'nuqs'
|
||||
@@ -29,7 +29,7 @@ import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { getStepByStepTourPermissionVariant, trackStepByStepTourEvent } from './analytics'
|
||||
import { StepByStepTourCoachmark } from './coachmark'
|
||||
import { FloatingChecklist } from './floating-widget'
|
||||
import { FloatingChecklist, MinimizedTourPill } from './floating-widget'
|
||||
import {
|
||||
activeStepByStepTourGuideGroupAtom,
|
||||
activeStepByStepTourGuideIndexAtom,
|
||||
@@ -158,6 +158,8 @@ export default function StepByStepTourMount({ className }: StepByStepTourMountPr
|
||||
const shellMode = useStepByStepTourShellModeValue()
|
||||
const setShellMode = useSetStepByStepTourShellMode()
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const checklistCloseButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const restoreTriggerRef = useRef<HTMLButtonElement>(null)
|
||||
const lastRequestedIntegrationRouteRef = useRef<string | undefined>(undefined)
|
||||
const previousSkippedRef = useRef(skipped)
|
||||
const permissionFallbackAnalyticsKeyRef = useRef<string | undefined>(undefined)
|
||||
@@ -588,19 +590,19 @@ export default function StepByStepTourMount({ className }: StepByStepTourMountPr
|
||||
setShellMode('expanded')
|
||||
}
|
||||
|
||||
const progress = {
|
||||
ariaValueText: t(($) => $['stepByStepTour.progressAriaValueText'], {
|
||||
completed: completedAvailableTaskIds.length,
|
||||
total: availableTasks.length,
|
||||
}),
|
||||
completed: completedAvailableTaskIds.length,
|
||||
total: availableTasks.length,
|
||||
}
|
||||
const floatingChecklist = (
|
||||
<FloatingChecklist
|
||||
title={title}
|
||||
duration={t(($) => $['stepByStepTour.duration'])}
|
||||
minimized={checklistMinimized}
|
||||
progress={{
|
||||
ariaValueText: t(($) => $['stepByStepTour.progressAriaValueText'], {
|
||||
completed: completedAvailableTaskIds.length,
|
||||
total: availableTasks.length,
|
||||
}),
|
||||
completed: completedAvailableTaskIds.length,
|
||||
total: availableTasks.length,
|
||||
}}
|
||||
progress={progress}
|
||||
completionPrompt={
|
||||
completionPromptVisible
|
||||
? {
|
||||
@@ -613,9 +615,9 @@ export default function StepByStepTourMount({ className }: StepByStepTourMountPr
|
||||
: undefined
|
||||
}
|
||||
tasks={tasks}
|
||||
closeButtonRef={checklistCloseButtonRef}
|
||||
skipLabel={t(($) => $['stepByStepTour.skip'])}
|
||||
minimizeLabel={t(($) => $['stepByStepTour.minimize'])}
|
||||
restoreLabel={t(($) => $['stepByStepTour.restore'])}
|
||||
getTaskCompleteLabel={(taskTitle) =>
|
||||
t(($) => $['stepByStepTour.markTaskComplete'], { title: taskTitle })
|
||||
}
|
||||
@@ -625,9 +627,6 @@ export default function StepByStepTourMount({ className }: StepByStepTourMountPr
|
||||
onMinimize={() => {
|
||||
setShellMode('collapsed')
|
||||
}}
|
||||
onRestore={() => {
|
||||
setShellMode('expanded')
|
||||
}}
|
||||
onSkip={skipTour}
|
||||
onCompleteTask={(taskId) => {
|
||||
const guides = getStepByStepTourGuides(taskId, sessionGuideGroup)
|
||||
@@ -730,13 +729,24 @@ export default function StepByStepTourMount({ className }: StepByStepTourMountPr
|
||||
/>
|
||||
)}
|
||||
{visible && (!allTasksCompleted || completionPromptVisible) && (
|
||||
<Popover open={overlayVisible && expanded}>
|
||||
<Popover
|
||||
open={overlayVisible && expanded}
|
||||
onOpenChange={(open) => {
|
||||
if (open) setShellMode('expanded')
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={anchorRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute bottom-0 left-0 h-0 w-full"
|
||||
/>
|
||||
{checklistMinimized && floatingChecklist}
|
||||
{checklistMinimized && (
|
||||
<PopoverTrigger
|
||||
ref={restoreTriggerRef}
|
||||
aria-label={t(($) => $['stepByStepTour.restore'])}
|
||||
render={(props) => <MinimizedTourPill {...props} title={title} progress={progress} />}
|
||||
/>
|
||||
)}
|
||||
{overlayVisible && (
|
||||
<PopoverContent
|
||||
placement="top-start"
|
||||
@@ -751,6 +761,10 @@ export default function StepByStepTourMount({ className }: StepByStepTourMountPr
|
||||
},
|
||||
}}
|
||||
popupClassName="overflow-visible rounded-none border-0 bg-transparent p-0 shadow-none"
|
||||
popupProps={{
|
||||
initialFocus: checklistCloseButtonRef,
|
||||
finalFocus: restoreTriggerRef,
|
||||
}}
|
||||
>
|
||||
{floatingChecklist}
|
||||
</PopoverContent>
|
||||
|
||||
Reference in New Issue
Block a user