refactor(web): move provider feature flags to query consumers (#41937)

This commit is contained in:
yyh 2026-09-08 02:56:31 +00:00 committed by GitHub
parent d43a23d8f5
commit e26dcf55a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 252 additions and 327 deletions

View File

@ -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 = (

View File

@ -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,
}),
)

View File

@ -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,
}),
)

View File

@ -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 = {

View File

@ -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<typeof import('@tanstack/react-query')>()
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<ProviderContextState>) => 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(() => {

View File

@ -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)

View File

@ -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<typeof import('@/service/use-common')>()),
useLogout: vi.fn(),
@ -69,7 +63,7 @@ const renderAccountDropdown = () => {
</button>
)}
/>,
{ 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<typeof useLogout>)
@ -90,7 +81,10 @@ describe('AccountDropdown', () => {
it('includes the visible account name in the main navigation trigger accessible name', () => {
const queryClient = createAccountProfileQueryClient(userProfile)
renderWithConsoleQuery(<AccountSection />, { queryClient })
renderWithConsoleQuery(<AccountSection />, {
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(<AccountSection compact />, { queryClient })
renderWithConsoleQuery(<AccountSection compact />, {
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(<AccountSection />, { queryClient })
renderWithConsoleQuery(<AccountSection />, {
queryClient,
features: { education: { enabled: false } },
})
expect(screen.getByText('Current User')).toBeInTheDocument()

View File

@ -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,
}),
)

View File

@ -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<typeof import('@/context/provider-context')>()
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(<StatefulAccountSetting />, {
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' })

View File

@ -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)

View File

@ -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<typeof useUpdateRolesOfMember>)
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()

View File

@ -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<Member | null>(null)
@ -151,9 +152,9 @@ const MembersPage = () => {
<div className="">{accounts.length}</div>
<div>/</div>
<div>
{billing.members.limit === 0
{features.members.limit === 0
? t(($) => $['plansCommon.unlimited'], { ns: 'billing' })
: billing.members.limit}
: features.members.limit}
</div>
</div>
) : (
@ -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}

View File

@ -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(),

View File

@ -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: ({

View File

@ -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
></ModelName>
<div className="flex shrink-0 items-center">
{modelLoadBalancingEnabled &&
{features?.model_load_balancing_enabled &&
!model.deprecated &&
model.load_balancing_enabled &&
!model.has_invalid_load_balancing_configs && (
@ -138,7 +136,9 @@ const ModelListItem = ({
</Badge>
)}
{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) && (
<ConfigModel

View File

@ -12,7 +12,7 @@ import { cn } from '@langgenius/dify-ui/cn'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { Switch } from '@langgenius/dify-ui/switch'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge/index'
@ -21,8 +21,8 @@ import { Infotip } from '@/app/components/base/infotip'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import s from '@/app/components/custom/style.module.css'
import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import { useProviderContextSelector } from '@/context/provider-context'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/console'
import { ConfigurationMethodEnum } from '../declarations'
import CooldownTimer from './cooldown-timer'
@ -60,8 +60,10 @@ const ModelLoadBalancingConfigs = ({
})
const providerFormSchemaPredefined =
configurationMethod === ConfigurationMethodEnum.predefinedModel
const modelLoadBalancingEnabled = useProviderContextSelector(
(state) => 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 = ({
)}
</div>
{!modelLoadBalancingEnabled && deploymentEdition === 'CLOUD' && (
{modelLoadBalancingEnabled === false && deploymentEdition === 'CLOUD' && (
<GridMask canvasClassName="rounded-xl!">
<div className="mt-2 flex h-14 items-center justify-between rounded-xl border-[0.5px] border-components-panel-border px-4 shadow-md">
<div className={cn('text-gradient text-sm/tight font-semibold', s.textGradient)}>

View File

@ -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<ProviderContextState>,
}))
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<ProviderContextState>) => unknown) =>
selector(mockProviderContextState.current),
),
}))
vi.mock('@/context/modal-context', () => ({
useModalContext: vi.fn(),
@ -603,7 +592,11 @@ const renderMainNav = (
</JotaiProvider>,
{
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 },

View File

@ -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()

View File

@ -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<HTMLButtonElement>(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,

View File

@ -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: <T,>(
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(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>)
}
@ -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(<Publisher />)
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(<Popup />)
@ -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(<Popup />)
@ -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(<Popup />)
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(<Publisher />)
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(<Publisher />)
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(<Popup />)
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(<Popup />)
const popupDiv = container.firstChild as HTMLElement

View File

@ -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',
}))

View File

@ -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
}
>
<div className="flex grow items-center gap-x-2 overflow-hidden">
<span aria-hidden className="i-custom-vender-pipeline-pipeline-line size-4 shrink-0" />
@ -328,17 +336,18 @@ export function Popup({
>
{t(($) => $['common.publishAs'], { ns: 'pipeline' })}
</span>
{deploymentEdition === 'CLOUD' && !isAllowPublishAsCustomKnowledgePipelineTemplate && (
<PremiumBadge className="shrink-0 select-none" size="s" color="indigo">
<SparklesSoft
aria-hidden="true"
className="flex size-3 items-center text-components-premium-badge-indigo-text-stop-0"
/>
<span className="p-0.5 system-2xs-medium">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</PremiumBadge>
)}
{deploymentEdition === 'CLOUD' &&
isAllowPublishAsCustomKnowledgePipelineTemplate === false && (
<PremiumBadge className="shrink-0 select-none" size="s" color="indigo">
<SparklesSoft
aria-hidden="true"
className="flex size-3 items-center text-components-premium-badge-indigo-text-stop-0"
/>
<span className="p-0.5 system-2xs-medium">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</PremiumBadge>
)}
</div>
</Button>
</div>

View File

@ -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(<MethodSelector data={[]} onAdd={handleAdd} onShowUpgradeTip={handleShowUpgradeTip} />)

View File

@ -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<MethodSelectorProps> = ({ 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<MethodSelectorProps> = ({ 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<MethodSelectorProps> = ({ data, onAdd, onShowUpgradeTip
<div
className={cn(
'relative flex cursor-pointer items-center gap-1 rounded-lg p-1 pl-3 hover:bg-state-base-hover',
emailDeliveryInfo.added && 'cursor-not-allowed bg-transparent hover:bg-transparent',
(emailDeliveryInfo.added || humanInputEmailDeliveryEnabled === undefined) &&
'cursor-not-allowed bg-transparent hover:bg-transparent',
)}
onClick={() => {
if (humanInputEmailDeliveryEnabled === undefined) return
if (emailDeliveryInfo.noPermission) {
onShowUpgradeTip()
return

View File

@ -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,

View File

@ -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,
}),
)

View File

@ -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}

View File

@ -15,16 +15,9 @@ export type ProviderContextState = {
refreshModelProviders: () => Promise<void>
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<ProviderContextState>(baseProviderContextValue)

View File

@ -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: [] }),

View File

@ -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,
})
}

View File

@ -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}

View File

@ -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<typeof import('@/service/console')>()).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(
<QueryClientProvider client={queryClient}>
@ -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 })

View File

@ -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<AgentOrchestrateAddActionOptions['onAdded']>(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 } =