settings-modal-context.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import { createContext, FC, useContext, useMemo, useState } from 'react'
  2. import { useLayoutContext } from '@/shared/context/layout-context'
  3. import AutoCloseBracketsSetting from '@/features/settings/components/editor-settings/auto-close-brackets-setting'
  4. import AutoCompleteSetting from '@/features/settings/components/editor-settings/auto-complete-setting'
  5. import CodeCheckSetting from '@/features/settings/components/editor-settings/code-check-setting'
  6. import PreviewTabsSetting from '@/features/settings/components/editor-settings/preview-tabs-setting'
  7. import KeybindingSetting from '@/features/settings/components/editor-settings/keybinding-setting'
  8. import PDFViewerSetting from '@/features/settings/components/editor-settings/pdf-viewer-setting'
  9. import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
  10. import SpellCheckSetting from '@/features/settings/components/editor-settings/spell-check-setting'
  11. import DictionarySetting from '@/features/settings/components/editor-settings/dictionary-setting'
  12. import { useTranslation } from 'react-i18next'
  13. import BreadcrumbsSetting from '@/features/settings/components/editor-settings/breadcrumbs-setting'
  14. import NonBlinkingCursorSetting from '@/features/settings/components/editor-settings/non-blinking-cursor-setting'
  15. import MathPreviewSetting from '@/features/settings/components/editor-settings/math-preview-setting'
  16. import RootDocumentSetting from '@/features/settings/components/compiler-settings/root-document-setting'
  17. import CompilerSetting from '@/features/settings/components/compiler-settings/compiler-setting'
  18. import ImageNameSetting from '@/features/settings/components/compiler-settings/image-name-setting'
  19. import DraftSetting from '@/features/settings/components/compiler-settings/draft-setting'
  20. import StopOnFirstErrorSetting from '@/features/settings/components/compiler-settings/stop-on-first-error-setting'
  21. import AutoCompileSetting from '@/features/settings/components/compiler-settings/auto-compile-setting'
  22. import OverallThemeSetting from '@/features/settings/components/appearance-settings/overall-theme-setting'
  23. import EditorThemeSetting from '@/features/settings/components/appearance-settings/editor-theme-setting'
  24. import FontSizeSetting from '@/features/settings/components/appearance-settings/font-size-setting'
  25. import LineHeightSetting from '@/features/settings/components/appearance-settings/line-height-setting'
  26. import FontFamilySetting from '@/features/settings/components/appearance-settings/font-family-setting'
  27. import { AvailableUnfilledIcon } from '@/shared/components/material-icon'
  28. import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
  29. import DarkModePdfSetting from '@/features/settings/components/appearance-settings/dark-mode-pdf-setting'
  30. import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
  31. import { useFeatureFlag } from '@/shared/context/split-test-context'
  32. import ProjectNotificationsSetting from '@/features/settings/components/editor-settings/project-notifications-setting'
  33. import getMeta from '@/utils/meta'
  34. const [referenceSearchSettingModule] = importOverleafModules(
  35. 'referenceSearchSetting'
  36. )
  37. const ReferenceSearchSetting = referenceSearchSettingModule?.import.default
  38. type Setting = {
  39. key: string
  40. component: React.ReactNode
  41. hidden?: boolean
  42. }
  43. type SettingsSection = {
  44. title?: string
  45. key: string
  46. settings: Setting[]
  47. }
  48. export type SettingsTab = {
  49. key: string
  50. icon: AvailableUnfilledIcon
  51. sections: SettingsSection[]
  52. title: string
  53. hidden?: boolean
  54. }
  55. type SettingsLink = {
  56. key: string
  57. icon: AvailableUnfilledIcon
  58. href: string
  59. title: string
  60. hidden?: boolean
  61. }
  62. export type SettingsEntry = SettingsLink | SettingsTab
  63. type SettingsModalState = {
  64. show: boolean
  65. setShow: (shown: boolean) => void
  66. activeTab: string | null | undefined
  67. setActiveTab: (tab: string | null | undefined) => void
  68. settingsTabs: SettingsEntry[]
  69. settingToTabMap: Map<string, string>
  70. }
  71. export const SettingsModalContext = createContext<
  72. SettingsModalState | undefined
  73. >(undefined)
  74. export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
  75. children,
  76. }) => {
  77. const { t } = useTranslation()
  78. const { isOverleaf } = getMeta('ol-ExposedSettings')
  79. const { overallTheme } = useProjectSettingsContext()
  80. // TODO ide-redesign-cleanup: Rename this field and move it directly into this context
  81. const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
  82. const hasEmailNotifications = useFeatureFlag('email-notifications')
  83. const hasEditorTabs = useFeatureFlag('editor-tabs')
  84. const allSettingsTabs: SettingsEntry[] = useMemo(
  85. () => [
  86. {
  87. key: 'editor',
  88. title: t('editor'),
  89. icon: 'code',
  90. sections: [
  91. {
  92. key: 'general',
  93. settings: [
  94. {
  95. key: 'autoComplete',
  96. component: <AutoCompleteSetting />,
  97. },
  98. {
  99. key: 'autoPairDelimiters',
  100. component: <AutoCloseBracketsSetting />,
  101. },
  102. {
  103. key: 'nonBlinkingCursor',
  104. component: <NonBlinkingCursorSetting />,
  105. },
  106. {
  107. key: 'syntaxValidation',
  108. component: <CodeCheckSetting />,
  109. },
  110. {
  111. key: 'previewTabs',
  112. component: <PreviewTabsSetting />,
  113. hidden: !hasEditorTabs,
  114. },
  115. {
  116. key: 'mode',
  117. component: <KeybindingSetting />,
  118. },
  119. {
  120. key: 'pdfViewer',
  121. component: <PDFViewerSetting />,
  122. },
  123. {
  124. key: 'write-and-cite-settings',
  125. component: <ReferenceSearchSetting />,
  126. hidden: !ReferenceSearchSetting,
  127. },
  128. ],
  129. },
  130. {
  131. key: 'spellcheck',
  132. title: t('spellcheck'),
  133. settings: [
  134. {
  135. key: 'spellCheckLanguage',
  136. component: <SpellCheckSetting />,
  137. },
  138. {
  139. key: 'dictionary-settings',
  140. component: <DictionarySetting />,
  141. },
  142. ],
  143. },
  144. {
  145. key: 'tools',
  146. title: t('tools'),
  147. settings: [
  148. {
  149. key: 'breadcrumbs-setting',
  150. component: <BreadcrumbsSetting />,
  151. },
  152. {
  153. key: 'mathPreview',
  154. component: <MathPreviewSetting />,
  155. },
  156. ],
  157. },
  158. ],
  159. },
  160. {
  161. key: 'compiler',
  162. title: t('compiler'),
  163. icon: 'picture_as_pdf',
  164. sections: [
  165. {
  166. key: 'general',
  167. settings: [
  168. {
  169. key: 'rootDocId',
  170. component: <RootDocumentSetting />,
  171. },
  172. {
  173. key: 'compiler',
  174. component: <CompilerSetting />,
  175. },
  176. {
  177. key: 'imageName',
  178. component: <ImageNameSetting />,
  179. },
  180. {
  181. key: 'draft',
  182. component: <DraftSetting />,
  183. },
  184. {
  185. key: 'stopOnFirstError',
  186. component: <StopOnFirstErrorSetting />,
  187. },
  188. {
  189. key: 'autoCompile',
  190. component: <AutoCompileSetting />,
  191. },
  192. ],
  193. },
  194. ],
  195. },
  196. {
  197. key: 'appearance',
  198. title: t('appearance'),
  199. icon: 'brush',
  200. sections: [
  201. {
  202. key: 'general',
  203. settings: [
  204. {
  205. key: 'overallTheme',
  206. component: <OverallThemeSetting />,
  207. },
  208. {
  209. key: 'editorTheme',
  210. component: <EditorThemeSetting />,
  211. },
  212. {
  213. key: 'pdfDarkMode',
  214. component: <DarkModePdfSetting />,
  215. hidden: overallTheme === 'light-',
  216. },
  217. {
  218. key: 'fontSize',
  219. component: <FontSizeSetting />,
  220. },
  221. {
  222. key: 'fontFamily',
  223. component: <FontFamilySetting />,
  224. },
  225. {
  226. key: 'lineHeight',
  227. component: <LineHeightSetting />,
  228. },
  229. ],
  230. },
  231. ],
  232. },
  233. {
  234. key: 'project_notifications',
  235. title: t('project_notifications'),
  236. icon: 'notifications' as const,
  237. sections: [
  238. {
  239. key: 'general',
  240. settings: [
  241. {
  242. key: 'projectNotifications',
  243. component: <ProjectNotificationsSetting />,
  244. },
  245. ],
  246. },
  247. ],
  248. hidden: !hasEmailNotifications,
  249. },
  250. {
  251. key: 'account_settings',
  252. title: t('account_settings'),
  253. icon: 'settings',
  254. href: '/user/settings',
  255. },
  256. {
  257. key: 'subscription',
  258. title: t('subscription'),
  259. icon: 'account_balance',
  260. href: '/user/subscription',
  261. hidden: !isOverleaf,
  262. },
  263. ],
  264. [t, overallTheme, hasEmailNotifications, isOverleaf, hasEditorTabs]
  265. )
  266. const settingsTabs = useMemo(
  267. () => allSettingsTabs.filter(tab => !tab.hidden),
  268. [allSettingsTabs]
  269. )
  270. const settingToTabMap = useMemo(() => {
  271. const map = new Map<string, string>()
  272. settingsTabs
  273. .filter(t => 'sections' in t)
  274. .forEach(tab => {
  275. tab.sections.forEach(section => {
  276. section.settings.forEach(setting => {
  277. map.set(setting.key, tab.key)
  278. })
  279. })
  280. })
  281. return map
  282. }, [settingsTabs])
  283. const [activeTab, setActiveTab] = useState<string | null | undefined>(
  284. settingsTabs[0]?.key
  285. )
  286. const value = useMemo(
  287. () => ({
  288. show: leftMenuShown,
  289. setShow: setLeftMenuShown,
  290. activeTab,
  291. setActiveTab,
  292. settingsTabs,
  293. settingToTabMap,
  294. }),
  295. [
  296. leftMenuShown,
  297. setLeftMenuShown,
  298. activeTab,
  299. setActiveTab,
  300. settingsTabs,
  301. settingToTabMap,
  302. ]
  303. )
  304. return (
  305. // TODO ide-redesign-cleanup: Merge <EditorLeftMenuProvider> into <SettingsModalProvider>
  306. <EditorLeftMenuProvider>
  307. <SettingsModalContext.Provider value={value}>
  308. {children}
  309. </SettingsModalContext.Provider>
  310. </EditorLeftMenuProvider>
  311. )
  312. }
  313. export const useSettingsModalContext = () => {
  314. const value = useContext(SettingsModalContext)
  315. if (!value) {
  316. throw new Error(
  317. `useSettingsModalContext is only available inside SettingsModalProvider`
  318. )
  319. }
  320. return value
  321. }