Просмотр исходного кода

Create settings menu react elements in editor left menu for editor left menu react migration

GitOrigin-RevId: e75581a43a8694398b20bee3dc5ec69b60db86ab
M Fahru 3 лет назад
Родитель
Сommit
5eff354971
23 измененных файлов с 661 добавлено и 0 удалено
  1. 5 0
      services/web/app/views/project/editor/meta.pug
  2. 21 0
      services/web/frontend/extracted-translations.json
  3. 2 0
      services/web/frontend/js/features/editor-left-menu/components/editor-left-menu.tsx
  4. 0 0
      services/web/frontend/js/features/editor-left-menu/components/settings-menu-dropdown.tsx
  5. 50 0
      services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx
  6. 23 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-auto-close-brackets.tsx
  7. 23 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-auto-complete.tsx
  8. 37 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-compiler.tsx
  9. 31 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-dictionary.tsx
  10. 53 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-document.tsx
  11. 44 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-editor-theme.tsx
  12. 23 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-font-family.tsx
  13. 21 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-font-size.tsx
  14. 40 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-image-name.tsx
  15. 27 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-keybindings.tsx
  16. 27 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-line-height.tsx
  17. 60 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-menu-select.tsx
  18. 47 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-overall-theme.tsx
  19. 23 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-pdf-viewer.tsx
  20. 36 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-spell-check-language.tsx
  21. 23 0
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-syntax-validation.tsx
  22. 9 0
      services/web/frontend/js/shared/context/layout-context.js
  23. 36 0
      services/web/frontend/stylesheets/app/editor/left-menu.less

+ 5 - 0
services/web/app/views/project/editor/meta.pug

@@ -26,6 +26,11 @@ meta(name="ol-galileoEnabled" data-type="string" content=galileoEnabled)
 meta(name="ol-galileoPromptWords" data-type="string" content=galileoPromptWords)
 meta(name="ol-galileoFeatures" data-type="json" content=galileoFeatures)
 meta(name="ol-detachRole" data-type="string" content=detachRole)
+meta(name="ol-allowedImageNames" data-type="json" content=allowedImageNames)
+meta(name="ol-languages" data-type="json" content=languages)
+meta(name="ol-dictionaryEditorEnabled" data-type="boolean" content=dictionaryEditorEnabled)
+meta(name="ol-editorThemes" data-type="json" content=editorThemes)
+meta(name="ol-legacyEditorThemes" data-type="json" content=legacyEditorThemes)
 meta(name="ol-showUpgradePrompt" data-type="boolean" content=showUpgradePrompt)
 meta(name="ol-useOpenTelemetry" data-type="boolean" content=useOpenTelemetry)
 meta(name="ol-showSupport", data-type="boolean" content=showSupport)

+ 21 - 0
services/web/frontend/extracted-translations.json

@@ -36,7 +36,9 @@
   "ask_proj_owner_to_upgrade_for_git_bridge": "",
   "ask_proj_owner_to_upgrade_for_longer_compiles": "",
   "ask_proj_owner_to_upgrade_for_references_search": "",
+  "auto_close_brackets": "",
   "auto_compile": "",
+  "auto_complete": "",
   "autocompile_disabled": "",
   "autocompile_disabled_reason": "",
   "autocomplete": "",
@@ -47,6 +49,7 @@
   "beta_program_not_participating": "",
   "blank_project": "",
   "blocked_filename": "",
+  "browser": "",
   "can_edit": "",
   "can_link_institution_email_acct_to_institution_acct": "",
   "can_link_your_institution_acct_2": "",
@@ -88,11 +91,13 @@
   "commit": "",
   "common": "",
   "commons_plan_tooltip": "",
+  "compact": "",
   "compile_error_entry_description": "",
   "compile_error_handling": "",
   "compile_larger_projects": "",
   "compile_mode": "",
   "compile_terminated_by_user": "",
+  "compiler": "",
   "compiling": "",
   "confirm": "",
   "confirm_affiliation": "",
