Compare commits

...

2 Commits

Author SHA1 Message Date
yyh
915a6cc090 refactor!: replace Zustand global store with TanStack Query for systemFeatures
Follow-up to SSR prefetch migration. Eliminates the Zustand middleman
that was syncing TanStack Query data into a separate store.

- Remove useGlobalPublicStore Zustand store entirely
- Create hooks/use-global-public.ts with useSystemFeatures,
  useSystemFeaturesQuery, useIsSystemFeaturesPending, useSetupStatusQuery
- Migrate all consumers to import from @/hooks/use-global-public
- Simplify global-public-context.tsx to a thin provider component
- Update test files to mock the new hook interface
- Fix SetupStatusResponse.setup_at type from Date to string (JSON)
- Fix setup-status.spec.ts mock target to match consoleClient
- Regenerate eslint-suppressions.json for main branch

BREAKING CHANGE: useGlobalPublicStore is removed. Use useSystemFeatures()
from @/hooks/use-global-public instead.
2026-02-01 19:14:58 +08:00
yyh
c63a73ef83 refactor!: migrate commonLayout to SSR prefetch with TanStack Query hydration
BREAKING CHANGE: commonLayout is now an async Server Component that
prefetches user-profile and current-workspace on the server via
TanStack Query's prefetchQuery + HydrationBoundary pattern. This
replaces the previous purely client-side data fetching approach.

Key changes:

- **SSR data prefetch (root layout)**: prefetch systemFeatures and
  setupStatus in the root layout server component, wrap children with
  HydrationBoundary to hydrate TanStack Query cache on the client.

- **SSR data prefetch (commonLayout)**: convert commonLayout from a
  client component to an async server component that prefetches
  user-profile (with x-version/x-env response headers) and
  current-workspace. Client-side providers/UI extracted to a new
  layout-client.tsx component.

- **Add loading.tsx (Next.js convention)**: add a Next.js loading.tsx
  file in commonLayout that shows a centered spinner. This replaces the
  deleted Splash component but works via Next.js built-in Suspense
  boundary for route segments, not a client-side overlay.

- **Extract shared SSR fetch utilities (utils/ssr-fetch.ts)**: create
  serverFetch (unauthenticated) and serverFetchWithAuth (with cookie
  forwarding + CSRF token). getAuthHeaders is wrapped with React.cache()
  for per-request deduplication across multiple SSR fetches.

- **Refactor AppInitializer**: split single monolithic async IIFE effect
  into three independent useEffects (oauth tracking, education verify,
  setup status check). Use useReducer for init flag, useRef to prevent
  duplicate tracking in StrictMode. Now reads setupStatus from TanStack
  Query cache (useSetupStatusQuery) instead of fetching independently.

- **Refactor global-public-context**: move Zustand store sync from
  queryFn side-effect to a dedicated useEffect, keeping queryFn pure.
  fetchSystemFeatures now simply returns the API response.

- **Fix usePSInfo SSR crash**: defer globalThis.location access from
  hook top-level to callback execution time via getDomain() helper,
  preventing "Cannot read properties of undefined" during server render.

- **Remove Splash component**: delete the client-side loading overlay
  that relied on useIsLogin polling, replaced by Next.js loading.tsx.

- **Remove staleTime/gcTime overrides in useUserProfile**: allow the
  SSR-prefetched data to be reused via default cache policy instead of
  forcing refetch on every mount.

- **Revert middleware auth guard**: remove the cookie-based session
  check in proxy.ts that caused false redirects to /signin for
  authenticated users (Dify's auth uses token refresh, not simple
  cookie presence).
2026-02-01 19:07:32 +08:00
102 changed files with 506 additions and 529 deletions

View File

@@ -26,37 +26,11 @@ vi.mock('@/app/components/base/chat/utils', () => ({
getProcessedSystemVariablesFromUrlParams: (...args: any[]) => mockGetProcessedSystemVariablesFromUrlParams(...args),
}))
// Use vi.hoisted to define mock state before vi.mock hoisting
const { mockGlobalStoreState } = vi.hoisted(() => ({
mockGlobalStoreState: {
isGlobalPending: false,
setIsGlobalPending: vi.fn(),
systemFeatures: {},
setSystemFeatures: vi.fn(),
},
vi.mock('@/context/global-public-context', () => ({
useSystemFeatures: vi.fn(() => ({})),
useIsSystemFeaturesPending: () => false,
}))
vi.mock('@/context/global-public-context', () => {
const useGlobalPublicStore = Object.assign(
(selector?: (state: typeof mockGlobalStoreState) => any) =>
selector ? selector(mockGlobalStoreState) : mockGlobalStoreState,
{
setState: (updater: any) => {
if (typeof updater === 'function')
Object.assign(mockGlobalStoreState, updater(mockGlobalStoreState) ?? {})
else
Object.assign(mockGlobalStoreState, updater)
},
__mockState: mockGlobalStoreState,
},
)
return {
useGlobalPublicStore,
useIsSystemFeaturesPending: () => false,
}
})
const TestConsumer = () => {
const embeddedUserId = useWebAppStore(state => state.embeddedUserId)
const embeddedConversationId = useWebAppStore(state => state.embeddedConversationId)
@@ -91,7 +65,6 @@ const initialWebAppStore = (() => {
})()
beforeEach(() => {
mockGlobalStoreState.isGlobalPending = false
mockGetProcessedSystemVariablesFromUrlParams.mockReset()
useWebAppStore.setState(initialWebAppStore, true)
})

View File

@@ -0,0 +1,39 @@
'use client'
import type { ReactNode } from 'react'
import { AppInitializer } from '@/app/components/app-initializer'
import AmplitudeProvider from '@/app/components/base/amplitude'
import GotoAnything from '@/app/components/goto-anything'
import Header from '@/app/components/header'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import ReadmePanel from '@/app/components/plugins/readme-panel'
import { AppContextProvider } from '@/context/app-context'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ModalContextProvider } from '@/context/modal-context'
import { ProviderContextProvider } from '@/context/provider-context'
import PartnerStack from '../components/billing/partner-stack'
export const CommonLayoutClient = ({ children }: { children: ReactNode }) => {
return (
<>
<AmplitudeProvider />
<AppInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
<ModalContextProvider>
<HeaderWrapper>
<Header />
</HeaderWrapper>
{children}
<PartnerStack />
<ReadmePanel />
<GotoAnything />
</ModalContextProvider>
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</AppInitializer>
</>
)
}

View File

@@ -1,45 +1,46 @@
import type { ReactNode } from 'react'
import * as React from 'react'
import { AppInitializer } from '@/app/components/app-initializer'
import AmplitudeProvider from '@/app/components/base/amplitude'
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import GA, { GaType } from '@/app/components/base/ga'
import Zendesk from '@/app/components/base/zendesk'
import GotoAnything from '@/app/components/goto-anything'
import Header from '@/app/components/header'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import ReadmePanel from '@/app/components/plugins/readme-panel'
import { AppContextProvider } from '@/context/app-context'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ModalContextProvider } from '@/context/modal-context'
import { ProviderContextProvider } from '@/context/provider-context'
import PartnerStack from '../components/billing/partner-stack'
import Splash from '../components/splash'
import { getQueryClientServer } from '@/context/query-client-server'
import { serverFetchWithAuth } from '@/utils/ssr-fetch'
import { CommonLayoutClient } from './layout-client'
const IS_DEV = process.env.NODE_ENV === 'development'
async function fetchUserProfileForSSR() {
const { data: profile, headers } = await serverFetchWithAuth('/account/profile')
return {
profile,
meta: {
currentVersion: headers.get('x-version'),
currentEnv: IS_DEV ? 'DEVELOPMENT' : headers.get('x-env'),
},
}
}
export default async function CommonLayout({ children }: { children: ReactNode }) {
const queryClient = getQueryClientServer()
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['common', 'user-profile'],
queryFn: fetchUserProfileForSSR,
}),
queryClient.prefetchQuery({
queryKey: ['common', 'current-workspace'],
queryFn: async () => {
const { data } = await serverFetchWithAuth('/workspaces/current', 'POST', {})
return data
},
}),
])
const Layout = ({ children }: { children: ReactNode }) => {
return (
<>
<HydrationBoundary state={dehydrate(queryClient)}>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<AppInitializer>
<AppContextProvider>
<EventEmitterContextProvider>
<ProviderContextProvider>
<ModalContextProvider>
<HeaderWrapper>
<Header />
</HeaderWrapper>
{children}
<PartnerStack />
<ReadmePanel />
<GotoAnything />
<Splash />
</ModalContextProvider>
</ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
<Zendesk />
</AppInitializer>
</>
<CommonLayoutClient>{children}</CommonLayoutClient>
<Zendesk />
</HydrationBoundary>
)
}
export default Layout

View File

