refactor(web): migrate plugins and tools app context consumers (#38533)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou 2026-07-08 14:26:56 +08:00 committed by GitHub
parent 98f67e9c82
commit 503d80be1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 745 additions and 189 deletions

View File

@ -3457,14 +3457,6 @@
"count": 2
}
},
"web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.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/model-auth/add-custom-model.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 2

View File

@ -44,7 +44,13 @@ vi.mock('@/context/app-context', () => ({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: {
current_env: 'CLOUD',
current_version: '1.0.0',
latest_version: '1.0.0',
version: '1.0.0',
release_date: '',
release_notes: '',
can_auto_update: false,
},
workspacePermissionKeys: [
'plugin.install',
@ -54,6 +60,33 @@ 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, () => ({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: {
current_env: 'CLOUD',
current_version: '1.0.0',
latest_version: '1.0.0',
version: '1.0.0',
release_date: '',
release_notes: '',
can_auto_update: false,
},
workspacePermissionKeys: [
'plugin.install',
'plugin.delete',
'plugin.plugin_preferences',
],
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/service/use-plugins', () => ({
hasPluginPermission: () => true,
useReferenceSettings: () => ({

View File

@ -53,6 +53,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, () => ({
isCurrentWorkspaceManager: true,
workspacePermissionKeys: ['tool.manage', 'credential.create', 'credential.manage', 'credential.use'],
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
const mockSetShowModelModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({

View File

@ -14,6 +14,10 @@ export type AppContextStateMockState = {
currentWorkspace?: {
id?: string
} | null
isCurrentWorkspaceManager?: boolean
isCurrentWorkspaceOwner?: boolean
isCurrentWorkspaceEditor?: boolean
isCurrentWorkspaceDatasetOperator?: boolean
isLoadingCurrentWorkspace?: boolean
isLoadingWorkspacePermissionKeys?: boolean
workspacePermissionKeys?: string[]
@ -25,6 +29,7 @@ type AppContextStateAtomKind
| 'userProfileId'
| 'currentWorkspace'
| 'currentWorkspaceId'
| 'workspaceRoleFlags'
| 'currentWorkspaceLoading'
| 'workspacePermissionKeys'
| 'workspacePermissionKeysLoading'
@ -98,6 +103,7 @@ export const createAppContextStateAtomMock = async (
userProfileIdAtom: createMockAtom('userProfileId'),
currentWorkspaceAtom: createMockAtom('currentWorkspace'),
currentWorkspaceIdAtom: createMockAtom('currentWorkspaceId'),
workspaceRoleFlagsAtom: createMockAtom('workspaceRoleFlags'),
currentWorkspaceLoadingAtom: createMockAtom('currentWorkspaceLoading'),
workspacePermissionKeysAtom: createMockAtom('workspacePermissionKeys'),
workspacePermissionKeysLoadingAtom: createMockAtom('workspacePermissionKeysLoading'),
@ -135,6 +141,15 @@ export const createAppContextStateJotaiMock = async (
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'currentWorkspaceId')
return currentWorkspace.id
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'workspaceRoleFlags') {
return {
isCurrentWorkspaceManager: state.isCurrentWorkspaceManager ?? false,
isCurrentWorkspaceOwner: state.isCurrentWorkspaceOwner ?? false,
isCurrentWorkspaceEditor: state.isCurrentWorkspaceEditor ?? false,
isCurrentWorkspaceDatasetOperator: state.isCurrentWorkspaceDatasetOperator ?? false,
}
}
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'currentWorkspaceLoading')
return state.isLoadingCurrentWorkspace ?? false

View File

@ -116,6 +116,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, () => ({
isCurrentWorkspaceManager: true,
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
error: (...args: unknown[]) => mockToastError(...args),

View File

@ -31,6 +31,16 @@ vi.mock('@/context/app-context', async (importOriginal) => {
}
})
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('@/next/navigation', () => ({
useRouter: vi.fn(() => ({
push: vi.fn(),
@ -77,10 +87,14 @@ vi.mock('@/service/use-datasource', () => ({
useGetDataSourceListAuth: vi.fn(() => ({ data: { result: [] } })),
}))
vi.mock('@/service/use-common', () => ({
useMembers: vi.fn(() => ({ data: { accounts: [] }, refetch: vi.fn() })),
useProviderContext: vi.fn(),
}))
vi.mock('@/service/use-common', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/use-common')>()
return {
...actual,
useMembers: vi.fn(() => ({ data: { accounts: [] }, refetch: vi.fn() })),
useProviderContext: vi.fn(),
}
})
vi.mock('@/service/client', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/client')>()

View File

@ -19,6 +19,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('@/app/components/plugins/plugin-auth', () => ({
ApiKeyModal: vi.fn(({ onClose, onUpdate, onRemove, disabled, editValues }: { onClose: () => void, onUpdate: () => void, onRemove: () => void, disabled: boolean, editValues: Record<string, unknown> }) => (
<div data-testid="mock-api-key-modal" data-disabled={disabled}>

View File

@ -13,6 +13,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('@/app/components/header/account-setting/model-provider-page/model-auth', () => ({
Authorized: ({
renderTrigger,

View File

@ -34,6 +34,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)
})
// Mock components
vi.mock('../../model-icon', () => ({
default: () => <div data-testid="model-icon" />,

View File

@ -14,6 +14,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)
})
// Mock components
vi.mock('../authorized', () => ({
default: ({
@ -37,7 +49,12 @@ vi.mock('../authorized', () => ({
data-hide-add-action={String(!!hideAddAction)}
data-trigger-only-open-modal={String(!!triggerOnlyOpenModal)}
>
<div data-testid="trigger-container" onClick={() => onItemClick?.(items[0]!.credentials[0])}>
<div
role="presentation"
data-testid="trigger-container"
onClick={() => onItemClick?.(items[0]!.credentials[0])}
onKeyDown={() => undefined}
>
{renderTrigger()}
</div>
</div>

View File

@ -19,6 +19,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('../../hooks', () => ({
useAuth: () => ({
openConfirmDelete: mockOpenConfirmDelete,

View File

@ -80,6 +80,18 @@ vi.mock('@/context/app-context', () => ({
selector({ workspacePermissionKeys: mockState.workspacePermissionKeys }),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
workspacePermissionKeys: mockState.workspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/hooks/use-i18n', () => ({
useRenderI18nObject: () => (value: { en_US: string }) => value.en_US,
}))

View File

@ -94,6 +94,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)
})
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
model: 'gpt-4',
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },

View File

@ -43,6 +43,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, () => ({
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager,
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
// Mock internal components to simplify testing of the index file
vi.mock('../credential-panel', () => ({
default: () => <div data-testid="credential-panel" />,

View File

@ -9,6 +9,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: ['credential.use', 'credential.create', 'credential.manage'],
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
const createCredential = (overrides: Partial<Credential> = {}): Credential => ({
credential_id: 'cred-1',
credential_name: 'Test API Key',

View File

@ -41,6 +41,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('../../../model-auth/authorized/credential-item', () => ({
default: ({ credential, disabled, disableEdit, disableDelete, onItemClick, onEdit, onDelete }: {
credential: { credential_id: string, credential_name: string }

View File

@ -103,6 +103,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: mockAppContextState.workspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
let mockCheckedInstalledData: { plugins: { id: string, name: string }[] } | null = null
const mockInvalidateInstalledPluginList = vi.fn()
vi.mock('@/service/use-plugins', () => ({

View File

@ -41,11 +41,17 @@ vi.mock('@/utils/format', () => ({
formatNumber: (num: number) => num.toLocaleString(),
}))
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (value: { currentWorkspace: { id: string } }) => string) => selector({
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
currentWorkspace: { id: 'workspace-123' },
}),
}))
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/utils/mcp', () => ({
shouldUseMcpIcon: (src: unknown) => typeof src === 'object' && src !== null && (src as { content?: string })?.content === '🔗',

View File

@ -1,9 +1,10 @@
'use client'
import type { Plugin } from '../types'
import { cn } from '@langgenius/dify-ui/cn'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useTranslation } from '#i18n'
import { useSelector } from '@/context/app-context'
import { currentWorkspaceIdAtom } from '@/context/app-context-state'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import {
@ -61,7 +62,7 @@ const Card = ({
const locale = useGetLanguage()
const { t } = useTranslation()
const { categoriesMap } = useCategories(true)
const currentWorkspaceId = useSelector(s => s.currentWorkspace.id)
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
const { category, type, name, org, label, brief, icon, icon_dark, verified, from } = payload
const badges = payload.badges ?? []
const { theme } = useTheme()

View File

@ -2,15 +2,27 @@ import { renderHook } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import useGetIcon from '../use-get-icon'
const mockCurrentWorkspaceIdAtom = vi.hoisted(() => Symbol('currentWorkspaceIdAtom'))
vi.mock('@/config', () => ({
API_PREFIX: 'https://api.example.com',
}))
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { currentWorkspace: { id: string } }) => string | { id: string }) =>
selector({ currentWorkspace: { id: 'workspace-123' } }),
vi.mock('@/context/app-context-state', () => ({
currentWorkspaceIdAtom: mockCurrentWorkspaceIdAtom,
}))
vi.mock('jotai', () => {
return {
useAtomValue: (atom: unknown) => {
if (atom === mockCurrentWorkspaceIdAtom)
return 'workspace-123'
throw new Error('Unexpected atom')
},
}
})
describe('useGetIcon', () => {
it('builds icon url with current workspace id', () => {
const { result } = renderHook(() => useGetIcon())

View File

@ -1,12 +1,13 @@
import { useAtomValue } from 'jotai'
import { useCallback } from 'react'
import { API_PREFIX } from '@/config'
import { useSelector } from '@/context/app-context'
import { currentWorkspaceIdAtom } from '@/context/app-context-state'
const useGetIcon = () => {
const currentWorkspace = useSelector(s => s.currentWorkspace)
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
const getIconUrl = useCallback((fileName: string) => {
return `${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${currentWorkspace.id}&filename=${fileName}`
}, [currentWorkspace.id])
return `${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${currentWorkspaceId}&filename=${fileName}`
}, [currentWorkspaceId])
return {
getIconUrl,

View File

@ -4,12 +4,26 @@ import useWorkspacePluginInstallPermission from '../use-workspace-plugin-install
let mockWorkspacePermissionKeys: string[] = []
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
langGeniusVersionInfo: { current_version: '1.0.0' },
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
langGeniusVersionInfo: {
current_env: '',
current_version: '1.0.0',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
},
workspacePermissionKeys: mockWorkspacePermissionKeys,
}),
}))
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
describe('useWorkspacePluginInstallPermission', () => {
beforeEach(() => {

View File

@ -1,12 +1,11 @@
import { useAtomValue } from 'jotai'
import { useMemo } from 'react'
import { useAppContext } from '@/context/app-context'
import { langGeniusVersionInfoAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { hasPermission } from '@/utils/permission'
const useWorkspacePluginInstallPermission = () => {
const {
langGeniusVersionInfo,
workspacePermissionKeys,
} = useAppContext()
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canInstallPlugin = useMemo(() => {
return hasPermission(workspacePermissionKeys, 'plugin.install')

View File

@ -57,11 +57,22 @@ vi.mock('../../../base/check-task-status', () => ({
}),
}))
const mockLangGeniusVersionInfo = { current_version: '1.0.0' }
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
langGeniusVersionInfo: mockLangGeniusVersionInfo,
}),
const mockAppContextState = vi.hoisted(() => ({
langGeniusVersionInfoAtom: Symbol('langGeniusVersionInfoAtom'),
langGeniusVersionInfo: { current_version: '1.0.0' as string | undefined },
}))
vi.mock('@/context/app-context-state', () => ({
langGeniusVersionInfoAtom: mockAppContextState.langGeniusVersionInfoAtom,
}))
vi.mock('jotai', () => ({
useAtomValue: (atom: unknown) => {
if (atom === mockAppContextState.langGeniusVersionInfoAtom)
return mockAppContextState.langGeniusVersionInfo
throw new Error('Unexpected atom')
},
}))
vi.mock('../../../../card', () => ({
@ -466,7 +477,7 @@ describe('Install', () => {
// ================================
describe('Dify Version Compatibility', () => {
it('should not show warning when dify version is compatible', () => {
mockLangGeniusVersionInfo.current_version = '1.0.0'
mockAppContextState.langGeniusVersionInfo.current_version = '1.0.0'
const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '0.8.0' } })
render(<Install {...defaultProps} payload={payload} />)
@ -475,7 +486,7 @@ describe('Install', () => {
})
it('should show warning when dify version is incompatible', () => {
mockLangGeniusVersionInfo.current_version = '1.0.0'
mockAppContextState.langGeniusVersionInfo.current_version = '1.0.0'
const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '2.0.0' } })
render(<Install {...defaultProps} payload={payload} />)
@ -484,7 +495,7 @@ describe('Install', () => {
})
it('should be compatible when minimum_dify_version is undefined', () => {
mockLangGeniusVersionInfo.current_version = '1.0.0'
mockAppContextState.langGeniusVersionInfo.current_version = '1.0.0'
const payload = createMockManifest({ meta: { version: '1.0.0' } })
render(<Install {...defaultProps} payload={payload} />)
@ -493,7 +504,7 @@ describe('Install', () => {
})
it('should be compatible when current_version is empty', () => {
mockLangGeniusVersionInfo.current_version = ''
mockAppContextState.langGeniusVersionInfo.current_version = ''
const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '2.0.0' } })
render(<Install {...defaultProps} payload={payload} />)
@ -503,7 +514,7 @@ describe('Install', () => {
})
it('should be compatible when current_version is undefined', () => {
mockLangGeniusVersionInfo.current_version = undefined as unknown as string
mockAppContextState.langGeniusVersionInfo.current_version = undefined as unknown as string
const payload = createMockManifest({ meta: { version: '1.0.0', minimum_dify_version: '2.0.0' } })
render(<Install {...defaultProps} payload={payload} />)

View File

@ -3,11 +3,12 @@ import type { FC } from 'react'
import type { PluginDeclaration } from '../../../types'
import { Button } from '@langgenius/dify-ui/button'
import { RiLoader2Line } from '@remixicon/react'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useEffect, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { useAppContext } from '@/context/app-context'
import { langGeniusVersionInfoAtom } from '@/context/app-context-state'
import { uninstallPlugin } from '@/service/plugins'
import { useInstallPackageFromLocal, usePluginTaskList } from '@/service/use-plugins'
import { isEqualOrLaterThanVersion } from '@/utils/semver'
@ -108,7 +109,7 @@ const Installed: FC<Props> = ({
}
}
const { langGeniusVersionInfo } = useAppContext()
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const isDifyVersionCompatible = useMemo(() => {
if (!langGeniusVersionInfo.current_version)
return true

View File

@ -60,7 +60,10 @@ const mockStopTaskStatus = vi.fn()
const mockHandleInstallTaskStart = vi.fn()
let mockPluginDeclaration: { manifest: { meta: { minimum_dify_version: string } } } | undefined
let mockCanInstall = true
let mockLangGeniusVersionInfo = { current_version: '1.0.0' }
const mockAppContextState = vi.hoisted(() => ({
langGeniusVersionInfoAtom: Symbol('langGeniusVersionInfoAtom'),
langGeniusVersionInfo: { current_version: '1.0.0' as string | null },
}))
// Mock useCheckInstalled
vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', () => ({
@ -71,10 +74,17 @@ vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', ()
}),
}))
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
langGeniusVersionInfo: mockLangGeniusVersionInfo,
}),
vi.mock('@/context/app-context-state', () => ({
langGeniusVersionInfoAtom: mockAppContextState.langGeniusVersionInfoAtom,
}))
vi.mock('jotai', () => ({
useAtomValue: (atom: unknown) => {
if (atom === mockAppContextState.langGeniusVersionInfoAtom)
return mockAppContextState.langGeniusVersionInfo
throw new Error('Unexpected atom')
},
}))
// Mock service hooks
@ -104,7 +114,7 @@ vi.mock('../../../base/check-task-status', () => ({
vi.mock('@/app/components/plugins/install-plugin/hooks/use-plugin-install-permission', () => ({
default: () => ({
canInstallPlugin: true,
currentDifyVersion: mockLangGeniusVersionInfo.current_version,
currentDifyVersion: mockAppContextState.langGeniusVersionInfo.current_version,
}),
}))
@ -170,7 +180,7 @@ describe('Install Component (steps/install.tsx)', () => {
mockIsLoading = false
mockPluginDeclaration = undefined
mockCanInstall = true
mockLangGeniusVersionInfo = { current_version: '1.0.0' }
mockAppContextState.langGeniusVersionInfo = { current_version: '1.0.0' }
mockInstallPackageFromMarketPlace.mockResolvedValue({
all_installed: false,
task_id: 'task-123',
@ -281,7 +291,7 @@ describe('Install Component (steps/install.tsx)', () => {
})
it('should not show warning when dify version is compatible', () => {
mockLangGeniusVersionInfo = { current_version: '2.0.0' }
mockAppContextState.langGeniusVersionInfo = { current_version: '2.0.0' }
mockPluginDeclaration = {
manifest: { meta: { minimum_dify_version: '1.0.0' } },
}
@ -291,7 +301,7 @@ describe('Install Component (steps/install.tsx)', () => {
})
it('should show warning when dify version is incompatible', () => {
mockLangGeniusVersionInfo = { current_version: '1.0.0' }
mockAppContextState.langGeniusVersionInfo = { current_version: '1.0.0' }
mockPluginDeclaration = {
manifest: { meta: { minimum_dify_version: '2.0.0' } },
}
@ -749,7 +759,7 @@ describe('Install Component (steps/install.tsx)', () => {
})
it('should handle null current_version in langGeniusVersionInfo', () => {
mockLangGeniusVersionInfo = { current_version: null as unknown as string }
mockAppContextState.langGeniusVersionInfo = { current_version: null as unknown as string }
mockPluginDeclaration = {
manifest: { meta: { minimum_dify_version: '1.0.0' } },
}

View File

@ -3,11 +3,12 @@ import type { FC } from 'react'
import type { InstallPackageResponse, Plugin, PluginManifestInMarket } from '../../../types'
import { Button } from '@langgenius/dify-ui/button'
import { RiLoader2Line } from '@remixicon/react'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { useAppContext } from '@/context/app-context'
import { langGeniusVersionInfoAtom } from '@/context/app-context-state'
import { useInstallPackageFromMarketPlace, usePluginDeclarationFromMarketPlace, usePluginTaskList, useUpdatePackageFromMarketPlace } from '@/service/use-plugins'
import { isEqualOrLaterThanVersion } from '@/utils/semver'
import Card from '../../../card'
@ -133,7 +134,7 @@ const Installed: FC<Props> = ({
}
}
const { langGeniusVersionInfo } = useAppContext()
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { data: pluginDeclaration } = usePluginDeclarationFromMarketPlace(uniqueIdentifier)
const isDifyVersionCompatible = useMemo(() => {
if (!pluginDeclaration || !langGeniusVersionInfo.current_version)

View File

@ -1,8 +1,9 @@
'use client'
import type { ReactNode } from 'react'
import { useAtomValue } from 'jotai'
import { PluginInstallPermissionProvider } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider'
import { useAppContext } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { hasPermission } from '@/utils/permission'
type MarketplaceInstallPermissionProviderProps = {
@ -12,7 +13,7 @@ type MarketplaceInstallPermissionProviderProps = {
const MarketplaceInstallPermissionProvider = ({
children,
}: MarketplaceInstallPermissionProviderProps) => {
const { workspacePermissionKeys } = useAppContext()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canInstallPlugin = hasPermission(workspacePermissionKeys, 'plugin.install')
return (

View File

@ -50,6 +50,20 @@ 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,
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(),
workspacePermissionKeys: ['credential.use', 'credential.create', 'credential.manage'],
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/hooks/use-oauth', () => ({
openOAuthPopup: vi.fn(),
}))

View File

@ -30,6 +30,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: mockAppContext.workspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowAccountSettingModal: mockSetShowAccountSettingModal,

View File

@ -71,6 +71,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: mockAppContext.workspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
// Mock service/use-triggers - API service
vi.mock('@/service/use-triggers', () => ({
useTriggerPluginDynamicOptions: () => ({

View File

@ -86,6 +86,19 @@ vi.mock('@/context/app-context', () => ({
}) => unknown) => selector(mockAppContext),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
userProfile: mockAppContext.userProfile,
workspacePermissionKeys: mockAppContext.workspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
// Mock service/use-triggers
vi.mock('@/service/use-triggers', () => ({
useTriggerPluginDynamicOptions: () => ({

View File

@ -5,16 +5,18 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CredentialTypeEnum } from '../../types'
import Item from '../item'
// Item uses useAppContextWithSelector(state => state.userProfile) for the
// borrowed-row heuristic; provide a minimal mock so the selector resolves.
const mockUserProfile = { id: 'test-user', name: 'Test User', email: 'test@example.com', avatar_url: '' }
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { userProfile: typeof mockUserProfile, workspacePermissionKeys: string[] }) => unknown) =>
selector({
userProfile: mockUserProfile,
workspacePermissionKeys: ['credential.use', 'credential.create', 'credential.manage'],
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
userProfile: { id: 'test-user' },
workspacePermissionKeys: ['credential.use', 'credential.create', 'credential.manage'],
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
// ==================== Test Utilities ====================

View File

@ -6,6 +6,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too
import {
RiInformationLine,
} from '@remixicon/react'
import { useAtomValue } from 'jotai'
import {
memo,
useMemo,
@ -15,7 +16,7 @@ import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Badge from '@/app/components/base/badge'
import Input from '@/app/components/base/input'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileIdAtom } from '@/context/app-context-state'
import { useCredentialPermissions } from '@/hooks/use-credential-permissions'
import { CredentialTypeEnum } from '../types'
@ -58,14 +59,14 @@ const Item = ({
const { canUseCredential, canManageCredential } = useCredentialPermissions()
const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2
const isPersonal = credential.visibility === 'only_me'
const userProfile = useAppContextWithSelector(state => state.userProfile)
const currentUserId = useAtomValue(userProfileIdAtom)
// Borrowed-from-teammate: the backend explicitly flagged this row as another member's
// only_me credential, returned only because the current node still references it.
// Fallback heuristic (created_by mismatch on a selected row) is kept for backends
// that don't yet emit the flag.
const isSelected = showSelectedIcon && selectedCredentialId === credential.id
const isConfiguredByOther
= !!credential.created_by && !!userProfile?.id && credential.created_by !== userProfile.id
= !!credential.created_by && !!currentUserId && credential.created_by !== currentUserId
const isBorrowed
= !!credential.from_other_member || (isSelected && isConfiguredByOther && isPersonal)
const showSwitchAwayHint = isBorrowed

View File

@ -1,4 +1,3 @@
// import { useAppContext } from '@/context/app-context'
// import { Button } from '@langgenius/dify-ui/button'
// import { StatusDot } from '@langgenius/dify-ui/status-dot'
// import ToolItem from '@/app/components/tools/provider/tool-item'
@ -18,7 +17,6 @@ const ActionList = ({
detail,
}: Props) => {
const { t } = useTranslation()
// const { isCurrentWorkspaceManager } = useAppContext()
// const providerBriefInfo = detail.declaration.datasource?.identity
// const providerKey = `${detail.plugin_id}/${providerBriefInfo?.name}`
const { data: dataSourceList } = useDataSourceList(true)

View File

@ -55,13 +55,36 @@ vi.mock('@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list',
}))
const mockLangGeniusVersionInfo = vi.fn(() => ({
current_env: '',
current_version: '1.0.0',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
}))
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
const createLangGeniusVersionInfo = (currentVersion: string) => ({
current_env: '',
current_version: currentVersion,
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
})
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
langGeniusVersionInfo: mockLangGeniusVersionInfo(),
}),
}))
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('../action', () => ({
default: ({ onDelete, pluginName }: { onDelete: () => void, pluginName: string }) => (
@ -162,7 +185,7 @@ describe('PluginItem', () => {
mockTheme.mockReturnValue('light')
mockCurrentPluginID.mockReturnValue(undefined)
mockEnableMarketplace.mockReturnValue(true)
mockLangGeniusVersionInfo.mockReturnValue({ current_version: '1.0.0' })
mockLangGeniusVersionInfo.mockReturnValue(createLangGeniusVersionInfo('1.0.0'))
mockGetValueFromI18nObject.mockImplementation((obj: Record<string, string>) => obj?.en_US || '')
})
@ -359,7 +382,7 @@ describe('PluginItem', () => {
describe('Version Compatibility', () => {
it('should show warning icon when Dify version is not compatible', () => {
// Arrange
mockLangGeniusVersionInfo.mockReturnValue({ current_version: '0.3.0' })
mockLangGeniusVersionInfo.mockReturnValue(createLangGeniusVersionInfo('0.3.0'))
const plugin = createPluginDetail({
declaration: createPluginDeclaration({
meta: { version: '1.0.0', minimum_dify_version: '0.5.0' },
@ -376,7 +399,7 @@ describe('PluginItem', () => {
it('should not show warning when Dify version is compatible', () => {
// Arrange
mockLangGeniusVersionInfo.mockReturnValue({ current_version: '1.0.0' })
mockLangGeniusVersionInfo.mockReturnValue(createLangGeniusVersionInfo('1.0.0'))
const plugin = createPluginDetail({
declaration: createPluginDeclaration({
meta: { version: '1.0.0', minimum_dify_version: '0.5.0' },
@ -393,7 +416,7 @@ describe('PluginItem', () => {
it('should handle missing current_version gracefully', () => {
// Arrange
mockLangGeniusVersionInfo.mockReturnValue({ current_version: '' })
mockLangGeniusVersionInfo.mockReturnValue(createLangGeniusVersionInfo(''))
const plugin = createPluginDetail()
// Act

View File

@ -11,12 +11,13 @@ import {
RiLoginCircleLine,
} from '@remixicon/react'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { API_PREFIX } from '@/config'
import { useAppContext } from '@/context/app-context'
import { langGeniusVersionInfoAtom } from '@/context/app-context-state'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import useTheme from '@/hooks/use-theme'
@ -69,7 +70,7 @@ const PluginItem: FC<Props> = ({
return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : ''
}, [source, author])
const { langGeniusVersionInfo } = useAppContext()
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const isDifyVersionCompatible = useMemo(() => {
if (!langGeniusVersionInfo.current_version)

View File

@ -51,11 +51,42 @@ vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0' },
langGeniusVersionInfo: {
current_env: 'CLOUD',
current_version: '1.0.0',
latest_version: '1.0.0',
version: '1.0.0',
release_date: '',
release_notes: '',
can_auto_update: false,
},
workspacePermissionKeys: ['plugin.install', 'plugin.delete', 'plugin.debug', 'plugin.plugin_preferences'],
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: {
current_env: 'CLOUD',
current_version: '1.0.0',
latest_version: '1.0.0',
version: '1.0.0',
release_date: '',
release_notes: '',
can_auto_update: false,
},
workspacePermissionKeys: ['plugin.install', 'plugin.delete', 'plugin.debug', 'plugin.plugin_preferences'],
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/service/use-plugins', () => ({
hasPluginPermission: (permission: string | undefined, isAdmin: boolean) => {
if (!permission)

View File

@ -1,20 +1,49 @@
// Import mocks for assertions
import type { AppContextStateMockState } from '@/__tests__/utils/mock-app-context-state'
import type { LangGeniusVersionResponse } from '@/models/common'
import { toast } from '@langgenius/dify-ui/toast'
import { waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { renderHookWithSystemFeatures as renderHook } from '@/__tests__/utils/mock-system-features'
import { useAppContext } from '@/context/app-context'
import { useInvalidateReferenceSettings, useMutationPluginPermissionSettings, useMutationReferenceSettings, usePluginAutoUpgradeSettings, usePluginPermissionSettings } from '@/service/use-plugins'
import { PermissionType, PluginCategoryEnum } from '../../types'
import useReferenceSetting, { useCanInstallPluginFromMarketplace } from '../use-reference-setting'
vi.mock('@/context/app-context', async () => {
const actual = await vi.importActual('@/context/app-context')
return {
...actual,
useAppContext: vi.fn(),
const defaultLangGeniusVersionInfo: LangGeniusVersionResponse = {
current_env: '',
current_version: '1.0.0',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
}
type MockAppContextState = Omit<AppContextStateMockState, 'langGeniusVersionInfo'> & {
langGeniusVersionInfo?: Partial<LangGeniusVersionResponse>
}
let mockAppContextState: AppContextStateMockState = {}
const setAppContextState = (state: MockAppContextState) => {
mockAppContextState = {
...state,
langGeniusVersionInfo: {
...defaultLangGeniusVersionInfo,
...state.langGeniusVersionInfo,
},
}
}
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('@/service/use-plugins', () => ({
@ -33,12 +62,12 @@ describe('useReferenceSetting Hook', () => {
toastSuccessSpy.mockClear()
// Default mocks
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: [] as string[],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginAutoUpgradeSettings).mockReturnValue({
data: {
@ -109,12 +138,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should allow install and debug when plugin permission keys are present', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.install', 'plugin.debug'],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
install_permission: PermissionType.everyone,
@ -129,12 +158,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should allow debug for managers with legacy admin permission when RBAC is disabled', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: [] as string[],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
@ -150,12 +179,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should allow debug for owners with legacy admin permission when RBAC is disabled', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: true,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: [] as string[],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
@ -171,12 +200,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should allow debug for normal users when legacy debug permission is everyone and RBAC is disabled', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.install'],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
@ -194,12 +223,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should use plugin keys even when legacy admin permission is configured and RBAC is enabled', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.install', 'plugin.debug'],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
@ -217,12 +246,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should apply legacy noOne plugin permissions when RBAC is disabled', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.install', 'plugin.delete', 'plugin.debug'],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
install_permission: PermissionType.noOne,
@ -245,12 +274,12 @@ describe('useReferenceSetting Hook', () => {
describe('canSetPermissions', () => {
it('should be true with plugin preferences permission when RBAC is disabled', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.plugin_preferences'],
} as ReturnType<typeof useAppContext>)
})
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
@ -258,12 +287,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should be false when RBAC is enabled even with plugin preferences permission', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: true,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.plugin_preferences'],
} as ReturnType<typeof useAppContext>)
})
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool), {
systemFeatures: { rbac_enabled: true },
@ -274,12 +303,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should be false without plugin preferences permission', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: [] as string[],
} as ReturnType<typeof useAppContext>)
})
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
@ -353,12 +382,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should keep permission key access available when reference setting data is still loading', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.install', 'plugin.debug'],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginAutoUpgradeSettings).mockReturnValue({
data: undefined,
} as unknown as ReturnType<typeof usePluginAutoUpgradeSettings>)
@ -371,13 +400,13 @@ describe('useReferenceSetting Hook', () => {
})
it('should keep permission state loading while workspace permission keys are loading', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
isLoadingWorkspacePermissionKeys: true,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: [] as string[],
} as ReturnType<typeof useAppContext>)
})
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
@ -386,13 +415,13 @@ describe('useReferenceSetting Hook', () => {
})
it('should keep permission state loading while current workspace is loading', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
isLoadingCurrentWorkspace: true,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.install'],
} as ReturnType<typeof useAppContext>)
})
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
@ -402,7 +431,7 @@ describe('useReferenceSetting Hook', () => {
describe('RBAC permissions', () => {
it('should use workspace permission keys when RBAC is enabled', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
@ -412,7 +441,7 @@ describe('useReferenceSetting Hook', () => {
'plugin.debug',
'plugin.plugin_preferences',
],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
install_permission: PermissionType.noOne,
@ -435,12 +464,12 @@ describe('useReferenceSetting Hook', () => {
})
it('should ignore legacy plugin permission settings when RBAC is enabled', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: [] as string[],
} as ReturnType<typeof useAppContext>)
})
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool), {
systemFeatures: { rbac_enabled: true },
@ -462,12 +491,12 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.install'],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
@ -501,12 +530,12 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
})
it('should return false without plugin.install', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: [] as string[],
} as ReturnType<typeof useAppContext>)
})
const { result } = renderHook(() => useCanInstallPluginFromMarketplace(), {
systemFeatures: { enable_marketplace: true },
@ -516,12 +545,12 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
})
it('should return false when both marketplace is disabled and plugin.install is missing', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: [] as string[],
} as ReturnType<typeof useAppContext>)
})
const { result } = renderHook(() => useCanInstallPluginFromMarketplace(), {
systemFeatures: { enable_marketplace: false },
@ -558,12 +587,12 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
})
it('should use plugin.install when marketplace and RBAC are enabled', () => {
vi.mocked(useAppContext).mockReturnValue({
setAppContextState({
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
langGeniusVersionInfo: { current_version: '1.0.0', latest_version: '', version: '' },
workspacePermissionKeys: ['plugin.install'],
} as ReturnType<typeof useAppContext>)
})
vi.mocked(usePluginPermissionSettings).mockReturnValue({
data: {
install_permission: PermissionType.noOne,

View File

@ -1,16 +1,23 @@
import type { PluginCategoryEnum } from '../types'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import {
currentWorkspaceLoadingAtom,
langGeniusVersionInfoAtom,
workspacePermissionKeysAtom,
workspacePermissionKeysLoadingAtom,
workspaceRoleFlagsAtom,
} from '@/context/app-context-state'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useInvalidateReferenceSettings, useMutationPluginPermissionSettings, useMutationReferenceSettings, usePluginAutoUpgradeSettings, usePluginPermissionSettings } from '@/service/use-plugins'
import { hasPermission } from '@/utils/permission'
import { hasLegacyPluginPermissionAccess } from '../plugin-permissions'
const useCanSetPluginSettings = () => {
const { workspacePermissionKeys } = useAppContext()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { data: rbacEnabled } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: s => s.rbac_enabled,
@ -28,11 +35,11 @@ export const usePluginSettingsAccess = () => {
const {
isCurrentWorkspaceManager,
isCurrentWorkspaceOwner,
isLoadingCurrentWorkspace,
isLoadingWorkspacePermissionKeys,
workspacePermissionKeys,
langGeniusVersionInfo,
} = useAppContext()
} = useAtomValue(workspaceRoleFlagsAtom)
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
const { data: rbacEnabled } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: s => s.rbac_enabled,
@ -125,7 +132,11 @@ export const useCanInstallPluginFromMarketplace = () => {
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const marketplaceAccess = systemFeatures.enable_marketplace
const rbacEnabled = systemFeatures.rbac_enabled
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner, workspacePermissionKeys } = useAppContext()
const {
isCurrentWorkspaceManager,
isCurrentWorkspaceOwner,
} = useAtomValue(workspaceRoleFlagsAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const permissionQuery = usePluginPermissionSettings()
const { data: permissions } = permissionQuery
const legacyCanInstallPlugin = hasLegacyPluginPermissionAccess({

View File

@ -1,16 +1,17 @@
'use client'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useAtomValue } from 'jotai'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { hasPermission } from '@/utils/permission'
export const useCanManageTools = () => {
const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
return hasPermission(workspacePermissionKeys, 'tool.manage')
}
export const useCanManageMCP = () => {
const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
return hasPermission(workspacePermissionKeys, 'mcp.manage')
}

View File

@ -40,14 +40,30 @@ vi.mock('../modal', () => ({
},
}))
let mockWorkspacePermissionKeys: string[] = ['mcp.manage']
const mockAppContextState = vi.hoisted(() => ({
workspacePermissionKeys: ['mcp.manage'] as string[],
workspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'),
}))
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => selector({
workspacePermissionKeys: mockWorkspacePermissionKeys,
workspacePermissionKeys: mockAppContextState.workspacePermissionKeys,
}),
}))
vi.mock('@/context/app-context-state', () => ({
workspacePermissionKeysAtom: mockAppContextState.workspacePermissionKeysAtom,
}))
vi.mock('jotai', () => ({
useAtomValue: (atom: unknown) => {
if (atom === mockAppContextState.workspacePermissionKeysAtom)
return mockAppContextState.workspacePermissionKeys
throw new Error('Unexpected atom')
},
}))
// Mock the plugins service
vi.mock('@/service/use-plugins', () => ({
useInstalledPluginList: () => ({
@ -84,7 +100,7 @@ describe('NewMCPCard', () => {
beforeEach(() => {
mockCreateMCP.mockClear()
mockWorkspacePermissionKeys = ['mcp.manage']
mockAppContextState.workspacePermissionKeys = ['mcp.manage']
})
describe('Rendering', () => {
@ -152,7 +168,7 @@ describe('NewMCPCard', () => {
describe('mcp.manage Permission', () => {
it('should not render card when user lacks mcp.manage', () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
@ -160,7 +176,7 @@ describe('NewMCPCard', () => {
})
it('should not render toolbar button when user lacks mcp.manage', () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
render(<NewMCPButton {...defaultProps} />, { wrapper: createWrapper() })

View File

@ -15,7 +15,10 @@ const mockRefetch = vi.fn()
const mockUseAllToolProviders = vi.fn()
let mockProviders: MockProvider[] = []
let mockIsLoadingToolProviders = false
let mockWorkspacePermissionKeys = ['mcp.manage']
const mockAppContextState = vi.hoisted(() => ({
workspacePermissionKeys: ['mcp.manage'] as string[],
workspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'),
}))
vi.mock('@/service/use-tools', () => ({
useAllToolProviders: (enabled?: boolean) => {
@ -30,10 +33,23 @@ vi.mock('@/service/use-tools', () => ({
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => selector({
workspacePermissionKeys: mockWorkspacePermissionKeys,
workspacePermissionKeys: mockAppContextState.workspacePermissionKeys,
}),
}))
vi.mock('@/context/app-context-state', () => ({
workspacePermissionKeysAtom: mockAppContextState.workspacePermissionKeysAtom,
}))
vi.mock('jotai', () => ({
useAtomValue: (atom: unknown) => {
if (atom === mockAppContextState.workspacePermissionKeysAtom)
return mockAppContextState.workspacePermissionKeys
throw new Error('Unexpected atom')
},
}))
vi.mock('@/app/components/tools/provider/tool-card-skeleton', () => ({
default: ({ variant }: { variant?: string }) => (
<>
@ -89,7 +105,7 @@ describe('MCPList', () => {
vi.useFakeTimers()
mockProviders = []
mockIsLoadingToolProviders = false
mockWorkspacePermissionKeys = ['mcp.manage']
mockAppContextState.workspacePermissionKeys = ['mcp.manage']
mockRefetch.mockResolvedValue(undefined)
})
@ -111,7 +127,7 @@ describe('MCPList', () => {
})
it('should render providers read-only when user lacks mcp.manage', () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
mockProviders = [
{ id: '1', name: 'Provider 1', type: 'mcp' },
]

View File

@ -81,14 +81,30 @@ vi.mock('../detail/operation-dropdown', () => ({
),
}))
let mockWorkspacePermissionKeys: string[] = ['mcp.manage']
const mockAppContextState = vi.hoisted(() => ({
workspacePermissionKeys: ['mcp.manage'] as string[],
workspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'),
}))
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => selector({
workspacePermissionKeys: mockWorkspacePermissionKeys,
workspacePermissionKeys: mockAppContextState.workspacePermissionKeys,
}),
}))
vi.mock('@/context/app-context-state', () => ({
workspacePermissionKeysAtom: mockAppContextState.workspacePermissionKeysAtom,
}))
vi.mock('jotai', () => ({
useAtomValue: (atom: unknown) => {
if (atom === mockAppContextState.workspacePermissionKeysAtom)
return mockAppContextState.workspacePermissionKeys
throw new Error('Unexpected atom')
},
}))
// Mock the format time hook
vi.mock('@/hooks/use-format-time-from-now', () => ({
useFormatTimeFromNow: () => ({
@ -155,7 +171,7 @@ describe('MCPCard', () => {
mockDeleteMCP.mockClear()
mockUpdateMCP.mockResolvedValue({ result: 'success' })
mockDeleteMCP.mockResolvedValue({ result: 'success' })
mockWorkspacePermissionKeys = ['mcp.manage']
mockAppContextState.workspacePermissionKeys = ['mcp.manage']
})
describe('Rendering', () => {
@ -343,7 +359,7 @@ describe('MCPCard', () => {
})
it('should not render operation dropdown when user lacks mcp.manage', () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })

View File

@ -107,16 +107,31 @@ vi.mock('../tool-item', () => ({
),
}))
// Mutable workspace permission state
let mockWorkspacePermissionKeys: string[] = ['mcp.manage']
const mockAppContextState = vi.hoisted(() => ({
workspacePermissionKeys: ['mcp.manage'] as string[],
workspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'),
}))
// Mock the app context
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => unknown) => selector({
workspacePermissionKeys: mockWorkspacePermissionKeys,
workspacePermissionKeys: mockAppContextState.workspacePermissionKeys,
}),
}))
vi.mock('@/context/app-context-state', () => ({
workspacePermissionKeysAtom: mockAppContextState.workspacePermissionKeysAtom,
}))
vi.mock('jotai', () => ({
useAtomValue: (atom: unknown) => {
if (atom === mockAppContextState.workspacePermissionKeysAtom)
return mockAppContextState.workspacePermissionKeys
throw new Error('Unexpected atom')
},
}))
// Mock the plugins service
vi.mock('@/service/use-plugins', () => ({
useInstalledPluginList: () => ({
@ -195,7 +210,7 @@ describe('MCPDetailContent', () => {
mockIsFetching = false
mockIsUpdating = false
mockIsAuthorizing = false
mockWorkspacePermissionKeys = ['mcp.manage']
mockAppContextState.workspacePermissionKeys = ['mcp.manage']
})
describe('Rendering', () => {
@ -232,7 +247,7 @@ describe('MCPDetailContent', () => {
})
it('should render read-only detail when user lacks mcp.manage', () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
render(<MCPDetailContent {...defaultProps} />, { wrapper: createWrapper() })
@ -466,7 +481,7 @@ describe('MCPDetailContent', () => {
})
it('should disable authorize action when user lacks mcp.manage', () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
const detail = createMockDetail({ is_team_authorization: false })
render(
<MCPDetailContent {...defaultProps} detail={detail} />,
@ -756,7 +771,7 @@ describe('MCPDetailContent', () => {
})
it('should not run OAuth authorization when user lacks mcp.manage', async () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
mockAuthorizeMcp.mockResolvedValue({ authorization_url: 'https://oauth.example.com' })
const detail = createMockDetail({ is_team_authorization: false })
@ -800,7 +815,7 @@ describe('MCPDetailContent', () => {
})
it('should disable authorized button when user lacks mcp.manage', () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
const detail = createMockDetail({ is_team_authorization: true })
render(
<MCPDetailContent {...defaultProps} detail={detail} />,

View File

@ -2,9 +2,10 @@
import type { AppDetailResponse } from '@/models/app'
import type { AppSSO } from '@/types/app'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { BlockEnum } from '@/app/components/workflow/types'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { fetchAppDetail } from '@/service/apps'
import {
useInvalidateMCPServerDetail,
@ -36,8 +37,8 @@ export const useMCPServiceCardState = (
const { mutateAsync: updateMCPServer } = useUpdateMCPServer()
const { mutateAsync: refreshMCPServerCode, isPending: genLoading } = useRefreshMCPServerCode()
const invalidateMCPServerDetail = useInvalidateMCPServerDetail()
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManageMCP = useMemo(
() => getAppACLCapabilities(appInfo.permission_keys, {

View File

@ -4,14 +4,30 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AuthType } from '../../types'
import CustomCreateCard, { NewCustomToolButton } from '../custom-create-card'
let mockWorkspacePermissionKeys: string[] = ['tool.manage']
const mockAppContextState = vi.hoisted(() => ({
workspacePermissionKeys: ['tool.manage'] as string[],
workspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'),
}))
vi.mock('@/context/app-context', () => ({
useSelector: <T,>(selector: (state: { workspacePermissionKeys: string[] }) => T): T => selector({
workspacePermissionKeys: mockWorkspacePermissionKeys,
workspacePermissionKeys: mockAppContextState.workspacePermissionKeys,
}),
}))
vi.mock('@/context/app-context-state', () => ({
workspacePermissionKeysAtom: mockAppContextState.workspacePermissionKeysAtom,
}))
vi.mock('jotai', () => ({
useAtomValue: (atom: unknown) => {
if (atom === mockAppContextState.workspacePermissionKeysAtom)
return mockAppContextState.workspacePermissionKeys
throw new Error('Unexpected atom')
},
}))
// Mock useLocale and useDocLink
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
@ -81,7 +97,7 @@ describe('CustomCreateCard', () => {
beforeEach(() => {
vi.clearAllMocks()
mockWorkspacePermissionKeys = ['tool.manage']
mockAppContextState.workspacePermissionKeys = ['tool.manage']
mockModalVisible = false
mockCreateCustomCollection.mockResolvedValue({})
})
@ -94,7 +110,7 @@ describe('CustomCreateCard', () => {
})
it('should not render anything when user does not have tool.manage', () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
const { container } = render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
@ -146,7 +162,7 @@ describe('CustomCreateCard', () => {
})
it('should not render toolbar add button when user does not have tool.manage', () => {
mockWorkspacePermissionKeys = []
mockAppContextState.workspacePermissionKeys = []
const { container } = render(<NewCustomToolButton onRefreshData={mockOnRefreshData} />)

View File

@ -15,6 +15,7 @@ vi.mock('@/i18n-config/language', () => ({
const mockIsCurrentWorkspaceManager = vi.fn(() => true)
const mockAppContextState = vi.hoisted(() => ({
workspacePermissionKeys: ['tool.manage', 'credential.use', 'credential.create', 'credential.manage'] as string[],
workspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'),
}))
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
@ -25,6 +26,19 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('@/context/app-context-state', () => ({
workspacePermissionKeysAtom: mockAppContextState.workspacePermissionKeysAtom,
}))
vi.mock('jotai', () => ({
useAtomValue: (atom: unknown) => {
if (atom === mockAppContextState.workspacePermissionKeysAtom)
return mockAppContextState.workspacePermissionKeys
throw new Error('Unexpected atom')
},
}))
const mockSetShowModelModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({

View File

@ -69,6 +69,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('@/hooks/use-theme', () => ({
default: vi.fn(),
}))

View File

@ -21,6 +21,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('@/service/use-tools', () => ({
useAllBuiltInTools: (enabled: boolean) => mockBuiltInTools(enabled),
useAllCustomTools: (enabled: boolean) => mockCustomTools(enabled),

View File

@ -1,13 +1,19 @@
import { renderHook } from '@testing-library/react'
import { useCredentialPermissions } from './use-credential-permissions'
let mockWorkspacePermissionKeys: string[] | null = []
let mockWorkspacePermissionKeys: string[] = []
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: { workspacePermissionKeys: string[] | null }) => unknown) => selector({
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)
})
describe('useCredentialPermissions', () => {
beforeEach(() => {
@ -39,8 +45,8 @@ describe('useCredentialPermissions', () => {
})
})
it('should handle missing workspace permissions as no credential capabilities', () => {
mockWorkspacePermissionKeys = null
it('should handle empty workspace permissions as no credential capabilities', () => {
mockWorkspacePermissionKeys = []
const { result } = renderHook(() => useCredentialPermissions())

View File

@ -1,8 +1,9 @@
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useAtomValue } from 'jotai'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { hasPermission } from '@/utils/permission'
export const useCredentialPermissions = () => {
const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
return {
canUseCredential: hasPermission(workspacePermissionKeys, 'credential.use'),

View File

@ -19,9 +19,11 @@ import {
const {
mockGet,
mockPost,
mockWorkspacePermissionKeysAtom,
} = vi.hoisted(() => ({
mockGet: vi.fn(),
mockPost: vi.fn(),
mockWorkspacePermissionKeysAtom: Symbol('workspacePermissionKeysAtom'),
}))
vi.mock('@/service/base', () => ({
@ -37,12 +39,17 @@ vi.mock('@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list',
}),
}))
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
isCurrentWorkspaceManager: true,
isCurrentWorkspaceOwner: false,
workspacePermissionKeys: ['plugin.install'],
}),
vi.mock('@/context/app-context-state', () => ({
workspacePermissionKeysAtom: mockWorkspacePermissionKeysAtom,
}))
vi.mock('jotai', () => ({
useAtomValue: (atom: unknown) => {
if (atom === mockWorkspacePermissionKeysAtom)
return ['plugin.install']
throw new Error('Unexpected atom')
},
}))
vi.mock('../use-tools', () => ({

View File

@ -40,12 +40,13 @@ import {
useQueryClient,
} from '@tanstack/react-query'
import { cloneDeep } from 'es-toolkit/object'
import { useAtomValue } from 'jotai'
import { useCallback, useEffect, useRef } from 'react'
import { FormTypeEnum } from '@/app/components/base/form/types'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
import { PluginCategoryEnum, PluginSource, TaskStatus } from '@/app/components/plugins/types'
import { useAppContext } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { fetchModelProviderModelList } from '@/service/common'
import { fetchPluginInfoFromMarketPlace, uninstallPlugin } from '@/service/plugins'
import { hasPermission } from '@/utils/permission'
@ -1233,7 +1234,7 @@ export const useFetchPluginsInMarketPlaceByInfo = (infos: MarketplacePluginInfoR
export const usePluginTaskList = (category?: PluginCategoryEnum | string) => {
const initializedRef = useRef(false)
const queryClient = useQueryClient()
const { workspacePermissionKeys } = useAppContext()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManagement = hasPermission(workspacePermissionKeys, 'plugin.install')
const { refreshPluginList } = useRefreshPluginList()
const query = useQuery<PluginTaskListResponse>({