Kaynağa Gözat

[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
Mathias Jakobsen 1 ay önce
ebeveyn
işleme
b7f984fc8d

+ 120 - 0
server-ce/test/editor.spec.ts

@@ -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
server-ce/test/helpers/project.ts

@@ -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)

+ 3 - 0
services/web/frontend/extracted-translations.json

@@ -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": "",

+ 13 - 5
services/web/frontend/js/features/ide-react/components/toolbar/menu-bar.tsx

@@ -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,10 +206,16 @@ export const ToolbarMenuBar = () => {
       id: 'pdf-controls',
       children: [
         'view-pdf-presentation-mode',
-        'view-pdf-zoom-in',
-        'view-pdf-zoom-out',
-        'view-pdf-fit-width',
-        'view-pdf-fit-height',
+        {
+          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')}

+ 105 - 0
services/web/frontend/js/features/ide-react/components/toolbar/review-mode-options.tsx

@@ -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 - 0
services/web/locales/en.json

@@ -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.",