@@ -0,0 +1,21 @@
import '@/app/components/base/loading/style.css'
export default function CommonLayoutLoading() {
return (
<div className="flex h-full w-full items-center justify-center">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="spin-animation">
<g clipPath="url(#clip0_324_2488)">
<path d="M15 0H10C9.44772 0 9 0.447715 9 1V6C9 6.55228 9.44772 7 10 7H15C15.5523 7 16 6.55228 16 6V1C16 0.447715 15.5523 0 15 0Z" fill="#1C64F2" />
<path opacity="0.5" d="M15 9H10C9.44772 9 9 9.44772 9 10V15C9 15.5523 9.44772 16 10 16H15C15.5523 16 16 15.5523 16 15V10C16 9.44772 15.5523 9 15 9Z" fill="#1C64F2" />
<path opacity="0.1" d="M6 9H1C0.447715 9 0 9.44772 0 10V15C0 15.5523 0.447715 16 1 16H6C6.55228 16 7 15.5523 7 15V10C7 9.44772 6.55228 9 6 9Z" fill="#1C64F2" />
<path opacity="0.2" d="M6 0H1C0.447715 0 0 0.447715 0 1V6C0 6.55228 0.447715 7 1 7H6C6.55228 7 7 6.55228 7 6V1C7 0.447715 6.55228 0 6 0Z" fill="#1C64F2" />
</g>
<defs>
<clipPath id="clip0_324_2488">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
</div>
)
}

View File

