Merge pull request #23848 from overleaf/dp-settings

Add initial components for new editor settings modal

GitOrigin-RevId: e3eb511d2af9265e0fc1cf54178b3e2953717950
This commit is contained in:
Mathias Jakobsen
2025-03-04 13:14:30 +00:00
committed by Copybot
parent 2fe8d87d5c
commit e13850aa53
20 changed files with 438 additions and 30 deletions

View File

@@ -134,6 +134,7 @@
"anyone_with_link_can_edit": "",
"anyone_with_link_can_view": "",
"app_on_x": "",
"appearance": "",
"apply_suggestion": "",
"archive": "",
"archive_projects": "",
@@ -159,6 +160,7 @@
"autocompile_disabled_reason": "",
"autocomplete": "",
"autocomplete_references": "",
"automatically_inserts_closing_brackets_and_parentheses": "",
"available_with_group_professional": "",
"back": "",
"back_to_configuration": "",
@@ -235,6 +237,7 @@
"change_the_ownership_of_your_personal_projects": "",
"change_to_group_plan": "",
"change_to_this_plan": "",
"changes_the_color_scheme_of_the_code_editor": "",
"changing_the_position_of_your_figure": "",
"changing_the_position_of_your_table": "",
"chat": "",
@@ -317,6 +320,7 @@
"continue_to": "",
"continue_using_free_features": "",
"continue_with_free_plan": "",
"controls_the_theme_of_the_application_interface": "",
"copied": "",
"copy": "",
"copy_code": "",
@@ -469,7 +473,6 @@
"editor_limit_exceeded_in_this_project": "",
"editor_only": "",
"editor_only_hide_pdf": "",
"editor_settings": "",
"editor_theme": "",
"educational_disclaimer": "",
"educational_disclaimer_heading": "",
@@ -494,6 +497,8 @@
"enable_stop_on_first_error_under_recompile_dropdown_menu": "",
"enable_stop_on_first_error_under_recompile_dropdown_menu_v2": "",
"enabled": "",
"enables_a_real_time_equation_preview_in_the_editor": "",
"enables_real_time_syntax_checking_in_the_editor": "",
"enabling": "",
"end_of_document": "",
"enter_6_digit_code": "",
@@ -592,7 +597,6 @@
"full_width": "",
"future_payments": "",
"general": "",
"general_settings": "",
"generate_token": "",
"generic_if_problem_continues_contact_us": "",
"generic_linked_file_compile_error": "",
@@ -806,8 +810,6 @@
"integrations": "",
"integrations_like_github": "",
"interested_in_cheaper_personal_plan": "",
"interface": "",
"interface_settings": "",
"invalid_confirmation_code": "",
"invalid_email": "",
"invalid_file_name": "",
@@ -1165,7 +1167,6 @@
"pdf_only_hide_editor": "",
"pdf_preview_error": "",
"pdf_rendering_error": "",
"pdf_settings": "",
"pdf_unavailable_for_download": "",
"pdf_viewer": "",
"pdf_viewer_error": "",
@@ -1627,6 +1628,7 @@
"suggested": "",
"suggested_fix_for_error_in_path": "",
"suggestion_applied": "",
"suggests_code_completions_while_typing": "",
"support_for_your_browser_is_ending_soon": "",
"supports_up_to_x_users": "",
"sure_you_want_to_cancel_plan_change": "",
@@ -1790,6 +1792,7 @@
"toolbar_toggle_symbol_palette": "",
"toolbar_undo": "",
"toolbar_undo_redo_actions": "",
"tools": "",
"tooltip_hide_filetree": "",
"tooltip_hide_panel": "",
"tooltip_hide_pdf": "",

View File

