diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 176341002bf..81c7678008c 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -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 diff --git a/web/__tests__/plugins/plugin-page-shell-flow.test.tsx b/web/__tests__/plugins/plugin-page-shell-flow.test.tsx index 483df7c8ec7..14eebf2554a 100644 --- a/web/__tests__/plugins/plugin-page-shell-flow.test.tsx +++ b/web/__tests__/plugins/plugin-page-shell-flow.test.tsx @@ -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: () => ({ diff --git a/web/__tests__/tools/tool-provider-detail-flow.test.tsx b/web/__tests__/tools/tool-provider-detail-flow.test.tsx index 8cbfae8d693..fe7b55189bc 100644 --- a/web/__tests__/tools/tool-provider-detail-flow.test.tsx +++ b/web/__tests__/tools/tool-provider-detail-flow.test.tsx @@ -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: () => ({ diff --git a/web/__tests__/utils/mock-app-context-state.ts b/web/__tests__/utils/mock-app-context-state.ts index 99795f60766..0c546de1471 100644 --- a/web/__tests__/utils/mock-app-context-state.ts +++ b/web/__tests__/utils/mock-app-context-state.ts @@ -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 diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx index 5bbe67b6890..d6fb10c8163 100644 --- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -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), diff --git a/web/app/components/header/account-setting/__tests__/index.spec.tsx b/web/app/components/header/account-setting/__tests__/index.spec.tsx index 3d580673d5a..b4635473532 100644 --- a/web/app/components/header/account-setting/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/index.spec.tsx @@ -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() + return { + ...actual, + useMembers: vi.fn(() => ({ data: { accounts: [] }, refetch: vi.fn() })), + useProviderContext: vi.fn(), + } +}) vi.mock('@/service/client', async (importOriginal) => { const actual = await importOriginal() diff --git a/web/app/components/header/account-setting/data-source-page-new/__tests__/card.spec.tsx b/web/app/components/header/account-setting/data-source-page-new/__tests__/card.spec.tsx index 0318645ea9f..464e4d7f367 100644 --- a/web/app/components/header/account-setting/data-source-page-new/__tests__/card.spec.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/__tests__/card.spec.tsx @@ -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 }) => (
diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-credential-in-load-balancing.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-credential-in-load-balancing.spec.tsx index c8ec3a1f2a2..499380c1e1a 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-credential-in-load-balancing.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-credential-in-load-balancing.spec.tsx @@ -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, diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-custom-model.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-custom-model.spec.tsx index 7230be997f4..515081c22f8 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-custom-model.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/add-custom-model.spec.tsx @@ -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: () =>
, diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx index 3199e26692c..902a2000425 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx @@ -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)} > -
onItemClick?.(items[0]!.credentials[0])}> +
onItemClick?.(items[0]!.credentials[0])} + onKeyDown={() => undefined} + > {renderTrigger()}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/__tests__/index.spec.tsx index ea1ff14d18a..bab7f605997 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/__tests__/index.spec.tsx @@ -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, diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx index 0fb0feaef02..655220ee6ac 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/__tests__/index.spec.tsx @@ -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, })) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx index 6fd2876c9cb..29d8f728693 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx @@ -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 => ({ model: 'gpt-4', label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' }, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/index.spec.tsx index 0756a172e59..9aee6fd9ead 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/index.spec.tsx @@ -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: () =>
, diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/api-key-section.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/api-key-section.spec.tsx index 0ad4a494285..82e4477b4cf 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/api-key-section.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/api-key-section.spec.tsx @@ -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_id: 'cred-1', credential_name: 'Test API Key', diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/dropdown-content.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/dropdown-content.spec.tsx index f50d654f82e..fe3f55e68dd 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/dropdown-content.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/dropdown-content.spec.tsx @@ -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 } diff --git a/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx b/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx index 91f0a437d24..c43d728916d 100644 --- a/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx +++ b/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx @@ -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', () => ({ diff --git a/web/app/components/plugins/card/__tests__/index.spec.tsx b/web/app/components/plugins/card/__tests__/index.spec.tsx index b6c97dfd026..9b7319ca580 100644 --- a/web/app/components/plugins/card/__tests__/index.spec.tsx +++ b/web/app/components/plugins/card/__tests__/index.spec.tsx @@ -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 === '🔗', diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index 48a3f450641..d4707936eea 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -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() diff --git a/web/app/components/plugins/install-plugin/base/__tests__/use-get-icon.spec.ts b/web/app/components/plugins/install-plugin/base/__tests__/use-get-icon.spec.ts index c5364ec47ff..ec7ba4c34db 100644 --- a/web/app/components/plugins/install-plugin/base/__tests__/use-get-icon.spec.ts +++ b/web/app/components/plugins/install-plugin/base/__tests__/use-get-icon.spec.ts @@ -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()) diff --git a/web/app/components/plugins/install-plugin/base/use-get-icon.ts b/web/app/components/plugins/install-plugin/base/use-get-icon.ts index 23d41aff07e..b390a7e951f 100644 --- a/web/app/components/plugins/install-plugin/base/use-get-icon.ts +++ b/web/app/components/plugins/install-plugin/base/use-get-icon.ts @@ -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, diff --git a/web/app/components/plugins/install-plugin/hooks/__tests__/use-workspace-plugin-install-permission.spec.ts b/web/app/components/plugins/install-plugin/hooks/__tests__/use-workspace-plugin-install-permission.spec.ts index f3df9915058..f7ab89645e2 100644 --- a/web/app/components/plugins/install-plugin/hooks/__tests__/use-workspace-plugin-install-permission.spec.ts +++ b/web/app/components/plugins/install-plugin/hooks/__tests__/use-workspace-plugin-install-permission.spec.ts @@ -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(() => { diff --git a/web/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission.ts b/web/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission.ts index efaba878813..5e408886b23 100644 --- a/web/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission.ts +++ b/web/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission.ts @@ -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') diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/__tests__/install.spec.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/__tests__/install.spec.tsx index 9c0e10d9059..3b2057b4c05 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/__tests__/install.spec.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/__tests__/install.spec.tsx @@ -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() @@ -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() @@ -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() @@ -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() @@ -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() diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx index 80a9eff7241..6fda49807a8 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx @@ -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 = ({ } } - const { langGeniusVersionInfo } = useAppContext() + const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom) const isDifyVersionCompatible = useMemo(() => { if (!langGeniusVersionInfo.current_version) return true diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/__tests__/install.spec.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/__tests__/install.spec.tsx index 222029ab013..7f878293e95 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/__tests__/install.spec.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/__tests__/install.spec.tsx @@ -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' } }, } diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx index b1c47be46bb..c8d934a39d4 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx @@ -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 = ({ } } - const { langGeniusVersionInfo } = useAppContext() + const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom) const { data: pluginDeclaration } = usePluginDeclarationFromMarketPlace(uniqueIdentifier) const isDifyVersionCompatible = useMemo(() => { if (!pluginDeclaration || !langGeniusVersionInfo.current_version) diff --git a/web/app/components/plugins/marketplace/marketplace-install-permission-provider.tsx b/web/app/components/plugins/marketplace/marketplace-install-permission-provider.tsx index ffa83c2a0ef..0e25810c6de 100644 --- a/web/app/components/plugins/marketplace/marketplace-install-permission-provider.tsx +++ b/web/app/components/plugins/marketplace/marketplace-install-permission-provider.tsx @@ -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 ( diff --git a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx index c7b66119c79..43b9ddf62dc 100644 --- a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx +++ b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx @@ -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(), })) diff --git a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth.spec.tsx b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth.spec.tsx index b7bd28875c5..a4775a37e41 100644 --- a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth.spec.tsx +++ b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth.spec.tsx @@ -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, diff --git a/web/app/components/plugins/plugin-auth/authorize/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-auth/authorize/__tests__/index.spec.tsx index 64813aac2a4..1c1f56e0b74 100644 --- a/web/app/components/plugins/plugin-auth/authorize/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/__tests__/index.spec.tsx @@ -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: () => ({ diff --git a/web/app/components/plugins/plugin-auth/authorized/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-auth/authorized/__tests__/index.spec.tsx index 2ab81fceb94..00a50f9b06d 100644 --- a/web/app/components/plugins/plugin-auth/authorized/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/__tests__/index.spec.tsx @@ -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: () => ({ diff --git a/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx b/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx index 58fbe6dfeac..d2b1c694105 100644 --- a/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/__tests__/item.spec.tsx @@ -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 ==================== diff --git a/web/app/components/plugins/plugin-auth/authorized/item.tsx b/web/app/components/plugins/plugin-auth/authorized/item.tsx index 03f8695449b..8db0a72f15e 100644 --- a/web/app/components/plugins/plugin-auth/authorized/item.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/item.tsx @@ -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 diff --git a/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx b/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx index e52ee795da0..87850a3ec5b 100644 --- a/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx @@ -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) diff --git a/web/app/components/plugins/plugin-item/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-item/__tests__/index.spec.tsx index 9ff2592c7ed..50c24df5a14 100644 --- a/web/app/components/plugins/plugin-item/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-item/__tests__/index.spec.tsx @@ -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) => 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 diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 91a0a5a5ef2..ac3087503ed 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -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 = ({ return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : '' }, [source, author]) - const { langGeniusVersionInfo } = useAppContext() + const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom) const isDifyVersionCompatible = useMemo(() => { if (!langGeniusVersionInfo.current_version) diff --git a/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx index 75c8bbf61c5..80ea6e03ab4 100644 --- a/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-page/__tests__/index.spec.tsx @@ -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) diff --git a/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts b/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts index 01e635d3a62..699738e5127 100644 --- a/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts +++ b/web/app/components/plugins/plugin-page/__tests__/use-reference-setting.spec.ts @@ -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 & { + langGeniusVersionInfo?: Partial +} + +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) + }) 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) + }) 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) + }) 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) + }) 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) + }) 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) + }) 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) + }) 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) + }) 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) + }) 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) + }) 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) + }) vi.mocked(usePluginAutoUpgradeSettings).mockReturnValue({ data: undefined, } as unknown as ReturnType) @@ -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) + }) 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) + }) 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) + }) 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) + }) 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) + }) 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) + }) 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) + }) 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) + }) vi.mocked(usePluginPermissionSettings).mockReturnValue({ data: { install_permission: PermissionType.noOne, diff --git a/web/app/components/plugins/plugin-page/use-reference-setting.ts b/web/app/components/plugins/plugin-page/use-reference-setting.ts index 681a791ffd4..545e7609b0e 100644 --- a/web/app/components/plugins/plugin-page/use-reference-setting.ts +++ b/web/app/components/plugins/plugin-page/use-reference-setting.ts @@ -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({ diff --git a/web/app/components/tools/hooks/use-tool-permissions.ts b/web/app/components/tools/hooks/use-tool-permissions.ts index 2d393007a29..b5d39bf7aae 100644 --- a/web/app/components/tools/hooks/use-tool-permissions.ts +++ b/web/app/components/tools/hooks/use-tool-permissions.ts @@ -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') } diff --git a/web/app/components/tools/mcp/__tests__/create-card.spec.tsx b/web/app/components/tools/mcp/__tests__/create-card.spec.tsx index 0d8c0b9bc2f..daa2a273000 100644 --- a/web/app/components/tools/mcp/__tests__/create-card.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/create-card.spec.tsx @@ -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(, { wrapper: createWrapper() }) @@ -160,7 +176,7 @@ describe('NewMCPCard', () => { }) it('should not render toolbar button when user lacks mcp.manage', () => { - mockWorkspacePermissionKeys = [] + mockAppContextState.workspacePermissionKeys = [] render(, { wrapper: createWrapper() }) diff --git a/web/app/components/tools/mcp/__tests__/index.spec.tsx b/web/app/components/tools/mcp/__tests__/index.spec.tsx index 6352bb146d5..0b55d4d4310 100644 --- a/web/app/components/tools/mcp/__tests__/index.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/index.spec.tsx @@ -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' }, ] diff --git a/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx b/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx index 6ff54b6ec1c..a399057b08a 100644 --- a/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx @@ -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(, { wrapper: createWrapper() }) diff --git a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx index 7e843277067..5dc235a6683 100644 --- a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx +++ b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx @@ -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(, { 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( , @@ -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( , diff --git a/web/app/components/tools/mcp/hooks/use-mcp-service-card.ts b/web/app/components/tools/mcp/hooks/use-mcp-service-card.ts index 81d3b84302a..074bf83ee0c 100644 --- a/web/app/components/tools/mcp/hooks/use-mcp-service-card.ts +++ b/web/app/components/tools/mcp/hooks/use-mcp-service-card.ts @@ -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, { diff --git a/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx b/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx index 0092eebdd6c..948366f2eaa 100644 --- a/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx +++ b/web/app/components/tools/provider/__tests__/custom-create-card.spec.tsx @@ -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: (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() @@ -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() diff --git a/web/app/components/tools/provider/__tests__/detail.spec.tsx b/web/app/components/tools/provider/__tests__/detail.spec.tsx index 670683d194c..497df1398c9 100644 --- a/web/app/components/tools/provider/__tests__/detail.spec.tsx +++ b/web/app/components/tools/provider/__tests__/detail.spec.tsx @@ -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: () => ({ diff --git a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx index 85d3adccc68..ff7e6db519d 100644 --- a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx @@ -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(), })) diff --git a/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts b/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts index 57652c54cce..9e80fb40f8a 100644 --- a/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts @@ -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), diff --git a/web/hooks/use-credential-permissions.spec.ts b/web/hooks/use-credential-permissions.spec.ts index 0f2cd554422..6d91b4b2299 100644 --- a/web/hooks/use-credential-permissions.spec.ts +++ b/web/hooks/use-credential-permissions.spec.ts @@ -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()) diff --git a/web/hooks/use-credential-permissions.ts b/web/hooks/use-credential-permissions.ts index 3f99c5e3345..c65f0e889db 100644 --- a/web/hooks/use-credential-permissions.ts +++ b/web/hooks/use-credential-permissions.ts @@ -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'), diff --git a/web/service/__tests__/use-plugins.spec.tsx b/web/service/__tests__/use-plugins.spec.tsx index 6d8374591a6..2c274da5a7b 100644 --- a/web/service/__tests__/use-plugins.spec.tsx +++ b/web/service/__tests__/use-plugins.spec.tsx @@ -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', () => ({ diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 521f4093cec..07dc4db4f2e 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -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({