@@ -1,11 +1,11 @@
'use client'
import type * as React from 'react'
import Header from '@/app/signin/_header'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
export default function SignInLayout({ children }: { children: React.ReactNode }) {
const systemFeatures = useSystemFeatures()
return (
<>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>

View File

@@ -5,12 +5,12 @@ import { useCallback, useEffect } from 'react'
import AppUnavailable from '@/app/components/base/app-unavailable'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
import { SSOProtocol } from '@/types/feature'
const ExternalMemberSSOAuth = () => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const searchParams = useSearchParams()
const router = useRouter()

View File

@@ -2,13 +2,13 @@
import type { PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
export default function SignInLayout({ children }: PropsWithChildren) {
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
useDocumentTitle(t('webapp.login', { ns: 'login' }))
return (
<>

View File

@@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { LicenseStatus } from '@/types/feature'
import { cn } from '@/utils/classnames'
import MailAndCodeAuth from './components/mail-and-code-auth'
@@ -17,7 +17,7 @@ const NormalForm = () => {
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState(true)
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)

View File

@@ -5,8 +5,8 @@ import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import AppUnavailable from '@/app/components/base/app-unavailable'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useWebAppStore } from '@/context/web-app-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { AccessMode } from '@/models/access-control'
import { webAppLogout } from '@/service/webapp-auth'
import ExternalMemberSsoAuth from './components/external-member-sso-auth'
@@ -14,7 +14,7 @@ import NormalForm from './normalForm'
const WebSSOForm: FC = () => {
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode)
const searchParams = useSearchParams()
const router = useRouter()

View File

@@ -16,8 +16,8 @@ import { ToastContext } from '@/app/components/base/toast'
import Collapse from '@/app/components/header/account-setting/collapse'
import { IS_CE_EDITION, validPassword } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { updateUserProfile } from '@/service/common'
import { useAppList } from '@/service/use-apps'
import DeleteAccount from '../delete-account'
@@ -34,7 +34,7 @@ const descriptionClassName = `
export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const { data: appList } = useAppList({ page: 1, limit: 100, name: '' })
const apps = appList?.data || []
const { mutateUserProfile, userProfile } = useAppContext()

View File

@@ -5,13 +5,13 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import Avatar from './avatar'
const Header = () => {
const { t } = useTranslation()
const router = useRouter()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const goToStudio = useCallback(() => {
router.push('/apps')

View File

@@ -3,13 +3,13 @@ import Loading from '@/app/components/base/loading'
import Header from '@/app/signin/_header'
import { AppContextProvider } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useIsLogin } from '@/service/use-common'
import { cn } from '@/utils/classnames'
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
useDocumentTitle('')
const { isLoading, data: loginData } = useIsLogin()
const isLoggedIn = loginData?.logged_in

View File

@@ -1,12 +1,12 @@
'use client'
import * as React from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import Header from '../signin/_header'
import ActivateForm from './activateForm'
const Activate = () => {
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
return (
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>

View File

@@ -2,15 +2,15 @@
import type { ReactNode } from 'react'
import Cookies from 'js-cookie'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useRouter, useSearchParams } from 'next/navigation'
import { parseAsString, useQueryState } from 'nuqs'
import { useCallback, useEffect, useState } from 'react'
import { useEffect, useReducer, useRef } from 'react'
import {
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
} from '@/app/education-apply/constants'
import { useSetupStatusQuery } from '@/hooks/use-global-public'
import { sendGAEvent } from '@/utils/gtag'
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
import { resolvePostLoginRedirect } from '../signin/utils/post-login-redirect'
import { trackEvent } from './base/amplitude'
@@ -23,80 +23,68 @@ export const AppInitializer = ({
}: AppInitializerProps) => {
const router = useRouter()
const searchParams = useSearchParams()
// Tokens are now stored in cookies, no need to check localStorage
const pathname = usePathname()
const [init, setInit] = useState(false)
const [init, markInit] = useReducer(() => true, false)
const { data: setupStatus } = useSetupStatusQuery()
const [oauthNewUser, setOauthNewUser] = useQueryState(
'oauth_new_user',
parseAsString.withOptions({ history: 'replace' }),
)
const isSetupFinished = useCallback(async () => {
try {
const setUpStatus = await fetchSetupStatusWithCache()
return setUpStatus.step === 'finished'
}
catch (error) {
console.error(error)
return false
}
}, [])
const oauthTrackedRef = useRef(false)
useEffect(() => {
(async () => {
const action = searchParams.get('action')
if (oauthNewUser === 'true') {
let utmInfo = null
const utmInfoStr = Cookies.get('utm_info')
if (utmInfoStr) {
try {
utmInfo = JSON.parse(utmInfoStr)
}
catch (e) {
console.error('Failed to parse utm_info cookie:', e)
}
}
// Track registration event with UTM params
trackEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
method: 'oauth',
...utmInfo,
})
sendGAEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
method: 'oauth',
...utmInfo,
})
// Clean up: remove utm_info cookie and URL params
Cookies.remove('utm_info')
setOauthNewUser(null)
}
if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
if (oauthNewUser !== 'true' || oauthTrackedRef.current)
return
oauthTrackedRef.current = true
let utmInfo = null
const utmInfoStr = Cookies.get('utm_info')
if (utmInfoStr) {
try {
const isFinished = await isSetupFinished()
if (!isFinished) {
router.replace('/install')
return
}
const redirectUrl = resolvePostLoginRedirect(searchParams)
if (redirectUrl) {
location.replace(redirectUrl)
return
}
setInit(true)
utmInfo = JSON.parse(utmInfoStr)
}
catch {
router.replace('/signin')
catch (e) {
console.error('Failed to parse utm_info cookie:', e)
}
})()
}, [isSetupFinished, router, pathname, searchParams, oauthNewUser, setOauthNewUser])
}
trackEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
method: 'oauth',
...utmInfo,
})
sendGAEvent(utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success', {
method: 'oauth',
...utmInfo,
})
Cookies.remove('utm_info')
// eslint-disable-next-line react-hooks-extra/no-direct-set-state-in-use-effect -- setOauthNewUser is from nuqs useQueryState, not useState
setOauthNewUser(null)
}, [oauthNewUser, setOauthNewUser])
useEffect(() => {
const action = searchParams.get('action')
if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
}, [searchParams])
useEffect(() => {
if (!setupStatus)
return
if (setupStatus.step !== 'finished') {
router.replace('/install')
return
}
const redirectUrl = resolvePostLoginRedirect(searchParams)
if (redirectUrl) {
location.replace(redirectUrl)
return
}
markInit()
}, [setupStatus, router, searchParams])
return init ? children : null
}

View File

@@ -5,7 +5,7 @@ import { Description as DialogDescription, DialogTitle } from '@headlessui/react
import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react'
import { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { AccessMode, SubjectType } from '@/models/access-control'
import { useUpdateAccessMode } from '@/service/access-control'
import useAccessControlStore from '../../../../context/access-control-store'
@@ -24,7 +24,7 @@ type AccessControlProps = {
export default function AccessControl(props: AccessControlProps) {
const { app, onClose, onConfirm } = props
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const setAppId = useAccessControlStore(s => s.setAppId)
const specificGroups = useAccessControlStore(s => s.specificGroups)
const specificMembers = useAccessControlStore(s => s.specificMembers)

View File

@@ -36,9 +36,9 @@ import {
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
import { appDefaultIconBackground } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { AccessMode } from '@/models/access-control'
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
import { fetchAppDetailDirect } from '@/service/apps'
@@ -149,7 +149,7 @@ const AppPublisher = ({
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(s => s.setAppDetail)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const { formatTimeFromNow } = useFormatTimeFromNow()
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}

View File

@@ -8,7 +8,7 @@ import { useContextSelector } from 'use-context-selector'
import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import AppListContext from '@/context/app-list-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import { AppTypeIcon, AppTypeLabel } from '../../type-selector'
@@ -25,7 +25,7 @@ const AppCard = ({
}: AppCardProps) => {
const { t } = useTranslation()
const { app: appBasicInfo } = app
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
const setShowTryAppPanel = useContextSelector(AppListContext, ctx => ctx.setShowTryAppPanel)
const showTryAPPPanel = useCallback((appId: string) => {

View File

@@ -31,8 +31,8 @@ import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-butt
import Indicator from '@/app/components/header/indicator'
import { BlockEnum } from '@/app/components/workflow/types'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useDocLink } from '@/context/i18n'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { AccessMode } from '@/models/access-control'
import { useAppWhiteListSubjects } from '@/service/access-control'
import { fetchAppDetailDirect } from '@/service/apps'
@@ -85,7 +85,7 @@ function AppCard({
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showAccessControl, setShowAccessControl] = useState<boolean>(false)
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const { data: appAccessSubjects } = useAppWhiteListSubjects(appDetail?.id, systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
const OPERATIONS_MAP = useMemo(() => {

View File

@@ -51,11 +51,9 @@ vi.mock('@/context/provider-context', () => ({
// Mock global public store - allow dynamic configuration
let mockWebappAuthEnabled = false
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (s: any) => any) => selector({
systemFeatures: {
webapp_auth: { enabled: mockWebappAuthEnabled },
branding: { enabled: false },
},
useSystemFeatures: () => ({
webapp_auth: { enabled: mockWebappAuthEnabled },
branding: { enabled: false },
}),
}))

View File

@@ -22,9 +22,9 @@ import Toast, { ToastContext } from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { AccessMode } from '@/models/access-control'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
@@ -64,7 +64,7 @@ export type AppCardProps = {
const AppCard = ({ app, onRefresh }: AppCardProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const { isCurrentWorkspaceEditor } = useAppContext()
const { onPlanInfoChanged } = useProviderContext()
const { push } = useRouter()

View File

@@ -26,10 +26,8 @@ vi.mock('@/context/app-context', () => ({
// Mock global public store
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: () => ({
systemFeatures: {
branding: { enabled: false },
},
useSystemFeatures: () => ({
branding: { enabled: false },
}),
}))

View File

@@ -24,7 +24,7 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { CheckModal } from '@/hooks/use-pay'
import { useInfiniteAppList } from '@/service/use-apps'
import { AppModeEnum } from '@/types/app'
@@ -61,7 +61,7 @@ const List: FC<Props> = ({
controlRefreshList = 0,
}) => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const router = useRouter()
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)

View File

@@ -17,7 +17,7 @@ import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/re
import Confirm from '@/app/components/base/confirm'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import { useChatWithHistoryContext } from '../context'
@@ -47,7 +47,7 @@ const Sidebar = ({ isPanel, panelVisible }: Props) => {
isResponding,
} = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
const [showRename, setShowRename] = useState<ConversationItem | null>(null)

View File

@@ -9,7 +9,7 @@ import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs
import Divider from '@/app/components/base/divider'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import Tooltip from '@/app/components/base/tooltip'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import { isClient } from '@/utils/client'
import {
@@ -45,7 +45,7 @@ const Header: FC<IHeaderProps> = ({
const [parentOrigin, setParentOrigin] = useState('')
const [showToggleExpandButton, setShowToggleExpandButton] = useState(false)
const [expanded, setExpanded] = useState(false)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const handleMessageReceived = useCallback((event: MessageEvent) => {
let currentParentOrigin = parentOrigin

View File

@@ -9,9 +9,9 @@ import Header from '@/app/components/base/chat/embedded-chatbot/header'
import Loading from '@/app/components/base/loading'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { AppSourceType } from '@/service/share'
import { cn } from '@/utils/classnames'
import {
@@ -34,7 +34,7 @@ const Chatbot = () => {
themeBuilder,
} = useEmbeddedChatbotContext()
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const customConfig = appData?.custom_config
const site = appData?.site

View File

@@ -23,8 +23,8 @@ const usePSInfo = () => {
setTrue: setBind,
}] = useBoolean(false)
const { mutateAsync } = useBindPartnerStackInfo()
// Save to top domain. cloud.dify.ai => .dify.ai
const domain = globalThis.location.hostname.replace('cloud', '')
const getDomain = () => globalThis.location?.hostname.replace('cloud', '') ?? ''
const saveOrUpdate = useCallback(() => {
if (!psPartnerKey || !psClickId)
@@ -37,7 +37,7 @@ const usePSInfo = () => {
}), {
expires: PARTNER_STACK_CONFIG.saveCookieDays,
path: '/',
domain,
domain: getDomain(),
})
}, [psPartnerKey, psClickId, isPSChanged])
@@ -56,7 +56,7 @@ const usePSInfo = () => {
shouldRemoveCookie = true
}
if (shouldRemoveCookie)
Cookies.remove(PARTNER_STACK_CONFIG.cookieName, { path: '/', domain })
Cookies.remove(PARTNER_STACK_CONFIG.cookieName, { path: '/', domain: getDomain() })
setBind()
}
}, [psPartnerKey, psClickId, mutateAsync, hasBind, setBind])

View File

@@ -4,8 +4,8 @@ import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/i
import { useToastContext } from '@/app/components/base/toast'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { updateCurrentWorkspace } from '@/service/common'
import CustomWebAppBrand from './index'
@@ -22,7 +22,7 @@ vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(),
useSystemFeatures: vi.fn(),
}))
vi.mock('@/app/components/base/image-uploader/utils', () => ({
imageUpload: vi.fn(),
@@ -34,7 +34,7 @@ const mockUseToastContext = vi.mocked(useToastContext)
const mockUpdateCurrentWorkspace = vi.mocked(updateCurrentWorkspace)
const mockUseAppContext = vi.mocked(useAppContext)
const mockUseProviderContext = vi.mocked(useProviderContext)
const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore)
const mockUseSystemFeatures = vi.mocked(useSystemFeatures)
const mockImageUpload = vi.mocked(imageUpload)
const mockGetImageUploadErrorMessage = vi.mocked(getImageUploadErrorMessage)
@@ -80,7 +80,7 @@ describe('CustomWebAppBrand', () => {
workspace_logo: 'https://example.com/workspace-logo.png',
},
}
mockUseGlobalPublicStore.mockImplementation(selector => selector ? selector({ systemFeatures: systemFeaturesState } as any) : { systemFeatures: systemFeaturesState })
mockUseSystemFeatures.mockReturnValue(systemFeaturesState as ReturnType<typeof mockUseSystemFeatures>)
mockGetImageUploadErrorMessage.mockReturnValue('upload error')
})

View File

@@ -19,8 +19,8 @@ import Switch from '@/app/components/base/switch'
import { useToastContext } from '@/app/components/base/toast'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import {
updateCurrentWorkspace,
} from '@/service/common'
@@ -40,7 +40,7 @@ const CustomWebAppBrand = () => {
const [fileId, setFileId] = useState('')
const [imgKey, setImgKey] = useState(() => Date.now())
const [uploadProgress, setUploadProgress] = useState(0)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const isSandbox = enableBilling && plan.type === Plan.sandbox
const uploading = uploadProgress > 0 && uploadProgress < 100
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''

View File

@@ -25,10 +25,7 @@ vi.mock('@/context/i18n', () => ({
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn((selector) => {
const state = { systemFeatures: { enable_marketplace: true } }
return selector(state)
}),
useSystemFeatures: vi.fn(() => ({ enable_marketplace: true })),
}))
const mockUsePipelineTemplateList = vi.fn()

View File

@@ -1,6 +1,6 @@
import { useMemo } from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useLocale } from '@/context/i18n'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { LanguagesSupported } from '@/i18n-config/language'
import { usePipelineTemplateList } from '@/service/use-pipeline'
import CreateCard from './create-card'
@@ -13,7 +13,7 @@ const BuiltInPipelineList = () => {
return locale
return LanguagesSupported[0]
}, [locale])
const enableMarketplace = useGlobalPublicStore(s => s.systemFeatures.enable_marketplace)
const enableMarketplace = useSystemFeatures().enable_marketplace
const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'built-in', language }, enableMarketplace)
const list = pipelineList?.pipeline_templates || []

View File

@@ -34,10 +34,8 @@ vi.mock('@/context/app-context', () => ({
// Mock global public context
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: () => ({
systemFeatures: {
branding: { enabled: false },
},
useSystemFeatures: () => ({
branding: { enabled: false },
}),
}))
@@ -333,10 +331,8 @@ describe('List', () => {
it('should not show DatasetFooter when branding is enabled', async () => {
vi.doMock('@/context/global-public-context', () => ({
useGlobalPublicStore: () => ({
systemFeatures: {
branding: { enabled: true },
},
useSystemFeatures: () => ({
branding: { enabled: true },
}),
}))

View File

@@ -16,8 +16,8 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
import { useAppContext, useSelector as useAppContextSelector } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useDatasetApiBaseUrl } from '@/service/knowledge/use-dataset'
// Components
import ExternalAPIPanel from '../external-api/external-api-panel'
@@ -27,7 +27,7 @@ import Datasets from './datasets'
const List = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const router = useRouter()
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)

View File

@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
import { useContextSelector } from 'use-context-selector'
import AppIcon from '@/app/components/base/app-icon'
import ExploreContext from '@/context/explore-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { AppModeEnum } from '@/types/app'
import { cn } from '@/utils/classnames'
import { AppTypeIcon } from '../../app/type-selector'
@@ -28,7 +28,7 @@ const AppCard = ({
}: AppCardProps) => {
const { t } = useTranslation()
const { app: appBasicInfo } = app
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const isTrialApp = app.can_trial && systemFeatures.enable_trial_app
const setShowTryAppPanel = useContextSelector(ExploreContext, ctx => ctx.setShowTryAppPanel)
const showTryAPPPanel = useCallback((appId: string) => {

View File

@@ -17,7 +17,7 @@ import Banner from '@/app/components/explore/banner/banner'
import Category from '@/app/components/explore/category'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import ExploreContext from '@/context/explore-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useImportDSL } from '@/hooks/use-import-dsl'
import {
DSLImportMode,
@@ -36,7 +36,7 @@ const Apps = ({
onSuccess,
}: AppsProps) => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const { hasEditPermission } = useContext(ExploreContext)
const allCategoriesEn = t('apps.allCategories', { ns: 'explore', lng: 'en' })

View File

@@ -7,7 +7,7 @@ import * as React from 'react'
import { useState } from 'react'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal/index'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useGetTryAppInfo } from '@/service/use-try-app'
import Button from '../../base/button'
import App from './app'
@@ -30,7 +30,7 @@ const TryApp: FC<Props> = ({
onClose,
onCreate,
}) => {
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const isTrialApp = !!(app && app.can_trial && systemFeatures.enable_trial_app)
const [type, setType] = useState<TypeEnum>(() => (app && !isTrialApp ? TypeEnum.DETAIL : TypeEnum.TRY))
const { data: appDetail, isLoading } = useGetTryAppInfo(appId)

View File

@@ -9,7 +9,7 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
import Modal from '@/app/components/base/modal'
import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
type IAccountSettingProps = {
langGeniusVersionInfo: LangGeniusVersionResponse
@@ -22,7 +22,7 @@ export default function AccountAbout({
}: IAccountSettingProps) {
const { t } = useTranslation()
const isLatest = langGeniusVersionInfo.current_version === langGeniusVersionInfo.latest_version
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
return (
<Modal

View File

@@ -24,10 +24,10 @@ import ThemeSwitcher from '@/app/components/base/theme-switcher'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useDocLink } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useLogout } from '@/service/use-common'
import { cn } from '@/utils/classnames'
import AccountAbout from '../account-about'
@@ -43,7 +43,7 @@ export default function AppSelector() {
`
const router = useRouter()
const [aboutVisible, setAboutVisible] = useState(false)
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const { t } = useTranslation()
const docLink = useDocLink()

View File

@@ -1,11 +1,11 @@
import { memo } from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useGetDataSourceListAuth } from '@/service/use-datasource'
import Card from './card'
import InstallFromMarketplace from './install-from-marketplace'
const DataSourcePage = () => {
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const { data } = useGetDataSourceListAuth()
return (

View File

@@ -9,10 +9,10 @@ import { NUM_INFINITE } from '@/app/components/billing/config'
import { Plan } from '@/app/components/billing/type'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useLocale } from '@/context/i18n'
import { useProviderContext } from '@/context/provider-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { LanguagesSupported } from '@/i18n-config/language'
import { useMembers } from '@/service/use-common'
import EditWorkspaceModal from './edit-workspace-modal'
@@ -36,7 +36,7 @@ const MembersPage = () => {
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
const { data, refetch } = useMembers()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const { formatTimeFromNow } = useFormatTimeFromNow()
const [inviteModalVisible, setInviteModalVisible] = useState(false)
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useWorkspacePermissions } from '@/service/use-workspace'
type InviteButtonProps = {
@@ -14,7 +14,7 @@ type InviteButtonProps = {
const InviteButton = (props: InviteButtonProps) => {
const { t } = useTranslation()
const { currentWorkspace } = useAppContext()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspace!.id, systemFeatures.branding.enabled)
if (systemFeatures.branding.enabled) {
if (isFetchingWorkspacePermissions) {

View File

@@ -7,7 +7,7 @@ import { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useWorkspacePermissions } from '@/service/use-workspace'
import { cn } from '@/utils/classnames'
@@ -18,7 +18,7 @@ type Props = {
const TransferOwnership = ({ onOperate }: Props) => {
const { t } = useTranslation()
const { currentWorkspace } = useAppContext()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const { data: workspacePermissions, isFetching: isFetchingWorkspacePermissions } = useWorkspacePermissions(currentWorkspace!.id, systemFeatures.branding.enabled)
if (systemFeatures.branding.enabled) {
if (isFetchingWorkspacePermissions) {

View File

@@ -10,8 +10,8 @@ import { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import {
CustomConfigurationStatusEnum,
@@ -41,7 +41,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
const { data: speech2textDefaultModel, isLoading: isSpeech2textDefaultModelLoading } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel, isLoading: isTTSDefaultModelLoading } = useDefaultModel(ModelTypeEnum.tts)
const { modelProviders: providers } = useProviderContext()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const isDefaultModelLoading = isTextGenerationDefaultModelLoading
|| isEmbeddingsDefaultModelLoading
|| isRerankDefaultModelLoading

View File

@@ -10,7 +10,7 @@ import Loading from '@/app/components/base/loading'
import Tooltip from '@/app/components/base/tooltip'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import useTimestamp from '@/hooks/use-timestamp'
import { ModelProviderQuotaGetPaid } from '@/types/model-provider'
import { cn } from '@/utils/classnames'
@@ -56,7 +56,7 @@ const QuotaPanel: FC<QuotaPanelProps> = ({
}) => {
const { t } = useTranslation()
const { currentWorkspace } = useAppContext()
const { trial_models } = useGlobalPublicStore(s => s.systemFeatures)
const { trial_models } = useSystemFeatures()
const credits = Math.max((currentWorkspace.trial_credits - currentWorkspace.trial_credits_used) || 0, 0)
const providerMap = useMemo(() => new Map(
providers.map(p => [p.provider, p.preferred_provider_type]),

View File

@@ -5,11 +5,11 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
import WorkplaceSelector from '@/app/components/header/account-dropdown/workplace-selector'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { WorkspaceProvider } from '@/context/workspace-context'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { Plan } from '../billing/type'
import AccountDropdown from './account-dropdown'
import AppNav from './app-nav'
@@ -33,7 +33,7 @@ const Header = () => {
const isMobile = media === MediaType.mobile
const { enableBilling, plan } = useProviderContext()
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const isFreePlan = plan.type === Plan.sandbox
const isBrandingEnabled = systemFeatures.branding.enabled
const handlePlanClick = useCallback(() => {

View File

@@ -3,13 +3,13 @@
import { RiHourglass2Fill } from '@remixicon/react'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { LicenseStatus } from '@/types/feature'
import PremiumBadge from '../../base/premium-badge'
const LicenseNav = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
if (systemFeatures.license?.status === LicenseStatus.EXPIRING) {
const expiredAt = systemFeatures.license?.expired_at

View File

@@ -1,6 +1,6 @@
import type { Plugin, PluginManifestInMarket } from '../../types'
import type { SystemFeatures } from '@/types/feature'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { InstallationScope } from '@/types/feature'
type PluginProps = (Plugin | PluginManifestInMarket) & { from: 'github' | 'marketplace' | 'package' }
@@ -41,6 +41,6 @@ export function pluginInstallLimit(plugin: PluginProps, systemFeatures: SystemFe
}
export default function usePluginInstallLimit(plugin: PluginProps) {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
return pluginInstallLimit(plugin, systemFeatures)
}

View File

@@ -181,7 +181,7 @@ vi.mock('@/context/mitt-context', () => ({
// Mock global public context
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: () => ({}),
useSystemFeatures: () => ({}),
}))
// Mock useCanInstallPluginFromMarketplace

View File

@@ -56,9 +56,9 @@ vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', ()
}),
}))
// Mock useGlobalPublicStore
// Mock useSystemFeatures
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: () => ({}),
useSystemFeatures: () => ({}),
}))
// Mock pluginInstallLimit

View File

@@ -4,7 +4,7 @@ import { produce } from 'immer'
import * as React from 'react'
import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins'
import LoadingError from '../../base/loading-error'
import { pluginInstallLimit } from '../../hooks/use-install-plugin-limit'
@@ -38,7 +38,7 @@ const InstallByDSLList = ({
isFromMarketPlace,
ref,
}: Props) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
// DSL has id, to get plugin info to show more info
const { isLoading: isFetchingMarketplaceDataById, data: infoGetById, error: infoByIdError } = useFetchPluginsInMarketPlaceByInfo(allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const dependecy = (d as GitHubItemAndMarketPlaceDependency).value

View File

@@ -66,8 +66,7 @@ vi.mock('@/context/i18n', () => ({
let mockEnableMarketplace = true
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => unknown) =>
selector({ systemFeatures: { enable_marketplace: mockEnableMarketplace } }),
useSystemFeatures: () => ({ enable_marketplace: mockEnableMarketplace }),
}))
vi.mock('@/context/modal-context', () => ({

View File

@@ -25,10 +25,10 @@ import UpdateFromMarketplace from '@/app/components/plugins/update-plugin/from-m
import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker'
import { API_PREFIX } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetLanguage, useLocale } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import useTheme from '@/hooks/use-theme'
import { uninstallPlugin } from '@/service/plugins'
import { useAllToolProviders, useInvalidateAllToolProviders } from '@/service/use-tools'
@@ -72,7 +72,7 @@ const DetailHeader = ({
const { setShowUpdatePluginModal } = useModalContext()
const { refreshModelProviders } = useProviderContext()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const {
id,

View File

@@ -11,8 +11,7 @@ vi.mock('react-i18next', () => ({
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: <T,>(selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => T): T =>
selector({ systemFeatures: { enable_marketplace: true } }),
useSystemFeatures: () => ({ enable_marketplace: true }),
}))
vi.mock('@/utils/classnames', () => ({

View File

@@ -11,7 +11,7 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import { PluginSource } from '../types'
@@ -42,7 +42,7 @@ const OperationDropdown: FC<Props> = ({
setOpen(!openRef.current)
}, [setOpen])
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
return (
<PortalToFollowElem

View File

@@ -68,8 +68,7 @@ vi.mock('@/context/app-context', () => ({
// Mock global public store
const mockEnableMarketplace = vi.fn(() => true)
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (s: any) => any) =>
selector({ systemFeatures: { enable_marketplace: mockEnableMarketplace() } }),
useSystemFeatures: () => ({ enable_marketplace: mockEnableMarketplace() }),
}))
// Mock Action component

View File

@@ -16,7 +16,7 @@ import Tooltip from '@/app/components/base/tooltip'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { API_PREFIX } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import useTheme from '@/hooks/use-theme'
import { cn } from '@/utils/classnames'
@@ -85,7 +85,7 @@ const PluginItem: FC<Props> = ({
const getValueFromI18nObject = useRenderI18nObject()
const title = getValueFromI18nObject(label)
const descriptionText = getValueFromI18nObject(description)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const iconFileName = theme === 'dark' && icon_dark ? icon_dark : icon
const iconSrc = iconFileName
? (iconFileName.startsWith('http') ? iconFileName : `${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${iconFileName}`)

View File

@@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Import mocks
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { PluginPageContext, PluginPageContextProvider, usePluginPageContext } from './context'
@@ -11,7 +11,7 @@ vi.mock('nuqs', () => ({
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(),
useSystemFeatures: vi.fn(),
}))
vi.mock('../hooks', () => ({
@@ -25,12 +25,11 @@ vi.mock('../hooks', () => ({
],
}))
// Helper function to mock useGlobalPublicStore with marketplace setting
// Helper function to mock useSystemFeatures with marketplace setting
const mockGlobalPublicStore = (enableMarketplace: boolean) => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = { systemFeatures: { enable_marketplace: enableMarketplace } }
return selector(state as Parameters<typeof selector>[0])
})
vi.mocked(useSystemFeatures).mockReturnValue({
enable_marketplace: enableMarketplace,
} as ReturnType<typeof useSystemFeatures>)
}
// Test component that uses the context

View File

@@ -13,7 +13,7 @@ import {
createContext,
useContextSelector,
} from 'use-context-selector'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { PLUGIN_PAGE_TABS_MAP, usePluginPageTabs } from '../hooks'
export type PluginPageContextValue = {
@@ -63,7 +63,7 @@ export const PluginPageContextProvider = ({
})
const [currentPluginID, setCurrentPluginID] = useState<string | undefined>()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const tabs = usePluginPageTabs()
const options = useMemo(() => {
return enable_marketplace ? tabs : tabs.filter(tab => tab.value !== PLUGIN_PAGE_TABS_MAP.marketplace)

View File

@@ -56,14 +56,10 @@ vi.mock('../context', () => ({
// Mock global public store (Zustand store)
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (state: any) => any) => {
return selector({
systemFeatures: {
...defaultSystemFeatures,
...mockState.systemFeatures,
},
})
},
useSystemFeatures: () => ({
...defaultSystemFeatures,
...mockState.systemFeatures,
}),
}))
// Mock useInstalledPluginList hook

View File

@@ -11,7 +11,7 @@ import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndD
import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-from-github'
import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package'
import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useInstalledPluginList } from '@/service/use-plugins'
import Line from '../../marketplace/empty/line'
import { usePluginPageContext } from '../context'
@@ -27,7 +27,7 @@ const Empty = () => {
const fileInputRef = useRef<HTMLInputElement>(null)
const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace, plugin_installation_permission } = useSystemFeatures()
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {

View File

@@ -28,14 +28,9 @@ vi.mock('@/context/i18n', () => ({
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn((selector) => {
const state = {
systemFeatures: {
enable_marketplace: true,
},
}
return selector(state)
}),
useSystemFeatures: vi.fn(() => ({
enable_marketplace: true,
})),
}))
vi.mock('@/context/app-context', () => ({
@@ -629,14 +624,9 @@ describe('PluginPage Component', () => {
it('should handle marketplace disabled', () => {
// Mock marketplace disabled
vi.mock('@/context/global-public-context', async () => ({
useGlobalPublicStore: vi.fn((selector) => {
const state = {
systemFeatures: {
enable_marketplace: false,
},
}
return selector(state)
}),
useSystemFeatures: vi.fn(() => ({
enable_marketplace: false,
})),
}))
vi.mocked(useQueryState).mockReturnValue(['discover', vi.fn()])

View File

@@ -16,9 +16,9 @@ import TabSlider from '@/app/components/base/tab-slider'
import Tooltip from '@/app/components/base/tooltip'
import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal'
import { MARKETPLACE_API_PREFIX, SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useDocLink } from '@/context/i18n'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { usePluginInstallation } from '@/hooks/use-query-params'
import { fetchBundleInfoFromMarketPlace, fetchManifestFromMarketPlace } from '@/service/plugins'
import { sleep } from '@/utils'
@@ -112,7 +112,7 @@ const PluginPage = ({
const options = usePluginPageContext(v => v.options)
const activeTab = usePluginPageContext(v => v.activeTab)
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab])
const isExploringMarketplace = useMemo(() => {

View File

@@ -16,7 +16,7 @@ import {
import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-from-github'
import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package'
import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
type Props = {
@@ -37,7 +37,7 @@ const InstallPluginDropdown = ({
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const { enable_marketplace, plugin_installation_permission } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace, plugin_installation_permission } = useSystemFeatures()
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]

View File

@@ -2,7 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Import mocks for assertions
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins'
import Toast from '../../base/toast'
@@ -21,7 +21,7 @@ vi.mock('@/context/app-context', () => ({
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(),
useSystemFeatures: vi.fn(),
}))
vi.mock('@/service/use-plugins', () => ({
@@ -309,14 +309,9 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
})
it('should return true when marketplace is enabled and canManagement is true', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = {
systemFeatures: {
enable_marketplace: true,
},
}
return selector(state as Parameters<typeof selector>[0])
})
vi.mocked(useSystemFeatures).mockReturnValue({
enable_marketplace: true,
} as ReturnType<typeof useSystemFeatures>)
const { result } = renderHook(() => useCanInstallPluginFromMarketplace())
@@ -324,14 +319,9 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
})
it('should return false when marketplace is disabled', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = {
systemFeatures: {
enable_marketplace: false,
},
}
return selector(state as Parameters<typeof selector>[0])
})
vi.mocked(useSystemFeatures).mockReturnValue({
enable_marketplace: false,
} as ReturnType<typeof useSystemFeatures>)
const { result } = renderHook(() => useCanInstallPluginFromMarketplace())
@@ -339,14 +329,9 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
})
it('should return false when canManagement is false', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = {
systemFeatures: {
enable_marketplace: true,
},
}
return selector(state as Parameters<typeof selector>[0])
})
vi.mocked(useSystemFeatures).mockReturnValue({
enable_marketplace: true,
} as ReturnType<typeof useSystemFeatures>)
vi.mocked(useReferenceSettings).mockReturnValue({
data: {
@@ -363,14 +348,9 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
})
it('should return false when both marketplace is disabled and canManagement is false', () => {
vi.mocked(useGlobalPublicStore).mockImplementation((selector) => {
const state = {
systemFeatures: {
enable_marketplace: false,
},
}
return selector(state as Parameters<typeof selector>[0])
})
vi.mocked(useSystemFeatures).mockReturnValue({
enable_marketplace: false,
} as ReturnType<typeof useSystemFeatures>)
vi.mocked(useReferenceSettings).mockReturnValue({
data: {

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins'
import Toast from '../../base/toast'
import { PermissionType } from '../types'
@@ -48,7 +48,7 @@ const useReferenceSetting = () => {
}
export const useCanInstallPluginFromMarketplace = () => {
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const { canManagement } = useReferenceSetting()
const canInstallPluginFromMarketplace = useMemo(() => {

View File

@@ -36,9 +36,7 @@ vi.mock('react-i18next', () => ({
// Mock global public store
const mockSystemFeatures = { enable_marketplace: true }
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (s: { systemFeatures: typeof mockSystemFeatures }) => typeof mockSystemFeatures) => {
return selector({ systemFeatures: mockSystemFeatures })
},
useSystemFeatures: () => mockSystemFeatures,
}))
// Mock Modal component

View File

@@ -9,7 +9,7 @@ import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { PermissionType } from '@/app/components/plugins/types'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import AutoUpdateSetting from './auto-update-setting'
import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config'
import Label from './label'
@@ -30,7 +30,7 @@ const PluginSettingModal: FC<Props> = ({
const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {}
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege)
const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig || autoUpdateDefaultValue)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const handlePrivilegeChange = useCallback((key: string) => {
return (value: PermissionType) => {
setTempPrivilege({

View File

@@ -27,11 +27,11 @@ import Toast from '@/app/components/base/toast'
import Res from '@/app/components/share/text-generation/result'
import RunOnce from '@/app/components/share/text-generation/run-once'
import { appDefaultIconBackground, BATCH_CONCURRENCY } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useWebAppStore } from '@/context/web-app-context'
import { useAppFavicon } from '@/hooks/use-app-favicon'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { changeLanguage } from '@/i18n-config/client'
import { AccessMode } from '@/models/access-control'
import { AppSourceType, fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share'
@@ -91,7 +91,7 @@ const TextGeneration: FC<IMainProps> = ({
doSetInputs(newInputs)
inputsRef.current = newInputs
}, [])
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const [appId, setAppId] = useState<string>('')
const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
const [customConfig, setCustomConfig] = useState<Record<string, any> | null>(null)

View File

@@ -1,21 +0,0 @@
'use client'
import type { FC, PropsWithChildren } from 'react'
import * as React from 'react'
import { useIsLogin } from '@/service/use-common'
import Loading from './base/loading'
const Splash: FC<PropsWithChildren> = () => {
// would auto redirect to signin page if not logged in
const { isLoading, data: loginData } = useIsLogin()
const isLoggedIn = loginData?.logged_in
if (isLoading || !isLoggedIn) {
return (
<div className="fixed inset-0 z-[9999999] flex h-full items-center justify-center bg-background-body">
<Loading />
</div>
)
}
return null
}
export default React.memo(Splash)

View File

@@ -14,7 +14,7 @@ import LabelFilter from '@/app/components/tools/labels/filter'
import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
import ProviderDetail from '@/app/components/tools/provider/detail'
import WorkflowToolEmpty from '@/app/components/tools/provider/empty'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useCheckInstalled, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useAllToolProviders } from '@/service/use-tools'
import { cn } from '@/utils/classnames'
@@ -42,7 +42,7 @@ const ProviderList = () => {
// searchParams.get('category') === 'workflow'
const { t } = useTranslation()
const { getTagLabel } = useTags()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const containerRef = useRef<HTMLDivElement>(null)
const [activeTab, setActiveTab] = useQueryState('category', {

View File

@@ -18,7 +18,7 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useFeaturedTriggersRecommendations } from '@/service/use-plugins'
import { useAllTriggerPlugins, useInvalidateAllTriggerPlugins } from '@/service/use-triggers'
import { cn } from '@/utils/classnames'
@@ -54,7 +54,7 @@ const AllStartBlocks = ({
const { t } = useTranslation()
const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false)
const [hasPluginContent, setHasPluginContent] = useState(false)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const pluginRef = useRef<ListRef>(null)
const wrapElemRef = useRef<HTMLDivElement>(null)

View File

@@ -19,8 +19,8 @@ import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetLanguage } from '@/context/i18n'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import { getMarketplaceUrl } from '@/utils/var'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
@@ -167,7 +167,7 @@ const AllTools = ({
plugins: notInstalledPlugins = [],
} = useMarketplacePlugins()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
useEffect(() => {
if (!enable_marketplace)

View File

@@ -11,8 +11,8 @@ import {
useRef,
} from 'react'
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useGetLanguage } from '@/context/i18n'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
import { PluginCategoryEnum } from '../../plugins/types'
@@ -76,7 +76,7 @@ const DataSources = ({
onSelect(BlockEnum.DataSource, toolDefaultValue && defaultValue)
}, [onSelect])
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const {
queryPluginsWithDebounced: fetchPlugins,

View File

@@ -8,7 +8,7 @@ import type {
import { memo, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools'
import { cn } from '@/utils/classnames'
@@ -64,7 +64,7 @@ const Tabs: FC<TabsProps> = ({
const { data: workflowTools } = useAllWorkflowTools()
const { data: mcpTools } = useAllMCPTools()
const invalidateBuiltInTools = useInvalidateAllBuiltInTools()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const workflowStore = useWorkflowStore()
const inRAGPipeline = dataSources.length > 0
const {

View File

@@ -20,7 +20,7 @@ import Toast from '@/app/components/base/toast'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import AllTools from '@/app/components/workflow/block-selector/all-tools'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import {
createCustomCollection,
} from '@/service/tools'
@@ -70,7 +70,7 @@ const ToolPicker: FC<Props> = ({
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState<string[]>([])
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const invalidateCustomTools = useInvalidateAllCustomTools()

View File

@@ -16,7 +16,7 @@ import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hook
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import { CollectionType } from '@/app/components/tools/types'
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useStrategyProviders } from '@/service/use-strategy'
import { cn } from '@/utils/classnames'
import Tools from '../../../block-selector/tools'
@@ -95,7 +95,7 @@ export type AgentStrategySelectorProps = {
}
export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => {
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { enable_marketplace } = useSystemFeatures()
const { value, onChange } = props
const [open, setOpen] = useState(false)

View File

@@ -2,8 +2,8 @@
import { useSearchParams } from 'next/navigation'
import * as React from 'react'
import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import Header from '../signin/_header'
import ForgotPasswordForm from './ForgotPasswordForm'
@@ -12,7 +12,7 @@ const ForgotPassword = () => {
useDocumentTitle('')
const searchParams = useSearchParams()
const token = searchParams.get('token')
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
return (
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>

View File

@@ -1,12 +1,12 @@
'use client'
import * as React from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import Header from '../signin/_header'
import InstallForm from './installForm'
const Install = () => {
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
return (
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>

View File

@@ -1,13 +1,16 @@
import type { Viewport } from 'next'
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { Provider as JotaiProvider } from 'jotai'
import { ThemeProvider } from 'next-themes'
import { Instrument_Serif } from 'next/font/google'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
import GlobalPublicStoreProvider from '@/context/global-public-context'
import { TanstackQueryInitializer } from '@/context/query-client'
import { getQueryClientServer } from '@/context/query-client-server'
import { getLocaleOnServer } from '@/i18n-config/server'
import { DatasetAttr } from '@/types/feature'
import { cn } from '@/utils/classnames'
import { serverFetch } from '@/utils/ssr-fetch'
import { ToastProvider } from './components/base/toast'
import BrowserInitializer from './components/browser-initializer'
import { ReactScanLoader } from './components/devtools/react-scan/loader'
@@ -39,6 +42,18 @@ const LocaleLayout = async ({
children: React.ReactNode
}) => {
const locale = await getLocaleOnServer()
const queryClient = getQueryClientServer()
await Promise.all([
queryClient.prefetchQuery({
queryKey: ['systemFeatures'],
queryFn: () => serverFetch('/system-features'),
}),
queryClient.prefetchQuery({
queryKey: ['setupStatus'],
queryFn: () => serverFetch('/setup'),
}),
])
const datasetMap: Record<DatasetAttr, string | undefined> = {
[DatasetAttr.DATA_API_PREFIX]: process.env.NEXT_PUBLIC_API_PREFIX,
@@ -107,13 +122,15 @@ const LocaleLayout = async ({
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServerProvider>
<ToastProvider>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</ToastProvider>
</I18nServerProvider>
<HydrationBoundary state={dehydrate(queryClient)}>
<I18nServerProvider>
<ToastProvider>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</ToastProvider>
</I18nServerProvider>
</HydrationBoundary>
</TanstackQueryInitializer>
</SentryInitializer>
</BrowserInitializer>

View File

@@ -1,11 +1,11 @@
'use client'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import Header from '../signin/_header'
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
return (
<>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>

View File

@@ -3,8 +3,8 @@ import type { Locale } from '@/i18n-config'
import dynamic from 'next/dynamic'
import Divider from '@/app/components/base/divider'
import LocaleSigninSelect from '@/app/components/base/select/locale-signin'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useLocale } from '@/context/i18n'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { setLocaleOnClient } from '@/i18n-config'
import { languages } from '@/i18n-config/language'
@@ -20,7 +20,7 @@ const ThemeSelector = dynamic(() => import('@/app/components/base/theme-selector
const Header = () => {
const locale = useLocale()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
return (
<div className="flex w-full items-center justify-between p-6">

View File

@@ -12,7 +12,7 @@ import Loading from '@/app/components/base/loading'
import { SimpleSelect } from '@/app/components/base/select'
import Toast from '@/app/components/base/toast'
import { LICENSE_LINK } from '@/constants/link'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { setLocaleOnClient } from '@/i18n-config'
import { languages, LanguagesSupported } from '@/i18n-config/language'
import { activateMember } from '@/service/common'
@@ -22,7 +22,7 @@ import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
export default function InviteSettingsPage() {
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const router = useRouter()
const searchParams = useSearchParams()
const token = decodeURIComponent(searchParams.get('invite_token') as string)

View File

@@ -1,12 +1,12 @@
'use client'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
import Header from './_header'
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
useDocumentTitle('')
return (
<>

View File

@@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { invitationCheck } from '@/service/common'
import { useIsLogin } from '@/service/use-common'
import { LicenseStatus } from '@/types/feature'
@@ -30,7 +30,7 @@ const NormalForm = () => {
const [isInitCheckLoading, setInitCheckLoading] = useState(true)
const [isRedirecting, setIsRedirecting] = useState(false)
const isLoading = isCheckLoading || isInitCheckLoading || isRedirecting
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)

View File

@@ -2,8 +2,8 @@ import type { MockedFunction } from 'vitest'
import type { SystemFeatures } from '@/types/feature'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useLocale } from '@/context/i18n'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useSendMail } from '@/service/use-common'
import { defaultSystemFeatures } from '@/types/feature'
import Form from './input-mail'
@@ -33,7 +33,7 @@ vi.mock('next/link', () => ({
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(),
useSystemFeatures: vi.fn(),
}))
vi.mock('@/context/i18n', () => ({
@@ -46,7 +46,7 @@ vi.mock('@/service/use-common', () => ({
type UseSendMailResult = ReturnType<typeof useSendMail>
const mockUseGlobalPublicStore = useGlobalPublicStore as unknown as MockedFunction<typeof useGlobalPublicStore>
const mockUseSystemFeatures = useSystemFeatures as unknown as MockedFunction<typeof useSystemFeatures>
const mockUseLocale = useLocale as unknown as MockedFunction<typeof useLocale>
const mockUseSendMail = useSendMail as unknown as MockedFunction<typeof useSendMail>
@@ -57,11 +57,9 @@ const renderForm = ({
brandingEnabled?: boolean
isPending?: boolean
} = {}) => {
mockUseGlobalPublicStore.mockReturnValue({
systemFeatures: buildSystemFeatures({
branding: { enabled: brandingEnabled },
}),
})
mockUseSystemFeatures.mockReturnValue(buildSystemFeatures({
branding: { enabled: brandingEnabled },
}))
mockUseLocale.mockReturnValue('en-US')
mockUseSendMail.mockReturnValue({
mutateAsync: mockSubmitMail,

View File

@@ -8,8 +8,8 @@ import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast'
import Split from '@/app/signin/split'
import { emailRegex } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useLocale } from '@/context/i18n'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { useSendMail } from '@/service/use-common'
type Props = {
@@ -21,7 +21,7 @@ export default function Form({
const { t } = useTranslation()
const [email, setEmail] = useState('')
const locale = useLocale()
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
const { mutateAsync: submitMail, isPending } = useSendMail()

View File

@@ -1,12 +1,12 @@
'use client'
import Header from '@/app/signin/_header'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { cn } from '@/utils/classnames'
export default function RegisterLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
const systemFeatures = useSystemFeatures()
useDocumentTitle('')
return (
<>

View File

@@ -10,12 +10,12 @@ import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import { ZENDESK_FIELD_IDS } from '@/config'
import { useSystemFeatures } from '@/hooks/use-global-public'
import {
useCurrentWorkspace,
useLangGeniusVersion,
useUserProfile,
} from '@/service/use-common'
import { useGlobalPublicStore } from './global-public-context'
export type AppContextValue = {
userProfile: UserProfileResponse
@@ -89,7 +89,7 @@ export type AppContextProviderProps = {
export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => {
const queryClient = useQueryClient()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const { data: userProfileResp } = useUserProfile()
const { data: currentWorkspaceResp, isPending: isLoadingCurrentWorkspace, isFetching: isValidatingCurrentWorkspace } = useCurrentWorkspace()
const langGeniusVersionQuery = useLangGeniusVersion(

View File

@@ -1,61 +1,12 @@
'use client'
import type { FC, PropsWithChildren } from 'react'
import type { SystemFeatures } from '@/types/feature'
import { useQuery } from '@tanstack/react-query'
import { create } from 'zustand'
import Loading from '@/app/components/base/loading'
import { consoleClient } from '@/service/client'
import { defaultSystemFeatures } from '@/types/feature'
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
type GlobalPublicStore = {
systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void
}
export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
systemFeatures: defaultSystemFeatures,
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
}))
const systemFeaturesQueryKey = ['systemFeatures'] as const
const setupStatusQueryKey = ['setupStatus'] as const
async function fetchSystemFeatures() {
const data = await consoleClient.systemFeatures()
const { setSystemFeatures } = useGlobalPublicStore.getState()
setSystemFeatures({ ...defaultSystemFeatures, ...data })
return data
}
export function useSystemFeaturesQuery() {
return useQuery({
queryKey: systemFeaturesQueryKey,
queryFn: fetchSystemFeatures,
})
}
export function useIsSystemFeaturesPending() {
const { isPending } = useSystemFeaturesQuery()
return isPending
}
export function useSetupStatusQuery() {
return useQuery({
queryKey: setupStatusQueryKey,
queryFn: fetchSetupStatusWithCache,
staleTime: Infinity,
})
}
import { useSetupStatusQuery, useSystemFeaturesQuery } from '@/hooks/use-global-public'
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
children,
}) => {
// Fetch systemFeatures and setupStatus in parallel to reduce waterfall.
// setupStatus is prefetched here and cached in localStorage for AppInitializer.
const { isPending } = useSystemFeaturesQuery()
// Prefetch setupStatus for AppInitializer (result not needed here)
useSetupStatusQuery()
if (isPending)

View File

@@ -8,9 +8,9 @@ import { useEffect } from 'react'
import { create } from 'zustand'
import { getProcessedSystemVariablesFromUrlParams } from '@/app/components/base/chat/utils'
import Loading from '@/app/components/base/loading'
import { useIsSystemFeaturesPending } from '@/hooks/use-global-public'
import { AccessMode } from '@/models/access-control'
import { useGetWebAppAccessModeByCode } from '@/service/use-share'
import { useIsSystemFeaturesPending } from './global-public-context'
type WebAppStore = {
shareCode: string | null

View File

@@ -1,3 +1,4 @@
import type { SetupStatusResponse } from '@/models/common'
import type { SystemFeatures } from '@/types/feature'
import { type } from '@orpc/contract'
import { base } from '../base'
@@ -9,3 +10,11 @@ export const systemFeaturesContract = base
})
.input(type<unknown>())
.output(type<SystemFeatures>())
export const setupStatusContract = base
.route({
path: '/setup',
method: 'GET',
})
.input(type<unknown>())
.output(type<SetupStatusResponse>())

View File

@@ -1,6 +1,6 @@
import type { InferContractRouterInputs } from '@orpc/contract'
import { bindPartnerStackContract, invoicesContract } from './console/billing'
import { systemFeaturesContract } from './console/system'
import { setupStatusContract, systemFeaturesContract } from './console/system'
import { trialAppDatasetsContract, trialAppInfoContract, trialAppParametersContract, trialAppWorkflowsContract } from './console/try-app'
import { collectionPluginsContract, collectionsContract, searchAdvancedContract } from './marketplace'
@@ -14,6 +14,7 @@ export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRout
export const consoleRouterContract = {
systemFeatures: systemFeaturesContract,
setupStatus: setupStatusContract,
trialApps: {
info: trialAppInfoContract,
datasets: trialAppDatasetsContract,

View File

@@ -24,7 +24,7 @@
},
"__tests__/embedded-user-id-store.test.tsx": {
"ts/no-explicit-any": {
"count": 3
"count": 1
}
},
"__tests__/goto-anything/command-selector.test.tsx": {
@@ -104,11 +104,6 @@
"count": 1
}
},
"app/(shareLayout)/webapp-reset-password/layout.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx": {
"ts/no-explicit-any": {
"count": 2
@@ -656,7 +651,7 @@
},
"app/components/apps/app-card.spec.tsx": {
"ts/no-explicit-any": {
"count": 22
"count": 20
}
},
"app/components/apps/app-card.tsx": {
@@ -1674,7 +1669,7 @@
},
"app/components/custom/custom-web-app-brand/index.spec.tsx": {
"ts/no-explicit-any": {
"count": 7
"count": 6
}
},
"app/components/custom/custom-web-app-brand/index.tsx": {
@@ -2543,7 +2538,7 @@
},
"app/components/plugins/plugin-item/index.spec.tsx": {
"ts/no-explicit-any": {
"count": 10
"count": 8
}
},
"app/components/plugins/plugin-item/index.tsx": {
@@ -2566,7 +2561,7 @@
},
"app/components/plugins/plugin-page/empty/index.spec.tsx": {
"ts/no-explicit-any": {
"count": 7
"count": 5
}
},
"app/components/plugins/plugin-page/empty/index.tsx": {
@@ -4346,11 +4341,6 @@
"count": 1
}
},
"context/global-public-context.tsx": {
"react-refresh/only-export-components": {
"count": 4
}
},
"context/hooks/use-trigger-events-limit-modal.ts": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 3

View File

@@ -1,5 +1,5 @@
import { act, renderHook } from '@testing-library/react'
import { useGlobalPublicStore, useIsSystemFeaturesPending } from '@/context/global-public-context'
import { renderHook } from '@testing-library/react'
import { useIsSystemFeaturesPending, useSystemFeatures } from '@/hooks/use-global-public'
/**
* Test suite for useDocumentTitle hook
*
@@ -15,13 +15,10 @@ import { useGlobalPublicStore, useIsSystemFeaturesPending } from '@/context/glob
import { defaultSystemFeatures } from '@/types/feature'
import useDocumentTitle from './use-document-title'
vi.mock('@/context/global-public-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/global-public-context')>()
return {
...actual,
useIsSystemFeaturesPending: vi.fn(() => false),
}
})
vi.mock('@/context/global-public-context', () => ({
useSystemFeatures: vi.fn(() => ({ ...defaultSystemFeatures })),
useIsSystemFeaturesPending: vi.fn(() => false),
}))
/**
* Test behavior when system features are still loading
@@ -30,11 +27,7 @@ vi.mock('@/context/global-public-context', async (importOriginal) => {
describe('title should be empty if systemFeatures is pending', () => {
beforeEach(() => {
vi.mocked(useIsSystemFeaturesPending).mockReturnValue(true)
act(() => {
useGlobalPublicStore.setState({
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
})
})
vi.mocked(useSystemFeatures).mockReturnValue({ ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } })
})
/**
* Test that title stays empty during loading even when a title is provided
@@ -59,11 +52,7 @@ describe('title should be empty if systemFeatures is pending', () => {
describe('use default branding', () => {
beforeEach(() => {
vi.mocked(useIsSystemFeaturesPending).mockReturnValue(false)
act(() => {
useGlobalPublicStore.setState({
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
})
})
vi.mocked(useSystemFeatures).mockReturnValue({ ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } })
})
/**
* Test title format with page title and default branding
@@ -91,11 +80,7 @@ describe('use default branding', () => {
describe('use specific branding', () => {
beforeEach(() => {
vi.mocked(useIsSystemFeaturesPending).mockReturnValue(false)
act(() => {
useGlobalPublicStore.setState({
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } },
})
})
vi.mocked(useSystemFeatures).mockReturnValue({ ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } })
})
/**
* Test title format with page title and custom branding

View File

@@ -1,12 +1,12 @@
'use client'
import { useFavicon, useTitle } from 'ahooks'
import { useEffect } from 'react'
import { useGlobalPublicStore, useIsSystemFeaturesPending } from '@/context/global-public-context'
import { useIsSystemFeaturesPending, useSystemFeatures } from '@/hooks/use-global-public'
import { basePath } from '@/utils/var'
export default function useDocumentTitle(title: string) {
const isPending = useIsSystemFeaturesPending()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
const prefix = title ? `${title} - ` : ''
let titleStr = ''
let favicon = ''

View File

@@ -0,0 +1,30 @@
import type { SystemFeatures } from '@/types/feature'
import { useQuery } from '@tanstack/react-query'
import { consoleClient, consoleQuery } from '@/service/client'
import { defaultSystemFeatures } from '@/types/feature'
import { fetchSetupStatusWithCache } from '@/utils/setup-status'
export function useSystemFeaturesQuery() {
return useQuery({
queryKey: consoleQuery.systemFeatures.queryKey(),
queryFn: () => consoleClient.systemFeatures(),
})
}
export function useSystemFeatures(): SystemFeatures {
const { data } = useSystemFeaturesQuery()
return { ...defaultSystemFeatures, ...data }
}
export function useIsSystemFeaturesPending() {
const { isPending } = useSystemFeaturesQuery()
return isPending
}
export function useSetupStatusQuery() {
return useQuery({
queryKey: consoleQuery.setupStatus.queryKey(),
queryFn: fetchSetupStatusWithCache,
staleTime: Infinity,
})
}

View File

@@ -23,7 +23,7 @@ export type OauthResponse = {
export type SetupStatusResponse = {
step: 'finished' | 'not_started'
setup_at?: Date
setup_at?: string
}
export type InitValidateStatusResponse = {

View File

@@ -1,7 +1,7 @@
import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control'
import type { App } from '@/types/app'
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { get, post } from './base'
import { getUserCanAccess } from './share'
@@ -71,7 +71,7 @@ export const useUpdateAccessMode = () => {
}
export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled }: { appId?: string, isInstalledApp?: boolean, enabled?: boolean }) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
return useQuery({
queryKey: [NAME_SPACE, 'user-can-access-app', appId, systemFeatures.webapp_auth.enabled, isInstalledApp],
queryFn: () => {

View File

@@ -92,8 +92,6 @@ export const useUserProfile = () => {
},
}
},
staleTime: 0,
gcTime: 0,
})
}

View File

@@ -1,6 +1,6 @@
import type { App, AppCategory } from '@/models/explore'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useSystemFeatures } from '@/hooks/use-global-public'
import { AccessMode } from '@/models/access-control'
import { fetchAppList, fetchBanners, fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore'
import { AppSourceType, fetchAppMeta, fetchAppParams } from './share'
@@ -57,7 +57,7 @@ export const useUpdateAppPinStatus = () => {
}
export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const systemFeatures = useSystemFeatures()
return useQuery({
queryKey: [NAME_SPACE, 'appAccessMode', appId, systemFeatures.webapp_auth.enabled],
queryFn: () => {

View File

@@ -1,14 +1,14 @@
import type { SetupStatusResponse } from '@/models/common'
import { fetchSetupStatus } from '@/service/common'
import { consoleClient } from '@/service/client'
import { fetchSetupStatusWithCache } from './setup-status'
vi.mock('@/service/common', () => ({
fetchSetupStatus: vi.fn(),
vi.mock('@/service/client', () => ({
consoleClient: {
setupStatus: vi.fn(),
},
}))
const mockFetchSetupStatus = vi.mocked(fetchSetupStatus)
const mockSetupStatus = vi.mocked(consoleClient.setupStatus)
describe('setup-status utilities', () => {
beforeEach(() => {
@@ -24,7 +24,7 @@ describe('setup-status utilities', () => {
const result = await fetchSetupStatusWithCache()
expect(result).toEqual({ step: 'finished' })
expect(mockFetchSetupStatus).not.toHaveBeenCalled()
expect(mockSetupStatus).not.toHaveBeenCalled()
})
it('should not modify localStorage when returning cached value', async () => {
@@ -39,22 +39,22 @@ describe('setup-status utilities', () => {
describe('when cache does not exist', () => {
it('should call API and cache finished status', async () => {
const apiResponse: SetupStatusResponse = { step: 'finished' }
mockFetchSetupStatus.mockResolvedValue(apiResponse)
mockSetupStatus.mockResolvedValue(apiResponse)
const result = await fetchSetupStatusWithCache()
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
expect(mockSetupStatus).toHaveBeenCalledTimes(1)
expect(result).toEqual(apiResponse)
expect(localStorage.getItem('setup_status')).toBe('finished')
})
it('should call API and remove cache when not finished', async () => {
const apiResponse: SetupStatusResponse = { step: 'not_started' }
mockFetchSetupStatus.mockResolvedValue(apiResponse)
mockSetupStatus.mockResolvedValue(apiResponse)
const result = await fetchSetupStatusWithCache()
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
expect(mockSetupStatus).toHaveBeenCalledTimes(1)
expect(result).toEqual(apiResponse)
expect(localStorage.getItem('setup_status')).toBeNull()
})
@@ -62,7 +62,7 @@ describe('setup-status utilities', () => {
it('should clear stale cache when API returns not_started', async () => {
localStorage.setItem('setup_status', 'some_invalid_value')
const apiResponse: SetupStatusResponse = { step: 'not_started' }
mockFetchSetupStatus.mockResolvedValue(apiResponse)
mockSetupStatus.mockResolvedValue(apiResponse)
const result = await fetchSetupStatusWithCache()
@@ -75,44 +75,44 @@ describe('setup-status utilities', () => {
it('should call API when cache value is empty string', async () => {
localStorage.setItem('setup_status', '')
const apiResponse: SetupStatusResponse = { step: 'finished' }
mockFetchSetupStatus.mockResolvedValue(apiResponse)
mockSetupStatus.mockResolvedValue(apiResponse)
const result = await fetchSetupStatusWithCache()
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
expect(mockSetupStatus).toHaveBeenCalledTimes(1)
expect(result).toEqual(apiResponse)
})
it('should call API when cache value is not "finished"', async () => {
localStorage.setItem('setup_status', 'not_started')
const apiResponse: SetupStatusResponse = { step: 'finished' }
mockFetchSetupStatus.mockResolvedValue(apiResponse)
mockSetupStatus.mockResolvedValue(apiResponse)
const result = await fetchSetupStatusWithCache()
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
expect(mockSetupStatus).toHaveBeenCalledTimes(1)
expect(result).toEqual(apiResponse)
})
it('should call API when localStorage key does not exist', async () => {
const apiResponse: SetupStatusResponse = { step: 'finished' }
mockFetchSetupStatus.mockResolvedValue(apiResponse)
mockSetupStatus.mockResolvedValue(apiResponse)
const result = await fetchSetupStatusWithCache()
expect(mockFetchSetupStatus).toHaveBeenCalledTimes(1)
expect(mockSetupStatus).toHaveBeenCalledTimes(1)
expect(result).toEqual(apiResponse)
})
})
describe('API response handling', () => {
it('should preserve setup_at from API response', async () => {
const setupDate = new Date('2024-01-01')
const setupDate = '2024-01-01T00:00:00.000Z'
const apiResponse: SetupStatusResponse = {
step: 'finished',
setup_at: setupDate,
}
mockFetchSetupStatus.mockResolvedValue(apiResponse)
mockSetupStatus.mockResolvedValue(apiResponse)
const result = await fetchSetupStatusWithCache()
@@ -122,13 +122,13 @@ describe('setup-status utilities', () => {
it('should propagate API errors', async () => {
const apiError = new Error('Network error')
mockFetchSetupStatus.mockRejectedValue(apiError)
mockSetupStatus.mockRejectedValue(apiError)
await expect(fetchSetupStatusWithCache()).rejects.toThrow('Network error')
})
it('should not update cache when API call fails', async () => {
mockFetchSetupStatus.mockRejectedValue(new Error('API error'))
mockSetupStatus.mockRejectedValue(new Error('API error'))
await expect(fetchSetupStatusWithCache()).rejects.toThrow()

Some files were not shown because too many files have changed in this diff Show More