diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index b4a834e3e8..08481d9e4a 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/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 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, diff --git a/services/web/app/src/Features/Project/ProjectListController.mjs b/services/web/app/src/Features/Project/ProjectListController.mjs index ad03dd24ed..26220d4816 100644 --- a/services/web/app/src/Features/Project/ProjectListController.mjs +++ b/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 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, diff --git a/services/web/app/src/Features/Project/UserSettingsHelper.mjs b/services/web/app/src/Features/Project/UserSettingsHelper.mjs new file mode 100644 index 0000000000..e4c6440c0d --- /dev/null +++ b/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, +} diff --git a/services/web/app/views/project/list-react.pug b/services/web/app/views/project/list-react.pug index da1ee05108..ba29486146 100644 --- a/services/web/app/views/project/list-react.pug +++ b/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-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' diff --git a/services/web/frontend/js/features/editor-left-menu/hooks/use-set-overall-theme.tsx b/services/web/frontend/js/features/editor-left-menu/hooks/use-set-overall-theme.tsx index a026da61e5..94f6b741fe 100644 --- a/services/web/frontend/js/features/editor-left-menu/hooks/use-set-overall-theme.tsx +++ b/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 { 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']) => { diff --git a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx index 488d5476a7..daf2e814f4 100644 --- a/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx +++ b/services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx @@ -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() diff --git a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx index d57d0eada4..60041059e8 100644 --- a/services/web/frontend/js/features/ide-react/context/react-context-root.tsx +++ b/services/web/frontend/js/features/ide-react/context/react-context-root.tsx @@ -76,9 +76,9 @@ export const ReactContextRoot: FC< - - - + + + @@ -128,9 +128,9 @@ export const ReactContextRoot: FC< - - - + + + diff --git a/services/web/frontend/js/features/project-list/components/project-list-root.tsx b/services/web/frontend/js/features/project-list/components/project-list-root.tsx index 1b3701ace0..3cb12533c7 100644 --- a/services/web/frontend/js/features/project-list/components/project-list-root.tsx +++ b/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 { 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() { - + + + @@ -70,6 +74,7 @@ function DefaultPageContentWrapper({ children }: { children: ReactNode }) { } function ProjectListPageContent() { + useThemedPage('themed-project-dashboard') const { totalProjectsCount, isLoading, loadProgress } = useProjectListContext() diff --git a/services/web/frontend/js/shared/context/ide-context.tsx b/services/web/frontend/js/shared/context/ide-context.tsx index f882680583..52c5e85bab 100644 --- a/services/web/frontend/js/shared/context/ide-context.tsx +++ b/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 { 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(() => { return { ...ide, diff --git a/services/web/frontend/js/shared/context/user-settings-context.tsx b/services/web/frontend/js/shared/context/user-settings-context.tsx index cc18c896fa..3ea8257ff0 100644 --- a/services/web/frontend/js/shared/context/user-settings-context.tsx +++ b/services/web/frontend/js/shared/context/user-settings-context.tsx @@ -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 = ({ () => 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( () => ({ userSettings, diff --git a/services/web/frontend/js/shared/hooks/use-themed-page.tsx b/services/web/frontend/js/shared/hooks/use-themed-page.tsx new file mode 100644 index 0000000000..bbebb95d92 --- /dev/null +++ b/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]) +} diff --git a/services/web/test/unit/src/Project/ProjectListController.test.mjs b/services/web/test/unit/src/Project/ProjectListController.test.mjs index e1d55b820c..f060fc6eca 100644 --- a/services/web/test/unit/src/Project/ProjectListController.test.mjs +++ b/services/web/test/unit/src/Project/ProjectListController.test.mjs @@ -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': {