Merge pull request #15707 from overleaf/td-user-settings-context

Move user settings to a context

GitOrigin-RevId: 9a9d55dfee9f71cee323fe64d1442303ac7cfeb2
This commit is contained in:
Tim Down 2023-11-21 14:29:44 +00:00 committed by Copybot
parent 4f13470345
commit 38efea39f2
25 changed files with 291 additions and 202 deletions

View file

@ -1,11 +1,11 @@
import { memo, useEffect, useRef, useState } from 'react'
import { useLayoutContext } from '../../../shared/context/layout-context'
import useScopeValue from '../../../shared/hooks/use-scope-value'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
export default memo(function LeftMenuMask() {
const { setLeftMenuShown } = useLayoutContext()
const [editorTheme] = useScopeValue('settings.editorTheme')
const [overallTheme] = useScopeValue('settings.overallTheme')
const { userSettings } = useUserSettingsContext()
const { editorTheme, overallTheme } = userSettings
const [original] = useState({ editorTheme, overallTheme })
const maskRef = useRef<HTMLDivElement | null>(null)

View file

@ -3,8 +3,8 @@ import { useProjectSettingsContext } from '../../context/project-settings-contex
import SettingsMenuSelect from './settings-menu-select'
import type { Option } from './settings-menu-select'
const sizes = ['10', '11', '12', '13', '14', '16', '18', '20', '22', '24']
const options: Array<Option> = sizes.map(size => ({
const sizes = [10, 11, 12, 13, 14, 16, 18, 20, 22, 24]
const options: Option<number>[] = sizes.map(size => ({
value: size,
label: `${size}px`,
}))

View file

@ -1,6 +1,6 @@
import { ChangeEventHandler, useCallback } from 'react'
type PossibleValue = string | boolean
type PossibleValue = string | number | boolean
export type Option<T extends PossibleValue = string> = {
value: T
@ -35,14 +35,16 @@ export default function SettingsMenuSelect<T extends PossibleValue = string>({
}: SettingsMenuSelectProps<T>) {
const handleChange: ChangeEventHandler<HTMLSelectElement> = useCallback(
event => {
let value: PossibleValue = event.target.value
if (value === 'true' || value === 'false') {
value = value === 'true'
const selectedValue = event.target.value
let onChangeValue: PossibleValue = selectedValue
if (typeof value === 'boolean') {
onChangeValue = selectedValue === 'true'
} else if (typeof value === 'number') {
onChangeValue = parseInt(selectedValue, 10)
}
onChange(value as T)
onChange(onChangeValue as T)
},
[onChange]
[onChange, value]
)
return (

View file

@ -2,7 +2,8 @@ import { createContext, FC, useContext, useMemo } from 'react'
import useProjectWideSettings from '../hooks/use-project-wide-settings'
import useUserWideSettings from '../hooks/use-user-wide-settings'
import useProjectWideSettingsSocketListener from '../hooks/use-project-wide-settings-socket-listener'
import type { ProjectSettings, UserSettings } from '../utils/api'
import type { ProjectSettings } from '../utils/api'
import { UserSettings } from '../../../../../types/user-settings'
type ProjectSettingsSetterContextValue = {
setCompiler: (compiler: ProjectSettings['compiler']) => void

View file

@ -1,12 +1,9 @@
import useScopeValue from '../../../shared/hooks/use-scope-value'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { saveUserSettings } from '../utils/api'
import type { UserSettings } from '../utils/api'
import { UserSettings } from '../../../../../types/user-settings'
export default function useSaveUserSettings() {
const [userSettings, setUserSettings] = useScopeValue<UserSettings>(
'settings',
true
)
const { userSettings, setUserSettings } = useUserSettingsContext()
return (
key: keyof UserSettings,

View file

@ -2,16 +2,25 @@ import { useCallback, useEffect, useState } from 'react'
import _ from 'lodash'
import useScopeValue from '../../../shared/hooks/use-scope-value'
import type { OverallThemeMeta } from '../../../../../types/project-settings'
import { saveUserSettings, type UserSettings } from '../utils/api'
import { saveUserSettings } from '../utils/api'
import { UserSettings } from '../../../../../types/user-settings'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
export default function useSetOverallTheme() {
const [chosenTheme, setChosenTheme] = useState<OverallThemeMeta | null>(null)
const [loadingStyleSheet, setLoadingStyleSheet] = useScopeValue<boolean>(
'ui.loadingStyleSheet'
)
const [overallTheme, setOverallTheme] = useScopeValue<
UserSettings['overallTheme']
>('settings.overallTheme')
const { userSettings, setUserSettings } = useUserSettingsContext()
const { overallTheme } = userSettings
const setOverallTheme = useCallback(
(overallTheme: UserSettings['overallTheme']) => {
setUserSettings(settings => ({ ...settings, overallTheme }))
},
[setUserSettings]
)
useEffect(() => {
const docHeadEl = document.querySelector('head')

View file

@ -1,17 +1,25 @@
import { useCallback } from 'react'
import useScopeValue from '../../../shared/hooks/use-scope-value'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import useSetOverallTheme from './use-set-overall-theme'
import useSaveUserSettings from './use-save-user-settings'
import type { UserSettings } from '../utils/api'
import { UserSettings } from '../../../../../types/user-settings'
export default function useUserWideSettings() {
const saveUserSettings = useSaveUserSettings()
// this may be undefined on test environments
const [userSettings] = useScopeValue<UserSettings | undefined>(
'settings',
true
)
const { userSettings } = useUserSettingsContext()
const {
overallTheme,
autoComplete,
autoPairDelimiters,
syntaxValidation,
editorTheme,
mode,
fontSize,
fontFamily,
lineHeight,
pdfViewer,
} = userSettings
const setOverallTheme = useSetOverallTheme()
const setAutoComplete = useCallback(
@ -78,25 +86,25 @@ export default function useUserWideSettings() {
)
return {
autoComplete: userSettings?.autoComplete,
autoComplete,
setAutoComplete,
autoPairDelimiters: userSettings?.autoPairDelimiters,
autoPairDelimiters,
setAutoPairDelimiters,
syntaxValidation: userSettings?.syntaxValidation,
syntaxValidation,
setSyntaxValidation,
editorTheme: userSettings?.editorTheme,
editorTheme,
setEditorTheme,
overallTheme: userSettings?.overallTheme,
overallTheme,
setOverallTheme,
mode: userSettings?.mode,
mode,
setMode,
fontSize: userSettings?.fontSize,
fontSize,
setFontSize,
fontFamily: userSettings?.fontFamily,
fontFamily,
setFontFamily,
lineHeight: userSettings?.lineHeight,
lineHeight,
setLineHeight,
pdfViewer: userSettings?.pdfViewer,
pdfViewer,
setPdfViewer,
}
}

View file

@ -1,29 +1,8 @@
import type {
FontFamily,
LineHeight,
OverallTheme,
} from '../../source-editor/extensions/theme'
import type {
Keybindings,
PdfViewer,
ProjectCompiler,
} from '../../../../../types/project-settings'
import type { ProjectCompiler } from '../../../../../types/project-settings'
import { sendMB } from '../../../infrastructure/event-tracking'
import { postJSON } from '../../../infrastructure/fetch-json'
import { debugConsole } from '@/utils/debugging'
export type UserSettings = {
pdfViewer: PdfViewer
autoComplete: boolean
autoPairDelimiters: boolean
syntaxValidation: boolean
editorTheme: string
overallTheme: OverallTheme
mode: Keybindings
fontSize: string
fontFamily: FontFamily
lineHeight: LineHeight
}
import { UserSettings } from '../../../../../types/user-settings'
export type ProjectSettings = {
compiler: ProjectCompiler

View file

@ -8,14 +8,8 @@ import {
import { EditorView, lineNumbers } from '@codemirror/view'
import { indentationMarkers } from '@replit/codemirror-indentation-markers'
import { highlights, setHighlightsEffect } from '../../extensions/highlights'
import useScopeValue from '../../../../shared/hooks/use-scope-value'
import {
theme,
Options,
setOptionsTheme,
FontFamily,
LineHeight,
} from '../../extensions/theme'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { theme, Options, setOptionsTheme } from '../../extensions/theme'
import { indentUnit } from '@codemirror/language'
import { Highlight } from '../../services/types/doc'
import useIsMounted from '../../../../shared/hooks/use-is-mounted'
@ -50,9 +44,8 @@ function DocumentDiffViewer({
doc: string
highlights: Highlight[]
}) {
const [fontFamily] = useScopeValue<FontFamily>('settings.fontFamily')
const [fontSize] = useScopeValue<number>('settings.fontSize')
const [lineHeight] = useScopeValue<LineHeight>('settings.lineHeight')
const { userSettings } = useUserSettingsContext()
const { fontFamily, fontSize, lineHeight } = userSettings
const isMounted = useIsMounted()
const { t } = useTranslation()

View file

@ -21,7 +21,6 @@ import { getMockIde } from '@/shared/context/mock/mock-ide'
import { populateEditorScope } from '@/features/ide-react/scope-adapters/editor-manager-context-adapter'
import { postJSON } from '@/infrastructure/fetch-json'
import { EventLog } from '@/features/ide-react/editor/event-log'
import { populateSettingsScope } from '@/features/ide-react/scope-adapters/settings-adapter'
import { populateOnlineUsersScope } from '@/features/ide-react/context/online-users-context'
import { populateReferenceScope } from '@/features/ide-react/context/references-context'
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
@ -69,7 +68,6 @@ function createReactScopeValueStore(projectId: string) {
populateLayoutScope(scopeStore)
populateProjectScope(scopeStore)
populatePdfScope(scopeStore)
populateSettingsScope(scopeStore)
populateOnlineUsersScope(scopeStore)
populateReferenceScope(scopeStore)
populateReviewPanelScope(scopeStore)

View file

@ -18,6 +18,7 @@ import { ReferencesProvider } from '@/features/ide-react/context/references-cont
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { ModalsContextProvider } from '@/features/ide-react/context/modals-context'
import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
export const ReactContextRoot: FC = ({ children }) => {
return (
@ -25,6 +26,7 @@ export const ReactContextRoot: FC = ({ children }) => {
<ConnectionProvider>
<IdeReactProvider>
<UserProvider>
<UserSettingsProvider>
<ProjectProvider>
<FileTreeDataProvider>
<FileTreePathProvider>
@ -56,6 +58,7 @@ export const ReactContextRoot: FC = ({ children }) => {
</FileTreePathProvider>
</FileTreeDataProvider>
</ProjectProvider>
</UserSettingsProvider>
</UserProvider>
</IdeReactProvider>
</ConnectionProvider>

View file

@ -1,5 +0,0 @@
import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
export function populateSettingsScope(store: ReactScopeValueStore) {
store.set('settings', window.userSettings)
}

View file

@ -20,7 +20,7 @@ import { useTranslation } from 'react-i18next'
import Tooltip from '../../../shared/components/tooltip'
import Icon from '../../../shared/components/icon'
import classnames from 'classnames'
import useScopeValue from '../../../shared/hooks/use-scope-value'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { getStoredSelection, setStoredSelection } from '../extensions/search'
import { debounce } from 'lodash'
import { EditorSelection, EditorState } from '@codemirror/state'
@ -46,8 +46,8 @@ const CodeMirrorSearchForm: FC = () => {
const view = useCodeMirrorViewContext()
const state = useCodeMirrorStateContext()
const [keybindings] = useScopeValue<string>('settings.mode')
const emacsKeybindingsActive = keybindings === 'emacs'
const { userSettings } = useUserSettingsContext()
const emacsKeybindingsActive = userSettings.mode === 'emacs'
const [activeSearchOption, setActiveSearchOption] =
useState<ActiveSearchOption>(null)

View file

@ -6,10 +6,7 @@ import useEventListener from '../../../shared/hooks/use-event-listener'
import useScopeEventListener from '../../../shared/hooks/use-scope-event-listener'
import { createExtensions } from '../extensions'
import {
FontFamily,
LineHeight,
lineHeights,
OverallTheme,
setEditorTheme,
setOptionsTheme,
} from '../extensions/theme'
@ -49,6 +46,7 @@ import { useErrorHandler } from 'react-error-boundary'
import { setVisual } from '../extensions/visual/visual'
import getMeta from '../../../utils/meta'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
function useCodeMirrorScope(view: EditorView) {
const ide = useIdeContext()
@ -67,17 +65,18 @@ function useCodeMirrorScope(view: EditorView) {
const [docName] = useScopeValue<string>('editor.open_doc_name')
const [trackChanges] = useScopeValue<boolean>('editor.trackChanges')
const [fontFamily] = useScopeValue<FontFamily>('settings.fontFamily')
const [fontSize] = useScopeValue<number>('settings.fontSize')
const [lineHeight] = useScopeValue<LineHeight>('settings.lineHeight')
const [overallTheme] = useScopeValue<OverallTheme>('settings.overallTheme')
const [autoComplete] = useScopeValue<boolean>('settings.autoComplete')
const [editorTheme] = useScopeValue<string>('settings.editorTheme')
const [autoPairDelimiters] = useScopeValue<boolean>(
'settings.autoPairDelimiters'
)
const [mode] = useScopeValue<string>('settings.mode')
const [syntaxValidation] = useScopeValue<boolean>('settings.syntaxValidation')
const { userSettings } = useUserSettingsContext()
const {
fontFamily,
fontSize,
lineHeight,
overallTheme,
autoComplete,
editorTheme,
autoPairDelimiters,
mode,
syntaxValidation,
} = userSettings
const [cursorHighlights] = useScopeValue<Record<string, Highlight[]>>(
'onlineUserCursorHighlights'

View file

@ -367,12 +367,9 @@ If the project has been renamed please look in your project list for a new proje
'vibrant_ink',
]
$scope.darkTheme = false
$scope.$watch('settings.editorTheme', function (theme) {
if (Array.from(DARK_THEMES).includes(theme)) {
return ($scope.darkTheme = true)
} else {
return ($scope.darkTheme = false)
}
// Listen for settings change from React
window.addEventListener('settings:change', event => {
$scope.darkTheme = DARK_THEMES.includes(event.detail.editorTheme)
})
ide.localStorage = localStorage

View file

@ -31,6 +31,7 @@ import { useLayoutContext } from './layout-context'
import { useUserContext } from './user-context'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
export const LocalCompileContext = createContext()
@ -114,8 +115,9 @@ export function LocalCompileProvider({ children }) {
'pdf.logEntryAnnotations'
)
// the PDF viewer
const [pdfViewer] = useScopeValue('settings.pdfViewer')
// the PDF viewer and whether syntax validation is enabled globally
const { userSettings } = useUserSettingsContext()
const { pdfViewer, syntaxValidation } = userSettings
// the URL for downloading the PDF
const [, setPdfDownloadUrl] = useScopeValueSetterOnly('pdf.downloadUrl')
@ -232,9 +234,6 @@ export function LocalCompileProvider({ children }) {
// whether the editor linter found errors
const [hasLintingError, setHasLintingError] = useScopeValue('hasLintingError')
// whether syntax validation is enabled globally
const [syntaxValidation] = useScopeValue('settings.syntaxValidation')
// the timestamp that a doc was last changed
const [changedAt, setChangedAt] = useState(0)

View file

@ -14,12 +14,14 @@ import { SplitTestProvider } from './split-test-context'
import { FileTreeDataProvider } from './file-tree-data-context'
import { ProjectSettingsProvider } from '@/features/editor-left-menu/context/project-settings-context'
import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
export function ContextRoot({ children, ide }) {
return (
<SplitTestProvider>
<IdeAngularProvider ide={ide}>
<UserProvider>
<UserSettingsProvider>
<ProjectProvider>
<FileTreeDataProvider>
<FileTreePathProvider>
@ -39,6 +41,7 @@ export function ContextRoot({ children, ide }) {
</FileTreePathProvider>
</FileTreeDataProvider>
</ProjectProvider>
</UserSettingsProvider>
</UserProvider>
</IdeAngularProvider>
</SplitTestProvider>

View file

@ -0,0 +1,74 @@
import {
createContext,
useContext,
useMemo,
Dispatch,
SetStateAction,
FC,
useState,
useEffect,
} from 'react'
import { UserSettings } from '../../../../types/user-settings'
import getMeta from '@/utils/meta'
const defaultSettings: UserSettings = {
pdfViewer: 'pdfjs',
autoComplete: true,
autoPairDelimiters: true,
syntaxValidation: false,
editorTheme: 'textmate',
overallTheme: '',
mode: 'default',
fontSize: 12,
fontFamily: 'monaco',
lineHeight: 'normal',
}
type UserSettingsContextValue = {
userSettings: UserSettings
setUserSettings: Dispatch<
SetStateAction<UserSettingsContextValue['userSettings']>
>
}
export const UserSettingsContext = createContext<
UserSettingsContextValue | undefined
>(undefined)
export const UserSettingsProvider: FC = ({ children }) => {
const [userSettings, setUserSettings] = useState<
UserSettingsContextValue['userSettings']
>(() => getMeta('ol-userSettings') || defaultSettings)
const value = useMemo<UserSettingsContextValue>(
() => ({
userSettings,
setUserSettings,
}),
[userSettings, setUserSettings]
)
// Fire an event to inform non-React code of settings changes
useEffect(() => {
window.dispatchEvent(
new CustomEvent('settings:change', { detail: userSettings })
)
}, [userSettings])
return (
<UserSettingsContext.Provider value={value}>
{children}
</UserSettingsContext.Provider>
)
}
export function useUserSettingsContext() {
const context = useContext(UserSettingsContext)
if (!context) {
throw new Error(
'useUserSettingsContext is only available inside UserSettingsProvider'
)
}
return context
}

View file

@ -231,12 +231,12 @@ describe('<PdfPreview/>', function () {
const scope = mockScope()
// enable linting in the editor
scope.settings.syntaxValidation = true
const userSettings = { syntaxValidation: true }
// mock a linting error
scope.hasLintingError = true
cy.mount(
<EditorProviders scope={scope}>
<EditorProviders scope={scope} userSettings={userSettings}>
<div className="pdf-viewer">
<PdfPreview />
</div>

View file

@ -140,11 +140,11 @@ contentLine3
\\end{document}`
const scope = mockScope(shortDoc)
scope.settings.mode = 'emacs'
const userSettings = { mode: 'emacs' }
cy.mount(
<Container>
<EditorProviders scope={scope}>
<EditorProviders scope={scope} userSettings={userSettings}>
<CodeMirrorEditor />
</EditorProviders>
</Container>
@ -245,11 +245,11 @@ contentLine3
`
const scope = mockScope(shortDoc)
scope.settings.mode = 'vim'
const userSettings = { mode: 'vim' }
cy.mount(
<Container>
<EditorProviders scope={scope}>
<EditorProviders scope={scope} userSettings={userSettings}>
<CodeMirrorEditor />
</EditorProviders>
</Container>

View file

@ -41,13 +41,13 @@ describe('<CodeMirrorEditor/>', { scrollBehavior: false }, function () {
it('renders client-side lint annotations in the gutter', function () {
const scope = mockScope()
scope.settings.syntaxValidation = true
const userSettings = { syntaxValidation: true }
cy.clock()
cy.mount(
<Container>
<EditorProviders scope={scope}>
<EditorProviders scope={scope} userSettings={userSettings}>
<CodeMirrorEditor />
</EditorProviders>
</Container>
@ -89,11 +89,13 @@ describe('<CodeMirrorEditor/>', { scrollBehavior: false }, function () {
],
}
const userSettings = { syntaxValidation: false }
cy.clock()
cy.mount(
<Container>
<EditorProviders scope={scope}>
<EditorProviders scope={scope} userSettings={userSettings}>
<CodeMirrorEditor />
</EditorProviders>
</Container>
@ -318,11 +320,11 @@ describe('<CodeMirrorEditor/>', { scrollBehavior: false }, function () {
cy.interceptCompile()
const scope = mockScope()
scope.settings.mode = 'vim'
const userSettings = { mode: 'vim' }
cy.mount(
<Container>
<EditorProviders scope={scope}>
<EditorProviders scope={scope} userSettings={userSettings}>
<CodeMirrorEditor />
</EditorProviders>
</Container>

View file

@ -7,18 +7,6 @@ export const figuresFolderId = '123456789012345678901234'
export const figureId = '234567890123456789012345'
export const mockScope = (content?: string) => {
return {
settings: {
fontSize: 12,
fontFamily: 'monaco',
lineHeight: 'normal',
editorTheme: 'textmate',
overallTheme: '',
mode: 'default',
autoComplete: true,
autoPairDelimiters: true,
trackChanges: true,
syntaxValidation: false,
},
editor: {
sharejs_doc: mockDoc(content),
open_doc_name: 'test.tex',

View file

@ -14,6 +14,7 @@ import { LocalCompileProvider } from '@/shared/context/local-compile-context'
import { DetachCompileProvider } from '@/shared/context/detach-compile-context'
import { ProjectSettingsProvider } from '@/features/editor-left-menu/context/project-settings-context'
import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
// these constants can be imported in tests instead of
// using magic strings
@ -22,6 +23,20 @@ export const PROJECT_NAME = 'project-name'
export const USER_ID = '123abd'
export const USER_EMAIL = 'testuser@example.com'
const defaultUserSettings = {
pdfViewer: 'pdfjs',
fontSize: 12,
fontFamily: 'monaco',
lineHeight: 'normal',
editorTheme: 'textmate',
overallTheme: '',
mode: 'default',
autoComplete: true,
autoPairDelimiters: true,
trackChanges: true,
syntaxValidation: false,
}
export function EditorProviders({
user = { id: USER_ID, email: USER_EMAIL },
projectId = PROJECT_ID,
@ -71,12 +86,17 @@ export function EditorProviders({
},
},
},
userSettings = {},
providers = {},
}) {
window.user = user || window.user
window.gitBridgePublicBaseUrl = 'https://git.overleaf.test'
window.project_id = projectId != null ? projectId : window.project_id
window.isRestrictedTokenMember = isRestrictedTokenMember
window.metaAttributesCache.set(
'ol-userSettings',
merge({}, defaultUserSettings, userSettings)
)
const $scope = merge(
{
@ -126,6 +146,7 @@ export function EditorProviders({
ProjectSettingsProvider,
SplitTestProvider,
UserProvider,
UserSettingsProvider,
...providers,
}
@ -133,6 +154,7 @@ export function EditorProviders({
<Providers.SplitTestProvider>
<Providers.IdeAngularProvider ide={window._ide}>
<Providers.UserProvider>
<Providers.UserSettingsProvider>
<Providers.ProjectProvider>
<Providers.FileTreeDataProvider>
<Providers.FileTreePathProvider>
@ -152,6 +174,7 @@ export function EditorProviders({
</Providers.FileTreePathProvider>
</Providers.FileTreeDataProvider>
</Providers.ProjectProvider>
</Providers.UserSettingsProvider>
</Providers.UserProvider>
</Providers.IdeAngularProvider>
</Providers.SplitTestProvider>

View file

@ -0,0 +1,19 @@
import { Keybindings, PdfViewer } from './project-settings'
import {
FontFamily,
LineHeight,
OverallTheme,
} from '@/features/source-editor/extensions/theme'
export type UserSettings = {
pdfViewer: PdfViewer
autoComplete: boolean
autoPairDelimiters: boolean
syntaxValidation: boolean
editorTheme: string
overallTheme: OverallTheme
mode: Keybindings
fontSize: number
fontFamily: FontFamily
lineHeight: LineHeight
}

View file

@ -3,7 +3,7 @@ import { OAuthProviders } from './oauth-providers'
import { OverallThemeMeta } from './project-settings'
import { User } from './user'
import 'recurly__recurly-js'
import { UserSettings } from '@/features/editor-left-menu/utils/api'
import { UserSettings } from './user-settings'
declare global {
// eslint-disable-next-line no-unused-vars