mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
refactor(web): migrate account settings app context consumers (#38544)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
5ffd4345dc
commit
dce45ef6ae
@ -279,11 +279,6 @@
|
|||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/account/(commonLayout)/delete-account/components/check-email.tsx": {
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/account/(commonLayout)/delete-account/components/verify-email.tsx": {
|
"web/app/account/(commonLayout)/delete-account/components/verify-email.tsx": {
|
||||||
"no-restricted-imports": {
|
"no-restricted-imports": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -3401,30 +3396,11 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx": {
|
|
||||||
"jsx-a11y/no-autofocus": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/header/account-setting/members-page/invite-modal/index.tsx": {
|
"web/app/components/header/account-setting/members-page/invite-modal/index.tsx": {
|
||||||
"jsx-a11y/no-autofocus": {
|
"jsx-a11y/no-autofocus": {
|
||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx": {
|
|
||||||
"erasable-syntax-only/enums": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"ts/no-explicit-any": {
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx": {
|
"web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx": {
|
||||||
"jsx-a11y/click-events-have-key-events": {
|
"jsx-a11y/click-events-have-key-events": {
|
||||||
"count": 1
|
"count": 1
|
||||||
@ -3566,14 +3542,6 @@
|
|||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx": {
|
|
||||||
"jsx-a11y/click-events-have-key-events": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"jsx-a11y/no-static-element-interactions": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx": {
|
"web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx": {
|
||||||
"jsx-a11y/click-events-have-key-events": {
|
"jsx-a11y/click-events-have-key-events": {
|
||||||
"count": 2
|
"count": 2
|
||||||
|
|||||||
@ -6,11 +6,32 @@ import AccountDropdown from '@/app/components/header/account-dropdown'
|
|||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
mockAppContextState,
|
||||||
mockPush,
|
mockPush,
|
||||||
mockLogout,
|
mockLogout,
|
||||||
mockResetUser,
|
mockResetUser,
|
||||||
mockSetShowAccountSettingModal,
|
mockSetShowAccountSettingModal,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
|
mockAppContextState: {
|
||||||
|
userProfile: {
|
||||||
|
id: 'user-1',
|
||||||
|
name: 'Ada Lovelace',
|
||||||
|
email: 'ada@example.com',
|
||||||
|
avatar: '',
|
||||||
|
avatar_url: '',
|
||||||
|
is_password_set: true,
|
||||||
|
},
|
||||||
|
langGeniusVersionInfo: {
|
||||||
|
current_env: 'CLOUD',
|
||||||
|
current_version: '1.0.0',
|
||||||
|
latest_version: '1.1.0',
|
||||||
|
release_date: '',
|
||||||
|
release_notes: 'https://example.com/releases/1.1.0',
|
||||||
|
version: '1.0.0',
|
||||||
|
can_auto_update: false,
|
||||||
|
},
|
||||||
|
isCurrentWorkspaceOwner: false,
|
||||||
|
},
|
||||||
mockPush: vi.fn(),
|
mockPush: vi.fn(),
|
||||||
mockLogout: vi.fn(),
|
mockLogout: vi.fn(),
|
||||||
mockResetUser: vi.fn(),
|
mockResetUser: vi.fn(),
|
||||||
@ -28,21 +49,19 @@ vi.mock('react-i18next', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/app-context', () => ({
|
vi.mock('@/context/app-context', () => ({
|
||||||
useAppContext: () => ({
|
useAppContext: () => mockAppContextState,
|
||||||
userProfile: {
|
|
||||||
name: 'Ada Lovelace',
|
|
||||||
email: 'ada@example.com',
|
|
||||||
avatar_url: '',
|
|
||||||
},
|
|
||||||
langGeniusVersionInfo: {
|
|
||||||
current_version: '1.0.0',
|
|
||||||
latest_version: '1.1.0',
|
|
||||||
release_notes: 'https://example.com/releases/1.1.0',
|
|
||||||
},
|
|
||||||
isCurrentWorkspaceOwner: false,
|
|
||||||
}),
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => ({
|
useProviderContext: () => ({
|
||||||
isEducationAccount: false,
|
isEducationAccount: false,
|
||||||
@ -62,7 +81,8 @@ vi.mock('@/context/i18n', () => ({
|
|||||||
useDocLink: () => (path: string) => `https://docs.example.com${path}`,
|
useDocLink: () => (path: string) => `https://docs.example.com${path}`,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/service/use-common', () => ({
|
vi.mock('@/service/use-common', async importOriginal => ({
|
||||||
|
...await importOriginal<typeof import('@/service/use-common')>(),
|
||||||
useLogout: () => ({
|
useLogout: () => ({
|
||||||
mutateAsync: mockLogout,
|
mutateAsync: mockLogout,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export type AppContextStateMockState = {
|
|||||||
} | null
|
} | null
|
||||||
currentWorkspace?: {
|
currentWorkspace?: {
|
||||||
id?: string
|
id?: string
|
||||||
|
name?: string
|
||||||
} | null
|
} | null
|
||||||
isCurrentWorkspaceManager?: boolean
|
isCurrentWorkspaceManager?: boolean
|
||||||
isCurrentWorkspaceOwner?: boolean
|
isCurrentWorkspaceOwner?: boolean
|
||||||
@ -22,6 +23,8 @@ export type AppContextStateMockState = {
|
|||||||
isLoadingWorkspacePermissionKeys?: boolean
|
isLoadingWorkspacePermissionKeys?: boolean
|
||||||
workspacePermissionKeys?: string[]
|
workspacePermissionKeys?: string[]
|
||||||
langGeniusVersionInfo?: LangGeniusVersionResponse
|
langGeniusVersionInfo?: LangGeniusVersionResponse
|
||||||
|
refreshUserProfile?: () => void
|
||||||
|
refreshCurrentWorkspace?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppContextStateAtomKind
|
type AppContextStateAtomKind
|
||||||
@ -32,11 +35,14 @@ type AppContextStateAtomKind
|
|||||||
| 'currentWorkspaceId'
|
| 'currentWorkspaceId'
|
||||||
| 'workspaceRoleFlags'
|
| 'workspaceRoleFlags'
|
||||||
| 'isCurrentWorkspaceManager'
|
| 'isCurrentWorkspaceManager'
|
||||||
|
| 'isCurrentWorkspaceOwner'
|
||||||
| 'currentWorkspaceLoading'
|
| 'currentWorkspaceLoading'
|
||||||
| 'workspacePermissionKeys'
|
| 'workspacePermissionKeys'
|
||||||
| 'workspacePermissionKeysLoading'
|
| 'workspacePermissionKeysLoading'
|
||||||
| 'langGeniusVersionInfo'
|
| 'langGeniusVersionInfo'
|
||||||
| 'langGeniusCurrentVersion'
|
| 'langGeniusCurrentVersion'
|
||||||
|
| 'refreshUserProfile'
|
||||||
|
| 'refreshCurrentWorkspace'
|
||||||
|
|
||||||
type AppContextStateMockAtom = {
|
type AppContextStateMockAtom = {
|
||||||
[APP_CONTEXT_STATE_ATOM_KIND]: AppContextStateAtomKind
|
[APP_CONTEXT_STATE_ATOM_KIND]: AppContextStateAtomKind
|
||||||
@ -57,6 +63,7 @@ const defaultUserProfile = {
|
|||||||
|
|
||||||
const defaultCurrentWorkspace = {
|
const defaultCurrentWorkspace = {
|
||||||
id: 'workspace-1',
|
id: 'workspace-1',
|
||||||
|
name: 'Workspace',
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLangGeniusVersionInfo = {
|
const defaultLangGeniusVersionInfo = {
|
||||||
@ -109,11 +116,14 @@ export const createAppContextStateAtomMock = async (
|
|||||||
currentWorkspaceIdAtom: createMockAtom('currentWorkspaceId'),
|
currentWorkspaceIdAtom: createMockAtom('currentWorkspaceId'),
|
||||||
workspaceRoleFlagsAtom: createMockAtom('workspaceRoleFlags'),
|
workspaceRoleFlagsAtom: createMockAtom('workspaceRoleFlags'),
|
||||||
isCurrentWorkspaceManagerAtom: createMockAtom('isCurrentWorkspaceManager'),
|
isCurrentWorkspaceManagerAtom: createMockAtom('isCurrentWorkspaceManager'),
|
||||||
|
isCurrentWorkspaceOwnerAtom: createMockAtom('isCurrentWorkspaceOwner'),
|
||||||
currentWorkspaceLoadingAtom: createMockAtom('currentWorkspaceLoading'),
|
currentWorkspaceLoadingAtom: createMockAtom('currentWorkspaceLoading'),
|
||||||
workspacePermissionKeysAtom: createMockAtom('workspacePermissionKeys'),
|
workspacePermissionKeysAtom: createMockAtom('workspacePermissionKeys'),
|
||||||
workspacePermissionKeysLoadingAtom: createMockAtom('workspacePermissionKeysLoading'),
|
workspacePermissionKeysLoadingAtom: createMockAtom('workspacePermissionKeysLoading'),
|
||||||
langGeniusVersionInfoAtom: createMockAtom('langGeniusVersionInfo'),
|
langGeniusVersionInfoAtom: createMockAtom('langGeniusVersionInfo'),
|
||||||
langGeniusCurrentVersionAtom: createMockAtom('langGeniusCurrentVersion'),
|
langGeniusCurrentVersionAtom: createMockAtom('langGeniusCurrentVersion'),
|
||||||
|
refreshUserProfileAtom: createMockAtom('refreshUserProfile'),
|
||||||
|
refreshCurrentWorkspaceAtom: createMockAtom('refreshCurrentWorkspace'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,6 +172,9 @@ export const createAppContextStateJotaiMock = async (
|
|||||||
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'isCurrentWorkspaceManager')
|
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'isCurrentWorkspaceManager')
|
||||||
return state.isCurrentWorkspaceManager ?? false
|
return state.isCurrentWorkspaceManager ?? false
|
||||||
|
|
||||||
|
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'isCurrentWorkspaceOwner')
|
||||||
|
return state.isCurrentWorkspaceOwner ?? false
|
||||||
|
|
||||||
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'currentWorkspaceLoading')
|
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'currentWorkspaceLoading')
|
||||||
return state.isLoadingCurrentWorkspace ?? false
|
return state.isLoadingCurrentWorkspace ?? false
|
||||||
|
|
||||||
@ -179,5 +192,22 @@ export const createAppContextStateJotaiMock = async (
|
|||||||
|
|
||||||
throw new Error(`Unsupported app context state atom: ${atom[APP_CONTEXT_STATE_ATOM_KIND]}`)
|
throw new Error(`Unsupported app context state atom: ${atom[APP_CONTEXT_STATE_ATOM_KIND]}`)
|
||||||
},
|
},
|
||||||
|
useSetAtom: (atom: unknown) => {
|
||||||
|
if (!isAppContextStateMockAtom(atom))
|
||||||
|
return actual.useSetAtom(atom as Parameters<typeof actual.useSetAtom>[0])
|
||||||
|
|
||||||
|
if (!appContextStateMockRegistry)
|
||||||
|
throw new Error('App context state atom mock is not initialized')
|
||||||
|
|
||||||
|
const state = appContextStateMockRegistry.getState()
|
||||||
|
|
||||||
|
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'refreshUserProfile')
|
||||||
|
return state.refreshUserProfile ?? (() => {})
|
||||||
|
|
||||||
|
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'refreshCurrentWorkspace')
|
||||||
|
return state.refreshCurrentWorkspace ?? (() => {})
|
||||||
|
|
||||||
|
throw new Error(`Unsupported app context state write atom: ${atom[APP_CONTEXT_STATE_ATOM_KIND]}`)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
|
import { Input } from '@langgenius/dify-ui/input'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Input from '@/app/components/base/input'
|
import { userProfileEmailAtom } from '@/context/app-context-state'
|
||||||
import { useAppContext } from '@/context/app-context'
|
|
||||||
import Link from '@/next/link'
|
import Link from '@/next/link'
|
||||||
import { useSendDeleteAccountEmail } from '../state'
|
import { useSendDeleteAccountEmail } from '../state'
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ type DeleteAccountProps = {
|
|||||||
|
|
||||||
export default function CheckEmail(props: DeleteAccountProps) {
|
export default function CheckEmail(props: DeleteAccountProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { userProfile } = useAppContext()
|
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||||
const [userInputEmail, setUserInputEmail] = useState('')
|
const [userInputEmail, setUserInputEmail] = useState('')
|
||||||
|
|
||||||
const { isPending: isSendingEmail, mutateAsync: getDeleteEmailVerifyCode } = useSendDeleteAccountEmail()
|
const { isPending: isSendingEmail, mutateAsync: getDeleteEmailVerifyCode } = useSendDeleteAccountEmail()
|
||||||
@ -45,7 +46,7 @@ export default function CheckEmail(props: DeleteAccountProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="mt-3 flex w-full flex-col gap-2">
|
<div className="mt-3 flex w-full flex-col gap-2">
|
||||||
<Button className="w-full" disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant="primary" onClick={handleConfirm}>{t('account.sendVerificationButton', { ns: 'common' })}</Button>
|
<Button className="w-full" disabled={userInputEmail !== userProfileEmail || isSendingEmail} loading={isSendingEmail} variant="primary" onClick={handleConfirm}>{t('account.sendVerificationButton', { ns: 'common' })}</Button>
|
||||||
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
<Button className="w-full" onClick={props.onCancel}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import { Button } from '@langgenius/dify-ui/button'
|
|||||||
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { userProfileEmailAtom } from '@/context/app-context-state'
|
||||||
import { useRouter } from '@/next/navigation'
|
import { useRouter } from '@/next/navigation'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
import { useDeleteAccountFeedback } from '../state'
|
import { useDeleteAccountFeedback } from '../state'
|
||||||
@ -17,7 +18,7 @@ type DeleteAccountProps = {
|
|||||||
|
|
||||||
export default function FeedBack(props: DeleteAccountProps) {
|
export default function FeedBack(props: DeleteAccountProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { userProfile } = useAppContext()
|
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [userFeedback, setUserFeedback] = useState('')
|
const [userFeedback, setUserFeedback] = useState('')
|
||||||
const { isPending, mutateAsync: sendFeedback } = useDeleteAccountFeedback()
|
const { isPending, mutateAsync: sendFeedback } = useDeleteAccountFeedback()
|
||||||
@ -35,12 +36,12 @@ export default function FeedBack(props: DeleteAccountProps) {
|
|||||||
|
|
||||||
const handleSubmit = useCallback(async () => {
|
const handleSubmit = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await sendFeedback({ feedback: userFeedback, email: userProfile.email })
|
await sendFeedback({ feedback: userFeedback, email: userProfileEmail })
|
||||||
props.onConfirm()
|
props.onConfirm()
|
||||||
await handleSuccess()
|
await handleSuccess()
|
||||||
}
|
}
|
||||||
catch (error) { console.error(error) }
|
catch (error) { console.error(error) }
|
||||||
}, [handleSuccess, userFeedback, sendFeedback, userProfile, props])
|
}, [handleSuccess, userFeedback, sendFeedback, userProfileEmail, props])
|
||||||
|
|
||||||
const handleSkip = useCallback(() => {
|
const handleSkip = useCallback(() => {
|
||||||
props.onCancel()
|
props.onCancel()
|
||||||
|
|||||||
@ -43,6 +43,9 @@ vi.mock('@/app/components/base/theme-switcher', () => ({
|
|||||||
const { mockSetTheme } = vi.hoisted(() => ({
|
const { mockSetTheme } = vi.hoisted(() => ({
|
||||||
mockSetTheme: vi.fn(),
|
mockSetTheme: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
const mockAppContextState = vi.hoisted(() => ({
|
||||||
|
current: undefined as AppContextValue | undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('next-themes', () => ({
|
vi.mock('next-themes', () => ({
|
||||||
useTheme: () => ({
|
useTheme: () => ({
|
||||||
@ -55,6 +58,16 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
useAppContext: vi.fn(),
|
useAppContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current ?? {})
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: vi.fn(),
|
useProviderContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@ -63,7 +76,8 @@ vi.mock('@/context/modal-context', () => ({
|
|||||||
useModalContext: vi.fn(),
|
useModalContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/service/use-common', () => ({
|
vi.mock('@/service/use-common', async importOriginal => ({
|
||||||
|
...await importOriginal<typeof import('@/service/use-common')>(),
|
||||||
useLogout: vi.fn(),
|
useLogout: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -150,6 +164,11 @@ const baseAppContextValue: AppContextValue = {
|
|||||||
workspacePermissionKeys: [],
|
workspacePermissionKeys: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setAppContextValue = (value: AppContextValue) => {
|
||||||
|
mockAppContextState.current = value
|
||||||
|
vi.mocked(useAppContext).mockReturnValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
describe('AccountDropdown', () => {
|
describe('AccountDropdown', () => {
|
||||||
const mockPush = vi.fn()
|
const mockPush = vi.fn()
|
||||||
const mockLogout = vi.fn()
|
const mockLogout = vi.fn()
|
||||||
@ -170,7 +189,7 @@ describe('AccountDropdown', () => {
|
|||||||
mockConfig.IS_CLOUD_EDITION = false
|
mockConfig.IS_CLOUD_EDITION = false
|
||||||
mockEnv.env.NEXT_PUBLIC_SITE_ABOUT = 'show'
|
mockEnv.env.NEXT_PUBLIC_SITE_ABOUT = 'show'
|
||||||
|
|
||||||
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
|
setAppContextValue(baseAppContextValue)
|
||||||
vi.mocked(useProviderContext).mockReturnValue({
|
vi.mocked(useProviderContext).mockReturnValue({
|
||||||
isEducationAccount: false,
|
isEducationAccount: false,
|
||||||
plan: { type: Plan.sandbox },
|
plan: { type: Plan.sandbox },
|
||||||
@ -267,7 +286,7 @@ describe('AccountDropdown', () => {
|
|||||||
it('should show Compliance in Cloud Edition for workspace owner', () => {
|
it('should show Compliance in Cloud Edition for workspace owner', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
mockConfig.IS_CLOUD_EDITION = true
|
mockConfig.IS_CLOUD_EDITION = true
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
...baseAppContextValue,
|
...baseAppContextValue,
|
||||||
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
|
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
|
||||||
isCurrentWorkspaceOwner: true,
|
isCurrentWorkspaceOwner: true,
|
||||||
@ -286,7 +305,7 @@ describe('AccountDropdown', () => {
|
|||||||
it('should hide Compliance in Cloud Edition when user is not workspace owner', () => {
|
it('should hide Compliance in Cloud Edition when user is not workspace owner', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
mockConfig.IS_CLOUD_EDITION = true
|
mockConfig.IS_CLOUD_EDITION = true
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
...baseAppContextValue,
|
...baseAppContextValue,
|
||||||
isCurrentWorkspaceOwner: false,
|
isCurrentWorkspaceOwner: false,
|
||||||
})
|
})
|
||||||
@ -382,7 +401,7 @@ describe('AccountDropdown', () => {
|
|||||||
describe('Version Indicators', () => {
|
describe('Version Indicators', () => {
|
||||||
it('should show orange indicator when version is not latest', () => {
|
it('should show orange indicator when version is not latest', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
...baseAppContextValue,
|
...baseAppContextValue,
|
||||||
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
|
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
|
||||||
langGeniusVersionInfo: {
|
langGeniusVersionInfo: {
|
||||||
@ -402,7 +421,7 @@ describe('AccountDropdown', () => {
|
|||||||
|
|
||||||
it('should show green indicator when version is latest', () => {
|
it('should show green indicator when version is latest', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
...baseAppContextValue,
|
...baseAppContextValue,
|
||||||
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
|
userProfile: { ...baseAppContextValue.userProfile, name: 'User' },
|
||||||
langGeniusVersionInfo: {
|
langGeniusVersionInfo: {
|
||||||
|
|||||||
@ -10,12 +10,13 @@ import {
|
|||||||
} from '@langgenius/dify-ui/dropdown-menu'
|
} from '@langgenius/dify-ui/dropdown-menu'
|
||||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||||
import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { IS_CLOUD_EDITION } from '@/config'
|
import { IS_CLOUD_EDITION } from '@/config'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { isCurrentWorkspaceOwnerAtom, langGeniusVersionInfoAtom, userProfileAtom } from '@/context/app-context-state'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
@ -119,7 +120,9 @@ export function DefaultMenuContent({
|
|||||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const docLink = useDocLink()
|
const docLink = useDocLink()
|
||||||
const { userProfile, langGeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
|
const userProfile = useAtomValue(userProfileAtom)
|
||||||
|
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||||
|
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
|
||||||
const { isEducationAccount } = useProviderContext()
|
const { isEducationAccount } = useProviderContext()
|
||||||
const { setShowAccountSettingModal } = useModalContext()
|
const { setShowAccountSettingModal } = useModalContext()
|
||||||
|
|
||||||
|
|||||||
@ -8,11 +8,12 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@langgenius/dify-ui/dropdown-menu'
|
} from '@langgenius/dify-ui/dropdown-menu'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { resetUser } from '@/app/components/base/amplitude/utils'
|
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||||
import { useSetEducationExpiredHasNoticed, useSetEducationReverifyHasNoticed, useSetEducationReverifyPrevExpireAt } from '@/app/education-apply/storage'
|
import { useSetEducationExpiredHasNoticed, useSetEducationReverifyHasNoticed, useSetEducationReverifyPrevExpireAt } from '@/app/education-apply/storage'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { langGeniusVersionInfoAtom, userProfileAtom } from '@/context/app-context-state'
|
||||||
import { useRouter } from '@/next/navigation'
|
import { useRouter } from '@/next/navigation'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
import AccountAbout from '../account-about'
|
import AccountAbout from '../account-about'
|
||||||
@ -37,7 +38,8 @@ export default function AppSelector({
|
|||||||
const [aboutVisible, setAboutVisible] = useState(false)
|
const [aboutVisible, setAboutVisible] = useState(false)
|
||||||
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
|
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { userProfile, langGeniusVersionInfo } = useAppContext()
|
const userProfile = useAtomValue(userProfileAtom)
|
||||||
|
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||||
const clearEducationReverifyPrevExpireAt = useSetEducationReverifyPrevExpireAt()
|
const clearEducationReverifyPrevExpireAt = useSetEducationReverifyPrevExpireAt()
|
||||||
const clearEducationReverifyHasNoticed = useSetEducationReverifyHasNoticed()
|
const clearEducationReverifyHasNoticed = useSetEducationReverifyHasNoticed()
|
||||||
const clearEducationExpiredHasNoticed = useSetEducationExpiredHasNoticed()
|
const clearEducationExpiredHasNoticed = useSetEducationExpiredHasNoticed()
|
||||||
|
|||||||
@ -16,11 +16,12 @@ import {
|
|||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
} from '@langgenius/dify-ui/dropdown-menu'
|
} from '@langgenius/dify-ui/dropdown-menu'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { userProfileAtom } from '@/context/app-context-state'
|
||||||
import { useModalContext } from '@/context/modal-context'
|
import { useModalContext } from '@/context/modal-context'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import Link from '@/next/link'
|
import Link from '@/next/link'
|
||||||
@ -87,7 +88,7 @@ export function MainNavMenuContent({
|
|||||||
onLogout,
|
onLogout,
|
||||||
}: MainNavMenuContentProps) {
|
}: MainNavMenuContentProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { userProfile } = useAppContext()
|
const userProfile = useAtomValue(userProfileAtom)
|
||||||
const { isEducationAccount } = useProviderContext()
|
const { isEducationAccount } = useProviderContext()
|
||||||
const { setShowAccountSettingModal } = useModalContext()
|
const { setShowAccountSettingModal } = useModalContext()
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,18 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||||
|
workspacePermissionKeys: mocks.workspacePermissionKeys,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
const rule: AccessPolicyWithBindings = {
|
const rule: AccessPolicyWithBindings = {
|
||||||
policy: {
|
policy: {
|
||||||
id: 'app-rule-1',
|
id: 'app-rule-1',
|
||||||
|
|||||||
@ -3,11 +3,12 @@
|
|||||||
import type { AccessPolicyWithBindings } from '@/models/access-control'
|
import type { AccessPolicyWithBindings } from '@/models/access-control'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { memo, useEffect, useRef, useState } from 'react'
|
import { memo, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { hasPermission } from '@/utils/permission'
|
import { hasPermission } from '@/utils/permission'
|
||||||
import AccessRuleRow from './access-rule-row'
|
import AccessRuleRow from './access-rule-row'
|
||||||
|
|
||||||
@ -46,7 +47,7 @@ const AccessRuleSection = ({
|
|||||||
const [expanded, setExpanded] = useState(defaultExpanded)
|
const [expanded, setExpanded] = useState(defaultExpanded)
|
||||||
const listRef = useRef<HTMLDivElement>(null)
|
const listRef = useRef<HTMLDivElement>(null)
|
||||||
const anchorRef = useRef<HTMLDivElement>(null)
|
const anchorRef = useRef<HTMLDivElement>(null)
|
||||||
const workspacePermissionKeys = useAppContextWithSelector(s => s.workspacePermissionKeys)
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const canManage = hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
const canManage = hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
||||||
const ruleCount = totalCount ?? rules.length
|
const ruleCount = totalCount ?? rules.length
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,18 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||||
|
workspacePermissionKeys: mockWorkspacePermissionKeys.current,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/service/client', () => ({
|
vi.mock('@/service/client', () => ({
|
||||||
consoleQuery: {
|
consoleQuery: {
|
||||||
apiBasedExtension: {
|
apiBasedExtension: {
|
||||||
|
|||||||
@ -2,11 +2,12 @@ import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { SearchInput } from '@/app/components/base/search-input'
|
import { SearchInput } from '@/app/components/base/search-input'
|
||||||
import { SkeletonContainer, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
|
import { SkeletonContainer, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
|
||||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { consoleQuery } from '@/service/client'
|
import { consoleQuery } from '@/service/client'
|
||||||
import { hasPermission } from '@/utils/permission'
|
import { hasPermission } from '@/utils/permission'
|
||||||
import { Empty } from './empty'
|
import { Empty } from './empty'
|
||||||
@ -51,7 +52,7 @@ export function ApiBasedExtensionPage({
|
|||||||
layout,
|
layout,
|
||||||
}: ApiBasedExtensionPageProps = {}) {
|
}: ApiBasedExtensionPageProps = {}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const canManage = hasPermission(workspacePermissionKeys, 'api_extension.manage')
|
const canManage = hasPermission(workspacePermissionKeys, 'api_extension.manage')
|
||||||
const { data: apiBasedExtensions = [], isPending: isLoading } = useQuery(consoleQuery.apiBasedExtension.get.queryOptions())
|
const { data: apiBasedExtensions = [], isPending: isLoading } = useQuery(consoleQuery.apiBasedExtension.get.queryOptions())
|
||||||
const [dialogState, setDialogState] = useState<ApiBasedExtensionDialogState>(null)
|
const [dialogState, setDialogState] = useState<ApiBasedExtensionDialogState>(null)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { Button } from '@langgenius/dify-ui/button'
|
|||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback, useRef, useState } from 'react'
|
import { useCallback, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import BillingPage from '@/app/components/billing/billing-page'
|
import BillingPage from '@/app/components/billing/billing-page'
|
||||||
@ -12,7 +13,7 @@ import {
|
|||||||
ACCOUNT_SETTING_TAB,
|
ACCOUNT_SETTING_TAB,
|
||||||
} from '@/app/components/header/account-setting/constants'
|
} from '@/app/components/header/account-setting/constants'
|
||||||
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
|
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
@ -54,7 +55,7 @@ export default function AccountSetting({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { enableBilling, enableReplaceWebAppLogo } = useProviderContext()
|
const { enableBilling, enableReplaceWebAppLogo } = useProviderContext()
|
||||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { workspacePermissionKeys } = useAppContext()
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const isRbacEnabled = systemFeatures.rbac_enabled
|
const isRbacEnabled = systemFeatures.rbac_enabled
|
||||||
const canManageWorkspaceRoles = isRbacEnabled && hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
const canManageWorkspaceRoles = isRbacEnabled && hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
||||||
const canViewBilling = enableBilling && hasPermission(workspacePermissionKeys, BillingPermission.View)
|
const canViewBilling = enableBilling && hasPermission(workspacePermissionKeys, BillingPermission.View)
|
||||||
|
|||||||
@ -14,7 +14,19 @@ import { useUpdateRolesOfMember } from '@/service/access-control/use-member-role
|
|||||||
import { useMembers } from '@/service/use-common'
|
import { useMembers } from '@/service/use-common'
|
||||||
import MembersPage from '../index'
|
import MembersPage from '../index'
|
||||||
|
|
||||||
|
const mockAppContextState = vi.hoisted(() => ({
|
||||||
|
current: {} as Partial<AppContextValue>,
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||||
|
})
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
vi.mock('@/context/provider-context')
|
vi.mock('@/context/provider-context')
|
||||||
vi.mock('@/hooks/use-format-time-from-now')
|
vi.mock('@/hooks/use-format-time-from-now')
|
||||||
vi.mock('@/service/access-control/use-member-roles')
|
vi.mock('@/service/access-control/use-member-roles')
|
||||||
@ -41,6 +53,11 @@ const createRole = (overrides: Partial<Role>): Role => ({
|
|||||||
...overrides,
|
...overrides,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const setAppContextValue = (value: AppContextValue) => {
|
||||||
|
mockAppContextState.current = value
|
||||||
|
vi.mocked(useAppContext).mockReturnValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
vi.mock('../edit-workspace-modal', () => ({
|
vi.mock('../edit-workspace-modal', () => ({
|
||||||
default: ({ onCancel }: { onCancel: () => void }) => (
|
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||||
<div>
|
<div>
|
||||||
@ -181,7 +198,7 @@ describe('MembersPage', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
userProfile: { email: 'owner@example.com' },
|
userProfile: { email: 'owner@example.com' },
|
||||||
currentWorkspace: { name: 'Test Workspace', role: 'owner' } as ICurrentWorkspace,
|
currentWorkspace: { name: 'Test Workspace', role: 'owner' } as ICurrentWorkspace,
|
||||||
isCurrentWorkspaceOwner: true,
|
isCurrentWorkspaceOwner: true,
|
||||||
@ -289,7 +306,7 @@ describe('MembersPage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should hide manager controls for non-owner non-manager users', () => {
|
it('should hide manager controls for non-owner non-manager users', () => {
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
userProfile: { email: 'admin@example.com' },
|
userProfile: { email: 'admin@example.com' },
|
||||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||||
isCurrentWorkspaceOwner: false,
|
isCurrentWorkspaceOwner: false,
|
||||||
@ -390,7 +407,7 @@ describe('MembersPage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should show invite button when user is manager but not owner', () => {
|
it('should show invite button when user is manager but not owner', () => {
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
userProfile: { email: 'admin@example.com' },
|
userProfile: { email: 'admin@example.com' },
|
||||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||||
isCurrentWorkspaceOwner: false,
|
isCurrentWorkspaceOwner: false,
|
||||||
@ -405,7 +422,7 @@ describe('MembersPage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should allow admins to operate other non-owner members only', () => {
|
it('should allow admins to operate other non-owner members only', () => {
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
userProfile: { email: 'admin@example.com' },
|
userProfile: { email: 'admin@example.com' },
|
||||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||||
isCurrentWorkspaceOwner: false,
|
isCurrentWorkspaceOwner: false,
|
||||||
@ -483,7 +500,7 @@ describe('MembersPage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render role badge names from account roles', () => {
|
it('should render role badge names from account roles', () => {
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
userProfile: { email: 'admin@example.com' },
|
userProfile: { email: 'admin@example.com' },
|
||||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||||
isCurrentWorkspaceOwner: false,
|
isCurrentWorkspaceOwner: false,
|
||||||
@ -551,7 +568,7 @@ describe('MembersPage', () => {
|
|||||||
|
|
||||||
it('should not allow assigning roles from member details when target is current user', async () => {
|
it('should not allow assigning roles from member details when target is current user', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
setAppContextValue({
|
||||||
userProfile: { email: 'admin@example.com' },
|
userProfile: { email: 'admin@example.com' },
|
||||||
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||||
isCurrentWorkspaceOwner: false,
|
isCurrentWorkspaceOwner: false,
|
||||||
|
|||||||
@ -8,6 +8,16 @@ import { useWorkspacePermissions } from '@/service/use-workspace'
|
|||||||
import InviteButton from '../invite-button'
|
import InviteButton from '../invite-button'
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||||
|
currentWorkspace: { id: 'workspace-id' },
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
vi.mock('@/service/use-workspace')
|
vi.mock('@/service/use-workspace')
|
||||||
|
|
||||||
describe('InviteButton', () => {
|
describe('InviteButton', () => {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import EditWorkspaceModal from '../index'
|
import EditWorkspaceModal from '../index'
|
||||||
@ -10,6 +11,9 @@ type DialogProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let latestOnOpenChange: DialogProps['onOpenChange']
|
let latestOnOpenChange: DialogProps['onOpenChange']
|
||||||
|
const mockAppContextState = vi.hoisted(() => ({
|
||||||
|
current: {} as Partial<AppContextValue>,
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||||
Dialog: ({ children, onOpenChange }: DialogProps) => {
|
Dialog: ({ children, onOpenChange }: DialogProps) => {
|
||||||
@ -29,14 +33,26 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
useAppContext: vi.fn(),
|
useAppContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
describe('EditWorkspaceModal dialog lifecycle', () => {
|
describe('EditWorkspaceModal dialog lifecycle', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
latestOnOpenChange = undefined
|
latestOnOpenChange = undefined
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
const appContextValue = {
|
||||||
currentWorkspace: { name: 'Test Workspace' },
|
currentWorkspace: { name: 'Test Workspace' },
|
||||||
isCurrentWorkspaceOwner: true,
|
isCurrentWorkspaceOwner: true,
|
||||||
} as never)
|
} as never
|
||||||
|
mockAppContextState.current = appContextValue
|
||||||
|
vi.mocked(useAppContext).mockReturnValue(appContextValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should only call onCancel when the dialog requests closing', () => {
|
it('should only call onCancel when the dialog requests closing', () => {
|
||||||
|
|||||||
@ -10,10 +10,21 @@ import EditWorkspaceModal from '../index'
|
|||||||
const toastMocks = vi.hoisted(() => ({
|
const toastMocks = vi.hoisted(() => ({
|
||||||
mockNotify: vi.fn(),
|
mockNotify: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
const mockAppContextState = vi.hoisted(() => ({
|
||||||
|
current: {} as Partial<AppContextValue>,
|
||||||
|
}))
|
||||||
|
|
||||||
const getSaveButton = () => screen.getByRole('button', { name: /operation\.(save|saving)/i })
|
const getSaveButton = () => screen.getByRole('button', { name: /operation\.(save|saving)/i })
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||||
|
})
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
vi.mock('@/service/common')
|
vi.mock('@/service/common')
|
||||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||||
default: {
|
default: {
|
||||||
@ -34,10 +45,12 @@ describe('EditWorkspaceModal', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
const appContextValue = {
|
||||||
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||||
isCurrentWorkspaceOwner: true,
|
isCurrentWorkspaceOwner: true,
|
||||||
} as unknown as AppContextValue)
|
} as unknown as AppContextValue
|
||||||
|
mockAppContextState.current = appContextValue
|
||||||
|
vi.mocked(useAppContext).mockReturnValue(appContextValue)
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@ -2,11 +2,12 @@
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||||
|
import { Input } from '@langgenius/dify-ui/input'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useId, useMemo, useState } from 'react'
|
import { useId, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Input from '@/app/components/base/input'
|
import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/app-context-state'
|
||||||
import { useAppContext } from '@/context/app-context'
|
|
||||||
import { updateWorkspaceInfo } from '@/service/common'
|
import { updateWorkspaceInfo } from '@/service/common'
|
||||||
|
|
||||||
type IEditWorkspaceModalProps = {
|
type IEditWorkspaceModalProps = {
|
||||||
@ -14,7 +15,8 @@ type IEditWorkspaceModalProps = {
|
|||||||
}
|
}
|
||||||
const EditWorkspaceModal = ({ onCancel }: IEditWorkspaceModalProps) => {
|
const EditWorkspaceModal = ({ onCancel }: IEditWorkspaceModalProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
|
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||||
|
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
|
||||||
const [name, setName] = useState<string>(currentWorkspace.name)
|
const [name, setName] = useState<string>(currentWorkspace.name)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const inputId = useId()
|
const inputId = useId()
|
||||||
@ -82,7 +84,6 @@ const EditWorkspaceModal = ({ onCancel }: IEditWorkspaceModalProps) => {
|
|||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id={inputId}
|
id={inputId}
|
||||||
autoFocus
|
|
||||||
value={name}
|
value={name}
|
||||||
placeholder={t('account.workspaceNamePlaceholder', { ns: 'common' })}
|
placeholder={t('account.workspaceNamePlaceholder', { ns: 'common' })}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@ -4,12 +4,13 @@ import type { InvitationResult, Member } from '@/models/common'
|
|||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { NUM_INFINITE } from '@/app/components/billing/config'
|
import { NUM_INFINITE } from '@/app/components/billing/config'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom, userProfileEmailAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
@ -30,7 +31,10 @@ const MembersPage = () => {
|
|||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
const language = getAccessControlTemplateLanguage(locale)
|
const language = getAccessControlTemplateLanguage(locale)
|
||||||
|
|
||||||
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, workspacePermissionKeys } = useAppContext()
|
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||||
|
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||||
|
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
|
||||||
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const { data, refetch } = useMembers(language)
|
const { data, refetch } = useMembers(language)
|
||||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const [inviteModalVisible, setInviteModalVisible] = useState(false)
|
const [inviteModalVisible, setInviteModalVisible] = useState(false)
|
||||||
@ -162,7 +166,7 @@ const MembersPage = () => {
|
|||||||
key={account.id}
|
key={account.id}
|
||||||
member={account}
|
member={account}
|
||||||
roles={account.roles}
|
roles={account.roles}
|
||||||
isCurrentUser={userProfile.email === account.email}
|
isCurrentUser={userProfileEmail === account.email}
|
||||||
canManage={canManageMembers}
|
canManage={canManageMembers}
|
||||||
canTransferOwnership={isCurrentWorkspaceOwner && isAllowTransferWorkspace}
|
canTransferOwnership={isCurrentWorkspaceOwner && isAllowTransferWorkspace}
|
||||||
allowMultipleRoles={systemFeatures.rbac_enabled}
|
allowMultipleRoles={systemFeatures.rbac_enabled}
|
||||||
@ -213,7 +217,7 @@ const MembersPage = () => {
|
|||||||
canAssignRoles={
|
canAssignRoles={
|
||||||
canManageMembers
|
canManageMembers
|
||||||
&& detailsMember.role !== 'owner'
|
&& detailsMember.role !== 'owner'
|
||||||
&& userProfile.email !== detailsMember.email
|
&& userProfileEmail !== detailsMember.email
|
||||||
}
|
}
|
||||||
allowMultipleRoles={systemFeatures.rbac_enabled}
|
allowMultipleRoles={systemFeatures.rbac_enabled}
|
||||||
onClose={handleCloseDetails}
|
onClose={handleCloseDetails}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { RiUserAddLine } from '@remixicon/react'
|
import { RiUserAddLine } from '@remixicon/react'
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { currentWorkspaceIdAtom } from '@/context/app-context-state'
|
||||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
import { useWorkspacePermissions } from '@/service/use-workspace'
|
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||||
|
|
||||||
@ -14,9 +15,9 @@ type InviteButtonProps = {
|
|||||||
|
|
||||||
const InviteButton = (props: InviteButtonProps) => {
|
const InviteButton = (props: InviteButtonProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { currentWorkspace } = useAppContext()
|
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
|
||||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||||
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspace!.id, systemFeatures.branding.enabled)
|
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspaceId, systemFeatures.branding.enabled)
|
||||||
if (systemFeatures.branding.enabled) {
|
if (systemFeatures.branding.enabled) {
|
||||||
if (isFetchingWorkspacePermissions) {
|
if (isFetchingWorkspacePermissions) {
|
||||||
return <Loading />
|
return <Loading />
|
||||||
|
|||||||
@ -11,8 +11,19 @@ import TransferOwnershipModal from '../index'
|
|||||||
const toastMocks = vi.hoisted(() => ({
|
const toastMocks = vi.hoisted(() => ({
|
||||||
mockNotify: vi.fn(),
|
mockNotify: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
const mockAppContextState = vi.hoisted(() => ({
|
||||||
|
current: {} as Partial<AppContextValue>,
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/app-context')
|
vi.mock('@/context/app-context')
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||||
|
})
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
vi.mock('@/service/common')
|
vi.mock('@/service/common')
|
||||||
vi.mock('@/service/use-common')
|
vi.mock('@/service/use-common')
|
||||||
vi.mock('@langgenius/dify-ui/toast', () => ({
|
vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||||
@ -40,10 +51,12 @@ describe('TransferOwnershipModal', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
|
||||||
vi.mocked(useAppContext).mockReturnValue({
|
const appContextValue = {
|
||||||
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||||
userProfile: { email: 'owner@example.com', id: 'owner-id' },
|
userProfile: { email: 'owner@example.com', id: 'owner-id' },
|
||||||
} as unknown as AppContextValue)
|
} as unknown as AppContextValue
|
||||||
|
mockAppContextState.current = appContextValue
|
||||||
|
vi.mocked(useAppContext).mockReturnValue(appContextValue)
|
||||||
|
|
||||||
vi.mocked(useMembers).mockReturnValue({
|
vi.mocked(useMembers).mockReturnValue({
|
||||||
data: { accounts: [] },
|
data: { accounts: [] },
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||||
|
import { Input } from '@langgenius/dify-ui/input'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import Input from '@/app/components/base/input'
|
import { currentWorkspaceAtom, userProfileAtom } from '@/context/app-context-state'
|
||||||
import { useAppContext } from '@/context/app-context'
|
|
||||||
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
||||||
import MemberSelector from './member-selector'
|
import MemberSelector from './member-selector'
|
||||||
|
|
||||||
@ -13,15 +14,22 @@ type Props = Readonly<{
|
|||||||
show: boolean
|
show: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}>
|
}>
|
||||||
enum STEP {
|
const STEP = {
|
||||||
start = 'start',
|
start: 'start',
|
||||||
verify = 'verify',
|
verify: 'verify',
|
||||||
transfer = 'transfer',
|
transfer: 'transfer',
|
||||||
}
|
}
|
||||||
|
type Step = typeof STEP[keyof typeof STEP]
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown) => {
|
||||||
|
return error instanceof Error ? error.message : ''
|
||||||
|
}
|
||||||
|
|
||||||
const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { currentWorkspace, userProfile } = useAppContext()
|
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||||
const [step, setStep] = useState<STEP>(STEP.start)
|
const userProfile = useAtomValue(userProfileAtom)
|
||||||
|
const [step, setStep] = useState<Step>(STEP.start)
|
||||||
const [code, setCode] = useState<string>('')
|
const [code, setCode] = useState<string>('')
|
||||||
const [time, setTime] = useState<number>(0)
|
const [time, setTime] = useState<number>(0)
|
||||||
const [stepToken, setStepToken] = useState<string>('')
|
const [stepToken, setStepToken] = useState<string>('')
|
||||||
@ -71,7 +79,7 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
toast.error(`Error verifying email: ${error ? (error as any).message : ''}`)
|
toast.error(`Error verifying email: ${getErrorMessage(error)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sendCodeToOriginEmail = async () => {
|
const sendCodeToOriginEmail = async () => {
|
||||||
@ -96,7 +104,7 @@ const TransferOwnershipModal = ({ onClose, show }: Props) => {
|
|||||||
globalThis.location.reload()
|
globalThis.location.reload()
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
toast.error(`Error ownership transfer: ${error ? (error as any).message : ''}`)
|
toast.error(`Error ownership transfer: ${getErrorMessage(error)}`)
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setIsTransfer(false)
|
setIsTransfer(false)
|
||||||
|
|||||||
@ -22,6 +22,18 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||||
|
workspacePermissionKeys: mockWorkspacePermissionKeys,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => ({
|
useProviderContext: () => ({
|
||||||
plan: { type: mockPlanType },
|
plan: { type: mockPlanType },
|
||||||
|
|||||||
@ -11,6 +11,18 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
selector({ workspacePermissionKeys: mockWorkspacePermissionKeys }),
|
selector({ workspacePermissionKeys: mockWorkspacePermissionKeys }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||||
|
workspacePermissionKeys: mockWorkspacePermissionKeys,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/context/modal-context', () => ({
|
vi.mock('@/context/modal-context', () => ({
|
||||||
useModalContextSelector: (selector: (state: { setShowModelLoadBalancingModal: typeof mockSetShowModelLoadBalancingModal }) => unknown) =>
|
useModalContextSelector: (selector: (state: { setShowModelLoadBalancingModal: typeof mockSetShowModelLoadBalancingModal }) => unknown) =>
|
||||||
selector({ setShowModelLoadBalancingModal: mockSetShowModelLoadBalancingModal }),
|
selector({ setShowModelLoadBalancingModal: mockSetShowModelLoadBalancingModal }),
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type { PluginDetail } from '@/app/components/plugins/types'
|
|||||||
|
|
||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { memo, useCallback } from 'react'
|
import { memo, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
@ -14,7 +15,7 @@ import {
|
|||||||
ManageCustomModelCredentials,
|
ManageCustomModelCredentials,
|
||||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
import { IS_CE_EDITION } from '@/config'
|
||||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { useProviderContextSelector } from '@/context/provider-context'
|
import { useProviderContextSelector } from '@/context/provider-context'
|
||||||
import { useCredentialPermissions } from '@/hooks/use-credential-permissions'
|
import { useCredentialPermissions } from '@/hooks/use-credential-permissions'
|
||||||
import { renderI18nObject } from '@/i18n-config'
|
import { renderI18nObject } from '@/i18n-config'
|
||||||
@ -68,7 +69,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
|||||||
}))
|
}))
|
||||||
const hasModelList = hasFetchedModelList && !!modelList.length
|
const hasModelList = hasFetchedModelList && !!modelList.length
|
||||||
const showCollapsedSection = !expanded || !hasFetchedModelList
|
const showCollapsedSection = !expanded || !hasFetchedModelList
|
||||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const showModelProvider = systemConfig.enabled && MODEL_PROVIDER_QUOTA_GET_PAID.includes(currentProviderName as ModelProviderQuotaGetPaid) && !IS_CE_EDITION
|
const showModelProvider = systemConfig.enabled && MODEL_PROVIDER_QUOTA_GET_PAID.includes(currentProviderName as ModelProviderQuotaGetPaid) && !IS_CE_EDITION
|
||||||
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
||||||
const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions()
|
const { canUseCredential, canCreateCredential, canManageCredential } = useCredentialPermissions()
|
||||||
|
|||||||
@ -4,12 +4,13 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
|
|||||||
import { Switch } from '@langgenius/dify-ui/switch'
|
import { Switch } from '@langgenius/dify-ui/switch'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useDebounceFn } from 'ahooks'
|
import { useDebounceFn } from 'ahooks'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { memo, useCallback } from 'react'
|
import { memo, useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Badge from '@/app/components/base/badge'
|
import Badge from '@/app/components/base/badge'
|
||||||
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||||
import { Plan } from '@/app/components/billing/type'
|
import { Plan } from '@/app/components/billing/type'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
|
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
|
||||||
import { consoleQuery } from '@/service/client'
|
import { consoleQuery } from '@/service/client'
|
||||||
import { disableModel, enableModel } from '@/service/common'
|
import { disableModel, enableModel } from '@/service/common'
|
||||||
@ -32,7 +33,7 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { plan } = useProviderContext()
|
const { plan } = useProviderContext()
|
||||||
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
|
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
|
||||||
const { workspacePermissionKeys } = useAppContext()
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const updateModelList = useUpdateModelList()
|
const updateModelList = useUpdateModelList()
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import type {
|
|||||||
ModelItem,
|
ModelItem,
|
||||||
ModelProvider,
|
ModelProvider,
|
||||||
} from '../declarations'
|
} from '../declarations'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
AddCustomModel,
|
AddCustomModel,
|
||||||
ManageCustomModelCredentials,
|
ManageCustomModelCredentials,
|
||||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
} from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { useModalContextSelector } from '@/context/modal-context'
|
import { useModalContextSelector } from '@/context/modal-context'
|
||||||
import { hasPermission } from '@/utils/permission'
|
import { hasPermission } from '@/utils/permission'
|
||||||
import {
|
import {
|
||||||
@ -33,7 +34,7 @@ const ModelList: FC<ModelListProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
|
const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
|
||||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
||||||
const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
|
const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
|
||||||
const setShowModelLoadBalancingModal = useModalContextSelector(state => state.setShowModelLoadBalancingModal)
|
const setShowModelLoadBalancingModal = useModalContextSelector(state => state.setShowModelLoadBalancingModal)
|
||||||
@ -58,13 +59,14 @@ const ModelList: FC<ModelListProps> = ({
|
|||||||
{t('modelProvider.modelsNum', { ns: 'common', num: models.length })}
|
{t('modelProvider.modelsNum', { ns: 'common', num: models.length })}
|
||||||
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
|
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<button
|
||||||
className="hidden h-6 cursor-pointer items-center rounded-lg bg-state-base-hover pr-1.5 pl-1 system-xs-medium text-text-tertiary group-hover:inline-flex"
|
type="button"
|
||||||
|
className="hidden h-6 cursor-pointer items-center rounded-lg border-none bg-state-base-hover pr-1.5 pl-1 system-xs-medium text-text-tertiary group-hover:inline-flex"
|
||||||
onClick={() => onCollapse()}
|
onClick={() => onCollapse()}
|
||||||
>
|
>
|
||||||
{t('modelProvider.modelsNum', { ns: 'common', num: models.length })}
|
{t('modelProvider.modelsNum', { ns: 'common', num: models.length })}
|
||||||
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
|
<span className="mr-0.5 i-ri-arrow-right-s-line size-4 rotate-90" />
|
||||||
</span>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
{
|
{
|
||||||
isConfigurable && canConfigureModels && (
|
isConfigurable && canConfigureModels && (
|
||||||
|
|||||||
@ -40,6 +40,18 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||||
|
workspacePermissionKeys: mockWorkspacePermissionKeys,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: () => ({
|
useProviderContext: () => ({
|
||||||
textGenerationModelList: [],
|
textGenerationModelList: [],
|
||||||
|
|||||||
@ -12,10 +12,11 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@langgenius/dify-ui/dialog'
|
} from '@langgenius/dify-ui/dialog'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Infotip } from '@/app/components/base/infotip'
|
import { Infotip } from '@/app/components/base/infotip'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
import { updateDefaultModel } from '@/service/common'
|
import { updateDefaultModel } from '@/service/common'
|
||||||
import { hasPermission } from '@/utils/permission'
|
import { hasPermission } from '@/utils/permission'
|
||||||
@ -68,7 +69,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
|
|||||||
onOpenMarketplace,
|
onOpenMarketplace,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { workspacePermissionKeys } = useAppContext()
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const { textGenerationModelList } = useProviderContext()
|
const { textGenerationModelList } = useProviderContext()
|
||||||
const canManageSystemDefaultModel = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
const canManageSystemDefaultModel = hasPermission(workspacePermissionKeys, 'plugin.model_config')
|
||||||
const updateModelList = useUpdateModelList()
|
const updateModelList = useUpdateModelList()
|
||||||
|
|||||||
@ -26,6 +26,18 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
} as AppContextValue)),
|
} as AppContextValue)),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||||
|
workspacePermissionKeys: mocks.workspacePermissionKeys,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/service/access-control/use-workspace-roles', () => ({
|
vi.mock('@/service/access-control/use-workspace-roles', () => ({
|
||||||
useCreateWorkspaceRole: vi.fn(),
|
useCreateWorkspaceRole: vi.fn(),
|
||||||
useUpdateWorkspaceRole: vi.fn(),
|
useUpdateWorkspaceRole: vi.fn(),
|
||||||
|
|||||||
@ -4,9 +4,10 @@ import type { RoleModalMode, submitRoleData } from './role-modal'
|
|||||||
import type { Role } from '@/models/access-control'
|
import type { Role } from '@/models/access-control'
|
||||||
import { Button } from '@langgenius/dify-ui/button'
|
import { Button } from '@langgenius/dify-ui/button'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import { getAccessControlTemplateLanguage } from '@/i18n-config/language'
|
import { getAccessControlTemplateLanguage } from '@/i18n-config/language'
|
||||||
import { useCreateWorkspaceRole, useUpdateWorkspaceRole } from '@/service/access-control/use-workspace-roles'
|
import { useCreateWorkspaceRole, useUpdateWorkspaceRole } from '@/service/access-control/use-workspace-roles'
|
||||||
@ -32,7 +33,7 @@ const PermissionsPage = ({ containerRef }: PermissionsPageProps) => {
|
|||||||
const [modalState, setModalState] = useState<ModalState>(null)
|
const [modalState, setModalState] = useState<ModalState>(null)
|
||||||
const anchorRef = useRef<HTMLDivElement>(null)
|
const anchorRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const workspacePermissionKeys = useAppContextWithSelector(s => s.workspacePermissionKeys)
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
|
|
||||||
const language = useMemo(() => getAccessControlTemplateLanguage(locale), [locale])
|
const language = useMemo(() => getAccessControlTemplateLanguage(locale), [locale])
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,18 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||||
|
workspacePermissionKeys: mockWorkspacePermissionKeys.value,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/service/access-control/use-workspace-roles', () => ({
|
vi.mock('@/service/access-control/use-workspace-roles', () => ({
|
||||||
useCopyWorkspaceRole: () => ({
|
useCopyWorkspaceRole: () => ({
|
||||||
mutate: mockCopyRole,
|
mutate: mockCopyRole,
|
||||||
|
|||||||
@ -18,10 +18,11 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@langgenius/dify-ui/dropdown-menu'
|
} from '@langgenius/dify-ui/dropdown-menu'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
|
||||||
import { useCopyWorkspaceRole, useDeleteWorkspaceRole } from '@/service/access-control/use-workspace-roles'
|
import { useCopyWorkspaceRole, useDeleteWorkspaceRole } from '@/service/access-control/use-workspace-roles'
|
||||||
import { hasPermission } from '@/utils/permission'
|
import { hasPermission } from '@/utils/permission'
|
||||||
import { CopyMembersConfirmDialog } from './copy-members-confirm-dialog'
|
import { CopyMembersConfirmDialog } from './copy-members-confirm-dialog'
|
||||||
@ -44,7 +45,7 @@ const RowMenu = ({
|
|||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
const [showCopyMembersConfirm, setShowCopyMembersConfirm] = useState(false)
|
const [showCopyMembersConfirm, setShowCopyMembersConfirm] = useState(false)
|
||||||
|
|
||||||
const workspacePermissionKeys = useAppContextWithSelector(s => s.workspacePermissionKeys)
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||||
const canManageRoles = hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
const canManageRoles = hasPermission(workspacePermissionKeys, 'workspace.role.manage')
|
||||||
|
|
||||||
const handleView = useCallback(() => onView?.(role), [onView, role])
|
const handleView = useCallback(() => onView?.(role), [onView, role])
|
||||||
|
|||||||
@ -75,6 +75,19 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => ({
|
||||||
|
userProfile: mockUserProfile,
|
||||||
|
refreshUserProfile: mockMutateUserProfile,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/context/i18n', () => ({
|
vi.mock('@/context/i18n', () => ({
|
||||||
useLocale: () => mockLocale,
|
useLocale: () => mockLocale,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@ -2,10 +2,11 @@
|
|||||||
import type { Locale } from '@/i18n-config'
|
import type { Locale } from '@/i18n-config'
|
||||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { useTheme } from 'next-themes'
|
import { useTheme } from 'next-themes'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { refreshUserProfileAtom, userProfileAtom } from '@/context/app-context-state'
|
||||||
import { useLocale } from '@/context/i18n'
|
import { useLocale } from '@/context/i18n'
|
||||||
import { setLocaleOnClient } from '@/i18n-config'
|
import { setLocaleOnClient } from '@/i18n-config'
|
||||||
import { languages } from '@/i18n-config/language'
|
import { languages } from '@/i18n-config/language'
|
||||||
@ -35,7 +36,8 @@ const isThemeOption = (value: string): value is ThemeOption => {
|
|||||||
|
|
||||||
export default function PreferencePage() {
|
export default function PreferencePage() {
|
||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
const { userProfile, mutateUserProfile } = useAppContext()
|
const userProfile = useAtomValue(userProfileAtom)
|
||||||
|
const refreshUserProfile = useSetAtom(refreshUserProfileAtom)
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -77,7 +79,7 @@ export default function PreferencePage() {
|
|||||||
try {
|
try {
|
||||||
await updateUserProfile({ url, body: { [bodyKey]: item.value } })
|
await updateUserProfile({ url, body: { [bodyKey]: item.value } })
|
||||||
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' }))
|
||||||
mutateUserProfile()
|
refreshUserProfile()
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
toast.error((e as Error).message)
|
toast.error((e as Error).message)
|
||||||
|
|||||||
@ -7,12 +7,13 @@ import { Button } from '@langgenius/dify-ui/button'
|
|||||||
import { cn } from '@langgenius/dify-ui/cn'
|
import { cn } from '@langgenius/dify-ui/cn'
|
||||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle, DialogTrigger } from '@langgenius/dify-ui/dialog'
|
import { Dialog, DialogCloseButton, DialogContent, DialogTitle, DialogTrigger } from '@langgenius/dify-ui/dialog'
|
||||||
import { toast } from '@langgenius/dify-ui/toast'
|
import { toast } from '@langgenius/dify-ui/toast'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useCallback, useMemo, useState } from 'react'
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||||
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils'
|
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { userProfileAtom } from '@/context/app-context-state'
|
||||||
import { useMutationPluginAutoUpgradeSettings, usePluginAutoUpgradeSettings } from '@/service/use-plugins'
|
import { useMutationPluginAutoUpgradeSettings, usePluginAutoUpgradeSettings } from '@/service/use-plugins'
|
||||||
import UpdateSettingDialogForm from './update-setting-dialog-form'
|
import UpdateSettingDialogForm from './update-setting-dialog-form'
|
||||||
|
|
||||||
@ -26,7 +27,7 @@ const UpdateSettingDialog = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { userProfile } = useAppContext()
|
const userProfile = useAtomValue(userProfileAtom)
|
||||||
const timezone = userProfile.timezone || 'UTC'
|
const timezone = userProfile.timezone || 'UTC'
|
||||||
const {
|
const {
|
||||||
data: autoUpgradeSetting,
|
data: autoUpgradeSetting,
|
||||||
|
|||||||
@ -4,10 +4,24 @@ import { vi } from 'vitest'
|
|||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import EnvNav from '../index'
|
import EnvNav from '../index'
|
||||||
|
|
||||||
|
const mockAppContextState = vi.hoisted(() => ({
|
||||||
|
current: {} as Partial<AppContextValue>,
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/context/app-context', () => ({
|
vi.mock('@/context/app-context', () => ({
|
||||||
useAppContext: vi.fn(),
|
useAppContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
describe('EnvNav', () => {
|
describe('EnvNav', () => {
|
||||||
const mockUseAppContext = vi.mocked(useAppContext)
|
const mockUseAppContext = vi.mocked(useAppContext)
|
||||||
|
|
||||||
@ -16,33 +30,39 @@ describe('EnvNav', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render null when environment is PRODUCTION', () => {
|
it('should render null when environment is PRODUCTION', () => {
|
||||||
mockUseAppContext.mockReturnValue({
|
const appContextValue = {
|
||||||
langGeniusVersionInfo: {
|
langGeniusVersionInfo: {
|
||||||
current_env: 'PRODUCTION',
|
current_env: 'PRODUCTION',
|
||||||
},
|
},
|
||||||
} as unknown as AppContextValue)
|
} as unknown as AppContextValue
|
||||||
|
mockAppContextState.current = appContextValue
|
||||||
|
mockUseAppContext.mockReturnValue(appContextValue)
|
||||||
|
|
||||||
const { container } = render(<EnvNav />)
|
const { container } = render(<EnvNav />)
|
||||||
expect(container.firstChild).toBeNull()
|
expect(container.firstChild).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render TESTING tag and icon when environment is TESTING', () => {
|
it('should render TESTING tag and icon when environment is TESTING', () => {
|
||||||
mockUseAppContext.mockReturnValue({
|
const appContextValue = {
|
||||||
langGeniusVersionInfo: {
|
langGeniusVersionInfo: {
|
||||||
current_env: 'TESTING',
|
current_env: 'TESTING',
|
||||||
},
|
},
|
||||||
} as unknown as AppContextValue)
|
} as unknown as AppContextValue
|
||||||
|
mockAppContextState.current = appContextValue
|
||||||
|
mockUseAppContext.mockReturnValue(appContextValue)
|
||||||
|
|
||||||
render(<EnvNav />)
|
render(<EnvNav />)
|
||||||
expect(screen.getByText('common.environment.testing')).toBeInTheDocument()
|
expect(screen.getByText('common.environment.testing')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render DEVELOPMENT tag and icon when environment is DEVELOPMENT', () => {
|
it('should render DEVELOPMENT tag and icon when environment is DEVELOPMENT', () => {
|
||||||
mockUseAppContext.mockReturnValue({
|
const appContextValue = {
|
||||||
langGeniusVersionInfo: {
|
langGeniusVersionInfo: {
|
||||||
current_env: 'DEVELOPMENT',
|
current_env: 'DEVELOPMENT',
|
||||||
},
|
},
|
||||||
} as unknown as AppContextValue)
|
} as unknown as AppContextValue
|
||||||
|
mockAppContextState.current = appContextValue
|
||||||
|
mockUseAppContext.mockReturnValue(appContextValue)
|
||||||
|
|
||||||
render(<EnvNav />)
|
render(<EnvNav />)
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { TerminalSquare } from '@/app/components/base/icons/src/vender/solid/development'
|
import { TerminalSquare } from '@/app/components/base/icons/src/vender/solid/development'
|
||||||
import { Beaker02 } from '@/app/components/base/icons/src/vender/solid/education'
|
import { Beaker02 } from '@/app/components/base/icons/src/vender/solid/education'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { langGeniusVersionInfoAtom } from '@/context/app-context-state'
|
||||||
|
|
||||||
const headerEnvClassName: { [k: string]: string } = {
|
const headerEnvClassName: { [k: string]: string } = {
|
||||||
DEVELOPMENT: 'bg-[#FEC84B] border-[#FDB022] text-[#93370D]',
|
DEVELOPMENT: 'bg-[#FEC84B] border-[#FDB022] text-[#93370D]',
|
||||||
@ -12,7 +13,7 @@ const headerEnvClassName: { [k: string]: string } = {
|
|||||||
|
|
||||||
const EnvNav = () => {
|
const EnvNav = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { langGeniusVersionInfo } = useAppContext()
|
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||||
const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT'
|
const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT'
|
||||||
|
|
||||||
if (!showEnvTag)
|
if (!showEnvTag)
|
||||||
|
|||||||
@ -30,6 +30,9 @@ const { mockIsAgentV2Enabled, mockSwitchWorkspace, mockToastSuccess } = vi.hoist
|
|||||||
mockToastSuccess: vi.fn(),
|
mockToastSuccess: vi.fn(),
|
||||||
mockIsAgentV2Enabled: vi.fn(() => true),
|
mockIsAgentV2Enabled: vi.fn(() => true),
|
||||||
}))
|
}))
|
||||||
|
const mockAppContextState = vi.hoisted(() => ({
|
||||||
|
current: undefined as AppContextValue | undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/features/agent-v2/feature-flag', () => ({
|
vi.mock('@/features/agent-v2/feature-flag', () => ({
|
||||||
isAgentV2Enabled: () => mockIsAgentV2Enabled(),
|
isAgentV2Enabled: () => mockIsAgentV2Enabled(),
|
||||||
@ -40,6 +43,16 @@ vi.mock('@/context/app-context', () => ({
|
|||||||
useSelector: vi.fn(),
|
useSelector: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context-state', async (importOriginal) => {
|
||||||
|
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current ?? {})
|
||||||
|
})
|
||||||
|
|
||||||
|
vi.mock('jotai', async (importOriginal) => {
|
||||||
|
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
|
||||||
|
return createAppContextStateJotaiMock(importOriginal)
|
||||||
|
})
|
||||||
|
|
||||||
vi.mock('@/context/provider-context', () => ({
|
vi.mock('@/context/provider-context', () => ({
|
||||||
useProviderContext: vi.fn(),
|
useProviderContext: vi.fn(),
|
||||||
}))
|
}))
|
||||||
@ -240,6 +253,7 @@ const renderMainNav = (
|
|||||||
const queryClient = createTestQueryClient()
|
const queryClient = createTestQueryClient()
|
||||||
const getMockAppContext = useAppContext as Mock
|
const getMockAppContext = useAppContext as Mock
|
||||||
const currentAppContext = getMockAppContext() as AppContextValue
|
const currentAppContext = getMockAppContext() as AppContextValue
|
||||||
|
mockAppContextState.current = currentAppContext
|
||||||
queryClient.setQueryData(consoleQuery.workspaces.current.post.queryKey(), currentAppContext.currentWorkspace)
|
queryClient.setQueryData(consoleQuery.workspaces.current.post.queryKey(), currentAppContext.currentWorkspace)
|
||||||
queryClient.setQueryData(consoleQuery.workspaces.get.queryKey(), { workspaces: mockWorkspaces })
|
queryClient.setQueryData(consoleQuery.workspaces.get.queryKey(), { workspaces: mockWorkspaces })
|
||||||
const resolvedSystemFeatures = {
|
const resolvedSystemFeatures = {
|
||||||
@ -287,6 +301,7 @@ describe('MainNav', () => {
|
|||||||
refresh: vi.fn(),
|
refresh: vi.fn(),
|
||||||
})
|
})
|
||||||
;(useAppContext as Mock).mockReturnValue(appContextValue)
|
;(useAppContext as Mock).mockReturnValue(appContextValue)
|
||||||
|
mockAppContextState.current = appContextValue
|
||||||
;(useAppContextSelector as Mock).mockImplementation((selector: (state: AppContextValue) => unknown) => selector((useAppContext as Mock)() as AppContextValue))
|
;(useAppContextSelector as Mock).mockImplementation((selector: (state: AppContextValue) => unknown) => selector((useAppContext as Mock)() as AppContextValue))
|
||||||
;(useProviderContext as Mock).mockReturnValue({
|
;(useProviderContext as Mock).mockReturnValue({
|
||||||
enableBilling: true,
|
enableBilling: true,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user