Merge pull request #5630 from overleaf/jel-layout-dropdown

New project layout dropdown

GitOrigin-RevId: 8d7f4ff6649fe249b762642e70522597e5e78dd4
This commit is contained in:
Jessica Lawshe 2021-11-03 08:21:14 -05:00 committed by Copybot
parent cc78541714
commit 3ad686c30b
13 changed files with 296 additions and 3 deletions

View file

@ -881,6 +881,10 @@ const ProjectController = {
'new_navigation_ui',
true
),
showPdfDetach: shouldDisplayFeature(
'pdf_detach',
user.alphaProgram
),
showNewPdfPreview: shouldDisplayFeature(
'new_pdf_preview',
user.alphaProgram

View file

@ -189,6 +189,7 @@ block append meta
meta(name="ol-showNewLogsUI" data-type="boolean" content=showNewLogsUI)
meta(name="ol-logsUISubvariant" content=logsUISubvariant)
meta(name="ol-showSymbolPalette" data-type="boolean" content=showSymbolPalette)
meta(name="ol-showPdfDetach" data-type="boolean" content=showPdfDetach)
meta(name="ol-showNewPdfPreview" data-type="boolean" content=showNewPdfPreview)
meta(name="ol-enablePdfCaching" data-type="boolean" content=enablePdfCaching)
meta(name="ol-trackPdfDownload" data-type="boolean" content=trackPdfDownload)

View file

@ -78,6 +78,8 @@
"duplicate_file": "",
"easily_manage_your_project_files_everywhere": "",
"editing": "",
"editor_and_pdf": "",
"editor_only_hide_pdf": "",
"error": "",
"expand": "",
"export_project_to_github": "",
@ -165,6 +167,7 @@
"invalid_filename": "",
"invalid_request": "",
"invite_not_accepted": "",
"layout": "",
"learn_how_to_make_documents_compile_quickly": "",
"learn_more_about_link_sharing": "",
"link_sharing_is_off": "",
@ -237,6 +240,7 @@
"pdf_compile_in_progress_error": "",
"pdf_compile_rate_limit_hit": "",
"pdf_compile_try_again": "",
"pdf_only_hide_editor": "",
"pdf_preview_error": "",
"pdf_rendering_error": "",
"pdf_viewer_error": "",
@ -253,6 +257,7 @@
"project_approaching_file_limit": "",
"project_flagged_too_many_compiles": "",
"project_has_too_many_files": "",
"project_layout_sharing_submission": "",
"project_not_linked_to_github": "",
"project_ownership_transfer_confirmation_1": "",
"project_ownership_transfer_confirmation_2": "",
@ -296,6 +301,7 @@
"select_from_output_files": "",
"select_from_source_files": "",
"select_from_your_computer": "",
"selected": "",
"send_first_message": "",
"server_error": "",
"session_error": "",

View file

@ -59,6 +59,7 @@ const EditorNavigationToolbarRoot = React.memo(
} = useEditorContext(editorContextPropTypes)
const {
changeLayout,
chatIsOpen,
setChatIsOpen,
reviewPanelOpen,
@ -93,6 +94,13 @@ const EditorNavigationToolbarRoot = React.memo(
setView(view === 'pdf' ? 'editor' : 'pdf')
}, [view, setView])
const handleChangeLayout = useCallback(
(newLayout, newView) => {
changeLayout(newLayout, newView)
},
[changeLayout]
)
const openShareModal = useCallback(() => {
openShareProjectModal(isProjectOwner)
}, [openShareProjectModal, isProjectOwner])
@ -118,6 +126,7 @@ const EditorNavigationToolbarRoot = React.memo(
style={loading ? { display: 'none' } : {}}
cobranding={cobranding}
onShowLeftMenuClick={onShowLeftMenuClick}
handleChangeLayout={handleChangeLayout}
chatIsOpen={chatIsOpen}
unreadMessageCount={unreadMessageCount}
toggleChatOpen={toggleChatOpen}
@ -139,6 +148,8 @@ const EditorNavigationToolbarRoot = React.memo(
pdfButtonIsVisible={pdfLayout === 'flat'}
togglePdfView={togglePdfView}
trackChangesVisible={trackChangesVisible}
pdfLayout={pdfLayout}
view={view}
/>
)
}

View file

@ -0,0 +1,39 @@
function IconEditorOnly() {
const color = '#505050' // match color from .dropdown-menu > li > a
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="1.5769"
width="15"
height="13.7692"
rx="1.5"
stroke={color}
/>
<line x1="1" y1="2.49976" x2="15" y2="2.49976" stroke={color} />
<line
x1="14"
y1="2.99976"
x2="14"
y2="14.9998"
stroke={color}
strokeWidth="2"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.74946 5.26853L10.7397 8.35194C11.0868 8.70989 11.0868 9.29025 10.7397 9.6482L7.74946 12.7316C7.40233 13.0896 6.83952 13.0896 6.49238 12.7316C6.14525 12.3736 6.51344 11.7904 6.86057 11.4324L8.33333 9.91374L3.88889 9.91667C3.39797 9.91667 3 9.50629 3 9.00007C3 8.49385 3.39797 8.08347 3.88889 8.08347L8.33333 8.08054L6.86057 6.56187C6.51344 6.20392 6.14525 5.62649 6.49238 5.26853C6.83952 4.91058 7.40233 4.91058 7.74946 5.26853Z"
fill={color}
/>
</svg>
)
}
export default IconEditorOnly

