Merge pull request #22855 from overleaf/mj-ide-settings

[web] Add settings modal skeleton to editor redesign

GitOrigin-RevId: bc2e7f07f7ab737a67965fa615a04c8ee88b1271
This commit is contained in:
Mathias Jakobsen
2025-01-20 12:17:49 +00:00
committed by Copybot
parent 144334ec58
commit d34d15242e
11 changed files with 325 additions and 2 deletions
@@ -441,6 +441,7 @@
"editor_disconected_click_to_reconnect": "",
"editor_limit_exceeded_in_this_project": "",
"editor_only_hide_pdf": "",
"editor_settings": "",
"editor_theme": "",
"educational_disclaimer": "",
"educational_disclaimer_heading": "",
@@ -554,6 +555,8 @@
"full_project_search": "",
"full_width": "",
"future_payments": "",
"general": "",
"general_settings": "",
"generate_token": "",
"generic_if_problem_continues_contact_us": "",
"generic_linked_file_compile_error": "",
@@ -757,6 +760,8 @@
"institutional_leavers_survey_notification": "",
"integrations": "",
"interested_in_cheaper_personal_plan": "",
"interface": "",
"interface_settings": "",
"invalid_confirmation_code": "",
"invalid_email": "",
"invalid_file_name": "",
@@ -1083,6 +1088,7 @@
"pay_now": "",
"payment_provider_unreachable_error": "",
"payment_summary": "",
"pdf": "",
"pdf_compile_in_progress_error": "",
"pdf_compile_rate_limit_hit": "",
"pdf_compile_try_again": "",
@@ -1090,6 +1096,7 @@
"pdf_only_hide_editor": "",
"pdf_preview_error": "",
"pdf_rendering_error": "",
"pdf_settings": "",
"pdf_unavailable_for_download": "",
"pdf_viewer": "",
"pdf_viewer_error": "",
@@ -3,9 +3,14 @@
// to update the font file with the latest icons.
export default /** @type {const} */ ([
'code',
'description',
'forum',
'help',
'integration_instructions',
'picture_as_pdf',
'rate_review',
'report',
'settings',
'web_asset',
])
@@ -16,6 +16,9 @@ import { useFeatureFlag } from '@/shared/context/split-test-context'
const MainLayoutNew = lazy(
() => import('@/features/ide-redesign/components/main-layout')
)
const SettingsModalNew = lazy(
() => import('@/features/ide-redesign/components/settings/settings-modal')
)
export default function IdePage() {
useLayoutEventTracking() // sent event when the layout changes
@@ -33,6 +36,7 @@ export default function IdePage() {
<Modals />
{newEditor ? (
<Suspense fallback={null}>
<SettingsModalNew />
<MainLayoutNew />
</Suspense>
) : (
@@ -1,9 +1,10 @@
import { ReactElement, useCallback, useState } from 'react'
import { ReactElement, useCallback, useMemo, useState } from 'react'
import { Nav, NavLink, Tab, TabContainer } from 'react-bootstrap-5'
import MaterialIcon, {
AvailableUnfilledIcon,
} from '@/shared/components/material-icon'
import { Panel } from 'react-resizable-panels'
import { useLayoutContext } from '@/shared/context/layout-context'
type RailElement = {
icon: AvailableUnfilledIcon
@@ -11,6 +12,14 @@ type RailElement = {
component: ReactElement
}
type RailActionLink = { key: string; icon: AvailableUnfilledIcon; href: string }
type RailActionButton = {
key: string
icon: AvailableUnfilledIcon
action: () => void
}
type RailAction = RailActionLink | RailActionButton
const RAIL_TABS: RailElement[] = [
// NOTE: The file tree **MUST** be the first (i.e. default) tab in the list
// since the file tree is responsible for opening the initial document.
@@ -45,6 +54,19 @@ export const RailLayout = () => {
const [selectedTab, setSelectedTab] = useState<string | undefined>(
RAIL_TABS[0]?.key
)
const { setLeftMenuShown } = useLayoutContext()
const railActions: RailAction[] = useMemo(
() => [
{ key: 'support', icon: 'help', href: '/learn' },
{
key: 'settings',
icon: 'settings',
action: () => setLeftMenuShown(true),
},
],
[setLeftMenuShown]
)
return (
<TabContainer
mountOnEnter // Only render when necessary (so that we can lazy load tab content)
@@ -55,7 +77,10 @@ export const RailLayout = () => {
id="ide-rail-tabs"
>
<div className="ide-rail">
<Nav defaultActiveKey={RAIL_TABS[0]?.key} className="flex-column">
<Nav
defaultActiveKey={RAIL_TABS[0]?.key}
className="d-flex flex-column ide-rail-tabs-nav"
>
{RAIL_TABS.map(({ icon, key }) => (
<RailTab
active={selectedTab === key}
@@ -64,6 +89,10 @@ export const RailLayout = () => {
icon={icon}
/>
))}
<div className="flex-grow-1" />
{railActions?.map(action => (
<RailActionElement key={action.key} action={action} />
))}
</Nav>
</div>
<Panel
@@ -106,3 +135,41 @@ const RailTab = ({
</NavLink>
)
}
const RailActionElement = ({ action }: { action: RailAction }) => {
const icon = (
<MaterialIcon
className="ide-rail-tab-link-icon"
type={action.icon}
unfilled
/>
)
const onActionClick = useCallback(() => {
if ('action' in action) {
action.action()
}
}, [action])
if ('href' in action) {
return (
<a
href={action.href}
target="_blank"
rel="noopener"
className="ide-rail-tab-link"
>
{icon}
</a>
)
} else {
return (
<button
onClick={onActionClick}
className="ide-rail-tab-link ide-rail-tab-button"
type="button"
>
{icon}
</button>
)
}
}
@@ -0,0 +1,147 @@
import MaterialIcon, {
AvailableUnfilledIcon,
} from '@/shared/components/material-icon'
import { ReactElement, useMemo, useState } from 'react'
import {
Nav,
NavLink,
TabContainer,
TabContent,
TabPane,
} from 'react-bootstrap-5'
import { useTranslation } from 'react-i18next'
export type SettingsEntry = SettingsLink | SettingsTab
type SettingsTab = {
icon: AvailableUnfilledIcon
key: string
component: ReactElement
title: string
subtitle: string
}
type SettingsLink = {
key: string
icon: AvailableUnfilledIcon
href: string
title: string
}
export const SettingsModalBody = () => {
const { t } = useTranslation()
const settingsTabs: SettingsEntry[] = useMemo(
() => [
{
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>,
},
{
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: 'account_settings',
title: t('account_settings'),
icon: 'settings',
href: '/user/settings',
},
],
[t]
)
const [activeTab, setActiveTab] = useState<string | null | undefined>(
settingsTabs[0]?.key
)
return (
<TabContainer
transition={false}
onSelect={setActiveTab}
defaultActiveKey={activeTab ?? undefined}
id="ide-settings-tabs"
>
<div className="d-flex flex-row">
<Nav
defaultActiveKey={settingsTabs[0]?.key}
className="d-flex flex-column ide-settings-tab-nav"
>
{settingsTabs.map(entry => (
<SettingsNavLink entry={entry} key={entry.key} />
))}
</Nav>
<TabContent>
{settingsTabs
.filter(t => 'component' in t)
.map(({ key, component, subtitle }) => (
<TabPane eventKey={key} key={key}>
<p className="ide-settings-tab-subtitle">{subtitle}</p>
<div className="ide-settings-tab-content">{component}</div>
</TabPane>
))}
</TabContent>
</div>
</TabContainer>
)
}
const SettingsNavLink = ({ entry }: { entry: SettingsEntry }) => {
if ('href' in entry) {
return (
<a
href={entry.href}
target="_blank"
rel="noopener"
className="ide-settings-tab-link"
>
<MaterialIcon
className="ide-settings-tab-link-icon"
type={entry.icon}
unfilled
/>
<span>{entry.title}</span>
<div className="flex-grow-1" />
<MaterialIcon
type="open_in_new"
className="ide-settings-tab-link-external"
/>
</a>
)
} else {
return (
<>
<NavLink
eventKey={entry.key}
className="ide-settings-tab-link"
key={entry.key}
>
<MaterialIcon
className="ide-settings-tab-link-icon"
type={entry.icon}
unfilled
/>
<span>{entry.title}</span>
</NavLink>
</>
)
}
}
@@ -0,0 +1,31 @@
import OLModal, {
OLModalBody,
OLModalHeader,
OLModalTitle,
} from '@/features/ui/components/ol/ol-modal'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useTranslation } from 'react-i18next'
import { SettingsModalBody } from './settings-modal-body'
const SettingsModal = () => {
// TODO ide-redesign-cleanup: Either rename the field, or introduce a separate
// one
const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
const { t } = useTranslation()
return (
<OLModal
show={leftMenuShown}
onHide={() => setLeftMenuShown(false)}
size="lg"
>
<OLModalHeader closeButton>
<OLModalTitle>{t('settings')}</OLModalTitle>
</OLModalHeader>
<OLModalBody className="ide-settings-modal-body">
<SettingsModalBody />
</OLModalBody>
</OLModal>
)
}
export default SettingsModal
@@ -8,6 +8,7 @@
@import 'editor/ide';
@import 'editor/ide-redesign';
@import 'editor/rail';
@import 'editor/settings';
@import 'editor/toolbar';
@import 'editor/online-users';
@import 'editor/hotkeys';
@@ -6,6 +6,11 @@
--ide-rail-link-active-indicator-background: var(--neutral-90);
}
.ide-rail-tab-button {
border: 0;
background: none;
}
.ide-rail-tab-link {
border-radius: 12px;
display: block;
@@ -55,3 +60,7 @@
height: 100%;
border: 1px solid var(--border-divider);
}
.ide-rail-tabs-nav {
height: 100%;
}
@@ -0,0 +1,45 @@
.ide-settings-tab-nav.nav {
width: 240px;
border-right: var(--bs-modal-header-border-width) solid
var(--bs-modal-header-border-color);
padding: var(--spacing-02);
gap: var(--spacing-02);
}
.ide-settings-tab-content {
padding: var(--spacing-06) var(--spacing-08);
max-height: 75%;
height: 400px;
}
.ide-settings-tab-subtitle {
font-size: var(--font-size-04);
line-height: var(--line-height-03);
padding: var(--spacing-06) var(--spacing-08);
}
.ide-settings-tab-link {
display: flex;
align-items: flex-start;
flex-direction: row;
gap: var(--spacing-02);
color: var(--neutral-90);
padding: var(--spacing-02);
border-radius: var(--border-radius-base);
font-size: var(--font-size-02);
line-height: var(--line-height-02);
text-decoration: none;
&:visited {
color: var(--neutral-90);
}
&.active {
color: #fff;
background-color: var(--neutral-90);
}
}
.ide-settings-modal-body {
padding: 0;
}
+7
View File
@@ -576,6 +576,7 @@
"editor_disconected_click_to_reconnect": "Editor disconnected, click anywhere to reconnect.",
"editor_limit_exceeded_in_this_project": "Too many editors in this project",
"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",
@@ -749,6 +750,8 @@
"gallery_page_items_lowercase": "gallery items",
"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.",
@@ -995,6 +998,8 @@
"institutional_login_unknown": "Sorry, we dont know which institution issued that email address. You can browse our <a href=\"__link__\">list of institutions</a> to find yours, or you can use one of the other options below.",
"integrations": "Integrations",
"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",
@@ -1468,6 +1473,7 @@
"payment_method_accepted": "__paymentMethod__ accepted",
"payment_provider_unreachable_error": "Sorry, there was an error talking to our payment provider. Please try again in a few moments.\nIf you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them.",
"payment_summary": "Payment summary",
"pdf": "PDF",
"pdf_compile_in_progress_error": "A previous compile is still running. Please wait a minute and try compiling again.",
"pdf_compile_rate_limit_hit": "Compile rate limit hit",
"pdf_compile_try_again": "Please wait for your other compile to finish before trying again.",
@@ -1475,6 +1481,7 @@
"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.",