Просмотр исходного кода

Merge pull request #29152 from overleaf/dp-dashboard-dark-mode

Add data-theme attribute to project list page

GitOrigin-RevId: 3a623e3258d55e01f0911bcc45b78bcdba21745b
David 11 месяцев назад
Родитель
Сommit
129ea72d36

+ 2 - 16
services/web/app/src/Features/Project/ProjectController.mjs

@@ -50,6 +50,7 @@ import UserGetter from '../User/UserGetter.js'
 import { isStandaloneAiAddOnPlanCode } from '../Subscription/AiHelper.js'
 import { isStandaloneAiAddOnPlanCode } from '../Subscription/AiHelper.js'
 import SubscriptionController from '../Subscription/SubscriptionController.mjs'
 import SubscriptionController from '../Subscription/SubscriptionController.mjs'
 import { formatCurrency } from '../../util/currency.js'
 import { formatCurrency } from '../../util/currency.js'
+import UserSettingsHelper from './UserSettingsHelper.mjs'
 
 
 const { ObjectId } = mongodb
 const { ObjectId } = mongodb
 /**
 /**
@@ -811,22 +812,7 @@ const _ProjectController = {
           isMemberOfGroupSubscription: userIsMemberOfGroupSubscription,
           isMemberOfGroupSubscription: userIsMemberOfGroupSubscription,
           hasInstitutionLicence: userHasInstitutionLicence,
           hasInstitutionLicence: userHasInstitutionLicence,
         },
         },
-        userSettings: {
-          mode: user.ace.mode,
-          editorTheme: user.ace.theme,
-          fontSize: user.ace.fontSize,
-          autoComplete: user.ace.autoComplete,
-          autoPairDelimiters: user.ace.autoPairDelimiters,
-          pdfViewer: user.ace.pdfViewer,
-          syntaxValidation: user.ace.syntaxValidation,
-          fontFamily: user.ace.fontFamily || 'lucida',
-          lineHeight: user.ace.lineHeight || 'normal',
-          overallTheme: user.ace.overallTheme,
-          mathPreview: user.ace.mathPreview,
-          breadcrumbs: user.ace.breadcrumbs,
-          referencesSearchMode: user.ace.referencesSearchMode,
-          enableNewEditor: user.ace.enableNewEditor ?? true,
-        },
+        userSettings: UserSettingsHelper.buildUserSettings(user),
         labsExperiments: user.labsExperiments ?? [],
         labsExperiments: user.labsExperiments ?? [],
         privilegeLevel,
         privilegeLevel,
         anonymous,
         anonymous,

+ 9 - 1
services/web/app/src/Features/Project/ProjectListController.mjs

@@ -31,6 +31,7 @@ import SubscriptionHelper from '../Subscription/SubscriptionHelper.js'
 import PermissionsManager from '../Authorization/PermissionsManager.mjs'
 import PermissionsManager from '../Authorization/PermissionsManager.mjs'
 import AnalyticsManager from '../Analytics/AnalyticsManager.js'
 import AnalyticsManager from '../Analytics/AnalyticsManager.js'
 import { OnboardingDataCollection } from '../../models/OnboardingDataCollection.js'
 import { OnboardingDataCollection } from '../../models/OnboardingDataCollection.js'
+import UserSettingsHelper from './UserSettingsHelper.mjs'
 
 
 /**
 /**
  * @import { GetProjectsRequest, GetProjectsResponse, AllUsersProjects, MongoProject, FormattedProject, MongoTag } from "./types"
  * @import { GetProjectsRequest, GetProjectsResponse, AllUsersProjects, MongoProject, FormattedProject, MongoTag } from "./types"
@@ -164,7 +165,7 @@ async function projectListPage(req, res, next) {
   })
   })
   const user = await User.findById(
   const user = await User.findById(
     userId,
     userId,
-    `email emails features alphaProgram betaProgram lastPrimaryEmailCheck lastActive signUpDate refProviders${
+    `email emails features alphaProgram betaProgram lastPrimaryEmailCheck lastActive signUpDate ace refProviders${
       isSaas ? ' enrollment writefull completedTutorials aiErrorAssistant' : ''
       isSaas ? ' enrollment writefull completedTutorials aiErrorAssistant' : ''
     }`
     }`
   )
   )
@@ -537,6 +538,12 @@ async function projectListPage(req, res, next) {
     }
     }
   }
   }
 
 
+  await SplitTestHandler.promises.getAssignment(
+    req,
+    res,
+    'themed-project-dashboard'
+  )
+
   res.render('project/list-react', {
   res.render('project/list-react', {
     title: 'your_projects',
     title: 'your_projects',
     usersBestSubscription,
     usersBestSubscription,
@@ -545,6 +552,7 @@ async function projectListPage(req, res, next) {
     user,
     user,
     userAffiliations,
     userAffiliations,
     userEmails,
     userEmails,
+    userSettings: UserSettingsHelper.buildUserSettings(user),
     reconfirmedViaSAML,
     reconfirmedViaSAML,
     allInReconfirmNotificationPeriods,
     allInReconfirmNotificationPeriods,
     survey,
     survey,

+ 22 - 0
services/web/app/src/Features/Project/UserSettingsHelper.mjs

@@ -0,0 +1,22 @@
+function buildUserSettings(user) {
+  return {
+    mode: user.ace.mode,
+    editorTheme: user.ace.theme,
+    fontSize: user.ace.fontSize,
+    autoComplete: user.ace.autoComplete,
+    autoPairDelimiters: user.ace.autoPairDelimiters,
+    pdfViewer: user.ace.pdfViewer,
+    syntaxValidation: user.ace.syntaxValidation,
+    fontFamily: user.ace.fontFamily || 'lucida',
+    lineHeight: user.ace.lineHeight || 'normal',
+    overallTheme: user.ace.overallTheme,
+    mathPreview: user.ace.mathPreview,
+    breadcrumbs: user.ace.breadcrumbs,
+    referencesSearchMode: user.ace.referencesSearchMode,
+    enableNewEditor: user.ace.enableNewEditor ?? true,
+  }
+}
+
+export default {
+  buildUserSettings,
+}

+ 1 - 0
services/web/app/views/project/list-react.pug

@@ -33,6 +33,7 @@ block append meta
 	meta(name='ol-survey' data-type='json' content=survey)
 	meta(name='ol-survey' data-type='json' content=survey)
 	meta(name='ol-tags' data-type='json' content=tags)
 	meta(name='ol-tags' data-type='json' content=tags)
 	meta(name='ol-portalTemplates' data-type='json' content=portalTemplates)
 	meta(name='ol-portalTemplates' data-type='json' content=portalTemplates)
+	meta(name='ol-userSettings' data-type='json' content=userSettings)
 	meta(
 	meta(
 		name='ol-prefetchedProjectsBlob'
 		name='ol-prefetchedProjectsBlob'
 		data-type='json'
 		data-type='json'

+ 1 - 9
services/web/frontend/js/features/editor-left-menu/hooks/use-set-overall-theme.tsx

@@ -1,10 +1,9 @@
-import { useCallback, useEffect } from 'react'
+import { useCallback } from 'react'
 import _ from 'lodash'
 import _ from 'lodash'
 import { saveUserSettings } from '../utils/api'
 import { saveUserSettings } from '../utils/api'
 import { UserSettings } from '../../../../../types/user-settings'
 import { UserSettings } from '../../../../../types/user-settings'
 import { useUserSettingsContext } from '@/shared/context/user-settings-context'
 import { useUserSettingsContext } from '@/shared/context/user-settings-context'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
-import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
 
 
 export default function useSetOverallTheme() {
 export default function useSetOverallTheme() {
   const { userSettings, setUserSettings } = useUserSettingsContext()
   const { userSettings, setUserSettings } = useUserSettingsContext()
@@ -16,13 +15,6 @@ export default function useSetOverallTheme() {
     },
     },
     [setUserSettings]
     [setUserSettings]
   )
   )
-  const activeOverallTheme = useActiveOverallTheme()
-
-  useEffect(() => {
-    // Sets the body's data-theme attribute for theming
-    document.body.dataset.theme =
-      activeOverallTheme === 'dark' ? 'default' : 'light'
-  }, [activeOverallTheme])
 
 
   return useCallback(
   return useCallback(
     (newOverallTheme: UserSettings['overallTheme']) => {
     (newOverallTheme: UserSettings['overallTheme']) => {

+ 2 - 0
services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx

@@ -17,6 +17,7 @@ import {
 import EditorSurvey from '../editor-survey'
 import EditorSurvey from '../editor-survey'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
 import { useStatusFavicon } from '@/features/ide-react/hooks/use-status-favicon'
 import { useStatusFavicon } from '@/features/ide-react/hooks/use-status-favicon'
+import useThemedPage from '@/shared/hooks/use-themed-page'
 
 
 const MainLayoutNew = lazy(
 const MainLayoutNew = lazy(
   () => import('@/features/ide-redesign/components/main-layout')
   () => import('@/features/ide-redesign/components/main-layout')
@@ -32,6 +33,7 @@ export default function IdePage() {
   useRegisterUserActivity() // record activity and ensure connection when user is active
   useRegisterUserActivity() // record activity and ensure connection when user is active
   useHasLintingError() // pass editor:lint hasLintingError to the compiler
   useHasLintingError() // pass editor:lint hasLintingError to the compiler
   useStatusFavicon() // update the favicon based on the compile status
   useStatusFavicon() // update the favicon based on the compile status
+  useThemedPage() // set the page theme based on user settings
 
 
   const newEditor = useIsNewEditorEnabled()
   const newEditor = useIsNewEditorEnabled()
   const canAccessNewEditor = canUseNewEditor()
   const canAccessNewEditor = canUseNewEditor()

+ 6 - 6
services/web/frontend/js/features/ide-react/context/react-context-root.tsx

@@ -76,9 +76,9 @@ export const ReactContextRoot: FC<
       <Providers.ModalsContextProvider>
       <Providers.ModalsContextProvider>
         <Providers.ConnectionProvider>
         <Providers.ConnectionProvider>
           <Providers.ProjectProvider>
           <Providers.ProjectProvider>
-            <Providers.IdeReactProvider>
-              <Providers.UserProvider>
-                <Providers.UserSettingsProvider>
+            <Providers.UserSettingsProvider>
+              <Providers.IdeReactProvider>
+                <Providers.UserProvider>
                   <Providers.SnapshotProvider>
                   <Providers.SnapshotProvider>
                     <Providers.DetachProvider>
                     <Providers.DetachProvider>
                       <Providers.EditorPropertiesProvider>
                       <Providers.EditorPropertiesProvider>
@@ -128,9 +128,9 @@ export const ReactContextRoot: FC<
                       </Providers.EditorPropertiesProvider>
                       </Providers.EditorPropertiesProvider>
                     </Providers.DetachProvider>
                     </Providers.DetachProvider>
                   </Providers.SnapshotProvider>
                   </Providers.SnapshotProvider>
-                </Providers.UserSettingsProvider>
-              </Providers.UserProvider>
-            </Providers.IdeReactProvider>
+                </Providers.UserProvider>
+              </Providers.IdeReactProvider>
+            </Providers.UserSettingsProvider>
           </Providers.ProjectProvider>
           </Providers.ProjectProvider>
         </Providers.ConnectionProvider>
         </Providers.ConnectionProvider>
       </Providers.ModalsContextProvider>
       </Providers.ModalsContextProvider>

+ 6 - 1
services/web/frontend/js/features/project-list/components/project-list-root.tsx

@@ -19,6 +19,8 @@ import WelcomePageContent from '@/features/project-list/components/welcome-page-
 import { ProjectListDsNav } from '@/features/project-list/components/project-list-ds-nav'
 import { ProjectListDsNav } from '@/features/project-list/components/project-list-ds-nav'
 import { DsNavStyleProvider } from '@/features/project-list/components/use-is-ds-nav'
 import { DsNavStyleProvider } from '@/features/project-list/components/use-is-ds-nav'
 import CookieBanner from '@/shared/components/cookie-banner'
 import CookieBanner from '@/shared/components/cookie-banner'
+import useThemedPage from '@/shared/hooks/use-themed-page'
+import { UserSettingsProvider } from '@/shared/context/user-settings-context'
 
 
 function ProjectListRoot() {
 function ProjectListRoot() {
   const { isReady } = useWaitForI18n()
   const { isReady } = useWaitForI18n()
@@ -35,7 +37,9 @@ export function ProjectListRootInner() {
     <ProjectListProvider>
     <ProjectListProvider>
       <ColorPickerProvider>
       <ColorPickerProvider>
         <SplitTestProvider>
         <SplitTestProvider>
-          <ProjectListPageContent />
+          <UserSettingsProvider>
+            <ProjectListPageContent />
+          </UserSettingsProvider>
         </SplitTestProvider>
         </SplitTestProvider>
       </ColorPickerProvider>
       </ColorPickerProvider>
     </ProjectListProvider>
     </ProjectListProvider>
@@ -70,6 +74,7 @@ function DefaultPageContentWrapper({ children }: { children: ReactNode }) {
 }
 }
 
 
 function ProjectListPageContent() {
 function ProjectListPageContent() {
+  useThemedPage('themed-project-dashboard')
   const { totalProjectsCount, isLoading, loadProgress } =
   const { totalProjectsCount, isLoading, loadProgress } =
     useProjectListContext()
     useProjectListContext()
 
 

+ 19 - 0
services/web/frontend/js/shared/context/ide-context.tsx

@@ -2,6 +2,10 @@ import { createContext, FC, useContext, useEffect, useMemo } from 'react'
 import { ScopeValueStore } from '../../../../types/ide/scope-value-store'
 import { ScopeValueStore } from '../../../../types/ide/scope-value-store'
 import { ScopeEventEmitter } from '../../../../types/ide/scope-event-emitter'
 import { ScopeEventEmitter } from '../../../../types/ide/scope-event-emitter'
 import { Socket } from '@/features/ide-react/connection/types/socket'
 import { Socket } from '@/features/ide-react/connection/types/socket'
+import { useUserSettingsContext } from './user-settings-context'
+import { userStyles } from '../utils/styles'
+import { canUseNewEditor } from '@/features/ide-redesign/utils/new-editor-utils'
+import { useActiveOverallTheme } from '../hooks/use-active-overall-theme'
 
 
 export type Ide = {
 export type Ide = {
   socket: Socket
   socket: Socket
@@ -44,6 +48,21 @@ export const IdeProvider: FC<
     }
     }
   }, [unstableStore])
   }, [unstableStore])
 
 
+  const { userSettings } = useUserSettingsContext()
+  const activeOverallTheme = useActiveOverallTheme()
+
+  useEffect(() => {
+    const { fontFamily, lineHeight } = userStyles(userSettings)
+    unstableStore.set('settings', {
+      overallTheme: activeOverallTheme,
+      keybindings: userSettings.mode === 'none' ? 'default' : userSettings.mode,
+      fontFamily,
+      lineHeight,
+      fontSize: userSettings.fontSize,
+      isNewEditor: canUseNewEditor() && userSettings.enableNewEditor,
+    })
+  }, [unstableStore, userSettings, activeOverallTheme])
+
   const value = useMemo<IdeContextValue>(() => {
   const value = useMemo<IdeContextValue>(() => {
     return {
     return {
       ...ide,
       ...ide,

+ 0 - 19
services/web/frontend/js/shared/context/user-settings-context.tsx

@@ -6,14 +6,9 @@ import {
   SetStateAction,
   SetStateAction,
   FC,
   FC,
   useState,
   useState,
-  useEffect,
 } from 'react'
 } from 'react'
-
 import { UserSettings } from '../../../../types/user-settings'
 import { UserSettings } from '../../../../types/user-settings'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
-import { userStyles } from '../utils/styles'
-import { canUseNewEditor } from '@/features/ide-redesign/utils/new-editor-utils'
-import { useIdeContext } from '@/shared/context/ide-context'
 
 
 const defaultSettings: UserSettings = {
 const defaultSettings: UserSettings = {
   pdfViewer: 'pdfjs',
   pdfViewer: 'pdfjs',
@@ -50,20 +45,6 @@ export const UserSettingsProvider: FC<React.PropsWithChildren> = ({
     () => getMeta('ol-userSettings') || defaultSettings
     () => getMeta('ol-userSettings') || defaultSettings
   )
   )
 
 
-  // update the global scope 'settings' value, for extensions
-  const { unstableStore } = useIdeContext()
-  useEffect(() => {
-    const { fontFamily, lineHeight } = userStyles(userSettings)
-    unstableStore.set('settings', {
-      overallTheme: userSettings.overallTheme === 'light-' ? 'light' : 'dark',
-      keybindings: userSettings.mode === 'none' ? 'default' : userSettings.mode,
-      fontFamily,
-      lineHeight,
-      fontSize: userSettings.fontSize,
-      isNewEditor: canUseNewEditor() && userSettings.enableNewEditor,
-    })
-  }, [unstableStore, userSettings])
-
   const value = useMemo<UserSettingsContextValue>(
   const value = useMemo<UserSettingsContextValue>(
     () => ({
     () => ({
       userSettings,
       userSettings,

+ 20 - 0
services/web/frontend/js/shared/hooks/use-themed-page.tsx

@@ -0,0 +1,20 @@
+import { useEffect } from 'react'
+import { useActiveOverallTheme } from './use-active-overall-theme'
+import { useSplitTestContext } from '../context/split-test-context'
+
+export default function useThemedPage(featureFlag?: string) {
+  const { splitTestVariants } = useSplitTestContext()
+
+  let activeOverallTheme = useActiveOverallTheme()
+
+  // Override theme if feature flag is provided and not enabled
+  if (featureFlag && splitTestVariants[featureFlag] !== 'enabled') {
+    activeOverallTheme = 'light'
+  }
+
+  useEffect(() => {
+    // Sets the body's data-theme attribute for theming
+    document.body.dataset.theme =
+      activeOverallTheme === 'dark' ? 'default' : 'light'
+  }, [activeOverallTheme])
+}

+ 10 - 0
services/web/test/unit/src/Project/ProjectListController.test.mjs

@@ -23,6 +23,16 @@ describe('ProjectListController', function () {
       lastActive: new Date(2),
       lastActive: new Date(2),
       signUpDate: new Date(1),
       signUpDate: new Date(1),
       lastLoginIp: '111.111.111.112',
       lastLoginIp: '111.111.111.112',
+      ace: {
+        syntaxValidation: true,
+        pdfViewer: 'pdfjs',
+        spellCheckLanguage: 'en',
+        autoPairDelimiters: true,
+        autoComplete: true,
+        fontSize: 12,
+        theme: 'textmate',
+        mode: 'none',
+      },
     }
     }
     ctx.users = {
     ctx.users = {
       'user-1': {
       'user-1': {