[web] View menu tweaks (#34548)

* [web] Add missing translation

* [web] Group PDF zoom options in view menu

* [web] Add review panel toggle to view menu

* [web] Address review comments

* [web] Show disabled buttons to anonymous users

* [web] Add e2e tests for editing mode in view menu

GitOrigin-RevId: d13dc307f1bcb7c50c76afbaa397c3aca8b96029
This commit is contained in:
Mathias Jakobsen
2026-06-16 08:06:03 +00:00
committed by Copybot
parent efd12c8feb
commit b7f984fc8d
6 changed files with 246 additions and 7 deletions
+120
View File
@@ -2,6 +2,8 @@ import {
createNewFile,
createProjectAndOpenInNewEditor,
openProjectById,
openProjectByName,
shareProjectByEmailAndAcceptInviteViaDash,
testNewFileUpload,
} from './helpers/project'
import { isExcludedBySharding, startWith } from './helpers/config'
@@ -321,6 +323,124 @@ describe('editor', function () {
})
})
describe('editing mode', function () {
if (isExcludedBySharding('PRO_DEFAULT_1')) return
startWith({ pro: true })
const OWNER = USER
const EDITOR = COLLABORATOR
const REVIEWER = 'reviewer@example.com'
const VIEWER = 'viewer@example.com'
ensureUserExists({ email: OWNER })
ensureUserExists({ email: EDITOR })
ensureUserExists({ email: REVIEWER })
ensureUserExists({ email: VIEWER })
let projectName: string
function openEditingModeMenu() {
cy.findByRole('button', { name: 'View' }).click()
cy.findByRole('menuitem', { name: 'Editing mode' }).click()
}
beforeWithReRunOnTestRetry(() => {
projectName = `project-${uuid()}`
login(OWNER)
createProjectAndOpenInNewEditor(projectName, { type: 'Example project' })
shareProjectByEmailAndAcceptInviteViaDash(
projectName,
EDITOR,
'Editor',
true
)
login(OWNER)
shareProjectByEmailAndAcceptInviteViaDash(
projectName,
REVIEWER,
'Reviewer',
true
)
login(OWNER)
shareProjectByEmailAndAcceptInviteViaDash(
projectName,
VIEWER,
'Viewer',
true
)
})
it('lets an editor switch between editing and reviewing', function () {
login(EDITOR)
openProjectByName(projectName, true)
openEditingModeMenu()
cy.findByRole('menuitem', { name: 'Editing' })
.should('have.attr', 'aria-selected', 'true')
.and('not.have.attr', 'aria-disabled')
cy.findByRole('menuitem', { name: 'Reviewing' })
.should('have.attr', 'aria-selected', 'false')
.and('not.have.attr', 'aria-disabled')
cy.findByRole('menuitem', { name: 'Viewing' }).should('not.exist')
cy.findByRole('menuitem', { name: 'Reviewing' }).click()
cy.findByRole('button', { name: 'Reviewing' }).should('exist')
openEditingModeMenu()
cy.findByRole('menuitem', { name: 'Reviewing' }).should(
'have.attr',
'aria-selected',
'true'
)
cy.findByRole('menuitem', { name: 'Editing' }).should(
'have.attr',
'aria-selected',
'false'
)
cy.findByRole('menuitem', { name: 'Editing' }).click()
cy.findByRole('button', { name: 'Editing' }).should('exist')
})
it('shows a reviewer in reviewing mode with editing disabled', function () {
login(REVIEWER)
openProjectByName(projectName, true)
openEditingModeMenu()
cy.findByRole('menuitem', { name: 'Reviewing' })
.should('have.attr', 'aria-selected', 'true')
.and('not.have.attr', 'aria-disabled')
cy.findByRole('menuitem', { name: 'Editing' }).should(
'have.attr',
'aria-disabled',
'true'
)
cy.findByRole('menuitem', { name: 'Viewing' }).should('not.exist')
})
it('shows a viewer in viewing mode with editing and reviewing disabled', function () {
login(VIEWER)
openProjectByName(projectName, true)
openEditingModeMenu()
cy.findByRole('menuitem', { name: 'Viewing' }).should(
'have.attr',
'aria-selected',
'true'
)
cy.findByRole('menuitem', { name: 'Editing' }).should(
'have.attr',
'aria-disabled',
'true'
)
cy.findByRole('menuitem', { name: 'Reviewing' }).should(
'have.attr',
'aria-disabled',
'true'
)
})
})
function createRandomLetterString() {
const chars = 'abcdefghijklmnopqrstuvwxyz'
let result = ''
+2 -2
View File
@@ -206,7 +206,7 @@ export function openProjectViaInviteNotification(
function shareProjectByEmail(
projectName: string,
email: string,
level: 'Viewer' | 'Editor',
level: 'Viewer' | 'Editor' | 'Reviewer',
newEditor: boolean = false
) {
openProjectByName(projectName, newEditor)
@@ -231,7 +231,7 @@ function shareProjectByEmail(
export function shareProjectByEmailAndAcceptInviteViaDash(
projectName: string,
email: string,
level: 'Viewer' | 'Editor',
level: 'Viewer' | 'Editor' | 'Reviewer',
newEditor: boolean = false
) {
shareProjectByEmail(projectName, email, level, newEditor)
@@ -585,6 +585,7 @@
"edited": "",
"editing": "",
"editing_captions": "",
"editing_mode": "",
"editor": "",
"editor_disconected_click_to_reconnect": "",
"editor_font_family": "",
@@ -592,6 +593,7 @@
"editor_limit_exceeded_in_this_project": "",
"editor_line_height": "",
"editor_only": "",
"editor_settings": "",
"editor_theme": "",
"editor_theme_dark": "",
"editor_theme_light": "",
@@ -1453,6 +1455,7 @@
"pdf_unavailable_for_download": "",
"pdf_viewer": "",
"pdf_viewer_error": "",
"pdf_zoom": "",
"pdf_zoom_level": "",
"pending": "",
"pending_additional_licenses": "",
@@ -26,6 +26,7 @@ import EditorCloneProjectModalWrapper from '@/features/clone-project-modal/compo
import useOpenProject from '@/shared/hooks/use-open-project'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import ReviewModeOptions from './review-mode-options'
const menubarExtraComponents = importOverleafModules(
'menubarExtraComponents'
@@ -205,11 +206,17 @@ export const ToolbarMenuBar = () => {
id: 'pdf-controls',
children: [
'view-pdf-presentation-mode',
{
id: 'pdf-zoom-control-group',
title: t('pdf_zoom'),
children: [
'view-pdf-zoom-in',
'view-pdf-zoom-out',
'view-pdf-fit-width',
'view-pdf-fit-height',
],
},
],
}),
[t]
)
@@ -262,8 +269,9 @@ export const ToolbarMenuBar = () => {
className="ide-redesign-toolbar-dropdown-toggle-subdued ide-redesign-toolbar-button-subdued"
>
<ChangeLayoutOptions />
<ReviewModeOptions />
<DropdownDivider />
<DropdownHeader>Editor settings</DropdownHeader>
<DropdownHeader>{t('editor_settings')}</DropdownHeader>
<MenuBarOption
eventKey="show_breadcrumbs"
title={t('show_breadcrumbs')}
@@ -0,0 +1,105 @@
import { DropdownDivider } from '@/shared/components/dropdown/dropdown-menu'
import { useTranslation } from 'react-i18next'
import { useEditorPropertiesContext } from '../../context/editor-properties-context'
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
import { sendMB } from '@/infrastructure/event-tracking'
import { useIdeReactContext } from '../../context/ide-react-context'
import { usePermissionsContext } from '../../context/permissions-context'
import { useProjectContext } from '@/shared/context/project-context'
import { useEditorContext } from '@/shared/context/editor-context'
import { NestedMenuBarDropdown } from '@/shared/components/menu-bar/menu-bar-dropdown'
import { useUserContext } from '@/shared/context/user-context'
function getMode(permissionsLevel: string, wantTrackChanges: boolean) {
if (permissionsLevel === 'readOnly') {
return 'view'
}
if (permissionsLevel === 'review') {
return 'review'
}
if (wantTrackChanges) {
return 'review'
}
return 'edit'
}
const ReviewModeOptions: React.FC = () => {
const { t } = useTranslation()
const { wantTrackChanges } = useEditorPropertiesContext()
const { write, trackedWrite } = usePermissionsContext()
const { permissionsLevel } = useIdeReactContext()
const { features } = useProjectContext()
const { setUpgradeTrackChangesModal } = useEditorContext()
const user = useUserContext()
const mode = getMode(permissionsLevel, wantTrackChanges)
const showViewOption = mode === 'view'
if (!features.trackChangesVisible) {
return null
}
return (
<>
<DropdownDivider />
<NestedMenuBarDropdown id="editing-mode-group" title={t('editing_mode')}>
<OLDropdownMenuItem
as="button"
disabled={!write || !user.id}
onClick={() => {
if (mode === 'edit') {
return
}
sendMB('editing-mode-change', {
role: permissionsLevel,
previousMode: mode,
newMode: 'edit',
})
window.dispatchEvent(new Event('toggle-track-changes'))
}}
leadingIcon="edit"
active={write && mode === 'edit'}
>
{t('editing')}
</OLDropdownMenuItem>
<OLDropdownMenuItem
as="button"
disabled={permissionsLevel === 'readOnly' || !user.id}
onClick={() => {
if (mode === 'review') {
return
}
if (!features.trackChanges) {
setUpgradeTrackChangesModal({
show: true,
location: 'menu-bar',
})
} else {
sendMB('editing-mode-change', {
role: permissionsLevel,
previousMode: mode,
newMode: mode,
})
window.dispatchEvent(new Event('toggle-track-changes'))
}
}}
leadingIcon="rate_review"
active={trackedWrite && mode === 'review'}
>
{t('reviewing')}
</OLDropdownMenuItem>
{showViewOption && (
<OLDropdownMenuItem
as="button"
leadingIcon="visibility"
active={mode === 'view'}
>
{t('viewing')}
</OLDropdownMenuItem>
)}
</NestedMenuBarDropdown>
</>
)
}
export default ReviewModeOptions
+3
View File
@@ -761,6 +761,7 @@
"editing": "Editing",
"editing_and_collaboration": "Editing and collaboration",
"editing_captions": "Editing captions",
"editing_mode": "Editing mode",
"editing_tools": "Editing tools",
"editor": "Editor",
"editor_disconected_click_to_reconnect": "Editor disconnected, click anywhere to reconnect.",
@@ -769,6 +770,7 @@
"editor_limit_exceeded_in_this_project": "Too many editors in this project",
"editor_line_height": "Editor line height",
"editor_only": "Editor only",
"editor_settings": "Editor settings",
"editor_theme": "Editor theme",
"editor_theme_dark": "Dark editor theme",
"editor_theme_light": "Light editor theme",
@@ -1911,6 +1913,7 @@
"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.",
"pdf_zoom": "PDF zoom",
"pdf_zoom_level": "PDF zoom level",
"pending": "Pending",
"pending_additional_licenses": "Your subscription is changing to include <0>__pendingAdditionalLicenses__</0> additional license(s) for a total of <1>__pendingTotalLicenses__</1> licenses.",