Migrating reference settings to mongo and exposing them through code … (#31726)

* Migrating reference settings to mongo and exposing them through code mirror context

* removing undefined optional for reference manager type settings

* allowing partial updates to user settings objects, and remove repeated cypress intercepts

* Zod schema parsing for user reference manager settings

* Splitting ref provider mongoose schema into const

* Persisting local storage reference settings to mongo and deleting from local

* Enforcing ref provider group id to always be type string

* Fixing test and format errors

* Migrated flag for settings to only migrate once

* fixing cypress tests adding migrated flag

* persisting local storage to allow for easy rollback

GitOrigin-RevId: f59522bdee6f0e56efb7d98b9d9373a743619ec8
This commit is contained in:
l-obrien-overleaf
2026-03-26 09:07:40 +00:00
committed by Copybot
parent 03c30a9d1b
commit 27e5044d30
9 changed files with 368 additions and 0 deletions
@@ -6,9 +6,13 @@ import {
SetStateAction,
FC,
useState,
useEffect,
} from 'react'
import { UserSettings } from '../../../../types/user-settings'
import getMeta from '@/utils/meta'
import customLocalStorage from '@/infrastructure/local-storage'
import { getLegacyWriteAndCiteMigration } from '../utils/write-and-cite-settings-migration'
import { saveUserSettings } from '@/features/editor-left-menu/utils/api'
export const defaultSettings: UserSettings = {
pdfViewer: 'pdfjs',
@@ -27,6 +31,21 @@ export const defaultSettings: UserSettings = {
referencesSearchMode: 'advanced',
breadcrumbs: true,
darkModePdf: false,
zotero: {
enabled: true,
groups: [],
disablePersonalLibrary: false,
},
mendeley: {
enabled: true,
groups: [],
disablePersonalLibrary: false,
},
papers: {
enabled: true,
groups: [],
disablePersonalLibrary: false,
},
}
type UserSettingsContextValue = {
@@ -47,6 +66,35 @@ export const UserSettingsProvider: FC<React.PropsWithChildren> = ({
() => getMeta('ol-userSettings') || defaultSettings
)
useEffect(() => {
const { patch, keysToRemove } = getLegacyWriteAndCiteMigration(userSettings)
if (keysToRemove.length === 0) {
return
}
if (Object.keys(patch).length === 0) {
keysToRemove.forEach(customLocalStorage.removeItem)
return
}
Promise.all(
Object.entries(patch).map(([key, value]) =>
saveUserSettings(
key as keyof Pick<UserSettings, 'mendeley' | 'zotero' | 'papers'>,
value
)
)
).then(() => {
setUserSettings(currentSettings => ({
...currentSettings,
...patch,
}))
keysToRemove.forEach(customLocalStorage.removeItem)
})
// Only run once when the provider mounts
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const value = useMemo<UserSettingsContextValue>(
() => ({
userSettings,
@@ -0,0 +1,99 @@
import customLocalStorage from '@/infrastructure/local-storage'
import getMeta from '@/utils/meta'
import {
RefProviderSettings,
UserSettings,
} from '../../../../types/user-settings'
type RefProvider = 'mendeley' | 'zotero' | 'papers'
const providers: RefProvider[] = ['mendeley', 'zotero', 'papers']
const buildLegacyKey = (userId: string, provider: RefProvider, key: string) =>
`user.${userId}.write-and-cite.${provider}.${key}`
const isBoolean = (value: unknown): value is boolean =>
typeof value === 'boolean'
const parseGroups = (value: unknown): { id: string }[] | undefined => {
return Array.isArray(value)
? (value as { id: string | number }[]).map(group => ({
id: typeof group.id === 'number' ? String(group.id) : group.id,
}))
: undefined
}
export type LegacyWriteAndCiteMigration = {
patch: Partial<Pick<UserSettings, 'mendeley' | 'zotero' | 'papers'>>
keysToRemove: string[]
}
export const getLegacyWriteAndCiteMigration = (
userSettings: UserSettings
): LegacyWriteAndCiteMigration => {
const userId = getMeta('ol-user_id')
if (!userId) {
return {
patch: {},
keysToRemove: [],
}
}
const patch: LegacyWriteAndCiteMigration['patch'] = {}
const keysToRemove: string[] = []
for (const provider of providers) {
const currentProviderSettings = userSettings[provider]
if (currentProviderSettings.migrated) {
continue
}
const enabledKey = buildLegacyKey(userId, provider, 'enabled')
const groupsKey = buildLegacyKey(userId, provider, 'groups')
const disablePersonalLibraryKey = buildLegacyKey(
userId,
provider,
'disablePersonalLibrary'
)
const enabledValue = customLocalStorage.getItem(enabledKey)
const groupsValue = customLocalStorage.getItem(groupsKey)
const disablePersonalLibraryValue = customLocalStorage.getItem(
disablePersonalLibraryKey
)
// Storage.getItem returns null if the key does not exist
const hasEnabledValue = enabledValue !== null
const hasGroupsValue = groupsValue !== null
const hasDisablePersonalLibraryValue = disablePersonalLibraryValue !== null
if (
!hasEnabledValue &&
!hasGroupsValue &&
!hasDisablePersonalLibraryValue
) {
continue
}
const nextProviderSettings: RefProviderSettings = {
enabled: isBoolean(enabledValue)
? enabledValue
: currentProviderSettings.enabled,
groups: parseGroups(groupsValue) ?? currentProviderSettings.groups,
disablePersonalLibrary: isBoolean(disablePersonalLibraryValue)
? disablePersonalLibraryValue
: currentProviderSettings.disablePersonalLibrary,
migrated: true,
}
patch[provider] = nextProviderSettings
keysToRemove.push(enabledKey, groupsKey, disablePersonalLibraryKey)
}
return {
patch,
keysToRemove: [],
}
}