mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
Merge commit '92bde350' into sandboxed-agent-rebase
Made-with: Cursor # Conflicts: # api/controllers/console/app/workflow_draft_variable.py # api/core/agent/cot_agent_runner.py # api/core/agent/cot_chat_agent_runner.py # api/core/agent/cot_completion_agent_runner.py # api/core/agent/fc_agent_runner.py # api/core/app/apps/advanced_chat/app_generator.py # api/core/app/apps/advanced_chat/app_runner.py # api/core/app/apps/agent_chat/app_runner.py # api/core/app/apps/workflow/app_generator.py # api/core/app/apps/workflow/app_runner.py # api/core/app/entities/app_invoke_entities.py # api/core/app/entities/queue_entities.py # api/core/llm_generator/output_parser/structured_output.py # api/core/workflow/workflow_entry.py # api/dify_graph/context/__init__.py # api/dify_graph/entities/tool_entities.py # api/dify_graph/file/file_manager.py # api/dify_graph/graph_engine/response_coordinator/coordinator.py # api/dify_graph/graph_events/node.py # api/dify_graph/node_events/node.py # api/dify_graph/nodes/agent/agent_node.py # api/dify_graph/nodes/llm/entities.py # api/dify_graph/nodes/llm/llm_utils.py # api/dify_graph/nodes/llm/node.py # api/dify_graph/nodes/question_classifier/question_classifier_node.py # api/dify_graph/runtime/graph_runtime_state.py # api/dify_graph/variables/segments.py # api/factories/variable_factory.py # api/services/variable_truncator.py # api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py # api/uv.lock # web/app/components/app-sidebar/app-info.tsx # web/app/components/app-sidebar/app-sidebar-dropdown.tsx # web/app/components/app/create-app-modal/index.spec.tsx # web/app/components/apps/__tests__/list.spec.tsx # web/app/components/apps/app-card.tsx # web/app/components/apps/list.tsx # web/app/components/header/account-dropdown/compliance.tsx # web/app/components/header/account-dropdown/index.tsx # web/app/components/header/account-dropdown/support.tsx # web/app/components/workflow-app/components/workflow-onboarding-modal/index.tsx # web/app/components/workflow/panel/debug-and-preview/hooks.ts # web/contract/console/apps.ts # web/contract/router.ts # web/eslint-suppressions.json # web/next.config.ts # web/pnpm-lock.yaml
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
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'
|
||||
@ -70,16 +71,26 @@ describe('Compliance', () => {
|
||||
)
|
||||
}
|
||||
|
||||
// Wrapper for tests that need the menu open
|
||||
const renderCompliance = () => {
|
||||
return renderWithQueryClient(
|
||||
<DropdownMenu open={true} onOpenChange={() => {}}>
|
||||
<DropdownMenuTrigger>open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<Compliance />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
}
|
||||
|
||||
const openMenuAndRender = () => {
|
||||
renderWithQueryClient(<Compliance />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
renderCompliance()
|
||||
fireEvent.click(screen.getByText('common.userProfile.compliance'))
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render compliance menu trigger', () => {
|
||||
// Act
|
||||
renderWithQueryClient(<Compliance />)
|
||||
renderCompliance()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.userProfile.compliance')).toBeInTheDocument()
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { FC, MouseEvent } from 'react'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
|
||||
import { RiArrowDownCircleLine, RiArrowRightSLine, RiVerifiedBadgeLine } from '@remixicon/react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { Fragment, useCallback } from 'react'
|
||||
import { 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,14 +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'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import { MenuItemContent } from './menu-item-content'
|
||||
|
||||
enum DocName {
|
||||
SOC2_Type_I = 'SOC2_Type_I',
|
||||
@ -27,27 +27,84 @@ enum DocName {
|
||||
GDPR = 'GDPR',
|
||||
}
|
||||
|
||||
type UpgradeOrDownloadProps = {
|
||||
doc_name: DocName
|
||||
type ComplianceDocActionVisualProps = {
|
||||
isCurrentPlanCanDownload: boolean
|
||||
isPending: boolean
|
||||
tooltipText: string
|
||||
downloadText: string
|
||||
upgradeText: string
|
||||
}
|
||||
const UpgradeOrDownload: FC<UpgradeOrDownloadProps> = ({ doc_name }) => {
|
||||
|
||||
function ComplianceDocActionVisual({
|
||||
isCurrentPlanCanDownload,
|
||||
isPending,
|
||||
tooltipText,
|
||||
downloadText,
|
||||
upgradeText,
|
||||
}: ComplianceDocActionVisualProps) {
|
||||
if (isCurrentPlanCanDownload) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
data-disabled={isPending || undefined}
|
||||
className={cn(
|
||||
'btn btn-small btn-secondary pointer-events-none flex items-center gap-[1px]',
|
||||
isPending && 'cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
<span className="i-ri-arrow-down-circle-line size-[14px] text-components-button-secondary-text-disabled" />
|
||||
<span className="px-[3px] text-components-button-secondary-text system-xs-medium">{downloadText}</span>
|
||||
{isPending && <Spinner loading={true} className="!ml-1 !h-3 !w-3 !border-2 !text-text-tertiary" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const canShowUpgradeTooltip = tooltipText.length > 0
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
delay={0}
|
||||
disabled={!canShowUpgradeTooltip}
|
||||
render={(
|
||||
<PremiumBadge color="blue" allowHover={true}>
|
||||
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-[1px] pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
<div className="px-1 system-xs-medium">
|
||||
{upgradeText}
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
/>
|
||||
{canShowUpgradeTooltip && (
|
||||
<TooltipContent>
|
||||
{tooltipText}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
type ComplianceDocRowItemProps = {
|
||||
icon: ReactNode
|
||||
label: ReactNode
|
||||
docName: DocName
|
||||
}
|
||||
|
||||
function ComplianceDocRowItem({
|
||||
icon,
|
||||
label,
|
||||
docName,
|
||||
}: ComplianceDocRowItemProps) {
|
||||
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', doc_name],
|
||||
mutationKey: ['downloadCompliance', docName],
|
||||
mutationFn: async () => {
|
||||
try {
|
||||
const ret = await getDocDownloadUrl(doc_name)
|
||||
const ret = await getDocDownloadUrl(docName)
|
||||
downloadUrl({ url: ret.url })
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
@ -63,6 +120,7 @@ const UpgradeOrDownload: FC<UpgradeOrDownloadProps> = ({ doc_name }) => {
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const whichPlanCanDownloadCompliance = {
|
||||
[DocName.SOC2_Type_I]: [Plan.professional, Plan.team],
|
||||
[DocName.SOC2_Type_II]: [Plan.team],
|
||||
@ -70,118 +128,85 @@ const UpgradeOrDownload: FC<UpgradeOrDownloadProps> = ({ doc_name }) => {
|
||||
[DocName.GDPR]: [Plan.team, Plan.professional, Plan.sandbox],
|
||||
}
|
||||
|
||||
const isCurrentPlanCanDownload = whichPlanCanDownloadCompliance[doc_name].includes(plan.type)
|
||||
const handleDownloadClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
downloadCompliance()
|
||||
}, [downloadCompliance])
|
||||
if (isCurrentPlanCanDownload) {
|
||||
return (
|
||||
<Button loading={isPending} disabled={isPending} size="small" variant="secondary" className="flex items-center gap-[1px]" onClick={handleDownloadClick}>
|
||||
<RiArrowDownCircleLine className="size-[14px] text-components-button-secondary-text-disabled" />
|
||||
<span className="px-[3px] text-components-button-secondary-text system-xs-medium">{t('operation.download', { ns: 'common' })}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
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 upgradeTooltip: Record<Plan, string> = {
|
||||
[Plan.sandbox]: t('compliance.sandboxUpgradeTooltip', { ns: 'common' }),
|
||||
[Plan.professional]: t('compliance.professionalUpgradeTooltip', { ns: 'common' }),
|
||||
[Plan.team]: '',
|
||||
[Plan.enterprise]: '',
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip asChild={false} popupContent={upgradeTooltip[plan.type]}>
|
||||
<PremiumBadge color="blue" allowHover={true} onClick={handlePlanClick}>
|
||||
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-[1px] pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</Tooltip>
|
||||
<DropdownMenuItem
|
||||
className="h-10 justify-between py-1 pl-1 pr-2"
|
||||
closeOnClick={!isCurrentPlanCanDownload}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
{icon}
|
||||
<div className="grow truncate px-1 text-text-secondary system-md-regular">{label}</div>
|
||||
<ComplianceDocActionVisual
|
||||
isCurrentPlanCanDownload={isCurrentPlanCanDownload}
|
||||
isPending={isPending}
|
||||
tooltipText={upgradeTooltip[plan.type]}
|
||||
downloadText={t('operation.download', { ns: 'common' })}
|
||||
upgradeText={t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<Menu as="div" className="relative h-full w-full">
|
||||
{
|
||||
({ open }) => (
|
||||
<>
|
||||
<MenuButton className={
|
||||
cn('group flex h-9 w-full items-center gap-1 rounded-lg py-2 pl-3 pr-2 hover:bg-state-base-hover', open && 'bg-state-base-hover')
|
||||
}
|
||||
>
|
||||
<RiVerifiedBadgeLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-left text-text-secondary system-md-regular">{t('userProfile.compliance', { ns: 'common' })}</div>
|
||||
<RiArrowRightSLine className="size-[14px] shrink-0 text-text-tertiary" />
|
||||
</MenuButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
className={cn(
|
||||
`absolute top-[1px] z-10 max-h-[70vh] w-[337px] origin-top-right -translate-x-full divide-y divide-divider-subtle overflow-y-scroll
|
||||
rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px] focus:outline-none
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<MenuItem>
|
||||
<div
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
>
|
||||
<Soc2 className="size-7 shrink-0" />
|
||||
<div className="grow truncate px-1 text-text-secondary system-md-regular">{t('compliance.soc2Type1', { ns: 'common' })}</div>
|
||||
<UpgradeOrDownload doc_name={DocName.SOC2_Type_I} />
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<div
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
>
|
||||
<Soc2 className="size-7 shrink-0" />
|
||||
<div className="grow truncate px-1 text-text-secondary system-md-regular">{t('compliance.soc2Type2', { ns: 'common' })}</div>
|
||||
<UpgradeOrDownload doc_name={DocName.SOC2_Type_II} />
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<div
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
>
|
||||
<Iso className="size-7 shrink-0" />
|
||||
<div className="grow truncate px-1 text-text-secondary system-md-regular">{t('compliance.iso27001', { ns: 'common' })}</div>
|
||||
<UpgradeOrDownload doc_name={DocName.ISO_27001} />
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<div
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
>
|
||||
<Gdpr className="size-7 shrink-0" />
|
||||
<div className="grow truncate px-1 text-text-secondary system-md-regular">{t('compliance.gdpr', { ns: 'common' })}</div>
|
||||
<UpgradeOrDownload doc_name={DocName.GDPR} />
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-verified-badge-line"
|
||||
label={t('userProfile.compliance', { ns: 'common' })}
|
||||
/>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
popupClassName="w-[337px] divide-y divide-divider-subtle !bg-components-panel-bg-blur !py-0 backdrop-blur-sm"
|
||||
>
|
||||
<DropdownMenuGroup className="p-1">
|
||||
<ComplianceDocRowItem
|
||||
icon={<Soc2 aria-hidden className="size-7 shrink-0" />}
|
||||
label={t('compliance.soc2Type1', { ns: 'common' })}
|
||||
docName={DocName.SOC2_Type_I}
|
||||
/>
|
||||
<ComplianceDocRowItem
|
||||
icon={<Soc2 aria-hidden className="size-7 shrink-0" />}
|
||||
label={t('compliance.soc2Type2', { ns: 'common' })}
|
||||
docName={DocName.SOC2_Type_II}
|
||||
/>
|
||||
<ComplianceDocRowItem
|
||||
icon={<Iso aria-hidden className="size-7 shrink-0" />}
|
||||
label={t('compliance.iso27001', { ns: 'common' })}
|
||||
docName={DocName.ISO_27001}
|
||||
/>
|
||||
<ComplianceDocRowItem
|
||||
icon={<Gdpr aria-hidden className="size-7 shrink-0" />}
|
||||
label={t('compliance.gdpr', { ns: 'common' })}
|
||||
docName={DocName.GDPR}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
)
|
||||
}
|
||||
|
||||
@ -29,6 +29,10 @@ vi.mock('@/app/components/header/github-star', () => ({
|
||||
default: () => <div data-testid="github-star">GithubStar</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/theme-switcher', () => ({
|
||||
default: () => <button type="button" data-testid="theme-switcher-button">Theme switcher</button>,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
@ -65,6 +69,7 @@ vi.mock('@/context/i18n', () => ({
|
||||
const { mockConfig, mockEnv } = vi.hoisted(() => ({
|
||||
mockConfig: {
|
||||
IS_CLOUD_EDITION: false,
|
||||
ZENDESK_WIDGET_KEY: '',
|
||||
},
|
||||
mockEnv: {
|
||||
env: {
|
||||
@ -74,6 +79,7 @@ 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,
|
||||
}))
|
||||
@ -187,6 +193,14 @@ describe('AccountDropdown', () => {
|
||||
expect(screen.getByText('test@example.com')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should set an accessible label on avatar trigger when menu trigger is rendered', () => {
|
||||
// Act
|
||||
renderWithRouter(<AppSelector />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button', { name: 'common.account.account' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show EDU badge for education accounts', () => {
|
||||
// Arrange
|
||||
vi.mocked(useProviderContext).mockReturnValue({
|
||||
@ -266,6 +280,16 @@ describe('AccountDropdown', () => {
|
||||
// Assert
|
||||
expect(screen.queryByTestId('account-about')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep account dropdown open when clicking the theme switcher', () => {
|
||||
// Act
|
||||
renderWithRouter(<AppSelector />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.account.account' }))
|
||||
fireEvent.click(screen.getByTestId('theme-switcher-button'))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.userProfile.logout')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Branding and Environment', () => {
|
||||
|
||||
@ -1,26 +1,15 @@
|
||||
'use client'
|
||||
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 type { MouseEventHandler, ReactNode } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { 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'
|
||||
@ -35,15 +24,90 @@ import AccountAbout from '../account-about'
|
||||
import GithubStar from '../github-star'
|
||||
import Indicator from '../indicator'
|
||||
import Compliance from './compliance'
|
||||
import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content'
|
||||
import Support from './support'
|
||||
|
||||
type AccountMenuRouteItemProps = {
|
||||
href: string
|
||||
iconClassName: string
|
||||
label: ReactNode
|
||||
trailing?: ReactNode
|
||||
}
|
||||
|
||||
function AccountMenuRouteItem({
|
||||
href,
|
||||
iconClassName,
|
||||
label,
|
||||
trailing,
|
||||
}: AccountMenuRouteItemProps) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className="justify-between"
|
||||
render={<Link href={href} />}
|
||||
>
|
||||
<MenuItemContent iconClassName={iconClassName} label={label} trailing={trailing} />
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
type AccountMenuExternalItemProps = {
|
||||
href: string
|
||||
iconClassName: string
|
||||
label: ReactNode
|
||||
trailing?: ReactNode
|
||||
}
|
||||
|
||||
function AccountMenuExternalItem({
|
||||
href,
|
||||
iconClassName,
|
||||
label,
|
||||
trailing,
|
||||
}: AccountMenuExternalItemProps) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className="justify-between"
|
||||
render={<a href={href} rel="noopener noreferrer" target="_blank" />}
|
||||
>
|
||||
<MenuItemContent iconClassName={iconClassName} label={label} trailing={trailing} />
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
type AccountMenuActionItemProps = {
|
||||
iconClassName: string
|
||||
label: ReactNode
|
||||
onClick?: MouseEventHandler<HTMLElement>
|
||||
trailing?: ReactNode
|
||||
}
|
||||
|
||||
function AccountMenuActionItem({
|
||||
iconClassName,
|
||||
label,
|
||||
onClick,
|
||||
trailing,
|
||||
}: AccountMenuActionItemProps) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className="justify-between"
|
||||
onClick={onClick}
|
||||
>
|
||||
<MenuItemContent iconClassName={iconClassName} label={label} trailing={trailing} />
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
type AccountMenuSectionProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function AccountMenuSection({ children }: AccountMenuSectionProps) {
|
||||
return <DropdownMenuGroup className="p-1">{children}</DropdownMenuGroup>
|
||||
}
|
||||
|
||||
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()
|
||||
@ -68,161 +132,124 @@ export default function AppSelector() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
{
|
||||
({ open, close }) => (
|
||||
<>
|
||||
<MenuButton className={cn('inline-flex items-center rounded-[20px] p-0.5 hover:bg-background-default-dodge', open && 'bg-background-default-dodge')}>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
</MenuButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<MenuItems
|
||||
className="
|
||||
absolute right-0 mt-1.5 w-60 max-w-80
|
||||
origin-top-right divide-y divide-divider-subtle rounded-xl bg-components-panel-bg-blur shadow-lg
|
||||
backdrop-blur-sm focus:outline-none
|
||||
"
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<MenuItem disabled>
|
||||
<div className="flex flex-nowrap items-center py-2 pl-3 pr-2">
|
||||
<div className="grow">
|
||||
<div className="break-all text-text-primary system-md-medium">
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size="s" color="blue" className="ml-1 !px-2">
|
||||
<RiGraduationCapFill className="mr-1 h-3 w-3" />
|
||||
<span className="system-2xs-medium">EDU</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</div>
|
||||
<div className="break-all text-text-tertiary system-xs-regular">{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<Link
|
||||
className={cn(itemClassName, 'group', 'data-[active]:bg-state-base-hover')}
|
||||
href="/account"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RiAccountCircleLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('account.account', { ns: 'common' })}</div>
|
||||
<RiArrowRightUpLine className="size-[14px] shrink-0 text-text-tertiary" />
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<div
|
||||
className={cn(itemClassName, 'data-[active]:bg-state-base-hover')}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
|
||||
>
|
||||
<RiSettings3Line className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.settings', { ns: 'common' })}</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</div>
|
||||
{!systemFeatures.branding.enabled && (
|
||||
<>
|
||||
<div className="p-1">
|
||||
<MenuItem>
|
||||
<Link
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
href={docLink('/use-dify/getting-started/introduction')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RiBookOpenLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.helpCenter', { ns: 'common' })}</div>
|
||||
<RiArrowRightUpLine className="size-[14px] shrink-0 text-text-tertiary" />
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<Support closeAccountDropdown={close} />
|
||||
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
||||
</div>
|
||||
<div className="p-1">
|
||||
<MenuItem>
|
||||
<Link
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
href="https://roadmap.dify.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RiMap2Line className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.roadmap', { ns: 'common' })}</div>
|
||||
<RiArrowRightUpLine className="size-[14px] shrink-0 text-text-tertiary" />
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<Link
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
href="https://github.com/langgenius/dify"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RiGithubLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.github', { ns: 'common' })}</div>
|
||||
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
|
||||
<RiStarLine className="size-3 shrink-0 text-text-tertiary" />
|
||||
<GithubStar className="text-text-tertiary system-2xs-medium-uppercase" />
|
||||
</div>
|
||||
</Link>
|
||||
</MenuItem>
|
||||
{
|
||||
env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
|
||||
<MenuItem>
|
||||
<div
|
||||
className={cn(itemClassName, 'justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
onClick={() => setAboutVisible(true)}
|
||||
>
|
||||
<RiInformation2Line className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.about', { ns: 'common' })}</div>
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="mr-2 text-text-tertiary system-xs-regular">{langGeniusVersionInfo.current_version}</div>
|
||||
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
<div>
|
||||
<DropdownMenu open={isAccountMenuOpen} onOpenChange={setIsAccountMenuOpen}>
|
||||
<DropdownMenuTrigger
|
||||
aria-label={t('account.account', { ns: 'common' })}
|
||||
className={cn('inline-flex items-center rounded-[20px] p-0.5 hover:bg-background-default-dodge', isAccountMenuOpen && 'bg-background-default-dodge')}
|
||||
>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
sideOffset={6}
|
||||
popupClassName="w-60 max-w-80 !bg-components-panel-bg-blur !py-0 backdrop-blur-sm"
|
||||
>
|
||||
<DropdownMenuGroup className="px-1 py-1">
|
||||
<div className="flex flex-nowrap items-center py-2 pl-3 pr-2">
|
||||
<div className="grow">
|
||||
<div className="break-all text-text-primary system-md-medium">
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size="s" color="blue" className="ml-1 !px-2">
|
||||
<span aria-hidden className="i-ri-graduation-cap-fill mr-1 h-3 w-3" />
|
||||
<span className="system-2xs-medium">EDU</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
<MenuItem disabled>
|
||||
<div className="p-1">
|
||||
<div className={cn(itemClassName, 'hover:bg-transparent')}>
|
||||
<RiTShirt2Line className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('theme.theme', { ns: 'common' })}</div>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
<div className="break-all text-text-tertiary system-xs-regular">{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
</div>
|
||||
<AccountMenuRouteItem
|
||||
href="/account"
|
||||
iconClassName="i-ri-account-circle-line"
|
||||
label={t('account.account', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-settings-3-line"
|
||||
label={t('userProfile.settings', { ns: 'common' })}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="!my-0 bg-divider-subtle" />
|
||||
{!systemFeatures.branding.enabled && (
|
||||
<>
|
||||
<AccountMenuSection>
|
||||
<AccountMenuExternalItem
|
||||
href={docLink('/use-dify/getting-started/introduction')}
|
||||
iconClassName="i-ri-book-open-line"
|
||||
label={t('userProfile.helpCenter', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
<Support closeAccountDropdown={() => setIsAccountMenuOpen(false)} />
|
||||
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="!my-0 bg-divider-subtle" />
|
||||
<AccountMenuSection>
|
||||
<AccountMenuExternalItem
|
||||
href="https://roadmap.dify.ai"
|
||||
iconClassName="i-ri-map-2-line"
|
||||
label={t('userProfile.roadmap', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
<AccountMenuExternalItem
|
||||
href="https://github.com/langgenius/dify"
|
||||
iconClassName="i-ri-github-line"
|
||||
label={t('userProfile.github', { ns: 'common' })}
|
||||
trailing={(
|
||||
<div className="flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]">
|
||||
<span aria-hidden className="i-ri-star-line size-3 shrink-0 text-text-tertiary" />
|
||||
<GithubStar className="text-text-tertiary system-2xs-medium-uppercase" />
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<div className="p-1" onClick={() => handleLogout()}>
|
||||
<div
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
>
|
||||
<RiLogoutBoxRLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.logout', { ns: 'common' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
)}
|
||||
/>
|
||||
{
|
||||
env.NEXT_PUBLIC_SITE_ABOUT !== 'hide' && (
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-information-2-line"
|
||||
label={t('userProfile.about', { ns: 'common' })}
|
||||
onClick={() => {
|
||||
setAboutVisible(true)
|
||||
setIsAccountMenuOpen(false)
|
||||
}}
|
||||
trailing={(
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className="mr-2 text-text-tertiary system-xs-regular">{langGeniusVersionInfo.current_version}</div>
|
||||
<Indicator color={langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="!my-0 bg-divider-subtle" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
)}
|
||||
<AccountMenuSection>
|
||||
<DropdownMenuItem
|
||||
closeOnClick={false}
|
||||
className="cursor-default data-[highlighted]:bg-transparent"
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-t-shirt-2-line"
|
||||
label={t('theme.theme', { ns: 'common' })}
|
||||
trailing={<ThemeSwitcher />}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</AccountMenuSection>
|
||||
<DropdownMenuSeparator className="!my-0 bg-divider-subtle" />
|
||||
<AccountMenuSection>
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-logout-box-r-line"
|
||||
label={t('userProfile.logout', { ns: 'common' })}
|
||||
onClick={() => {
|
||||
void handleLogout()
|
||||
}}
|
||||
/>
|
||||
</AccountMenuSection>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{
|
||||
aboutVisible && <AccountAbout onCancel={() => setAboutVisible(false)} langGeniusVersionInfo={langGeniusVersionInfo} />
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const menuLabelClassName = 'min-w-0 grow truncate px-1 text-text-secondary system-md-regular'
|
||||
const menuLeadingIconClassName = 'size-4 shrink-0 text-text-tertiary'
|
||||
|
||||
export const menuTrailingIconClassName = 'size-[14px] shrink-0 text-text-tertiary'
|
||||
|
||||
type MenuItemContentProps = {
|
||||
iconClassName: string
|
||||
label: ReactNode
|
||||
trailing?: ReactNode
|
||||
}
|
||||
|
||||
export function MenuItemContent({
|
||||
iconClassName,
|
||||
label,
|
||||
trailing,
|
||||
}: MenuItemContentProps) {
|
||||
return (
|
||||
<>
|
||||
<span aria-hidden className={cn(menuLeadingIconClassName, iconClassName)} />
|
||||
<div className={menuLabelClassName}>{label}</div>
|
||||
{trailing}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ExternalLinkIndicator() {
|
||||
return <span aria-hidden className={cn('i-ri-arrow-right-up-line', menuTrailingIconClassName)} />
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
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'
|
||||
@ -93,10 +94,21 @@ describe('Support', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const renderSupport = () => {
|
||||
return render(
|
||||
<DropdownMenu open={true} onOpenChange={() => {}}>
|
||||
<DropdownMenuTrigger>open</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<Support closeAccountDropdown={mockCloseAccountDropdown} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render support menu trigger', () => {
|
||||
// Act
|
||||
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
|
||||
renderSupport()
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.userProfile.support')).toBeInTheDocument()
|
||||
@ -104,8 +116,8 @@ describe('Support', () => {
|
||||
|
||||
it('should show forum and community links when opened', () => {
|
||||
// Act
|
||||
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
renderSupport()
|
||||
fireEvent.click(screen.getByText('common.userProfile.support'))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.userProfile.forum')).toBeInTheDocument()
|
||||
@ -116,8 +128,8 @@ describe('Support', () => {
|
||||
describe('Plan-based Channels', () => {
|
||||
it('should show "Contact Us" when ZENDESK_WIDGET_KEY is present', () => {
|
||||
// Act
|
||||
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
renderSupport()
|
||||
fireEvent.click(screen.getByText('common.userProfile.support'))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.userProfile.contactUs')).toBeInTheDocument()
|
||||
@ -134,8 +146,8 @@ describe('Support', () => {
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
renderSupport()
|
||||
fireEvent.click(screen.getByText('common.userProfile.support'))
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument()
|
||||
@ -147,8 +159,8 @@ describe('Support', () => {
|
||||
mockZendeskKey.value = ''
|
||||
|
||||
// Act
|
||||
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
renderSupport()
|
||||
fireEvent.click(screen.getByText('common.userProfile.support'))
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.userProfile.emailSupport')).toBeInTheDocument()
|
||||
@ -159,8 +171,8 @@ describe('Support', () => {
|
||||
describe('Interactions and Links', () => {
|
||||
it('should call toggleZendeskWindow and closeAccountDropdown when "Contact Us" is clicked', () => {
|
||||
// Act
|
||||
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
renderSupport()
|
||||
fireEvent.click(screen.getByText('common.userProfile.support'))
|
||||
fireEvent.click(screen.getByText('common.userProfile.contactUs'))
|
||||
|
||||
// Assert
|
||||
@ -170,8 +182,8 @@ describe('Support', () => {
|
||||
|
||||
it('should have correct forum and community links', () => {
|
||||
// Act
|
||||
render(<Support closeAccountDropdown={mockCloseAccountDropdown} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
renderSupport()
|
||||
fireEvent.click(screen.getByText('common.userProfile.support'))
|
||||
|
||||
// Assert
|
||||
const forumLink = screen.getByText('common.userProfile.forum').closest('a')
|
||||
|
||||
@ -1,119 +1,85 @@
|
||||
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'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { mailToSupport } from '../utils/util'
|
||||
import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content'
|
||||
|
||||
type SupportProps = {
|
||||
closeAccountDropdown: () => void
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<Menu as="div" className="relative h-full w-full">
|
||||
{
|
||||
({ open }) => (
|
||||
<>
|
||||
<MenuButton className={
|
||||
cn('group flex h-9 w-full items-center gap-1 rounded-lg py-2 pl-3 pr-2 hover:bg-state-base-hover', open && 'bg-state-base-hover')
|
||||
}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-question-line"
|
||||
label={t('userProfile.support', { ns: 'common' })}
|
||||
/>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
popupClassName="w-[216px] divide-y divide-divider-subtle !bg-components-panel-bg-blur !py-0 backdrop-blur-sm"
|
||||
>
|
||||
<DropdownMenuGroup className="p-1">
|
||||
{hasDedicatedChannel && hasZendeskWidget && (
|
||||
<DropdownMenuItem
|
||||
className="justify-between"
|
||||
onClick={() => {
|
||||
toggleZendeskWindow(true)
|
||||
closeAccountDropdown()
|
||||
}}
|
||||
>
|
||||
<RiQuestionLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-left text-text-secondary system-md-regular">{t('userProfile.support', { ns: 'common' })}</div>
|
||||
<RiArrowRightSLine className="size-[14px] shrink-0 text-text-tertiary" />
|
||||
</MenuButton>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-chat-smile-2-line"
|
||||
label={t('userProfile.contactUs', { ns: 'common' })}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{hasDedicatedChannel && !hasZendeskWidget && (
|
||||
<DropdownMenuItem
|
||||
className="justify-between"
|
||||
render={<a href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo?.current_version)} rel="noopener noreferrer" target="_blank" />}
|
||||
>
|
||||
<MenuItems
|
||||
className={cn(
|
||||
`absolute top-[1px] z-10 max-h-[70vh] w-[216px] origin-top-right -translate-x-full divide-y divide-divider-subtle overflow-y-auto
|
||||
rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px] focus:outline-none
|
||||
`,
|
||||
)}
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
{hasDedicatedChannel && (
|
||||
<MenuItem>
|
||||
{ZENDESK_WIDGET_KEY && ZENDESK_WIDGET_KEY.trim() !== ''
|
||||
? (
|
||||
<button
|
||||
className={cn(itemClassName, 'group justify-between text-left data-[active]:bg-state-base-hover')}
|
||||
onClick={() => {
|
||||
toggleZendeskWindow(true)
|
||||
closeAccountDropdown()
|
||||
}}
|
||||
>
|
||||
<RiChatSmile2Line className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.contactUs', { ns: 'common' })}</div>
|
||||
</button>
|
||||
)
|
||||
: (
|
||||
<a
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo?.current_version)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RiMailSendLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.emailSupport', { ns: 'common' })}</div>
|
||||
<RiArrowRightUpLine className="size-[14px] shrink-0 text-text-tertiary" />
|
||||
</a>
|
||||
)}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem>
|
||||
<Link
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
href="https://forum.dify.ai/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RiDiscussLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.forum', { ns: 'common' })}</div>
|
||||
<RiArrowRightUpLine className="size-[14px] shrink-0 text-text-tertiary" />
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<Link
|
||||
className={cn(itemClassName, 'group justify-between', 'data-[active]:bg-state-base-hover')}
|
||||
href="https://discord.gg/5AEfbxcd9k"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<RiDiscordLine className="size-4 shrink-0 text-text-tertiary" />
|
||||
<div className="grow px-1 text-text-secondary system-md-regular">{t('userProfile.community', { ns: 'common' })}</div>
|
||||
<RiArrowRightUpLine className="size-[14px] shrink-0 text-text-tertiary" />
|
||||
</Link>
|
||||
</MenuItem>
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Transition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Menu>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-mail-send-line"
|
||||
label={t('userProfile.emailSupport', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="justify-between"
|
||||
render={<a href="https://forum.dify.ai/" rel="noopener noreferrer" target="_blank" />}
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-discuss-line"
|
||||
label={t('userProfile.forum', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="justify-between"
|
||||
render={<a href="https://discord.gg/5AEfbxcd9k" rel="noopener noreferrer" target="_blank" />}
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-discord-line"
|
||||
label={t('userProfile.community', { ns: 'common' })}
|
||||
trailing={<ExternalLinkIndicator />}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import type { IWorkspace } from '@/models/common'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
import { switchWorkspace } from '@/service/common'
|
||||
|
||||
@ -4,7 +4,7 @@ import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { Fragment } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import PlanBadge from '@/app/components/header/plan-badge'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
import { switchWorkspace } from '@/service/common'
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { IToastProps } from '@/app/components/base/toast'
|
||||
import type { IToastProps } from '@/app/components/base/toast/context'
|
||||
import { fireEvent, render as RTLRender, screen, waitFor } from '@testing-library/react'
|
||||
import * as reactI18next from 'react-i18next'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common'
|
||||
import ApiBasedExtensionModal from './modal'
|
||||
|
||||
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import {
|
||||
addApiBasedExtension,
|
||||
|
||||
@ -7,7 +7,7 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ICurrentWorkspace } from '@/models/common'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { vi } from 'vitest'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { updateWorkspaceInfo } from '@/service/common'
|
||||
import EditWorkspaceModal from './index'
|
||||
|
||||
@ -6,7 +6,7 @@ import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { updateWorkspaceInfo } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@ -2,7 +2,7 @@ import type { InvitationResponse } from '@/models/common'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { vi } from 'vitest'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { inviteMember } from '@/service/common'
|
||||
import InviteModal from './index'
|
||||
|
||||
@ -9,7 +9,7 @@ import { ReactMultiEmail } from 'react-multi-email'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { emailRegex } from '@/config'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
|
||||
@ -2,7 +2,7 @@ import type { Member } from '@/models/common'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { vi } from 'vitest'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import Operation from './index'
|
||||
|
||||
const mockUpdateMemberRole = vi.fn()
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { deleteMemberOrCancelInvitation, updateMemberRole } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ICurrentWorkspace } from '@/models/common'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { vi } from 'vitest'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
||||
import { useMembers } from '@/service/use-common'
|
||||
|
||||
@ -6,7 +6,7 @@ import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { ToastContext } from '@/app/components/base/toast/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
ownershipTransfer,
|
||||
|
||||
@ -20,7 +20,7 @@ const mockAddModelCredential = vi.fn()
|
||||
const mockEditProviderCredential = vi.fn()
|
||||
const mockEditModelCredential = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({ notify: mockNotify }),
|
||||
}))
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import {
|
||||
useModelModalHandler,
|
||||
useRefreshModel,
|
||||
|
||||
@ -24,7 +24,7 @@ vi.mock('@/config', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
} from '../declarations'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { ConfigProvider } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
@ -43,7 +43,7 @@ let mockCredentialData: CredentialData | undefined = {
|
||||
current_credential_name: 'Default',
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
|
||||
@ -12,7 +12,7 @@ import Button from '@/app/components/base/button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import {
|
||||
useGetModelCredential,
|
||||
|
||||
@ -42,7 +42,7 @@ vi.mock('@/context/provider-context', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { PluginProvider } from '@/models/common'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import SerpapiPlugin from './SerpapiPlugin'
|
||||
import { updatePluginKey, validatePluginKey } from './utils'
|
||||
@ -20,7 +20,7 @@ const mockEventEmitter = vi.hoisted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: vi.fn(),
|
||||
}))
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import type { Form, ValidateValue } from '../key-validator/declarations'
|
||||
import type { PluginProvider } from '@/models/common'
|
||||
import Image from 'next/image'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import SerpapiLogo from '../../assets/serpapi.png'
|
||||
import KeyValidator from '../key-validator'
|
||||
|
||||
@ -14,7 +14,7 @@ vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
vi.mock('@/app/components/base/toast/context', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: vi.fn(),
|
||||
}),
|
||||
|
||||
@ -9,7 +9,7 @@ import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import RadioUI from '@/app/components/base/radio/ui'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import {
|
||||
useActivateSandboxProvider,
|
||||
useDeleteSandboxProviderConfig,
|
||||
|
||||
@ -5,7 +5,7 @@ import { memo, useCallback } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { useActivateSandboxProvider } from '@/service/use-sandbox-provider'
|
||||
import { PROVIDER_STATIC_LABELS } from './constants'
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ vi.mock('@/app/components/header/plan-badge', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/workspace-context', () => ({
|
||||
vi.mock('@/context/workspace-context-provider', () => ({
|
||||
WorkspaceProvider: ({ children }: { children?: React.ReactNode }) => children,
|
||||
}))
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import { useAppContext } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { WorkspaceProvider } from '@/context/workspace-context'
|
||||
import { WorkspaceProvider } from '@/context/workspace-context-provider'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { Plan } from '../billing/type'
|
||||
import AccountDropdown from './account-dropdown'
|
||||
|
||||
Reference in New Issue
Block a user