@@ -131,6 +136,7 @@
   "department": "",
   "descending": "",
   "description": "",
+  "dictionary": "",
   "did_you_know_institution_providing_professional": "",
   "disable_stop_on_first_error": "",
   "dismiss": "",
@@ -162,6 +168,7 @@
   "duplicate_file": "",
   "duplicate_projects": "",
   "easily_manage_your_project_files_everywhere": "",
+  "edit": "",
   "edit_dictionary": "",
   "edit_dictionary_empty": "",
   "edit_dictionary_remove": "",
@@ -169,6 +176,7 @@
   "editing": "",
   "editor_and_pdf": "",
   "editor_only_hide_pdf": "",
+  "editor_theme": "",
   "email": "",
   "email_or_password_wrong_try_again": "",
   "emails_and_affiliations_explanation": "",
@@ -198,6 +206,8 @@
   "first_name": "",
   "fold_line": "",
   "following_paths_conflict": "",
+  "font_family": "",
+  "font_size": "",
   "free_accounts_have_timeout_upgrade_to_increase": "",
   "free_plan_label": "",
   "free_plan_tooltip": "",
@@ -323,6 +333,7 @@
   "is_email_affiliated": "",
   "join_project": "",
   "joining": "",
+  "keybindings": "",
   "labs_program_already_participating": "",
   "labs_program_benefits": "<0></0>",
   "labs_program_not_participating": "",
@@ -339,6 +350,7 @@
   "leave_projects": "",
   "let_us_know": "",
   "limited_offer": "",
+  "line_height": "",
   "link": "",
   "link_account": "",
   "link_accounts": "",
@@ -368,6 +380,7 @@
   "login_with_service": "",
   "logs_and_output_files": "",
   "looks_like_youre_at": "",
+  "main_document": "",
   "main_file_not_found": "",
   "make_a_copy": "",
   "make_email_primary_description": "",
@@ -433,6 +446,8 @@
   "or": "",
   "other_logs_and_files": "",
   "other_output_files": "",
+  "overall_theme": "",
+  "overleaf": "",
   "overleaf_labs": "",
   "owned_by_x": "",
   "owner": "",
@@ -448,6 +463,7 @@
   "pdf_only_hide_editor": "",
   "pdf_preview_error": "",
   "pdf_rendering_error": "",
+  "pdf_viewer": "",
   "pdf_viewer_error": "",
   "plan_tooltip": "",
   "please_change_primary_to_remove": "",
@@ -579,6 +595,7 @@
   "session_error": "",
   "session_expired_redirecting_to_login": "",
   "sessions": "",
+  "settings": "",
   "share": "",
   "share_project": "",
   "share_with_your_collabs": "",
@@ -605,6 +622,7 @@
   "sort_by": "",
   "sort_by_x": "",
   "source": "",
+  "spell_check": "",
   "sso_link_error": "",
   "start_by_adding_your_email": "",
   "start_free_trial": "",
@@ -624,6 +642,7 @@
   "sync_project_to_github_explanation": "",
   "sync_to_dropbox": "",
   "sync_to_github": "",
+  "syntax_validation": "",
   "tab_connecting": "",
   "tab_no_longer_connected": "",
   "tag_name_cannot_exceed_characters": "",
@@ -634,6 +653,7 @@
   "template_approved_by_publisher": "",
   "templates": "",
   "terminated": "",
+  "tex_live_version": "",
   "thank_you_exclamation": "",
   "thanks_settings_updated": "",
   "this_action_cannot_be_undone": "",
@@ -709,6 +729,7 @@
   "we_cant_find_any_sections_or_subsections_in_this_file": "",
   "we_logged_you_in": "",
   "welcome_to_sl": "",
+  "wide": "",
   "with_premium_subscription_you_also_get": "",
   "word_count": "",
   "work_offline": "",

+ 2 - 0
services/web/frontend/js/features/editor-left-menu/components/editor-left-menu.tsx