@@ -5,6 +5,7 @@
export default /** @type {const} */ ([
'book_5',
'brush',
'code',
'create_new_folder',
'description',

View File

@@ -0,0 +1,10 @@
import SettingsSection from '../settings-section'
import OverallThemeSetting from '../appearance-settings/overall-theme-setting'
export default function AppearanceSettings() {
return (
<SettingsSection>
<OverallThemeSetting />
</SettingsSection>
)
}

View File

@@ -0,0 +1,45 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import DropdownSetting from '../dropdown-setting'
import getMeta from '@/utils/meta'
import { useMemo } from 'react'
import type { Option } from '../dropdown-setting'
import { useTranslation } from 'react-i18next'
import { OverallThemeMeta } from '../../../../../../../types/project-settings'
import { isIEEEBranded } from '@/utils/is-ieee-branded'
import { useLayoutContext } from '@/shared/context/layout-context'
import { OverallTheme } from '@/shared/utils/styles'
export default function OverallThemeSetting() {
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 (
<DropdownSetting
id="overallTheme"
label={t('overall_theme')}
description={t('controls_the_theme_of_the_application_interface')}
options={options}
onChange={setOverallTheme}
value={overallTheme}
loading={loadingStyleSheet}
/>
)
}

View File

@@ -0,0 +1,90 @@
import OLFormSelect from '@/features/ui/components/ol/ol-form-select'
import { ChangeEventHandler, useCallback } from 'react'
import Setting from './setting'
import classNames from 'classnames'
import { Spinner } from 'react-bootstrap-5'
type PossibleValue = string | number
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> = {
id: string
label: string
description: string
options: Array<Option<T>>
onChange: (val: T) => void
value?: T
disabled?: boolean
width?: 'default' | 'wide'
loading?: boolean
}
export default function DropdownSetting<T extends PossibleValue = string>({
id,
label,
description,
options,
onChange,
value,
disabled = false,
width = 'default',
loading = false,
}: SettingsMenuSelectProps<T>) {
const handleChange: ChangeEventHandler<HTMLSelectElement> = useCallback(
event => {
const selectedValue = event.target.value
let onChangeValue: PossibleValue = selectedValue
if (typeof value === 'number') {
onChangeValue = parseInt(selectedValue, 10)
}
onChange(onChangeValue as T)
},
[onChange, value]
)
return (
<Setting controlId={id} label={label} description={description}>
{loading ? (
<Spinner
animation="border"
aria-hidden="true"
size="sm"
role="status"
/>
) : (
<OLFormSelect
id={id}
className={classNames('ide-dropdown-setting', {
'ide-dropdown-setting-wide': width === 'wide',
})}
size="sm"
onChange={handleChange}
value={value?.toString()}
disabled={disabled}
>
{options.map(option => (
<option
key={`${id}-${option.value}`}
value={option.value.toString()}
aria-hidden={option.ariaHidden}
disabled={option.disabled}
>
{option.label}
</option>
))}
</OLFormSelect>
)}
</Setting>
)
}

View File

@@ -0,0 +1,19 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function AutoCloseBracketsSetting() {
const { autoPairDelimiters, setAutoPairDelimiters } =
useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="autoPairDelimiters"
label={t('auto_close_brackets')}
description={t('automatically_inserts_closing_brackets_and_parentheses')}
checked={autoPairDelimiters}
onChange={setAutoPairDelimiters}
/>
)
}

View File

@@ -0,0 +1,18 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function AutoCompleteSetting() {
const { autoComplete, setAutoComplete } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="autoComplete"
label={t('auto_complete')}
description={t('suggests_code_completions_while_typing')}
checked={autoComplete}
onChange={setAutoComplete}
/>
)
}

View File

@@ -0,0 +1,18 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function CodeCheckSetting() {
const { syntaxValidation, setSyntaxValidation } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="syntaxValidation"
label={t('syntax_validation')}
description={t('enables_real_time_syntax_checking_in_the_editor')}
checked={syntaxValidation}
onChange={setSyntaxValidation}
/>
)
}

View File

@@ -0,0 +1,25 @@
import EditorThemeSetting from './editor-theme-setting'
import AutoCompleteSetting from './auto-complete-setting'
import CodeCheckSetting from './code-check-setting'
import AutoCloseBracketsSetting from './auto-close-brackets-setting'
import SettingsSection from '../settings-section'
import MathPreviewSetting from './math-preview-setting'
import { useTranslation } from 'react-i18next'
export default function EditorSettings() {
const { t } = useTranslation()
return (
<>
<SettingsSection>
<EditorThemeSetting />
<AutoCompleteSetting />
<AutoCloseBracketsSetting />
<CodeCheckSetting />
</SettingsSection>
<SettingsSection title={t('tools')}>
<MathPreviewSetting />
</SettingsSection>
</>
)
}