View file

@ -0,0 +1,25 @@
function IconPdfOnly() {
const color = '#505050' // match color from .dropdown-menu > li > a
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="0.5" y="1.5" width="15" height="14" rx="1.5" stroke={color} />
<line x1="1" y1="2.5" x2="15" y2="2.5" stroke={color} />
<line x1="2" y1="3" x2="2" y2="15" stroke={color} strokeWidth="2" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.25054 12.7315L5.26035 9.64806C4.91322 9.29011 4.91322 8.70975 5.26035 8.3518L8.25054 5.2684C8.59767 4.91044 9.16048 4.91044 9.50762 5.2684C9.85475 5.62635 9.48656 6.20964 9.13943 6.56759L7.66667 8.08626L12.1111 8.08333C12.602 8.08333 13 8.49371 13 8.99993C13 9.50615 12.602 9.91653 12.1111 9.91653L7.66667 9.91946L9.13943 11.4381C9.48656 11.7961 9.85475 12.3735 9.50762 12.7315C9.16048 13.0894 8.59767 13.0894 8.25054 12.7315Z"
fill={color}
/>
</svg>
)
}
export default IconPdfOnly

View file

@ -0,0 +1,89 @@
import PropTypes from 'prop-types'
import { Dropdown, MenuItem } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import IconChecked from '../../../shared/components/icon-checked'
import ControlledDropdown from '../../../shared/components/controlled-dropdown'
import IconEditorOnly from './icon-editor-only'
import IconPdfOnly from './icon-pdf-only'
function IconCheckmark({ iconFor, pdfLayout, view }) {
if (iconFor === 'editorOnly' && pdfLayout === 'flat' && view === 'editor') {
return <IconChecked />
} else if (iconFor === 'pdfOnly' && pdfLayout === 'flat' && view === 'pdf') {
return <IconChecked />
} else if (iconFor === 'sideBySide' && pdfLayout === 'sideBySide') {
return <IconChecked />
}
// return empty icon for placeholder
return <Icon type="" modifier="fw" />
}
function LayoutDropdownButton({ handleChangeLayout, pdfLayout, view }) {
const { t } = useTranslation()
// bsStyle is required for Dropdown.Toggle, but we will override style
return (
<ControlledDropdown id="layout-dropdown" className="toolbar-item">
<Dropdown.Toggle className="btn-full-height" bsStyle="link">
<Icon type="columns" modifier="fw" />
<span className="toolbar-label">{t('layout')}</span>
</Dropdown.Toggle>
<Dropdown.Menu id="layout-dropdown-list">
<MenuItem header>{t('layout')}</MenuItem>
<MenuItem onSelect={() => handleChangeLayout('sideBySide')}>
<IconCheckmark
iconFor="sideBySide"
pdfLayout={pdfLayout}
view={view}
/>
<Icon type="columns" />
{t('editor_and_pdf')}
</MenuItem>
<MenuItem
onSelect={() => handleChangeLayout('flat', 'editor')}
className="menu-item-with-svg"
>
<IconCheckmark
iconFor="editorOnly"
pdfLayout={pdfLayout}
view={view}
/>
<IconEditorOnly />
<Trans
i18nKey="editor_only_hide_pdf"
components={[
<span key="editor_only_hide_pdf" className="subdued" />,
]}
/>
</MenuItem>
<MenuItem
onSelect={() => handleChangeLayout('flat', 'pdf')}
className="menu-item-with-svg"
>
<IconCheckmark iconFor="pdfOnly" pdfLayout={pdfLayout} view={view} />
<IconPdfOnly />
<Trans
i18nKey="pdf_only_hide_editor"
components={[
<span key="pdf_only_hide_editor" className="subdued" />,
]}
/>
</MenuItem>
</Dropdown.Menu>
</ControlledDropdown>
)
}
export default LayoutDropdownButton
IconCheckmark.propTypes = {
iconFor: PropTypes.string.isRequired,
pdfLayout: PropTypes.string.isRequired,
view: PropTypes.string,
}
LayoutDropdownButton.propTypes = {
handleChangeLayout: PropTypes.func.isRequired,
pdfLayout: PropTypes.string.isRequired,
view: PropTypes.string,
}

