Compare commits

..

3 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
3216b67bfa refactor: examples of use match case (#31312)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-01 19:25:54 +09:00
210 changed files with 1518 additions and 47682 deletions

View File

@ -107,10 +107,11 @@ class AnnotationReplyActionApi(Resource):
def post(self, app_id, action: Literal["enable", "disable"]):
app_id = str(app_id)
args = AnnotationReplyPayload.model_validate(console_ns.payload)
if action == "enable":
result = AppAnnotationService.enable_app_annotation(args.model_dump(), app_id)
elif action == "disable":
result = AppAnnotationService.disable_app_annotation(app_id)
match action:
case "enable":
result = AppAnnotationService.enable_app_annotation(args.model_dump(), app_id)
case "disable":
result = AppAnnotationService.disable_app_annotation(app_id)
return result, 200

View File

@ -155,43 +155,43 @@ class OAuthServerUserTokenApi(Resource):
grant_type = OAuthGrantType(payload.grant_type)
except ValueError:
raise BadRequest("invalid grant_type")
match grant_type:
case OAuthGrantType.AUTHORIZATION_CODE:
if not payload.code:
raise BadRequest("code is required")
if grant_type == OAuthGrantType.AUTHORIZATION_CODE:
if not payload.code:
raise BadRequest("code is required")
if payload.client_secret != oauth_provider_app.client_secret:
raise BadRequest("client_secret is invalid")
if payload.client_secret != oauth_provider_app.client_secret:
raise BadRequest("client_secret is invalid")
if payload.redirect_uri not in oauth_provider_app.redirect_uris:
raise BadRequest("redirect_uri is invalid")
if payload.redirect_uri not in oauth_provider_app.redirect_uris:
raise BadRequest("redirect_uri is invalid")
access_token, refresh_token = OAuthServerService.sign_oauth_access_token(
grant_type, code=payload.code, client_id=oauth_provider_app.client_id
)
return jsonable_encoder(
{
"access_token": access_token,
"token_type": "Bearer",
"expires_in": OAUTH_ACCESS_TOKEN_EXPIRES_IN,
"refresh_token": refresh_token,
}
)
case OAuthGrantType.REFRESH_TOKEN:
if not payload.refresh_token:
raise BadRequest("refresh_token is required")
access_token, refresh_token = OAuthServerService.sign_oauth_access_token(
grant_type, code=payload.code, client_id=oauth_provider_app.client_id
)
return jsonable_encoder(
{
"access_token": access_token,
"token_type": "Bearer",
"expires_in": OAUTH_ACCESS_TOKEN_EXPIRES_IN,
"refresh_token": refresh_token,
}
)
elif grant_type == OAuthGrantType.REFRESH_TOKEN:
if not payload.refresh_token:
raise BadRequest("refresh_token is required")
access_token, refresh_token = OAuthServerService.sign_oauth_access_token(
grant_type, refresh_token=payload.refresh_token, client_id=oauth_provider_app.client_id
)
return jsonable_encoder(
{
"access_token": access_token,
"token_type": "Bearer",
"expires_in": OAUTH_ACCESS_TOKEN_EXPIRES_IN,
"refresh_token": refresh_token,
}
)
access_token, refresh_token = OAuthServerService.sign_oauth_access_token(
grant_type, refresh_token=payload.refresh_token, client_id=oauth_provider_app.client_id
)
return jsonable_encoder(
{
"access_token": access_token,
"token_type": "Bearer",
"expires_in": OAUTH_ACCESS_TOKEN_EXPIRES_IN,
"refresh_token": refresh_token,
}
)
@console_ns.route("/oauth/provider/account")

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
{"openapi":"3.0.0","info":{"title":"Dify API (FastOpenAPI PoC)","version":"1.0","description":"FastOpenAPI proof of concept for Dify API"},"paths":{"/console/api/ping":{"get":{"summary":"Health check endpoint for connection testing.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PingResponse"}}}}},"tags":["console"]}}},"components":{"schemas":{"ErrorSchema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string"},"message":{"type":"string"},"status":{"type":"integer"},"details":{"type":"string"}},"required":["type","message","status"]}},"required":["error"]},"PingResponse":{"properties":{"result":{"description":"Health check result","examples":["pong"],"title":"Result","type":"string"}},"required":["result"],"title":"PingResponse","type":"object"}}}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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,377 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import { oc } from '@orpc/contract'
import { z } from 'zod'
import { zAudioToTextData, zAudioToTextResponse2, zCreateAnnotationData, zCreateAnnotationResponse, zDeleteAnnotationData, zDeleteConversationData, zGetAnnotationListData, zGetAnnotationListResponse, zGetChatAppFeedbacksData, zGetChatAppFeedbacksResponse, zGetChatAppInfoResponse, zGetChatAppMetaResponse, zGetChatAppParametersData, zGetChatAppParametersResponse, zGetChatWebAppSettingsResponse, zGetConversationHistoryData, zGetConversationHistoryResponse, zGetConversationsListData, zGetConversationsListResponse, zGetConversationVariablesData, zGetConversationVariablesResponse, zGetInitialAnnotationReplySettingsStatusData, zGetInitialAnnotationReplySettingsStatusResponse, zGetSuggestedQuestionsData, zGetSuggestedQuestionsResponse, zInitialAnnotationReplySettingsData, zInitialAnnotationReplySettingsResponse2, zPostChatMessageFeedbackData, zPostChatMessageFeedbackResponse, zPreviewChatFileData, zPreviewChatFileResponse, zRenameConversationData, zRenameConversationResponse, zSendChatMessageData, zSendChatMessageResponse, zStopChatMessageGenerationData, zStopChatMessageGenerationResponse, zTextToAudioChatData, zTextToAudioChatResponse, zUpdateAnnotationData, zUpdateAnnotationResponse, zUploadChatFileData, zUploadChatFileResponse } from './zod'
export const base = oc.$route({ inputStructure: 'detailed', outputStructure: 'detailed' })
/**
* Send Chat Message
*
* Send a request to the chat application.
*/
export const sendChatMessageContract = base.route({
method: 'POST',
path: '/chat-messages',
operationId: 'sendChatMessage',
summary: 'Send Chat Message',
description: 'Send a request to the chat application.',
tags: ['Chat'],
}).input(zSendChatMessageData).output(z.object({ body: zSendChatMessageResponse, status: z.literal(200) }))
/**
* File Upload
*
* Upload a file (currently only images are supported) for use when sending messages, enabling multimodal understanding of images and text. Supports png, jpg, jpeg, webp, gif formats. Uploaded files are for use by the current end-user only.
*/
export const uploadChatFileContract = base.route({
method: 'POST',
path: '/files/upload',
operationId: 'uploadChatFile',
summary: 'File Upload',
description: 'Upload a file (currently only images are supported) for use when sending messages, enabling multimodal understanding of images and text. Supports png, jpg, jpeg, webp, gif formats. Uploaded files are for use by the current end-user only.',
tags: ['Files'],
}).input(zUploadChatFileData).output(z.object({ body: zUploadChatFileResponse, status: z.literal(200) }))
/**
* File Preview
*
* Preview or download uploaded files. This endpoint allows you to access files that have been previously uploaded via the File Upload API. Files can only be accessed if they belong to messages within the requesting application.
*/
export const previewChatFileContract = base.route({
method: 'GET',
path: '/files/{file_id}/preview',
operationId: 'previewChatFile',
summary: 'File Preview',
description: 'Preview or download uploaded files. This endpoint allows you to access files that have been previously uploaded via the File Upload API. Files can only be accessed if they belong to messages within the requesting application.',
tags: ['Files'],
}).input(zPreviewChatFileData).output(z.object({ body: zPreviewChatFileResponse, status: z.literal(200) }))
/**
* Stop Chat Message Generation
*
* Stops a chat message generation task. Only supported in streaming mode.
*/
export const stopChatMessageGenerationContract = base.route({
method: 'POST',
path: '/chat-messages/{task_id}/stop',
operationId: 'stopChatMessageGeneration',
summary: 'Stop Chat Message Generation',
description: 'Stops a chat message generation task. Only supported in streaming mode.',
tags: ['Chat'],
}).input(zStopChatMessageGenerationData).output(z.object({ body: zStopChatMessageGenerationResponse, status: z.literal(200) }))
/**
* Message Feedback
*
* End-users can provide feedback messages, facilitating application developers to optimize expected outputs.
*/
export const postChatMessageFeedbackContract = base.route({
method: 'POST',
path: '/messages/{message_id}/feedbacks',
operationId: 'postChatMessageFeedback',
summary: 'Message Feedback',
description: 'End-users can provide feedback messages, facilitating application developers to optimize expected outputs.',
tags: ['Feedback'],
}).input(zPostChatMessageFeedbackData).output(z.object({ body: zPostChatMessageFeedbackResponse, status: z.literal(200) }))
/**
* Get feedbacks of application
*
* Get application's feedbacks.
*/
export const getChatAppFeedbacksContract = base.route({
method: 'GET',
path: '/app/feedbacks',
operationId: 'getChatAppFeedbacks',
summary: 'Get feedbacks of application',
description: 'Get application\'s feedbacks.',
tags: ['Feedback'],
}).input(zGetChatAppFeedbacksData).output(z.object({ body: zGetChatAppFeedbacksResponse, status: z.literal(200) }))
/**
* Next Suggested Questions
*
* Get next questions suggestions for the current message.
*/
export const getSuggestedQuestionsContract = base.route({
method: 'GET',
path: '/messages/{message_id}/suggested',
operationId: 'getSuggestedQuestions',
summary: 'Next Suggested Questions',
description: 'Get next questions suggestions for the current message.',
tags: ['Chat'],
}).input(zGetSuggestedQuestionsData).output(z.object({ body: zGetSuggestedQuestionsResponse, status: z.literal(200) }))
/**
* Get Conversation History Messages
*
* Returns historical chat records in a scrolling load format, with the first page returning the latest `{limit}` messages, i.e., in reverse order.
*/
export const getConversationHistoryContract = base.route({
method: 'GET',
path: '/messages',
operationId: 'getConversationHistory',
summary: 'Get Conversation History Messages',
description: 'Returns historical chat records in a scrolling load format, with the first page returning the latest `{limit}` messages, i.e., in reverse order.',
tags: ['Conversations'],
}).input(zGetConversationHistoryData).output(z.object({ body: zGetConversationHistoryResponse, status: z.literal(200) }))
/**
* Get Conversations
*
* Retrieve the conversation list for the current user, defaulting to the most recent 20 entries.
*/
export const getConversationsListContract = base.route({
method: 'GET',
path: '/conversations',
operationId: 'getConversationsList',
summary: 'Get Conversations',
description: 'Retrieve the conversation list for the current user, defaulting to the most recent 20 entries.',
tags: ['Conversations'],
}).input(zGetConversationsListData).output(z.object({ body: zGetConversationsListResponse, status: z.literal(200) }))
/**
* Delete Conversation
*
* Delete a conversation.
*/
export const deleteConversationContract = base.route({
method: 'DELETE',
path: '/conversations/{conversation_id}',
operationId: 'deleteConversation',
summary: 'Delete Conversation',
description: 'Delete a conversation.',
tags: ['Conversations'],
}).input(zDeleteConversationData)
/**
* Conversation Rename
*
* Rename the session. The session name is used for display on clients that support multiple sessions.
*/
export const renameConversationContract = base.route({
method: 'POST',
path: '/conversations/{conversation_id}/name',
operationId: 'renameConversation',
summary: 'Conversation Rename',
description: 'Rename the session. The session name is used for display on clients that support multiple sessions.',
tags: ['Conversations'],
}).input(zRenameConversationData).output(z.object({ body: zRenameConversationResponse, status: z.literal(200) }))
/**
* Get Conversation Variables
*
* Retrieve variables from a specific conversation.
*/
export const getConversationVariablesContract = base.route({
method: 'GET',
path: '/conversations/{conversation_id}/variables',
operationId: 'getConversationVariables',
summary: 'Get Conversation Variables',
description: 'Retrieve variables from a specific conversation.',
tags: ['Conversations'],
}).input(zGetConversationVariablesData).output(z.object({ body: zGetConversationVariablesResponse, status: z.literal(200) }))
/**
* Speech to Text
*
* Convert audio file to text. Supported formats: mp3, mp4, mpeg, mpga, m4a, wav, webm. File size limit: 15MB.
*/
export const audioToTextContract = base.route({
method: 'POST',
path: '/audio-to-text',
operationId: 'audioToText',
summary: 'Speech to Text',
description: 'Convert audio file to text. Supported formats: mp3, mp4, mpeg, mpga, m4a, wav, webm. File size limit: 15MB.',
tags: ['TTS'],
}).input(zAudioToTextData).output(z.object({ body: zAudioToTextResponse2, status: z.literal(200) }))
/**
* Text to Audio
*
* Convert text to speech.
*/
export const textToAudioChatContract = base.route({
method: 'POST',
path: '/text-to-audio',
operationId: 'textToAudioChat',
summary: 'Text to Audio',
description: 'Convert text to speech.',
tags: ['TTS'],
}).input(zTextToAudioChatData).output(z.object({ body: zTextToAudioChatResponse, status: z.literal(200) }))
/**
* Get Application Basic Information
*
* Used to get basic information about this application.
*/
export const getChatAppInfoContract = base.route({
method: 'GET',
path: '/info',
operationId: 'getChatAppInfo',
summary: 'Get Application Basic Information',
description: 'Used to get basic information about this application.',
tags: ['Application'],
}).output(z.object({ body: zGetChatAppInfoResponse, status: z.literal(200) }))
/**
* Get Application Parameters Information
*
* Used at the start of entering the page to obtain information such as features, input parameter names, types, and default values.
*/
export const getChatAppParametersContract = base.route({
method: 'GET',
path: '/parameters',
operationId: 'getChatAppParameters',
summary: 'Get Application Parameters Information',
description: 'Used at the start of entering the page to obtain information such as features, input parameter names, types, and default values.',
tags: ['Application'],
}).input(zGetChatAppParametersData).output(z.object({ body: zGetChatAppParametersResponse, status: z.literal(200) }))
/**
* Get Application Meta Information
*
* Used to get icons of tools in this application.
*/
export const getChatAppMetaContract = base.route({
method: 'GET',
path: '/meta',
operationId: 'getChatAppMeta',
summary: 'Get Application Meta Information',
description: 'Used to get icons of tools in this application.',
tags: ['Application'],
}).output(z.object({ body: zGetChatAppMetaResponse, status: z.literal(200) }))
/**
* Get Application WebApp Settings
*
* Used to get the WebApp settings of the application.
*/
export const getChatWebAppSettingsContract = base.route({
method: 'GET',
path: '/site',
operationId: 'getChatWebAppSettings',
summary: 'Get Application WebApp Settings',
description: 'Used to get the WebApp settings of the application.',
tags: ['Application'],
}).output(z.object({ body: zGetChatWebAppSettingsResponse, status: z.literal(200) }))
/**
* Get Annotation List
*
* Retrieves a list of annotations for the application.
*/
export const getAnnotationListContract = base.route({
method: 'GET',
path: '/apps/annotations',
operationId: 'getAnnotationList',
summary: 'Get Annotation List',
description: 'Retrieves a list of annotations for the application.',
tags: ['Annotations'],
}).input(zGetAnnotationListData).output(z.object({ body: zGetAnnotationListResponse, status: z.literal(200) }))
/**
* Create Annotation
*
* Creates a new annotation.
*/
export const createAnnotationContract = base.route({
method: 'POST',
path: '/apps/annotations',
operationId: 'createAnnotation',
summary: 'Create Annotation',
description: 'Creates a new annotation.',
tags: ['Annotations'],
}).input(zCreateAnnotationData).output(z.object({ body: zCreateAnnotationResponse, status: z.literal(200) }))
/**
* Delete Annotation
*
* Deletes an annotation.
*/
export const deleteAnnotationContract = base.route({
method: 'DELETE',
path: '/apps/annotations/{annotation_id}',
operationId: 'deleteAnnotation',
summary: 'Delete Annotation',
description: 'Deletes an annotation.',
tags: ['Annotations'],
}).input(zDeleteAnnotationData)
/**
* Update Annotation
*
* Updates an existing annotation.
*/
export const updateAnnotationContract = base.route({
method: 'PUT',
path: '/apps/annotations/{annotation_id}',
operationId: 'updateAnnotation',
summary: 'Update Annotation',
description: 'Updates an existing annotation.',
tags: ['Annotations'],
}).input(zUpdateAnnotationData).output(z.object({ body: zUpdateAnnotationResponse, status: z.literal(200) }))
/**
* Initial Annotation Reply Settings
*
* Enable or disable annotation reply settings and configure embedding models. This interface is executed asynchronously.
*/
export const initialAnnotationReplySettingsContract = base.route({
method: 'POST',
path: '/apps/annotation-reply/{action}',
operationId: 'initialAnnotationReplySettings',
summary: 'Initial Annotation Reply Settings',
description: 'Enable or disable annotation reply settings and configure embedding models. This interface is executed asynchronously.',
tags: ['Annotations'],
}).input(zInitialAnnotationReplySettingsData).output(z.object({ body: zInitialAnnotationReplySettingsResponse2, status: z.literal(200) }))
/**
* Query Initial Annotation Reply Settings Task Status
*
* Queries the status of an asynchronously executed annotation reply settings task.
*/
export const getInitialAnnotationReplySettingsStatusContract = base.route({
method: 'GET',
path: '/apps/annotation-reply/{action}/status/{job_id}',
operationId: 'getInitialAnnotationReplySettingsStatus',
summary: 'Query Initial Annotation Reply Settings Task Status',
description: 'Queries the status of an asynchronously executed annotation reply settings task.',
tags: ['Annotations'],
}).input(zGetInitialAnnotationReplySettingsStatusData).output(z.object({ body: zGetInitialAnnotationReplySettingsStatusResponse, status: z.literal(200) }))
export const router = {
chatMessages: { send: sendChatMessageContract, stopGeneration: stopChatMessageGenerationContract },
files: { uploadChat: uploadChatFileContract, previewChat: previewChatFileContract },
messages: {
postChatFeedback: postChatMessageFeedbackContract,
getSuggestedQuestions: getSuggestedQuestionsContract,
getConversationHistory: getConversationHistoryContract,
},
app: { getChatFeedbacks: getChatAppFeedbacksContract },
conversations: {
getList: getConversationsListContract,
delete: deleteConversationContract,
rename: renameConversationContract,
getVariables: getConversationVariablesContract,
},
audioToText: { audioToText: audioToTextContract },
textToAudio: { textToAudioChat: textToAudioChatContract },
info: { getChatApp: getChatAppInfoContract },
parameters: { getChatApp: getChatAppParametersContract },
meta: { getChatApp: getChatAppMetaContract },
site: { getChatWebAppSettings: getChatWebAppSettingsContract },
apps: {
getAnnotationList: getAnnotationListContract,
createAnnotation: createAnnotationContract,
deleteAnnotation: deleteAnnotationContract,
updateAnnotation: updateAnnotationContract,
initialAnnotationReplySettings: initialAnnotationReplySettingsContract,
getInitialAnnotationReplySettingsStatus: getInitialAnnotationReplySettingsStatusContract,
},
}
export type Router = typeof router

View File

@ -1,20 +0,0 @@
// This file is auto-generated by @hey-api/openapi-ts
import { z } from 'zod'
import { zGetChatAppFeedbacksData, zGetChatAppFeedbacksResponse } from '../../zod/api/app'
import { base } from '../common'
/**
* Get feedbacks of application
*
* Get application's feedbacks.
*/
export const getChatAppFeedbacksContract = base.route({
method: 'GET',
path: '/app/feedbacks',
operationId: 'getChatAppFeedbacks',
summary: 'Get feedbacks of application',
description: 'Get application\'s feedbacks.',
tags: ['Feedback'],
}).input(zGetChatAppFeedbacksData).output(z.object({ body: zGetChatAppFeedbacksResponse, status: z.literal(200) }))

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