Sfoglia il codice sorgente

Merge pull request #22398 from overleaf/dp-change-edit-mode-ui-part-2

Add ReviewModeSwitcher

GitOrigin-RevId: bfbbdac30530d859da0e8b5673357ba805b100ab
David 1 anno fa
parent
commit
f8a1e7c43b

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

@@ -171,12 +171,15 @@
   "bulk_reject_confirm": "",
   "bulk_reject_confirm": "",
   "buy_overleaf_assist": "",
   "buy_overleaf_assist": "",
   "by_subscribing_you_agree_to_our_terms_of_service": "",
   "by_subscribing_you_agree_to_our_terms_of_service": "",
+  "can_add_tracked_changes_and_comments": "",
   "can_edit": "",
   "can_edit": "",
+  "can_edit_content": "",
   "can_link_institution_email_acct_to_institution_acct": "",
   "can_link_institution_email_acct_to_institution_acct": "",
   "can_link_your_institution_acct_2": "",
   "can_link_your_institution_acct_2": "",
   "can_now_relink_dropbox": "",
   "can_now_relink_dropbox": "",
   "can_review": "",
   "can_review": "",
   "can_view": "",
   "can_view": "",
+  "can_view_content": "",
   "cancel": "",
   "cancel": "",
   "cancel_add_on": "",
   "cancel_add_on": "",
   "cancel_anytime": "",
   "cancel_anytime": "",
@@ -1300,6 +1303,7 @@
   "review": "",
   "review": "",
   "review_your_peers_work": "",
   "review_your_peers_work": "",
   "reviewer": "",
   "reviewer": "",
+  "reviewing": "",
   "revoke": "",
   "revoke": "",
   "revoke_invite": "",
   "revoke_invite": "",
   "right": "",
   "right": "",
@@ -1839,6 +1843,7 @@
   "view_pdf": "",
   "view_pdf": "",
   "view_your_invoices": "",
   "view_your_invoices": "",
   "viewer": "",
   "viewer": "",
+  "viewing": "",
   "viewing_x": "",
   "viewing_x": "",
   "visual_editor": "",
   "visual_editor": "",
   "visual_editor_is_only_available_for_tex_files": "",
   "visual_editor_is_only_available_for_tex_files": "",

+ 171 - 0
services/web/frontend/js/features/review-panel-new/components/review-mode-switcher.tsx

