diff --git a/web/__mocks__/provider-context.ts b/web/__mocks__/provider-context.ts index 406ef2cc306..cda58b3f513 100644 --- a/web/__mocks__/provider-context.ts +++ b/web/__mocks__/provider-context.ts @@ -11,14 +11,6 @@ export const baseProviderContextValue: ProviderContextState = { isSuccessModelProviders: false, textGenerationModelList: [], isAPIKeySet: true, - - enableSkill: false, - enableReplaceWebAppLogo: false, - modelLoadBalancingEnabled: false, - enableEducationPlan: false, - isAllowTransferWorkspace: false, - isAllowPublishAsCustomKnowledgePipelineTemplate: false, - humanInputEmailDeliveryEnabled: false, } export const createMockProviderContextValue = ( diff --git a/web/app/account/(commonLayout)/account-page/index.tsx b/web/app/account/(commonLayout)/account-page/index.tsx index 95d8891ab3e..6fa3ee3d882 100644 --- a/web/app/account/(commonLayout)/account-page/index.tsx +++ b/web/app/account/(commonLayout)/account-page/index.tsx @@ -16,7 +16,6 @@ import AppIcon from '@/app/components/base/app-icon' import PremiumBadge from '@/app/components/base/premium-badge' import Collapse from '@/app/components/header/account-setting/collapse' import { validPassword } from '@/config' -import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { updateUserProfile } from '@/service/common' @@ -55,10 +54,14 @@ export default function AccountPage() { const userProfile = userProfileResp.profile const mutateUserProfile = () => queryClient.invalidateQueries({ queryKey: userProfileQueryOptions().queryKey }) - const { enableEducationPlan } = useProviderContext() + const { data: enableEducationPlan } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.education.enabled, + }), + ) const { data: isEducationAccount = false } = useQuery( consoleQuery.account.education.get.queryOptions({ - enabled: enableEducationPlan, + enabled: enableEducationPlan === true, select: ({ is_student }) => is_student ?? false, }), ) diff --git a/web/app/account/(commonLayout)/avatar.tsx b/web/app/account/(commonLayout)/avatar.tsx index 8989f7fa695..ab87124de25 100644 --- a/web/app/account/(commonLayout)/avatar.tsx +++ b/web/app/account/(commonLayout)/avatar.tsx @@ -11,7 +11,6 @@ import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { resetUser } from '@/app/components/base/amplitude/utils' import PremiumBadge from '@/app/components/base/premium-badge' -import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' import { useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/console' @@ -23,10 +22,14 @@ export default function AppSelector() { // Cache is hydrated by CommonLayoutHydrationBoundary; this hits cache synchronously. const { data: userProfileResp } = useSuspenseQuery(userProfileQueryOptions()) const userProfile = userProfileResp.profile - const { enableEducationPlan } = useProviderContext() + const { data: enableEducationPlan } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.education.enabled, + }), + ) const { data: isEducationAccount = false } = useQuery( consoleQuery.account.education.get.queryOptions({ - enabled: enableEducationPlan, + enabled: enableEducationPlan === true, select: ({ is_student }) => is_student ?? false, }), ) diff --git a/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx b/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx index 8c7e45a9640..05d10c13de2 100644 --- a/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx +++ b/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx @@ -43,14 +43,6 @@ const defaultProviderContext = { isSuccessModelProviders: false, textGenerationModelList: [], isAPIKeySet: false, - - enableSkill: false, - enableReplaceWebAppLogo: false, - modelLoadBalancingEnabled: false, - enableEducationPlan: false, - isAllowTransferWorkspace: false, - isAllowPublishAsCustomKnowledgePipelineTemplate: false, - humanInputEmailDeliveryEnabled: false, } type MockOverrides = { diff --git a/web/app/components/goto-anything/__tests__/index.spec.tsx b/web/app/components/goto-anything/__tests__/index.spec.tsx index d15209e931f..4fb6e3825f4 100644 --- a/web/app/components/goto-anything/__tests__/index.spec.tsx +++ b/web/app/components/goto-anything/__tests__/index.spec.tsx @@ -1,11 +1,11 @@ import type { ReactNode } from 'react' import type { ActionItem, SearchResult } from '../actions/types' -import type { ProviderContextState } from '@/context/provider-context' import { DialogTrigger } from '@langgenius/dify-ui/dialog' import { detectPlatform } from '@tanstack/react-hotkeys' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' +import { createConsoleQueryWrapper } from '@/test/console/query-data' import { gotoAnythingDialogHandle } from '../dialog-handle' import { GotoAnything } from '../index' @@ -79,28 +79,34 @@ function setRemoteResults(results: TestSearchResult[]) { }) } -vi.mock('@tanstack/react-query', () => ({ - keepPreviousData: (previousData: unknown) => previousData, - useQuery: (options: { - queryKey: [key: keyof typeof remoteQueryStates, searchTerm: string] - enabled?: boolean - placeholderData?: (previousData: unknown) => unknown - }) => { - const provider = options.queryKey[0] - if (!options.enabled) return emptyRemoteQueryState() +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + keepPreviousData: (previousData: unknown) => previousData, + useQuery: (options: { + queryKey: readonly unknown[] + enabled?: boolean + placeholderData?: (previousData: unknown) => unknown + }) => { + if (typeof options.queryKey[0] !== 'string') + return actual.useQuery({ ...options, placeholderData: undefined }) + const provider = options.queryKey[0] as keyof typeof remoteQueryStates + if (!options.enabled) return emptyRemoteQueryState() - enabledRemoteQueryKeys.push(provider) - enabledRemoteSearches.push(options.queryKey) - const state = remoteQueryStates[provider] - let data = state.data - if (state.isFetching && data.length === 0 && options.placeholderData) - data = (options.placeholderData(previousRemoteData[provider]) as TestSearchResult[]) ?? [] - if (!state.isLoading && !state.isFetching && !state.isError) - previousRemoteData[provider] = state.data + enabledRemoteQueryKeys.push(provider) + enabledRemoteSearches.push([provider, options.queryKey[1] as string]) + const state = remoteQueryStates[provider] + let data = state.data + if (state.isFetching && data.length === 0 && options.placeholderData) + data = (options.placeholderData(previousRemoteData[provider]) as TestSearchResult[]) ?? [] + if (!state.isLoading && !state.isFetching && !state.isError) + previousRemoteData[provider] = state.data - return { ...state, data } - }, -})) + return { ...state, data } + }, + } +}) vi.mock('../actions/app', () => ({ appSearchQueryOptions: (searchTerm: string) => ({ queryKey: ['app', searchTerm] }), @@ -145,12 +151,6 @@ vi.mock('@/features/agent-v2/permissions', () => ({ useCanManageAgents: () => visibilityState.canManageAgents, })) -vi.mock('@/context/provider-context', () => ({ - useProviderContextSelector: vi.fn((selector: (state: Partial) => unknown) => - selector({ enableSkill: visibilityState.enableSkill }), - ), -})) - vi.mock( '@/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission', () => ({ @@ -249,7 +249,12 @@ vi.mock('../../plugins/install-plugin/install-from-marketplace', () => ({ ), })) -const renderGotoAnything = (ui: React.ReactElement) => render(ui) +const renderGotoAnything = (ui: React.ReactElement) => { + const { wrapper } = createConsoleQueryWrapper({ + features: { enable_skill: visibilityState.enableSkill }, + }) + return render(ui, { wrapper }) +} describe('GotoAnything', () => { beforeEach(() => { diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index aa31771d5ed..336f0d8e99f 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -40,11 +40,11 @@ import { useTranslation } from 'react-i18next' import { MAIN_NAV_ROUTES } from '@/app/components/main-nav/routes' import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation' import { useGetLanguage } from '@/context/i18n' -import { useProviderContextSelector } from '@/context/provider-context' import { isCurrentWorkspaceDatasetOperatorAtom } from '@/context/workspace-state' import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag' import { useCanManageAgents } from '@/features/agent-v2/permissions' import { usePathname, useRouter } from '@/next/navigation' +import { consoleQuery } from '@/service/console' import { PluginInstallPermissionProvider } from '../plugins/install-plugin/components/plugin-install-permission-provider' import useWorkspacePluginInstallPermission from '../plugins/install-plugin/hooks/use-workspace-plugin-install-permission' import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace' @@ -233,9 +233,13 @@ function GotoAnythingDialog() { const defaultLocale = useGetLanguage() const canManageAgents = useCanManageAgents() const isCurrentWorkspaceDatasetOperator = useAtomValue(isCurrentWorkspaceDatasetOperatorAtom) - const enableSkill = useProviderContextSelector((state) => state.enableSkill) + const { data: enableSkill } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.enable_skill, + }), + ) const agentsAvailable = isAgentV2Enabled() && canManageAgents - const skillsAvailable = enableSkill && !isCurrentWorkspaceDatasetOperator + const skillsAvailable = enableSkill === true && !isCurrentWorkspaceDatasetOperator const isWorkflowPage = appWorkflowPathPattern.test(pathname) || sharedWorkflowPathPattern.test(pathname) const isRagPipelinePage = ragPipelinePathPattern.test(pathname) diff --git a/web/app/components/header/account-dropdown/__tests__/index.spec.tsx b/web/app/components/header/account-dropdown/__tests__/index.spec.tsx index 9a9fd35e581..ccdbf95ea5a 100644 --- a/web/app/components/header/account-dropdown/__tests__/index.spec.tsx +++ b/web/app/components/header/account-dropdown/__tests__/index.spec.tsx @@ -1,10 +1,8 @@ -import type { ProviderContextState } from '@/context/provider-context' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderToString } from 'react-dom/server' import { resetUser } from '@/app/components/base/amplitude/utils' import AccountSection from '@/app/components/main-nav/components/account-section' -import { useProviderContext } from '@/context/provider-context' import { useLogout } from '@/service/use-common' import { createAccountProfileQueryClient } from '@/test/console/account-profile' import { renderWithConsoleQuery } from '@/test/console/query-data' @@ -21,10 +19,6 @@ vi.mock('@/app/components/base/amplitude/utils', () => ({ resetUser: mockResetUser, })) -vi.mock('@/context/provider-context', () => ({ - useProviderContext: vi.fn(), -})) - vi.mock('@/service/use-common', async (importOriginal) => ({ ...(await importOriginal()), useLogout: vi.fn(), @@ -69,7 +63,7 @@ const renderAccountDropdown = () => { )} />, - { queryClient }, + { queryClient, features: { education: { enabled: false } } }, ) } @@ -79,9 +73,6 @@ describe('AccountDropdown', () => { beforeEach(() => { vi.clearAllMocks() mockUseRouter.mockReturnValue({ push: mockPush }) - vi.mocked(useProviderContext).mockReturnValue({ - enableEducationPlan: false, - } as ProviderContextState) vi.mocked(useLogout).mockReturnValue({ mutateAsync: mockLogout, } as unknown as ReturnType) @@ -90,7 +81,10 @@ describe('AccountDropdown', () => { it('includes the visible account name in the main navigation trigger accessible name', () => { const queryClient = createAccountProfileQueryClient(userProfile) - renderWithConsoleQuery(, { queryClient }) + renderWithConsoleQuery(, { + queryClient, + features: { education: { enabled: false } }, + }) expect(screen.getByRole('button', { name: accountMenuAccessibleName })).toBeInTheDocument() }) @@ -98,7 +92,10 @@ describe('AccountDropdown', () => { it('keeps the account identity in the compact trigger accessible name', () => { const queryClient = createAccountProfileQueryClient(userProfile) - renderWithConsoleQuery(, { queryClient }) + renderWithConsoleQuery(, { + queryClient, + features: { education: { enabled: false } }, + }) expect(screen.getByRole('button', { name: accountMenuAccessibleName })).toBeInTheDocument() expect(screen.queryByText('Current User')).not.toBeInTheDocument() @@ -108,7 +105,10 @@ describe('AccountDropdown', () => { const user = userEvent.setup() const queryClient = createAccountProfileQueryClient(userProfile) - renderWithConsoleQuery(, { queryClient }) + renderWithConsoleQuery(, { + queryClient, + features: { education: { enabled: false } }, + }) expect(screen.getByText('Current User')).toBeInTheDocument() diff --git a/web/app/components/header/account-dropdown/main-nav-menu-content.tsx b/web/app/components/header/account-dropdown/main-nav-menu-content.tsx index 12491bb837b..bab433bada1 100644 --- a/web/app/components/header/account-dropdown/main-nav-menu-content.tsx +++ b/web/app/components/header/account-dropdown/main-nav-menu-content.tsx @@ -25,7 +25,6 @@ import { settingsQueryParamName, settingsQueryParser, } from '@/app/components/header/account-setting/query-params' -import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' import Link from '@/next/link' import { consoleQuery } from '@/service/console' @@ -124,10 +123,14 @@ export function MainNavMenuContent({ onLogout }: MainNavMenuContentProps) { ...userProfileQueryOptions(), select: (data) => data.profile, }) - const { enableEducationPlan } = useProviderContext() + const { data: enableEducationPlan } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.education.enabled, + }), + ) const { data: isEducationAccount = false } = useQuery( consoleQuery.account.education.get.queryOptions({ - enabled: enableEducationPlan, + enabled: enableEducationPlan === true, select: ({ is_student }) => is_student ?? false, }), ) diff --git a/web/app/components/header/account-setting/__tests__/index.spec.tsx b/web/app/components/header/account-setting/__tests__/index.spec.tsx index 0c94188a555..bfdebe8331c 100644 --- a/web/app/components/header/account-setting/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/index.spec.tsx @@ -3,24 +3,17 @@ import type { ConsoleStateFixture } from '@/test/console/state-fixture' import { fireEvent, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' -import { baseProviderContextValue, useProviderContext } from '@/context/provider-context' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { renderWithConsoleQuery } from '@/test/console/query-data' import { ACCOUNT_SETTING_TAB } from '../constants' import AccountSetting from '../index' +let canReplaceLogo = true + const mockConsoleState = vi.hoisted(() => ({ current: null as unknown, })) -vi.mock('@/context/provider-context', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useProviderContext: vi.fn(), - } -}) - vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState.current ?? {}) @@ -197,7 +190,10 @@ describe('AccountSetting', () => { } return renderWithConsoleQuery(, { - features: { billing: { subscription: { plan: 'sandbox' } } }, + features: { + billing: { subscription: { plan: 'sandbox' } }, + can_replace_logo: canReplaceLogo, + }, accountProfile: (mockConsoleState.current as ConsoleStateFixture).userProfile, systemFeatures: { deployment_edition: deploymentEdition, @@ -212,11 +208,7 @@ describe('AccountSetting', () => { beforeEach(() => { vi.clearAllMocks() - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - - enableReplaceWebAppLogo: true, - }) + canReplaceLogo = true mockConsoleState.current = baseConsoleState vi.mocked(useBreakpoints).mockReturnValue(MediaType.pc) }) @@ -442,11 +434,7 @@ describe('AccountSetting', () => { it('should hide billing and custom tabs when disabled', () => { // Arrange - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - - enableReplaceWebAppLogo: false, - }) + canReplaceLogo = false // Act renderAccountSetting({ deploymentEdition: 'COMMUNITY' }) diff --git a/web/app/components/header/account-setting/index.tsx b/web/app/components/header/account-setting/index.tsx index 7150689af37..68cbb9933c8 100644 --- a/web/app/components/header/account-setting/index.tsx +++ b/web/app/components/header/account-setting/index.tsx @@ -1,4 +1,5 @@ 'use client' + import type { AccountSettingTab } from '@/app/components/header/account-setting/constants' import { cn } from '@langgenius/dify-ui/cn' import { @@ -8,7 +9,7 @@ import { ScrollAreaThumb, ScrollAreaViewport, } from '@langgenius/dify-ui/scroll-area' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useRef } from 'react' import { useTranslation } from 'react-i18next' @@ -17,13 +18,13 @@ import CustomPage from '@/app/components/custom/custom-page' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import MenuDialog from '@/app/components/header/account-setting/menu-dialog' import { workspacePermissionKeysAtom } from '@/context/permission-state' -import { useProviderContext } from '@/context/provider-context' import { isCurrentWorkspaceDatasetOperatorAtom, isCurrentWorkspaceManagerAtom, } from '@/context/workspace-state' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import { consoleQuery } from '@/service/console' import { hasPermission } from '@/utils/permission' import AccessRulesPage from './access-rules-page' import MembersPage from './members-page' @@ -56,7 +57,11 @@ export default function AccountSetting({ onTabChangeAction, }: IAccountSettingProps) { const { t } = useTranslation() - const { enableReplaceWebAppLogo } = useProviderContext() + const { data: enableReplaceWebAppLogo } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.can_replace_logo, + }), + ) const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom) diff --git a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx index e62f9f47af6..4453362701e 100644 --- a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx @@ -6,8 +6,6 @@ import type { ConsoleStateFixture } from '@/test/console/state-fixture' import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi } from 'vite-plus/test' -import { createMockProviderContextValue } from '@/__mocks__/provider-context' -import { useProviderContext } from '@/context/provider-context' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' import { useMembers } from '@/service/use-common' @@ -31,7 +29,6 @@ vi.mock('@/context/permission-state', async () => { return createPermissionStateModuleMock(() => mockConsoleState.current) }) -vi.mock('@/context/provider-context') vi.mock('@/hooks/use-format-time-from-now') vi.mock('@/service/access-control/use-member-roles') vi.mock('@/service/use-common') @@ -247,11 +244,7 @@ describe('MembersPage', () => { } as unknown as ReturnType) deploymentEdition = 'COMMUNITY' - vi.mocked(useProviderContext).mockReturnValue( - createMockProviderContextValue({ - isAllowTransferWorkspace: true, - }), - ) + memberFeatures = { ...memberFeatures, is_allow_transfer_workspace: true } vi.mocked(useFormatTimeFromNow).mockReturnValue({ formatTimeFromNow: mockFormatTimeFromNow, @@ -334,11 +327,7 @@ describe('MembersPage', () => { it('should show non-interactive owner role when transfer ownership is not allowed', () => { deploymentEdition = 'COMMUNITY' - vi.mocked(useProviderContext).mockReturnValue( - createMockProviderContextValue({ - isAllowTransferWorkspace: false, - }), - ) + memberFeatures = { ...memberFeatures, is_allow_transfer_workspace: false } renderMembersPage() @@ -406,7 +395,6 @@ describe('MembersPage', () => { billing: { subscription: { plan: 'sandbox' } }, members: { size: 2, limit: 5 }, } - vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({})) renderMembersPage() @@ -422,7 +410,6 @@ describe('MembersPage', () => { billing: { subscription: { plan: 'sandbox' } }, members: { size: 2, limit: 0 }, } - vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({})) renderMembersPage() @@ -435,7 +422,6 @@ describe('MembersPage', () => { billing: { subscription: { plan: 'team' } }, members: { size: 2, limit: 50 }, } - vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({})) renderMembersPage() @@ -540,7 +526,6 @@ describe('MembersPage', () => { billing: { subscription: { plan: 'sandbox' } }, members: { size: 2, limit: 5 }, } - vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({})) renderMembersPage() @@ -708,7 +693,6 @@ describe('MembersPage', () => { billing: { subscription: { plan: 'sandbox' } }, members: { size: 2, limit: 2 }, } - vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({})) renderMembersPage() diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index e894c2c52f4..2c9d35f3dce 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -12,7 +12,6 @@ import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar' import UpgradeBtn from '@/app/components/billing/upgrade-btn' import { useLocale } from '@/context/i18n' import { workspacePermissionKeysAtom } from '@/context/permission-state' -import { useProviderContext } from '@/context/provider-context' import { currentWorkspaceAtom, isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' @@ -48,22 +47,24 @@ const MembersPage = () => { MemberInviteResponse['invitation_results'] | null >(null) const accounts = data?.accounts || [] - const { isAllowTransferWorkspace } = useProviderContext() const deploymentEdition = systemFeatures.deployment_edition - const { data: billing } = useQuery( + const { data: features } = useQuery( consoleQuery.features.get.queryOptions({ - enabled: deploymentEdition === 'CLOUD', - select: (data) => ({ plan: data.billing.subscription.plan, members: data.members }), + select: (data) => ({ + plan: data.billing.subscription.plan, + members: data.members, + is_allow_transfer_workspace: data.is_allow_transfer_workspace, + }), }), ) const isNotUnlimitedMemberPlan = - deploymentEdition === 'CLOUD' && billing !== undefined && billing.plan !== 'team' + deploymentEdition === 'CLOUD' && features !== undefined && features.plan !== 'team' // A limit of 0 means unlimited. const isMemberFull = isNotUnlimitedMemberPlan && - billing.members.limit > 0 && - accounts.length >= billing.members.limit + features.members.limit > 0 && + accounts.length >= features.members.limit const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false) const [showTransferOwnershipModal, setShowTransferOwnershipModal] = useState(false) const [detailsMember, setDetailsMember] = useState(null) @@ -151,9 +152,9 @@ const MembersPage = () => {
{accounts.length}
/
- {billing.members.limit === 0 + {features.members.limit === 0 ? t(($) => $['plansCommon.unlimited'], { ns: 'billing' }) - : billing.members.limit} + : features.members.limit}
) : ( @@ -200,7 +201,9 @@ const MembersPage = () => { roles={account.roles} isCurrentUser={userProfileEmail === account.email} canManage={canManageMembers} - canTransferOwnership={isCurrentWorkspaceOwner && isAllowTransferWorkspace} + canTransferOwnership={ + isCurrentWorkspaceOwner && features?.is_allow_transfer_workspace === true + } allowMultipleRoles={systemFeatures.rbac_enabled} onOpenDetails={handleOpenDetails} onTransferOwnership={handleTransferOwnership} diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx index 3284996dddc..d33df01fdea 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx @@ -14,7 +14,10 @@ let mockWorkspacePermissionKeys: string[] = ['plugin.model_config'] function createWrapper(deploymentEdition: 'CLOUD' | 'COMMUNITY' = 'CLOUD') { return createConsoleQueryWrapper({ systemFeatures: { deployment_edition: deploymentEdition }, - features: { billing: { subscription: { plan: mockPlanType } } }, + features: { + billing: { subscription: { plan: mockPlanType } }, + model_load_balancing_enabled: mockModelLoadBalancingEnabled, + }, }).wrapper } @@ -25,10 +28,6 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/context/provider-context', () => ({ - useProviderContextSelector: () => mockModelLoadBalancingEnabled, -})) - vi.mock('@/service/common', () => ({ enableModel: vi.fn(), disableModel: vi.fn(), diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-configs.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-configs.spec.tsx index fe20648bebe..0ad271c99e9 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-configs.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-load-balancing-configs.spec.tsx @@ -15,13 +15,10 @@ import ModelLoadBalancingConfigs from '../model-load-balancing-configs' let mockModelLoadBalancingEnabled = true const render = (ui: React.ReactElement) => - renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } }) - -vi.mock('@/context/provider-context', () => ({ - useProviderContextSelector: ( - selector: (state: { modelLoadBalancingEnabled: boolean }) => boolean, - ) => selector({ modelLoadBalancingEnabled: mockModelLoadBalancingEnabled }), -})) + renderWithConsoleQuery(ui, { + systemFeatures: { deployment_edition: 'CLOUD' }, + features: { model_load_balancing_enabled: mockModelLoadBalancingEnabled }, + }) vi.mock('../cooldown-timer', () => ({ default: ({ diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx index 0abb42477ab..89e8d5a529e 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx @@ -11,7 +11,6 @@ import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' import { workspacePermissionKeysAtom } from '@/context/permission-state' -import { useProviderContextSelector } from '@/context/provider-context' import { deploymentEditionAtom } from '@/features/system-features/state' import { disableModel, enableModel } from '@/service/common' import { consoleQuery } from '@/service/console' @@ -43,15 +42,14 @@ const ModelListItem = ({ }: ModelListItemProps) => { const { t } = useTranslation() const deploymentEdition = useAtomValue(deploymentEditionAtom) - const { data: plan } = useQuery( + const { data: features } = useQuery( consoleQuery.features.get.queryOptions({ - enabled: deploymentEdition === 'CLOUD', - select: (features) => features.billing.subscription.plan, + select: (features) => ({ + plan: features.billing.subscription.plan, + model_load_balancing_enabled: features.model_load_balancing_enabled, + }), }), ) - const modelLoadBalancingEnabled = useProviderContextSelector( - (state) => state.modelLoadBalancingEnabled, - ) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const canConfigureModels = hasPermission(workspacePermissionKeys, 'plugin.model_config') const queryClient = useQueryClient() @@ -129,7 +127,7 @@ const ModelListItem = ({ showFeaturesLabel >
- {modelLoadBalancingEnabled && + {features?.model_load_balancing_enabled && !model.deprecated && model.load_balancing_enabled && !model.has_invalid_load_balancing_configs && ( @@ -138,7 +136,9 @@ const ModelListItem = ({ )} {canConfigureModels && - (deploymentEdition !== 'CLOUD' || modelLoadBalancingEnabled || plan === 'sandbox') && + (deploymentEdition !== 'CLOUD' || + features?.model_load_balancing_enabled || + features?.plan === 'sandbox') && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status) && ( state.modelLoadBalancingEnabled, + const { data: modelLoadBalancingEnabled } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.model_load_balancing_enabled, + }), ) const updateConfigEntry = useCallback( @@ -315,7 +317,7 @@ const ModelLoadBalancingConfigs = ({ )}
- {!modelLoadBalancingEnabled && deploymentEdition === 'CLOUD' && ( + {modelLoadBalancingEnabled === false && deploymentEdition === 'CLOUD' && (
diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index afba8badfe4..e727ea48c4f 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -12,7 +12,6 @@ import type { ReactNode } from 'react' import type { Mock } from 'vite-plus/test' import type { StepByStepTourSessionState } from '@/app/components/step-by-step-tour/types' import type { ModalContextState } from '@/context/modal-context' -import type { ProviderContextState } from '@/context/provider-context' import type { UserProfileWithMeta } from '@/features/account-profile/client' import type { ConsoleStateFixture } from '@/test/console/state-fixture' import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' @@ -30,7 +29,6 @@ import { } from '@/app/components/step-by-step-tour/state' import { STEP_BY_STEP_TOUR_SHELL_MODE_STORAGE_KEY } from '@/app/components/step-by-step-tour/storage' import { useModalContext } from '@/context/modal-context' -import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' import { usePathname, useRouter } from '@/next/navigation' import { consoleQuery } from '@/service/console' @@ -168,11 +166,8 @@ type MainNavConsoleState = ConsoleStateFixture & { const mockConsoleState = vi.hoisted(() => ({ current: undefined as MainNavConsoleState | undefined, })) -const mockProviderContextState = vi.hoisted(() => ({ - current: { - enableSkill: true, - } as Partial, -})) +let skillEnabled = true +let educationEnabled = false vi.mock('@tanstack/react-virtual') @@ -192,12 +187,6 @@ vi.mock('@/context/permission-state', async () => { const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') return createPermissionStateModuleMock(() => mockConsoleState.current ?? {}) }) -vi.mock('@/context/provider-context', () => ({ - useProviderContext: vi.fn(), - useProviderContextSelector: vi.fn((selector: (state: Partial) => unknown) => - selector(mockProviderContextState.current), - ), -})) vi.mock('@/context/modal-context', () => ({ useModalContext: vi.fn(), @@ -603,7 +592,11 @@ const renderMainNav = ( , { systemFeatures: resolvedSystemFeatures, - features: { billing: { subscription: { plan: 'sandbox' } } }, + features: { + billing: { subscription: { plan: 'sandbox' } }, + enable_skill: skillEnabled, + education: { enabled: educationEnabled }, + }, educationStatus: options.educationStatus, workspacePermissionKeys: currentConsoleState.workspacePermissionKeys, queryClient, @@ -651,12 +644,8 @@ describe('MainNav', () => { refresh: vi.fn(), }) mockConsoleState.current = consoleState - mockProviderContextState.current = { - enableSkill: true, - } - ;(useProviderContext as Mock).mockReturnValue({ - enableEducationPlan: false, - } as ProviderContextState) + skillEnabled = true + educationEnabled = false ;(useModalContext as Mock).mockReturnValue({ setShowPricingModal: mockSetShowPricingModal, } as unknown as ModalContextState) @@ -775,9 +764,7 @@ describe('MainNav', () => { }) it('hides the skills entry when skill is disabled', () => { - mockProviderContextState.current = { - enableSkill: false, - } + skillEnabled = false renderMainNav() @@ -831,9 +818,7 @@ describe('MainNav', () => { }) it('shows the user education badge in the account popup without adding the workspace plan there', async () => { - ;(useProviderContext as Mock).mockReturnValue({ - enableEducationPlan: true, - } as ProviderContextState) + educationEnabled = true renderMainNav(defaultMainNavSystemFeatures, { educationStatus: { is_student: true }, diff --git a/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx index aafe967efe5..5419491fbe3 100644 --- a/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx +++ b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx @@ -3,13 +3,11 @@ import type { TenantListItemResponse, } from '@dify/contracts/api/console/workspaces/types.gen' import type { ModalContextState } from '@/context/modal-context' -import type { ProviderContextState } from '@/context/provider-context' import { zLicenseStatus } from '@dify/contracts/api/console/system-features/zod.gen' import { fireEvent, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { useModalContext } from '@/context/modal-context' -import { useProviderContext } from '@/context/provider-context' import { consoleQuery } from '@/service/console' import { createConsoleQueryClient, @@ -35,10 +33,6 @@ const mockConsoleState = vi.hoisted(() => ({ }, })) -vi.mock('@/context/provider-context', () => ({ - useProviderContext: vi.fn(), -})) - vi.mock('@/context/permission-state', async () => { const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') return createPermissionStateModuleMock(() => mockConsoleState.current) @@ -176,9 +170,6 @@ describe('WorkspaceCard', () => { mockFetchWorkspaces.mockResolvedValue({ workspaces: mockWorkspaces }) mockSwitchWorkspace.mockReturnValue(new Promise(() => {})) mockCurrentWorkspaceQuery() - vi.mocked(useProviderContext).mockReturnValue({ - enableEducationPlan: false, - } as ProviderContextState) mockWorkspacePermissionKeys(['workspace.member.manage']) vi.mocked(useModalContext).mockReturnValue({ setShowPricingModal: mockSetShowPricingModal, @@ -340,9 +331,6 @@ describe('WorkspaceCard', () => { ...currentWorkspaceValue, plan: 'team', }) - vi.mocked(useProviderContext).mockReturnValue({ - enableEducationPlan: false, - } as ProviderContextState) renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } }) expect(screen.getByText('team')).toBeInTheDocument() diff --git a/web/app/components/main-nav/index.tsx b/web/app/components/main-nav/index.tsx index fffccdcc31f..a2c59cbe291 100644 --- a/web/app/components/main-nav/index.tsx +++ b/web/app/components/main-nav/index.tsx @@ -2,7 +2,7 @@ import type { MainNavItem, MainNavProps } from './types' import { cn } from '@langgenius/dify-ui/cn' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' @@ -10,7 +10,6 @@ import Badge from '@/app/components/base/badge' import { DifyLogo } from '@/app/components/base/logo/dify-logo' import EnvNav from '@/app/components/header/env-nav' import StepByStepTourMount from '@/app/components/step-by-step-tour/mount' -import { useProviderContextSelector } from '@/context/provider-context' import { isCurrentWorkspaceDatasetOperatorAtom } from '@/context/workspace-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag' @@ -20,6 +19,7 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client' import dynamic from '@/next/dynamic' import Link from '@/next/link' import { usePathname } from '@/next/navigation' +import { consoleQuery } from '@/service/console' import AccountSection from './components/account-section' import HelpMenu from './components/help-menu' import MainNavLink from './components/nav-link' @@ -41,7 +41,11 @@ export function MainNav({ className }: MainNavProps) { const agentV2Enabled = isAgentV2Enabled() const canManageAgents = useCanManageAgents() const canViewSkills = useCanViewSkills() - const enableSkill = useProviderContextSelector((state) => state.enableSkill) + const { data: enableSkill } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.enable_skill, + }), + ) const showEnvTag = currentEnv === 'TESTING' || currentEnv === 'DEVELOPMENT' const helpMenuTriggerRef = useRef(null) @@ -54,7 +58,7 @@ export function MainNav({ className }: MainNavProps) { canViewSkills, isCurrentWorkspaceDatasetOperator, marketplaceEnabled: systemFeatures.enable_marketplace, - skillEnabled: enableSkill, + skillEnabled: enableSkill === true, }), ).map((route) => ({ href: route.href, diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx index e57afa364d1..99b3ea417ab 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx @@ -4,7 +4,7 @@ import { act, fireEvent, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { seedAccountProfileQuery } from '@/test/console/account-profile' -import { seedSystemFeatures } from '@/test/console/query-data' +import { seedFeatures, seedSystemFeatures } from '@/test/console/query-data' import { render } from '@/test/console/render' import Publisher from '../index' import { Popup } from '../popup' @@ -126,20 +126,7 @@ vi.mock('@/context/modal-context', () => ({ ): T => selector({ setShowPricingModal: mockSetShowPricingModal }), })) -const mockIsAllowPublishAsCustomKnowledgePipelineTemplate = vi.fn(() => true) -vi.mock('@/context/provider-context', () => ({ - useProviderContext: () => ({ - isAllowPublishAsCustomKnowledgePipelineTemplate: - mockIsAllowPublishAsCustomKnowledgePipelineTemplate(), - }), - useProviderContextSelector: ( - selector: (s: { isAllowPublishAsCustomKnowledgePipelineTemplate: boolean }) => T, - ): T => - selector({ - isAllowPublishAsCustomKnowledgePipelineTemplate: - mockIsAllowPublishAsCustomKnowledgePipelineTemplate(), - }), -})) +let publishEnabled = true const toastMocks = vi.hoisted(() => ({ call: vi.fn(), @@ -246,6 +233,7 @@ const createQueryClient = () => defaultOptions: { queries: { retry: false, + staleTime: Infinity, }, }, }) @@ -254,6 +242,7 @@ const renderWithQueryClient = (ui: React.ReactElement) => { const queryClient = createQueryClient() seedAccountProfileQuery(queryClient, { id: 'user-1' }) seedSystemFeatures(queryClient, { deployment_edition: 'CLOUD' }) + seedFeatures(queryClient, { knowledge_pipeline: { publish_enabled: publishEnabled } }) return render({ui}) } @@ -265,7 +254,7 @@ describe('publisher', () => { mockPublishedAt.mockReturnValue(null) mockDraftUpdatedAt.mockReturnValue(1700000000) mockPipelineId.mockReturnValue('test-pipeline-id') - mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) + publishEnabled = true mockHandleCheckBeforePublish.mockResolvedValue(true) mockDatasetPermissionKeys = ['dataset.acl.use'] mockDatasetMaintainer = undefined @@ -360,7 +349,7 @@ describe('publisher', () => { it('should close the outer popover before opening publish-as follow-up flow', async () => { mockPublishedAt.mockReturnValue(1700000000) - mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false) + publishEnabled = false renderWithQueryClient() fireEvent.click(screen.getByText('workflow.common.publish')) @@ -429,7 +418,7 @@ describe('publisher', () => { it('should show premium badge when publish as template is not allowed', () => { mockPublishedAt.mockReturnValue(1700000000) - mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false) + publishEnabled = false renderWithQueryClient() @@ -438,7 +427,7 @@ describe('publisher', () => { it('should not show premium badge when publish as template is allowed', () => { mockPublishedAt.mockReturnValue(1700000000) - mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) + publishEnabled = true renderWithQueryClient() @@ -500,7 +489,7 @@ describe('publisher', () => { it('should show pricing modal when publish as template is clicked without permission', async () => { mockPublishedAt.mockReturnValue(1700000000) - mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false) + publishEnabled = false renderWithQueryClient() const publishAsButton = screen @@ -513,7 +502,7 @@ describe('publisher', () => { it('should show publish as knowledge pipeline modal when permitted', async () => { mockPublishedAt.mockReturnValue(1700000000) - mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) + publishEnabled = true renderWithQueryClient() fireEvent.click(screen.getByText('workflow.common.publish')) @@ -530,7 +519,7 @@ describe('publisher', () => { it('should close publish as knowledge pipeline modal when cancel is clicked', async () => { mockPublishedAt.mockReturnValue(1700000000) - mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) + publishEnabled = true renderWithQueryClient() fireEvent.click(screen.getByText('workflow.common.publish')) @@ -851,7 +840,7 @@ describe('publisher', () => { describe('Prop Variations', () => { it('should display correct width when permission is allowed', () => { - mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) + publishEnabled = true const { container } = renderWithQueryClient() const popupDiv = container.firstChild as HTMLElement @@ -859,7 +848,7 @@ describe('publisher', () => { }) it('should display correct width when permission is not allowed', () => { - mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false) + publishEnabled = false const { container } = renderWithQueryClient() const popupDiv = container.firstChild as HTMLElement diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx index 88d1aefb459..0a7c4dfabd0 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx @@ -4,9 +4,12 @@ import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' import { Popup } from '../popup' +let mockIsAllowPublishAsCustom = true + const render = (ui: React.ReactElement) => { const { wrapper } = createConsoleQueryWrapper({ systemFeatures: { deployment_edition: 'CLOUD' }, + features: { knowledge_pipeline: { publish_enabled: mockIsAllowPublishAsCustom } }, }) return renderWithConsoleState(ui, { wrapper }) } @@ -50,7 +53,6 @@ const mockInvalidCustomizedTemplateList = vi.fn() let mockPublishedAt: string | undefined = '2024-01-01T00:00:00Z' let mockDraftUpdatedAt: string | undefined = '2024-06-01T00:00:00Z' let mockPipelineId: string | undefined = 'pipeline-123' -let mockIsAllowPublishAsCustom = true let mockDatasetPermissionKeys = ['dataset.acl.use'] let mockDatasetMaintainer: string | undefined let mockCurrentUserId = 'user-1' @@ -154,10 +156,6 @@ vi.mock('@/context/modal-context', () => ({ ) => selector({ setShowPricingModal: mockSetShowPricingModal }), })) -vi.mock('@/context/provider-context', () => ({ - useProviderContextSelector: () => mockIsAllowPublishAsCustom, -})) - vi.mock('@/hooks/use-api-access-url', () => ({ useDatasetApiAccessUrl: () => '/api/datasets/ds-123', })) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx index 73158b9790a..48321d393ea 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx @@ -14,7 +14,7 @@ import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { toast } from '@langgenius/dify-ui/toast' import { RiArrowRightUpLine, RiPlayCircleLine, RiTerminalBoxLine } from '@remixicon/react' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import { useAtomValue } from 'jotai' import { useCallback, useState } from 'react' @@ -31,13 +31,13 @@ import { workspacePermissionKeysAtom, workspacePermissionKeysLoadingAtom, } from '@/context/permission-state' -import { useProviderContextSelector } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import Link from '@/next/link' import { useParams } from '@/next/navigation' +import { consoleQuery } from '@/service/console' import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' import { useInvalid } from '@/service/use-base' import { publishedPipelineInfoQueryKeyPrefix } from '@/service/use-pipeline' @@ -84,8 +84,10 @@ export function Popup({ const { handleCheckBeforePublish } = useChecklistBeforePublish() const { mutateAsync: publishWorkflow } = usePublishWorkflow() const workflowStore = useWorkflowStore() - const isAllowPublishAsCustomKnowledgePipelineTemplate = useProviderContextSelector( - (s) => s.isAllowPublishAsCustomKnowledgePipelineTemplate, + const { data: isAllowPublishAsCustomKnowledgePipelineTemplate } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.knowledge_pipeline.publish_enabled, + }), ) const setShowPricingModal = useModalContextSelector((s) => s.setShowPricingModal) const apiReferenceUrl = useDatasetApiAccessUrl() @@ -195,6 +197,8 @@ export function Popup({ preventDefault: true, }) const handleClickPublishAsKnowledgePipeline = useCallback(() => { + if (isAllowPublishAsCustomKnowledgePipelineTemplate === undefined) return + onRequestClose?.() if (!isAllowPublishAsCustomKnowledgePipelineTemplate) { if (deploymentEdition === 'CLOUD') setShowPricingModal() @@ -318,7 +322,11 @@ export function Popup({ className="w-full hover:bg-state-accent-hover hover:text-text-accent" variant="tertiary" onClick={handleClickPublishAsKnowledgePipeline} - disabled={!publishedAt || isPublishingAsCustomizedPipeline} + disabled={ + isAllowPublishAsCustomKnowledgePipelineTemplate === undefined || + !publishedAt || + isPublishingAsCustomizedPipeline + } >
@@ -328,17 +336,18 @@ export function Popup({ > {t(($) => $['common.publishAs'], { ns: 'pipeline' })} - {deploymentEdition === 'CLOUD' && !isAllowPublishAsCustomKnowledgePipelineTemplate && ( - - - )} + {deploymentEdition === 'CLOUD' && + isAllowPublishAsCustomKnowledgePipelineTemplate === false && ( + + + )}
diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/method-selector.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/method-selector.spec.tsx index c0afaeafb98..61174895021 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/method-selector.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/__tests__/method-selector.spec.tsx @@ -5,12 +5,16 @@ import { renderWithConsoleQuery } from '@/test/console/query-data' import { DeliveryMethodType } from '../../../types' import MethodSelector from '../method-selector' +let emailDeliveryEnabled = true + const render = (ui: React.ReactElement) => - renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } }) + renderWithConsoleQuery(ui, { + systemFeatures: { deployment_edition: 'CLOUD' }, + features: { human_input_email_delivery_enabled: emailDeliveryEnabled }, + }) const mockUuid = vi.hoisted(() => vi.fn()) const mockUseWorkflowNodes = vi.hoisted(() => vi.fn()) -const mockUseProviderContextSelector = vi.hoisted(() => vi.fn()) vi.mock('uuid', () => ({ v4: () => mockUuid(), @@ -21,12 +25,6 @@ vi.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({ default: () => mockUseWorkflowNodes(), })) -vi.mock('@/context/provider-context', () => ({ - useProviderContextSelector: ( - selector: (state: { humanInputEmailDeliveryEnabled: boolean }) => boolean, - ) => mockUseProviderContextSelector(selector), -})) - describe('human-input/delivery-method/method-selector', () => { beforeEach(() => { vi.clearAllMocks() @@ -37,11 +35,7 @@ describe('human-input/delivery-method/method-selector', () => { data: { type: BlockEnum.Start }, }, ] as Node[]) - mockUseProviderContextSelector.mockImplementation((selector) => - selector({ - humanInputEmailDeliveryEnabled: true, - }), - ) + emailDeliveryEnabled = true }) it('should add webapp and email delivery methods when both entries are available', () => { @@ -114,11 +108,7 @@ describe('human-input/delivery-method/method-selector', () => { data: { type: BlockEnum.TriggerSchedule }, }, ] as Node[]) - mockUseProviderContextSelector.mockImplementation((selector) => - selector({ - humanInputEmailDeliveryEnabled: false, - }), - ) + emailDeliveryEnabled = false render() diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/method-selector.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/method-selector.tsx index da39288ed53..9fc9c72b033 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/method-selector.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/method-selector.tsx @@ -1,18 +1,19 @@ 'use client' + import type { FC } from 'react' import type { DeliveryMethod } from '../../types' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { memo, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { v4 as uuid4 } from 'uuid' import Badge from '@/app/components/base/badge' import useWorkflowNodes from '@/app/components/workflow/store/workflow/use-nodes' import { isTriggerWorkflow } from '@/app/components/workflow/utils/workflow-entry' -import { useProviderContextSelector } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' +import { consoleQuery } from '@/service/console' import { DeliveryMethodType } from '../../types' const i18nPrefix = 'nodes.humanInput' @@ -30,8 +31,10 @@ const MethodSelector: FC = ({ data, onAdd, onShowUpgradeTip select: ({ deployment_edition }) => deployment_edition, }) const [open, setOpen] = useState(false) - const humanInputEmailDeliveryEnabled = useProviderContextSelector( - (s) => s.humanInputEmailDeliveryEnabled, + const { data: humanInputEmailDeliveryEnabled } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.human_input_email_delivery_enabled, + }), ) const nodes = useWorkflowNodes() @@ -46,7 +49,7 @@ const MethodSelector: FC = ({ data, onAdd, onShowUpgradeTip const emailDeliveryInfo = useMemo(() => { return { - noPermission: !humanInputEmailDeliveryEnabled, + noPermission: humanInputEmailDeliveryEnabled === false, added: data.some((method) => method.type === DeliveryMethodType.Email), } }, [data, humanInputEmailDeliveryEnabled]) @@ -124,9 +127,11 @@ const MethodSelector: FC = ({ data, onAdd, onShowUpgradeTip
{ + if (humanInputEmailDeliveryEnabled === undefined) return if (emailDeliveryInfo.noPermission) { onShowUpgradeTip() return diff --git a/web/app/education/expire-notice/__tests__/index.spec.tsx b/web/app/education/expire-notice/__tests__/index.spec.tsx index 235fd56ebec..5cdc338c779 100644 --- a/web/app/education/expire-notice/__tests__/index.spec.tsx +++ b/web/app/education/expire-notice/__tests__/index.spec.tsx @@ -11,12 +11,6 @@ const mockEducationStatus = vi.hoisted(() => ({ })) const mockPricingModal = vi.hoisted(() => ({ isOpen: false })) -vi.mock('@/context/provider-context', () => ({ - useProviderContext: () => ({ - enableEducationPlan: true, - }), -})) - vi.mock('@/hooks/use-query-params', () => ({ usePricingModal: () => [mockPricingModal.isOpen, vi.fn()], })) @@ -37,6 +31,7 @@ vi.mock('@/next/dynamic', () => ({ const renderNotice = (accountId = 'user-1') => { const { wrapper } = createConsoleQueryWrapper({ accountProfile: { id: accountId, timezone: 'UTC' }, + features: { education: { enabled: true } }, educationStatus: { allow_refresh: mockEducationStatus.allowRefresh, expire_at: mockEducationStatus.expireAt, diff --git a/web/app/education/expire-notice/use-expire-notice.ts b/web/app/education/expire-notice/use-expire-notice.ts index aacd3ee9829..dad184b8746 100644 --- a/web/app/education/expire-notice/use-expire-notice.ts +++ b/web/app/education/expire-notice/use-expire-notice.ts @@ -6,7 +6,6 @@ import { useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' -import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' import { consoleQuery } from '@/service/console' import { useDismissedEducationExpireNotice } from './storage' @@ -74,10 +73,14 @@ export function useEducationExpireNotice() { timezone: profile.timezone ?? undefined, }), }) - const { enableEducationPlan } = useProviderContext() + const { data: enableEducationPlan } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.education.enabled, + }), + ) const { data: educationStatus, isLoading: isLoadingEducationStatus } = useQuery( consoleQuery.account.education.get.queryOptions({ - enabled: enableEducationPlan, + enabled: enableEducationPlan === true, select: selectEducationExpireStatus, }), ) diff --git a/web/context/provider-context-provider.tsx b/web/context/provider-context-provider.tsx index c1cfc953089..960c8ea5155 100644 --- a/web/context/provider-context-provider.tsx +++ b/web/context/provider-context-provider.tsx @@ -16,7 +16,6 @@ type ProviderContextProviderProps = { export const ProviderContextProvider = ({ children }: ProviderContextProviderProps) => { const queryClient = useQueryClient() - const featuresQuery = useQuery(consoleQuery.features.get.queryOptions()) const { data: providersData, isLoading: isLoadingModelProviders, @@ -24,16 +23,6 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro } = useQuery(consoleQuery.workspaces.current.modelProviders.summary.get.queryOptions()) const { data: textGenerationModelList } = useModelListByType(ModelTypeEnum.textGeneration) - const features = featuresQuery.data - const enableEducationPlan = features?.education.enabled ?? false - const enableSkill = features?.enable_skill ?? false - const enableReplaceWebAppLogo = features?.can_replace_logo ?? false - const modelLoadBalancingEnabled = features?.model_load_balancing_enabled ?? false - const isAllowTransferWorkspace = features?.is_allow_transfer_workspace ?? false - const isAllowPublishAsCustomKnowledgePipelineTemplate = - features?.knowledge_pipeline.publish_enabled ?? false - const humanInputEmailDeliveryEnabled = features?.human_input_email_delivery_enabled ?? false - const refreshModelProviders = () => Promise.all([ queryClient.invalidateQueries({ @@ -54,13 +43,6 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro isAPIKeySet: !!textGenerationModelList?.data?.some( (model) => model.status === ModelStatusEnum.active, ), - enableSkill, - enableReplaceWebAppLogo, - modelLoadBalancingEnabled, - enableEducationPlan, - isAllowTransferWorkspace, - isAllowPublishAsCustomKnowledgePipelineTemplate, - humanInputEmailDeliveryEnabled, }} > {children} diff --git a/web/context/provider-context.ts b/web/context/provider-context.ts index da2a8523482..89203abd09b 100644 --- a/web/context/provider-context.ts +++ b/web/context/provider-context.ts @@ -15,16 +15,9 @@ export type ProviderContextState = { refreshModelProviders: () => Promise textGenerationModelList: Model[] isAPIKeySet: boolean - enableSkill: boolean - enableReplaceWebAppLogo: boolean - modelLoadBalancingEnabled: boolean - enableEducationPlan: boolean - isAllowTransferWorkspace: boolean - isAllowPublishAsCustomKnowledgePipelineTemplate: boolean - humanInputEmailDeliveryEnabled: boolean } -export const baseProviderContextValue: ProviderContextState = { +const baseProviderContextValue: ProviderContextState = { modelProviders: [], modelProviderPlugins: {}, isLoadingModelProviders: false, @@ -32,13 +25,6 @@ export const baseProviderContextValue: ProviderContextState = { refreshModelProviders: async () => {}, textGenerationModelList: [], isAPIKeySet: true, - enableSkill: false, - enableReplaceWebAppLogo: false, - modelLoadBalancingEnabled: false, - enableEducationPlan: false, - isAllowTransferWorkspace: false, - isAllowPublishAsCustomKnowledgePipelineTemplate: false, - humanInputEmailDeliveryEnabled: false, } export const ProviderContext = createContext(baseProviderContextValue) diff --git a/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx index 48fde00309c..ff477edfc54 100644 --- a/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx @@ -10,11 +10,17 @@ import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store import { agentComposerKnowledgeRetrievalsAtom } from '@/features/agent-v2/agent-composer/store-modules/knowledge' import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt' import { agentComposerToolsAtom } from '@/features/agent-v2/agent-composer/store-modules/tools' -import { render } from '@/test/console/render' +import { createConsoleQueryWrapper } from '@/test/console/query-data' +import { render as renderWithState } from '@/test/console/render' import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture' import { AgentPromptEditor } from '../orchestrate/prompt-editor' import { AgentPromptSlashMenu } from '../orchestrate/prompt-editor/slash' +const render = (ui: React.ReactElement) => { + const { wrapper } = createConsoleQueryWrapper({ features: { enable_skill: true } }) + return renderWithState(ui, { wrapper }) +} + const mockPromptEditor = vi.hoisted(() => vi.fn()) const mockCopy = vi.hoisted(() => vi.fn()) const mockReset = vi.hoisted(() => vi.fn()) @@ -199,11 +205,6 @@ vi.mock('@/context/workspace-state', async () => { })) }) -vi.mock('@/context/provider-context', () => ({ - useProviderContextSelector: (selector: (state: { enableSkill: boolean }) => unknown) => - selector({ enableSkill: true }), -})) - vi.mock('@/service/use-tools', () => ({ useAllBuiltInTools: () => ({ data: mockBuiltInTools }), useAllCustomTools: () => ({ data: [] }), diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/config-context.ts b/web/features/agent-v2/agent-detail/configure/components/orchestrate/config-context.ts index 181f6a9f4c4..fcfe293ab6e 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/config-context.ts +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/config-context.ts @@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { createContext, use } from 'react' -import { useProviderContextSelector } from '@/context/provider-context' import { agentComposerFilesAtom } from '@/features/agent-v2/agent-composer/store-modules/files' import { agentComposerSkillsAtom } from '@/features/agent-v2/agent-composer/store-modules/skills' import { consoleQuery } from '@/service/console' @@ -41,7 +40,11 @@ export const useAgentConfigSkills = () => { export const useAgentWorkspaceSkillBindings = () => { const { agentId } = useAgentConfigApiContext() - const enableSkill = useProviderContextSelector((state) => state.enableSkill) + const { data: enableSkill } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.enable_skill, + }), + ) return useQuery({ ...consoleQuery.workspaces.current.agents.byAgentId.skills.get.queryOptions({ @@ -51,7 +54,7 @@ export const useAgentWorkspaceSkillBindings = () => { }, }, }), - enabled: enableSkill, + enabled: enableSkill === true, }) } diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx index c77e9d85e31..c463fcbe942 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx @@ -21,6 +21,7 @@ import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' +import { useQuery } from '@tanstack/react-query' import { useClipboard } from 'foxact/use-clipboard' import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { @@ -38,7 +39,6 @@ import { Infotip } from '@/app/components/base/infotip' import PromptEditor from '@/app/components/base/prompt-editor' import BlockIcon from '@/app/components/workflow/block-icon' import { BlockEnum } from '@/app/components/workflow/types' -import { useProviderContextSelector } from '@/context/provider-context' import { agentComposerKnowledgeRetrievalsAtom } from '@/features/agent-v2/agent-composer/store-modules/knowledge' import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt' import { @@ -49,6 +49,7 @@ import { ENABLE_AGENT_CLI_TOOLS, ENABLE_AGENT_KNOWLEDGE_RETRIEVAL, } from '@/features/agent-v2/agent-detail/configure/feature-flags' +import { consoleQuery } from '@/service/console' import { useAgentOrchestrateAddActions } from '../add-actions-context' import { AgentConfigureTipContent } from '../common/tip-content' import { @@ -422,7 +423,11 @@ function AgentPromptSelectionBridge({ export function AgentPromptEditor() { const { t } = useTranslation('agentV2') const readOnly = useAgentOrchestrateReadOnly() - const enableSkill = useProviderContextSelector((state) => state.enableSkill) + const { data: enableSkill } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.enable_skill, + }), + ) const [value, setValue] = useAtom(agentComposerPromptAtom) const { skills: embeddedSkills } = useAgentConfigSkills() const workspaceSkillBindingsQuery = useAgentWorkspaceSkillBindings() @@ -1060,7 +1065,7 @@ export function AgentPromptEditor() { onAddFile={addActions.files} onAddKnowledge={addActions.knowledge} onAddSkill={addActions.skills} - canAddWorkspaceSkill={enableSkill} + canAddWorkspaceSkill={enableSkill === true} knowledgeRetrievals={retrievals} onBack={returnToSlashMenuMain} onOpenCategory={handleOpenSlashMenuCategory} diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx index 7c2c62d4128..debcb886f34 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx @@ -12,6 +12,7 @@ import { formStateToAgentSoulConfig } from '@/features/agent-v2/agent-composer/c import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider' import { agentComposerDraftAtom } from '@/features/agent-v2/agent-composer/store' +import { seedFeatures } from '@/test/console/query-data' import { AgentOrchestrateAddActionsProvider } from '../../add-actions' import { useAgentOrchestrateAddActions } from '../../add-actions-context' import { AgentConfigApiContextProvider } from '../../config-context' @@ -81,9 +82,7 @@ const mocks = vi.hoisted(() => ({ fileUploadConfig: { skill_file_size_limit: 64, }, - providerContext: { - enableSkill: true, - }, + skillEnabled: true, })) vi.mock('@langgenius/dify-ui/toast', () => ({ @@ -115,13 +114,9 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/context/provider-context', () => ({ - useProviderContextSelector: (selector: (state: { enableSkill: boolean }) => unknown) => - selector(mocks.providerContext), -})) - -vi.mock('@/service/console', () => ({ +vi.mock('@/service/console', async (importOriginal) => ({ consoleQuery: { + features: (await importOriginal()).consoleQuery.features, tags: { get: { queryOptions: mocks.workspaceSkillTagsQueryOptions, @@ -224,7 +219,9 @@ vi.mock('@/service/console', () => ({ queryOptions: mocks.agentSkillBindingsQueryOptions, }, put: { - mutationOptions: () => ({ mutationFn: mocks.replaceAgentSkillBindingsMutationFn }), + mutationOptions: () => ({ + mutationFn: mocks.replaceAgentSkillBindingsMutationFn, + }), }, }, }, @@ -328,6 +325,8 @@ function renderAgentSkills({ }, }) + seedFeatures(queryClient, { enable_skill: mocks.skillEnabled }) + return { ...render( @@ -353,7 +352,7 @@ function renderAgentSkills({ describe('AgentSkills', () => { beforeEach(() => { vi.clearAllMocks() - mocks.providerContext.enableSkill = true + mocks.skillEnabled = true mocks.fileUploadConfig.skill_file_size_limit = 64 vi.stubGlobal('fetch', mocks.fetch) document.cookie = 'csrf_token=csrf-token; path=/' @@ -790,7 +789,7 @@ describe('AgentSkills', () => { it('should hide workspace skill selection when skill is disabled', async () => { const user = userEvent.setup() - mocks.providerContext.enableSkill = false + mocks.skillEnabled = false renderAgentSkills({ initialDraft: defaultAgentSoulConfigFormState }) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx index 4d2135e2412..7e431b8cb2b 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx @@ -31,7 +31,6 @@ import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' import { SkeletonRectangle } from '@/app/components/base/skeleton' -import { useProviderContextSelector } from '@/context/provider-context' import { agentComposerSkillsAtom, removeAgentSkillAtom, @@ -467,7 +466,11 @@ export function AgentSkills() { const [isUploadOpen, setIsUploadOpen] = useState(false) const promptAddCallbackRef = useRef(undefined) const apiContext = useAgentConfigApiContext() - const enableSkill = useProviderContextSelector((state) => state.enableSkill) + const { data: enableSkill } = useQuery( + consoleQuery.features.get.queryOptions({ + select: (features) => features.enable_skill, + }), + ) const skills = useAtomValue(agentComposerSkillsAtom) const upsertAgentSkill = useSetAtom(upsertAgentSkillAtom) const removeAgentSkill = useSetAtom(removeAgentSkillAtom) @@ -487,7 +490,7 @@ export function AgentSkills() { }) const agentSkillBindingsQuery = useQuery({ ...agentSkillBindingsQueryOptions, - enabled: enableSkill, + enabled: enableSkill === true, }) const hasLoadedAgentSkillBindings = agentSkillBindingsQuery.data !== undefined const { isPending: isReplacingAgentSkillBindings, mutate: replaceAgentSkillBindings } =