refactor(web): remove remaining app context consumers (#38565)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou 2026-07-08 19:28:25 +08:00 committed by GitHub
parent 1340bef096
commit d67123e5fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 571 additions and 279 deletions

View File

@ -2023,11 +2023,6 @@
"count": 2
}
},
"web/app/components/base/permission-selector/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/base/permission-selector/member-item.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -3230,14 +3225,6 @@
"count": 2
}
},
"web/app/components/develop/secret-key/secret-key-modal.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/explore/banner/__tests__/indicator-button.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -4134,11 +4121,6 @@
"count": 1
}
},
"web/app/components/rag-pipeline/components/rag-pipeline-main.tsx": {
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/rag-pipeline/hooks/index.ts": {
"no-barrel-files/no-barrel-files": {
"count": 9
@ -4427,11 +4409,6 @@
"count": 1
}
},
"web/app/components/workflow-app/hooks/use-workflow-init.ts": {
"ts/no-explicit-any": {
"count": 3
}
},
"web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts": {
"ts/no-explicit-any": {
"count": 2
@ -4457,11 +4434,6 @@
"count": 2
}
},
"web/app/components/workflow-app/index.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/workflow-app/store/workflow/workflow-slice.ts": {
"ts/no-explicit-any": {
"count": 2
@ -6853,11 +6825,6 @@
"count": 1
}
},
"web/features/tag-management/components/tag-management-modal.tsx": {
"jsx-a11y/no-autofocus": {
"count": 1
}
},
"web/hooks/use-async-window-open.spec.ts": {
"ts/no-explicit-any": {
"count": 6

View File

@ -10,7 +10,6 @@ import type { App } from '@/models/explore'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { createTestQueryClient, renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
import AppList from '@/app/components/explore/app-list'
import { useAppContext } from '@/context/app-context'
import { fetchAppDetail, fetchAppList, fetchBanners } from '@/service/explore'
import { useMembers } from '@/service/use-common'
import { AppModeEnum } from '@/types/app'
@ -124,6 +123,18 @@ vi.mock('@/context/app-context', () => ({
useSelector: <T,>(selector: (state: MockAppContext) => T): T => selector(mockUseAppContext()),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockUseAppContext())
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/service/use-common', () => ({
useMembers: vi.fn(),
}))
@ -198,15 +209,15 @@ const createApp = (overrides: Partial<App> = {}): App => ({
})
const mockMemberRole = (hasEditPermission: boolean) => {
;(useAppContext as Mock).mockReturnValue({
mockUseAppContext.mockReturnValue({
userProfile: { id: 'user-1' },
workspacePermissionKeys: hasEditPermission ? ['app.create_and_management'] : [],
})
;(useMembers as Mock).mockReturnValue({
vi.mocked(useMembers).mockReturnValue({
data: {
accounts: [{ id: 'user-1', role: hasEditPermission ? 'admin' : 'normal' }],
},
})
} as unknown as ReturnType<typeof useMembers>)
}
const localeInput = { query: { language: 'en' } }

View File

@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { renderWithSystemFeatures as render } from '@/__tests__/utils/mock-system-features'
import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useRouter } from '@/next/navigation'
import { createApp } from '@/service/apps'
@ -21,6 +20,7 @@ const mockAppContextState = vi.hoisted(() => ({
userProfile: { id: 'user-1' },
workspacePermissionKeys: ['app.create_and_management'] as string[],
}))
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('ahooks', () => ({
useDebounceFn: <T extends (...args: unknown[]) => unknown>(fn: T) => {
@ -75,7 +75,7 @@ vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
@ -100,7 +100,6 @@ const mockCreateApp = vi.mocked(createApp)
const mockTrackCreateApp = vi.mocked(trackCreateApp)
const mockGetRedirection = vi.mocked(getRedirection)
const mockUseProviderContext = vi.mocked(useProviderContext)
const mockUseAppContext = vi.mocked(useAppContext)
const { mockToastSuccess, mockToastError } = toastMocks
const defaultPlanUsage = {
@ -148,7 +147,7 @@ describe('CreateAppModal', () => {
mockUseAppContext.mockReturnValue({
userProfile: { id: 'user-1' },
workspacePermissionKeys: ['app.create_and_management'],
} as unknown as ReturnType<typeof useAppContext>)
})
mockAppContextState.userProfile = { id: 'user-1' }
mockAppContextState.workspacePermissionKeys = ['app.create_and_management']
mockSetItem.mockClear()

View File

@ -1,30 +1,29 @@
import type { AppContextValue } from '@/context/app-context'
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { useSelector } from '@/context/app-context'
import Tips from '../tips'
// Mock AppContext's useSelector to control user profile data
vi.mock('@/context/app-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/app-context')>()
return {
...actual,
useSelector: vi.fn(),
}
const mockAppContextState = vi.hoisted(() => ({
userProfile: {
email: 'test@example.com',
},
}))
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)
})
describe('Tips', () => {
const mockEmail = 'test@example.com'
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useSelector).mockImplementation((selector: (value: AppContextValue) => unknown) => {
return selector({
userProfile: {
email: mockEmail,
},
} as AppContextValue)
})
mockAppContextState.userProfile.email = 'test@example.com'
})
it('should render email tip in normal mode', () => {

View File

@ -1,7 +1,8 @@
import { useAtomValue } from 'jotai'
import { memo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { useSelector as useAppSelector } from '@/context/app-context'
import { userProfileEmailAtom } from '@/context/app-context-state'
type TipsProps = {
showEmailTip: boolean
@ -15,7 +16,7 @@ const Tips = ({
showDebugModeTip,
}: TipsProps) => {
const { t } = useTranslation()
const email = useAppSelector(s => s.userProfile.email)
const email = useAtomValue(userProfileEmailAtom)
return (
<>

View File

@ -8,11 +8,13 @@ import {
} from '@langgenius/dify-ui/popover'
import { RiArrowDownSLine, RiGroup2Line, RiLock2Line } from '@remixicon/react'
import { useDebounceFn } from 'ahooks'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
// eslint-disable-next-line no-restricted-imports -- This legacy selector still relies on showLeftIcon/showClearIcon props from the old input.
import Input from '@/app/components/base/input'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileAtom } from '@/context/app-context-state'
import { PermissionLevel } from '@/models/permission'
import MemberItem from './member-item'
import Item from './permission-item'
@ -45,7 +47,7 @@ const PermissionSelector = ({
hidePartialMembers = false,
}: PermissionSelectorProps) => {
const { t } = useTranslation()
const userProfile = useAppContextWithSelector(state => state.userProfile)
const userProfile = useAtomValue(userProfileAtom)
const [open, setOpen] = useState(false)
const [keywords, setKeywords] = useState('')
@ -72,7 +74,7 @@ const PermissionSelector = ({
}, [userProfile, value, memberList])
const showMe = useMemo(() => {
return userProfile.name.includes(searchKeywords) || userProfile.email.includes(searchKeywords)
return (userProfile.name ?? '').includes(searchKeywords) || (userProfile.email ?? '').includes(searchKeywords)
}, [searchKeywords, userProfile])
const filteredMemberList = useMemo(() => {

View File

@ -1,9 +1,10 @@
import type { AvatarSize } from '@langgenius/dify-ui/avatar'
import type { FC } from 'react'
import { AvatarFallback, AvatarImage, AvatarRoot } from '@langgenius/dify-ui/avatar'
import { useAtomValue } from 'jotai'
import { memo } from 'react'
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
import { useAppContext } from '@/context/app-context'
import { userProfileIdAtom } from '@/context/app-context-state'
type User = {
id: string
@ -37,7 +38,7 @@ export const UserAvatarList: FC<UserAvatarListProps> = memo(({
className = '',
showCount = true,
}) => {
const { userProfile } = useAppContext()
const currentUserId = useAtomValue(userProfileIdAtom)
if (!users.length)
return null
@ -46,8 +47,6 @@ export const UserAvatarList: FC<UserAvatarListProps> = memo(({
const visibleUsers = users.slice(0, actualMaxVisible)
const remainingCount = users.length - actualMaxVisible
const currentUserId = userProfile?.id
return (
<div className={`flex items-center -space-x-1 ${className}`}>
{visibleUsers.map((user, index) => {

View File

@ -7,11 +7,11 @@ import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/comm
import { render, screen } from '@testing-library/react'
import { Plan } from '@/app/components/billing/type'
import { mailToSupport } from '@/app/components/header/utils/util'
import { useAppContext } from '@/context/app-context'
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
import AppsFull from '../index'
let mockAppContextState: AppContextValue
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
@ -22,7 +22,7 @@ vi.mock('@/config', async (importOriginal) => {
})
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
@ -136,7 +136,7 @@ describe('AppsFull', () => {
vi.clearAllMocks()
;(useProviderContext as Mock).mockReturnValue(buildProviderContext())
mockAppContextState = buildAppContext()
;(useAppContext as Mock).mockReturnValue(mockAppContextState)
mockUseAppContext.mockReturnValue(mockAppContextState)
;(mailToSupport as Mock).mockReturnValue('mailto:support@example.com')
})

View File

@ -2,7 +2,6 @@ import type { Mock } from 'vitest'
import { toast, ToastHost } from '@langgenius/dify-ui/toast'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { fetchSubscriptionUrls } from '@/service/billing'
@ -13,9 +12,10 @@ import { PlanRange } from '../../../plan-switcher/plan-range-switcher'
import CloudPlanItem from '../index'
let mockAppCtx: Record<string, unknown> = {}
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
@ -56,7 +56,6 @@ vi.mock('../../../assets', () => ({
Team: () => <div>Team Icon</div>,
}))
const mockUseAppContext = useAppContext as Mock
const mockUseProviderContext = useProviderContext as Mock
const mockUseAsyncWindowOpen = useAsyncWindowOpen as Mock
const mockBillingInvoices = consoleClient.billing.invoices.get as Mock

View File

@ -1,13 +1,12 @@
import type { Mock } from 'vitest'
import { toast, ToastHost } from '@langgenius/dify-ui/toast'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { useAppContext } from '@/context/app-context'
import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../../../../config'
import { SelfHostedPlan } from '../../../../type'
import SelfHostedPlanItem from '../index'
let mockAppCtx: Record<string, unknown> = {}
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('../list', () => ({
default: ({ plan }: { plan: string }) => (
@ -19,7 +18,7 @@ vi.mock('../list', () => ({
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
@ -40,8 +39,6 @@ vi.mock('../../../assets', () => ({
EnterpriseNoise: () => <div>EnterpriseNoise</div>,
}))
const mockUseAppContext = useAppContext as Mock
let assignedHref = ''
const originalLocation = window.location

View File

@ -10,7 +10,6 @@ import { Plan } from '@/app/components/billing/type'
import {
initialLangGeniusVersionInfo,
initialWorkspaceInfo,
useAppContext,
userProfilePlaceholder,
} from '@/context/app-context'
import { useModalContext } from '@/context/modal-context'
@ -46,6 +45,7 @@ const { mockToast } = vi.hoisted(() => {
})
return { mockToast }
})
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
@ -57,7 +57,7 @@ vi.mock('@/context/app-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/app-context')>()
return {
...actual,
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}
})
vi.mock('@langgenius/dify-ui/toast', () => ({
@ -66,7 +66,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
const mockUseProviderContext = vi.mocked(useProviderContext)
const mockUseModalContext = vi.mocked(useModalContext)
const mockUseAppContext = vi.mocked(useAppContext)
const createProviderContext = ({
enableBilling = false,

View File

@ -11,7 +11,6 @@ import { Plan } from '@/app/components/billing/type'
import {
initialLangGeniusVersionInfo,
initialWorkspaceInfo,
useAppContext,
userProfilePlaceholder,
} from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
@ -43,6 +42,10 @@ const { mockNotify, mockToast } = vi.hoisted(() => {
})
return { mockNotify, mockToast }
})
const appContextStateRef = vi.hoisted(() => ({
value: undefined as AppContextValue | undefined,
}))
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: mockToast,
@ -54,9 +57,22 @@ vi.mock('@/context/app-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/app-context')>()
return {
...actual,
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}
})
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
...appContextStateRef.value,
refreshCurrentWorkspace: appContextStateRef.value?.mutateCurrentWorkspace,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
@ -66,7 +82,6 @@ vi.mock('@/app/components/base/image-uploader/utils', () => ({
}))
const mockUpdateCurrentWorkspace = vi.mocked(updateCurrentWorkspace)
const mockUseAppContext = vi.mocked(useAppContext)
const mockUseProviderContext = vi.mocked(useProviderContext)
const mockImageUpload = vi.mocked(imageUpload)
const mockGetImageUploadErrorMessage = vi.mocked(getImageUploadErrorMessage)
@ -120,11 +135,15 @@ const createAppContextValue = (overrides: Partial<AppContextValue> = {}): AppCon
describe('useWebAppBrand', () => {
let appContextValue: AppContextValue
const setAppContextValue = (nextValue: AppContextValue) => {
appContextValue = nextValue
appContextStateRef.value = nextValue
}
beforeEach(() => {
vi.clearAllMocks()
appContextValue = createAppContextValue()
setAppContextValue(createAppContextValue())
currentBrandingOverrides = {}
mockUpdateCurrentWorkspace.mockResolvedValue(appContextValue.currentWorkspace)
@ -146,10 +165,10 @@ describe('useWebAppBrand', () => {
})
it('should disable uploads when customization management permission is missing', () => {
appContextValue = createAppContextValue({
setAppContextValue(createAppContextValue({
workspacePermissionKeys: [],
isCurrentWorkspaceManager: true,
})
}))
const { result } = renderHook(() => useWebAppBrand())
@ -158,10 +177,10 @@ describe('useWebAppBrand', () => {
})
it('should allow uploads for non-manager users with customization management permission', () => {
appContextValue = createAppContextValue({
setAppContextValue(createAppContextValue({
workspacePermissionKeys: ['customization.manage'],
isCurrentWorkspaceManager: false,
})
}))
const { result } = renderHook(() => useWebAppBrand())
@ -174,7 +193,7 @@ describe('useWebAppBrand', () => {
enableBilling: true,
planType: Plan.sandbox,
}))
appContextValue = createAppContextValue({
setAppContextValue(createAppContextValue({
currentWorkspace: {
...initialWorkspaceInfo,
custom_config: {
@ -182,7 +201,7 @@ describe('useWebAppBrand', () => {
remove_webapp_brand: true,
},
},
})
}))
const { result } = renderHook(() => useWebAppBrand())
@ -200,12 +219,12 @@ describe('useWebAppBrand', () => {
})
it('should fall back to an empty custom logo when custom config is missing', () => {
appContextValue = {
setAppContextValue({
...createAppContextValue(),
currentWorkspace: {
...initialWorkspaceInfo,
},
}
})
const { result } = renderHook(() => useWebAppBrand())
@ -308,9 +327,9 @@ describe('useWebAppBrand', () => {
it('should persist the selected logo and reset transient state on apply', async () => {
const mutateCurrentWorkspace = vi.fn()
appContextValue = createAppContextValue({
setAppContextValue(createAppContextValue({
mutateCurrentWorkspace,
})
}))
mockImageUpload.mockImplementation(({ onSuccessCallback }) => {
onSuccessCallback({ id: 'new-logo' })
})
@ -345,9 +364,9 @@ describe('useWebAppBrand', () => {
it('should restore the default branding configuration', async () => {
const mutateCurrentWorkspace = vi.fn()
appContextValue = createAppContextValue({
setAppContextValue(createAppContextValue({
mutateCurrentWorkspace,
})
}))
const { result } = renderHook(() => useWebAppBrand())
@ -367,9 +386,9 @@ describe('useWebAppBrand', () => {
it('should persist brand removal changes', async () => {
const mutateCurrentWorkspace = vi.fn()
appContextValue = createAppContextValue({
setAppContextValue(createAppContextValue({
mutateCurrentWorkspace,
})
}))
const { result } = renderHook(() => useWebAppBrand())

View File

@ -1,11 +1,12 @@
import type { ChangeEvent } from 'react'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue, useSetAtom } from 'jotai'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { currentWorkspaceAtom, refreshCurrentWorkspaceAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useProviderContext } from '@/context/provider-context'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { updateCurrentWorkspace } from '@/service/common'
@ -17,7 +18,9 @@ const WEB_APP_LOGO_UPLOAD_URL = '/workspaces/custom-config/webapp-logo/upload'
const useWebAppBrand = () => {
const { t } = useTranslation()
const { plan, enableBilling } = useProviderContext()
const { currentWorkspace, mutateCurrentWorkspace, workspacePermissionKeys } = useAppContext()
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const mutateCurrentWorkspace = useSetAtom(refreshCurrentWorkspaceAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const [fileId, setFileId] = useState('')
const [imgKey, setImgKey] = useState(() => Date.now())
const [uploadProgress, setUploadProgress] = useState(0)

View File

@ -1,9 +1,10 @@
'use client'
import { useAtomValue } from 'jotai'
import { useStore as useAppStore } from '@/app/components/app/store'
import Loading from '@/app/components/base/loading'
import ApiServer from '@/app/components/develop/ApiServer'
import Doc from '@/app/components/develop/doc'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { getAppACLCapabilities } from '@/utils/permission'
type IDevelopMainProps = {
@ -12,8 +13,8 @@ type IDevelopMainProps = {
const DevelopMain = ({ appId }: IDevelopMainProps) => {
const appDetail = useAppStore(state => state.appDetail)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const appACLCapabilities = getAppACLCapabilities(appDetail?.permission_keys, {
currentUserId,
resourceMaintainer: appDetail?.maintainer,

View File

@ -35,6 +35,22 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
currentWorkspace: mockCurrentWorkspace(),
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager(),
isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor(),
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: vi.fn((value: number, _format: string) => `Formatted: ${value}`),
@ -357,7 +373,7 @@ describe('SecretKeyModal', () => {
})
it('should disable create button when no workspace', async () => {
mockCurrentWorkspace.mockReturnValue(null)
mockCurrentWorkspace.mockReturnValue({ id: '', name: '' })
await renderModal(<SecretKeyModal {...defaultProps} />)
const createButton = screen.getByText('appApi.apiKeyModal.createNewSecretKey').closest('button')

View File

@ -12,6 +12,7 @@ import {
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useAtomValue } from 'jotai'
import {
useState,
} from 'react'
@ -19,7 +20,7 @@ import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import { currentWorkspaceAtom } from '@/context/app-context-state'
import useTimestamp from '@/hooks/use-timestamp'
import {
createApikey as createAppApikey,
@ -49,7 +50,7 @@ const SecretKeyModal = ({
}: ISecretKeyModalProps) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
const { currentWorkspace } = useAppContext()
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [isVisible, setIsVisible] = useState(false)
const [newKey, setNewKey] = useState<CreateApiKeyResponse | undefined>(undefined)
@ -81,7 +82,7 @@ const SecretKeyModal = ({
}
const onCreate = async () => {
if (!currentWorkspace || !canManage)
if (!currentWorkspace.id || !canManage)
return
const params = appId
@ -129,7 +130,14 @@ const SecretKeyModal = ({
</DialogTitle>
<div className="-mt-6 -mr-2 mb-4 flex justify-end">
<span className="i-heroicons-x-mark-20-solid size-6 cursor-pointer text-text-tertiary" onClick={handleClose} />
<button
type="button"
aria-label={t('operation.close', { ns: 'common' })}
className="flex size-6 cursor-pointer items-center justify-center text-text-tertiary"
onClick={handleClose}
>
<span className="i-heroicons-x-mark-20-solid size-6 cursor-pointer" aria-hidden="true" />
</button>
</div>
<p className="mt-1 shrink-0 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.apiSecretKeyTips', { ns: 'appApi' })}</p>
{isApiKeysLoading && <div className="mt-4"><Loading /></div>}
@ -168,7 +176,7 @@ const SecretKeyModal = ({
)
}
<div className="flex">
<Button className={`mt-4 flex shrink-0 ${s.autoWidth}`} onClick={onCreate} disabled={!currentWorkspace || !canManage}>
<Button className={`mt-4 flex shrink-0 ${s.autoWidth}`} onClick={onCreate} disabled={!currentWorkspace.id || !canManage}>
<span className="mr-1 i-heroicons-plus-20-solid flex size-4 shrink-0" />
<div className="text-xs font-medium text-text-secondary">{t('apiKeyModal.createNewSecretKey', { ns: 'appApi' })}</div>
</Button>

View File

@ -1,6 +1,4 @@
import type { Mock } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { useAppContext } from '@/context/app-context'
import { render, screen } from '@testing-library/react'
import { MediaType } from '@/hooks/use-breakpoints'
import Explore from '../index'
@ -42,17 +40,10 @@ vi.mock('@/service/use-explore', () => ({
}),
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
}))
describe('Explore', () => {
beforeEach(() => {
vi.clearAllMocks()
mockMediaType = MediaType.pc
;(useAppContext as Mock).mockReturnValue({
isCurrentWorkspaceDatasetOperator: false,
})
})
describe('Rendering', () => {
@ -90,23 +81,19 @@ describe('Explore', () => {
})
describe('Effects', () => {
it('should not redirect dataset operators at component level', async () => {
;(useAppContext as Mock).mockReturnValue({
isCurrentWorkspaceDatasetOperator: true,
})
it('should not redirect at component level', () => {
render((
<Explore>
<div>child</div>
</Explore>
))
await waitFor(() => {
expect(mockReplace).not.toHaveBeenCalled()
})
expect(mockReplace).not.toHaveBeenCalled()
})
it('should not redirect non dataset operators', () => {
it('should not redirect on mobile', () => {
mockMediaType = MediaType.mobile
render((
<Explore>
<div>child</div>

View File

@ -7,7 +7,6 @@ import type { App as WorkspaceApp } from '@/types/app'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import { createStore, Provider as JotaiProvider } from 'jotai'
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
import { useAppContext } from '@/context/app-context'
import { fetchAppDetail, fetchAppList, fetchBanners } from '@/service/explore'
import { renderWithNuqs } from '@/test/nuqs-testing'
import { AppModeEnum } from '@/types/app'
@ -15,12 +14,10 @@ import { AppACLPermission } from '@/utils/permission'
import { LEARN_DIFY_HIDDEN_STORAGE_KEY } from '../../learn-dify/storage'
import AppList from '../index'
type MockAppContext = {
userProfile: { id: string }
workspacePermissionKeys: string[]
}
const mockUseAppContext = vi.hoisted(() => vi.fn<() => MockAppContext>())
const mockAppContextState = vi.hoisted(() => ({
userProfile: { id: 'user-1' },
workspacePermissionKeys: [] as string[],
}))
let mockExploreData: { categories: string[], allList: App[] } | undefined = { categories: [], allList: [] }
let mockLearnDifyApps: App[] = []
@ -127,10 +124,17 @@ vi.mock('@/service/client', () => ({
},
}))
vi.mock('@/context/app-context', () => ({
useAppContext: mockUseAppContext,
useSelector: <T,>(selector: (state: MockAppContext) => T): T => selector(mockUseAppContext()),
}))
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('@/hooks/use-import-dsl', () => ({
useImportDSL: () => ({
@ -282,10 +286,7 @@ const createBanner = (overrides: Partial<BannerType> = {}): BannerType => ({
})
const mockAppCreatePermission = (hasEditPermission: boolean) => {
;(useAppContext as Mock).mockReturnValue({
userProfile: { id: 'user-1' },
workspacePermissionKeys: hasEditPermission ? ['app.create_and_management'] : [],
})
mockAppContextState.workspacePermissionKeys = hasEditPermission ? ['app.create_and_management'] : []
}
type RenderOptions = {

View File

@ -9,6 +9,7 @@ import type { TrackCreateAppParams } from '@/utils/create-app-tracking'
import { cn } from '@langgenius/dify-ui/cn'
import { queryOptions, useQueries, useSuspenseQuery } from '@tanstack/react-query'
import { useDebounceFn } from 'ahooks'
import { useAtomValue } from 'jotai'
import { useQueryState } from 'nuqs'
import * as React from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
@ -17,7 +18,7 @@ import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-conf
import AppCard from '@/app/components/explore/app-card'
import Banner from '@/app/components/explore/banner/banner'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import { useAppContext } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useLocale } from '@/context/i18n'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useImportDSL } from '@/hooks/use-import-dsl'
@ -101,7 +102,7 @@ function getDisabledBannersQueryOptions() {
const Apps = ({ onSuccess }: { onSuccess?: () => void }) => {
const { t } = useTranslation()
const locale = useLocale()
const { workspacePermissionKeys } = useAppContext()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { data: systemFeatures } = useSuspenseQuery(
systemFeaturesQueryOptions(),
)

View File

@ -6,10 +6,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import Banner from '../banner'
const mockUseGetBanners = vi.fn()
const mockUseSelector = vi.fn()
const mockTrackEvent = vi.fn()
let mockSelectedIndex = 0
const mockCarouselListeners = new Set<() => void>()
const mockAppContextState = vi.hoisted(() => ({
userProfile: {
id: 'account-123',
name: 'Evan',
},
}))
const setMockSelectedIndex = (index: number) => {
mockSelectedIndex = index
@ -20,9 +25,17 @@ vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
}))
vi.mock('@/context/app-context', () => ({
useSelector: (...args: unknown[]) => mockUseSelector(...args),
}))
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('@/app/components/base/amplitude', () => ({
trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
@ -133,12 +146,10 @@ describe('Banner', () => {
vi.useFakeTimers()
mockSelectedIndex = 0
mockCarouselListeners.clear()
mockUseSelector.mockImplementation(selector => selector({
userProfile: {
id: 'account-123',
name: 'Evan',
},
}))
mockAppContextState.userProfile = {
id: 'account-123',
name: 'Evan',
}
})
afterEach(() => {
@ -357,12 +368,10 @@ describe('Banner', () => {
})
it('does not track impressions when account id is unavailable', () => {
mockUseSelector.mockImplementation(selector => selector({
userProfile: {
id: '',
name: '',
},
}))
mockAppContextState.userProfile = {
id: '',
name: '',
}
mockUseGetBanners.mockReturnValue({
data: [createMockBanner('1', 'enabled', 'Enabled Banner 1')],
isLoading: false,

View File

@ -1,10 +1,11 @@
import type { Banner as BannerType } from '@/models/app'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
import { Carousel, useCarousel } from '@/app/components/base/carousel'
import { useSelector } from '@/context/app-context'
import { userProfileAtom } from '@/context/app-context-state'
import { useLocale } from '@/context/i18n'
import { BannerItem } from './banner-item'
@ -59,8 +60,9 @@ function Banner({
}: BannerProps) {
const { t } = useTranslation()
const locale = useLocale()
const accountId = useSelector(s => s.userProfile.id)
const userName = useSelector(s => s.userProfile.name)
const userProfile = useAtomValue(userProfileAtom)
const accountId = userProfile.id
const userName = userProfile.name
const [isHovered, setIsHovered] = useState(false)
const [isResizing, setIsResizing] = useState(false)
const resizeTimerRef = useRef<NodeJS.Timeout | null>(null)

View File

@ -7,7 +7,7 @@ import { AppModeEnum } from '@/types/app'
import { AppACLPermission } from '@/utils/permission'
import ContinueWorkItem from '../item'
const mockAppContext = vi.hoisted(() => ({
const mockAppContextState = vi.hoisted(() => ({
userProfile: { id: 'user-1' },
workspacePermissionKeys: ['app.create_and_management'],
}))
@ -18,9 +18,17 @@ const toastMocks = vi.hoisted(() => ({
warning: vi.fn(),
}))
vi.mock('@/context/app-context', () => ({
useSelector: (selector: (state: typeof mockAppContext) => unknown) => selector(mockAppContext),
}))
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('@/hooks/use-format-time-from-now', () => ({
useFormatTimeFromNow: () => ({
@ -82,8 +90,8 @@ const renderItem = (
describe('ContinueWorkItem', () => {
beforeEach(() => {
vi.clearAllMocks()
mockAppContext.userProfile = { id: 'user-1' }
mockAppContext.workspacePermissionKeys = ['app.create_and_management']
mockAppContextState.userProfile = { id: 'user-1' }
mockAppContextState.workspacePermissionKeys = ['app.create_and_management']
mockFormatTimeFromNow.mockReturnValue('5 minutes ago')
})

View File

@ -4,11 +4,12 @@ import type { App } from '@/types/app'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { AppTypeIcon } from '@/app/components/app/type-selector'
import AppIcon from '@/app/components/base/app-icon'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import Link from '@/next/link'
@ -24,8 +25,8 @@ const ContinueWorkItem = ({
}: ContinueWorkItemProps) => {
const { t } = useTranslation()
const { formatTimeFromNow } = useFormatTimeFromNow()
const currentUserId = useAppContextSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const isRbacEnabled = systemFeatures.rbac_enabled
const updatedAt = (app.updated_at || app.created_at) * 1000

View File

@ -6,7 +6,6 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { Plan } from '@/app/components/billing/type'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useAppContext } from '@/context/app-context'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { useRouter } from '@/next/navigation'
@ -46,6 +45,7 @@ const { mockSetTheme } = vi.hoisted(() => ({
const mockAppContextState = vi.hoisted(() => ({
current: undefined as AppContextValue | undefined,
}))
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('next-themes', () => ({
useTheme: () => ({
@ -55,7 +55,7 @@ vi.mock('next-themes', () => ({
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
@ -166,7 +166,7 @@ const baseAppContextValue: AppContextValue = {
const setAppContextValue = (value: AppContextValue) => {
mockAppContextState.current = value
vi.mocked(useAppContext).mockReturnValue(value)
mockUseAppContext.mockReturnValue(value)
}
describe('AccountDropdown', () => {

View File

@ -3,7 +3,6 @@ import type { AppContextValue } from '@/context/app-context'
import { fireEvent, screen } from '@testing-library/react'
import { useState } from 'react'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { useAppContext } from '@/context/app-context'
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { ACCOUNT_SETTING_TAB } from '../constants'
@ -13,6 +12,7 @@ const mockResetModelProviderListExpanded = vi.fn()
const mockAppContextState = vi.hoisted(() => ({
current: null as unknown,
}))
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@/context/provider-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/provider-context')>()
@ -26,7 +26,7 @@ vi.mock('@/context/app-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/app-context')>()
return {
...actual,
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
useSelector: vi.fn((selector: (state: unknown) => unknown) => selector(mockAppContextState.current)),
}
})
@ -239,7 +239,7 @@ describe('AccountSetting', () => {
enableBilling: true,
enableReplaceWebAppLogo: true,
})
vi.mocked(useAppContext).mockReturnValue(baseAppContextValue)
mockUseAppContext.mockReturnValue(baseAppContextValue)
mockAppContextState.current = baseAppContextValue
vi.mocked(useBreakpoints).mockReturnValue(MediaType.pc)
})
@ -362,7 +362,7 @@ describe('AccountSetting', () => {
...baseAppContextValue,
isCurrentWorkspaceDatasetOperator: true,
}
vi.mocked(useAppContext).mockReturnValue(datasetOperatorContext)
mockUseAppContext.mockReturnValue(datasetOperatorContext)
mockAppContextState.current = datasetOperatorContext
// Act
@ -383,7 +383,7 @@ describe('AccountSetting', () => {
...baseAppContextValue,
workspacePermissionKeys: baseAppContextValue.workspacePermissionKeys.filter(key => key !== 'api_extension.manage'),
}
vi.mocked(useAppContext).mockReturnValue(contextWithoutApiExtensionPermission)
mockUseAppContext.mockReturnValue(contextWithoutApiExtensionPermission)
mockAppContextState.current = contextWithoutApiExtensionPermission
// Act
@ -401,7 +401,7 @@ describe('AccountSetting', () => {
...baseAppContextValue,
workspacePermissionKeys: baseAppContextValue.workspacePermissionKeys.filter(key => key !== 'customization.manage'),
}
vi.mocked(useAppContext).mockReturnValue(contextWithoutCustomizationPermission)
mockUseAppContext.mockReturnValue(contextWithoutCustomizationPermission)
mockAppContextState.current = contextWithoutCustomizationPermission
// Act
@ -420,7 +420,7 @@ describe('AccountSetting', () => {
...baseAppContextValue,
workspacePermissionKeys: baseAppContextValue.workspacePermissionKeys.filter(key => key !== 'workspace.role.manage'),
}
vi.mocked(useAppContext).mockReturnValue(contextWithoutRoleManagePermission)
mockUseAppContext.mockReturnValue(contextWithoutRoleManagePermission)
mockAppContextState.current = contextWithoutRoleManagePermission
// Act
@ -505,7 +505,7 @@ describe('AccountSetting', () => {
...baseAppContextValue,
workspacePermissionKeys: baseAppContextValue.workspacePermissionKeys.filter(key => key !== 'billing.view'),
}
vi.mocked(useAppContext).mockReturnValue(contextWithoutBillingViewPermission)
mockUseAppContext.mockReturnValue(contextWithoutBillingViewPermission)
mockAppContextState.current = contextWithoutBillingViewPermission
// Act
@ -521,7 +521,7 @@ describe('AccountSetting', () => {
...baseAppContextValue,
workspacePermissionKeys: baseAppContextValue.workspacePermissionKeys.filter(key => key !== 'billing.view'),
}
vi.mocked(useAppContext).mockReturnValue(contextWithoutBillingViewPermission)
mockUseAppContext.mockReturnValue(contextWithoutBillingViewPermission)
mockAppContextState.current = contextWithoutBillingViewPermission
// Act

View File

@ -7,7 +7,6 @@ import { vi } from 'vitest'
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles'
@ -17,8 +16,11 @@ import MembersPage from '../index'
const mockAppContextState = vi.hoisted(() => ({
current: {} as Partial<AppContextValue>,
}))
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@/context/app-context')
vi.mock('@/context/app-context', () => ({
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
@ -55,7 +57,7 @@ const createRole = (overrides: Partial<Role>): Role => ({
const setAppContextValue = (value: AppContextValue) => {
mockAppContextState.current = value
vi.mocked(useAppContext).mockReturnValue(value)
mockUseAppContext.mockReturnValue(value)
}
vi.mock('../edit-workspace-modal', () => ({

View File

@ -3,11 +3,14 @@ import type { ICurrentWorkspace } from '@/models/common'
import { screen } from '@testing-library/react'
import { vi } from 'vitest'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { useAppContext } from '@/context/app-context'
import { useWorkspacePermissions } from '@/service/use-workspace'
import InviteButton from '../invite-button'
vi.mock('@/context/app-context')
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@/context/app-context', () => ({
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
@ -41,7 +44,7 @@ describe('InviteButton', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(useAppContext).mockReturnValue({
mockUseAppContext.mockReturnValue({
currentWorkspace: { id: 'workspace-id' } as ICurrentWorkspace,
} as unknown as AppContextValue)
})

View File

@ -1,7 +1,6 @@
import type { ReactNode } from 'react'
import type { AppContextValue } from '@/context/app-context'
import { render } from '@testing-library/react'
import { useAppContext } from '@/context/app-context'
import EditWorkspaceModal from '../index'
type DialogProps = {
@ -14,6 +13,7 @@ let latestOnOpenChange: DialogProps['onOpenChange']
const mockAppContextState = vi.hoisted(() => ({
current: {} as Partial<AppContextValue>,
}))
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@langgenius/dify-ui/dialog', () => ({
Dialog: ({ children, onOpenChange }: DialogProps) => {
@ -30,7 +30,7 @@ vi.mock('@langgenius/dify-ui/dialog', () => ({
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
@ -52,7 +52,7 @@ describe('EditWorkspaceModal dialog lifecycle', () => {
isCurrentWorkspaceOwner: true,
} as never
mockAppContextState.current = appContextValue
vi.mocked(useAppContext).mockReturnValue(appContextValue)
mockUseAppContext.mockReturnValue(appContextValue)
})
it('should only call onCancel when the dialog requests closing', () => {

View File

@ -3,7 +3,6 @@ import type { ICurrentWorkspace } from '@/models/common'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import { useAppContext } from '@/context/app-context'
import { updateWorkspaceInfo } from '@/service/common'
import EditWorkspaceModal from '../index'
@ -13,10 +12,13 @@ const toastMocks = vi.hoisted(() => ({
const mockAppContextState = vi.hoisted(() => ({
current: {} as Partial<AppContextValue>,
}))
const mockUseAppContext = vi.hoisted(() => vi.fn())
const getSaveButton = () => screen.getByRole('button', { name: /operation\.(save|saving)/i })
vi.mock('@/context/app-context')
vi.mock('@/context/app-context', () => ({
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
@ -50,7 +52,7 @@ describe('EditWorkspaceModal', () => {
isCurrentWorkspaceOwner: true,
} as unknown as AppContextValue
mockAppContextState.current = appContextValue
vi.mocked(useAppContext).mockReturnValue(appContextValue)
mockUseAppContext.mockReturnValue(appContextValue)
})
afterEach(() => {
@ -165,7 +167,7 @@ describe('EditWorkspaceModal', () => {
})
it('should disable confirm button for non-owners', async () => {
vi.mocked(useAppContext).mockReturnValue({
mockUseAppContext.mockReturnValue({
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
isCurrentWorkspaceOwner: false,
} as unknown as AppContextValue)

View File

@ -3,7 +3,6 @@ import type { ICurrentWorkspace } from '@/models/common'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi } from 'vitest'
import { useAppContext } from '@/context/app-context'
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
import { useMembers } from '@/service/use-common'
import TransferOwnershipModal from '../index'
@ -14,8 +13,11 @@ const toastMocks = vi.hoisted(() => ({
const mockAppContextState = vi.hoisted(() => ({
current: {} as Partial<AppContextValue>,
}))
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@/context/app-context')
vi.mock('@/context/app-context', () => ({
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState.current)
@ -56,7 +58,7 @@ describe('TransferOwnershipModal', () => {
userProfile: { email: 'owner@example.com', id: 'owner-id' },
} as unknown as AppContextValue
mockAppContextState.current = appContextValue
vi.mocked(useAppContext).mockReturnValue(appContextValue)
mockUseAppContext.mockReturnValue(appContextValue)
vi.mocked(useMembers).mockReturnValue({
data: { accounts: [] },

View File

@ -1,15 +1,15 @@
import type { AppContextValue } from '@/context/app-context'
import { render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { useAppContext } from '@/context/app-context'
import EnvNav from '../index'
const mockAppContextState = vi.hoisted(() => ({
current: {} as Partial<AppContextValue>,
}))
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
@ -23,8 +23,6 @@ vi.mock('jotai', async (importOriginal) => {
})
describe('EnvNav', () => {
const mockUseAppContext = vi.mocked(useAppContext)
beforeEach(() => {
vi.clearAllMocks()
})

View File

@ -9,10 +9,11 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useParams } from '@/next/navigation'
import { datasetDetailQueryKeyPrefix } from '@/service/knowledge/use-dataset'
@ -25,8 +26,8 @@ const Conversion = () => {
const { t } = useTranslation()
const { datasetId } = useParams()
const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const { mutateAsync: convert, isPending } = useConvertDatasetToPipeline()
const invalidDatasetDetail = useInvalid([...datasetDetailQueryKeyPrefix, datasetId])

View File

@ -157,6 +157,24 @@ 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: {
id: mockCurrentUserId,
},
isLoadingWorkspacePermissionKeys: mockIsLoadingWorkspacePermissionKeys,
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
const mockSetShowPricingModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
useModalContextSelector: () => mockSetShowPricingModal,

View File

@ -149,6 +149,24 @@ 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: {
id: mockCurrentUserId,
},
isLoadingWorkspacePermissionKeys: mockIsLoadingWorkspacePermissionKeys,
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
const mockSetShowPricingModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
useModalContextSelector: <T,>(selector: (state: { setShowPricingModal: typeof mockSetShowPricingModal }) => T): T =>

View File

@ -137,6 +137,7 @@ vi.mock('@/app/components/base/premium-badge', () => ({
vi.mock('@/config', () => ({
IS_CLOUD_EDITION: true,
MARKETPLACE_API_PREFIX: '/marketplace/api',
}))
vi.mock('@/app/components/workflow/hooks', () => ({
@ -165,6 +166,24 @@ 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: {
id: mockCurrentUserId,
},
isLoadingWorkspacePermissionKeys: mockIsLoadingWorkspacePermissionKeys,
workspacePermissionKeys: mockWorkspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/i18n', () => ({
useDocLink: () => () => 'https://docs.dify.ai',
}))

View File

@ -15,6 +15,7 @@ import { toast } from '@langgenius/dify-ui/toast'
import { RiArrowRightUpLine, RiPlayCircleLine, RiTerminalBoxLine } from '@remixicon/react'
import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys'
import { useBoolean } from 'ahooks'
import { useAtomValue } from 'jotai'
import { memo, useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
@ -24,7 +25,7 @@ import PremiumBadge from '@/app/components/base/premium-badge'
import { useChecklistBeforePublish } from '@/app/components/workflow/hooks'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { IS_CLOUD_EDITION } from '@/config'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom, workspacePermissionKeysLoadingAtom } from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useModalContextSelector } from '@/context/modal-context'
import { useProviderContextSelector } from '@/context/provider-context'
@ -64,9 +65,9 @@ const Popup = ({
const pipelineId = useStore(s => s.pipelineId)
const dataset = useDatasetDetailContextWithSelector(s => s.dataset)
const mutateDatasetRes = useDatasetDetailContextWithSelector(s => s.mutateDatasetRes)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const isLoadingWorkspacePermissionKeys = useAppContextWithSelector(state => state.isLoadingWorkspacePermissionKeys)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const [published, setPublished] = useState(false)
const { formatTimeFromNow } = useFormatTimeFromNow()
const { handleCheckBeforePublish } = useChecklistBeforePublish()

View File

@ -1,4 +1,6 @@
import type { WorkflowProps } from '@/app/components/workflow'
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store'
import { useAtomValue } from 'jotai'
import {
useCallback,
useMemo,
@ -6,7 +8,7 @@ import {
import { WorkflowWithInnerContext } from '@/app/components/workflow'
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { getDatasetACLCapabilities } from '@/utils/permission'
import {
@ -30,8 +32,8 @@ const RagPipelineMain = ({
}: RagPipelineMainProps) => {
const workflowStore = useWorkflowStore()
const dataset = useDatasetDetailContextWithSelector(s => s.dataset)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const datasetACLCapabilities = useMemo(
() => getDatasetACLCapabilities(dataset?.permission_keys, {
currentUserId,
@ -41,7 +43,12 @@ const RagPipelineMain = ({
[dataset?.maintainer, dataset?.permission_keys, currentUserId, workspacePermissionKeys],
)
const handleWorkflowDataUpdate = useCallback((payload: any) => {
type WorkflowDataUpdatePayload = {
rag_pipeline_variables?: Parameters<NonNullable<ReturnType<typeof workflowStore.getState>['setRagPipelineVariables']>>[0]
environment_variables?: Parameters<ReturnType<typeof workflowStore.getState>['setEnvironmentVariables']>[0]
}
const handleWorkflowDataUpdate = useCallback((payload: WorkflowDataUpdatePayload) => {
const {
rag_pipeline_variables,
environment_variables,
@ -179,7 +186,7 @@ const RagPipelineMain = ({
nodes={nodes}
edges={edges}
viewport={viewport}
hooksStore={hooksStore as any}
hooksStore={hooksStore as unknown as Partial<HooksStoreShape>}
onWorkflowDataUpdate={handleWorkflowDataUpdate}
>
<RagPipelineChildren />

View File

@ -40,6 +40,21 @@ 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, () => ({
isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor(),
workspacePermissionKeys: mockWorkspacePermissionKeys(),
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/service/use-common', () => ({
useMembers: () => ({
data: {

View File

@ -34,6 +34,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, () => ({
workspacePermissionKeys: mockWorkspacePermissionKeys(),
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/service/use-snippets', () => ({
useCreateSnippetMutation: () => ({
mutateAsync: mockCreateMutateAsync,

View File

@ -19,11 +19,12 @@ import {
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import CreateSnippetDialog from '@/app/components/snippets/create-snippet-dialog'
import { canCreateAndModifySnippets, canManageSnippets } from '@/app/components/snippets/utils/permission'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { TagSelector } from '@/features/tag-management/components/tag-selector'
import Link from '@/next/link'
import { useMembers } from '@/service/use-common'
@ -46,7 +47,7 @@ const SnippetCard = ({
}: Props) => {
const { t } = useTranslation('snippet')
const { t: tCommon } = useTranslation()
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { data: membersData } = useMembers()
const [isOperationsMenuOpen, setIsOperationsMenuOpen] = useState(false)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)

View File

@ -5,9 +5,10 @@ import type { SnippetListItem } from '@/types/snippet'
import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import { useDebounce } from 'ahooks'
import { useAtomValue } from 'jotai'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { currentWorkspaceLoadingAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { TagFilter } from '@/features/tag-management/components/tag-filter'
import useDocumentTitle from '@/hooks/use-document-title'
import dynamic from '@/next/dynamic'
@ -56,8 +57,8 @@ const SnippetCardSkeleton = ({ count }: SnippetCardSkeletonProps) => {
const SnippetList = () => {
const { t } = useTranslation()
const isLoadingCurrentWorkspace = useAppContextWithSelector(state => state.isLoadingCurrentWorkspace)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
// eslint-disable-next-line react/use-state -- custom URL query hook, not React.useState
const {
query: { tagIDs, keywords, creatorIDs },

View File

@ -33,6 +33,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, () => ({
workspacePermissionKeys: contextMocks.workspacePermissionKeys,
}))
})
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: toastMocks,
}))

View File

@ -58,6 +58,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, () => ({
workspacePermissionKeys: mockAppContext.workspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
let capturedHooksStore: Record<string, unknown> | undefined
let capturedWorkflowNodes: WorkflowProps['nodes'] | undefined
let snippetDetailStoreState: {

View File

@ -5,6 +5,7 @@ import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-s
import type { SnippetCanvasData, SnippetDetailPayload, SnippetInputField } from '@/models/snippet'
import type { SnippetDraftSyncPayload } from '@/types/snippet'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import {
useCallback,
useEffect,
@ -23,7 +24,7 @@ import {
initialEdges,
initialNodes,
} from '@/app/components/workflow/utils'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useSnippetDraftStore } from '../draft-store'
import { useConfigsMap } from '../hooks/use-configs-map'
import { useGetRunAndTraceUrl } from '../hooks/use-get-run-and-trace-url'
@ -143,7 +144,7 @@ const SnippetMain = ({
const effectiveDraftEdges = localDraftState?.edges ?? draftEdges
const effectiveDraftViewport = localDraftState?.viewport ?? draftViewport
const { graph, snippet } = effectiveDraftPayload
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canEditSnippet = canCreateAndModifySnippets(workspacePermissionKeys)
const canSave = canEditSnippet && currentCanvasNodeCount > 0
const {

View File

@ -52,6 +52,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, () => ({
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: {
success: (...args: unknown[]) => mockToastSuccess(...args),

View File

@ -1,8 +1,9 @@
import type { CreateSnippetDialogPayload } from '@/app/components/snippets/create-snippet-dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useRouter } from '@/next/navigation'
import { consoleClient } from '@/service/client'
import { useCreateSnippetMutation } from '@/service/use-snippets'
@ -11,7 +12,7 @@ import { canCreateAndModifySnippets } from '../utils/permission'
export const useCreateSnippet = () => {
const { t } = useTranslation()
const { push } = useRouter()
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const createSnippetMutation = useCreateSnippetMutation()
const [isCreateSnippetDialogOpen, setIsCreateSnippetDialogOpen] = useState(false)
const [isCreatingSnippet, setIsCreatingSnippet] = useState(false)

View File

@ -16,10 +16,11 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Input } from '@langgenius/dify-ui/input'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useRouter } from '@/next/navigation'
import {
useConfirmSnippetImportMutation,
@ -107,7 +108,7 @@ function ImportSnippetDSLDialog({
}: ImportSnippetDSLDialogProps) {
const { t } = useTranslation()
const { push } = useRouter()
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canCreateAndModifySnippet = canCreateAndModifySnippets(workspacePermissionKeys)
const importSnippetMutation = useImportSnippetDSLMutation()
const confirmSnippetImportMutation = useConfirmSnippetImportMutation()

View File

@ -88,6 +88,23 @@ vi.mock('@/context/app-context', () => ({
useAppContext: () => appContextState,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
isLoadingCurrentWorkspace: appContextState.isLoadingCurrentWorkspace,
currentWorkspace: appContextState.currentWorkspace,
userProfile: appContextState.userProfile,
workspacePermissionKeys: appContextState.workspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/next/navigation', () => ({
useSearchParams: () => ({
get: (key: string) => (key === 'replayRunId' ? searchParamsValue : null),

View File

@ -4,6 +4,7 @@ import type { CollaborationUpdate } from '@/app/components/workflow/collaboratio
import type { Shape as HooksStoreShape } from '@/app/components/workflow/hooks-store/store'
import type { Edge, Node } from '@/app/components/workflow/types'
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
import { useAtomValue } from 'jotai'
import {
useCallback,
useEffect,
@ -21,7 +22,7 @@ import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { fetchWorkflowDraft } from '@/service/workflow'
import { getAppACLCapabilities } from '@/utils/permission'
import {
@ -77,8 +78,8 @@ const WorkflowMain = ({
const filteredCursors = Object.fromEntries(
Object.entries(cursors).filter(([userId]) => userId !== myUserId),
)
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const appACLCapabilities = useMemo(
() => getAppACLCapabilities(appDetail?.permission_keys, {
currentUserId,

View File

@ -54,6 +54,21 @@ 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: { id: 'user-1' },
workspacePermissionKeys: ['app.create_and_management'],
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('../use-workflow-template', () => ({
useWorkflowTemplate: () => ({
nodes: appStoreState.appDetail.mode === 'workflow'

View File

@ -1,6 +1,7 @@
import type { Edge, Node } from '@/app/components/workflow/types'
import type { FileUploadConfigResponse } from '@/models/common'
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
import { useAtomValue } from 'jotai'
import {
useCallback,
useEffect,
@ -13,7 +14,7 @@ import {
useWorkflowStore,
} from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useWorkflowConfig } from '@/service/use-workflow'
import {
fetchNodesDefaultConfigs,
@ -71,8 +72,8 @@ export const useWorkflowInit = () => {
edges: edgesTemplate,
} = useWorkflowTemplate()
const appDetail = useAppStore(state => state.appDetail)!
const currentUserId = useAppContextWithSelector(state => state.userProfile?.id)
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const appACLCapabilities = useMemo(() => getAppACLCapabilities(appDetail.permission_keys, {
currentUserId,
resourceMaintainer: appDetail.maintainer,
@ -118,9 +119,10 @@ export const useWorkflowInit = () => {
setSyncWorkflowDraftHash(initialData.hash)
setIsLoading(false)
}
catch (error: any) {
if (error && error.json && !error.bodyUsed && appDetail) {
error.json().then((err: any) => {
catch (error: unknown) {
const responseError = error as { bodyUsed?: boolean, json?: () => Promise<{ code?: string }> }
if (responseError.json && !responseError.bodyUsed && appDetail) {
responseError.json().then((err) => {
if (err.code === 'draft_workflow_not_exist') {
const isAdvancedChat = appDetail.mode === AppModeEnum.ADVANCED_CHAT
const initialGraph = {
@ -187,7 +189,7 @@ export const useWorkflowInit = () => {
if (!acc[block.type])
acc[block.type] = { ...block.config }
return acc
}, {} as Record<string, any>),
}, {} as Record<string, unknown>),
})
workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at ?? 0)
const graph = publishedWorkflow?.graph

View File

@ -2,6 +2,7 @@
import type { Features as FeaturesData } from '@/app/components/base/features/types'
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
import { useAtomValue } from 'jotai'
import {
useEffect,
useMemo,
@ -19,7 +20,7 @@ import {
initialEdges,
initialNodes,
} from '@/app/components/workflow/utils'
import { useAppContext } from '@/context/app-context'
import { currentWorkspaceAtom, currentWorkspaceLoadingAtom, userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useSearchParams } from '@/next/navigation'
import { fetchRunDetail } from '@/service/log'
import { useAppTriggers } from '@/service/use-tools'
@ -45,16 +46,19 @@ const WorkflowAppWithAdditionalContext = () => {
fileUploadConfigResponse,
} = useWorkflowInit()
const workflowStore = useWorkflowStore()
const { isLoadingCurrentWorkspace, currentWorkspace, userProfile, workspacePermissionKeys } = useAppContext()
const isLoadingCurrentWorkspace = useAtomValue(currentWorkspaceLoadingAtom)
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
// Initialize trigger status at application level
const { setTriggerStatuses } = useTriggerStatusStore()
const appDetail = useAppStore(s => s.appDetail)
const appACLCapabilities = useMemo(() => getAppACLCapabilities(appDetail?.permission_keys, {
currentUserId: userProfile?.id,
currentUserId,
resourceMaintainer: appDetail?.maintainer,
workspacePermissionKeys,
}), [appDetail?.maintainer, appDetail?.permission_keys, userProfile?.id, workspacePermissionKeys])
}), [appDetail?.maintainer, appDetail?.permission_keys, currentUserId, workspacePermissionKeys])
const appId = appDetail?.id
const isWorkflowMode = appDetail?.mode === AppModeEnum.WORKFLOW
const { data: triggersResponse } = useAppTriggers(isWorkflowMode ? appId : undefined, {
@ -78,8 +82,8 @@ const WorkflowAppWithAdditionalContext = () => {
// Cancel any pending debounced sync operations
const { debouncedSyncWorkflowDraft } = workflowStore.getState()
// The debounced function from lodash has a cancel method
if (debouncedSyncWorkflowDraft && 'cancel' in debouncedSyncWorkflowDraft)
(debouncedSyncWorkflowDraft as any).cancel()
const cancellableSyncWorkflowDraft = debouncedSyncWorkflowDraft as { cancel?: () => void } | undefined
cancellableSyncWorkflowDraft?.cancel?.()
}
}, [workflowStore])

View File

@ -2,11 +2,12 @@ import type { ScheduleTriggerNodeType } from '../types'
import { renderHook } from '@testing-library/react'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { useAppContext } from '@/context/app-context'
import { createAccountProfileQueryWrapper } from '@/test/account-profile-query'
import { BlockEnum } from '../../../types'
import useConfig from '../use-config'
const mockUseAppContext = vi.hoisted(() => vi.fn())
vi.mock('@/app/components/workflow/hooks', () => ({
useNodesReadOnly: vi.fn(),
}))
@ -17,12 +18,11 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: mockUseAppContext,
}))
const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly)
const mockUseNodeCrud = vi.mocked(useNodeCrud)
const mockUseAppContext = vi.mocked(useAppContext)
const setInputs = vi.fn()
@ -48,7 +48,7 @@ describe('trigger-schedule/use-config', () => {
mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: false, getNodesReadOnly: () => false })
mockUseAppContext.mockReturnValue({
userProfile: { timezone: 'Asia/Shanghai' },
} as ReturnType<typeof useAppContext>)
})
mockUseNodeCrud.mockReturnValue({
inputs: createData(),
setInputs,

View File

@ -8,12 +8,13 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { noop } from 'es-toolkit/function'
import { useAtomValue } from 'jotai'
import { useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useEducationDiscount } from '@/app/components/billing/hooks/use-education-discount'
import { Plan } from '@/app/components/billing/type'
import { useSetEducationVerifying } from '@/app/education-apply/storage'
import { useAppContext } from '@/context/app-context'
import { currentWorkspaceAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useDocLink } from '@/context/i18n'
import { useProviderContext } from '@/context/provider-context'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
@ -52,7 +53,8 @@ const EducationApplyAgeContent = () => {
mutateAsync: educationAdd,
} = useEducationAdd({ onSuccess: noop })
const { onPlanInfoChanged, isEducationAccount, plan } = useProviderContext()
const { currentWorkspace, workspacePermissionKeys } = useAppContext()
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const updateEducationStatus = useInvalidateEducationStatus()
const docLink = useDocLink()
const { handleEducationDiscount } = useEducationDiscount()

View File

@ -1,15 +1,16 @@
import { Avatar } from '@langgenius/dify-ui/avatar'
import { Button } from '@langgenius/dify-ui/button'
import { useAtomValue } from 'jotai'
import { useTranslation } from 'react-i18next'
import { Triangle } from '@/app/components/base/icons/src/public/education'
import { useAppContext } from '@/context/app-context'
import { userProfileAtom } from '@/context/app-context-state'
import { useRouter } from '@/next/navigation'
import { useLogout } from '@/service/use-common'
const UserInfo = () => {
const router = useRouter()
const { t } = useTranslation()
const { userProfile } = useAppContext()
const userProfile = useAtomValue(userProfileAtom)
const { mutateAsync: logout } = useLogout()
const handleLogout = async () => {

View File

@ -5,11 +5,12 @@ import { Provider as JotaiProvider } from 'jotai'
import { queryClientAtom } from 'jotai-tanstack-query'
import { useHydrateAtoms } from 'jotai/react/utils'
import { Suspense } from 'react'
import { useContext } from 'use-context-selector'
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import { ZENDESK_FIELD_IDS } from '@/config'
import { initialWorkspaceInfo, useAppContext, useSelector } from '../app-context'
import { AppContext, initialWorkspaceInfo, useSelector } from '../app-context'
import { AppContextProvider } from '../app-context-provider'
const mockGetRequest = vi.hoisted(() => vi.fn())
@ -157,7 +158,7 @@ vi.mock('@/app/components/header/maintenance-notice', () => ({
}))
function AppContextProbe() {
const context = useAppContext()
const context = useContext(AppContext)
const selectedWorkspacePermissionKeys = useSelector(state => state.workspacePermissionKeys)
return (

View File

@ -3,7 +3,7 @@
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common'
import { noop } from 'es-toolkit/function'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import { createContext, useContextSelector } from 'use-context-selector'
import {
initialLangGeniusVersionInfo as defaultLangGeniusVersionInfo,
userProfilePlaceholder as defaultUserProfilePlaceholder,
@ -53,5 +53,3 @@ export const AppContext = createContext<AppContextValue>({
export function useSelector<T>(selector: (value: AppContextValue) => T): T {
return useContextSelector(AppContext, selector)
}
export const useAppContext = () => useContext(AppContext)

View File

@ -11,6 +11,7 @@ import type { InputVar } from '@/app/components/workflow/types'
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
import type { ExternalDataTool } from '@/models/common'
import type { ModerationConfig, PromptVariable } from '@/models/debug'
import { useAtomValue } from 'jotai'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
DEFAULT_ACCOUNT_SETTING_TAB,
@ -20,7 +21,7 @@ import {
isWorkspaceSettingTab,
} from '@/app/components/header/account-setting/constants'
import { useSetEducationVerifying } from '@/app/education-apply/storage'
import { useAppContext } from '@/context/app-context'
import { currentWorkspaceIdAtom } from '@/context/app-context-state'
import { useProviderContext } from '@/context/provider-context'
import {
useAccountSettingModal,
@ -103,7 +104,7 @@ export const ModalContextProvider = ({
}> | null>(null)
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
const { currentWorkspace } = useAppContext()
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
const setEducationVerifying = useSetEducationVerifying()
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
@ -146,7 +147,7 @@ export const ModalContextProvider = ({
} = useTriggerEventsLimitModal({
plan,
isFetchedPlan,
currentWorkspaceId: currentWorkspace?.id,
currentWorkspaceId,
})
const handleCancelModerationSettingModal = () => {

View File

@ -52,6 +52,18 @@ vi.mock('@/context/app-context', () => ({
useAppContext: () => mockUseAppContext(),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockUseAppContext())
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
type DefaultPlanShape = typeof defaultPlan
type ResetShape = {
apiRateLimit: number | null

View File

@ -15,8 +15,9 @@ import {
ComboboxItem,
ComboboxItemText,
} from '@langgenius/dify-ui/combobox'
import { useAtomValue } from 'jotai'
import { useTranslation } from 'react-i18next'
import { useSelector } from '@/context/app-context'
import { userProfileAtom } from '@/context/app-context-state'
import { SubjectType } from '@/models/access-control'
export function SubjectItem({
@ -154,7 +155,7 @@ type MemberItemProps = {
}
function MemberItem({ member, subject, selectedMembers }: MemberItemProps) {
const currentUser = useSelector(s => s.userProfile)
const currentUser = useAtomValue(userProfileAtom)
const { t } = useTranslation()
const isChecked = selectedMembers.some(selectedMember => selectedMember.id === member.id)
return (

View File

@ -21,6 +21,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, () => ({
workspacePermissionKeys: mockWorkspacePermissionKeys.value,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
const mockTags: Tag[] = [
{ id: 'tag-1', name: 'Frontend', type: 'app', binding_count: '' },
{ id: 'tag-2', name: 'Backend', type: 'app', binding_count: '' },

View File

@ -50,6 +50,20 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
workspacePermissionKeys: mockWorkspacePermissionKeys.value,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/service/client', () => ({
consoleQuery: {
tags: {

View File

@ -21,6 +21,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, () => ({
workspacePermissionKeys: mockWorkspacePermissionKeys.value,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
const i18n = {
selectorPlaceholder: 'common.tag.selectorPlaceholder',
operationClear: 'common.operation.clear',

View File

@ -39,6 +39,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, () => ({
workspacePermissionKeys: mockWorkspacePermissionKeys.value,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@tanstack/react-query', () => ({
useQuery: () => ({ data: mockUseQueryData.current }),
useMutation: (mutationOptions: { mutationFn: (input: unknown) => Promise<unknown> }) => ({

View File

@ -3,9 +3,10 @@ import type { TagType } from '@dify/contracts/api/console/tags/types.gen'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { consoleQuery } from '@/service/client'
import { hasPermission } from '@/utils/permission'
import { getTagManagePermissionKey } from '../utils'
@ -19,7 +20,7 @@ type TagManagementModalProps = {
}
export const TagManagementModal = ({ show, type, onClose, onTagsChange }: TagManagementModalProps) => {
const { t } = useTranslation()
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManageTags = hasPermission(workspacePermissionKeys, getTagManagePermissionKey(type))
const { data: tagList = [] } = useQuery(consoleQuery.tags.get.queryOptions({
input: {
@ -69,7 +70,7 @@ export const TagManagementModal = ({ show, type, onClose, onTagsChange }: TagMan
<div className="relative pb-2 text-xl/7.5 font-semibold text-text-primary">{t('tag.manageTags', { ns: 'common' })}</div>
<DialogCloseButton className="top-4 right-4" />
<div className="mt-3 flex flex-wrap gap-2">
<input aria-label={t('tag.addNew', { ns: 'common' }) || ''} className="w-25 shrink-0 appearance-none rounded-lg border border-dashed border-divider-regular bg-transparent px-2 py-1 text-sm/5 text-text-secondary caret-primary-600 outline-hidden placeholder:text-text-quaternary focus:border-solid" placeholder={t('tag.addNew', { ns: 'common' }) || ''} autoFocus value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && !e.nativeEvent.isComposing && createNewTag()} onBlur={createNewTag} />
<input aria-label={t('tag.addNew', { ns: 'common' }) || ''} className="w-25 shrink-0 appearance-none rounded-lg border border-dashed border-divider-regular bg-transparent px-2 py-1 text-sm/5 text-text-secondary caret-primary-600 outline-hidden placeholder:text-text-quaternary focus:border-solid" placeholder={t('tag.addNew', { ns: 'common' }) || ''} value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && !e.nativeEvent.isComposing && createNewTag()} onBlur={createNewTag} />
{tagList.map(tag => (<TagItemEditor key={tag.id} tag={tag} onTagsChange={onTagsChange} />))}
</div>
</DialogContent>

View File

@ -1,9 +1,10 @@
import type { TagType } from '@dify/contracts/api/console/tags/types.gen'
import type { TagComboboxItem } from './tag-combobox-item'
import { ComboboxEmpty, ComboboxInput, ComboboxInputGroup, ComboboxItem, ComboboxItemIndicator, ComboboxItemText, ComboboxList, ComboboxSeparator, useComboboxFilteredItems } from '@langgenius/dify-ui/combobox'
import { useAtomValue } from 'jotai'
import { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { hasPermission } from '@/utils/permission'
import { getTagManagePermissionKey } from '../utils'
import { isCreateTagOption } from './tag-combobox-item'
@ -26,7 +27,7 @@ export const TagSearchContent = ({
canBindOrUnbindTags = false,
}: TagSearchContentProps) => {
const { t } = useTranslation()
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManageTags = hasPermission(workspacePermissionKeys, getTagManagePermissionKey(type))
const filteredItems = useComboboxFilteredItems<TagComboboxItem>()
const realItemCount = filteredItems.filter(tag => !isCreateTagOption(tag)).length

View File

@ -6,9 +6,10 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Combobox, ComboboxContent, ComboboxTrigger } from '@langgenius/dify-ui/combobox'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { consoleQuery } from '@/service/client'
import { hasPermission } from '@/utils/permission'
import { useApplyTagBindingsMutation } from '../hooks/use-tag-mutations'
@ -71,7 +72,7 @@ export const TagSelector = ({
const [open, setOpen] = useState(false)
const [draftTags, setDraftTags] = useState<Tag[]>(value)
const [inputValue, setInputValue] = useState('')
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManageTags = hasPermission(workspacePermissionKeys, getTagManagePermissionKey(type))
const applyTagBindingsMutation = useApplyTagBindingsMutation()

View File

@ -5,6 +5,7 @@ import type {
import type { AppIconType } from '@/types/app'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import {
useCallback,
useRef,
@ -13,7 +14,7 @@ import {
import { useTranslation } from 'react-i18next'
import { useSetNeedRefreshAppList } from '@/app/components/apps/storage'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { useAppContext } from '@/context/app-context'
import { userProfileIdAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { DSLImportStatus } from '@/models/app'
import { useRouter } from '@/next/navigation'
@ -46,7 +47,8 @@ export const useImportDSL = () => {
const { push } = useRouter()
const invalidateAppList = useInvalidateAppList()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const { userProfile, workspacePermissionKeys } = useAppContext()
const currentUserId = useAtomValue(userProfileIdAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const isRbacEnabled = systemFeatures.rbac_enabled
const [versions, setVersions] = useState<{ importedVersion: string, systemVersion: string }>()
const importIdRef = useRef<string>('')
@ -98,8 +100,8 @@ export const useImportDSL = () => {
invalidateAppList()
await handleCheckPluginDependencies(app_id)
getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, {
currentUserId: userProfile?.id,
resourceMaintainer: userProfile?.id,
currentUserId,
resourceMaintainer: currentUserId,
workspacePermissionKeys,
isRbacEnabled,
})
@ -124,7 +126,7 @@ export const useImportDSL = () => {
finally {
setIsFetching(false)
}
}, [isFetching, t, handleCheckPluginDependencies, isRbacEnabled, push, setNeedRefresh, invalidateAppList, userProfile?.id, workspacePermissionKeys])
}, [isFetching, t, handleCheckPluginDependencies, isRbacEnabled, push, setNeedRefresh, invalidateAppList, currentUserId, workspacePermissionKeys])
const handleImportDSLConfirm = useCallback(async (
{
@ -154,8 +156,8 @@ export const useImportDSL = () => {
setNeedRefresh('1')
invalidateAppList()
getRedirection({ id: app_id, mode: app_mode, permission_keys }, push, {
currentUserId: userProfile?.id,
resourceMaintainer: userProfile?.id,
currentUserId,
resourceMaintainer: currentUserId,
workspacePermissionKeys,
isRbacEnabled,
})
@ -172,7 +174,7 @@ export const useImportDSL = () => {
finally {
setIsFetching(false)
}
}, [isFetching, t, handleCheckPluginDependencies, isRbacEnabled, setNeedRefresh, push, invalidateAppList, userProfile?.id, workspacePermissionKeys])
}, [isFetching, t, handleCheckPluginDependencies, isRbacEnabled, setNeedRefresh, push, invalidateAppList, currentUserId, workspacePermissionKeys])
return {
handleImportDSL,