View File

@@ -0,0 +1,46 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import DropdownSetting from '../dropdown-setting'
import getMeta from '@/utils/meta'
import { useMemo } from 'react'
import type { Option } from '../dropdown-setting'
import { useTranslation } from 'react-i18next'
export default function EditorThemeSetting() {
const editorThemes = getMeta('ol-editorThemes')
const legacyEditorThemes = getMeta('ol-legacyEditorThemes')
const { editorTheme, setEditorTheme } = useProjectSettingsContext()
const { t } = useTranslation()
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 (
<DropdownSetting
id="editorTheme"
label={t('editor_theme')}
description={t('changes_the_color_scheme_of_the_code_editor')}
options={options}
onChange={setEditorTheme}
value={editorTheme}
/>
)
}

View File

@@ -0,0 +1,18 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function MathPreviewSetting() {
const { mathPreview, setMathPreview } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="mathPreview"
label={t('equation_preview')}
description={t('enables_a_real_time_equation_preview_in_the_editor')}
checked={mathPreview}
onChange={setMathPreview}
/>
)
}

View File

@@ -0,0 +1,23 @@
export default function Setting({
label,
description,
controlId,
children,
}: {
label: string
description: string
controlId: string
children: React.ReactNode
}) {
return (
<div className="ide-setting">
<div>
<label htmlFor={controlId} className="ide-setting-title">
{label}
</label>
<div className="ide-setting-description">{description}</div>
</div>
{children}
</div>
)
}

View File

