refactor(web): move app context layout styles to shell (#38511)

This commit is contained in:
yyh 2026-07-07 17:47:30 +08:00 committed by GitHub
parent 31b17513c2
commit 2c6ec1a761
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 73 additions and 47 deletions

View File

@ -1,8 +1,9 @@
import type { ReactNode } from 'react' import * as React from 'react'
import AmplitudeProvider from '@/app/components/base/amplitude' import AmplitudeProvider from '@/app/components/base/amplitude'
import { GoogleAnalyticsScripts } from '@/app/components/base/ga' import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
import Zendesk from '@/app/components/base/zendesk' import Zendesk from '@/app/components/base/zendesk'
import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder' import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import MainNavLayout from '@/app/components/main-nav/layout' import MainNavLayout from '@/app/components/main-nav/layout'
import { NextRouteStateBridge } from '@/app/components/next-route-state' import { NextRouteStateBridge } from '@/app/components/next-route-state'
import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics' import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
@ -17,32 +18,35 @@ export default async function Layout({
children, children,
detailSidebar, detailSidebar,
}: { }: {
children: ReactNode children: React.ReactNode
detailSidebar: ReactNode detailSidebar: React.ReactNode
}) { }) {
return ( return (
<> <React.Fragment>
<GoogleAnalyticsScripts /> <GoogleAnalyticsScripts />
<AmplitudeProvider /> <AmplitudeProvider />
<OAuthRegistrationAnalytics /> <OAuthRegistrationAnalytics />
<EducationVerifyActionRecorder /> <EducationVerifyActionRecorder />
<CommonLayoutHydrationBoundary> <CommonLayoutHydrationBoundary>
<NextRouteStateBridge> <NextRouteStateBridge>
<AppContextProvider> <div className="flex h-full flex-col overflow-hidden">
<EventEmitterContextProvider> <MaintenanceNotice />
<ProviderContextProvider> <AppContextProvider>
<ModalContextProvider> <EventEmitterContextProvider>
<MainNavLayout detailSidebar={detailSidebar}> <ProviderContextProvider>
{children} <ModalContextProvider>
</MainNavLayout> <MainNavLayout detailSidebar={detailSidebar}>
<CommonLayoutGlobalMounts /> {children}
</ModalContextProvider> </MainNavLayout>
</ProviderContextProvider> <CommonLayoutGlobalMounts />
</EventEmitterContextProvider> </ModalContextProvider>
</AppContextProvider> </ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</div>
</NextRouteStateBridge> </NextRouteStateBridge>
</CommonLayoutHydrationBoundary> </CommonLayoutHydrationBoundary>
<Zendesk /> <Zendesk />
</> </React.Fragment>
) )
} }

View File

@ -1,10 +1,10 @@
import type { ReactNode } from 'react'
import * as React from 'react' import * as React from 'react'
import { CommonLayoutHydrationBoundary } from '@/app/(commonLayout)/hydration-boundary' import { CommonLayoutHydrationBoundary } from '@/app/(commonLayout)/hydration-boundary'
import AmplitudeProvider from '@/app/components/base/amplitude' import AmplitudeProvider from '@/app/components/base/amplitude'
import { GoogleAnalyticsScripts } from '@/app/components/base/ga' import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder' import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
import HeaderWrapper from '@/app/components/header/header-wrapper' import HeaderWrapper from '@/app/components/header/header-wrapper'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics' import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
import { AppContextProvider } from '@/context/app-context-provider' import { AppContextProvider } from '@/context/app-context-provider'
import { EventEmitterContextProvider } from '@/context/event-emitter-provider' import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
@ -12,30 +12,32 @@ import { ModalContextProvider } from '@/context/modal-context-provider'
import { ProviderContextProvider } from '@/context/provider-context-provider' import { ProviderContextProvider } from '@/context/provider-context-provider'
import Header from './header' import Header from './header'
const Layout = async ({ children }: { children: ReactNode }) => { export default async function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<> <React.Fragment>
<GoogleAnalyticsScripts /> <GoogleAnalyticsScripts />
<AmplitudeProvider /> <AmplitudeProvider />
<OAuthRegistrationAnalytics /> <OAuthRegistrationAnalytics />
<EducationVerifyActionRecorder /> <EducationVerifyActionRecorder />
<CommonLayoutHydrationBoundary> <CommonLayoutHydrationBoundary>
<AppContextProvider> <div className="flex h-full flex-col overflow-hidden bg-background-body">
<EventEmitterContextProvider> <MaintenanceNotice />
<ProviderContextProvider> <AppContextProvider>
<ModalContextProvider> <EventEmitterContextProvider>
<HeaderWrapper> <ProviderContextProvider>
<Header /> <ModalContextProvider>
</HeaderWrapper> <HeaderWrapper>
<div className="relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-components-panel-bg"> <Header />
{children} </HeaderWrapper>
</div> <div className="relative flex h-0 min-h-0 shrink-0 grow flex-col overflow-y-auto bg-components-panel-bg">
</ModalContextProvider> {children}
</ProviderContextProvider> </div>
</EventEmitterContextProvider> </ModalContextProvider>
</AppContextProvider> </ProviderContextProvider>
</EventEmitterContextProvider>
</AppContextProvider>
</div>
</CommonLayoutHydrationBoundary> </CommonLayoutHydrationBoundary>
</> </React.Fragment>
) )
} }
export default Layout

