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

Add data-theme attribute to project list page

GitOrigin-RevId: 3a623e3258d55e01f0911bcc45b78bcdba21745b
This commit is contained in:
David
2025-10-21 10:07:45 +01:00
committed by Copybot
parent 7baf11da8b
commit 6f1e8cea6d
12 changed files with 98 additions and 52 deletions
@@ -50,6 +50,7 @@ import UserGetter from '../User/UserGetter.js'
import { isStandaloneAiAddOnPlanCode } from '../Subscription/AiHelper.js'
import SubscriptionController from '../Subscription/SubscriptionController.mjs'
import { formatCurrency } from '../../util/currency.js'
import UserSettingsHelper from './UserSettingsHelper.mjs'
const { ObjectId } = mongodb
/**
@@ -811,22 +812,7 @@ const _ProjectController = {
isMemberOfGroupSubscription: userIsMemberOfGroupSubscription,
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 ?? [],
privilegeLevel,
anonymous,
@@ -31,6 +31,7 @@ import SubscriptionHelper from '../Subscription/SubscriptionHelper.js'
import PermissionsManager from '../Authorization/PermissionsManager.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.js'
import { OnboardingDataCollection } from '../../models/OnboardingDataCollection.js'
import UserSettingsHelper from './UserSettingsHelper.mjs'
/**
* @import { GetProjectsRequest, GetProjectsResponse, AllUsersProjects, MongoProject, FormattedProject, MongoTag } from "./types"
@@ -164,7 +165,7 @@ async function projectListPage(req, res, next) {
})
const user = await User.findById(
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' : ''
}`
)
@@ -537,6 +538,12 @@ async function projectListPage(req, res, next) {
}
}
await SplitTestHandler.promises.getAssignment(
req,
res,
'themed-project-dashboard'
)
res.render('project/list-react', {
title: 'your_projects',
usersBestSubscription,
@@ -545,6 +552,7 @@ async function projectListPage(req, res, next) {
user,
userAffiliations,
userEmails,
userSettings: UserSettingsHelper.buildUserSettings(user),
reconfirmedViaSAML,
allInReconfirmNotificationPeriods,
survey,
@@ -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,
}
@@ -33,6 +33,7 @@ block append meta
meta(name='ol-survey' data-type='json' content=survey)
meta(name='ol-tags' data-type='json' content=tags)
meta(name='ol-portalTemplates' data-type='json' content=portalTemplates)
meta(name='ol-userSettings' data-type='json' content=userSettings)
meta(
name='ol-prefetchedProjectsBlob'
data-type='json'
@@ -1,10 +1,9 @@
import { useCallback, useEffect } from 'react'
import { useCallback } from 'react'
import _ from 'lodash'
import { saveUserSettings } from '../utils/api'
import { UserSettings } from '../../../../../types/user-settings'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import getMeta from '@/utils/meta'
import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
export default function useSetOverallTheme() {
const { userSettings, setUserSettings } = useUserSettingsContext()
@@ -16,13 +15,6 @@ export default function useSetOverallTheme() {
},
[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(
(newOverallTheme: UserSettings['overallTheme']) => {
@@ -17,6 +17,7 @@ import {
import EditorSurvey from '../editor-survey'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useStatusFavicon } from '@/features/ide-react/hooks/use-status-favicon'
import useThemedPage from '@/shared/hooks/use-themed-page'
const MainLayoutNew = lazy(
() => 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
useHasLintingError() // pass editor:lint hasLintingError to the compiler
useStatusFavicon() // update the favicon based on the compile status
useThemedPage() // set the page theme based on user settings
const newEditor = useIsNewEditorEnabled()
const canAccessNewEditor = canUseNewEditor()
@@ -76,9 +76,9 @@ export const ReactContextRoot: FC<
<Providers.ModalsContextProvider>
<Providers.ConnectionProvider>
<Providers.ProjectProvider>
<Providers.IdeReactProvider>
<Providers.UserProvider>
<Providers.UserSettingsProvider>
<Providers.UserSettingsProvider>
<Providers.IdeReactProvider>
<Providers.UserProvider>
<Providers.SnapshotProvider>
<Providers.DetachProvider>
<Providers.EditorPropertiesProvider>
@@ -128,9 +128,9 @@ export const ReactContextRoot: FC<
</Providers.EditorPropertiesProvider>
</Providers.DetachProvider>
</Providers.SnapshotProvider>
</Providers.UserSettingsProvider>
</Providers.UserProvider>
</Providers.IdeReactProvider>
</Providers.UserProvider>
</Providers.IdeReactProvider>
</Providers.UserSettingsProvider>
</Providers.ProjectProvider>
</Providers.ConnectionProvider>
</Providers.ModalsContextProvider>
@@ -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 { DsNavStyleProvider } from '@/features/project-list/components/use-is-ds-nav'
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() {
const { isReady } = useWaitForI18n()
@@ -35,7 +37,9 @@ export function ProjectListRootInner() {
<ProjectListProvider>
<ColorPickerProvider>
<SplitTestProvider>
<ProjectListPageContent />
<UserSettingsProvider>
<ProjectListPageContent />
</UserSettingsProvider>
</SplitTestProvider>
</ColorPickerProvider>
</ProjectListProvider>
@@ -70,6 +74,7 @@ function DefaultPageContentWrapper({ children }: { children: ReactNode }) {
}
function ProjectListPageContent() {
useThemedPage('themed-project-dashboard')
const { totalProjectsCount, isLoading, loadProgress } =
useProjectListContext()
@@ -2,6 +2,10 @@ import { createContext, FC, useContext, useEffect, useMemo } from 'react'
import { ScopeValueStore } from '../../../../types/ide/scope-value-store'
import { ScopeEventEmitter } from '../../../../types/ide/scope-event-emitter'
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 = {
socket: Socket
@@ -44,6 +48,21 @@ export const IdeProvider: FC<
}
}, [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>(() => {
return {
...ide,
@@ -6,14 +6,9 @@ import {
SetStateAction,
FC,
useState,
useEffect,
} from 'react'
import { UserSettings } from '../../../../types/user-settings'
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 = {
pdfViewer: 'pdfjs',
@@ -50,20 +45,6 @@ export const UserSettingsProvider: FC<React.PropsWithChildren> = ({
() => 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>(
() => ({
userSettings,
@@ -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])
}
@@ -23,6 +23,16 @@ describe('ProjectListController', function () {
lastActive: new Date(2),
signUpDate: new Date(1),
lastLoginIp: '111.111.111.112',
ace: {
syntaxValidation: true,
pdfViewer: 'pdfjs',
spellCheckLanguage: 'en',
autoPairDelimiters: true,
autoComplete: true,
fontSize: 12,
theme: 'textmate',
mode: 'none',
},
}
ctx.users = {
'user-1': {