@@ -10,6 +10,8 @@ import {
TabPane,
} from 'react-bootstrap-5'
import { useTranslation } from 'react-i18next'
import EditorSettings from './editor-settings/editor-settings'
import AppearanceSettings from './appearance-settings/appearance-settings'
export type SettingsEntry = SettingsLink | SettingsTab
@@ -18,7 +20,6 @@ type SettingsTab = {
key: string
component: ReactElement
title: string
subtitle: string
}
type SettingsLink = {
@@ -35,30 +36,26 @@ export const SettingsModalBody = () => {
{
key: 'general',
title: t('general'),
subtitle: t('general_settings'),
icon: 'settings',
component: <div>General</div>,
},
{
key: 'editor',
title: t('editor'),
subtitle: t('editor_settings'),
icon: 'code',
component: <div>Editor</div>,
component: <EditorSettings />,
},
{
key: 'pdf',
title: t('pdf'),
subtitle: t('pdf_settings'),
icon: 'picture_as_pdf',
component: <div>PDF</div>,
},
{
key: 'interface',
title: t('interface'),
subtitle: t('interface_settings'),
icon: 'web_asset',
component: <div>Interface</div>,
key: 'appearance',
title: t('appearance'),
icon: 'brush',
component: <AppearanceSettings />,
},
{
key: 'account_settings',
@@ -89,13 +86,12 @@ export const SettingsModalBody = () => {
<SettingsNavLink entry={entry} key={entry.key} />
))}
</Nav>
<TabContent>
<TabContent className="ide-settings-tab-content">
{settingsTabs
.filter(t => 'component' in t)
.map(({ key, component, subtitle }) => (
.map(({ key, component }) => (
<TabPane eventKey={key} key={key}>
<p className="ide-settings-tab-subtitle">{subtitle}</p>
<div className="ide-settings-tab-content">{component}</div>
{component}
</TabPane>
))}
</TabContent>

View File

@@ -0,0 +1,18 @@
export default function SettingsSection({
children,
title,
}: {
children: React.ReactNode | React.ReactNode[]
title?: string
}) {
if (!children) {
return null
}
return (
<div className="ide-settings-section">
{title && <div className="ide-settings-section-title">{title}</div>}
{children}
</div>
)
}

View File

@@ -0,0 +1,34 @@
import Setting from './setting'
import OLFormSwitch from '@/features/ui/components/ol/ol-form-switch'
export default function ToggleSetting({
id,
label,
description,
checked,
onChange,
disabled,
}: {
id: string
label: string
description: string
checked: boolean | undefined
onChange: (newValue: boolean) => void
disabled?: boolean
}) {
const handleChange = () => {
onChange(!checked)
}
return (
<Setting controlId={id} label={label} description={description}>
<OLFormSwitch
id={id}
onChange={handleChange}
checked={checked}
label={label}
disabled={disabled}
/>
</Setting>
)
}

View File

@@ -7,9 +7,11 @@
}
.ide-settings-tab-content {
padding: var(--spacing-06) var(--spacing-08);
max-height: 75%;
height: 400px;
height: 500px;
width: 100%;
padding: 0 var(--spacing-06);
overflow: auto;
}
.ide-settings-tab-subtitle {
@@ -43,3 +45,42 @@
.ide-settings-modal-body {
padding: 0;
}
.ide-settings-section {
.ide-setting:not(:last-child) {
border-bottom: 1px solid var(--border-divider);
}
}
.ide-settings-section-title {
color: var(--content-secondary);
font-size: var(--font-size-01);
line-height: var(--line-height-01);
margin-top: var(--spacing-06);
margin-bottom: var(--spacing-03);
}
.ide-setting {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-06) 0;
}
.ide-dropdown-setting {
width: 120px;
}
.ide-dropdown-setting-wide {
width: 160px;
}
.ide-setting-title {
color: var(--content-primary);
font-size: var(--font-size-02);
}
.ide-setting-description {
color: var(--content-secondary);
font-size: var(--font-size-02);
}

View File

@@ -164,6 +164,7 @@
"anyone_with_link_can_edit": "Anyone with this link can edit this project",
"anyone_with_link_can_view": "Anyone with this link can view this project",
"app_on_x": "__appName__ on __social__",
"appearance": "Appearance",
"apply_educational_discount": "Apply educational discount",
"apply_educational_discount_description": "40% discount for groups using __appName__ for teaching",
"apply_suggestion": "Apply suggestion",
@@ -191,7 +192,7 @@
"at_most_x_libraries_can_be_selected": "At most __maxCount__ libraries can be selected",
"august": "August",
"author": "Author",
"auto_close_brackets": "Auto-close Brackets",
"auto_close_brackets": "Auto-close brackets",
"auto_compile": "Auto Compile",
"auto_complete": "Auto-complete",
"autocompile_disabled": "Autocompile disabled",
@@ -199,6 +200,7 @@
"autocomplete": "Autocomplete",
"autocomplete_references": "Reference Autocomplete (inside a <code>\\cite{}</code> block)",
"automatic_user_registration_uppercase": "Automatic user registration",
"automatically_inserts_closing_brackets_and_parentheses": "Automatically inserts closing brackets and parentheses",
"available_with_group_professional": "Available with Group Professional",
"back": "Back",
"back_to_account_settings": "Back to account settings",
@@ -300,6 +302,7 @@
"change_the_ownership_of_your_personal_projects": "Change the ownership of your personal projects to the new account. <0>Find out how to change project owner.</0>",
"change_to_group_plan": "Change to a group plan",
"change_to_this_plan": "Change to this plan",
"changes_the_color_scheme_of_the_code_editor": "Changes the color scheme of the code editor",
"changing_the_position_of_your_figure": "Changing the position of your figure",
"changing_the_position_of_your_table": "Changing the position of your table",
"chat": "Chat",
@@ -414,6 +417,7 @@
"continue_using_free_features": "Continue using our free features",
"continue_with_free_plan": "Continue with free plan",
"continue_with_service": "Continue with __service__",
"controls_the_theme_of_the_application_interface": "Controls the theme of the application interface",
"copied": "Copied",
"copy": "Copy",
"copy_code": "Copy code",
@@ -604,7 +608,6 @@
"editor_limit_exceeded_in_this_project": "Too many editors in this project",
"editor_only": "Editor only",
"editor_only_hide_pdf": "Editor only <0>(hide PDF)</0>",
"editor_settings": "Editor settings",
"editor_theme": "Editor theme",
"educational_disclaimer": "I confirm that users will be students or faculty using Overleaf primarily for study and teaching, and can provide evidence of this if requested.",
"educational_disclaimer_heading": "Educational discount confirmation",
@@ -639,6 +642,8 @@
"enable_stop_on_first_error_under_recompile_dropdown_menu": "Enable <0>“Stop on first error”</0> under the <1>Recompile</1> drop-down menu to help you find and fix errors right away.",
"enable_stop_on_first_error_under_recompile_dropdown_menu_v2": "Enable <0>Stop on first error</0> under the <1>Recompile</1> drop-down menu to help you find and fix errors right away.",
"enabled": "Enabled",
"enables_a_real_time_equation_preview_in_the_editor": "Enables a real-time equation preview in the editor",
"enables_real_time_syntax_checking_in_the_editor": "Enables real-time syntax checking in the editor",
"enabling": "Enabling",
"end_of_document": "End of document",
"enter_6_digit_code": "Enter 6-digit code",
@@ -787,7 +792,6 @@
"gallery_page_title": "Gallery - Templates, Examples and Articles written in LaTeX",
"gallery_show_more_tags": "Show more",
"general": "General",
"general_settings": "General settings",
"generate_token": "Generate token",
"generic_if_problem_continues_contact_us": "If the problem continues please contact us",
"generic_linked_file_compile_error": "This projects output files are not available because it failed to compile. Please open the project to see the compilation error details.",
@@ -1042,8 +1046,6 @@
"integrations": "Integrations",
"integrations_like_github": "Integrations like GitHub Sync",
"interested_in_cheaper_personal_plan": "Would you be interested in the cheaper <0>__price__</0> Personal plan?",
"interface": "Interface",
"interface_settings": "Interface settings",
"invalid_certificate": "Invalid certificate. Please check the certificate and try again.",
"invalid_confirmation_code": "That didnt work. Please check the code and try again.",
"invalid_email": "An email address is invalid",
@@ -1551,7 +1553,6 @@
"pdf_only_hide_editor": "PDF only <0>(hide editor)</0>",
"pdf_preview_error": "There was a problem displaying the compilation results for this project.",
"pdf_rendering_error": "PDF Rendering Error",
"pdf_settings": "PDF settings",
"pdf_unavailable_for_download": "PDF unavailable for download",
"pdf_viewer": "PDF Viewer",
"pdf_viewer_error": "There was a problem displaying the PDF for this project.",
@@ -2116,6 +2117,7 @@
"suggested_fix_for_error_in_path": "Suggested fix for error in __path__",
"suggestion": "Suggestion",
"suggestion_applied": "Suggestion applied",
"suggests_code_completions_while_typing": "Suggests code completions while typing",
"support": "Support",
"support_for_your_browser_is_ending_soon": "Support for your browser is ending soon",
"supports_up_to_x_users": "Supports up to <0>__count__ users</0>",
@@ -2310,6 +2312,7 @@
"toolbar_toggle_symbol_palette": "Toggle Symbol Palette",
"toolbar_undo": "Undo",
"toolbar_undo_redo_actions": "Undo/Redo actions",
"tools": "Tools",
"tooltip_hide_filetree": "Click to hide the file tree",
"tooltip_hide_panel": "Click to hide the panel",
"tooltip_hide_pdf": "Click to hide the PDF",

View File

@@ -92,7 +92,7 @@ describe('<EditorLeftMenu />', function () {
cy.findByLabelText('Main document')
cy.findByLabelText('Spell check')
cy.findByLabelText('Auto-complete')
cy.findByLabelText('Auto-close Brackets')
cy.findByLabelText('Auto-close brackets')
cy.findByLabelText('Code check')
cy.findByLabelText('Editor theme')
cy.findByLabelText('Overall theme')
@@ -886,7 +886,7 @@ describe('<EditorLeftMenu />', function () {
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('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')

View File

@@ -19,7 +19,7 @@ describe('<SettingsAutoCloseBrackets />', function () {
</EditorProviders>
)
const select = screen.getByLabelText('Auto-close Brackets')
const select = screen.getByLabelText('Auto-close brackets')
const optionOn = within(select).getByText('On')
expect(optionOn.getAttribute('value')).to.equal('true')