Tearing down of old Editor (Left settings menu) (#31357)

* removing one class

* removing the ide redesign class

* moving error logs files from ide-redesign to orignal directory

* moving the final file of error logs

* removing the left settings menu files

* deleting left-menu-mask.tsx and related css styling

* deleting editor-left-menu-body.tsx

* deleting download-menu.tsx and related css and test and story files

* deleting actions-menu.tsx and  test and story files

* deleting help-menu.tsx and test and story files

* deleting sync-menu.tsx  and test and story files

* deleting settings-menu.tsx file

* deleting download-pdf.tsx

* deleting download-source.tsx

* deleting actions-copy-project.tsx

* deleting actions-word-count.tsx ans tests

* deleting help-contact-us.tsx

* deleting help-documentation.tsx and related files

* deleting help-show-hotkeys.tsx and related files

* deleting settings-auto-close-brackets.tsx

* deleting settings-auto-complete

* settings-compiler

* settings-dictionary

* deleting setting-menu files and test files

* styles:fix

* make cleanup_unused_locales

* removing some extra css and adding some comments

* npm run extract-translations

* adding settings-menu-select.tsx back

* adding back settings-overall-theme.tsx

* format:fix

* removing the settings-overall-theme.tsx and related test file

* deleting settings-menu-select and adding option type in use-editor-theme-option-group

* removing css

* deleting files and styling related to LeftMenuButton

* removing the related left editor menu files

* removing the paths

* Revert "removing the related left editor menu files"

This reverts commit 78ffbfff88cfd3ceb19946ac45a1ed6a790388f4.

* adding back the overall-theme-settings.tsx and removing the tests related to removed file

* adding back the tests with new component

* make cleanup_unused_locales

* extract-translations

* deleting the actions-manage-template.tsx file

GitOrigin-RevId: 75bcbef81740cea0452eca62f2ce52b7f10acd08
This commit is contained in:
Davinder Singh
2026-02-12 10:31:32 +00:00
committed by Copybot
parent 9162c9c7b5
commit 51b5f9a862
70 changed files with 12 additions and 3761 deletions
@@ -648,8 +648,6 @@
"folder_location": "",
"folder_name": "",
"following_paths_conflict": "",
"font_family": "",
"font_size": "",
"footer_about_us": "",
"footer_contact_us": "",
"footnotes": "",
@@ -1011,7 +1009,6 @@
"limited_to_n_collaborators_per_project": "",
"limited_to_n_collaborators_per_project_plural": "",
"line": "",
"line_height": "",
"line_width_is_the_width_of_the_line_in_the_current_environment": "",
"link": "",
"link_account": "",
@@ -1152,7 +1149,6 @@
"new_compile_domain_notice": "",
"new_compiles_in_this_project_will_automatically_use_the_newest_version": "",
"new_create_tables_and_equations": "",
"new_editor_look": "",
"new_error_logs_make_it_easier_to_find_whats_wrong": "",
"new_file": "",
"new_folder": "",
@@ -1326,7 +1322,6 @@
"please_ask_the_project_owner_to_upgrade_to_track_changes": "",
"please_change_primary_to_remove": "",
"please_compile_pdf_before_download": "",
"please_compile_pdf_before_word_count": "",
"please_confirm_primary_email_or_edit": "",
"please_confirm_secondary_email_or_edit": "",
"please_confirm_your_email_before_making_it_default": "",
@@ -1678,7 +1673,6 @@
"show_document_preamble": "",
"show_equation_preview": "",
"show_file_tree": "",
"show_hotkeys": "",
"show_in_code": "",
"show_in_pdf": "",
"show_less": "",
@@ -1723,10 +1717,8 @@
"sort_by": "",
"sort_by_x": "",
"sort_projects": "",
"source": "",
"speak": "",
"speech_input_not_available": "",
"spell_check": "",
"spellcheck": "",
"spellcheck_language": "",
"split_view": "",
@@ -2243,7 +2235,6 @@
"wide": "",
"will_lose_edit_access_on_date": "",
"with_premium_subscription_you_also_get": "",
"word_count": "",
"word_count_lower": "",
"words": "",
"work_in_vim_or_emacs_emulation_mode": "",
@@ -1,30 +0,0 @@
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import EditorCloneProjectModalWrapper from '../../clone-project-modal/components/editor-clone-project-modal-wrapper'
import LeftMenuButton from './left-menu-button'
import * as eventTracking from '../../../infrastructure/event-tracking'
import useOpenProject from '@/shared/hooks/use-open-project'
export default function ActionsCopyProject() {
const [showModal, setShowModal] = useState(false)
const { t } = useTranslation()
const openProject = useOpenProject()
const handleShowModal = useCallback(() => {
eventTracking.sendMB('left-menu-copy')
setShowModal(true)
}, [])
return (
<>
<LeftMenuButton onClick={handleShowModal} icon="file_copy">
{t('copy_project')}
</LeftMenuButton>
<EditorCloneProjectModalWrapper
show={showModal}
handleHide={() => setShowModal(false)}
openProject={openProject}
/>
</>
)
}
@@ -1,39 +0,0 @@
import { ElementType } from 'react'
import { useTranslation } from 'react-i18next'
import getMeta from '../../../utils/meta'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import ActionsCopyProject from './actions-copy-project'
import ActionsWordCount from './actions-word-count'
const components = importOverleafModules('editorLeftMenuManageTemplate') as {
import: { default: ElementType }
path: string
}[]
export default function ActionsMenu() {
const { t } = useTranslation()
const anonymous = getMeta('ol-anonymous')
if (anonymous) {
return null
}
return (
<>
<h4>{t('actions')}</h4>
<ul className="list-unstyled nav">
<li>
<ActionsCopyProject />
</li>
{components.map(({ import: { default: Component }, path }) => (
<li key={path}>
<Component />
</li>
))}
<li>
<ActionsWordCount />
</li>
</ul>
</>
)
}
@@ -1,20 +0,0 @@
import { useState, useCallback } from 'react'
import WordCountModal from '../../word-count-modal/components/word-count-modal'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { WordCountButton } from '@/features/word-count-modal/components/word-count-button'
export default function ActionsWordCount() {
const [showModal, setShowModal] = useState(false)
const handleShowModal = useCallback(() => {
eventTracking.sendMB('left-menu-count')
setShowModal(true)
}, [])
return (
<>
<WordCountButton handleShowModal={handleShowModal} />
<WordCountModal show={showModal} handleHide={() => setShowModal(false)} />
</>
)
}
@@ -1,21 +0,0 @@
import { useTranslation } from 'react-i18next'
import DownloadPDF from './download-pdf'
import DownloadSource from './download-source'
export default function DownloadMenu() {
const { t } = useTranslation()
return (
<>
<h4 className="mt-0">{t('download')}</h4>
<ul className="list-unstyled nav nav-downloads text-center">
<li>
<DownloadSource />
</li>
<li>
<DownloadPDF />
</li>
</ul>
</>
)
}
@@ -1,51 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
import { useProjectContext } from '../../../shared/context/project-context'
import { isSmallDevice } from '../../../infrastructure/event-tracking'
import MaterialIcon from '@/shared/components/material-icon'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
export default function DownloadPDF() {
const { t } = useTranslation()
const { pdfDownloadUrl, pdfUrl } = useCompileContext()
const { projectId } = useProjectContext()
const { sendEvent } = useEditorAnalytics()
function sendDownloadEvent() {
sendEvent('download-pdf-button-click', {
projectId,
location: 'left-menu',
isSmallDevice,
})
}
if (pdfUrl) {
return (
<a
href={pdfDownloadUrl || pdfUrl}
target="_blank"
rel="noreferrer"
onClick={sendDownloadEvent}
>
<MaterialIcon type="picture_as_pdf" size="2x" />
<br />
PDF
</a>
)
} else {
return (
<OLTooltip
id="disabled-pdf-download"
description={t('please_compile_pdf_before_download')}
overlayProps={{ placement: 'bottom' }}
>
<div className="link-disabled">
<MaterialIcon type="picture_as_pdf" size="2x" />
<br />
PDF
</div>
</OLTooltip>
)
}
}
@@ -1,31 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useProjectContext } from '../../../shared/context/project-context'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { isSmallDevice } from '../../../infrastructure/event-tracking'
import MaterialIcon from '@/shared/components/material-icon'
export default function DownloadSource() {
const { t } = useTranslation()
const { projectId } = useProjectContext()
function sendDownloadEvent() {
eventTracking.sendMB('download-zip-button-click', {
projectId,
location: 'left-menu',
isSmallDevice,
})
}
return (
<a
href={`/project/${projectId}/download/zip`}
target="_blank"
rel="noreferrer"
onClick={sendDownloadEvent}
>
<MaterialIcon type="folder_zip" size="2x" />
<br />
{t('source')}
</a>
)
}
@@ -1,17 +0,0 @@
import DownloadMenu from './download-menu'
import ActionsMenu from './actions-menu'
import HelpMenu from './help-menu'
import SyncMenu from './sync-menu'
import SettingsMenu from './settings-menu'
export default function EditorLeftMenuBody() {
return (
<>
<DownloadMenu />
<ActionsMenu />
<SyncMenu />
<SettingsMenu />
<HelpMenu />
</>
)
}
@@ -1,56 +0,0 @@
import { useLayoutContext } from '../../../shared/context/layout-context'
import LeftMenuMask from './left-menu-mask'
import classNames from 'classnames'
import { lazy, memo, Suspense } from 'react'
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
import { Offcanvas } from 'react-bootstrap'
import { EditorLeftMenuProvider } from './editor-left-menu-context'
import withErrorBoundary from '@/infrastructure/error-boundary'
import OLNotification from '@/shared/components/ol/ol-notification'
import { useTranslation } from 'react-i18next'
const EditorLeftMenuBody = lazy(() => import('./editor-left-menu-body'))
const LazyEditorLeftMenuWithErrorBoundary = withErrorBoundary(
() => (
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
<EditorLeftMenuBody />
</Suspense>
),
() => {
const { t } = useTranslation()
return <OLNotification type="error" content={t('something_went_wrong')} />
}
)
function EditorLeftMenu() {
const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
const closeLeftMenu = () => {
setLeftMenuShown(false)
}
return (
<EditorLeftMenuProvider>
<Offcanvas
show={leftMenuShown}
onHide={closeLeftMenu}
backdropClassName="left-menu-modal-backdrop"
id="left-menu-offcanvas"
>
<Offcanvas.Body
className={classNames('full-size', 'left-menu', {
shown: leftMenuShown,
})}
id="left-menu"
data-testid="left-menu"
>
<LazyEditorLeftMenuWithErrorBoundary />
</Offcanvas.Body>
</Offcanvas>
{leftMenuShown && <LeftMenuMask />}
</EditorLeftMenuProvider>
)
}
export default memo(EditorLeftMenu)
@@ -1,24 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useCallback } from 'react'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useContactUsModal } from '../../../shared/hooks/use-contact-us-modal'
import LeftMenuButton from './left-menu-button'
export default function HelpContactUs() {
const { modal, showModal } = useContactUsModal()
const { t } = useTranslation()
const showModalWithAnalytics = useCallback(() => {
eventTracking.sendMB('left-menu-contact')
showModal()
}, [showModal])
return (
<>
<LeftMenuButton onClick={showModalWithAnalytics} icon="contact_support">
{t('contact_us')}
</LeftMenuButton>
{modal}
</>
)
}
@@ -1,14 +0,0 @@
import { useTranslation } from 'react-i18next'
import LeftMenuButton from './left-menu-button'
export default function HelpDocumentation() {
const { t } = useTranslation()
return (
<>
<LeftMenuButton type="link" href="/learn" icon="book_4">
{t('documentation')}
</LeftMenuButton>
</>
)
}
@@ -1,31 +0,0 @@
import { useTranslation } from 'react-i18next'
import getMeta from '../../../utils/meta'
import HelpContactUs from './help-contact-us'
import HelpDocumentation from './help-documentation'
import HelpShowHotkeys from './help-show-hotkeys'
export default function HelpMenu() {
const { t } = useTranslation()
const showSupport = getMeta('ol-showSupport')
return (
<>
<h4>{t('help')}</h4>
<ul className="list-unstyled nav">
<li>
<HelpShowHotkeys />
</li>
{showSupport ? (
<>
<li>
<HelpDocumentation />
</li>
<li>
<HelpContactUs />
</li>
</>
) : null}
</ul>
</>
)
}
@@ -1,32 +0,0 @@
import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useProjectContext } from '../../../shared/context/project-context'
import HotkeysModal from '../../hotkeys-modal/components/hotkeys-modal'
import LeftMenuButton from './left-menu-button'
import { isMac } from '@/shared/utils/os'
export default function HelpShowHotkeys() {
const [showModal, setShowModal] = useState(false)
const { t } = useTranslation()
const { features } = useProjectContext()
const showModalWithAnalytics = useCallback(() => {
eventTracking.sendMB('left-menu-hotkeys')
setShowModal(true)
}, [])
return (
<>
<LeftMenuButton onClick={showModalWithAnalytics} icon="keyboard">
{t('show_hotkeys')}
</LeftMenuButton>
<HotkeysModal
show={showModal}
handleHide={() => setShowModal(false)}
isMac={isMac}
trackChangesVisible={features?.trackChangesVisible}
/>
</>
)
}
@@ -1,72 +0,0 @@
import { PropsWithChildren } from 'react'
import MaterialIcon from '@/shared/components/material-icon'
type Props = {
onClick?: () => void
icon?: string
svgIcon?: React.ReactElement | null
disabled?: boolean
disabledAccesibilityText?: string
type?: 'button' | 'link'
href?: string
translate?: React.HTMLAttributes<HTMLElement>['translate']
}
function LeftMenuButtonIcon({
svgIcon,
icon,
}: {
svgIcon?: React.ReactElement | null
icon?: string
}) {
if (svgIcon) {
return <div className="material-symbols">{svgIcon}</div>
} else if (icon) {
return <MaterialIcon type={icon} />
} else return null
}
export default function LeftMenuButton({
children,
svgIcon,
onClick,
icon,
disabled = false,
disabledAccesibilityText,
type = 'button',
href,
translate,
}: PropsWithChildren<Props>) {
if (disabled) {
return (
<div className="left-menu-button link-disabled">
<LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
<span translate={translate}>{children}</span>
{disabledAccesibilityText ? (
<span className="visually-hidden">{disabledAccesibilityText}</span>
) : null}
</div>
)
}
if (type === 'button') {
return (
<button onClick={onClick} className="left-menu-button">
<LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
<span translate={translate}>{children}</span>
</button>
)
} else {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
className="left-menu-button"
>
<LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
<span translate={translate}>{children}</span>
</a>
)
}
}
@@ -1,39 +0,0 @@
import { memo, useEffect, useRef, useState } from 'react'
import { useLayoutContext } from '../../../shared/context/layout-context'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
export default memo(function LeftMenuMask() {
const { setLeftMenuShown } = useLayoutContext()
const { userSettings } = useUserSettingsContext()
const { editorTheme, editorLightTheme, editorDarkTheme, overallTheme } =
userSettings
const [original] = useState({
editorTheme,
overallTheme,
editorLightTheme,
editorDarkTheme,
})
const maskRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (maskRef.current) {
if (
editorTheme !== original.editorTheme ||
editorLightTheme !== original.editorLightTheme ||
editorDarkTheme !== original.editorDarkTheme ||
overallTheme !== original.overallTheme
) {
maskRef.current.style.opacity = '0'
}
}
}, [editorTheme, editorLightTheme, editorDarkTheme, overallTheme, original])
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
id="left-menu-mask"
ref={maskRef}
onClick={() => setLeftMenuShown(false)}
/>
)
})
@@ -1,64 +0,0 @@
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'
import SettingsMathPreview from './settings/settings-math-preview'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { ElementType } from 'react'
import OLForm from '@/shared/components/ol/ol-form'
import SettingsNewEditor from './settings/settings-new-editor'
const moduleSettings: Array<{
import: { default: ElementType }
path: string
}> = importOverleafModules('settingsEntries')
export default function SettingsMenu() {
const { t } = useTranslation()
const anonymous = getMeta('ol-anonymous')
if (anonymous) {
return null
}
return (
<>
<h4>{t('settings')}</h4>
<OLForm id="left-menu-setting" className="settings">
<SettingsCompiler />
<SettingsImageName />
<SettingsDocument />
<SettingsSpellCheckLanguage />
<SettingsDictionary />
{moduleSettings.map(({ import: { default: Component }, path }) => (
<Component key={path} />
))}
<SettingsAutoComplete />
<SettingsAutoCloseBrackets />
<SettingsSyntaxValidation />
<SettingsMathPreview />
<SettingsOverallTheme />
<SettingsEditorTheme />
<SettingsKeybindings />
<SettingsFontSize />
<SettingsFontFamily />
<SettingsLineHeight />
<SettingsPdfViewer />
<SettingsNewEditor />
</OLForm>
</>
)
}
@@ -1,28 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
export default function SettingsAutoCloseBrackets() {
const { t } = useTranslation()
const { autoPairDelimiters, setAutoPairDelimiters } =
useProjectSettingsContext()
return (
<SettingsMenuSelect
onChange={setAutoPairDelimiters}
value={autoPairDelimiters}
options={[
{
value: true,
label: t('on'),
},
{
value: false,
label: t('off'),
},
]}
label={t('auto_close_brackets')}
name="autoPairDelimiters"
/>
)
}
@@ -1,27 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
export default function SettingsAutoComplete() {
const { t } = useTranslation()
const { autoComplete, setAutoComplete } = useProjectSettingsContext()
return (
<SettingsMenuSelect
onChange={setAutoComplete}
value={autoComplete}
options={[
{
value: true,
label: t('on'),
},
{
value: false,
label: t('off'),
},
]}
label={t('auto_complete')}
name="autoComplete"
/>
)
}
@@ -1,45 +0,0 @@
import { useTranslation } from 'react-i18next'
import type { ProjectCompiler } from '../../../../../../types/project-settings'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
import { useSetCompilationSettingWithEvent } from '../../hooks/use-set-compilation-setting'
export default function SettingsCompiler() {
const { t } = useTranslation()
const { write } = usePermissionsContext()
const { compiler, setCompiler } = useProjectSettingsContext()
const changeCompiler = useSetCompilationSettingWithEvent(
'compiler',
setCompiler
)
return (
<SettingsMenuSelect<ProjectCompiler>
onChange={changeCompiler}
value={compiler}
disabled={!write}
options={[
{
value: 'pdflatex',
label: 'pdfLaTeX',
},
{
value: 'latex',
label: 'LaTeX',
},
{
value: 'xelatex',
label: 'XeLaTeX',
},
{
value: 'lualatex',
label: 'LuaLaTeX',
},
]}
label={t('compiler')}
name="compiler"
translateOptions="no"
/>
)
}
@@ -1,30 +0,0 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import DictionaryModal from '../../../dictionary/components/dictionary-modal'
import OLButton from '@/shared/components/ol/ol-button'
import OLFormGroup from '@/shared/components/ol/ol-form-group'
import OLFormLabel from '@/shared/components/ol/ol-form-label'
export default function SettingsDictionary() {
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
return (
<OLFormGroup className="left-menu-setting">
<OLFormLabel htmlFor="dictionary-settings">{t('dictionary')}</OLFormLabel>
<OLButton
id="dictionary-settings"
variant="secondary"
size="sm"
onClick={() => setShowModal(true)}
>
{t('edit')}
</OLButton>
<DictionaryModal
show={showModal}
handleHide={() => setShowModal(false)}
/>
</OLFormGroup>
)
}
@@ -1,55 +0,0 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { isValidTeXFile } from '../../../../main/is-valid-tex-file'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
import type { Option } from './settings-menu-select'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useSetCompilationSettingWithEvent } from '../../hooks/use-set-compilation-setting'
export default function SettingsDocument() {
const { t } = useTranslation()
const { write } = usePermissionsContext()
const { docs } = useFileTreeData()
const { rootDocId, setRootDocId } = useProjectSettingsContext()
const changeRootDoc = useSetCompilationSettingWithEvent(
'root-doc-id',
setRootDocId,
{ omitValueInEvent: true }
)
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.path,
}))
if (!rootDocId) {
mappedDocs.unshift({
value: '',
label: 'None',
disabled: true,
})
}
return mappedDocs
}, [docs, rootDocId])
return (
<SettingsMenuSelect
onChange={changeRootDoc}
value={rootDocId ?? ''}
disabled={!write}
options={validDocsOptions}
label={t('main_document')}
name="rootDocId"
translateOptions="no"
/>
)
}
@@ -1,53 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
import { useEditorThemesOptionGroups } from '../../hooks/use-editor-theme-option-groups'
import { isIEEEBranded } from '@/utils/is-ieee-branded'
export default function SettingsEditorTheme() {
const { t } = useTranslation()
const optGroups = useEditorThemesOptionGroups()
const {
editorTheme,
setEditorTheme,
editorLightTheme,
editorDarkTheme,
setEditorLightTheme,
setEditorDarkTheme,
overallTheme,
} = useProjectSettingsContext()
if (overallTheme === 'system' && !isIEEEBranded()) {
return (
<>
<SettingsMenuSelect
onChange={setEditorLightTheme}
value={editorLightTheme}
optgroups={optGroups}
label={t('editor_theme_light')}
name="editorLightTheme"
translateOptions="no"
/>
<SettingsMenuSelect
onChange={setEditorDarkTheme}
value={editorDarkTheme}
optgroups={optGroups}
label={t('editor_theme_dark')}
name="editorDarkTheme"
translateOptions="no"
/>
</>
)
}
return (
<SettingsMenuSelect
onChange={setEditorTheme}
value={editorTheme}
optgroups={optGroups}
label={t('editor_theme')}
name="editorTheme"
translateOptions="no"
/>
)
}
@@ -1,33 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
import { FontFamily } from '@/shared/utils/styles'
export default function SettingsFontFamily() {
const { t } = useTranslation()
const { fontFamily, setFontFamily } = useProjectSettingsContext()
return (
<SettingsMenuSelect<FontFamily>
onChange={setFontFamily}
value={fontFamily}
options={[
{
value: 'monaco',
label: 'Monaco / Menlo / Consolas',
},
{
value: 'lucida',
label: 'Lucida / Source Code Pro',
},
{
value: 'opendyslexicmono',
label: 'OpenDyslexic Mono',
},
]}
label={t('font_family')}
name="fontFamily"
translateOptions="no"
/>
)
}
@@ -1,25 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useProjectSettingsContext } from '../../context/project-settings-context'
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: Option<number>[] = sizes.map(size => ({
value: size,
label: `${size}px`,
}))
export default function SettingsFontSize() {
const { t } = useTranslation()
const { fontSize, setFontSize } = useProjectSettingsContext()
return (
<SettingsMenuSelect
onChange={setFontSize}
value={fontSize}
options={options}
label={t('font_size')}
name="fontSize"
/>
)
}
@@ -1,48 +0,0 @@
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'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import { useSetCompilationSettingWithEvent } from '../../hooks/use-set-compilation-setting'
export default function SettingsImageName() {
const { t } = useTranslation()
const { imageName, setImageName } = useProjectSettingsContext()
const { write } = usePermissionsContext()
const changeImageName = useSetCompilationSettingWithEvent(
'image-name',
setImageName
)
const allowedImageNames = useMemo(() => getMeta('ol-imageNames') || [], [])
const options: Array<Option> = useMemo(
() =>
allowedImageNames
// filter out images that aren't allowed, unless thats the current image the project is on
.filter(image => image.allowed || image.imageName === imageName)
.map(({ imageName, imageDesc }) => ({
value: imageName,
label: imageDesc,
})),
[allowedImageNames, imageName]
)
if (allowedImageNames.length === 0) {
return null
}
return (
<SettingsMenuSelect
onChange={changeImageName}
value={imageName}
disabled={!write}
options={options}
label={t('tex_live_version')}
name="imageName"
translateOptions="no"
/>
)
}
@@ -1,32 +0,0 @@
import { useTranslation } from 'react-i18next'
import type { Keybindings } from '../../../../../../types/user-settings'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
export default function SettingsKeybindings() {
const { t } = useTranslation()
const { mode, setMode } = useProjectSettingsContext()
return (
<SettingsMenuSelect<Keybindings>
onChange={setMode}
value={mode}
options={[
{
value: 'default',
label: 'None',
},
{
value: 'vim',
label: 'Vim',
},
{
value: 'emacs',
label: 'Emacs',
},
]}
label={t('keybindings')}
name="mode"
/>
)
}
@@ -1,32 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
import { LineHeight } from '@/shared/utils/styles'
export default function SettingsLineHeight() {
const { t } = useTranslation()
const { lineHeight, setLineHeight } = useProjectSettingsContext()
return (
<SettingsMenuSelect<LineHeight>
onChange={setLineHeight}
value={lineHeight}
options={[
{
value: 'compact',
label: t('compact'),
},
{
value: 'normal',
label: t('normal'),
},
{
value: 'wide',
label: t('wide'),
},
]}
label={t('line_height')}
name="lineHeight"
/>
)
}
@@ -1,27 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
export default function SettingsMathPreview() {
const { t } = useTranslation()
const { mathPreview, setMathPreview } = useProjectSettingsContext()
return (
<SettingsMenuSelect
onChange={setMathPreview}
value={mathPreview}
options={[
{
value: true,
label: t('on'),
},
{
value: false,
label: t('off'),
},
]}
label={t('equation_preview')}
name="mathPreview"
/>
)
}
@@ -1,121 +0,0 @@
import OLFormGroup from '@/shared/components/ol/ol-form-group'
import OLFormLabel from '@/shared/components/ol/ol-form-label'
import OLFormSelect from '@/shared/components/ol/ol-form-select'
import { ChangeEventHandler, useCallback, useEffect, useRef } from 'react'
import { useEditorLeftMenuContext } from '@/features/editor-left-menu/components/editor-left-menu-context'
import OLSpinner from '@/shared/components/ol/ol-spinner'
type PossibleValue = string | number | boolean
export type Option<T extends PossibleValue = string> = {
value: T
label: string
ariaHidden?: 'true' | 'false'
disabled?: boolean
}
export type Optgroup<T extends PossibleValue = string> = {
label: string
options: Array<Option<T>>
}
type SettingsMenuSelectProps<T extends PossibleValue = string> = {
label: string
name: string
options?: Array<Option<T>>
optgroups?: Array<Optgroup<T>>
loading?: boolean
onChange: (val: T) => void
value?: T
disabled?: boolean
translateOptions?: 'yes' | 'no'
}
export default function SettingsMenuSelect<T extends PossibleValue = string>({
label,
name,
options,
optgroups,
loading,
onChange,
value,
disabled = false,
translateOptions,
}: SettingsMenuSelectProps<T>) {
const handleChange: ChangeEventHandler<HTMLSelectElement> = useCallback(
event => {
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(onChangeValue as T)
},
[onChange, value]
)
const { settingToFocus } = useEditorLeftMenuContext()
const selectRef = useRef<HTMLSelectElement | null>(null)
useEffect(() => {
if (settingToFocus === name && selectRef.current) {
selectRef.current.scrollIntoView({
block: 'center',
behavior: 'smooth',
})
selectRef.current.focus()
}
// clear the focus setting
window.dispatchEvent(
new CustomEvent('ui.focus-setting', { detail: undefined })
)
}, [name, settingToFocus])
return (
<OLFormGroup
controlId={`settings-menu-${name}`}
className="left-menu-setting"
>
<OLFormLabel>{label}</OLFormLabel>
{loading ? (
<OLSpinner size="sm" />
) : (
<OLFormSelect
size="sm"
onChange={handleChange}
value={value?.toString()}
disabled={disabled}
ref={selectRef}
translate={translateOptions}
>
{options?.map(option => (
<option
key={`${name}-${option.value}`}
value={option.value.toString()}
aria-hidden={option.ariaHidden}
disabled={option.disabled}
>
{option.label}
</option>
))}
{optgroups?.map(optgroup => (
<optgroup label={optgroup.label} key={optgroup.label}>
{optgroup.options.map(option => (
<option
value={option.value.toString()}
key={option.value.toString()}
>
{option.label}
</option>
))}
</optgroup>
))}
</OLFormSelect>
)}
</OLFormGroup>
)
}
@@ -1,52 +0,0 @@
import { useTranslation } from 'react-i18next'
import SettingsMenuSelect from './settings-menu-select'
import { useSwitchEnableNewEditorState } from '@/features/ide-redesign/hooks/use-switch-enable-new-editor-state'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useCallback } from 'react'
import {
canUseNewEditor,
useIsNewEditorEnabled,
} from '@/features/ide-redesign/utils/new-editor-utils'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
export default function SettingsNewEditor() {
const { t } = useTranslation()
const { setEditorRedesignStatus } = useSwitchEnableNewEditorState()
const { setLeftMenuShown } = useLayoutContext()
const enabled = useIsNewEditorEnabled()
const show = canUseNewEditor()
const { sendEvent } = useEditorAnalytics()
const onChange = useCallback(
(newValue: boolean) => {
sendEvent('switch-to-new-editor', {
location: 'left-menu',
})
setEditorRedesignStatus(newValue).then(() => setLeftMenuShown(false))
},
[setEditorRedesignStatus, setLeftMenuShown, sendEvent]
)
if (!show) {
return null
}
return (
<SettingsMenuSelect
onChange={onChange}
value={enabled}
options={[
{
value: true,
label: t('on'),
},
{
value: false,
label: t('off'),
},
]}
label={t('new_editor_look')}
name="new-editor-setting"
/>
)
}
@@ -1,42 +0,0 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useLayoutContext } from '../../../../shared/context/layout-context'
import getMeta from '../../../../utils/meta'
import SettingsMenuSelect, { Option } from './settings-menu-select'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import type { OverallThemeMeta } from '../../../../../../types/project-settings'
import { isIEEEBranded } from '@/utils/is-ieee-branded'
import { OverallTheme } from '@/shared/utils/styles'
export default function SettingsOverallTheme() {
const { t } = useTranslation()
const overallThemes = getMeta('ol-overallThemes') as
| OverallThemeMeta[]
| undefined
const { loadingStyleSheet } = useLayoutContext()
const { overallTheme, setOverallTheme } = useProjectSettingsContext()
const options: Array<Option<OverallTheme>> = useMemo(
() =>
overallThemes?.map(({ name, val }) => ({
value: val,
label: name,
})) ?? [],
[overallThemes]
)
if (!overallThemes || isIEEEBranded()) {
return null
}
return (
<SettingsMenuSelect<OverallTheme>
onChange={setOverallTheme}
value={overallTheme}
options={options}
loading={loadingStyleSheet}
label={t('overall_theme')}
name="overallTheme"
/>
)
}
@@ -1,29 +0,0 @@
import { useTranslation } from 'react-i18next'
import type { PdfViewer } from '../../../../../../types/user-settings'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
export default function SettingsPdfViewer() {
const { t } = useTranslation()
const { pdfViewer, setPdfViewer } = useProjectSettingsContext()
return (
<SettingsMenuSelect<PdfViewer>
onChange={setPdfViewer}
value={pdfViewer}
options={[
{
value: 'pdfjs',
label: t('overleaf'),
},
{
value: 'native',
label: t('browser'),
},
]}
label={t('pdf_viewer')}
name="pdfViewer"
translateOptions="no"
/>
)
}
@@ -1,42 +0,0 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import getMeta from '../../../../utils/meta'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
import type { Optgroup } from './settings-menu-select'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { supportsWebAssembly } from '@/utils/wasm'
export default function SettingsSpellCheckLanguage() {
const { t } = useTranslation()
const { spellCheckLanguage, setSpellCheckLanguage } =
useProjectSettingsContext()
const { permissionsLevel } = useIdeReactContext()
const optgroup: Optgroup = useMemo(() => {
const options = (getMeta('ol-languages') ?? [])
// only include spell-check languages that are available in the client
.filter(language => language.dic !== undefined)
return {
label: 'Language',
options: options.map(language => ({
value: language.code,
label: language.name,
})),
}
}, [])
return (
<SettingsMenuSelect
onChange={setSpellCheckLanguage}
value={supportsWebAssembly() ? spellCheckLanguage : ''}
options={[{ value: '', label: t('off') }]}
optgroups={[optgroup]}
label={t('spell_check')}
name="spellCheckLanguage"
disabled={permissionsLevel === 'readOnly' || !supportsWebAssembly()}
/>
)
}
@@ -1,27 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useProjectSettingsContext } from '../../context/project-settings-context'
import SettingsMenuSelect from './settings-menu-select'
export default function SettingsSyntaxValidation() {
const { t } = useTranslation()
const { syntaxValidation, setSyntaxValidation } = useProjectSettingsContext()
return (
<SettingsMenuSelect<boolean>
onChange={setSyntaxValidation}
value={syntaxValidation}
options={[
{
value: true,
label: t('on'),
},
{
value: false,
label: t('off'),
},
]}
label={t('syntax_validation')}
name="syntaxValidation"
/>
)
}
@@ -1,42 +0,0 @@
import { ElementType } from 'react'
import { useTranslation } from 'react-i18next'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import getMeta from '../../../utils/meta'
const components = importOverleafModules('editorLeftMenuSync') as {
import: { default: ElementType }
path: string
}[]
export default function SyncMenu() {
const { t } = useTranslation()
const anonymous = getMeta('ol-anonymous')
const gitBridgeEnabled = getMeta('ol-gitBridgeEnabled')
if (anonymous) {
return null
}
if (components.length === 0) {
return null
}
// This flag can only be false in CE and Server Pro. In this case we skip rendering the
// entire sync section, since Dropbox and GitHub are never available in SP
if (!gitBridgeEnabled) {
return null
}
return (
<>
<h4>{t('sync')}</h4>
<ul className="list-unstyled nav">
{components.map(({ import: { default: Component }, path }) => (
<li key={path}>
<Component />
</li>
))}
</ul>
</>
)
}
@@ -1,6 +1,5 @@
import getMeta from '@/utils/meta'
import { useMemo } from 'react'
import { Option } from '../components/settings/settings-menu-select'
import { useTranslation } from 'react-i18next'
const overrides = new Map([['overleaf', 'overleaf light']])
@@ -8,6 +7,15 @@ function getThemeName(theme: string): string {
return (overrides.get(theme) ?? theme).replace(/_/g, ' ')
}
type PossibleValue = string | number | boolean
type Option<T extends PossibleValue = string> = {
value: T
label: string
ariaHidden?: 'true' | 'false'
disabled?: boolean
}
export function useEditorThemesOptionGroups() {
const editorThemes = getMeta('ol-editorThemes')
const legacyEditorThemes = getMeta('ol-legacyEditorThemes')
@@ -1,44 +0,0 @@
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import { useTranslation } from 'react-i18next'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import LeftMenuButton from '@/features/editor-left-menu/components/left-menu-button'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { memo } from 'react'
export const WordCountButton = memo<{
handleShowModal: () => void
}>(function WordCountButton({ handleShowModal }) {
const { pdfUrl } = useCompileContext()
const { t } = useTranslation()
const enabled = pdfUrl || isSplitTestEnabled('word-count-client')
if (!enabled) {
return (
<OLTooltip
id="disabled-word-count"
description={t('please_compile_pdf_before_word_count')}
overlayProps={{
placement: 'top',
}}
>
{/* OverlayTrigger won't fire unless the child is a non-react html element (e.g div, span) */}
<div>
<LeftMenuButton
icon="match_case"
disabled
disabledAccesibilityText={t('please_compile_pdf_before_word_count')}
>
{t('word_count')}
</LeftMenuButton>
</div>
</OLTooltip>
)
}
return (
<LeftMenuButton onClick={handleShowModal} icon="match_case">
{t('word_count')}
</LeftMenuButton>
)
})
@@ -1,57 +0,0 @@
import ActionsMenu from '../../js/features/editor-left-menu/components/actions-menu'
import { ScopeDecorator } from '../decorators/scope'
import { mockCompile, mockCompileError } from '../fixtures/compile'
import { document, mockDocument } from '../fixtures/document'
import useFetchMock from '../hooks/use-fetch-mock'
import { useScope } from '../hooks/use-scope'
export default {
title: 'Editor / Left Menu / Actions Menu',
component: ActionsMenu,
decorators: [
(Story: any) => ScopeDecorator(Story, { mockCompileOnLoad: false }),
],
}
export const NotCompiled = () => {
window.metaAttributesCache.set('ol-anonymous', false)
useFetchMock(fetchMock => {
mockCompileError(fetchMock, 'failure')
})
return (
<div id="left-menu" className="shown">
<ActionsMenu />
</div>
)
}
export const CompileSuccess = () => {
window.metaAttributesCache.set('ol-anonymous', false)
useScope({
editor: {
sharejs_doc: mockDocument(document.tex),
},
})
useFetchMock(fetchMock => {
mockCompile(fetchMock)
fetchMock.get('express:/project/:projectId/wordcount', {
texcount: {
encode: 'ascii',
textWords: 10,
headers: 11,
mathInline: 12,
mathDisplay: 13,
},
})
})
return (
<div id="left-menu" className="shown">
<ActionsMenu />
</div>
)
}
@@ -1,44 +0,0 @@
import DownloadMenu from '../../js/features/editor-left-menu/components/download-menu'
import { ScopeDecorator } from '../decorators/scope'
import { mockCompile, mockCompileError } from '../fixtures/compile'
import { document, mockDocument } from '../fixtures/document'
import useFetchMock from '../hooks/use-fetch-mock'
import { useScope } from '../hooks/use-scope'
export default {
title: 'Editor / Left Menu / Download Menu',
component: DownloadMenu,
decorators: [
(Story: any) => ScopeDecorator(Story, { mockCompileOnLoad: false }),
],
}
export const NotCompiled = () => {
useFetchMock(fetchMock => {
mockCompileError(fetchMock, 'failure')
})
return (
<div id="left-menu" className="shown">
<DownloadMenu />
</div>
)
}
export const CompileSuccess = () => {
useScope({
editor: {
sharejs_doc: mockDocument(document.tex),
},
})
useFetchMock(fetchMock => {
mockCompile(fetchMock)
})
return (
<div id="left-menu" className="shown">
<DownloadMenu />
</div>
)
}
@@ -1,33 +0,0 @@
import HelpMenu from '../../js/features/editor-left-menu/components/help-menu'
import { ScopeDecorator } from '../decorators/scope'
export default {
title: 'Editor / Left Menu / Help Menu',
component: HelpMenu,
decorators: [ScopeDecorator],
}
export const ShowSupport = () => {
window.metaAttributesCache.set('ol-showSupport', true)
window.metaAttributesCache.set('ol-user', {
email: 'sherlock@holmes.co.uk',
first_name: 'Sherlock',
last_name: 'Holmes',
})
return (
<div id="left-menu" className="shown">
<HelpMenu />
</div>
)
}
export const HideSupport = () => {
window.metaAttributesCache.set('ol-showSupport', false)
return (
<div id="left-menu" className="shown">
<HelpMenu />
</div>
)
}
@@ -1,37 +0,0 @@
import SyncMenu from '../../js/features/editor-left-menu/components/sync-menu'
import { ScopeDecorator } from '../decorators/scope'
import { useScope } from '../hooks/use-scope'
export default {
title: 'Editor / Left Menu / Sync Menu',
component: SyncMenu,
decorators: [ScopeDecorator],
}
export const WriteAccess = () => {
window.metaAttributesCache.set('ol-anonymous', false)
window.metaAttributesCache.set('ol-gitBridgeEnabled', true)
useScope({
permissionsLevel: 'owner',
})
return (
<div id="left-menu" className="shown">
<SyncMenu />
</div>
)
}
export const ReadOnlyAccess = () => {
window.metaAttributesCache.set('ol-anonymous', false)
window.metaAttributesCache.set('ol-gitBridgeEnabled', true)
useScope({
permissionsLevel: 'readOnly',
})
return (
<div id="left-menu" className="shown">
<SyncMenu />
</div>
)
}
@@ -6,175 +6,6 @@
--left-menu-form-select-border: var(--border-disabled);
}
.left-menu {
position: absolute;
top: 0;
bottom: 0;
background-color: var(--bg-light-secondary);
z-index: 100;
overflow: hidden auto;
transition: left ease-in-out 0.5s;
font-size: var(--font-size-02);
width: 340px;
&.shown {
left: 0;
}
h4 {
font-family: $font-family-sans-serif;
font-weight: 400;
font-size: var(--font-size-03);
margin: var(--spacing-05) 0;
padding-bottom: var(--spacing-03);
color: var(--content-secondary);
border-bottom: 1px solid var(--border-primary-dark);
&:first-child {
margin-top: 0;
}
}
ul.nav {
.left-menu-button {
cursor: pointer;
padding: var(--spacing-03);
font-weight: 700;
color: var(--link-ui);
display: flex;
align-items: center;
width: 100%;
background-color: inherit;
border: none;
text-decoration: none;
.material-symbols {
margin-right: var(--spacing-04);
color: var(--neutral-70);
}
&:hover,
&:active {
background-color: var(--bg-info-01);
color: var(--white);
.material-symbols {
color: var(--white);
}
}
}
a {
cursor: pointer;
&:hover,
&:active,
&:focus {
background-color: var(--bg-info-01);
color: var(--white);
.material-symbols {
color: var(--white);
}
}
.material-symbols {
color: var(--neutral-70);
}
padding: var(--spacing-03);
font-weight: 700;
}
.link-disabled {
color: var(--content-disabled);
}
}
> ul.nav:last-child {
margin-bottom: var(--spacing-05);
}
ul.nav-downloads {
li {
display: inline-block;
text-align: center;
width: 100px;
a {
color: var(--content-secondary);
text-decoration: none;
}
.material-symbols {
margin: var(--spacing-03) 0;
}
}
}
form.settings {
label {
font-weight: normal;
color: var(--content-secondary);
flex: 1 0 50%;
margin-bottom: 0;
padding-right: var(--spacing-03);
white-space: nowrap;
}
button,
select {
width: 50%;
margin: var(--spacing-04) 0;
}
}
.left-menu-setting {
padding: 0 var(--spacing-02);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
border-bottom: 1px solid rgb(0 0 0 / 7%);
margin-bottom: 0;
height: 43px;
&:first-child {
margin-top: calc(var(--spacing-04) * -1);
}
&:last-child {
border-bottom: 0;
}
&:hover,
&:focus-within {
background-color: var(--bg-info-01);
label {
color: var(--white);
}
}
.form-select {
border: 1px solid var(--left-menu-form-select-border);
}
}
}
#left-menu-mask {
opacity: 0.4;
background-color: #999;
z-index: 99;
transition: opacity 0.5s;
position: absolute;
inset: 0;
}
.left-menu-modal-backdrop {
background-color: transparent;
}
.loading-spinner-container {
display: flex;
align-items: center;
-7
View File
@@ -830,8 +830,6 @@
"folder_name": "Folder name",
"folders": "Folders",
"following_paths_conflict": "The following files and folders conflict with the same path",
"font_family": "Font Family",
"font_size": "Font Size",
"footer_about_us": "About us",
"footer_contact_us": "Contact us",
"footnotes": "Footnotes",
@@ -1298,7 +1296,6 @@
"limited_document_history": "Limited document history",
"limited_to_n_collaborators_per_project": "Limited to __count__ collaborator per project",
"limited_to_n_collaborators_per_project_plural": "Limited to __count__ collaborators per project",
"line_height": "Line Height",
"line_width_is_the_width_of_the_line_in_the_current_environment": "Line width is the width of the line in the current environment. e.g. a full page width in single-column layout or half a page width in a two-column layout.",
"link": "Link",
"link_account": "Link Account",
@@ -1487,7 +1484,6 @@
"new_compile_domain_notice": "Something might be blocking your browser from accessing Overleafs PDF download location, <0>__compilesUserContentDomain__</0>. This could be caused by network blocking or a strict browser plugin rule. Please follow our <1>troubleshooting guide</1>.",
"new_compiles_in_this_project_will_automatically_use_the_newest_version": "New compiles in this project will automatically use the newest version. <0>Learn how to change compiler settings</0>",
"new_create_tables_and_equations": "NEW! <sparkle/> Create tables and equations in seconds",
"new_editor_look": "New editor look",
"new_error_logs_make_it_easier_to_find_whats_wrong": "New error logs make it easier to find whats wrong and fix your document, so you can get compiling again.",
"new_file": "New file",
"new_folder": "New folder",
@@ -1727,7 +1723,6 @@
"please_ask_the_project_owner_to_upgrade_to_track_changes": "Please ask the project owner to upgrade to use track changes",
"please_change_primary_to_remove": "Please change your primary email in order to remove",
"please_compile_pdf_before_download": "Please compile your project before downloading the PDF",
"please_compile_pdf_before_word_count": "Please compile your project before performing a word count",
"please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ",
"please_confirm_primary_email_or_edit": "Please confirm your primary email address __emailAddress__. To edit it, go to <0>Account settings</0>.",
"please_confirm_secondary_email_or_edit": "Please confirm your secondary email address __emailAddress__. To edit it, go to <0>Account settings</0>.",
@@ -2152,7 +2147,6 @@
"show_document_preamble": "Show document preamble",
"show_equation_preview": "Show equation preview",
"show_file_tree": "Show file tree",
"show_hotkeys": "Show Hotkeys",
"show_in_code": "Show in code",
"show_in_pdf": "Show in PDF",
"show_less": "show less",
@@ -2212,7 +2206,6 @@
"source": "Source",
"speak": "Speak",
"speech_input_not_available": "Speech input is not yet available in this browser",
"spell_check": "Spell check",
"spellcheck": "Spellcheck",
"spellcheck_language": "Spellcheck language",
"split_view": "Split view",
@@ -1,900 +0,0 @@
import EditorLeftMenu from '../../../../frontend/js/features/editor-left-menu/components/editor-left-menu'
import {
ImageName,
OverallThemeMeta,
SpellCheckLanguage,
} from '../../../../types/project-settings'
import {
EditorProviders,
makeProjectProvider,
} from '../../helpers/editor-providers'
import { mockScope } from './scope'
import { Folder } from '../../../../types/folder'
import { docsInFolder } from '@/features/file-tree/util/docs-in-folder'
import getMeta from '@/utils/meta'
import { mockProject } from '../../features/source-editor/helpers/mock-project'
import { UserId } from '../../../../types/user'
describe('<EditorLeftMenu />', function () {
beforeEach(function () {
cy.viewport(800, 800)
cy.interceptCompile()
})
describe('for non-anonymous users', function () {
const overallThemes: OverallThemeMeta[] = [
{
name: 'Overall Theme 1',
val: '',
path: 'https://overleaf.com/overalltheme-1.css',
},
{
name: 'Overall Theme 2',
val: 'light-',
path: 'https://overleaf.com/overalltheme-2.css',
},
]
const ImageNames: ImageName[] = [
{
imageDesc: 'Image 1',
imageName: 'img-1',
allowed: true,
},
{
imageDesc: 'Image 2',
imageName: 'img-2',
allowed: true,
},
]
beforeEach(function () {
window.metaAttributesCache.set('ol-overallThemes', overallThemes)
window.metaAttributesCache.set('ol-imageNames', ImageNames)
window.metaAttributesCache.set('ol-anonymous', false)
window.metaAttributesCache.set('ol-gitBridgeEnabled', true)
window.metaAttributesCache.set('ol-showSupport', true)
Object.assign(getMeta('ol-ExposedSettings'), { ieeeBrandId: 123 })
window.metaAttributesCache.set('ol-user', {
email: 'sherlock@holmes.co.uk',
first_name: 'Sherlock',
last_name: 'Holmes',
})
})
it('render full menu', function () {
const scope = mockScope()
const project = mockProject()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
providers={{ ProjectProvider: makeProjectProvider(project) }}
>
<EditorLeftMenu />
</EditorProviders>
)
// Download Menu
cy.findByRole('heading', { name: 'Download' })
cy.findByRole('link', { name: 'Source' })
cy.findByRole('link', { name: 'PDF' })
// Actions Menu
cy.findByRole('heading', { name: 'Actions' })
cy.findByRole('button', { name: 'Copy project' })
cy.findByRole('button', { name: 'Word Count' })
// Sync Menu
cy.findByRole('heading', { name: 'Sync' })
cy.findByRole('button', { name: 'Dropbox' })
cy.findByRole('button', { name: 'Git' })
cy.findByRole('button', { name: 'GitHub' })
// Settings Menu
cy.findByRole('heading', { name: 'Settings' })
cy.findByLabelText('Compiler')
cy.findByLabelText('TeX Live version')
cy.findByLabelText('Main document')
cy.findByLabelText('Spell check')
cy.findByLabelText('Auto-complete')
cy.findByLabelText('Auto-close brackets')
cy.findByLabelText('Code check')
cy.findByLabelText('Editor theme')
cy.findByLabelText('Overall theme')
cy.findByLabelText('Keybindings')
cy.findByLabelText('Font Size')
cy.findByLabelText('Font Family')
cy.findByLabelText('Line Height')
cy.findByLabelText('PDF Viewer')
// Help Menu
cy.findByRole('heading', { name: 'Help' })
cy.findByRole('button', { name: 'Show Hotkeys' })
cy.findByRole('link', { name: 'Documentation' })
cy.findByRole('button', { name: 'Contact us' })
})
describe('download menu', function () {
it('have a correct source & pdf download url', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.findByRole('link', { name: 'Source' }).should(
'have.attr',
'href',
'/project/project123/download/zip'
)
cy.findByRole('link', { name: 'PDF' })
.should('have.attr', 'href')
.and('match', /\/download\/project\/project123\/build/)
})
})
describe('actions menu', function () {
it('shows copy project modal correctly', function () {
cy.intercept('POST', '/project/*/clone', {
body: {
project_id: 'new_project_id',
},
})
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.findByRole('button', { name: 'Copy project' }).click()
cy.findByRole('heading', { name: 'Copy project' })
// try closing & re-opening the modal with different methods
cy.findByRole('button', { name: 'Close dialog' }).click()
cy.findByRole('button', { name: 'Copy project' }).click()
cy.findByRole('button', { name: 'Cancel' }).click()
cy.findByRole('button', { name: 'Copy project' }).click()
cy.findByLabelText(/New name/i).focus()
cy.findByLabelText(/New name/i).clear()
cy.findByLabelText(/New name/i).type('Project Renamed')
cy.get('#clone-project-form-name[value="Project Renamed"')
})
it('shows word count modal correctly', function () {
cy.intercept('GET', '/project/*/wordcount*', {
texcount: {
encode: 'ascii',
textWords: 781,
headWords: 66,
outside: 11,
headers: 41,
elements: 2,
mathInline: 6,
mathDisplay: 1,
errors: 0,
},
}).as('wordCount')
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.findByRole('button', { name: 'Word Count' }).click()
cy.wait('@wordCount')
cy.findByText('Total Words:')
cy.findByText('781')
cy.findByText('Headers:')
cy.findByText('41')
cy.findByText('Math Inline:')
cy.findByText('6')
cy.findByText('Math Display:')
cy.findByText('1')
})
})
describe('sync menu', function () {
it('shows dropbox modal correctly', function () {
cy.intercept('GET', '/dropbox/status', {
registered: true,
})
const scope = mockScope({
user: {
features: {
dropbox: false,
},
},
})
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
projectOwner={{
_id: '123' as UserId,
email: 'owner@example.com',
first_name: 'Test',
last_name: 'Owner',
privileges: 'owner',
signUpDate: new Date('2025-07-07').toISOString(),
}}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.findByRole('button', { name: 'Dropbox' }).click()
cy.findByText('Dropbox Sync')
})
it('shows git modal correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
projectOwner={{
_id: '123' as UserId,
email: 'owner@example.com',
first_name: 'Test',
last_name: 'Owner',
privileges: 'owner',
signUpDate: new Date('2025-07-07').toISOString(),
}}
projectFeatures={
{
gitBridge: true,
} as any
}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.findByRole('button', { name: 'Git' }).click()
cy.findByText('Clone with Git')
cy.findByText(/clone your project by using the link below/)
})
it('shows git modal paywall correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
projectOwner={{
_id: '123' as UserId,
email: 'owner@example.com',
first_name: 'Test',
last_name: 'Owner',
privileges: 'owner',
signUpDate: new Date('2025-07-07').toISOString(),
}}
projectFeatures={
{
gitBridge: false,
} as any
}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.findByRole('button', { name: 'Git' }).click()
cy.findByText('Collaborate online and offline, using your own workflow')
})
it('shows github modal correctly', function () {
cy.intercept('GET', '/user/github-sync/status', {
available: false,
enabled: false,
}).as('user-status')
cy.intercept('GET', '/project/*/github-sync/status', {
enabled: false,
}).as('project-status')
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.wait('@compile')
cy.findByRole('button', { name: 'GitHub' }).click()
cy.findByText('GitHub Sync')
cy.wait(['@user-status', '@project-status'])
cy.findByText('Push to GitHub, pull to Overleaf')
})
it('hides the entire sync section when git bridge is disabled', function () {
window.metaAttributesCache.set('ol-gitBridgeEnabled', false)
cy.findByRole('button', { name: 'Dropbox' }).should('not.exist')
cy.findByRole('button', { name: 'Git' }).should('not.exist')
cy.findByRole('button', { name: 'GitHub' }).should('not.exist')
})
})
describe('settings menu', function () {
it('shows compiler menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-compiler option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq([
'pdflatex',
'latex',
'xelatex',
'lualatex',
])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq([
'pdfLaTeX',
'LaTeX',
'XeLaTeX',
'LuaLaTeX',
])
}
)
})
it('shows texlive version menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-imageName option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['img-1', 'img-2'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(['Image 1', 'Image 2'])
}
)
})
it('shows document menu correctly', function () {
const rootFolder: Folder = {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{
_id: 'id1',
name: 'main.tex',
},
{
_id: 'id2',
name: 'main2.tex',
},
],
fileRefs: [],
folders: [],
}
const scope = mockScope()
cy.mount(
<EditorProviders
layoutContext={{ leftMenuShown: true }}
scope={scope}
rootFolder={[rootFolder as any]}
>
<EditorLeftMenu />
</EditorProviders>
)
const docs = docsInFolder(rootFolder)
cy.get<HTMLOptionElement>('#settings-menu-rootDocId option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(docs.map(doc => doc.doc.id))
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(docs.map(doc => doc.path))
}
)
})
it('shows spellcheck menu correctly', function () {
const languages: SpellCheckLanguage[] = [
{
name: 'Lang 1',
code: 'lang-1',
dic: 'lang_1',
},
{
name: 'Lang 2',
code: 'lang-2',
dic: 'lang_2',
},
]
window.metaAttributesCache.set('ol-languages', languages)
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>(
'#settings-menu-spellCheckLanguage option'
).then(options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['', 'lang-1', 'lang-2'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(['Off', 'Lang 1', 'Lang 2'])
})
})
it('shows dictionary modal correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get('label[for="dictionary-settings"] ~ button').click()
cy.findByText('Edit Dictionary')
cy.findByText('Your custom dictionary is empty.')
})
it('shows auto-complete menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-autoComplete option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['true', 'false'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(['On', 'Off'])
}
)
})
it('shows auto-close brackets menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>(
'#settings-menu-autoPairDelimiters option'
).then(options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['true', 'false'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(['On', 'Off'])
})
})
it('shows code check menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>(
'#settings-menu-syntaxValidation option'
).then(options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['true', 'false'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(['On', 'Off'])
})
})
it('shows editor theme menu correctly', function () {
const editorThemes = [
{ name: 'editortheme-1', dark: false },
{ name: 'editortheme-2', dark: false },
{ name: 'editortheme-3', dark: false },
]
const legacyEditorThemes = [
{ name: 'legacytheme-1', dark: false },
{ name: 'legacytheme-2', dark: false },
{ name: 'legacytheme-3', dark: false },
]
window.metaAttributesCache.set('ol-editorThemes', editorThemes)
window.metaAttributesCache.set(
'ol-legacyEditorThemes',
legacyEditorThemes
)
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-editorTheme option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq([
'editortheme-1',
'editortheme-2',
'editortheme-3',
'legacytheme-1',
'legacytheme-2',
'legacytheme-3',
])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq([
'editortheme-1',
'editortheme-2',
'editortheme-3',
'legacytheme-1 (Legacy)',
'legacytheme-2 (Legacy)',
'legacytheme-3 (Legacy)',
])
}
)
})
it('shows overall theme menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-overallTheme option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['', 'light-'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(['Overall Theme 1', 'Overall Theme 2'])
}
)
})
it('shows keybindings menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-mode option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['default', 'vim', 'emacs'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(['None', 'Vim', 'Emacs'])
}
)
})
it('shows font size menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-fontSize option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq([
'10',
'11',
'12',
'13',
'14',
'16',
'18',
'20',
'22',
'24',
])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq([
'10px',
'11px',
'12px',
'13px',
'14px',
'16px',
'18px',
'20px',
'22px',
'24px',
])
}
)
})
it('shows font family menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-fontFamily option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['monaco', 'lucida', 'opendyslexicmono'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq([
'Monaco / Menlo / Consolas',
'Lucida / Source Code Pro',
'OpenDyslexic Mono',
])
}
)
})
it('shows line height menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-lineHeight option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['compact', 'normal', 'wide'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(['Compact', 'Normal', 'Wide'])
}
)
})
it('shows pdf viewer menu correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.get<HTMLOptionElement>('#settings-menu-pdfViewer option').then(
options => {
const values = [...options].map(o => o.value)
expect(values).to.deep.eq(['pdfjs', 'native'])
const texts = [...options].map(o => o.text)
expect(texts).to.deep.eq(['Overleaf', 'Browser'])
}
)
})
})
describe('help menu', function () {
it('shows hotkeys modal correctly', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.findByRole('button', { name: 'Show Hotkeys' }).click()
cy.findByText('Hotkeys')
})
it('shows correct url for documentation', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.findByRole('link', { name: 'Documentation' }).should(
'have.attr',
'href',
'/learn'
)
})
it('shows correct contact us modal', function () {
const scope = mockScope()
cy.mount(
<EditorProviders
scope={scope}
layoutContext={{ leftMenuShown: true }}
>
<EditorLeftMenu />
</EditorProviders>
)
cy.findByRole('button', { name: 'Contact us' }).click()
cy.findByText('Affected project URL (Optional)')
})
})
})
describe('for anonymous users', function () {
it('render minimal menu', function () {
const scope = mockScope()
window.metaAttributesCache.set('ol-anonymous', true)
Object.assign(getMeta('ol-ExposedSettings'), { ieeeBrandId: 123 })
cy.mount(
<EditorProviders scope={scope} layoutContext={{ leftMenuShown: true }}>
<EditorLeftMenu />
</EditorProviders>
)
// Download Menu
cy.findByRole('heading', { name: 'Download' })
cy.findByRole('link', { name: 'Source' })
cy.findByRole('link', { name: 'PDF' })
// Actions Menu
cy.findByRole('heading', { name: 'Actions' }).should('not.exist')
cy.findByRole('button', { name: 'Copy project' }).should('not.exist')
cy.findByRole('button', { name: 'Word Count' }).should('not.exist')
// Sync Menu
cy.findByRole('heading', { name: 'Sync' }).should('not.exist')
cy.findByRole('button', { name: 'Dropbox' }).should('not.exist')
cy.findByRole('button', { name: 'Git' }).should('not.exist')
cy.findByRole('button', { name: 'GitHub' }).should('not.exist')
// Settings Menu
cy.findByRole('heading', { name: 'Settings' }).should('not.exist')
cy.findByLabelText('Compiler').should('not.exist')
cy.findByLabelText('TeX Live version').should('not.exist')
cy.findByLabelText('Main document').should('not.exist')
cy.findByLabelText('Spell check').should('not.exist')
cy.findByLabelText('Auto-complete').should('not.exist')
cy.findByLabelText('Auto-close brackets').should('not.exist')
cy.findByLabelText('Code check').should('not.exist')
cy.findByLabelText('Editor theme').should('not.exist')
cy.findByLabelText('Overall theme').should('not.exist')
cy.findByLabelText('Keybindings').should('not.exist')
cy.findByLabelText('Font Size').should('not.exist')
cy.findByLabelText('Font Family').should('not.exist')
cy.findByLabelText('Line Height').should('not.exist')
cy.findByLabelText('PDF Viewer').should('not.exist')
// Help Menu
cy.findByRole('heading', { name: 'Help' })
cy.findByRole('button', { name: 'Show Hotkeys' })
cy.findByRole('button', { name: 'Documentation' }).should('not.exist')
cy.findByRole('link', { name: 'Contact us' }).should('not.exist')
})
})
})
@@ -1,49 +0,0 @@
import { MainDocument } from '../../../../types/project-settings'
import { PdfViewer } from '../../../../types/user-settings'
type Scope = {
settings?: {
syntaxValidation?: boolean
pdfViewer?: PdfViewer
}
editor?: {
sharejs_doc?: {
doc_id?: string
getSnapshot?: () => string
}
}
ui?: {
view?: 'editor' | 'history' | 'file' | 'pdf'
pdfLayout?: 'flat' | 'sideBySide' | 'split'
leftMenuShown?: boolean
}
project?: {
members?: any[]
owner: {
_id: string
}
features?: {
gitBridge?: boolean
}
}
user?: {
features?: {
dropbox: boolean
}
}
docs?: MainDocument[]
}
export const mockScope = (scope?: Scope) => ({
settings: {
syntaxValidation: false,
pdfViewer: 'pdfjs',
},
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
...scope,
})
@@ -1,56 +0,0 @@
import { fireEvent, screen, waitFor } from '@testing-library/react'
import fetchMock from 'fetch-mock'
import sinon from 'sinon'
import { expect } from 'chai'
import ActionsCopyProject from '../../../../../frontend/js/features/editor-left-menu/components/actions-copy-project'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import { location } from '@/shared/components/location'
describe('<ActionsCopyProject />', function () {
beforeEach(function () {
this.locationWrapperSandbox = sinon.createSandbox()
this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
window.metaAttributesCache.set('ol-preventCompileOnLoad', true)
})
afterEach(function () {
this.locationWrapperSandbox.restore()
fetchMock.removeRoutes().clearHistory()
})
it('shows correct modal when clicked', async function () {
renderWithEditorContext(<ActionsCopyProject />)
fireEvent.click(screen.getByRole('button', { name: 'Copy project' }))
screen.getByLabelText(/New name/i)
})
it('loads the project page when submitted', async function () {
fetchMock.post('express:/project/:id/clone', {
status: 200,
body: {
project_id: 'new-project',
},
})
renderWithEditorContext(<ActionsCopyProject />)
fireEvent.click(screen.getByRole('button', { name: 'Copy project' }))
const input = screen.getByLabelText(/New name/i)
fireEvent.change(input, { target: { value: 'New project' } })
const button = screen.getByRole('button', { name: 'Copy' })
button.click()
await waitFor(() => {
expect(button.textContent).to.equal('Copying…')
})
const assignStub = this.locationWrapperStub.assign
await waitFor(() => {
expect(assignStub).to.have.been.calledOnceWith('/project/new-project')
})
})
})
@@ -1,86 +0,0 @@
import { screen, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import ActionsMenu from '../../../../../frontend/js/features/editor-left-menu/components/actions-menu'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
describe('<ActionsMenu />', function () {
beforeEach(function () {
fetchMock.post('express:/project/:projectId/compile', {
status: 'success',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: [
{
path: 'output.pdf',
build: 'build-123',
url: '/build/build-123/output.pdf',
type: 'pdf',
},
],
})
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu for non-anonymous users', async function () {
window.metaAttributesCache.set('ol-anonymous', false)
renderWithEditorContext(<ActionsMenu />, {
projectId: '123abc',
mockCompileOnLoad: true,
scope: {
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
},
})
screen.getByText('Actions')
screen.getByRole('button', {
name: 'Copy project',
})
await waitFor(() => {
screen.getByRole('button', {
name: 'Word Count',
})
})
})
it('does not show anything for anonymous users', async function () {
window.metaAttributesCache.set('ol-anonymous', true)
renderWithEditorContext(<ActionsMenu />, {
projectId: '123abc',
mockCompileOnLoad: true,
scope: {
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
},
})
expect(screen.queryByText('Actions')).to.equal(null)
expect(
screen.queryByRole('button', {
name: 'Copy project',
})
).to.equal(null)
await waitFor(() => {
expect(
screen.queryByRole('button', {
name: 'Word Count',
})
).to.equal(null)
})
})
})
@@ -1,66 +0,0 @@
import { screen, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import ActionsWordCount from '../../../../../frontend/js/features/editor-left-menu/components/actions-word-count'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
describe('<ActionsWordCount />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct modal when clicked after document is compiled', async function () {
const compileEndpoint = 'express:/project/:projectId/compile'
const wordcountEndpoint = 'express:/project/:projectId/wordcount'
fetchMock.post(compileEndpoint, {
status: 'success',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: [
{
path: 'output.pdf',
build: 'build-123',
url: '/build/build-123/output.pdf',
type: 'pdf',
},
],
})
fetchMock.get(wordcountEndpoint, {
texcount: {
encode: 'ascii',
textWords: 0,
headers: 0,
mathInline: 0,
mathDisplay: 0,
},
})
renderWithEditorContext(<ActionsWordCount />, {
projectId: '123abc',
mockCompileOnLoad: true,
scope: {
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
},
})
// when loading, we don't render the "Word Count" as button yet
expect(screen.queryByRole('button', { name: 'Word Count' })).to.equal(null)
await waitFor(
() => expect(fetchMock.callHistory.called(compileEndpoint)).to.be.true
)
const button = await screen.findByRole('button', { name: 'Word Count' })
button.click()
await waitFor(
() => expect(fetchMock.callHistory.called(wordcountEndpoint)).to.be.true
)
})
})
@@ -1,59 +0,0 @@
import { screen, waitFor } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import DownloadMenu from '../../../../../frontend/js/features/editor-left-menu/components/download-menu'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
describe('<DownloadMenu />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows download links with correct url', async function () {
fetchMock.post('express:/project/:projectId/compile', {
clsiServerId: 'foo',
compileGroup: 'priority',
status: 'success',
pdfDownloadDomain: 'https://clsi.test-overleaf.com',
outputFiles: [
{
path: 'output.pdf',
build: 'build-123',
url: '/build/build-123/output.pdf',
type: 'pdf',
},
],
})
renderWithEditorContext(<DownloadMenu />, {
projectId: '123abc',
mockCompileOnLoad: true,
scope: {
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
},
})
const sourceLink = screen.getByRole('link', {
name: 'Source',
})
expect(sourceLink.getAttribute('href')).to.equal(
'/project/123abc/download/zip'
)
await waitFor(() => {
const pdfLink = screen.getByRole('link', {
name: 'PDF',
})
expect(pdfLink.getAttribute('href')).to.equal(
'/download/project/123abc/build/build-123/output/output.pdf?compileGroup=priority&clsiserverid=foo&popupDownload=true'
)
})
})
})
@@ -1,29 +0,0 @@
import { expect } from 'chai'
import { screen, fireEvent, within } from '@testing-library/react'
import HelpContactUs from '../../../../../frontend/js/features/editor-left-menu/components/help-contact-us'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import fetchMock from 'fetch-mock'
describe('<HelpContactUs />', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-user', {
email: 'sherlock@holmes.co.uk',
first_name: 'Sherlock',
last_name: 'Holmes',
})
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('open contact us modal when clicked', function () {
renderWithEditorContext(<HelpContactUs />)
expect(screen.queryByRole('dialog')).to.equal(null)
fireEvent.click(screen.getByRole('button', { name: 'Contact us' }))
const modal = screen.getAllByRole('dialog')[0]
within(modal).getAllByText('Get in touch')
within(modal).getByText('Subject')
})
})
@@ -1,12 +0,0 @@
import { expect } from 'chai'
import { screen, render } from '@testing-library/react'
import HelpDocumentation from '../../../../../frontend/js/features/editor-left-menu/components/help-documentation'
describe('<HelpDocumentation />', function () {
it('has correct href attribute', function () {
render(<HelpDocumentation />)
const link = screen.getByRole('link', { name: 'Documentation' })
expect(link.getAttribute('href')).to.equal('/learn')
})
})
@@ -1,39 +0,0 @@
import { screen } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import HelpMenu from '../../../../../frontend/js/features/editor-left-menu/components/help-menu'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
describe('<HelpMenu />', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-user', {
email: 'sherlock@holmes.co.uk',
first_name: 'Sherlock',
last_name: 'Holmes',
})
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu if `showSupport` is `true`', function () {
window.metaAttributesCache.set('ol-showSupport', true)
renderWithEditorContext(<HelpMenu />)
screen.getByRole('button', { name: 'Show Hotkeys' })
screen.getByRole('button', { name: 'Contact us' })
screen.getByRole('link', { name: 'Documentation' })
})
it('shows correct menu if `showSupport` is `false`', function () {
window.metaAttributesCache.set('ol-showSupport', false)
renderWithEditorContext(<HelpMenu />)
screen.getByRole('button', { name: 'Show Hotkeys' })
expect(screen.queryByRole('button', { name: 'Contact us' })).to.equal(null)
expect(screen.queryByRole('link', { name: 'Documentation' })).to.equal(null)
})
})
@@ -1,20 +0,0 @@
import { expect } from 'chai'
import { screen, fireEvent, within } from '@testing-library/react'
import HelpShowHotkeys from '../../../../../frontend/js/features/editor-left-menu/components/help-show-hotkeys'
import { renderWithEditorContext } from '../../../helpers/render-with-context'
import fetchMock from 'fetch-mock'
describe('<HelpShowHotkeys />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('open hotkeys modal when clicked', function () {
renderWithEditorContext(<HelpShowHotkeys />)
expect(screen.queryByRole('dialog')).to.equal(null)
fireEvent.click(screen.getByRole('button', { name: 'Show Hotkeys' }))
const modal = screen.getAllByRole('dialog')[0]
within(modal).getByText('Common')
})
})
@@ -1,30 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsAutoCloseBrackets from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-auto-close-brackets'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsAutoCloseBrackets />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsAutoCloseBrackets />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Auto-close brackets')
const optionOn = within(select).getByText('On')
expect(optionOn.getAttribute('value')).to.equal('true')
const optionOff = within(select).getByText('Off')
expect(optionOff.getAttribute('value')).to.equal('false')
})
})
@@ -1,30 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsAutoComplete from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-auto-complete'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsAutoComplete />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsAutoComplete />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Auto-complete')
const optionOn = within(select).getByText('On')
expect(optionOn.getAttribute('value')).to.equal('true')
const optionOff = within(select).getByText('Off')
expect(optionOff.getAttribute('value')).to.equal('false')
})
})
@@ -1,36 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsCompiler from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-compiler'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsCompiler />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsCompiler />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Compiler')
const optionPdfLaTeX = within(select).getByText('pdfLaTeX')
expect(optionPdfLaTeX.getAttribute('value')).to.equal('pdflatex')
const optionLaTeX = within(select).getByText('LaTeX')
expect(optionLaTeX.getAttribute('value')).to.equal('latex')
const optionXeLaTeX = within(select).getByText('XeLaTeX')
expect(optionXeLaTeX.getAttribute('value')).to.equal('xelatex')
const optionLuaLaTeX = within(select).getByText('LuaLaTeX')
expect(optionLuaLaTeX.getAttribute('value')).to.equal('lualatex')
})
})
@@ -1,33 +0,0 @@
import { fireEvent, screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import SettingsDictionary from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-dictionary'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsDictionary />', function () {
it('open dictionary modal', function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsDictionary />
</EditorLeftMenuProvider>
</EditorProviders>
)
screen.getByText('Dictionary')
const button = screen.getByText('Edit')
fireEvent.click(button)
const modal = screen.getByTestId('dictionary-modal')
within(modal).getByRole('heading', { name: 'Edit Dictionary' })
within(modal).getByText('Your custom dictionary is empty.')
const closeButton = within(modal).getByRole('button', {
name: 'Close dialog',
})
fireEvent.click(closeButton)
expect(screen.getByTestId('dictionary-modal')).to.not.be.null
})
})
@@ -1,51 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsDocument from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-document'
import { Folder } from '../../../../../../types/folder'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsDocument />', function () {
const rootFolder: Folder = {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [
{
_id: '123abc',
name: 'main.tex',
},
],
fileRefs: [],
folders: [],
}
let originalSettings: typeof window.metaAttributesCache
beforeEach(function () {
originalSettings = window.metaAttributesCache.get('ol-ExposedSettings')
window.metaAttributesCache.set('ol-ExposedSettings', {
validRootDocExtensions: ['tex'],
})
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
window.metaAttributesCache.set('ol-ExposedSettings', originalSettings)
})
it('shows correct menu', async function () {
render(
<EditorProviders rootFolder={[rootFolder as any]}>
<EditorLeftMenuProvider>
<SettingsDocument />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Main document')
const optionOn = within(select).getByText('main.tex')
expect(optionOn.getAttribute('value')).to.equal('123abc')
})
})
@@ -1,112 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsEditorTheme from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-editor-theme'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
const MOCK_IEEE_BRAND_ID = 123
describe('<SettingsEditorTheme />', function () {
const editorThemes = [
{ name: 'editortheme-1', dark: false },
{ name: 'editortheme-2', dark: false },
{ name: 'editortheme-3', dark: false },
]
const legacyEditorThemes = [
{ name: 'legacytheme-1', dark: false },
{ name: 'legacytheme-2', dark: false },
{ name: 'legacytheme-3', dark: false },
]
beforeEach(function () {
window.metaAttributesCache.set('ol-editorThemes', editorThemes)
window.metaAttributesCache.set('ol-legacyEditorThemes', legacyEditorThemes)
window.metaAttributesCache.set('ol-brandVariation', {
brand_id: undefined,
})
window.metaAttributesCache.get('ol-ExposedSettings').ieeeBrandId =
MOCK_IEEE_BRAND_ID
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
function checkSelect(select: HTMLElement) {
for (const theme of editorThemes) {
const option = within(select).getByText(theme.name.replace(/_/g, ' '))
expect(option.getAttribute('value')).to.equal(theme.name)
}
for (const theme of legacyEditorThemes) {
const option = within(select).getByText(
theme.name.replace(/_/g, ' ') + ' (Legacy)'
)
expect(option.getAttribute('value')).to.equal(theme.name)
}
}
describe('with default theme', function () {
beforeEach(function () {
render(
<EditorProviders userSettings={{ overallTheme: '' }}>
<EditorLeftMenuProvider>
<SettingsEditorTheme />
</EditorLeftMenuProvider>
</EditorProviders>
)
})
it('shows correct menu', async function () {
const select = screen.getByLabelText('Editor theme')
expect(select).to.exist
checkSelect(select)
})
})
describe('with system theme', function () {
beforeEach(function () {
render(
<EditorProviders userSettings={{ overallTheme: 'system' }}>
<EditorLeftMenuProvider>
<SettingsEditorTheme />
</EditorLeftMenuProvider>
</EditorProviders>
)
})
it('shows correct menu', async function () {
const select = screen.queryByLabelText('Editor theme')
expect(select).to.not.exist
const lightSelect = screen.getByLabelText('Light editor theme')
expect(lightSelect).to.exist
checkSelect(lightSelect)
const darkSelect = screen.getByLabelText('Dark editor theme')
expect(darkSelect).to.exist
checkSelect(darkSelect)
})
})
describe('with IEEE branding', function () {
beforeEach(function () {
window.metaAttributesCache.set('ol-brandVariation', {
brand_id: MOCK_IEEE_BRAND_ID,
})
render(
<EditorProviders userSettings={{ overallTheme: 'system' }}>
<EditorLeftMenuProvider>
<SettingsEditorTheme />
</EditorLeftMenuProvider>
</EditorProviders>
)
})
it('ignores the system theme and shows single selection', async function () {
const select = screen.getByLabelText('Editor theme')
expect(select).to.exist
checkSelect(select)
})
})
})
@@ -1,35 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsFontFamily from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-font-family'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsFontFamily />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsFontFamily />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Font Family')
const optionMonaco = within(select).getByText('Monaco / Menlo / Consolas')
expect(optionMonaco.getAttribute('value')).to.equal('monaco')
const optionLucida = within(select).getByText('Lucida / Source Code Pro')
expect(optionLucida.getAttribute('value')).to.equal('lucida')
const optionOpenDyslexicMono = within(select).getByText('OpenDyslexic Mono')
expect(optionOpenDyslexicMono.getAttribute('value')).to.equal(
'opendyslexicmono'
)
})
})
@@ -1,31 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsFontSize from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-font-size'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsFontSize />', function () {
const sizes = ['10', '11', '12', '13', '14', '16', '18', '20', '22', '24']
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsFontSize />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Font Size')
for (const size of sizes) {
const option = within(select).getByText(`${size}px`)
expect(option.getAttribute('value')).to.equal(size)
}
})
})
@@ -1,47 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsImageName from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-image-name'
import type { ImageName } from '../../../../../../types/project-settings'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsImageName />', function () {
const imageNames: ImageName[] = [
{
imageDesc: 'Image 1',
imageName: 'img-1',
allowed: true,
},
{
imageDesc: 'Image 2',
imageName: 'img-2',
allowed: true,
},
]
beforeEach(function () {
window.metaAttributesCache.set('ol-imageNames', imageNames)
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsImageName />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('TeX Live version')
for (const { imageName, imageDesc } of imageNames) {
const option = within(select).getByText(imageDesc)
expect(option.getAttribute('value')).to.equal(imageName)
}
})
})
@@ -1,33 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsKeybindings from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-keybindings'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
describe('<SettingsKeybindings />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsKeybindings />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Keybindings')
const optionNone = within(select).getByText('None')
expect(optionNone.getAttribute('value')).to.equal('default')
const optionVim = within(select).getByText('Vim')
expect(optionVim.getAttribute('value')).to.equal('vim')
const optionEmacs = within(select).getByText('Emacs')
expect(optionEmacs.getAttribute('value')).to.equal('emacs')
})
})
@@ -1,33 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsLineHeight from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-line-height'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
describe('<SettingsLineHeight />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsLineHeight />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Line Height')
const optionCompact = within(select).getByText('Compact')
expect(optionCompact.getAttribute('value')).to.equal('compact')
const optionNormal = within(select).getByText('Normal')
expect(optionNormal.getAttribute('value')).to.equal('normal')
const optionWide = within(select).getByText('Wide')
expect(optionWide.getAttribute('value')).to.equal('wide')
})
})
@@ -1,30 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsMathPreview from '@/features/editor-left-menu/components/settings/settings-math-preview'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
describe('<SettingsMathPreview />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsMathPreview />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Equation preview')
const optionOn = within(select).getByText('On')
expect(optionOn.getAttribute('value')).to.equal('true')
const optionOff = within(select).getByText('Off')
expect(optionOff.getAttribute('value')).to.equal('false')
})
})
@@ -1,98 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsOverallTheme from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-overall-theme'
import type { OverallThemeMeta } from '../../../../../../types/project-settings'
import getMeta from '@/utils/meta'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
const IEEE_BRAND_ID = 1234
const OTHER_BRAND_ID = 2234
describe('<SettingsOverallTheme />', function () {
const overallThemes: OverallThemeMeta[] = [
{
name: 'Overall Theme 1',
val: '',
path: 'https://overleaf.com/overalltheme-1.css',
},
{
name: 'Overall Theme 2',
val: 'light-',
path: 'https://overleaf.com/overalltheme-2.css',
},
]
beforeEach(function () {
window.metaAttributesCache.set('ol-overallThemes', overallThemes)
Object.assign(getMeta('ol-ExposedSettings'), {
ieeeBrandId: IEEE_BRAND_ID,
})
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsOverallTheme />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Overall theme')
for (const theme of overallThemes) {
const option = within(select).getByText(theme.name)
expect(option.getAttribute('value')).to.equal(theme.val)
}
})
describe('Branded Project', function () {
it('should hide overall theme picker for IEEE branded projects', function () {
window.metaAttributesCache.set('ol-brandVariation', {
brand_id: IEEE_BRAND_ID,
})
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsOverallTheme />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.queryByText('Overall theme')
expect(select).to.not.exist
})
it('should show overall theme picker for branded projects that are not IEEE', function () {
window.metaAttributesCache.set('ol-brandVariation', {
brand_id: OTHER_BRAND_ID,
})
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsOverallTheme />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Overall theme')
expect(select).to.exist
})
it('should show overall theme picker for non branded projects', function () {
window.metaAttributesCache.set('ol-brandVariation', undefined)
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsOverallTheme />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Overall theme')
expect(select).to.exist
})
})
})
@@ -1,30 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsPdfViewer from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-pdf-viewer'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
describe('<SettingsPdfViewer />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsPdfViewer />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('PDF Viewer')
const optionOverleaf = within(select).getByText('Overleaf')
expect(optionOverleaf.getAttribute('value')).to.equal('pdfjs')
const optionBrowser = within(select).getByText('Browser')
expect(optionBrowser.getAttribute('value')).to.equal('native')
})
})
@@ -1,50 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsSpellCheckLanguage from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-spell-check-language'
import type { SpellCheckLanguage } from '../../../../../../types/project-settings'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
describe('<SettingsSpellCheckLanguage />', function () {
const languages: SpellCheckLanguage[] = [
{
name: 'Lang 1',
code: 'lang-1',
dic: 'lang_1',
},
{
name: 'Lang 2',
code: 'lang-2',
dic: 'lang_2',
},
]
beforeEach(function () {
window.metaAttributesCache.set('ol-languages', languages)
})
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsSpellCheckLanguage />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Spell check')
const optionEmpty = within(select).getByText('Off')
expect(optionEmpty.getAttribute('value')).to.equal('')
for (const language of languages) {
const option = within(select).getByText(language.name)
expect(option.getAttribute('value')).to.equal(language.code)
}
})
})
@@ -1,30 +0,0 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsSyntaxValidation from '../../../../../../frontend/js/features/editor-left-menu/components/settings/settings-syntax-validation'
import { EditorProviders } from '../../../../helpers/editor-providers'
import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/editor-left-menu-context'
describe('<SettingsSyntaxValidation />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('shows correct menu', async function () {
render(
<EditorProviders>
<EditorLeftMenuProvider>
<SettingsSyntaxValidation />
</EditorLeftMenuProvider>
</EditorProviders>
)
const select = screen.getByLabelText('Code check')
const optionOn = within(select).getByText('On')
expect(optionOn.getAttribute('value')).to.equal('true')
const optionOff = within(select).getByText('Off')
expect(optionOff.getAttribute('value')).to.equal('false')
})
})
@@ -1,7 +1,6 @@
import { screen, within, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import SettingsOverallTheme from '../../../../../frontend/js/features/editor-left-menu/components/settings/settings-overall-theme'
import type { OverallThemeMeta } from '../../../../../types/project-settings'
import getMeta from '@/utils/meta'
import { EditorProviders } from '../../../helpers/editor-providers'
@@ -76,7 +75,7 @@ describe('<OverallThemeSetting />', function () {
render(
<EditorProviders>
<SettingsModalProvider>
<SettingsOverallTheme />
<OverallThemeSetting />
</SettingsModalProvider>
</EditorProviders>
)
@@ -91,7 +90,7 @@ describe('<OverallThemeSetting />', function () {
render(
<EditorProviders>
<SettingsModalProvider>
<SettingsOverallTheme />
<OverallThemeSetting />
</SettingsModalProvider>
</EditorProviders>
)
@@ -104,7 +103,7 @@ describe('<OverallThemeSetting />', function () {
render(
<EditorProviders>
<SettingsModalProvider>
<SettingsOverallTheme />
<OverallThemeSetting />
</SettingsModalProvider>
</EditorProviders>
)