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:
committed by
Copybot
parent
144334ec58
commit
d34d15242e
@@ -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": "",
|
||||
|
||||
BIN
Binary file not shown.
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+147
@@ -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;
|
||||
}
|
||||
@@ -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 project’s 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 don’t 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 didn’t 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.",
|
||||
|
||||
Reference in New Issue
Block a user