diff --git a/web/app/components/header/account-dropdown/compliance.spec.tsx b/web/app/components/header/account-dropdown/compliance.spec.tsx
index 1eb747e154..54a0460f82 100644
--- a/web/app/components/header/account-dropdown/compliance.spec.tsx
+++ b/web/app/components/header/account-dropdown/compliance.spec.tsx
@@ -1,7 +1,6 @@
import type { ModalContextState } from '@/context/modal-context'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
import { Plan } from '@/app/components/billing/type'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
@@ -71,26 +70,16 @@ describe('Compliance', () => {
)
}
- const renderCompliance = () => {
- return renderWithQueryClient(
- {}}>
- open
-
-
-
- ,
- )
- }
-
+ // Wrapper for tests that need the menu open
const openMenuAndRender = () => {
- renderCompliance()
- fireEvent.click(screen.getByText('common.userProfile.compliance'))
+ renderWithQueryClient()
+ fireEvent.click(screen.getByRole('button'))
}
describe('Rendering', () => {
it('should render compliance menu trigger', () => {
// Act
- renderCompliance()
+ renderWithQueryClient()
// Assert
expect(screen.getByText('common.userProfile.compliance')).toBeInTheDocument()
diff --git a/web/app/components/header/account-dropdown/compliance.tsx b/web/app/components/header/account-dropdown/compliance.tsx
index 6bf19674d0..a8d505627f 100644
--- a/web/app/components/header/account-dropdown/compliance.tsx
+++ b/web/app/components/header/account-dropdown/compliance.tsx
@@ -1,9 +1,9 @@
-import type { ReactNode } from 'react'
+import type { FC, MouseEvent } from 'react'
+import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
+import { RiArrowDownCircleLine, RiArrowRightSLine, RiVerifiedBadgeLine } from '@remixicon/react'
import { useMutation } from '@tanstack/react-query'
-import { useCallback } from 'react'
+import { Fragment, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
-import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu'
-import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
import { Plan } from '@/app/components/billing/type'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
@@ -11,19 +11,14 @@ import { useProviderContext } from '@/context/provider-context'
import { getDocDownloadUrl } from '@/service/common'
import { cn } from '@/utils/classnames'
import { downloadUrl } from '@/utils/download'
+import Button from '../../base/button'
import Gdpr from '../../base/icons/src/public/common/Gdpr'
import Iso from '../../base/icons/src/public/common/Iso'
import Soc2 from '../../base/icons/src/public/common/Soc2'
import SparklesSoft from '../../base/icons/src/public/common/SparklesSoft'
import PremiumBadge from '../../base/premium-badge'
-import Spinner from '../../base/spinner'
import Toast from '../../base/toast'
-
-const submenuTriggerClassName = '!mx-0 !h-8 !rounded-lg !px-3 data-[highlighted]:!bg-state-base-hover'
-const submenuItemClassName = '!mx-0 !h-10 !rounded-lg !py-1 !pl-1 !pr-2 data-[highlighted]:!bg-state-base-hover'
-const menuLabelClassName = 'grow px-1 text-text-secondary system-md-regular'
-const menuLeadingIconClassName = 'size-4 shrink-0 text-text-tertiary'
-const menuTrailingIconClassName = 'size-[14px] shrink-0 text-text-tertiary'
+import Tooltip from '../../base/tooltip'
enum DocName {
SOC2_Type_I = 'SOC2_Type_I',
@@ -32,103 +27,27 @@ enum DocName {
GDPR = 'GDPR',
}
-type ComplianceMenuItemContentProps = {
- iconClassName: string
- label: ReactNode
- trailing?: ReactNode
+type UpgradeOrDownloadProps = {
+ doc_name: DocName
}
-
-function ComplianceMenuItemContent({
- iconClassName,
- label,
- trailing,
-}: ComplianceMenuItemContentProps) {
- return (
- <>
-
-
{label}
- {trailing}
- >
- )
-}
-
-type ComplianceDocActionVisualProps = {
- isCurrentPlanCanDownload: boolean
- isPending: boolean
- tooltipText: string
- downloadText: string
- upgradeText: string
-}
-
-function ComplianceDocActionVisual({
- isCurrentPlanCanDownload,
- isPending,
- tooltipText,
- downloadText,
- upgradeText,
-}: ComplianceDocActionVisualProps) {
- if (isCurrentPlanCanDownload) {
- return (
-
-
- {downloadText}
- {isPending && }
-
- )
- }
-
- const canShowUpgradeTooltip = tooltipText.length > 0
-
- return (
-
-
-
-
- {upgradeText}
-
-
- )}
- />
- {canShowUpgradeTooltip && (
-
- {tooltipText}
-
- )}
-
- )
-}
-
-type ComplianceDocRowItemProps = {
- icon: ReactNode
- label: ReactNode
- docName: DocName
-}
-
-function ComplianceDocRowItem({
- icon,
- label,
- docName,
-}: ComplianceDocRowItemProps) {
+const UpgradeOrDownload: FC = ({ doc_name }) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
const isFreePlan = plan.type === Plan.sandbox
+ const handlePlanClick = useCallback(() => {
+ if (isFreePlan)
+ setShowPricingModal()
+ else
+ setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
+ }, [isFreePlan, setShowAccountSettingModal, setShowPricingModal])
+
const { isPending, mutate: downloadCompliance } = useMutation({
- mutationKey: ['downloadCompliance', docName],
+ mutationKey: ['downloadCompliance', doc_name],
mutationFn: async () => {
try {
- const ret = await getDocDownloadUrl(docName)
+ const ret = await getDocDownloadUrl(doc_name)
downloadUrl({ url: ret.url })
Toast.notify({
type: 'success',
@@ -144,7 +63,6 @@ function ComplianceDocRowItem({
}
},
})
-
const whichPlanCanDownloadCompliance = {
[DocName.SOC2_Type_I]: [Plan.professional, Plan.team],
[DocName.SOC2_Type_II]: [Plan.team],
@@ -152,86 +70,118 @@ function ComplianceDocRowItem({
[DocName.GDPR]: [Plan.team, Plan.professional, Plan.sandbox],
}
- const isCurrentPlanCanDownload = whichPlanCanDownloadCompliance[docName].includes(plan.type)
-
- const handleSelect = useCallback(() => {
- if (isCurrentPlanCanDownload) {
- if (!isPending)
- downloadCompliance()
- return
- }
-
- if (isFreePlan)
- setShowPricingModal()
- else
- setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
- }, [downloadCompliance, isCurrentPlanCanDownload, isFreePlan, isPending, setShowAccountSettingModal, setShowPricingModal])
-
+ const isCurrentPlanCanDownload = whichPlanCanDownloadCompliance[doc_name].includes(plan.type)
+ const handleDownloadClick = useCallback((e: MouseEvent) => {
+ e.preventDefault()
+ downloadCompliance()
+ }, [downloadCompliance])
+ if (isCurrentPlanCanDownload) {
+ return (
+
+ )
+ }
const upgradeTooltip: Record = {
[Plan.sandbox]: t('compliance.sandboxUpgradeTooltip', { ns: 'common' }),
[Plan.professional]: t('compliance.professionalUpgradeTooltip', { ns: 'common' }),
[Plan.team]: '',
[Plan.enterprise]: '',
}
-
return (
-
- {icon}
- {label}
-
-
+
+
+
+
+
+ {t('upgradeBtn.encourageShort', { ns: 'billing' })}
+
+
+
+
)
}
-// Submenu-only: this component must be rendered within an existing DropdownMenu root.
export default function Compliance() {
+ const itemClassName = `
+ flex items-center w-full h-10 pl-1 pr-2 py-1 text-text-secondary system-md-regular
+ rounded-lg hover:bg-state-base-hover gap-1
+`
const { t } = useTranslation()
return (
-
-
- }
- />
-
-
-
- }
- label={t('compliance.soc2Type1', { ns: 'common' })}
- docName={DocName.SOC2_Type_I}
- />
- }
- label={t('compliance.soc2Type2', { ns: 'common' })}
- docName={DocName.SOC2_Type_II}
- />
- }
- label={t('compliance.iso27001', { ns: 'common' })}
- docName={DocName.ISO_27001}
- />
- }
- label={t('compliance.gdpr', { ns: 'common' })}
- docName={DocName.GDPR}
- />
-
-
-
+
)
}
diff --git a/web/app/components/header/account-dropdown/index.spec.tsx b/web/app/components/header/account-dropdown/index.spec.tsx
index 65399af36d..a954351267 100644
--- a/web/app/components/header/account-dropdown/index.spec.tsx
+++ b/web/app/components/header/account-dropdown/index.spec.tsx
@@ -65,7 +65,6 @@ vi.mock('@/context/i18n', () => ({
const { mockConfig, mockEnv } = vi.hoisted(() => ({
mockConfig: {
IS_CLOUD_EDITION: false,
- ZENDESK_WIDGET_KEY: '',
},
mockEnv: {
env: {
@@ -75,7 +74,6 @@ const { mockConfig, mockEnv } = vi.hoisted(() => ({
}))
vi.mock('@/config', () => ({
get IS_CLOUD_EDITION() { return mockConfig.IS_CLOUD_EDITION },
- get ZENDESK_WIDGET_KEY() { return mockConfig.ZENDESK_WIDGET_KEY },
IS_DEV: false,
IS_CE_EDITION: false,
}))
diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx
index d682d0ccb8..c61ffb4a7a 100644
--- a/web/app/components/header/account-dropdown/index.tsx
+++ b/web/app/components/header/account-dropdown/index.tsx
@@ -1,15 +1,26 @@
'use client'
-
-import type { MouseEventHandler, ReactNode } from 'react'
+import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
+import {
+ RiAccountCircleLine,
+ RiArrowRightUpLine,
+ RiBookOpenLine,
+ RiGithubLine,
+ RiGraduationCapFill,
+ RiInformation2Line,
+ RiLogoutBoxRLine,
+ RiMap2Line,
+ RiSettings3Line,
+ RiStarLine,
+ RiTShirt2Line,
+} from '@remixicon/react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
-import { useState } from 'react'
+import { Fragment, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { resetUser } from '@/app/components/base/amplitude/utils'
import Avatar from '@/app/components/base/avatar'
import PremiumBadge from '@/app/components/base/premium-badge'
import ThemeSwitcher from '@/app/components/base/theme-switcher'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
@@ -26,117 +37,13 @@ import Indicator from '../indicator'
import Compliance from './compliance'
import Support from './support'
-const menuItemClassName = '!mx-0 !h-8 !rounded-lg !px-3 data-[highlighted]:!bg-state-base-hover'
-const menuStaticRowClassName = 'flex h-8 w-full items-center rounded-lg px-3 text-text-secondary system-md-regular'
-const menuLabelClassName = 'grow px-1 text-text-secondary system-md-regular'
-const menuLeadingIconClassName = 'size-4 shrink-0 text-text-tertiary'
-const menuTrailingIconClassName = 'size-[14px] shrink-0 text-text-tertiary'
-
-type AccountMenuItemContentProps = {
- iconClassName: string
- label: ReactNode
- trailing?: ReactNode
-}
-
-function AccountMenuItemContent({
- iconClassName,
- label,
- trailing,
-}: AccountMenuItemContentProps) {
- return (
- <>
-
- {label}
- {trailing}
- >
- )
-}
-
-type AccountMenuRouteItemProps = {
- href: string
- iconClassName: string
- label: ReactNode
- trailing?: ReactNode
-}
-
-function AccountMenuRouteItem({
- href,
- iconClassName,
- label,
- trailing,
-}: AccountMenuRouteItemProps) {
- return (
- }
- >
-
-
- )
-}
-
-type AccountMenuExternalItemProps = {
- href: string
- iconClassName: string
- label: ReactNode
- trailing?: ReactNode
-}
-
-function AccountMenuExternalItem({
- href,
- iconClassName,
- label,
- trailing,
-}: AccountMenuExternalItemProps) {
- return (
- }
- >
-
-
- )
-}
-
-type AccountMenuActionItemProps = {
- iconClassName: string
- label: ReactNode
- onClick?: MouseEventHandler
- trailing?: ReactNode
-}
-
-function AccountMenuActionItem({
- iconClassName,
- label,
- onClick,
- trailing,
-}: AccountMenuActionItemProps) {
- return (
-
-
-
- )
-}
-
-function ExternalLinkIndicator() {
- return
-}
-
-type AccountMenuSectionProps = {
- children: ReactNode
-}
-
-function AccountMenuSection({ children }: AccountMenuSectionProps) {
- return {children}
-}
-
export default function AppSelector() {
+ const itemClassName = `
+ flex items-center w-full h-8 pl-3 pr-2 text-text-secondary system-md-regular
+ rounded-lg hover:bg-state-base-hover cursor-pointer gap-1
+ `
const router = useRouter()
const [aboutVisible, setAboutVisible] = useState(false)
- const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
const { systemFeatures } = useGlobalPublicStore()
const { t } = useTranslation()
@@ -162,116 +69,160 @@ export default function AppSelector() {
return (
-
-
-
-
-
-
-
-
-
- {userProfile.name}
- {isEducationAccount && (
-
-
- EDU
-
- )}
-
-
{userProfile.email}
-
-
-
- }
- />
- setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
- />
-
-
- {!systemFeatures.branding.enabled && (
+
- )}
- />
- {
- env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
- {
- setAboutVisible(true)
- setIsAccountMenuOpen(false)
- }}
- trailing={(
-
-
{langGeniusVersionInfo.current_version}
-
+
+
+
+
+
+
+
+
+
+
+ {!systemFeatures.branding.enabled && (
+ <>
+
+
+
+ {IS_CLOUD_EDITION && isCurrentWorkspaceOwner &&
}
+
+
+
+
+ {
+ env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
+
+ )
+ }
+
+ >
+ )}
+
+
+
+
>
- )}
-
-
-
-
-
- {
- void handleLogout()
- }}
- />
-
-
-
+ )
+ }
+
{
aboutVisible &&
setAboutVisible(false)} langGeniusVersionInfo={langGeniusVersionInfo} />
}
diff --git a/web/app/components/header/account-dropdown/support.spec.tsx b/web/app/components/header/account-dropdown/support.spec.tsx
index 90bcb9f3ec..b30a290ea5 100644
--- a/web/app/components/header/account-dropdown/support.spec.tsx
+++ b/web/app/components/header/account-dropdown/support.spec.tsx
@@ -1,7 +1,6 @@
import type { AppContextValue } from '@/context/app-context'
import { fireEvent, render, screen } from '@testing-library/react'
-import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/app/components/base/ui/dropdown-menu'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
@@ -94,21 +93,10 @@ describe('Support', () => {
})
})
- const renderSupport = () => {
- return render(
- {}}>
- open
-
-
-
- ,
- )
- }
-
describe('Rendering', () => {
it('should render support menu trigger', () => {
// Act
- renderSupport()
+ render()
// Assert
expect(screen.getByText('common.userProfile.support')).toBeInTheDocument()
@@ -116,8 +104,8 @@ describe('Support', () => {
it('should show forum and community links when opened', () => {
// Act
- renderSupport()
- fireEvent.click(screen.getByText('common.userProfile.support'))
+ render()
+ fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getByText('common.userProfile.forum')).toBeInTheDocument()
@@ -128,8 +116,8 @@ describe('Support', () => {
describe('Plan-based Channels', () => {
it('should show "Contact Us" when ZENDESK_WIDGET_KEY is present', () => {
// Act
- renderSupport()
- fireEvent.click(screen.getByText('common.userProfile.support'))
+ render()
+ fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getByText('common.userProfile.contactUs')).toBeInTheDocument()
@@ -146,8 +134,8 @@ describe('Support', () => {
})
// Act
- renderSupport()
- fireEvent.click(screen.getByText('common.userProfile.support'))
+ render()
+ fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument()
@@ -159,8 +147,8 @@ describe('Support', () => {
mockZendeskKey.value = ''
// Act
- renderSupport()
- fireEvent.click(screen.getByText('common.userProfile.support'))
+ render()
+ fireEvent.click(screen.getByRole('button'))
// Assert
expect(screen.getByText('common.userProfile.emailSupport')).toBeInTheDocument()
@@ -171,8 +159,8 @@ describe('Support', () => {
describe('Interactions and Links', () => {
it('should call toggleZendeskWindow and closeAccountDropdown when "Contact Us" is clicked', () => {
// Act
- renderSupport()
- fireEvent.click(screen.getByText('common.userProfile.support'))
+ render()
+ fireEvent.click(screen.getByRole('button'))
fireEvent.click(screen.getByText('common.userProfile.contactUs'))
// Assert
@@ -182,8 +170,8 @@ describe('Support', () => {
it('should have correct forum and community links', () => {
// Act
- renderSupport()
- fireEvent.click(screen.getByText('common.userProfile.support'))
+ render()
+ fireEvent.click(screen.getByRole('button'))
// Assert
const forumLink = screen.getByText('common.userProfile.forum').closest('a')
diff --git a/web/app/components/header/account-dropdown/support.tsx b/web/app/components/header/account-dropdown/support.tsx
index d13ca5335e..566a9ff56f 100644
--- a/web/app/components/header/account-dropdown/support.tsx
+++ b/web/app/components/header/account-dropdown/support.tsx
@@ -1,6 +1,8 @@
-import type { ReactNode } from 'react'
+import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
+import { RiArrowRightSLine, RiArrowRightUpLine, RiChatSmile2Line, RiDiscordLine, RiDiscussLine, RiMailSendLine, RiQuestionLine } from '@remixicon/react'
+import Link from 'next/link'
+import { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
-import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '@/app/components/base/ui/dropdown-menu'
import { toggleZendeskWindow } from '@/app/components/base/zendesk/utils'
import { Plan } from '@/app/components/billing/type'
import { ZENDESK_WIDGET_KEY } from '@/config'
@@ -9,109 +11,109 @@ import { useProviderContext } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
import { mailToSupport } from '../utils/util'
-const submenuTriggerClassName = '!mx-0 !h-8 !rounded-lg !px-3 data-[highlighted]:!bg-state-base-hover'
-const submenuItemClassName = '!mx-0 !h-8 !rounded-lg !px-3 data-[highlighted]:!bg-state-base-hover'
-const menuLabelClassName = 'grow px-1 text-text-secondary system-md-regular'
-const menuLeadingIconClassName = 'size-4 shrink-0 text-text-tertiary'
-const menuTrailingIconClassName = 'size-[14px] shrink-0 text-text-tertiary'
-
type SupportProps = {
closeAccountDropdown: () => void
}
-type SupportMenuItemContentProps = {
- iconClassName: string
- label: ReactNode
- trailing?: ReactNode
-}
-
-function SupportMenuItemContent({
- iconClassName,
- label,
- trailing,
-}: SupportMenuItemContentProps) {
- return (
- <>
-
- {label}
- {trailing}
- >
- )
-}
-
-function SupportExternalLinkIndicator() {
- return
-}
-
-// Submenu-only: this component must be rendered within an existing DropdownMenu root.
export default function Support({ closeAccountDropdown }: SupportProps) {
+ const itemClassName = `
+ flex items-center w-full h-9 pl-3 pr-2 text-text-secondary system-md-regular
+ rounded-lg hover:bg-state-base-hover cursor-pointer gap-1
+`
const { t } = useTranslation()
const { plan } = useProviderContext()
const { userProfile, langGeniusVersionInfo } = useAppContext()
const hasDedicatedChannel = plan.type !== Plan.sandbox
- const hasZendeskWidget = !!ZENDESK_WIDGET_KEY?.trim()
return (
-
-
- }
- />
-
-
-
- {hasDedicatedChannel && hasZendeskWidget && (
- {
- toggleZendeskWindow(true)
- closeAccountDropdown()
- }}
+
- )}
- {hasDedicatedChannel && !hasZendeskWidget && (
- }
+
+ {t('userProfile.support', { ns: 'common' })}
+
+
+
- }
- />
-
- )}
- }
- >
- }
- />
-
- }
- >
- }
- />
-
-
-
-
+
+
+ {hasDedicatedChannel && (
+
+ )}
+
+
+
+
+
+ >
+ )
+ }
+
)
}