(
hook: (props: P) => R,
options?: WorkflowHookTestOptions,
): WorkflowHookTestResult {
- const { initialStoreState, hooksStoreProps, historyStore: historyConfig, ...rest } = options ?? {}
+ const { initialStoreState, hooksStoreProps, historyStore: historyConfig, queryClient, ...rest } = options ?? {}
const stores = createStoresFromOptions({ initialStoreState, hooksStoreProps })
- const wrapper = createWorkflowWrapper(stores, historyConfig)
+ const wrapper = createWorkflowWrapper(stores, historyConfig, queryClient)
const renderResult = renderHook(hook, { wrapper, ...rest })
return { ...renderResult, ...stores }
@@ -244,10 +249,10 @@ export function renderWorkflowComponent(
ui: React.ReactElement,
options?: WorkflowComponentTestOptions,
): WorkflowComponentTestResult {
- const { initialStoreState, hooksStoreProps, historyStore: historyConfig, ...renderOptions } = options ?? {}
+ const { initialStoreState, hooksStoreProps, historyStore: historyConfig, queryClient, ...renderOptions } = options ?? {}
const stores = createStoresFromOptions({ initialStoreState, hooksStoreProps })
- const wrapper = createWorkflowWrapper(stores, historyConfig)
+ const wrapper = createWorkflowWrapper(stores, historyConfig, queryClient)
const renderResult = render(ui, { wrapper, ...renderOptions })
return { ...renderResult, ...stores }
diff --git a/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx
index 2b28662b45..802516d729 100644
--- a/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx
+++ b/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx
@@ -1,24 +1,20 @@
+import type { ReactElement } from 'react'
import type { TriggerWithProvider } from '../types'
-import { render, screen, waitFor } from '@testing-library/react'
+import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
+import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
import { CollectionType } from '@/app/components/tools/types'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetLanguage, useLocale } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import { useFeaturedTriggersRecommendations } from '@/service/use-plugins'
import { useAllTriggerPlugins, useInvalidateAllTriggerPlugins } from '@/service/use-triggers'
import { Theme } from '@/types/app'
-import { defaultSystemFeatures } from '@/types/feature'
import { useAvailableNodesMetaData } from '../../../workflow-app/hooks'
import useNodes from '../../store/workflow/use-nodes'
import { BlockEnum } from '../../types'
import AllStartBlocks from '../all-start-blocks'
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: vi.fn(),
-}))
-
vi.mock('@/context/i18n', () => ({
useGetLanguage: vi.fn(),
useLocale: vi.fn(),
@@ -57,7 +53,6 @@ vi.mock('@/utils/var', async (importOriginal) => {
}
})
-const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
const mockUseGetLanguage = vi.mocked(useGetLanguage)
const mockUseLocale = vi.mocked(useLocale)
const mockUseTheme = vi.mocked(useTheme)
@@ -106,15 +101,9 @@ const createTriggerProvider = (overrides: Partial = {}): Tr
...overrides,
})
-const createSystemFeatures = (enableMarketplace: boolean) => ({
- ...defaultSystemFeatures,
- enable_marketplace: enableMarketplace,
-})
-
-const createGlobalPublicStoreState = (enableMarketplace: boolean) => ({
- systemFeatures: createSystemFeatures(enableMarketplace),
- setSystemFeatures: vi.fn(),
-})
+let enableMarketplaceForRender = false
+const render = (ui: ReactElement) =>
+ renderWithSystemFeatures(ui, { systemFeatures: { enable_marketplace: enableMarketplaceForRender } })
const createMarketplacePluginsMock = (
overrides: Partial = {},
@@ -179,7 +168,7 @@ const createAvailableNodesMetaData = (): ReturnType {
beforeEach(() => {
vi.clearAllMocks()
- mockUseGlobalPublicStore.mockImplementation(selector => selector(createGlobalPublicStoreState(false)))
+ enableMarketplaceForRender = false
mockUseGetLanguage.mockReturnValue('en_US')
mockUseLocale.mockReturnValue('en_US')
mockUseTheme.mockReturnValue({ theme: Theme.light } as ReturnType)
@@ -226,7 +215,7 @@ describe('AllStartBlocks', () => {
})
it('should show marketplace footer when marketplace is enabled without filters', async () => {
- mockUseGlobalPublicStore.mockImplementation(selector => selector(createGlobalPublicStoreState(true)))
+ enableMarketplaceForRender = true
render(
{
describe('Filtered Empty State', () => {
it('should query marketplace and show the no-results state when filters have no matches', async () => {
const queryPluginsWithDebounced = vi.fn()
- mockUseGlobalPublicStore.mockImplementation(selector => selector(createGlobalPublicStoreState(true)))
+ enableMarketplaceForRender = true
mockUseMarketplacePlugins.mockReturnValue(createMarketplacePluginsMock({
queryPluginsWithDebounced,
}))
diff --git a/web/app/components/workflow/block-selector/__tests__/all-tools.spec.tsx b/web/app/components/workflow/block-selector/__tests__/all-tools.spec.tsx
index 64f012fae3..9c7caeaa06 100644
--- a/web/app/components/workflow/block-selector/__tests__/all-tools.spec.tsx
+++ b/web/app/components/workflow/block-selector/__tests__/all-tools.spec.tsx
@@ -1,16 +1,13 @@
-import { render, screen, waitFor } from '@testing-library/react'
+import type { ReactElement } from 'react'
+import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
+import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import AllTools from '../all-tools'
-import { createGlobalPublicStoreState, createToolProvider } from './factories'
-
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: vi.fn(),
-}))
+import { createToolProvider } from './factories'
vi.mock('@/context/i18n', () => ({
useGetLanguage: vi.fn(),
@@ -36,10 +33,12 @@ vi.mock('@/utils/var', async importOriginal => ({
}))
const mockUseMarketplacePlugins = vi.mocked(useMarketplacePlugins)
-const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
const mockUseGetLanguage = vi.mocked(useGetLanguage)
const mockUseTheme = vi.mocked(useTheme)
+const render = (ui: ReactElement, enableMarketplace = false) =>
+ renderWithSystemFeatures(ui, { systemFeatures: { enable_marketplace: enableMarketplace } })
+
const createMarketplacePluginsMock = () => ({
plugins: [],
total: 0,
@@ -57,7 +56,6 @@ const createMarketplacePluginsMock = () => ({
describe('AllTools', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockUseGlobalPublicStore.mockImplementation(selector => selector(createGlobalPublicStoreState(false)))
mockUseGetLanguage.mockReturnValue('en_US')
mockUseTheme.mockReturnValue({ theme: Theme.light } as ReturnType)
mockUseMarketplacePlugins.mockReturnValue(createMarketplacePluginsMock())
diff --git a/web/app/components/workflow/block-selector/__tests__/data-sources.spec.tsx b/web/app/components/workflow/block-selector/__tests__/data-sources.spec.tsx
index 64bcd514c6..0d242dbf78 100644
--- a/web/app/components/workflow/block-selector/__tests__/data-sources.spec.tsx
+++ b/web/app/components/workflow/block-selector/__tests__/data-sources.spec.tsx
@@ -1,21 +1,17 @@
+import type { ReactElement } from 'react'
import type { ToolWithProvider } from '../../types'
-import { render, screen, waitFor } from '@testing-library/react'
+import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
+import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { CollectionType } from '@/app/components/tools/types'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
-import { defaultSystemFeatures } from '@/types/feature'
import { BlockEnum } from '../../types'
import DataSources from '../data-sources'
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: vi.fn(),
-}))
-
vi.mock('@/context/i18n', () => ({
useGetLanguage: vi.fn(),
}))
@@ -28,11 +24,14 @@ vi.mock('@/app/components/plugins/marketplace/hooks', () => ({
useMarketplacePlugins: vi.fn(),
}))
-const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
const mockUseGetLanguage = vi.mocked(useGetLanguage)
const mockUseTheme = vi.mocked(useTheme)
const mockUseMarketplacePlugins = vi.mocked(useMarketplacePlugins)
+let enableMarketplaceForRender = false
+const render = (ui: ReactElement) =>
+ renderWithSystemFeatures(ui, { systemFeatures: { enable_marketplace: enableMarketplaceForRender } })
+
type UseMarketplacePluginsReturn = ReturnType
const createToolProvider = (overrides: Partial = {}): ToolWithProvider => ({
@@ -63,16 +62,6 @@ const createToolProvider = (overrides: Partial = {}): ToolWith
...overrides,
})
-const createSystemFeatures = (enableMarketplace: boolean) => ({
- ...defaultSystemFeatures,
- enable_marketplace: enableMarketplace,
-})
-
-const createGlobalPublicStoreState = (enableMarketplace: boolean) => ({
- systemFeatures: createSystemFeatures(enableMarketplace),
- setSystemFeatures: vi.fn(),
-})
-
const createMarketplacePluginsMock = (
overrides: Partial = {},
): UseMarketplacePluginsReturn => ({
@@ -93,7 +82,7 @@ const createMarketplacePluginsMock = (
describe('DataSources', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockUseGlobalPublicStore.mockImplementation(selector => selector(createGlobalPublicStoreState(false)))
+ enableMarketplaceForRender = false
mockUseGetLanguage.mockReturnValue('en_US')
mockUseTheme.mockReturnValue({ theme: Theme.light } as ReturnType)
mockUseMarketplacePlugins.mockReturnValue(createMarketplacePluginsMock())
@@ -162,7 +151,7 @@ describe('DataSources', () => {
describe('Marketplace Search', () => {
it('should query marketplace plugins for datasource search results', async () => {
const queryPluginsWithDebounced = vi.fn()
- mockUseGlobalPublicStore.mockImplementation(selector => selector(createGlobalPublicStoreState(true)))
+ enableMarketplaceForRender = true
mockUseMarketplacePlugins.mockReturnValue(createMarketplacePluginsMock({
queryPluginsWithDebounced,
}))
diff --git a/web/app/components/workflow/block-selector/__tests__/factories.ts b/web/app/components/workflow/block-selector/__tests__/factories.ts
index b7d82f7cb3..dfb7c36aea 100644
--- a/web/app/components/workflow/block-selector/__tests__/factories.ts
+++ b/web/app/components/workflow/block-selector/__tests__/factories.ts
@@ -3,7 +3,6 @@ import type { Plugin } from '@/app/components/plugins/types'
import type { Tool } from '@/app/components/tools/types'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { CollectionType } from '@/app/components/tools/types'
-import { defaultSystemFeatures } from '@/types/feature'
export const createTool = (
name: string,
@@ -91,11 +90,3 @@ export const createPlugin = (overrides: Partial = {}): Plugin => ({
from: 'github',
...overrides,
})
-
-export const createGlobalPublicStoreState = (enableMarketplace: boolean) => ({
- systemFeatures: {
- ...defaultSystemFeatures,
- enable_marketplace: enableMarketplace,
- },
- setSystemFeatures: vi.fn(),
-})
diff --git a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx
index 735a831c10..d426b43cfd 100644
--- a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx
+++ b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx
@@ -23,12 +23,6 @@ vi.mock('@/service/use-tools', () => ({
useInvalidateAllBuiltInTools: () => vi.fn(),
}))
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => unknown) => selector({
- systemFeatures: { enable_marketplace: false },
- }),
-}))
-
const createBlock = (type: BlockEnum, title: string): NodeDefault => ({
metaData: {
type,
diff --git a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx
index 1deb6ce84c..2cb0d3e98f 100644
--- a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx
+++ b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx
@@ -14,12 +14,6 @@ vi.mock('reactflow', () => ({
}),
}))
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => unknown) => selector({
- systemFeatures: { enable_marketplace: false },
- }),
-}))
-
vi.mock('@/service/use-plugins', () => ({
useFeaturedToolsRecommendations: () => ({
plugins: [],
diff --git a/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx
index 3ee21f9999..3002cafa0a 100644
--- a/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx
+++ b/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx
@@ -1,9 +1,13 @@
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
+import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import Tabs from '../tabs'
import { TabsEnum } from '../types'
+const render = (ui: React.ReactElement) =>
+ renderWithSystemFeatures(ui, { systemFeatures: { enable_marketplace: true } })
+
const {
mockSetState,
mockInvalidateBuiltInTools,
@@ -34,12 +38,6 @@ vi.mock('@/app/components/base/tooltip', () => ({
),
}))
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => unknown) => selector({
- systemFeatures: { enable_marketplace: true },
- }),
-}))
-
vi.mock('@/service/use-plugins', () => ({
useFeaturedToolsRecommendations: () => ({
plugins: [],
diff --git a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx
index 1a2f7a4c93..9c55d174fd 100644
--- a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx
+++ b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx
@@ -2,14 +2,13 @@ import type { ToolWithProvider } from '../../types'
import type { ToolValue } from '../types'
import type { Plugin } from '@/app/components/plugins/types'
import type { Tool } from '@/app/components/tools/types'
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
+import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { useTags } from '@/app/components/plugins/hooks'
import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { CollectionType } from '@/app/components/tools/types'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import { createCustomCollection } from '@/service/tools'
@@ -25,11 +24,9 @@ import {
useInvalidateAllWorkflowTools,
} from '@/service/use-tools'
import { Theme } from '@/types/app'
-import { defaultSystemFeatures } from '@/types/feature'
import ToolPicker from '../tool-picker'
const mockNotify = vi.fn()
-const mockSetSystemFeatures = vi.fn()
const mockInvalidateBuiltInTools = vi.fn()
const mockInvalidateCustomTools = vi.fn()
const mockInvalidateWorkflowTools = vi.fn()
@@ -39,7 +36,6 @@ const mockInstallPackageFromMarketPlace = vi.fn()
const mockCheckInstalled = vi.fn()
const mockRefreshPluginList = vi.fn()
-const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
const mockUseGetLanguage = vi.mocked(useGetLanguage)
const mockUseTheme = vi.mocked(useTheme)
const mockUseTags = vi.mocked(useTags)
@@ -54,10 +50,6 @@ const mockUseInvalidateAllWorkflowTools = vi.mocked(useInvalidateAllWorkflowTool
const mockUseInvalidateAllMCPTools = vi.mocked(useInvalidateAllMCPTools)
const mockUseFeaturedToolsRecommendations = vi.mocked(useFeaturedToolsRecommendations)
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: vi.fn(),
-}))
-
vi.mock('@/context/i18n', () => ({
useGetLanguage: vi.fn(),
}))
@@ -313,27 +305,18 @@ const mcpTools = [
]
const renderToolPicker = (props: Partial> = {}) => {
- const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- retry: false,
- },
- },
- })
-
- return render(
-
- open-picker}
- isShow={false}
- onShowChange={vi.fn()}
- onSelect={vi.fn()}
- onSelectMultiple={vi.fn()}
- selectedTools={[createToolValue()]}
- {...props}
- />
- ,
+ return renderWithSystemFeatures(
+ open-picker}
+ isShow={false}
+ onShowChange={vi.fn()}
+ onSelect={vi.fn()}
+ onSelectMultiple={vi.fn()}
+ selectedTools={[createToolValue()]}
+ {...props}
+ />,
+ { systemFeatures: { enable_marketplace: true } },
)
}
@@ -341,13 +324,6 @@ describe('ToolPicker', () => {
beforeEach(() => {
vi.clearAllMocks()
- mockUseGlobalPublicStore.mockImplementation(selector => selector({
- systemFeatures: {
- ...defaultSystemFeatures,
- enable_marketplace: true,
- },
- setSystemFeatures: mockSetSystemFeatures,
- }))
mockUseGetLanguage.mockReturnValue('en_US')
mockUseTheme.mockReturnValue({ theme: Theme.light } as ReturnType)
mockUseTags.mockReturnValue({
diff --git a/web/app/components/workflow/block-selector/all-start-blocks.tsx b/web/app/components/workflow/block-selector/all-start-blocks.tsx
index 72a98bed00..b7f9ee7bbb 100644
--- a/web/app/components/workflow/block-selector/all-start-blocks.tsx
+++ b/web/app/components/workflow/block-selector/all-start-blocks.tsx
@@ -8,6 +8,7 @@ import type { TriggerDefaultValue, TriggerWithProvider } from './types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowRightUpLine } from '@remixicon/react'
+import { useSuspenseQuery } from '@tanstack/react-query'
import {
useCallback,
useEffect,
@@ -18,8 +19,8 @@ import {
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import Link from '@/next/link'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useFeaturedTriggersRecommendations } from '@/service/use-plugins'
import { useAllTriggerPlugins, useInvalidateAllTriggerPlugins } from '@/service/use-triggers'
import { getMarketplaceUrl } from '@/utils/var'
@@ -54,7 +55,10 @@ const AllStartBlocks = ({
const { t } = useTranslation()
const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false)
const [hasPluginContent, setHasPluginContent] = useState(false)
- const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
+ const { data: enable_marketplace } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_marketplace,
+ })
const pluginRef = useRef(null)
const wrapElemRef = useRef(null)
diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx
index 2483b92f90..72389ec376 100644
--- a/web/app/components/workflow/block-selector/all-tools.tsx
+++ b/web/app/components/workflow/block-selector/all-tools.tsx
@@ -14,14 +14,15 @@ import type { OnSelectBlock } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowRightUpLine } from '@remixicon/react'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetLanguage } from '@/context/i18n'
import Link from '@/next/link'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { getMarketplaceUrl } from '@/utils/var'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
import { PluginCategoryEnum } from '../../plugins/types'
@@ -167,7 +168,10 @@ const AllTools = ({
plugins: notInstalledPlugins = [],
} = useMarketplacePlugins()
- const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
+ const { data: enable_marketplace } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_marketplace,
+ })
useEffect(() => {
if (!enable_marketplace)
diff --git a/web/app/components/workflow/block-selector/data-sources.tsx b/web/app/components/workflow/block-selector/data-sources.tsx
index 447442818a..1ee2bf9b9b 100644
--- a/web/app/components/workflow/block-selector/data-sources.tsx
+++ b/web/app/components/workflow/block-selector/data-sources.tsx
@@ -5,6 +5,7 @@ import type {
import type { DataSourceDefaultValue, ToolDefaultValue } from './types'
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { cn } from '@langgenius/dify-ui/cn'
+import { useSuspenseQuery } from '@tanstack/react-query'
import {
useCallback,
useEffect,
@@ -12,8 +13,8 @@ import {
useRef,
} from 'react'
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetLanguage } from '@/context/i18n'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
import { PluginCategoryEnum } from '../../plugins/types'
import { BlockEnum } from '../types'
@@ -76,7 +77,10 @@ const DataSources = ({
onSelect(BlockEnum.DataSource, toolDefaultValue && defaultValue)
}, [onSelect])
- const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
+ const { data: enable_marketplace } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_marketplace,
+ })
const {
queryPluginsWithDebounced: fetchPlugins,
diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx
index 5605908b71..48af942df7 100644
--- a/web/app/components/workflow/block-selector/tabs.tsx
+++ b/web/app/components/workflow/block-selector/tabs.tsx
@@ -6,10 +6,11 @@ import type {
ToolWithProvider,
} from '../types'
import { cn } from '@langgenius/dify-ui/cn'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { memo, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools'
import { basePath } from '@/utils/var'
@@ -180,7 +181,10 @@ const Tabs: FC = ({
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const invalidateBuiltInTools = useInvalidateAllBuiltInTools()
- const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
+ const { data: enable_marketplace } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_marketplace,
+ })
const workflowStore = useWorkflowStore()
const inRAGPipeline = dataSources.length > 0
const {
diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx
index 7ba93798e2..e7e948a54d 100644
--- a/web/app/components/workflow/block-selector/tool-picker.tsx
+++ b/web/app/components/workflow/block-selector/tool-picker.tsx
@@ -9,6 +9,7 @@ import type { CustomCollectionBackend } from '@/app/components/tools/types'
import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useMemo, useState } from 'react'
@@ -21,7 +22,7 @@ import {
import SearchBox from '@/app/components/plugins/marketplace/search-box'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import AllTools from '@/app/components/workflow/block-selector/all-tools'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import {
createCustomCollection,
} from '@/service/tools'
@@ -70,7 +71,10 @@ const ToolPicker: FC = ({
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState([])
- const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
+ const { data: enable_marketplace } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_marketplace,
+ })
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const invalidateCustomTools = useInvalidateAllCustomTools()
diff --git a/web/app/components/workflow/collaboration/hooks/__tests__/use-collaboration.spec.ts b/web/app/components/workflow/collaboration/hooks/__tests__/use-collaboration.spec.ts
index 0f8a9e2c9a..7c063ab2cf 100644
--- a/web/app/components/workflow/collaboration/hooks/__tests__/use-collaboration.spec.ts
+++ b/web/app/components/workflow/collaboration/hooks/__tests__/use-collaboration.spec.ts
@@ -1,5 +1,6 @@
import type { CursorPosition, NodePanelPresenceMap, OnlineUser } from '../../types/collaboration'
-import { renderHook, waitFor } from '@testing-library/react'
+import { waitFor } from '@testing-library/react'
+import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { useCollaboration } from '../use-collaboration'
type HookReactFlowStore = NonNullable[1]>
@@ -29,11 +30,6 @@ const mockStartTracking = vi.hoisted(() => vi.fn())
const mockStopTracking = vi.hoisted(() => vi.fn())
const cursorServiceInstances: Array<{ startTracking: typeof mockStartTracking, stopTracking: typeof mockStopTracking }> = []
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_collaboration_mode: boolean } }) => boolean) =>
- selector({ systemFeatures: { enable_collaboration_mode: isCollaborationEnabled } }),
-}))
-
vi.mock('../../core/collaboration-manager', () => ({
collaborationManager: {
connect: (...args: unknown[]) => mockConnect(...args),
@@ -92,7 +88,9 @@ describe('useCollaboration', () => {
const reactFlowStore: HookReactFlowStore = {
getState: vi.fn(),
}
- const { result, unmount } = renderHook(() => useCollaboration('app-1', reactFlowStore))
+ const { result, unmount } = renderHookWithSystemFeatures(() => useCollaboration('app-1', reactFlowStore), {
+ systemFeatures: { enable_collaboration_mode: isCollaborationEnabled },
+ })
await waitFor(() => {
expect(mockConnect).toHaveBeenCalledWith('app-1', reactFlowStore)
@@ -138,7 +136,9 @@ describe('useCollaboration', () => {
it('does not connect or start cursor tracking when collaboration is disabled', async () => {
isCollaborationEnabled = false
- const { result } = renderHook(() => useCollaboration('app-1'))
+ const { result } = renderHookWithSystemFeatures(() => useCollaboration('app-1'), {
+ systemFeatures: { enable_collaboration_mode: isCollaborationEnabled },
+ })
await waitFor(() => {
expect(mockConnect).not.toHaveBeenCalled()
diff --git a/web/app/components/workflow/collaboration/hooks/use-collaboration.ts b/web/app/components/workflow/collaboration/hooks/use-collaboration.ts
index b24d9faea8..0030ffb77c 100644
--- a/web/app/components/workflow/collaboration/hooks/use-collaboration.ts
+++ b/web/app/components/workflow/collaboration/hooks/use-collaboration.ts
@@ -5,8 +5,9 @@ import type {
NodePanelPresenceMap,
OnlineUser,
} from '../types/collaboration'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { collaborationManager } from '../core/collaboration-manager'
import { CursorService } from '../services/cursor-service'
@@ -33,7 +34,10 @@ export function useCollaboration(appId: string, reactFlowStore?: ReactFlowStore)
const cursorServiceRef = useRef(null)
const lastDisconnectReasonRef = useRef(null)
- const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
+ const { data: isCollaborationEnabled } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_collaboration_mode,
+ })
useEffect(() => {
if (!appId || !isCollaborationEnabled) {
diff --git a/web/app/components/workflow/hooks/__tests__/use-leader-restore.spec.ts b/web/app/components/workflow/hooks/__tests__/use-leader-restore.spec.ts
index a050994f4c..b0fb43f768 100644
--- a/web/app/components/workflow/hooks/__tests__/use-leader-restore.spec.ts
+++ b/web/app/components/workflow/hooks/__tests__/use-leader-restore.spec.ts
@@ -2,6 +2,7 @@ import type { RestoreIntentData, RestoreRequestData } from '../../collaboration/
import type { SyncDraftCallback } from '../../hooks-store/store'
import type { Edge, Node } from '../../types'
import { act, renderHook } from '@testing-library/react'
+import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { ChatVarType } from '../../panel/chat-variable-panel/type'
import { useLeaderRestore, useLeaderRestoreListener } from '../use-leader-restore'
@@ -72,11 +73,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
},
}))
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_collaboration_mode: boolean } }) => boolean) =>
- selector({ systemFeatures: { enable_collaboration_mode: isCollaborationEnabled } }),
-}))
-
vi.mock('../../store', () => ({
useWorkflowStore: () => ({
getState: () => ({
@@ -155,7 +151,9 @@ describe('useLeaderRestore', () => {
const onSuccess = vi.fn()
const onSettled = vi.fn()
- const { result } = renderHook(() => useLeaderRestore())
+ const { result } = renderHookWithSystemFeatures(() => useLeaderRestore(), {
+ systemFeatures: { enable_collaboration_mode: isCollaborationEnabled },
+ })
await act(async () => {
result.current.requestRestore(restoreData, { onSuccess, onSettled })
@@ -186,7 +184,9 @@ describe('useLeaderRestore', () => {
const onError = vi.fn()
const onSettled = vi.fn()
- const { result } = renderHook(() => useLeaderRestore())
+ const { result } = renderHookWithSystemFeatures(() => useLeaderRestore(), {
+ systemFeatures: { enable_collaboration_mode: isCollaborationEnabled },
+ })
act(() => {
result.current.requestRestore(restoreData, { onSuccess, onError, onSettled })
diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts
index 5745603a5e..b2edfa5234 100644
--- a/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts
+++ b/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts
@@ -1,5 +1,6 @@
import type { WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment'
import { act, waitFor } from '@testing-library/react'
+import { createTestQueryClient, seedSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { renderWorkflowHook } from '../../__tests__/workflow-test-env'
import { ControlMode } from '../../types'
import { useWorkflowComment } from '../use-workflow-comment'
@@ -51,14 +52,6 @@ vi.mock('@/context/app-context', () => ({
}),
}))
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_collaboration_mode: boolean } }) => unknown) => selector({
- systemFeatures: {
- enable_collaboration_mode: globalFeatureState.enableCollaboration,
- },
- }),
-}))
-
vi.mock('@/service/workflow-comment', () => ({
createWorkflowComment: (...args: unknown[]) => mockCreateWorkflowComment(...args),
createWorkflowCommentReply: (...args: unknown[]) => mockCreateWorkflowCommentReply(...args),
@@ -120,6 +113,14 @@ const baseCommentDetail = (): WorkflowCommentDetail => ({
replies: [],
})
+const createSeededQueryClient = () => {
+ const queryClient = createTestQueryClient()
+ seedSystemFeatures(queryClient, {
+ enable_collaboration_mode: globalFeatureState.enableCollaboration,
+ })
+ return queryClient
+}
+
describe('useWorkflowComment', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -139,7 +140,9 @@ describe('useWorkflowComment', () => {
const comment = baseComment()
mockFetchWorkflowComments.mockResolvedValue([comment])
- const { store } = renderWorkflowHook(() => useWorkflowComment())
+ const { store } = renderWorkflowHook(() => useWorkflowComment(), {
+ queryClient: createSeededQueryClient(),
+ })
await waitFor(() => {
expect(mockFetchWorkflowComments).toHaveBeenCalledWith('app-1')
@@ -152,7 +155,9 @@ describe('useWorkflowComment', () => {
it('does not load comment list when collaboration is disabled', async () => {
globalFeatureState.enableCollaboration = false
- renderWorkflowHook(() => useWorkflowComment())
+ renderWorkflowHook(() => useWorkflowComment(), {
+ queryClient: createSeededQueryClient(),
+ })
await Promise.resolve()
@@ -161,6 +166,7 @@ describe('useWorkflowComment', () => {
it('creates a comment, updates local cache, and emits collaboration sync', async () => {
const { result, store } = renderWorkflowHook(() => useWorkflowComment(), {
+ queryClient: createSeededQueryClient(),
initialStoreState: {
comments: [],
pendingComment: { pageX: 100, pageY: 200, elementX: 10, elementY: 20 },
@@ -214,6 +220,7 @@ describe('useWorkflowComment', () => {
mockUpdateWorkflowComment.mockRejectedValue(new Error('update failed'))
const { result, store } = renderWorkflowHook(() => useWorkflowComment(), {
+ queryClient: createSeededQueryClient(),
initialStoreState: {
comments: [comment],
activeCommentId: comment.id,
@@ -254,6 +261,7 @@ describe('useWorkflowComment', () => {
mockFetchWorkflowComment.mockResolvedValue({ data: detail })
const { unmount } = renderWorkflowHook(() => useWorkflowComment(), {
+ queryClient: createSeededQueryClient(),
initialStoreState: {
activeCommentId: comment.id,
},
@@ -295,6 +303,7 @@ describe('useWorkflowComment', () => {
})
const { result, store } = renderWorkflowHook(() => useWorkflowComment(), {
+ queryClient: createSeededQueryClient(),
initialStoreState: {
comments: [commentA, commentB],
commentDetailCache: {
@@ -363,6 +372,7 @@ describe('useWorkflowComment', () => {
})
const { result, store } = renderWorkflowHook(() => useWorkflowComment(), {
+ queryClient: createSeededQueryClient(),
initialStoreState: {
comments: [commentA, commentB],
activeCommentId: commentA.id,
diff --git a/web/app/components/workflow/hooks/use-leader-restore.ts b/web/app/components/workflow/hooks/use-leader-restore.ts
index da0036a4ea..9083767f40 100644
--- a/web/app/components/workflow/hooks/use-leader-restore.ts
+++ b/web/app/components/workflow/hooks/use-leader-restore.ts
@@ -1,12 +1,13 @@
import type { RestoreCompleteData, RestoreIntentData, RestoreRequestData } from '../collaboration/types/collaboration'
import type { SyncCallback } from './use-nodes-sync-draft'
import { toast } from '@langgenius/dify-ui/toast'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow } from 'reactflow'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { collaborationManager } from '../collaboration/core/collaboration-manager'
import { useWorkflowStore } from '../store'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
@@ -115,7 +116,10 @@ export const useLeaderRestore = () => {
versionId: string
callbacks: RestoreCallbacks | null
} | null>(null)
- const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
+ const { data: isCollaborationEnabled } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_collaboration_mode,
+ })
const requestRestore = useCallback((data: RestoreRequestData, callbacks?: RestoreCallbacks) => {
if (!isCollaborationEnabled || !collaborationManager.isConnected() || collaborationManager.getIsLeader()) {
diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts
index a29c88e9cb..f0e23586eb 100644
--- a/web/app/components/workflow/hooks/use-nodes-interactions.ts
+++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts
@@ -14,6 +14,7 @@ import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
import type { Edge, Node, OnNodeAdd } from '../types'
import type { RAGPipelineVariables } from '@/models/pipeline'
import { toast } from '@langgenius/dify-ui/toast'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { produce } from 'immer'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -22,7 +23,7 @@ import {
getOutgoers,
useReactFlow,
} from 'reactflow'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { collaborationManager } from '../collaboration/core/collaboration-manager'
import {
CUSTOM_EDGE,
@@ -138,7 +139,10 @@ const getUniquePastedNodeTitle = (
export const useNodesInteractions = () => {
const { t } = useTranslation()
- const appDslVersion = useGlobalPublicStore(s => s.systemFeatures.app_dsl_version)
+ const { data: appDslVersion } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.app_dsl_version,
+ })
const collaborativeWorkflow = useCollaborativeWorkflow()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
diff --git a/web/app/components/workflow/hooks/use-panel-interactions.ts b/web/app/components/workflow/hooks/use-panel-interactions.ts
index 37120068d4..3b7a2158a1 100644
--- a/web/app/components/workflow/hooks/use-panel-interactions.ts
+++ b/web/app/components/workflow/hooks/use-panel-interactions.ts
@@ -1,12 +1,16 @@
import type { MouseEvent } from 'react'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback } from 'react'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useWorkflowStore } from '../store'
import { readWorkflowClipboard } from '../utils'
export const usePanelInteractions = () => {
const workflowStore = useWorkflowStore()
- const appDslVersion = useGlobalPublicStore(s => s.systemFeatures.app_dsl_version)
+ const { data: appDslVersion } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.app_dsl_version,
+ })
const handlePaneContextMenu = useCallback((e: MouseEvent) => {
e.preventDefault()
diff --git a/web/app/components/workflow/hooks/use-workflow-comment.ts b/web/app/components/workflow/hooks/use-workflow-comment.ts
index 5b3f2e17b0..cdd14ceef1 100644
--- a/web/app/components/workflow/hooks/use-workflow-comment.ts
+++ b/web/app/components/workflow/hooks/use-workflow-comment.ts
@@ -1,10 +1,11 @@
import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useRef } from 'react'
import { useReactFlow } from 'reactflow'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
import { useAppContext } from '@/context/app-context'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useParams } from '@/next/navigation'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { createWorkflowComment, createWorkflowCommentReply, deleteWorkflowComment, deleteWorkflowCommentReply, fetchWorkflowComment, fetchWorkflowComments, resolveWorkflowComment, updateWorkflowComment, updateWorkflowCommentReply } from '@/service/workflow-comment'
import { useStore } from '../store'
import { ControlMode } from '../types'
@@ -50,7 +51,10 @@ export const useWorkflowComment = () => {
appId ? state.mentionableUsersCache[appId] ?? EMPTY_USERS : EMPTY_USERS
))
const { userProfile } = useAppContext()
- const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
+ const { data: isCollaborationEnabled } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_collaboration_mode,
+ })
const commentDetailCacheRef = useRef>(commentDetailCache)
const activeCommentIdRef = useRef(null)
diff --git a/web/app/components/workflow/hooks/use-workflow-panel-interactions.ts b/web/app/components/workflow/hooks/use-workflow-panel-interactions.ts
index 62628c533e..e7ca512d34 100644
--- a/web/app/components/workflow/hooks/use-workflow-panel-interactions.ts
+++ b/web/app/components/workflow/hooks/use-workflow-panel-interactions.ts
@@ -1,5 +1,6 @@
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback } from 'react'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useStore, useWorkflowStore } from '../store'
import { ControlMode } from '../types'
import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync'
@@ -30,7 +31,10 @@ export const useWorkflowMoveMode = () => {
const setControlMode = useStore(s => s.setControlMode)
const { getNodesReadOnly } = useNodesReadOnly()
const { handleSelectionCancel } = useSelectionInteractions()
- const isCommentModeAvailable = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
+ const { data: isCommentModeAvailable } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_collaboration_mode,
+ })
const handleModePointer = useCallback(() => {
if (getNodesReadOnly())
diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx
index 489c69412a..8e7c1609d1 100644
--- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx
+++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx
@@ -5,6 +5,7 @@ import type { StrategyPluginDetail } from '@/app/components/plugins/types'
import type { ListProps, ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowDownSLine, RiErrorWarningFill } from '@remixicon/react'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
@@ -16,8 +17,8 @@ import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hook
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { CollectionType } from '@/app/components/tools/types'
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import Link from '@/next/link'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useStrategyProviders } from '@/service/use-strategy'
import Tools from '../../../block-selector/tools'
import ViewTypeSelect, { ViewType } from '../../../block-selector/view-type-select'
@@ -95,7 +96,10 @@ type AgentStrategySelectorProps = {
}
export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {
- const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
+ const { data: enable_marketplace } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_marketplace,
+ })
const { value, onChange } = props
const [open, setOpen] = useState(false)
diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx
index f1da87bed2..7e13b8ef34 100644
--- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx
+++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx
@@ -2,6 +2,7 @@ import type { ToolParameter } from '@/app/components/tools/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
+import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
import { CollectionType } from '@/app/components/tools/types'
import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
import { createTool, createToolProvider } from '@/app/components/workflow/block-selector/__tests__/factories'
@@ -15,12 +16,6 @@ vi.mock('reactflow', () => ({
}),
}))
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => unknown) => selector({
- systemFeatures: { enable_marketplace: false },
- }),
-}))
-
vi.mock('@/service/use-plugins', () => ({
useFeaturedToolsRecommendations: () => ({
plugins: [],
@@ -65,6 +60,20 @@ const createToolParameter = (overrides: Partial = {}): ToolParame
...overrides,
})
+const renderImportFromTool = (ui: React.ReactElement) => {
+ const { wrapper: SystemFeaturesWrapper } = createSystemFeaturesWrapper({
+ systemFeatures: { enable_marketplace: false },
+ })
+ return renderWorkflowComponent(
+ {ui},
+ {
+ hooksStoreProps: {
+ availableNodesMetaData: { nodes: [] },
+ },
+ },
+ )
+}
+
describe('parameter-extractor/extract-parameter/import-from-tool', () => {
beforeEach(() => {
vi.clearAllMocks()
@@ -97,14 +106,7 @@ describe('parameter-extractor/extract-parameter/import-from-tool', () => {
provider.tools[0]!.parameters = builtInParameters
mockToolCollections.builtIn = [provider]
- renderWorkflowComponent(
- ,
- {
- hooksStoreProps: {
- availableNodesMetaData: { nodes: [] },
- },
- },
- )
+ renderImportFromTool()
await user.click(screen.getByText('workflow.nodes.parameterExtractor.importFromTool'))
await user.click(await screen.findByText('Provider One'))
@@ -136,14 +138,7 @@ describe('parameter-extractor/extract-parameter/import-from-tool', () => {
provider.tools[0]!.parameters = workflowParameters
mockToolCollections.workflow = [provider]
- renderWorkflowComponent(
- ,
- {
- hooksStoreProps: {
- availableNodesMetaData: { nodes: [] },
- },
- },
- )
+ renderImportFromTool()
await user.click(screen.getByText('workflow.nodes.parameterExtractor.importFromTool'))
await user.click(await screen.findByText('Workflow Tool'))
diff --git a/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx b/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx
index 9d38d112e9..990153d308 100644
--- a/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx
+++ b/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx
@@ -1,10 +1,11 @@
import type { ToolVarInputs } from '../../types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { App } from '@/types/app'
-import { render, screen } from '@testing-library/react'
+import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
+import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import {
ConfigurationMethodEnum,
FormTypeEnum,
@@ -211,14 +212,6 @@ vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference
),
}))
-vi.mock('@/context/global-public-context', () => ({
- useSystemFeaturesQuery: () => ({
- data: {
- trial_models: [],
- },
- }),
-}))
-
vi.mock('@/app/components/header/account-setting/model-provider-page/provider-added-card/use-trial-credits', () => ({
useTrialCredits: () => ({
isExhausted: false,
@@ -341,7 +334,7 @@ const renderInputVarList = (ui: React.ReactElement) => {
}] as ReturnType['modelProviders'],
})
- return render(
+ return renderWithSystemFeatures(
{ui}
,
diff --git a/web/app/components/workflow/operator/__tests__/zoom-in-out.spec.tsx b/web/app/components/workflow/operator/__tests__/zoom-in-out.spec.tsx
index 8b0f12c7d6..a58bdef727 100644
--- a/web/app/components/workflow/operator/__tests__/zoom-in-out.spec.tsx
+++ b/web/app/components/workflow/operator/__tests__/zoom-in-out.spec.tsx
@@ -1,4 +1,5 @@
-import { fireEvent, render, screen, within } from '@testing-library/react'
+import { fireEvent, screen, within } from '@testing-library/react'
+import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import ZoomInOut from '../zoom-in-out'
const {
@@ -46,18 +47,15 @@ vi.mock('@/app/components/workflow/hooks', () => ({
}),
}))
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_collaboration_mode: boolean } }) => unknown) => selector({
- systemFeatures: {
- enable_collaboration_mode: collaborationEnabled,
- },
- }),
-}))
-
vi.mock('../tip-popup', () => ({
default: ({ children }: { children: React.ReactNode }) => <>{children}>,
}))
+const renderZoomInOut = (ui: React.ReactElement = ) =>
+ renderWithSystemFeatures(ui, {
+ systemFeatures: { enable_collaboration_mode: collaborationEnabled },
+ })
+
const getZoomControls = () => {
const label = Array.from(document.querySelectorAll('button')).find((element) => {
return /^\d+%$/.test(element.textContent ?? '') && element.className.includes('w-[34px]')
@@ -89,7 +87,7 @@ describe('workflow zoom controls', () => {
})
it('zooms out and zooms in when the viewport is within the supported range', () => {
- render()
+ renderZoomInOut()
const { zoomOutTrigger, zoomInTrigger } = getZoomControls()
@@ -101,7 +99,7 @@ describe('workflow zoom controls', () => {
})
it('zooms to a preset value and syncs the draft', () => {
- render()
+ renderZoomInOut()
const menu = openZoomMenu()
fireEvent.click(menu.getByText('50%'))
@@ -114,7 +112,7 @@ describe('workflow zoom controls', () => {
['100%', 1],
['200%', 2],
])('zooms to %s and syncs the draft', (label, zoom) => {
- render()
+ renderZoomInOut()
const menu = openZoomMenu()
fireEvent.click(menu.getByText(label))
@@ -124,7 +122,7 @@ describe('workflow zoom controls', () => {
})
it('toggles collaboration options without syncing the draft', () => {
- render(
+ renderZoomInOut(
{
})
it('keeps the show-user-comments action disabled in comment mode', () => {
- render(
+ renderZoomInOut(
{
it('does not open the menu when the workflow is read only', () => {
workflowReadOnly = true
- render()
+ renderZoomInOut()
fireEvent.click(getZoomControls().label)
@@ -171,7 +169,7 @@ describe('workflow zoom controls', () => {
it('blocks inline zooming out at the minimum viewport scale', () => {
mockViewport.zoom = 0.25
- render()
+ renderZoomInOut()
fireEvent.click(getZoomControls().zoomOutTrigger)
expect(mockZoomOut).not.toHaveBeenCalled()
@@ -179,7 +177,7 @@ describe('workflow zoom controls', () => {
it('blocks inline zooming in at the maximum viewport scale', () => {
mockViewport.zoom = 2
- render()
+ renderZoomInOut()
fireEvent.click(getZoomControls().zoomInTrigger)
expect(mockZoomIn).not.toHaveBeenCalled()
@@ -187,7 +185,7 @@ describe('workflow zoom controls', () => {
it('renders collaboration menu entries only when collaboration is enabled', () => {
collaborationEnabled = false
- render()
+ renderZoomInOut()
const menu = openZoomMenu()
expect(menu.getByText('workflow.operator.showMiniMap')).toBeInTheDocument()
diff --git a/web/app/components/workflow/operator/zoom-in-out.tsx b/web/app/components/workflow/operator/zoom-in-out.tsx
index 78b07af347..d7f172d2d1 100644
--- a/web/app/components/workflow/operator/zoom-in-out.tsx
+++ b/web/app/components/workflow/operator/zoom-in-out.tsx
@@ -7,6 +7,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
+import { useSuspenseQuery } from '@tanstack/react-query'
import {
Fragment,
memo,
@@ -17,7 +18,7 @@ import {
useReactFlow,
useViewport,
} from 'reactflow'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import {
useNodesSyncDraft,
useWorkflowReadOnly,
@@ -70,7 +71,10 @@ const ZoomInOut: FC = ({
workflowReadOnly,
getWorkflowReadOnly,
} = useWorkflowReadOnly()
- const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
+ const { data: isCollaborationEnabled } = useSuspenseQuery({
+ ...systemFeaturesQueryOptions(),
+ select: s => s.enable_collaboration_mode,
+ })
const zoomOptions = [
[
diff --git a/web/app/forgot-password/page.tsx b/web/app/forgot-password/page.tsx
index b2ac8d2640..3f8f17c98e 100644
--- a/web/app/forgot-password/page.tsx
+++ b/web/app/forgot-password/page.tsx
@@ -1,10 +1,11 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
+import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSearchParams } from '@/next/navigation'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import Header from '../signin/_header'
import ForgotPasswordForm from './ForgotPasswordForm'
@@ -12,7 +13,7 @@ const ForgotPassword = () => {
useDocumentTitle('')
const searchParams = useSearchParams()
const token = searchParams.get('token')
- const { systemFeatures } = useGlobalPublicStore()
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
return (
diff --git a/web/app/install/installForm.spec.tsx b/web/app/install/installForm.spec.tsx
index 1286d02343..a9b8cc02be 100644
--- a/web/app/install/installForm.spec.tsx
+++ b/web/app/install/installForm.spec.tsx
@@ -1,9 +1,13 @@
+import type { ReactElement } from 'react'
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, screen, waitFor } from '@testing-library/react'
+import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
import { encryptPassword } from '@/utils/encryption'
import InstallForm from './installForm'
+const render = (ui: ReactElement) => renderWithSystemFeatures(ui)
+
const mockPush = vi.fn()
const mockReplace = vi.fn()
@@ -18,14 +22,6 @@ vi.mock('@/service/common', () => ({
login: vi.fn(),
}))
-vi.mock('@/context/global-public-context', async (importOriginal) => {
- const actual = await importOriginal
()
- return {
- ...actual,
- useIsSystemFeaturesPending: () => false,
- }
-})
-
const mockFetchSetupStatus = vi.mocked(fetchSetupStatus)
const mockFetchInitValidateStatus = vi.mocked(fetchInitValidateStatus)
const mockSetup = vi.mocked(setup)
diff --git a/web/app/install/page.tsx b/web/app/install/page.tsx
index b5d92a3298..8d1f71f817 100644
--- a/web/app/install/page.tsx
+++ b/web/app/install/page.tsx
@@ -1,12 +1,13 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
+import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import Header from '../signin/_header'
import InstallForm from './installForm'
const Install = () => {
- const { systemFeatures } = useGlobalPublicStore()
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
return (
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
index be0c854f02..009f2ca584 100644
--- a/web/app/layout.tsx
+++ b/web/app/layout.tsx
@@ -4,7 +4,6 @@ import { TooltipProvider } from '@langgenius/dify-ui/tooltip'
import { Provider as JotaiProvider } from 'jotai/react'
import { ThemeProvider } from 'next-themes'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
-import GlobalPublicStoreProvider from '@/context/global-public-context'
import { TanstackQueryInitializer } from '@/context/query-client'
import { getDatasetMap } from '@/env'
import { getLocaleOnServer } from '@/i18n-config/server'
@@ -70,11 +69,9 @@ const LocaleLayout = async ({
-
-
- {children}
-
-
+
+ {children}
+
diff --git a/web/app/loading.tsx b/web/app/loading.tsx
new file mode 100644
index 0000000000..b108baaa97
--- /dev/null
+++ b/web/app/loading.tsx
@@ -0,0 +1,9 @@
+import Loading from '@/app/components/base/loading'
+
+export default function RootLoading() {
+ return (
+
+
+
+ )
+}
diff --git a/web/app/reset-password/layout.tsx b/web/app/reset-password/layout.tsx
index e9909fe438..e2691a397e 100644
--- a/web/app/reset-password/layout.tsx
+++ b/web/app/reset-password/layout.tsx
@@ -1,11 +1,12 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
+import { useSuspenseQuery } from '@tanstack/react-query'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import Header from '../signin/_header'
export default function SignInLayout({ children }: any) {
- const { systemFeatures } = useGlobalPublicStore()
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
return (
<>
diff --git a/web/app/signin/_header.tsx b/web/app/signin/_header.tsx
index 7ebff9f73c..0b424d45c2 100644
--- a/web/app/signin/_header.tsx
+++ b/web/app/signin/_header.tsx
@@ -1,12 +1,13 @@
'use client'
import type { Locale } from '@/i18n-config'
+import { useSuspenseQuery } from '@tanstack/react-query'
import Divider from '@/app/components/base/divider'
import LocaleSigninSelect from '@/app/components/base/select/locale-signin'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useLocale } from '@/context/i18n'
import { setLocaleOnClient } from '@/i18n-config'
import { languages } from '@/i18n-config/language'
import dynamic from '@/next/dynamic'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
// Avoid rendering the logo and theme selector on the server
const DifyLogo = dynamic(() => import('@/app/components/base/logo/dify-logo'), {
@@ -20,7 +21,7 @@ const ThemeSelector = dynamic(() => import('@/app/components/base/theme-selector
const Header = () => {
const locale = useLocale()
- const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
return (
diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx
index fe3dac7153..6f198b99ca 100644
--- a/web/app/signin/invite-settings/page.tsx
+++ b/web/app/signin/invite-settings/page.tsx
@@ -3,6 +3,7 @@ import type { Locale } from '@/i18n-config'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { RiAccountCircleLine } from '@remixicon/react'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { noop } from 'es-toolkit/function'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -10,19 +11,19 @@ import Input from '@/app/components/base/input'
import Loading from '@/app/components/base/loading'
import { SimpleSelect } from '@/app/components/base/select'
import { LICENSE_LINK } from '@/constants/link'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { setLocaleOnClient } from '@/i18n-config'
import { languages, LanguagesSupported } from '@/i18n-config/language'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { activateMember } from '@/service/common'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useInvitationCheck } from '@/service/use-common'
import { timezones } from '@/utils/timezone'
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
export default function InviteSettingsPage() {
const { t } = useTranslation()
- const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const router = useRouter()
const searchParams = useSearchParams()
const token = decodeURIComponent(searchParams.get('invite_token') as string)
diff --git a/web/app/signin/layout.tsx b/web/app/signin/layout.tsx
index ed32727ae2..a6537793c8 100644
--- a/web/app/signin/layout.tsx
+++ b/web/app/signin/layout.tsx
@@ -1,12 +1,13 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
+import { useSuspenseQuery } from '@tanstack/react-query'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import Header from './_header'
export default function SignInLayout({ children }: any) {
- const { systemFeatures } = useGlobalPublicStore()
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
useDocumentTitle('')
return (
<>
diff --git a/web/app/signin/normal-form.tsx b/web/app/signin/normal-form.tsx
index 2ead90f068..779aba5c9c 100644
--- a/web/app/signin/normal-form.tsx
+++ b/web/app/signin/normal-form.tsx
@@ -1,15 +1,16 @@
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react'
+import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { IS_CE_EDITION } from '@/config'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import Link from '@/next/link'
import { useRouter, useSearchParams } from '@/next/navigation'
import { invitationCheck } from '@/service/common'
-import { useIsLogin } from '@/service/use-common'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
+import { isLegacyBase401, userProfileQueryOptions } from '@/service/use-common'
import { LicenseStatus } from '@/types/feature'
import Loading from '../components/base/loading'
import MailAndCodeAuth from './components/mail-and-code-auth'
@@ -23,14 +24,20 @@ const NormalForm = () => {
const { t } = useTranslation()
const router = useRouter()
const searchParams = useSearchParams()
- const { isLoading: isCheckLoading, data: loginData } = useIsLogin()
- const isLoggedIn = loginData?.logged_in
+ // Login probe: 401 stays as `error` (legitimate "not logged in" state on /signin),
+ // other errors throw to error.tsx. jumpTo same-pathname guard in service/base.ts
+ // prevents the redirect loop on 401.
+ const { isPending: isCheckLoading, data: userResp, error: probeError } = useQuery({
+ ...userProfileQueryOptions(),
+ throwOnError: err => !isLegacyBase401(err),
+ })
+ const isLoggedIn = !!userResp && !probeError
const message = decodeURIComponent(searchParams.get('message') || '')
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
const [isInitCheckLoading, setInitCheckLoading] = useState(true)
const [isRedirecting, setIsRedirecting] = useState(false)
const isLoading = isCheckLoading || isInitCheckLoading || isRedirecting
- const { systemFeatures } = useGlobalPublicStore()
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
diff --git a/web/app/signup/components/input-mail.spec.tsx b/web/app/signup/components/input-mail.spec.tsx
index e16c381585..9afea73df7 100644
--- a/web/app/signup/components/input-mail.spec.tsx
+++ b/web/app/signup/components/input-mail.spec.tsx
@@ -1,29 +1,14 @@
import type { MockedFunction } from 'vitest'
-import type { SystemFeatures } from '@/types/feature'
-import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { fireEvent, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
import { useLocale } from '@/context/i18n'
import { useSendMail } from '@/service/use-common'
-import { defaultSystemFeatures } from '@/types/feature'
import Form from './input-mail'
const mockSubmitMail = vi.fn()
const mockOnSuccess = vi.fn()
-type SystemFeaturesOverrides = Partial
> & {
- branding?: Partial
-}
-
-const buildSystemFeatures = (overrides: SystemFeaturesOverrides = {}): SystemFeatures => ({
- ...defaultSystemFeatures,
- ...overrides,
- branding: {
- ...defaultSystemFeatures.branding,
- ...overrides.branding,
- },
-})
-
vi.mock('@/next/link', () => ({
default: ({ children, href, className, target, rel }: { children: React.ReactNode, href: string, className?: string, target?: string, rel?: string }) => (
@@ -32,10 +17,6 @@ vi.mock('@/next/link', () => ({
),
}))
-vi.mock('@/context/global-public-context', () => ({
- useGlobalPublicStore: vi.fn(),
-}))
-
vi.mock('@/context/i18n', () => ({
useLocale: vi.fn(),
}))
@@ -46,7 +27,6 @@ vi.mock('@/service/use-common', () => ({
type UseSendMailResult = ReturnType
-const mockUseGlobalPublicStore = useGlobalPublicStore as unknown as MockedFunction
const mockUseLocale = useLocale as unknown as MockedFunction
const mockUseSendMail = useSendMail as unknown as MockedFunction
@@ -57,17 +37,14 @@ const renderForm = ({
brandingEnabled?: boolean
isPending?: boolean
} = {}) => {
- mockUseGlobalPublicStore.mockReturnValue({
- systemFeatures: buildSystemFeatures({
- branding: { enabled: brandingEnabled },
- }),
- })
mockUseLocale.mockReturnValue('en-US')
mockUseSendMail.mockReturnValue({
mutateAsync: mockSubmitMail,
isPending,
} as unknown as UseSendMailResult)
- return render()
+ return renderWithSystemFeatures(, {
+ systemFeatures: { branding: { enabled: brandingEnabled } },
+ })
}
describe('InputMail Form', () => {
diff --git a/web/app/signup/components/input-mail.tsx b/web/app/signup/components/input-mail.tsx
index f4c5214c11..a0a09001d2 100644
--- a/web/app/signup/components/input-mail.tsx
+++ b/web/app/signup/components/input-mail.tsx
@@ -2,14 +2,15 @@
import type { MailSendResponse } from '@/service/use-common'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
+import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Split from '@/app/signin/split'
import { emailRegex } from '@/config'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useLocale } from '@/context/i18n'
import Link from '@/next/link'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useSendMail } from '@/service/use-common'
type Props = {
@@ -21,7 +22,7 @@ export default function Form({
const { t } = useTranslation()
const [email, setEmail] = useState('')
const locale = useLocale()
- const { systemFeatures } = useGlobalPublicStore()
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const { mutateAsync: submitMail, isPending } = useSendMail()
diff --git a/web/app/signup/layout.tsx b/web/app/signup/layout.tsx
index 16c5fcefd2..3c1b17ae10 100644
--- a/web/app/signup/layout.tsx
+++ b/web/app/signup/layout.tsx
@@ -1,12 +1,13 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
+import { useSuspenseQuery } from '@tanstack/react-query'
import Header from '@/app/signin/_header'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
export default function RegisterLayout({ children }: any) {
- const { systemFeatures } = useGlobalPublicStore()
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
useDocumentTitle('')
return (
<>
diff --git a/web/context/app-context-provider.tsx b/web/context/app-context-provider.tsx
index 0bf3851046..fb17664d6d 100644
--- a/web/context/app-context-provider.tsx
+++ b/web/context/app-context-provider.tsx
@@ -2,7 +2,7 @@
import type { FC, ReactNode } from 'react'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
-import { useQueryClient } from '@tanstack/react-query'
+import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo } from 'react'
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
@@ -16,12 +16,12 @@ import {
useSelector,
} from '@/context/app-context'
import { env } from '@/env'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import {
useCurrentWorkspace,
useLangGeniusVersion,
- useUserProfile,
+ userProfileQueryOptions,
} from '@/service/use-common'
-import { useGlobalPublicStore } from './global-public-context'
type AppContextProviderProps = {
children: ReactNode
@@ -29,8 +29,13 @@ type AppContextProviderProps = {
export const AppContextProvider: FC = ({ children }) => {
const queryClient = useQueryClient()
- const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
- const { data: userProfileResp } = useUserProfile()
+ // Boot point for the (commonLayout) tree:
+ // - useSuspenseQuery for systemFeatures triggers app/loading.tsx until cache is warm.
+ // - useSuspenseQuery for userProfile triggers (commonLayout)/loading.tsx until cache is warm.
+ // After this provider mounts, downstream components reading the same queryKeys hit cache
+ // and never suspend again, so their useSuspenseQuery calls return data synchronously.
+ const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
+ const { data: userProfileResp } = useSuspenseQuery(userProfileQueryOptions())
const { data: currentWorkspaceResp, isPending: isLoadingCurrentWorkspace, isFetching: isValidatingCurrentWorkspace } = useCurrentWorkspace()
const langGeniusVersionQuery = useLangGeniusVersion(
userProfileResp?.meta.currentVersion,
diff --git a/web/context/global-public-context.tsx b/web/context/global-public-context.tsx
deleted file mode 100644
index 190033bbf7..0000000000
--- a/web/context/global-public-context.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-'use client'
-import type { FC, PropsWithChildren } from 'react'
-import type { SystemFeatures } from '@/types/feature'
-import { useQuery } from '@tanstack/react-query'
-import { create } from 'zustand'
-import Loading from '@/app/components/base/loading'
-import { consoleClient } from '@/service/client'
-import { defaultSystemFeatures } from '@/types/feature'
-import { fetchSetupStatusWithCache } from '@/utils/setup-status'
-
-type GlobalPublicStore = {
- systemFeatures: SystemFeatures
- setSystemFeatures: (systemFeatures: SystemFeatures) => void
-}
-
-export const useGlobalPublicStore = create(set => ({
- systemFeatures: defaultSystemFeatures,
- setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
-}))
-
-const systemFeaturesQueryKey = ['systemFeatures'] as const
-const setupStatusQueryKey = ['setupStatus'] as const
-
-async function fetchSystemFeatures() {
- const data = await consoleClient.systemFeatures()
- const { setSystemFeatures } = useGlobalPublicStore.getState()
- setSystemFeatures({ ...defaultSystemFeatures, ...data })
- return data
-}
-
-export function useSystemFeaturesQuery() {
- return useQuery({
- queryKey: systemFeaturesQueryKey,
- queryFn: fetchSystemFeatures,
- })
-}
-
-export function useIsSystemFeaturesPending() {
- const { isPending } = useSystemFeaturesQuery()
- return isPending
-}
-
-function useSetupStatusQuery() {
- return useQuery({
- queryKey: setupStatusQueryKey,
- queryFn: fetchSetupStatusWithCache,
- staleTime: Infinity,
- })
-}
-
-const GlobalPublicStoreProvider: FC = ({
- children,
-}) => {
- // Fetch systemFeatures and setupStatus in parallel to reduce waterfall.
- // setupStatus is prefetched here and cached in localStorage for AppInitializer.
- const { isPending } = useSystemFeaturesQuery()
-
- // Prefetch setupStatus for AppInitializer (result not needed here)
- useSetupStatusQuery()
-
- if (isPending)
- return
- return <>{children}>
-}
-export default GlobalPublicStoreProvider
diff --git a/web/context/web-app-context.tsx b/web/context/web-app-context.tsx
index 33679fd44f..be10a85f99 100644
--- a/web/context/web-app-context.tsx
+++ b/web/context/web-app-context.tsx
@@ -3,14 +3,15 @@
import type { FC, PropsWithChildren } from 'react'
import type { ChatConfig } from '@/app/components/base/chat/types'
import type { AppData, AppMeta } from '@/models/share'
+import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { create } from 'zustand'
import { getProcessedSystemVariablesFromUrlParams } from '@/app/components/base/chat/utils'
import Loading from '@/app/components/base/loading'
import { AccessMode } from '@/models/access-control'
import { usePathname, useSearchParams } from '@/next/navigation'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useGetWebAppAccessModeByCode } from '@/service/use-share'
-import { useIsSystemFeaturesPending } from './global-public-context'
type WebAppStore = {
shareCode: string | null
@@ -65,7 +66,7 @@ const getShareCodeFromPathname = (pathname: string): string | null => {
}
const WebAppStoreProvider: FC = ({ children }) => {
- const isGlobalPending = useIsSystemFeaturesPending()
+ const { isPending: isGlobalPending } = useQuery(systemFeaturesQueryOptions())
const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode)
const updateShareCode = useWebAppStore(state => state.updateShareCode)
const updateEmbeddedUserId = useWebAppStore(state => state.updateEmbeddedUserId)
diff --git a/web/contract/console/system.ts b/web/contract/console/system.ts
index bce0a8226e..73e920b804 100644
--- a/web/contract/console/system.ts
+++ b/web/contract/console/system.ts
@@ -7,5 +7,4 @@ export const systemFeaturesContract = base
path: '/system-features',
method: 'GET',
})
- .input(type())
.output(type())
diff --git a/web/hooks/use-document-title.spec.ts b/web/hooks/use-document-title.spec.ts
index 7ce1e693db..36a7bcc83b 100644
--- a/web/hooks/use-document-title.spec.ts
+++ b/web/hooks/use-document-title.spec.ts
@@ -1,5 +1,4 @@
-import { act, renderHook } from '@testing-library/react'
-import { useGlobalPublicStore, useIsSystemFeaturesPending } from '@/context/global-public-context'
+import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
/**
* Test suite for useDocumentTitle hook
*
@@ -12,42 +11,20 @@ import { useGlobalPublicStore, useIsSystemFeaturesPending } from '@/context/glob
* Title format: "[Page Title] - [Brand Name]"
* If no page title: "[Brand Name]"
*/
-import { defaultSystemFeatures } from '@/types/feature'
import useDocumentTitle from './use-document-title'
-vi.mock('@/context/global-public-context', async (importOriginal) => {
- const actual = await importOriginal()
- return {
- ...actual,
- useIsSystemFeaturesPending: vi.fn(() => false),
- }
-})
-
/**
* Test behavior when system features are still loading
* Title should remain empty to prevent flicker
*/
describe('title should be empty if systemFeatures is pending', () => {
- beforeEach(() => {
- vi.mocked(useIsSystemFeaturesPending).mockReturnValue(true)
- act(() => {
- useGlobalPublicStore.setState({
- systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
- })
- })
- })
- /**
- * Test that title stays empty during loading even when a title is provided
- */
it('document title should be empty if set title', () => {
- renderHook(() => useDocumentTitle('test'))
+ renderHookWithSystemFeatures(() => useDocumentTitle('test'), { systemFeatures: null })
expect(document.title).toBe('')
})
- /**
- * Test that title stays empty during loading when no title is provided
- */
+
it('document title should be empty if not set title', () => {
- renderHook(() => useDocumentTitle(''))
+ renderHookWithSystemFeatures(() => useDocumentTitle(''), { systemFeatures: null })
expect(document.title).toBe('')
})
})
@@ -57,29 +34,17 @@ describe('title should be empty if systemFeatures is pending', () => {
* When custom branding is disabled, should use "Dify" as the brand name
*/
describe('use default branding', () => {
- beforeEach(() => {
- vi.mocked(useIsSystemFeaturesPending).mockReturnValue(false)
- act(() => {
- useGlobalPublicStore.setState({
- systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
- })
- })
- })
- /**
- * Test title format with page title and default branding
- * Format: "[page] - Dify"
- */
it('document title should be test-Dify if set title', () => {
- renderHook(() => useDocumentTitle('test'))
+ renderHookWithSystemFeatures(() => useDocumentTitle('test'), {
+ systemFeatures: { branding: { enabled: false } },
+ })
expect(document.title).toBe('test - Dify')
})
- /**
- * Test title with only default branding (no page title)
- * Format: "Dify"
- */
it('document title should be Dify if not set title', () => {
- renderHook(() => useDocumentTitle(''))
+ renderHookWithSystemFeatures(() => useDocumentTitle(''), {
+ systemFeatures: { branding: { enabled: false } },
+ })
expect(document.title).toBe('Dify')
})
})
@@ -89,29 +54,17 @@ describe('use default branding', () => {
* When custom branding is enabled, should use the configured application_title
*/
describe('use specific branding', () => {
- beforeEach(() => {
- vi.mocked(useIsSystemFeaturesPending).mockReturnValue(false)
- act(() => {
- useGlobalPublicStore.setState({
- systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } },
- })
- })
- })
- /**
- * Test title format with page title and custom branding
- * Format: "[page] - [Custom Brand]"
- */
it('document title should be test-Test if set title', () => {
- renderHook(() => useDocumentTitle('test'))
+ renderHookWithSystemFeatures(() => useDocumentTitle('test'), {
+ systemFeatures: { branding: { enabled: true, application_title: 'Test' } },
+ })
expect(document.title).toBe('test - Test')
})
- /**
- * Test title with only custom branding (no page title)
- * Format: "[Custom Brand]"
- */
it('document title should be Test if not set title', () => {
- renderHook(() => useDocumentTitle(''))
+ renderHookWithSystemFeatures(() => useDocumentTitle(''), {
+ systemFeatures: { branding: { enabled: true, application_title: 'Test' } },
+ })
expect(document.title).toBe('Test')
})
})
diff --git a/web/hooks/use-document-title.ts b/web/hooks/use-document-title.ts
index 37b31a7dea..ad275f55c9 100644
--- a/web/hooks/use-document-title.ts
+++ b/web/hooks/use-document-title.ts
@@ -1,12 +1,14 @@
'use client'
+import { useQuery } from '@tanstack/react-query'
import { useFavicon, useTitle } from 'ahooks'
import { useEffect } from 'react'
-import { useGlobalPublicStore, useIsSystemFeaturesPending } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
+import { defaultSystemFeatures } from '@/types/feature'
import { basePath } from '@/utils/var'
export default function useDocumentTitle(title: string) {
- const isPending = useIsSystemFeaturesPending()
- const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
+ const { data, isPending } = useQuery(systemFeaturesQueryOptions())
+ const systemFeatures = data ?? defaultSystemFeatures
const prefix = title ? `${title} - ` : ''
let titleStr = ''
let favicon = ''
diff --git a/web/next.config.ts b/web/next.config.ts
index aa4d9318f4..db44f5b9ed 100644
--- a/web/next.config.ts
+++ b/web/next.config.ts
@@ -34,9 +34,6 @@ const nextConfig: NextConfig = {
compiler: {
removeConsole: isDev ? false : { exclude: ['warn', 'error'] },
},
- experimental: {
- turbopackFileSystemCacheForDev: false,
- },
}
export default withMDX(nextConfig)
diff --git a/web/service/access-control.ts b/web/service/access-control.ts
index c87e01f482..fa7cfd7055 100644
--- a/web/service/access-control.ts
+++ b/web/service/access-control.ts
@@ -1,7 +1,7 @@
import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control'
import type { App } from '@/types/app'
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
-import { useGlobalPublicStore } from '@/context/global-public-context'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { get, post } from './base'
import { getUserCanAccess } from './share'
@@ -71,11 +71,15 @@ export const useUpdateAccessMode = () => {
}
export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled }: { appId?: string, isInstalledApp?: boolean, enabled?: boolean }) => {
- const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
+ // useQuery (not useSuspenseQuery) to keep this service hook's call contract
+ // unchanged from the zustand era: callers should not need a Suspense boundary.
+ // First-fetch undefined is bridged via `?? false` so the inner queryKey is stable.
+ const { data: systemFeatures } = useQuery(systemFeaturesQueryOptions())
+ const webappAuthEnabled = systemFeatures?.webapp_auth.enabled ?? false
return useQuery({
- queryKey: [NAME_SPACE, 'user-can-access-app', appId, systemFeatures.webapp_auth.enabled, isInstalledApp],
+ queryKey: [NAME_SPACE, 'user-can-access-app', appId, webappAuthEnabled, isInstalledApp],
queryFn: () => {
- if (systemFeatures.webapp_auth.enabled)
+ if (webappAuthEnabled)
return getUserCanAccess(appId!, isInstalledApp)
else
return { result: true }
diff --git a/web/service/system-features.ts b/web/service/system-features.ts
new file mode 100644
index 0000000000..4bf79a16fd
--- /dev/null
+++ b/web/service/system-features.ts
@@ -0,0 +1,33 @@
+import type { SystemFeatures } from '@/types/feature'
+import { queryOptions } from '@tanstack/react-query'
+import { defaultSystemFeatures } from '@/types/feature'
+import { consoleClient, consoleQuery } from './client'
+
+/**
+ * Soft-fallback to defaults so the dashboard stays usable when /system-features fails.
+ *
+ * No `retry`: the queryFn never throws (errors are caught and turned into the
+ * default payload), so react-query's retry would never fire under this model.
+ * This trades main's transient-blip resilience for guaranteed dashboard
+ * availability via defaults; the trade-off is acceptable because /system-features
+ * is a small, dependency-free endpoint in the community edition.
+ *
+ * No `staleTime` override either: inherit the 5-minute default from
+ * query-client-server.ts. Combined with `refetchOnWindowFocus`, this lets us
+ * recover from a transient startup failure (which got cached as "successful
+ * defaults") within ~5 minutes or on tab focus. `staleTime: Infinity` would
+ * pin the whole tab to defaults until reload — strictly worse than main.
+ */
+export const systemFeaturesQueryOptions = () =>
+ queryOptions({
+ queryKey: consoleQuery.systemFeatures.queryKey(),
+ queryFn: async () => {
+ try {
+ return await consoleClient.systemFeatures()
+ }
+ catch (err) {
+ console.error('[systemFeatures] fetch failed, using defaults', err)
+ return defaultSystemFeatures
+ }
+ },
+ })
diff --git a/web/service/use-common.ts b/web/service/use-common.ts
index 238f4f6ecf..0154be09ff 100644
--- a/web/service/use-common.ts
+++ b/web/service/use-common.ts
@@ -21,10 +21,20 @@ import type {
UserProfileResponse,
} from '@/models/common'
import type { RETRIEVE_METHOD } from '@/types/app'
-import { useMutation, useQuery } from '@tanstack/react-query'
+import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { IS_DEV } from '@/config'
import { get, post } from './base'
+/**
+ * True iff `err` is a 401 Response thrown by `service/base.ts`.
+ *
+ * Narrow on purpose: oRPC throws `ORPCError`, not `Response`, so this predicate
+ * returns `false` for oRPC 401s. Naming makes that scope visible. If you need
+ * 401 detection for an oRPC path, add a separate `isOrpc401` helper.
+ */
+export const isLegacyBase401 = (err: unknown): boolean =>
+ err instanceof Response && err.status === 401
+
const NAME_SPACE = 'common'
export const commonQueryKeys = {
@@ -35,7 +45,6 @@ export const commonQueryKeys = {
members: [NAME_SPACE, 'members'] as const,
filePreview: (fileID: string) => [NAME_SPACE, 'file-preview', fileID] as const,
schemaDefinitions: [NAME_SPACE, 'schema-type-definitions'] as const,
- isLogin: [NAME_SPACE, 'is-login'] as const,
modelProviders: [NAME_SPACE, 'model-providers'] as const,
modelList: (type: ModelTypeEnum) => [NAME_SPACE, 'model-list', type] as const,
defaultModel: (type: ModelTypeEnum) => [NAME_SPACE, 'default-model', type] as const,
@@ -74,11 +83,25 @@ type UserProfileWithMeta = {
}
}
-export const useUserProfile = () => {
- return useQuery({
+/**
+ * Session probe for `/account/profile`. Helper (not hook) because oRPC can't
+ * express the `x-version` / `x-env` response headers we post-process.
+ *
+ * Bindings:
+ * commonLayout -> `useSuspenseQuery(userProfileQueryOptions())`
+ * signin/oauth -> `useQuery({ ...userProfileQueryOptions(), throwOnError: err => !isLegacyBase401(err) })`
+ *
+ * `silent: true` + `retry: !isLegacyBase401` makes 401 a synchronous *state* (no toast,
+ * no ~7s retry storm). Transient errors still get the default 3 retries.
+ */
+export const userProfileQueryOptions = () =>
+ queryOptions({
queryKey: commonQueryKeys.userProfile,
queryFn: async () => {
- const response = await get('/account/profile', {}, { needAllResponseContent: true }) as Response
+ const response = await get('/account/profile', {}, {
+ needAllResponseContent: true,
+ silent: true,
+ }) as Response
const profile = await response.clone().json() as UserProfileResponse
return {
profile,
@@ -92,8 +115,8 @@ export const useUserProfile = () => {
},
staleTime: 0,
gcTime: 0,
+ retry: (failureCount, error) => !isLegacyBase401(error) && failureCount < 3,
})
-}
export const useLangGeniusVersion = (currentVersion?: string | null, enabled?: boolean) => {
return useQuery({
@@ -205,34 +228,21 @@ export const useSchemaTypeDefinitions = () => {
})
}
-type isLogin = {
- logged_in: boolean
-}
-
-export const useIsLogin = () => {
- return useQuery({
- queryKey: commonQueryKeys.isLogin,
- staleTime: 0,
- gcTime: 0,
- queryFn: async (): Promise => {
- try {
- await get('/account/profile', {}, {
- silent: true,
- })
- return { logged_in: true }
- }
- catch {
- // Any error (401, 500, network error, etc.) means not logged in
- return { logged_in: false }
- }
- },
- })
-}
-
export const useLogout = () => {
+ const queryClient = useQueryClient()
return useMutation({
mutationKey: [NAME_SPACE, 'logout'],
mutationFn: () => post('/logout'),
+ onSuccess: () => {
+ // Drop all cached queries so the post-logout /signin probe doesn't read
+ // the previous user's profile (the userProfile queryKey is shared with
+ // the (commonLayout) tree, which keeps observing it during React's
+ // concurrent transition — gcTime: 0 is not enough on its own).
+ // Nuclear over targeted: every new user-scoped query would otherwise
+ // need to be remembered here. systemFeatures (user-agnostic) just
+ // refetches once on the way to /signin, which is cheap.
+ queryClient.clear()
+ },
})
}
diff --git a/web/service/use-explore.ts b/web/service/use-explore.ts
index 1f3c0ed6b9..de508b642f 100644
--- a/web/service/use-explore.ts
+++ b/web/service/use-explore.ts
@@ -1,8 +1,8 @@
import type { App, AppCategory } from '@/models/explore'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
-import { useGlobalPublicStore } from '@/context/global-public-context'
import { useLocale } from '@/context/i18n'
import { AccessMode } from '@/models/access-control'
+import { systemFeaturesQueryOptions } from '@/service/system-features'
import { consoleQuery } from './client'
import { fetchAppList, fetchBanners, fetchInstalledAppList, fetchInstalledAppMeta, fetchInstalledAppParams, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore'
@@ -66,18 +66,22 @@ export const useUpdateAppPinStatus = () => {
}
export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => {
- const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
+ // useQuery (not useSuspenseQuery) to keep this service hook's call contract
+ // unchanged from the zustand era: callers should not need a Suspense boundary.
+ // First-fetch undefined is bridged via `?? false` so the inner queryKey is stable.
+ const { data: systemFeatures } = useQuery(systemFeaturesQueryOptions())
+ const webappAuthEnabled = systemFeatures?.webapp_auth.enabled ?? false
const appAccessModeInput = { query: { appId: appId ?? '' } }
const installedAppId = appAccessModeInput.query.appId
return useQuery({
queryKey: [
...consoleQuery.explore.appAccessMode.queryKey({ input: appAccessModeInput }),
- systemFeatures.webapp_auth.enabled,
+ webappAuthEnabled,
installedAppId,
],
queryFn: () => {
- if (systemFeatures.webapp_auth.enabled === false) {
+ if (webappAuthEnabled === false) {
return {
accessMode: AccessMode.PUBLIC,
}