mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
fix(web): align main nav app item states (#38326)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jingyi-Dify <jingyi.qi@dify.ai>
This commit is contained in:
parent
2f0c6c10bd
commit
57f687fb45
@ -3299,14 +3299,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/explore/sidebar/app-nav-item/index.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/explore/try-app/app/text-generation.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
|
||||
@ -204,12 +204,5 @@ describe('Sidebar Lifecycle Flow', () => {
|
||||
|
||||
expect(screen.getByText('explore.sidebar.noApps.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide NoApps on mobile', () => {
|
||||
mockMediaType = MediaType.mobile
|
||||
renderSidebar()
|
||||
|
||||
expect(screen.queryByText('explore.sidebar.noApps.title')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { expectLoadingButton } from '@/test/button'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import SideBar from '../index'
|
||||
@ -16,7 +15,6 @@ const mockUpdatePinStatus = vi.fn()
|
||||
let mockIsPending = false
|
||||
let mockIsUninstallPending = false
|
||||
let mockInstalledApps: InstalledApp[] = []
|
||||
let mockMediaType: string = MediaType.pc
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
usePathname: () => '/',
|
||||
@ -26,15 +24,6 @@ vi.mock('@/next/navigation', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-breakpoints', () => ({
|
||||
default: () => mockMediaType,
|
||||
MediaType: {
|
||||
mobile: 'mobile',
|
||||
tablet: 'tablet',
|
||||
pc: 'pc',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-explore', () => ({
|
||||
useGetInstalledApps: () => ({
|
||||
isPending: mockIsPending,
|
||||
@ -87,7 +76,6 @@ describe('SideBar', () => {
|
||||
mockIsPending = false
|
||||
mockIsUninstallPending = false
|
||||
mockInstalledApps = []
|
||||
mockMediaType = MediaType.pc
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
@ -97,13 +85,6 @@ describe('SideBar', () => {
|
||||
expect(screen.getByText('explore.sidebar.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose an accessible name for the discovery link when the text is hidden', () => {
|
||||
mockMediaType = MediaType.mobile
|
||||
renderSideBar()
|
||||
|
||||
expect(screen.getByRole('link', { name: 'explore.sidebar.title' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render workspace items when installed apps exist', () => {
|
||||
mockInstalledApps = [createInstalledApp()]
|
||||
renderSideBar()
|
||||
@ -156,6 +137,17 @@ describe('SideBar', () => {
|
||||
|
||||
expect(screen.getByRole('button', { name: 'layout.sidebar.expandSidebar' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render icon-only content when folded', () => {
|
||||
mockInstalledApps = [createInstalledApp()]
|
||||
renderSideBar()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'layout.sidebar.collapseSidebar' }))
|
||||
|
||||
expect(screen.getByRole('link', { name: 'explore.sidebar.title' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'My App' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'layout.sidebar.expandSidebar' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
@ -229,14 +221,4 @@ describe('SideBar', () => {
|
||||
expectLoadingButton(screen.getByText('common.operation.confirm').closest('button'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should hide NoApps and app names on mobile', () => {
|
||||
mockMediaType = MediaType.mobile
|
||||
renderSideBar()
|
||||
|
||||
expect(screen.queryByText('explore.sidebar.noApps.title')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('explore.sidebar.webApps')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,7 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import AppNavItem from '../index'
|
||||
|
||||
const baseProps = {
|
||||
isMobile: false,
|
||||
name: 'My App',
|
||||
id: 'app-123',
|
||||
icon_type: 'emoji' as const,
|
||||
@ -22,18 +21,12 @@ describe('AppNavItem', () => {
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render name and item operation on desktop', () => {
|
||||
it('should render name and item operation when expanded', () => {
|
||||
render(<AppNavItem {...baseProps} />)
|
||||
|
||||
expect(screen.getByText('My App')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('item-operation-trigger')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide name on mobile', () => {
|
||||
render(<AppNavItem {...baseProps} isMobile />)
|
||||
|
||||
expect(screen.queryByText('My App')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
@ -43,6 +36,26 @@ describe('AppNavItem', () => {
|
||||
const link = screen.getByRole('link', { name: 'My App' })
|
||||
|
||||
expect(link).toHaveAttribute('href', '/installed/app-123')
|
||||
expect(link).toHaveAttribute('aria-label', 'My App')
|
||||
expect(link).not.toHaveAttribute('aria-current')
|
||||
})
|
||||
|
||||
it('should use a contextual accessible name when ariaLabel is provided', () => {
|
||||
render(<AppNavItem {...baseProps} variant="mainNav" ariaLabel="Open My App web app" />)
|
||||
|
||||
const link = screen.getByRole('link', { name: 'Open My App web app' })
|
||||
|
||||
expect(link).toHaveAttribute('href', '/installed/app-123')
|
||||
expect(link).toHaveAttribute('aria-label', 'Open My App web app')
|
||||
expect(screen.getByText('My App')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should expose selected state through the current link', () => {
|
||||
render(<AppNavItem {...baseProps} isSelected />)
|
||||
|
||||
const link = screen.getByRole('link', { name: 'My App' })
|
||||
|
||||
expect(link).toHaveAttribute('aria-current', 'page')
|
||||
})
|
||||
|
||||
it('should call onDelete with app id when delete action is clicked', async () => {
|
||||
|
||||
@ -9,9 +9,9 @@ import ItemOperation from '@/app/components/explore/item-operation'
|
||||
import Link from '@/next/link'
|
||||
|
||||
type IAppNavItemProps = {
|
||||
isMobile: boolean
|
||||
variant?: 'default' | 'mainNav'
|
||||
name: string
|
||||
ariaLabel?: string
|
||||
id: string
|
||||
icon_type: AppIconType | null
|
||||
icon: string
|
||||
@ -25,9 +25,9 @@ type IAppNavItemProps = {
|
||||
}
|
||||
|
||||
export default function AppNavItem({
|
||||
isMobile,
|
||||
variant = 'default',
|
||||
name,
|
||||
ariaLabel,
|
||||
id,
|
||||
icon_type,
|
||||
icon,
|
||||
@ -47,43 +47,31 @@ export default function AppNavItem({
|
||||
key={id}
|
||||
className={cn(
|
||||
isMainNav
|
||||
? 'group flex h-8 items-center justify-between gap-2 rounded-lg py-0.5 pr-0.5 pl-2 transition-colors has-[>a:focus-visible]:inset-ring-2 has-[>a:focus-visible]:inset-ring-state-accent-solid'
|
||||
: 'group flex h-8 items-center justify-between rounded-lg px-2 system-sm-medium text-sm font-normal text-components-menu-item-text has-[>a:focus-visible]:inset-ring-2 has-[>a:focus-visible]:inset-ring-state-accent-solid mobile:justify-center mobile:px-1',
|
||||
isMainNav
|
||||
? (isSelected ? 'bg-state-base-hover' : 'hover:bg-state-base-hover')
|
||||
: (isSelected ? 'bg-state-base-active text-components-menu-item-text-active' : 'hover:bg-state-base-hover hover:text-components-menu-item-text-hover'),
|
||||
? 'group flex h-8 items-center justify-between gap-2 rounded-lg py-0.5 pr-0.5 pl-2 transition-colors not-has-[>a[aria-current=page]]:hover:bg-state-base-hover has-[>a:focus-visible]:inset-ring-2 has-[>a:focus-visible]:inset-ring-state-accent-solid has-[>a[aria-current=page]]:bg-state-base-active'
|
||||
: cn(
|
||||
'group flex h-8 items-center rounded-lg system-sm-medium text-sm font-normal text-components-menu-item-text transition-colors not-has-[>a[aria-current=page]]:hover:bg-state-base-hover not-has-[>a[aria-current=page]]:hover:text-components-menu-item-text-hover has-[>a:focus-visible]:inset-ring-2 has-[>a:focus-visible]:inset-ring-state-accent-solid has-[>a[aria-current=page]]:bg-state-base-active has-[>a[aria-current=page]]:text-components-menu-item-text-active',
|
||||
'w-full justify-start px-2 group-data-[folded=true]/sidebar:justify-center group-data-[folded=true]/sidebar:px-1',
|
||||
),
|
||||
)}
|
||||
>
|
||||
{isMobile && (
|
||||
<Link
|
||||
href={url}
|
||||
aria-label={name}
|
||||
title={name}
|
||||
className="flex min-w-0 flex-1 items-center justify-center outline-hidden"
|
||||
>
|
||||
<AppIcon size="tiny" iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />
|
||||
</Link>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<>
|
||||
<Link
|
||||
href={url}
|
||||
title={name}
|
||||
className={cn(isMainNav ? 'flex min-w-0 flex-1 items-center gap-2 outline-hidden' : 'flex w-0 grow items-center space-x-2 outline-hidden')}
|
||||
>
|
||||
<AppIcon size="tiny" className={cn(isMainNav && 'size-5 rounded-md text-sm')} iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />
|
||||
<div className={cn(isMainNav ? 'min-w-0 flex-1 truncate py-1 pr-1 system-sm-regular' : 'truncate system-sm-regular text-components-menu-item-text')} title={name}>{name}</div>
|
||||
</Link>
|
||||
<div className="h-6 shrink-0" onClick={e => e.stopPropagation()}>
|
||||
<ItemOperation
|
||||
isPinned={isPinned}
|
||||
togglePin={togglePin}
|
||||
isShowDelete={!uninstallable && !isSelected}
|
||||
onDelete={() => onDelete(id)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href={url}
|
||||
aria-current={isSelected ? 'page' : undefined}
|
||||
aria-label={ariaLabel ?? name}
|
||||
title={name}
|
||||
className={cn(isMainNav ? 'flex min-w-0 flex-1 items-center gap-2 outline-hidden' : 'flex w-0 grow items-center space-x-2 outline-hidden group-data-[folded=true]/sidebar:w-auto group-data-[folded=true]/sidebar:justify-center group-data-[folded=true]/sidebar:space-x-0')}
|
||||
>
|
||||
<AppIcon size="tiny" className={cn(isMainNav && 'size-5 rounded-md text-sm')} iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />
|
||||
<div className={cn(isMainNav ? 'min-w-0 flex-1 truncate py-1 pr-1 system-sm-regular' : 'truncate system-sm-regular text-components-menu-item-text group-data-[folded=true]/sidebar:hidden')} title={name}>{name}</div>
|
||||
</Link>
|
||||
<div className={cn(isMainNav ? 'h-6 shrink-0' : 'h-6 shrink-0 group-data-[folded=true]/sidebar:hidden')}>
|
||||
<ItemOperation
|
||||
isPinned={isPinned}
|
||||
togglePin={togglePin}
|
||||
isShowDelete={!uninstallable && !isSelected}
|
||||
onDelete={() => onDelete(id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -16,7 +16,6 @@ import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import Link from '@/next/link'
|
||||
import { usePathname, useSelectedLayoutSegments } from '@/next/navigation'
|
||||
import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore'
|
||||
@ -34,8 +33,6 @@ const SideBar = () => {
|
||||
const { mutateAsync: uninstallApp, isPending: isUninstalling } = useUninstallApp()
|
||||
const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus()
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const [isFold, {
|
||||
toggle: toggleIsFold,
|
||||
}] = useBoolean(false)
|
||||
@ -55,12 +52,10 @@ const SideBar = () => {
|
||||
}
|
||||
|
||||
const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length
|
||||
const shouldUseExpandedScrollArea = !isMobile && !isFold
|
||||
const webAppsLabelId = React.useId()
|
||||
const installedAppItems = installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }, index) => (
|
||||
<React.Fragment key={id}>
|
||||
<Item
|
||||
isMobile={isMobile || isFold}
|
||||
name={name}
|
||||
icon_type={icon_type}
|
||||
icon={icon}
|
||||
@ -81,21 +76,24 @@ const SideBar = () => {
|
||||
))
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full w-fit shrink-0 cursor-pointer flex-col px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}>
|
||||
<div
|
||||
data-folded={isFold ? 'true' : undefined}
|
||||
className={cn('group/sidebar flex h-full w-fit shrink-0 cursor-pointer flex-col px-3 pt-6 sm:w-[240px]', isFold && 'sm:w-[56px]')}
|
||||
>
|
||||
<div className={cn(isDiscoverySelected ? 'text-text-accent' : 'text-text-tertiary')}>
|
||||
<Link
|
||||
href="/"
|
||||
aria-label={isMobile || isFold ? t('sidebar.title', { ns: 'explore' }) : undefined}
|
||||
className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover', 'flex h-8 items-center gap-2 rounded-lg px-1 mobile:w-fit mobile:justify-center pc:w-full pc:justify-start')}
|
||||
aria-label={isFold ? t('sidebar.title', { ns: 'explore' }) : undefined}
|
||||
className={cn(isDiscoverySelected ? 'bg-state-base-active' : 'hover:bg-state-base-hover', 'flex h-8 w-full items-center justify-start gap-2 rounded-lg px-1')}
|
||||
>
|
||||
<div className="flex size-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid">
|
||||
<span aria-hidden="true" className="i-ri-apps-fill size-3.5 text-components-avatar-shape-fill-stop-100" />
|
||||
</div>
|
||||
{!isMobile && !isFold && <div className={cn('truncate', isDiscoverySelected ? 'system-sm-semibold text-components-menu-item-text-active' : 'system-sm-regular text-components-menu-item-text')}>{t('sidebar.title', { ns: 'explore' })}</div>}
|
||||
{!isFold && <div className={cn('truncate', isDiscoverySelected ? 'system-sm-semibold text-components-menu-item-text-active' : 'system-sm-regular text-components-menu-item-text')}>{t('sidebar.title', { ns: 'explore' })}</div>}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{!isPending && installedApps.length === 0 && !isMobile && !isFold
|
||||
{!isPending && installedApps.length === 0 && !isFold
|
||||
&& (
|
||||
<div className="mt-5">
|
||||
<NoApps />
|
||||
@ -104,8 +102,8 @@ const SideBar = () => {
|
||||
|
||||
{installedApps.length > 0 && (
|
||||
<div className="mt-5 flex min-h-0 flex-1 flex-col">
|
||||
{!isMobile && !isFold && <p id={webAppsLabelId} className="mb-1.5 pl-2 system-xs-medium-uppercase break-all text-text-tertiary uppercase mobile:px-0">{t('sidebar.webApps', { ns: 'explore' })}</p>}
|
||||
{shouldUseExpandedScrollArea
|
||||
{!isFold && <p id={webAppsLabelId} className="mb-1.5 pl-2 system-xs-medium-uppercase break-all text-text-tertiary uppercase">{t('sidebar.webApps', { ns: 'explore' })}</p>}
|
||||
{!isFold
|
||||
? (
|
||||
<div className="min-h-0 flex-1">
|
||||
<ScrollArea
|
||||
@ -130,22 +128,20 @@ const SideBar = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<div className="mt-auto flex py-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isFold ? t('sidebar.expandSidebar', { ns: 'layout' }) : t('sidebar.collapseSidebar', { ns: 'layout' })}
|
||||
className="flex size-8 items-center justify-center rounded-lg text-text-tertiary transition-colors hover:bg-state-base-hover focus-visible:inset-ring-1 focus-visible:inset-ring-components-input-border-hover focus-visible:outline-hidden"
|
||||
onClick={toggleIsFold}
|
||||
>
|
||||
{isFold
|
||||
? <span aria-hidden="true" className="i-ri-expand-right-line" />
|
||||
: (
|
||||
<span aria-hidden="true" className="i-ri-layout-left-2-line" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-auto flex py-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={isFold ? t('sidebar.expandSidebar', { ns: 'layout' }) : t('sidebar.collapseSidebar', { ns: 'layout' })}
|
||||
className="flex size-8 items-center justify-center rounded-lg text-text-tertiary transition-colors hover:bg-state-base-hover focus-visible:inset-ring-1 focus-visible:inset-ring-components-input-border-hover focus-visible:outline-hidden"
|
||||
onClick={toggleIsFold}
|
||||
>
|
||||
{isFold
|
||||
? <span aria-hidden="true" className="i-ri-expand-right-line" />
|
||||
: (
|
||||
<span aria-hidden="true" className="i-ri-layout-left-2-line" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={showConfirm} onOpenChange={setShowConfirm}>
|
||||
<AlertDialogContent>
|
||||
|
||||
@ -26,10 +26,6 @@ import { useProviderContext } from '@/context/provider-context'
|
||||
import Link from '@/next/link'
|
||||
import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content'
|
||||
|
||||
const mainNavMenuGroupClassName = 'p-1'
|
||||
const mainNavMenuItemClassName = 'mx-0 h-8 gap-1 px-3 py-1'
|
||||
const mainNavMenuSubPopupClassName = 'w-60 max-h-[360px] bg-components-panel-bg-blur! p-1! backdrop-blur-[5px]'
|
||||
|
||||
type MainNavRadioItemContentProps = {
|
||||
iconClassName?: string
|
||||
label: ReactNode
|
||||
@ -56,7 +52,7 @@ function AppearanceSubmenu() {
|
||||
|
||||
return (
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className={mainNavMenuItemClassName}>
|
||||
<DropdownMenuSubTrigger className="mx-0 h-8 gap-1 px-3 py-1">
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-sun-line"
|
||||
label={t('account.appearanceLabel', { ns: 'common' })}
|
||||
@ -65,16 +61,16 @@ function AppearanceSubmenu() {
|
||||
<DropdownMenuSubContent
|
||||
placement="right-start"
|
||||
sideOffset={6}
|
||||
popupClassName={mainNavMenuSubPopupClassName}
|
||||
popupClassName="w-[139px] max-h-[360px] bg-components-panel-bg-blur p-1 backdrop-blur-[5px]"
|
||||
>
|
||||
<DropdownMenuRadioGroup value={theme || 'system'} onValueChange={value => setTheme(value as Theme)}>
|
||||
<DropdownMenuRadioItem value="light" closeOnClick className={mainNavMenuItemClassName}>
|
||||
<DropdownMenuRadioItem value="light" closeOnClick className="mx-0 h-8 gap-1 px-2 py-1">
|
||||
<MainNavRadioItemContent iconClassName="i-ri-sun-line" label={t('account.appearanceLight', { ns: 'common' })} />
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="dark" closeOnClick className={mainNavMenuItemClassName}>
|
||||
<DropdownMenuRadioItem value="dark" closeOnClick className="mx-0 h-8 gap-1 px-2 py-1">
|
||||
<MainNavRadioItemContent iconClassName="i-ri-moon-line" label={t('account.appearanceDark', { ns: 'common' })} />
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="system" closeOnClick className={mainNavMenuItemClassName}>
|
||||
<DropdownMenuRadioItem value="system" closeOnClick className="mx-0 h-8 gap-1 px-2 py-1">
|
||||
<MainNavRadioItemContent iconClassName="i-ri-computer-line" label={t('account.appearanceSystem', { ns: 'common' })} />
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
@ -97,7 +93,7 @@ export function MainNavMenuContent({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
|
||||
<DropdownMenuGroup className="p-1">
|
||||
<div className="flex items-center gap-3 rounded-xl bg-gradient-to-b from-background-section-burn to-background-section p-3">
|
||||
<div className="flex min-w-0 grow flex-col gap-1">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
@ -114,9 +110,9 @@ export function MainNavMenuContent({
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size="lg" className="shrink-0" />
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
|
||||
<DropdownMenuGroup className="p-1">
|
||||
<DropdownMenuLinkItem
|
||||
className={cn('justify-between', mainNavMenuItemClassName)}
|
||||
className="mx-0 h-8 justify-between gap-1 px-3 py-1"
|
||||
render={<Link href="/account" />}
|
||||
>
|
||||
<MenuItemContent
|
||||
@ -126,7 +122,7 @@ export function MainNavMenuContent({
|
||||
/>
|
||||
</DropdownMenuLinkItem>
|
||||
<DropdownMenuItem
|
||||
className={mainNavMenuItemClassName}
|
||||
className="mx-0 h-8 gap-1 px-3 py-1"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
|
||||
>
|
||||
<MenuItemContent
|
||||
@ -137,9 +133,9 @@ export function MainNavMenuContent({
|
||||
<AppearanceSubmenu />
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
<DropdownMenuGroup className={mainNavMenuGroupClassName}>
|
||||
<DropdownMenuGroup className="p-1">
|
||||
<DropdownMenuItem
|
||||
className={mainNavMenuItemClassName}
|
||||
className="mx-0 h-8 gap-1 px-3 py-1"
|
||||
onClick={() => {
|
||||
void onLogout()
|
||||
}}
|
||||
|
||||
@ -782,7 +782,8 @@ describe('MainNav', () => {
|
||||
})
|
||||
|
||||
expect(screen.queryByText('Alpha App')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'Beta Tool' })).toHaveAttribute('href', '/installed/installed-2')
|
||||
expect(screen.getByText('Beta Tool')).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: 'common.mainNav.webApps.openApp:{"name":"Beta Tool"}' })).toHaveAttribute('href', '/installed/installed-2')
|
||||
})
|
||||
|
||||
it('renders web app skeleton rows while installed apps are loading', () => {
|
||||
|
||||
@ -148,8 +148,8 @@ const WebAppsSectionContent = () => {
|
||||
<AppNavItem
|
||||
key={id}
|
||||
variant="mainNav"
|
||||
isMobile={false}
|
||||
name={app.name}
|
||||
ariaLabel={t('mainNav.webApps.openApp', { ns: 'common', name: app.name })}
|
||||
icon_type={app.icon_type}
|
||||
icon={app.icon}
|
||||
icon_background={app.icon_background}
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "التكاملات",
|
||||
"mainNav.marketplace": "سوق الإضافات",
|
||||
"mainNav.webApps.noResults": "لم يتم العثور على تطبيقات ويب",
|
||||
"mainNav.webApps.openApp": "فتح تطبيق الويب {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "البحث في تطبيقات الويب",
|
||||
"mainNav.workspace.credits": "{{count}} رصيد",
|
||||
"mainNav.workspace.creditsUnit": "أرصدة",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integrationen",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Keine Web-Apps gefunden",
|
||||
"mainNav.webApps.openApp": "Web-App {{name}} öffnen",
|
||||
"mainNav.webApps.searchPlaceholder": "Web-Apps suchen",
|
||||
"mainNav.workspace.credits": "{{count}} Credits",
|
||||
"mainNav.workspace.creditsUnit": "Credits",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integrations",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "No web apps found",
|
||||
"mainNav.webApps.openApp": "Open {{name}} web app",
|
||||
"mainNav.webApps.searchPlaceholder": "Search web apps",
|
||||
"mainNav.workspace.credits": "{{count}} credits",
|
||||
"mainNav.workspace.creditsUnit": "credits",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integraciones",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "No se encontraron aplicaciones web",
|
||||
"mainNav.webApps.openApp": "Abrir la aplicación web {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Buscar aplicaciones web",
|
||||
"mainNav.workspace.credits": "{{count}} créditos",
|
||||
"mainNav.workspace.creditsUnit": "créditos",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "یکپارچهسازیها",
|
||||
"mainNav.marketplace": "بازارچه",
|
||||
"mainNav.webApps.noResults": "هیچ برنامه وبی یافت نشد",
|
||||
"mainNav.webApps.openApp": "باز کردن برنامه وب {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "جستجوی برنامههای وب",
|
||||
"mainNav.workspace.credits": "{{count}} اعتبار",
|
||||
"mainNav.workspace.creditsUnit": "اعتبار",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Intégrations",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Aucune application web trouvée",
|
||||
"mainNav.webApps.openApp": "Ouvrir l’application web {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Rechercher des applications web",
|
||||
"mainNav.workspace.credits": "{{count}} crédits",
|
||||
"mainNav.workspace.creditsUnit": "crédits",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "इंटीग्रेशन",
|
||||
"mainNav.marketplace": "मार्केटप्लेस",
|
||||
"mainNav.webApps.noResults": "कोई वेब ऐप नहीं मिला",
|
||||
"mainNav.webApps.openApp": "{{name}} वेब ऐप खोलें",
|
||||
"mainNav.webApps.searchPlaceholder": "वेब ऐप खोजें",
|
||||
"mainNav.workspace.credits": "{{count}} क्रेडिट",
|
||||
"mainNav.workspace.creditsUnit": "क्रेडिट",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integrasi",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Tidak ada aplikasi web ditemukan",
|
||||
"mainNav.webApps.openApp": "Buka aplikasi web {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Cari aplikasi web",
|
||||
"mainNav.workspace.credits": "{{count}} kredit",
|
||||
"mainNav.workspace.creditsUnit": "kredit",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integrazioni",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Nessuna app web trovata",
|
||||
"mainNav.webApps.openApp": "Apri l'app web {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Cerca app web",
|
||||
"mainNav.workspace.credits": "{{count}} crediti",
|
||||
"mainNav.workspace.creditsUnit": "crediti",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "連携",
|
||||
"mainNav.marketplace": "マーケットプレイス",
|
||||
"mainNav.webApps.noResults": "Webアプリが見つかりません",
|
||||
"mainNav.webApps.openApp": "{{name}} のWebアプリを開く",
|
||||
"mainNav.webApps.searchPlaceholder": "Webアプリを検索",
|
||||
"mainNav.workspace.credits": "{{count}} クレジット",
|
||||
"mainNav.workspace.creditsUnit": "クレジット",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "연동",
|
||||
"mainNav.marketplace": "마켓플레이스",
|
||||
"mainNav.webApps.noResults": "웹 앱을 찾을 수 없습니다",
|
||||
"mainNav.webApps.openApp": "{{name}} 웹 앱 열기",
|
||||
"mainNav.webApps.searchPlaceholder": "웹 앱 검색",
|
||||
"mainNav.workspace.credits": "{{count}} 크레딧",
|
||||
"mainNav.workspace.creditsUnit": "크레딧",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integraties",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Geen webapps gevonden",
|
||||
"mainNav.webApps.openApp": "Webapp {{name}} openen",
|
||||
"mainNav.webApps.searchPlaceholder": "Webapps zoeken",
|
||||
"mainNav.workspace.credits": "{{count}} credits",
|
||||
"mainNav.workspace.creditsUnit": "credits",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integracje",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Nie znaleziono aplikacji webowych",
|
||||
"mainNav.webApps.openApp": "Otwórz aplikację webową {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Szukaj aplikacji webowych",
|
||||
"mainNav.workspace.credits": "{{count}} kredytów",
|
||||
"mainNav.workspace.creditsUnit": "kredyty",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integrações",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Nenhum app web encontrado",
|
||||
"mainNav.webApps.openApp": "Abrir o app web {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Pesquisar apps web",
|
||||
"mainNav.workspace.credits": "{{count}} créditos",
|
||||
"mainNav.workspace.creditsUnit": "créditos",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integrări",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Nu s-au găsit aplicații web",
|
||||
"mainNav.webApps.openApp": "Deschide aplicația web {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Caută aplicații web",
|
||||
"mainNav.workspace.credits": "{{count}} credite",
|
||||
"mainNav.workspace.creditsUnit": "credite",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Интеграции",
|
||||
"mainNav.marketplace": "Маркетплейс",
|
||||
"mainNav.webApps.noResults": "Веб-приложения не найдены",
|
||||
"mainNav.webApps.openApp": "Открыть веб-приложение {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Поиск веб-приложений",
|
||||
"mainNav.workspace.credits": "{{count}} кредитов",
|
||||
"mainNav.workspace.creditsUnit": "кредиты",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Integracije",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Ni najdenih spletnih aplikacij",
|
||||
"mainNav.webApps.openApp": "Odpri spletno aplikacijo {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Išči spletne aplikacije",
|
||||
"mainNav.workspace.credits": "{{count}} kreditov",
|
||||
"mainNav.workspace.creditsUnit": "krediti",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "การผสานรวม",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "ไม่พบเว็บแอป",
|
||||
"mainNav.webApps.openApp": "เปิดเว็บแอป {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "ค้นหาเว็บแอป",
|
||||
"mainNav.workspace.credits": "{{count}} เครดิต",
|
||||
"mainNav.workspace.creditsUnit": "เครดิต",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Entegrasyonlar",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Web uygulaması bulunamadı",
|
||||
"mainNav.webApps.openApp": "{{name}} web uygulamasını aç",
|
||||
"mainNav.webApps.searchPlaceholder": "Web uygulamalarında ara",
|
||||
"mainNav.workspace.credits": "{{count}} kredi",
|
||||
"mainNav.workspace.creditsUnit": "kredi",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Інтеграції",
|
||||
"mainNav.marketplace": "Маркетплейс",
|
||||
"mainNav.webApps.noResults": "Вебзастосунки не знайдено",
|
||||
"mainNav.webApps.openApp": "Відкрити вебзастосунок {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Пошук вебзастосунків",
|
||||
"mainNav.workspace.credits": "{{count}} кредитів",
|
||||
"mainNav.workspace.creditsUnit": "кредити",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "Tích hợp",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "Không tìm thấy ứng dụng web",
|
||||
"mainNav.webApps.openApp": "Mở ứng dụng web {{name}}",
|
||||
"mainNav.webApps.searchPlaceholder": "Tìm kiếm ứng dụng web",
|
||||
"mainNav.workspace.credits": "{{count}} tín dụng",
|
||||
"mainNav.workspace.creditsUnit": "tín dụng",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "集成",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "未找到 Web 应用",
|
||||
"mainNav.webApps.openApp": "打开 {{name}} Web 应用",
|
||||
"mainNav.webApps.searchPlaceholder": "搜索 Web 应用",
|
||||
"mainNav.workspace.credits": "{{count}} 消息额度",
|
||||
"mainNav.workspace.creditsUnit": "消息额度",
|
||||
|
||||
@ -202,6 +202,7 @@
|
||||
"mainNav.integrations": "集成",
|
||||
"mainNav.marketplace": "Marketplace",
|
||||
"mainNav.webApps.noResults": "未找到 Web 應用",
|
||||
"mainNav.webApps.openApp": "開啟 {{name}} Web 應用",
|
||||
"mainNav.webApps.searchPlaceholder": "搜尋 Web 應用",
|
||||
"mainNav.workspace.credits": "{{count}} 點額度",
|
||||
"mainNav.workspace.creditsUnit": "點額度",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user