View File

@ -4,10 +4,18 @@ import { useLanguage } from '@/app/components/header/account-setting/model-provi
import { NOTICE_I18N } from '@/i18n-config/language' import { NOTICE_I18N } from '@/i18n-config/language'
import MaintenanceNotice from '../maintenance-notice' import MaintenanceNotice from '../maintenance-notice'
const mockEnv = vi.hoisted(() => ({
NEXT_PUBLIC_MAINTENANCE_NOTICE: 'true',
}))
vi.mock('@/app/components/base/icons/src/vender/line/general', () => ({ vi.mock('@/app/components/base/icons/src/vender/line/general', () => ({
X: (props: React.SVGProps<SVGSVGElement>) => <svg {...props} />, X: (props: React.SVGProps<SVGSVGElement>) => <svg {...props} />,
})) }))
vi.mock('@/env', () => ({
env: mockEnv,
}))
vi.mock( vi.mock(
'@/app/components/header/account-setting/model-provider-page/hooks', '@/app/components/header/account-setting/model-provider-page/hooks',
() => ({ () => ({
@ -44,6 +52,7 @@ describe('MaintenanceNotice', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
localStorage.clear() localStorage.clear()
mockEnv.NEXT_PUBLIC_MAINTENANCE_NOTICE = 'true'
vi.mocked(useLanguage).mockReturnValue('en_US') vi.mocked(useLanguage).mockReturnValue('en_US')
setNoticeHref('#') setNoticeHref('#')
}) })
@ -71,6 +80,14 @@ describe('MaintenanceNotice', () => {
const { container } = render(<MaintenanceNotice />) const { container } = render(<MaintenanceNotice />)
expect(container.firstChild).toBeNull() expect(container.firstChild).toBeNull()
}) })
it('should not render when the notice env flag is disabled', () => {
mockEnv.NEXT_PUBLIC_MAINTENANCE_NOTICE = ''
const { container } = render(<MaintenanceNotice />)
expect(container.firstChild).toBeNull()
})
}) })
describe('User Interactions', () => { describe('User Interactions', () => {

View File

@ -1,11 +1,21 @@
'use client'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { X } from '@/app/components/base/icons/src/vender/line/general' import { X } from '@/app/components/base/icons/src/vender/line/general'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { env } from '@/env'
import { NOTICE_I18N } from '@/i18n-config/language' import { NOTICE_I18N } from '@/i18n-config/language'
import { useHideMaintenanceNotice } from './storage' import { useHideMaintenanceNotice } from './storage'
const MaintenanceNotice = () => { function MaintenanceNotice() {
if (!env.NEXT_PUBLIC_MAINTENANCE_NOTICE)
return null
return <MaintenanceNoticeContent />
}
function MaintenanceNoticeContent() {
const { t } = useTranslation() const { t } = useTranslation()
const locale = useLanguage() const locale = useLanguage()

View File

@ -2,14 +2,13 @@
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen' import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen' import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { FC, ReactNode } from 'react' import type { ReactNode } from 'react'
import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common' import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common'
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo } from 'react'
import { setUserId, setUserProperties } from '@/app/components/base/amplitude' import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking' import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import { ZENDESK_FIELD_IDS } from '@/config' import { ZENDESK_FIELD_IDS } from '@/config'
import { import {
AppContext, AppContext,
@ -18,7 +17,6 @@ import {
userProfilePlaceholder, userProfilePlaceholder,
useSelector, useSelector,
} from '@/context/app-context' } from '@/context/app-context'
import { env } from '@/env'
import { userProfileQueryOptions } from '@/features/account-profile/client' import { userProfileQueryOptions } from '@/features/account-profile/client'
import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useWorkspacePermissionKeys } from '@/service/access-control/use-permission-keys' import { useWorkspacePermissionKeys } from '@/service/access-control/use-permission-keys'
@ -66,7 +64,7 @@ const normalizeCurrentWorkspace = (workspace?: PostWorkspacesCurrentResponse): I
} }
} }
export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => { export function AppContextProvider({ children }: AppContextProviderProps) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
const { data: userProfileResp } = useSuspenseQuery(userProfileQueryOptions()) const { data: userProfileResp } = useSuspenseQuery(userProfileQueryOptions())
@ -192,12 +190,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
workspacePermissionKeys: workspacePermissionKeysQuery.data?.workspace.permission_keys ?? emptyWorkspacePermissionKeys, workspacePermissionKeys: workspacePermissionKeysQuery.data?.workspace.permission_keys ?? emptyWorkspacePermissionKeys,
}} }}
> >
<div className="flex h-full flex-col overflow-hidden"> {children}
{env.NEXT_PUBLIC_MAINTENANCE_NOTICE && <MaintenanceNotice />}
<div className="relative flex h-0 min-h-0 grow flex-col overflow-hidden bg-background-body">
{children}
</div>
</div>
</AppContext.Provider> </AppContext.Provider>
) )
} }