@@ -0,0 +1,171 @@
+import { forwardRef, memo, MouseEventHandler } from 'react'
+import {
+  Dropdown,
+  DropdownMenu,
+  DropdownToggle,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
+import MaterialIcon from '@/shared/components/material-icon'
+import classNames from 'classnames'
+import {
+  useTrackChangesStateActionsContext,
+  useTrackChangesStateContext,
+} from '../context/track-changes-state-context'
+import { useUserContext } from '@/shared/context/user-context'
+import { useTranslation } from 'react-i18next'
+import { useEditorContext } from '@/shared/context/editor-context'
+import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
+
+type Mode = 'viewing' | 'reviewing' | 'editing'
+
+const useCurrentMode = (): Mode => {
+  const trackChanges = useTrackChangesStateContext()
+  const user = useUserContext()
+  const trackChangesForCurrentUser =
+    trackChanges?.onForEveryone ||
+    (user && user.id && trackChanges?.onForMembers[user.id])
+  const { write } = usePermissionsContext()
+
+  if (write && !trackChangesForCurrentUser) {
+    return 'editing'
+  } else if (write) {
+    return 'reviewing'
+  }
+
+  return 'viewing'
+}
+
+function ReviewModeSwitcher() {
+  const { t } = useTranslation()
+  const { saveTrackChangesForCurrentUser } =
+    useTrackChangesStateActionsContext()
+  const mode = useCurrentMode()
+
+  const { permissionsLevel } = useEditorContext()
+
+  const enableEditing =
+    permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite'
+  const enableReviewing = enableEditing || permissionsLevel === 'review'
+  const showViewOption = !enableReviewing
+
+  return (
+    <div className="review-mode-switcher-container">
+      <Dropdown className="review-mode-switcher" align="end">
+        <DropdownToggle
+          as={ModeSwitcherToggleButton}
+          id="review-mode-switcher"
+        />
+        <DropdownMenu flip={false}>
+          <OLDropdownMenuItem
+            disabled={!enableEditing}
+            onClick={() => {
+              saveTrackChangesForCurrentUser(false)
+            }}
+            description={t('can_edit_content')}
+            leadingIcon="edit"
+            active={enableEditing && mode === 'editing'}
+          >
+            {t('editing')}
+          </OLDropdownMenuItem>
+          <OLDropdownMenuItem
+            disabled={!enableReviewing}
+            onClick={() => {
+              saveTrackChangesForCurrentUser(true)
+            }}
+            description={t('can_add_tracked_changes_and_comments')}
+            leadingIcon="rate_review"
+            active={enableReviewing && mode === 'reviewing'}
+          >
+            {t('reviewing')}
+          </OLDropdownMenuItem>
+          {showViewOption && (
+            <OLDropdownMenuItem
+              onClick={() => {
+                saveTrackChangesForCurrentUser(true)
+              }}
+              description={t('can_view_content')}
+              leadingIcon="visibility"
+              active={mode === 'viewing'}
+            >
+              {t('viewing')}
+            </OLDropdownMenuItem>
+          )}
+        </DropdownMenu>
+      </Dropdown>
+    </div>
+  )
+}
+
+const ModeSwitcherToggleButton = forwardRef<
+  HTMLButtonElement,
+  { onClick: MouseEventHandler<HTMLButtonElement>; 'aria-expanded': boolean }
+>(({ onClick, 'aria-expanded': ariaExpanded }, ref) => {
+  const { t } = useTranslation()
+  const mode = useCurrentMode()
+
+  if (mode === 'editing') {
+    return (
+      <ModeSwitcherToggleButtonContent
+        ref={ref}
+        onClick={onClick}
+        className="editing"
+        iconType="edit"
+        label={t('editing')}
+        ariaExpanded={ariaExpanded}
+      />
+    )
+  } else if (mode === 'reviewing') {
+    return (
+      <ModeSwitcherToggleButtonContent
+        ref={ref}
+        onClick={onClick}
+        className="reviewing"
+        iconType="rate_review"
+        label={t('reviewing')}
+        ariaExpanded={ariaExpanded}
+      />
+    )
+  }
+
+  return (
+    <ModeSwitcherToggleButtonContent
+      ref={ref}
+      onClick={onClick}
+      className="viewing"
+      iconType="visibility"
+      label={t('viewing')}
+      ariaExpanded={ariaExpanded}
+    />
+  )
+})
+
+const ModeSwitcherToggleButtonContent = forwardRef<
+  HTMLButtonElement,
+  {
+    onClick: MouseEventHandler<HTMLButtonElement>
+    className: string
+    iconType: string
+    label: string
+    ariaExpanded: boolean
+  }
+>(({ onClick, className, iconType, label, ariaExpanded }, ref) => {
+  return (
+    <button
+      className={classNames('review-mode-switcher-toggle-button', className)}
+      ref={ref}
+      onClick={onClick}
+      aria-expanded={ariaExpanded}
+    >
+      <MaterialIcon className="material-symbols-outlined" type={iconType} />
+      <div className="review-mode-switcher-toggle-label" aria-label={label}>
+        {label}
+      </div>
+      <MaterialIcon type="keyboard_arrow_down" />
+    </button>
+  )
+})
+
+ModeSwitcherToggleButton.displayName = 'ModeSwitcherToggleButton'
+ModeSwitcherToggleButtonContent.displayName = 'ModeSwitcherToggleButtonContent'
+
+export default memo(ReviewModeSwitcher)

+ 6 - 1
services/web/frontend/js/features/review-panel-new/components/review-panel-container.tsx

@@ -8,6 +8,8 @@ import { useThreadsContext } from '@/features/review-panel-new/context/threads-c
 import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range'
 import { hasActiveRange } from '@/features/review-panel-new/utils/has-active-range'
 import TrackChangesOnWidget from './track-changes-on-widget'
 import TrackChangesOnWidget from './track-changes-on-widget'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
+import ReviewModeSwitcher from './review-mode-switcher'
+import getMeta from '@/utils/meta'
 
 
 function ReviewPanelContainer() {
 function ReviewPanelContainer() {
   const view = useCodeMirrorViewContext()
   const view = useCodeMirrorViewContext()
@@ -15,6 +17,7 @@ function ReviewPanelContainer() {
   const threads = useThreadsContext()
   const threads = useThreadsContext()
   const { reviewPanelOpen } = useLayoutContext()
   const { reviewPanelOpen } = useLayoutContext()
   const { wantTrackChanges } = useEditorManagerContext()
   const { wantTrackChanges } = useEditorManagerContext()
+  const enableReviewerRole = getMeta('ol-isReviewerRoleEnabled')
 
 
   if (!view) {
   if (!view) {
     return null
     return null
@@ -22,11 +25,13 @@ function ReviewPanelContainer() {
 
 
   const hasCommentOrChange = hasActiveRange(ranges, threads)
   const hasCommentOrChange = hasActiveRange(ranges, threads)
   const showPanel = reviewPanelOpen || hasCommentOrChange
   const showPanel = reviewPanelOpen || hasCommentOrChange
-  const showTrackChangesWidget = wantTrackChanges && !reviewPanelOpen
+  const showTrackChangesWidget =
+    !enableReviewerRole && wantTrackChanges && !reviewPanelOpen
 
 
   return ReactDOM.createPortal(
   return ReactDOM.createPortal(
     <>
     <>
       {showTrackChangesWidget && <TrackChangesOnWidget />}
       {showTrackChangesWidget && <TrackChangesOnWidget />}
+      {enableReviewerRole && <ReviewModeSwitcher />}
       {showPanel && <ReviewPanel mini={!reviewPanelOpen} />}
       {showPanel && <ReviewPanel mini={!reviewPanelOpen} />}
     </>,
     </>,
     view.scrollDOM
     view.scrollDOM

+ 33 - 11
services/web/frontend/js/features/review-panel-new/context/track-changes-state-context.tsx

@@ -36,6 +36,7 @@ type SaveTrackChangesRequestBody = {
 
 
 type TrackChangesStateActions = {
 type TrackChangesStateActions = {
   saveTrackChanges: (trackChangesBody: SaveTrackChangesRequestBody) => void
   saveTrackChanges: (trackChangesBody: SaveTrackChangesRequestBody) => void
+  saveTrackChangesForCurrentUser: (trackChanges: boolean) => void
 }
 }
 
 
 const TrackChangesStateActionsContext = createContext<
 const TrackChangesStateActionsContext = createContext<
@@ -64,17 +65,6 @@ export const TrackChangesStateProvider: FC = ({ children }) => {
     )
     )
   }, [setWantTrackChanges, trackChangesValue, user.id])
   }, [setWantTrackChanges, trackChangesValue, user.id])
 
 
-  const actions = useMemo(
-    () => ({
-      async saveTrackChanges(trackChangesBody: SaveTrackChangesRequestBody) {
-        postJSON(`/project/${project._id}/track_changes`, {
-          body: trackChangesBody,
-        })
-      },
-    }),
-    [project._id]
-  )
-
   const trackChangesIsObject =
   const trackChangesIsObject =
     trackChangesValue !== true && trackChangesValue !== false
     trackChangesValue !== true && trackChangesValue !== false
   const onForEveryone = trackChangesValue === true
   const onForEveryone = trackChangesValue === true
@@ -94,6 +84,38 @@ export const TrackChangesStateProvider: FC = ({ children }) => {
     return onForMembers
     return onForMembers
   }, [trackChangesIsObject, trackChangesValue])
   }, [trackChangesIsObject, trackChangesValue])
 
 
+  const saveTrackChanges = useCallback(
+    async (trackChangesBody: SaveTrackChangesRequestBody) => {
+      postJSON(`/project/${project._id}/track_changes`, {
+        body: trackChangesBody,
+      })
+    },
+    [project._id]
+  )
+
+  const saveTrackChangesForCurrentUser = useCallback(
+    async (trackChanges: boolean) => {
+      if (user.id) {
+        saveTrackChanges({
+          on_for: {
+            ...onForMembers,
+            [user.id]: trackChanges,
+          },
+          on_for_guests: onForGuests,
+        })
+      }
+    },
+    [onForMembers, onForGuests, user.id, saveTrackChanges]
+  )
+
+  const actions = useMemo(
+    () => ({
+      saveTrackChanges,
+      saveTrackChangesForCurrentUser,
+    }),
+    [saveTrackChanges, saveTrackChangesForCurrentUser]
+  )
+
   useEventListener(
   useEventListener(
     'toggle-track-changes',
     'toggle-track-changes',
     useCallback(() => {
     useCallback(() => {

+ 1 - 1
services/web/frontend/js/shared/context/editor-context.tsx

@@ -43,7 +43,7 @@ export const EditorContext = createContext<
       isProjectOwner: boolean
       isProjectOwner: boolean
       isRestrictedTokenMember?: boolean
       isRestrictedTokenMember?: boolean
       isPendingEditor: boolean
       isPendingEditor: boolean
-      permissionsLevel: 'readOnly' | 'readAndWrite' | 'owner'
+      permissionsLevel: PermissionsLevel
       deactivateTutorial: (tutorial: string) => void
       deactivateTutorial: (tutorial: string) => void
       inactiveTutorials: string[]
       inactiveTutorials: string[]
       currentPopup: string | null
       currentPopup: string | null

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss

@@ -109,6 +109,7 @@
 
 
   color: var(--content-secondary);
   color: var(--content-secondary);
   margin-top: var(--spacing-01);
   margin-top: var(--spacing-01);
+  text-wrap: wrap;
 }
 }
 
 
 .dropdown-item-description-container {
 .dropdown-item-description-container {

+ 72 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss

@@ -768,3 +768,75 @@
     color: $rp-type-blue;
     color: $rp-type-blue;
   }
   }
 }
 }
+
+.review-mode-switcher-container {
+  position: sticky;
+  top: 0;
+  right: 0;
+}
+
+.review-mode-switcher {
+  position: absolute;
+  top: var(--spacing-03);
+  right: var(--spacing-03);
+
+  &:hover,
+  &:focus {
+    .review-mode-switcher-toggle-button.editing {
+      background-color: var(--bg-light-tertiary);
+    }
+
+    .review-mode-switcher-toggle-button.reviewing {
+      background-color: var(--yellow-20);
+    }
+
+    .review-mode-switcher-toggle-button.viewing {
+      background-color: var(--blue-20);
+    }
+
+    .review-mode-switcher-toggle-label {
+      display: block;
+    }
+  }
+}
+
+.review-mode-switcher-toggle-button {
+  all: unset;
+  z-index: 2;
+  font-family: $font-family-base;
+  display: flex;
+  align-items: center;
+  border-radius: 14px;
+  font-size: var(--font-size-02);
+  padding: var(--spacing-02) var(--spacing-03);
+  gap: var(--spacing-02);
+  height: 20px;
+
+  .material-symbols {
+    font-size: 16px;
+    font-variation-settings:
+      'FILL' 0,
+      'wght' 400,
+      'GRAD' 0,
+      'opsz' 20;
+  }
+
+  &.editing {
+    background-color: var(--bg-light-secondary);
+    color: var(--content-primary);
+  }
+
+  &.reviewing {
+    background-color: var(--yellow-10);
+    color: var(--yellow-60);
+  }
+
+  &.viewing {
+    background-color: var(--blue-10);
+    color: var(--blue-70);
+  }
+
+  .review-mode-switcher-toggle-label {
+    display: none;
+  }
+}

+ 5 - 0
services/web/locales/en.json

@@ -233,7 +233,9 @@
   "by_joining_labs": "By joining Labs, you agree to receive occasional emails and updates from Overleaf—for example, to request your feedback. You also agree to our <0>terms of service</0> and <1>privacy notice</1>.",
   "by_joining_labs": "By joining Labs, you agree to receive occasional emails and updates from Overleaf—for example, to request your feedback. You also agree to our <0>terms of service</0> and <1>privacy notice</1>.",
   "by_registering_you_agree_to_our_terms_of_service": "By registering, you agree to our <0>terms of service</0> and <1>privacy notice</1>.",
   "by_registering_you_agree_to_our_terms_of_service": "By registering, you agree to our <0>terms of service</0> and <1>privacy notice</1>.",
   "by_subscribing_you_agree_to_our_terms_of_service": "By subscribing, you agree to our <0>terms of service</0>.",
   "by_subscribing_you_agree_to_our_terms_of_service": "By subscribing, you agree to our <0>terms of service</0>.",
+  "can_add_tracked_changes_and_comments": "Can add tracked changes and comments",
   "can_edit": "Can edit",
   "can_edit": "Can edit",
+  "can_edit_content": "Can edit content",
   "can_link_institution_email_acct_to_institution_acct": "You can now link your <b>__email__</b> <b>__appName__</b> account to your <b>__institutionName__</b> institutional account.",
   "can_link_institution_email_acct_to_institution_acct": "You can now link your <b>__email__</b> <b>__appName__</b> account to your <b>__institutionName__</b> institutional account.",
   "can_link_institution_email_by_clicking": "You can link your <b>__email__</b> <b>__appName__</b> account to your  <b>__institutionName__</b> account by clicking <b>__clickText__</b>.",
   "can_link_institution_email_by_clicking": "You can link your <b>__email__</b> <b>__appName__</b> account to your  <b>__institutionName__</b> account by clicking <b>__clickText__</b>.",
   "can_link_institution_email_to_login": "You can link your <b>__email__</b> <b>__appName__</b> account to your <b>__institutionName__</b> account, which will allow you to log in to <b>__appName__</b> through your institution and will reconfirm your institutional email address.",
   "can_link_institution_email_to_login": "You can link your <b>__email__</b> <b>__appName__</b> account to your <b>__institutionName__</b> account, which will allow you to log in to <b>__appName__</b> through your institution and will reconfirm your institutional email address.",
@@ -241,6 +243,7 @@
   "can_now_relink_dropbox": "You can now <0>relink your Dropbox account</0>.",
   "can_now_relink_dropbox": "You can now <0>relink your Dropbox account</0>.",
   "can_review": "Can review",
   "can_review": "Can review",
   "can_view": "Can view",
   "can_view": "Can view",
+  "can_view_content": "Can view content",
   "cancel": "Cancel",
   "cancel": "Cancel",
   "cancel_add_on": "Cancel add-on",
   "cancel_add_on": "Cancel add-on",
   "cancel_anytime": "We’re confident that you’ll love __appName__, but if not you can cancel anytime. We’ll give you your money back, no questions asked, if you let us know within 30 days.",
   "cancel_anytime": "We’re confident that you’ll love __appName__, but if not you can cancel anytime. We’ll give you your money back, no questions asked, if you let us know within 30 days.",
@@ -1825,6 +1828,7 @@
   "review": "Review",
   "review": "Review",
   "review_your_peers_work": "Review your peers’ work",
   "review_your_peers_work": "Review your peers’ work",
   "reviewer": "Reviewer",
   "reviewer": "Reviewer",
+  "reviewing": "Reviewing",
   "revoke": "Revoke",
   "revoke": "Revoke",
   "revoke_invite": "Revoke Invite",
   "revoke_invite": "Revoke Invite",
   "right": "Right",
   "right": "Right",
@@ -2482,6 +2486,7 @@
   "view_source": "View Source",
   "view_source": "View Source",
   "view_your_invoices": "View your invoices",
   "view_your_invoices": "View your invoices",
   "viewer": "Viewer",
   "viewer": "Viewer",
+  "viewing": "Viewing",
   "viewing_x": "Viewing <0>__endTime__</0>",
   "viewing_x": "Viewing <0>__endTime__</0>",
   "visual_editor": "Visual Editor",
   "visual_editor": "Visual Editor",
   "visual_editor_is_only_available_for_tex_files": "Visual Editor is only available for TeX files",
   "visual_editor_is_only_available_for_tex_files": "Visual Editor is only available for TeX files",