first commit
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
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 { useLocation } from '../../../shared/hooks/use-location'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
|
||||
type ProjectCopyResponse = {
|
||||
project_id: string
|
||||
}
|
||||
|
||||
export default function ActionsCopyProject() {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const location = useLocation()
|
||||
|
||||
const openProject = useCallback(
|
||||
({ project_id: projectId }: ProjectCopyResponse) => {
|
||||
location.assign(`/project/${projectId}`)
|
||||
},
|
||||
[location]
|
||||
)
|
||||
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import WordCountModal from '../../word-count-modal/components/word-count-modal'
|
||||
import LeftMenuButton from './left-menu-button'
|
||||
import * as eventTracking from '../../../infrastructure/event-tracking'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
|
||||
export default function ActionsWordCount() {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const { pdfUrl } = useCompileContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleShowModal = useCallback(() => {
|
||||
eventTracking.sendMB('left-menu-count')
|
||||
setShowModal(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{pdfUrl ? (
|
||||
<LeftMenuButton onClick={handleShowModal} icon="match_case">
|
||||
{t('word_count')}
|
||||
</LeftMenuButton>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
<WordCountModal show={showModal} handleHide={() => setShowModal(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
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'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
|
||||
export default function DownloadPDF() {
|
||||
const { t } = useTranslation()
|
||||
const { pdfDownloadUrl, pdfUrl } = useCompileContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
|
||||
function sendDownloadEvent() {
|
||||
eventTracking.sendMB('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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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 { _id: 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { createContext, FC, useCallback, useContext, useState } from 'react'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
|
||||
type EditorLeftMenuState = {
|
||||
settingToFocus?: string
|
||||
}
|
||||
|
||||
export const EditorLeftMenuContext = createContext<
|
||||
EditorLeftMenuState | undefined
|
||||
>(undefined)
|
||||
|
||||
export const EditorLeftMenuProvider: FC = ({ children }) => {
|
||||
const [value, setValue] = useState<EditorLeftMenuState>(() => ({
|
||||
settingToFocus: undefined,
|
||||
}))
|
||||
|
||||
useEventListener(
|
||||
'ui.focus-setting',
|
||||
useCallback(event => {
|
||||
setValue(value => ({
|
||||
...value,
|
||||
settingToFocus: (event as CustomEvent<string>).detail,
|
||||
}))
|
||||
}, [])
|
||||
)
|
||||
|
||||
return (
|
||||
<EditorLeftMenuContext.Provider value={value}>
|
||||
{children}
|
||||
</EditorLeftMenuContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useEditorLeftMenuContext = () => {
|
||||
const value = useContext(EditorLeftMenuContext)
|
||||
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
`useEditorLeftMenuContext is only available inside EditorLeftMenuProvider`
|
||||
)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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-5'
|
||||
import { EditorLeftMenuProvider } from './editor-left-menu-context'
|
||||
import withErrorBoundary from '@/infrastructure/error-boundary'
|
||||
import OLNotification from '@/features/ui/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"
|
||||
>
|
||||
<LazyEditorLeftMenuWithErrorBoundary />
|
||||
</Offcanvas.Body>
|
||||
</Offcanvas>
|
||||
{leftMenuShown && <LeftMenuMask />}
|
||||
</EditorLeftMenuProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(EditorLeftMenu)
|
||||
@@ -0,0 +1,24 @@
|
||||
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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
}: PropsWithChildren<Props>) {
|
||||
if (disabled) {
|
||||
return (
|
||||
<div className="left-menu-button link-disabled">
|
||||
<LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
|
||||
<span>{children}</span>
|
||||
{disabledAccesibilityText ? (
|
||||
<span className="sr-only">{disabledAccesibilityText}</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'button') {
|
||||
return (
|
||||
<button onClick={onClick} className="left-menu-button">
|
||||
<LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
|
||||
<span>{children}</span>
|
||||
</button>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="left-menu-button"
|
||||
>
|
||||
<LeftMenuButtonIcon svgIcon={svgIcon} icon={icon} />
|
||||
<span>{children}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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, overallTheme } = userSettings
|
||||
const [original] = useState({ editorTheme, overallTheme })
|
||||
const maskRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (maskRef.current) {
|
||||
if (
|
||||
editorTheme !== original.editorTheme ||
|
||||
overallTheme !== original.overallTheme
|
||||
) {
|
||||
maskRef.current.style.opacity = '0'
|
||||
}
|
||||
}
|
||||
}, [editorTheme, 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)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
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 '@/features/ui/components/ol/ol-form'
|
||||
|
||||
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 />
|
||||
<SettingsEditorTheme />
|
||||
<SettingsOverallTheme />
|
||||
<SettingsKeybindings />
|
||||
<SettingsFontSize />
|
||||
<SettingsFontFamily />
|
||||
<SettingsLineHeight />
|
||||
<SettingsPdfViewer />
|
||||
</OLForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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'
|
||||
|
||||
export default function SettingsCompiler() {
|
||||
const { t } = useTranslation()
|
||||
const { write } = usePermissionsContext()
|
||||
const { compiler, setCompiler } = useProjectSettingsContext()
|
||||
|
||||
return (
|
||||
<SettingsMenuSelect<ProjectCompiler>
|
||||
onChange={setCompiler}
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DictionaryModal from '../../../dictionary/components/dictionary-modal'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLFormLabel from '@/features/ui/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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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'
|
||||
|
||||
export default function SettingsDocument() {
|
||||
const { t } = useTranslation()
|
||||
const { write } = usePermissionsContext()
|
||||
const { docs } = useFileTreeData()
|
||||
const { rootDocId, setRootDocId } = useProjectSettingsContext()
|
||||
|
||||
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={setRootDocId}
|
||||
value={rootDocId ?? ''}
|
||||
disabled={!write}
|
||||
options={validDocsOptions}
|
||||
label={t('main_document')}
|
||||
name="rootDocId"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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 { Option } from './settings-menu-select'
|
||||
|
||||
export default function SettingsEditorTheme() {
|
||||
const { t } = useTranslation()
|
||||
const editorThemes = getMeta('ol-editorThemes')
|
||||
const legacyEditorThemes = getMeta('ol-legacyEditorThemes')
|
||||
const { editorTheme, setEditorTheme } = useProjectSettingsContext()
|
||||
|
||||
const options = useMemo(() => {
|
||||
const editorThemeOptions: Array<Option> =
|
||||
editorThemes?.map(theme => ({
|
||||
value: theme,
|
||||
label: theme.replace(/_/g, ' '),
|
||||
})) ?? []
|
||||
|
||||
const dividerOption: Option = {
|
||||
value: '-',
|
||||
label: '—————————————————',
|
||||
disabled: true,
|
||||
}
|
||||
|
||||
const legacyEditorThemeOptions: Array<Option> =
|
||||
legacyEditorThemes?.map(theme => ({
|
||||
value: theme,
|
||||
label: theme.replace(/_/g, ' ') + ' (Legacy)',
|
||||
})) ?? []
|
||||
|
||||
return [...editorThemeOptions, dividerOption, ...legacyEditorThemeOptions]
|
||||
}, [editorThemes, legacyEditorThemes])
|
||||
|
||||
return (
|
||||
<SettingsMenuSelect
|
||||
onChange={setEditorTheme}
|
||||
value={editorTheme}
|
||||
options={options}
|
||||
label={t('editor_theme')}
|
||||
name="editorTheme"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useProjectSettingsContext } from '../../context/project-settings-context'
|
||||
import SettingsMenuSelect from './settings-menu-select'
|
||||
import BetaBadge from '@/shared/components/beta-badge'
|
||||
import { FontFamily } from '@/shared/utils/styles'
|
||||
|
||||
export default function SettingsFontFamily() {
|
||||
const { t } = useTranslation()
|
||||
const { fontFamily, setFontFamily } = useProjectSettingsContext()
|
||||
|
||||
return (
|
||||
<div className="left-menu-setting-position">
|
||||
<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"
|
||||
/>
|
||||
<BetaBadge
|
||||
phase="release"
|
||||
link={{
|
||||
href: 'https://docs.google.com/forms/d/e/1FAIpQLScOt_IHTrcaM_uitP9dgCo_r4dl4cy9Ry6LhYYcwTN4qDTDUg/viewform',
|
||||
className: 'left-menu-setting-icon',
|
||||
}}
|
||||
tooltip={{
|
||||
id: 'font-family-tooltip',
|
||||
text: `${t('new_font_open_dyslexic')} ${t('click_to_give_feedback')}`,
|
||||
placement: 'right',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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'
|
||||
|
||||
export default function SettingsImageName() {
|
||||
const { t } = useTranslation()
|
||||
const { imageName, setImageName } = useProjectSettingsContext()
|
||||
const { write } = usePermissionsContext()
|
||||
|
||||
const allowedImageNames = useMemo(
|
||||
() => getMeta('ol-allowedImageNames') || [],
|
||||
[]
|
||||
)
|
||||
|
||||
const options: Array<Option> = useMemo(
|
||||
() =>
|
||||
allowedImageNames.map(({ imageName, imageDesc }) => ({
|
||||
value: imageName,
|
||||
label: imageDesc,
|
||||
})),
|
||||
[allowedImageNames]
|
||||
)
|
||||
|
||||
if (allowedImageNames.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsMenuSelect
|
||||
onChange={setImageName}
|
||||
value={imageName}
|
||||
disabled={!write}
|
||||
options={options}
|
||||
label={t('tex_live_version')}
|
||||
name="imageName"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import OLFormGroup from '@/features/ui/components/ol/ol-form-group'
|
||||
import OLFormLabel from '@/features/ui/components/ol/ol-form-label'
|
||||
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
|
||||
import { ChangeEventHandler, useCallback, useEffect, useRef } from 'react'
|
||||
import { Spinner } from 'react-bootstrap-5'
|
||||
import { useEditorLeftMenuContext } from '@/features/editor-left-menu/components/editor-left-menu-context'
|
||||
|
||||
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>>
|
||||
optgroup?: Optgroup<T>
|
||||
loading?: boolean
|
||||
onChange: (val: T) => void
|
||||
value?: T
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function SettingsMenuSelect<T extends PossibleValue = string>({
|
||||
label,
|
||||
name,
|
||||
options,
|
||||
optgroup,
|
||||
loading,
|
||||
onChange,
|
||||
value,
|
||||
disabled = false,
|
||||
}: 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 ? (
|
||||
<p className="mb-0">
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
size="sm"
|
||||
role="status"
|
||||
/>
|
||||
</p>
|
||||
) : (
|
||||
<OLFormSelect
|
||||
size="sm"
|
||||
onChange={handleChange}
|
||||
value={value?.toString()}
|
||||
disabled={disabled}
|
||||
ref={selectRef}
|
||||
>
|
||||
{options.map(option => (
|
||||
<option
|
||||
key={`${name}-${option.value}`}
|
||||
value={option.value.toString()}
|
||||
aria-hidden={option.ariaHidden}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
{optgroup ? (
|
||||
<optgroup label={optgroup.label}>
|
||||
{optgroup.options.map(option => (
|
||||
<option
|
||||
value={option.value.toString()}
|
||||
key={option.value.toString()}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
) : null}
|
||||
</OLFormSelect>
|
||||
)}
|
||||
</OLFormGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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 { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { supportsWebAssembly } from '@/utils/wasm'
|
||||
|
||||
export default function SettingsSpellCheckLanguage() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { spellCheckLanguage, setSpellCheckLanguage } =
|
||||
useProjectSettingsContext()
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
|
||||
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') }]}
|
||||
optgroup={optgroup}
|
||||
label={t('spell_check')}
|
||||
name="spellCheckLanguage"
|
||||
disabled={permissionsLevel === 'readOnly' || !supportsWebAssembly()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user