@@ -4,6 +4,7 @@ import HelpMenu from './help-menu'
 import { useLayoutContext } from '../../../shared/context/layout-context'
 import classNames from 'classnames'
 import SyncMenu from './sync-menu'
+import SettingsMenu from './settings-menu'
 
 export default function EditorLeftMenu() {
   const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
@@ -17,6 +18,7 @@ export default function EditorLeftMenu() {
         <DownloadMenu />
         <ActionsMenu />
         <SyncMenu />
+        <SettingsMenu />
         <HelpMenu />
       </aside>
       {leftMenuShown ? (

+ 0 - 0
services/web/frontend/js/features/editor-left-menu/components/settings-menu-dropdown.tsx


+ 50 - 0
services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx

@@ -0,0 +1,50 @@
+import { Form } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+import getMeta from '../../../utils/meta'
+import SettingsAutoCloseBrackets from './settings/settings-auto-close-brackets'
+import SettingsAutoComplete from './settings/settings-auto-complete'
+import SettingsCompiler from './settings/settings-compiler'
+import SettingsDictionary from './settings/settings-dictionary'
+import SettingsDocument from './settings/settings-document'
+import SettingsEditorTheme from './settings/settings-editor-theme'
+import SettingsFontFamily from './settings/settings-font-family'
+import SettingsFontSize from './settings/settings-font-size'
+import SettingsImageName from './settings/settings-image-name'
+import SettingsKeybindings from './settings/settings-keybindings'
+import SettingsLineHeight from './settings/settings-line-height'
+import SettingsOverallTheme from './settings/settings-overall-theme'
+import SettingsPdfViewer from './settings/settings-pdf-viewer'
+import SettingsSpellCheckLanguage from './settings/settings-spell-check-language'
+import SettingsSyntaxValidation from './settings/settings-syntax-validation'
+
+export default function SettingsMenu() {
+  const { t } = useTranslation()
+  const anonymous = getMeta('ol-anonymous') as boolean | undefined
+
+  if (anonymous === true || anonymous === undefined) {
+    return null
+  }
+
+  return (
+    <>
+      <h4>{t('settings')}</h4>
+      <Form className="settings">
+        <SettingsCompiler />
+        <SettingsImageName />
+        <SettingsDocument />
+        <SettingsSpellCheckLanguage />
+        <SettingsDictionary />
+        <SettingsAutoComplete />
+        <SettingsAutoCloseBrackets />
+        <SettingsSyntaxValidation />
+        <SettingsEditorTheme />
+        <SettingsOverallTheme />
+        <SettingsKeybindings />
+        <SettingsFontSize />
+        <SettingsFontFamily />
+        <SettingsLineHeight />
+        <SettingsPdfViewer />
+      </Form>
+    </>
+  )
+}

+ 23 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-auto-close-brackets.tsx

@@ -0,0 +1,23 @@
+import { useTranslation } from 'react-i18next'
+import SettingsMenuSelect from './settings-menu-select'
+
+export default function SettingsAutoCloseBrackets() {
+  const { t } = useTranslation()
+
+  return (
+    <SettingsMenuSelect
+      options={[
+        {
+          value: 'true',
+          label: t('on'),
+        },
+        {
+          value: 'false',
+          label: t('off'),
+        },
+      ]}
+      label={t('auto_close_brackets')}
+      name="autoPairDelimiters"
+    />
+  )
+}

+ 23 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-auto-complete.tsx

@@ -0,0 +1,23 @@
+import { useTranslation } from 'react-i18next'
+import SettingsMenuSelect from './settings-menu-select'
+
+export default function SettingsAutoComplete() {
+  const { t } = useTranslation()
+
+  return (
+    <SettingsMenuSelect
+      options={[
+        {
+          value: 'true',
+          label: t('on'),
+        },
+        {
+          value: 'false',
+          label: t('off'),
+        },
+      ]}
+      label={t('auto_complete')}
+      name="autoComplete"
+    />
+  )
+}

+ 37 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-compiler.tsx

@@ -0,0 +1,37 @@
+import { useTranslation } from 'react-i18next'
+import { useEditorContext } from '../../../../shared/context/editor-context'
+import SettingsMenuSelect from './settings-menu-select'
+
+export default function SettingsCompiler() {
+  const { t } = useTranslation()
+  const { permissionsLevel } = useEditorContext()
+
+  if (permissionsLevel === 'readOnly') {
+    return null
+  }
+
+  return (
+    <SettingsMenuSelect
+      options={[
+        {
+          value: 'pdflatex',
+          label: 'pdfLaTeX',
+        },
+        {
+          value: 'latex',
+          label: 'LaTeX',
+        },
+        {
+          value: 'xelatex',
+          label: 'XeLaTeX',
+        },
+        {
+          value: 'lualatex',
+          label: 'LuaLaTeX',
+        },
+      ]}
+      label={t('compiler')}
+      name="compiler"
+    />
+  )
+}

+ 31 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-dictionary.tsx

@@ -0,0 +1,31 @@
+import { useState } from 'react'
+import { Button } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+import getMeta from '../../../../utils/meta'
+import DictionaryModal from '../../../dictionary/components/dictionary-modal'
+
+export default function SettingsDictionary() {
+  const { t } = useTranslation()
+  const [showModal, setShowModal] = useState(false)
+  const dictionaryEditorEnabled = getMeta(
+    'ol-dictionaryEditorEnabled'
+  ) as boolean
+
+  if (!dictionaryEditorEnabled) {
+    return null
+  }
+
+  return (
+    <div className="form-group left-menu-setting">
+      <label htmlFor="dictionary">{t('dictionary')}</label>
+      <Button bsSize="xs" bsStyle="default" onClick={() => setShowModal(true)}>
+        {t('edit')}
+      </Button>
+
+      <DictionaryModal
+        show={showModal}
+        handleHide={() => setShowModal(false)}
+      />
+    </div>
+  )
+}

+ 53 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-document.tsx

@@ -0,0 +1,53 @@
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import isValidTeXFile from '../../../../main/is-valid-tex-file'
+import { useEditorContext } from '../../../../shared/context/editor-context'
+import { useProjectContext } from '../../../../shared/context/project-context'
+import useScopeValue from '../../../../shared/hooks/use-scope-value'
+import SettingsMenuSelect from './settings-menu-select'
+import type { Option } from './settings-menu-select'
+
+type Doc = {
+  doc: {
+    name: string
+    id: string
+    type: string
+    selected: boolean
+  }
+  path: string
+}
+
+export default function SettingsDocument() {
+  const { t } = useTranslation()
+
+  const { permissionsLevel } = useEditorContext()
+
+  const { rootDocId } = useProjectContext()
+  const [docs] = useScopeValue<Doc[] | undefined>('docs')
+
+  const validDocsOptions = useMemo(() => {
+    const filteredDocs =
+      docs?.filter(
+        doc => isValidTeXFile(doc.doc.name) || rootDocId === doc.doc.id
+      ) ?? []
+
+    const mappedDocs: Array<Option> = filteredDocs.map(doc => ({
+      value: doc.doc.id,
+      label: doc.doc.name,
+    }))
+
+    return mappedDocs
+  }, [docs, rootDocId])
+
+  if (permissionsLevel === 'readOnly') {
+    return null
+  }
+
+  return (
+    <SettingsMenuSelect
+      options={validDocsOptions}
+      label={t('main_document')}
+      name="rootDoc_id"
+    />
+  )
+}

+ 44 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-editor-theme.tsx

@@ -0,0 +1,44 @@
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import getMeta from '../../../../utils/meta'
+import SettingsMenuSelect from './settings-menu-select'
+import type { Option } from './settings-menu-select'
+
+export default function SettingsEditorTheme() {
+  const { t } = useTranslation()
+  const editorThemes = getMeta('ol-editorThemes') as string[] | undefined
+  const legacyEditorThemes = getMeta('ol-legacyEditorThemes') as
+    | string[]
+    | undefined
+
+  const options = useMemo(() => {
+    const editorThemeOptions: Array<Option> =
+      editorThemes?.map(theme => ({
+        value: theme,
+        label: theme.replace(/_/g, ' '),
+      })) ?? []
+
+    const dividerOption: Option = {
+      value: '-',
+      label: '—————————————————',
+      ariaHidden: 'true',
+      disabled: true,
+    }
+
+    const legacyEditorThemeOptions: Array<Option> =
+      legacyEditorThemes?.map(theme => ({
+        value: theme,
+        label: theme.replace(/_/g, ' '),
+      })) ?? []
+
+    return [...editorThemeOptions, dividerOption, ...legacyEditorThemeOptions]
+  }, [editorThemes, legacyEditorThemes])
+
+  return (
+    <SettingsMenuSelect
+      label={t('editor_theme')}
+      name="editorTheme"
+      options={options}
+    />
+  )
+}

+ 23 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-font-family.tsx

@@ -0,0 +1,23 @@
+import { useTranslation } from 'react-i18next'
+import SettingsMenuSelect from './settings-menu-select'
+
+export default function SettingsFontFamily() {
+  const { t } = useTranslation()
+
+  return (
+    <SettingsMenuSelect
+      options={[
+        {
+          value: 'monaco',
+          label: 'Monaco / Menlo / Consolas',
+        },
+        {
+          value: 'lucida',
+          label: 'Lucida / Source Code Pro',
+        },
+      ]}
+      label={t('font_family')}
+      name="fontFamily"
+    />
+  )
+}

+ 21 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-font-size.tsx

@@ -0,0 +1,21 @@
+import { useTranslation } from 'react-i18next'
+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 => ({
+  value: size,
+  label: `${size}px`,
+}))
+
+export default function SettingsFontSize() {
+  const { t } = useTranslation()
+
+  return (
+    <SettingsMenuSelect
+      options={options}
+      label={t('font_size')}
+      name="fontSize"
+    />
+  )
+}

+ 40 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-image-name.tsx

@@ -0,0 +1,40 @@
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import getMeta from '../../../../utils/meta'
+import SettingsMenuSelect from './settings-menu-select'
+import type { Option } from './settings-menu-select'
+
+/* eslint-disable react/no-unused-prop-types */
+type AllowedImageName = {
+  imageDesc: string
+  imageName: string
+}
+/* eslint-enable */
+
+export default function SettingsImageName() {
+  const { t } = useTranslation()
+  const allowedImageNames = getMeta('ol-allowedImageNames') as
+    | AllowedImageName[]
+    | undefined
+
+  const options: Array<Option> = useMemo(
+    () =>
+      allowedImageNames?.map(({ imageName, imageDesc }) => ({
+        value: imageName,
+        label: imageDesc,
+      })) ?? [],
+    [allowedImageNames]
+  )
+
+  if ((allowedImageNames?.length ?? 0) === 0) {
+    return null
+  }
+
+  return (
+    <SettingsMenuSelect
+      options={options}
+      label={t('tex_live_version')}
+      name="imageName"
+    />
+  )
+}

+ 27 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-keybindings.tsx

@@ -0,0 +1,27 @@
+import { useTranslation } from 'react-i18next'
+import SettingsMenuSelect from './settings-menu-select'
+
+export default function SettingsKeybindings() {
+  const { t } = useTranslation()
+
+  return (
+    <SettingsMenuSelect
+      options={[
+        {
+          value: 'default',
+          label: 'None',
+        },
+        {
+          value: 'vim',
+          label: 'Vim',
+        },
+        {
+          value: 'emacs',
+          label: 'Emacs',
+        },
+      ]}
+      label={t('keybindings')}
+      name="mode"
+    />
+  )
+}

+ 27 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-line-height.tsx

@@ -0,0 +1,27 @@
+import { useTranslation } from 'react-i18next'
+import SettingsMenuSelect from './settings-menu-select'
+
+export default function SettingsLineHeight() {
+  const { t } = useTranslation()
+
+  return (
+    <SettingsMenuSelect
+      options={[
+        {
+          value: 'compact',
+          label: t('compact'),
+        },
+        {
+          value: 'normal',
+          label: t('normal'),
+        },
+        {
+          value: 'wide',
+          label: t('wide'),
+        },
+      ]}
+      label={t('line_height')}
+      name="lineHeight"
+    />
+  )
+}

+ 60 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-menu-select.tsx

@@ -0,0 +1,60 @@
+export type Option = {
+  value: string
+  label: string
+  ariaHidden?: 'true' | 'false'
+  disabled?: boolean
+}
+
+export type Optgroup = {
+  label: string
+  options: Array<Option>
+}
+
+type SettingsMenuSelectProps = {
+  label: string
+  name: string
+  options: Array<Option>
+  optgroup?: Optgroup
+  loading?: boolean
+}
+
+export default function SettingsMenuSelect({
+  label,
+  name,
+  options,
+  optgroup,
+  loading,
+}: SettingsMenuSelectProps) {
+  return (
+    <div className="form-group left-menu-setting">
+      <label htmlFor={name}>{label}</label>
+      {loading ? (
+        <p className="loading pull-right">
+          <i className="fa fa-fw fa-spin fa-refresh" />
+        </p>
+      ) : (
+        <select name={name} className="form-control">
+          {options.map(option => (
+            <option
+              key={`${name}-${option.value}`}
+              value={option.value}
+              aria-hidden={option.ariaHidden}
+              disabled={option.disabled}
+            >
+              {option.label}
+            </option>
+          ))}
+          {optgroup ? (
+            <optgroup label={optgroup.label}>
+              {optgroup.options.map(option => (
+                <option value={option.value} key={option.value}>
+                  {option.label}
+                </option>
+              ))}
+            </optgroup>
+          ) : null}
+        </select>
+      )}
+    </div>
+  )
+}

+ 47 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-overall-theme.tsx

@@ -0,0 +1,47 @@
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useLayoutContext } from '../../../../shared/context/layout-context'
+import getMeta from '../../../../utils/meta'
+import SettingsMenuSelect from './settings-menu-select'
+import type { Option } from './settings-menu-select'
+
+type OverallTheme = {
+  name: string
+  path: string
+  val: string
+}
+
+export default function SettingsOverallTheme() {
+  const { t } = useTranslation()
+  const overallThemes = getMeta('ol-overallThemes') as
+    | OverallTheme[]
+    | undefined
+  const { loadingStyleSheet } = useLayoutContext() as {
+    loadingStyleSheet: boolean
+  }
+
+  const options: Array<Option> = useMemo(
+    () =>
+      overallThemes?.map(({ name, val }) => ({
+        value: val,
+        label: name,
+      })) ?? [],
+    [overallThemes]
+  )
+
+  // TODO: check for IEEE brand by:
+  // - const brandVariation = getMeta('ol-brandVariation') as any[]
+  // - settings.overleaf != null && !isIEEE(brandVariation)
+  if (!overallThemes) {
+    return null
+  }
+
+  return (
+    <SettingsMenuSelect
+      options={options}
+      loading={loadingStyleSheet}
+      label={t('overall_theme')}
+      name="overallTheme"
+    />
+  )
+}

+ 23 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-pdf-viewer.tsx

@@ -0,0 +1,23 @@
+import { useTranslation } from 'react-i18next'
+import SettingsMenuSelect from './settings-menu-select'
+
+export default function SettingsPdfViewer() {
+  const { t } = useTranslation()
+
+  return (
+    <SettingsMenuSelect
+      options={[
+        {
+          value: 'pdfjs',
+          label: t('overleaf'),
+        },
+        {
+          value: 'native',
+          label: t('browser'),
+        },
+      ]}
+      label={t('pdf_viewer')}
+      name="pdfViewer"
+    />
+  )
+}

+ 36 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-spell-check-language.tsx

@@ -0,0 +1,36 @@
+import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
+import getMeta from '../../../../utils/meta'
+import SettingsMenuSelect from './settings-menu-select'
+import type { Optgroup } from './settings-menu-select'
+
+type Language = {
+  name: string
+  code: string
+}
+
+export default function SettingsSpellCheckLanguage() {
+  const { t } = useTranslation()
+  const languages = getMeta('ol-languages') as Language[] | undefined
+
+  const optgroup: Optgroup = useMemo(
+    () => ({
+      label: 'Language',
+      options:
+        languages?.map(language => ({
+          value: language.code,
+          label: language.name,
+        })) ?? [],
+    }),
+    [languages]
+  )
+
+  return (
+    <SettingsMenuSelect
+      options={[{ value: '', label: t('off') }]}
+      optgroup={optgroup}
+      label={t('spell_check')}
+      name="spellCheckLanguage"
+    />
+  )
+}

+ 23 - 0
services/web/frontend/js/features/editor-left-menu/components/settings/settings-syntax-validation.tsx

@@ -0,0 +1,23 @@
+import { useTranslation } from 'react-i18next'
+import SettingsMenuSelect from './settings-menu-select'
+
+export default function SettingsSyntaxValidation() {
+  const { t } = useTranslation()
+
+  return (
+    <SettingsMenuSelect
+      options={[
+        {
+          value: 'true',
+          label: t('on'),
+        },
+        {
+          value: 'false',
+          label: t('off'),
+        },
+      ]}
+      label={t('syntax_validation')}
+      name="syntaxValidation"
+    />
+  )
+}

+ 9 - 0
services/web/frontend/js/shared/context/layout-context.js

@@ -84,6 +84,11 @@ export function LayoutProvider({ children }) {
   // whether to display the editor and preview side-by-side or full-width ("flat")
   const [pdfLayout, setPdfLayout] = useScopeValue('ui.pdfLayout')
 
+  // whether stylesheet on theme is loading
+  const [loadingStyleSheet, setLoadingStyleSheet] = useScopeValue(
+    'ui.loadingStyleSheet'
+  )
+
   // switch to either side-by-side or flat (full-width) layout
   const switchLayout = useCallback(() => {
     setPdfLayout(layout => {
@@ -157,10 +162,12 @@ export function LayoutProvider({ children }) {
       pdfLayout,
       pdfPreviewOpen,
       reviewPanelOpen,
+      loadingStyleSheet,
       setChatIsOpen,
       setLeftMenuShown,
       setPdfLayout,
       setReviewPanelOpen,
+      setLoadingStyleSheet,
       setView,
       switchLayout,
       view,
@@ -176,10 +183,12 @@ export function LayoutProvider({ children }) {
       pdfLayout,
       pdfPreviewOpen,
       reviewPanelOpen,
+      loadingStyleSheet,
       setChatIsOpen,
       setLeftMenuShown,
       setPdfLayout,
       setReviewPanelOpen,
+      setLoadingStyleSheet,
       setView,
       switchLayout,
       view,

+ 36 - 0
services/web/frontend/stylesheets/app/editor/left-menu.less

@@ -145,6 +145,42 @@
       }
     }
   }
+
+  .left-menu-setting {
+    padding: 0 9px;
+    display: flex;
+    flex-wrap: wrap;
+    align-items: baseline;
+    justify-content: flex-end;
+    border-bottom: solid 1px rgba(0, 0, 0, 0.07);
+    margin-bottom: 0;
+
+    &:first-child {
+      margin-top: -9px;
+    }
+
+    &:last-child {
+      border-bottom: 0;
+    }
+
+    &:hover {
+      background-color: @link-color;
+
+      label {
+        color: @white;
+      }
+    }
+
+    select.form-control {
+      height: 23px;
+      padding: 1px @padding-xs;
+      font-size: inherit;
+
+      &:hover {
+        background-color: @ol-blue-gray-1;
+      }
+    }
+  }
 }
 
 #left-menu-mask {