View file

@ -1,9 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useTranslation } from 'react-i18next'
import MenuButton from './menu-button'
import CobrandingLogo from './cobranding-logo'
import BackToProjectsButton from './back-to-projects-button'
import ChatToggleButton from './chat-toggle-button'
import LayoutDropdownButton from './layout-dropdown-button'
import OnlineUsersWidget from './online-users-widget'
import ProjectNameEditableLabel from './project-name-editable-label'
import TrackChangesToggleButton from './track-changes-toggle-button'
@ -18,6 +20,7 @@ const PublishButton = publishModalModules?.import.default
const ToolbarHeader = React.memo(function ToolbarHeader({
cobranding,
onShowLeftMenuClick,
handleChangeLayout,
chatIsOpen,
toggleChatOpen,
reviewPanelOpen,
@ -33,17 +36,24 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
renameProject,
hasRenamePermissions,
openShareModal,
pdfLayout,
pdfViewIsOpen,
pdfButtonIsVisible,
togglePdfView,
trackChangesVisible,
view,
}) {
const { t } = useTranslation()
const shouldDisplayPublishButton = hasPublishPermissions && PublishButton
const shouldDisplayTrackChangesButton =
trackChangesVisible && !isRestrictedTokenMember
return (
<header className="toolbar toolbar-header toolbar-with-labels">
<header
className="toolbar toolbar-header toolbar-with-labels"
role="navigation"
aria-label={t('project_layout_sharing_submission')}
>
<div className="toolbar-left">
<MenuButton onClick={onShowLeftMenuClick} />
{cobranding && cobranding.logoImgUrl && (
@ -67,6 +77,14 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
<div className="toolbar-right">
<OnlineUsersWidget onlineUsers={onlineUsers} goToUser={goToUser} />
{window.showPdfDetach && (
<LayoutDropdownButton
handleChangeLayout={handleChangeLayout}
pdfLayout={pdfLayout}
view={view}
/>
)}
{shouldDisplayTrackChangesButton && (
<TrackChangesToggleButton
onClick={toggleReviewPanelOpen}
@ -98,6 +116,7 @@ const ToolbarHeader = React.memo(function ToolbarHeader({
ToolbarHeader.propTypes = {
onShowLeftMenuClick: PropTypes.func.isRequired,
handleChangeLayout: PropTypes.func.isRequired,
cobranding: PropTypes.object,
chatIsOpen: PropTypes.bool,
toggleChatOpen: PropTypes.func.isRequired,
@ -114,10 +133,12 @@ ToolbarHeader.propTypes = {
renameProject: PropTypes.func.isRequired,
hasRenamePermissions: PropTypes.bool,
openShareModal: PropTypes.func.isRequired,
pdfLayout: PropTypes.string.isRequired,
pdfViewIsOpen: PropTypes.bool,
pdfButtonIsVisible: PropTypes.bool,
togglePdfView: PropTypes.func.isRequired,
trackChangesVisible: PropTypes.bool,
view: PropTypes.string,
}
export default ToolbarHeader

View file

@ -0,0 +1,10 @@
import { useTranslation } from 'react-i18next'
import Icon from './icon'
function IconChecked() {
const { t } = useTranslation()
return <Icon type="check" modifier="fw" accessibilityLabel={t('selected')} />
}
export default IconChecked

View file

@ -64,8 +64,18 @@ export function LayoutProvider({ children }) {
})
}, [setPdfLayout, setView])
const changeLayout = useCallback(
(newLayout, newView) => {
setPdfLayout(newLayout)
setView(newLayout === 'sideBySide' ? 'editor' : newView)
localStorage.setItem('pdf.layout', newLayout)
},
[setPdfLayout, setView]
)
const value = useMemo(
() => ({
changeLayout,
chatIsOpen,
leftMenuShown,
pdfLayout,
@ -79,6 +89,7 @@ export function LayoutProvider({ children }) {
view,
}),
[
changeLayout,
chatIsOpen,
leftMenuShown,
pdfLayout,

View file

@ -7,7 +7,8 @@
border-bottom: @toolbar-border-bottom;
> a,
.toolbar-right > a {
.toolbar-right > a,
button {
position: relative;
.label {
position: absolute;
@ -19,6 +20,13 @@
}
}
.toolbar-right {
button {
background: transparent;
box-shadow: none;
}
}
> a:focus {
outline: none;
}
@ -151,6 +159,20 @@
}
}
#layout-dropdown {
// override style added by required bsStyle react-bootstrap prop
text-decoration: none !important;
}
#layout-dropdown-list {
a {
i,
svg {
margin-right: @margin-xs;
}
}
}
.header-cobranding-logo {
display: block;
width: auto;
@ -375,3 +397,28 @@
.opacity(0.65);
.box-shadow(none);
}
.menu-item-with-svg {
svg {
line,
rect {
stroke: @dropdown-link-color;
}
path {
fill: @dropdown-link-color;
}
}
a:hover,
a:focus {
svg {
line,
rect {
stroke: @dropdown-link-hover-color;
}
path {
fill: @dropdown-link-hover-color;
}
}
}
}

View file

@ -1523,5 +1523,11 @@
"showing_x_results": "Showing __x__ results",
"showing_1_result": "Showing 1 result",
"showing_1_result_of_total": "Showing 1 result of __total__",
"history_resync": "History Resync"
"history_resync": "History Resync",
"layout": "Layout",
"editor_and_pdf": "Editor & PDF",
"editor_only_hide_pdf": "Editor only <0>(hide PDF)</0>",
"pdf_only_hide_editor": "PDF only <0>(hide editor)</0>",
"selected": "Selected",
"project_layout_sharing_submission": "Project Layout, Sharing, and Submission"
}

View file

@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/react'
import LayoutDropdownButton from '../../../../../frontend/js/features/editor-navigation-toolbar/components/layout-dropdown-button'
describe('<LayoutDropdownButton />', function () {
const defaultProps = {
handleChangeLayout: () => {},
pdfLayout: 'flat',
view: 'editor',
}
it('should mark current layout option as selected (visually by checkmark, and aria-label for accessibility)', function () {
render(<LayoutDropdownButton {...defaultProps} />)
screen.getByRole('menuitem', {
name: 'Editor & PDF',
})
screen.getByRole('menuitem', {
name: 'PDF only (hide editor)',
})
screen.getByRole('menuitem', {
name: 'Selected Editor only (hide PDF)',
})
})
})