From d457aa82390a7a5df9bf03e42ffc30ee676cb0e6 Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:01:41 +0000 Subject: [PATCH] Merge pull request #22398 from overleaf/dp-change-edit-mode-ui-part-2 Add ReviewModeSwitcher GitOrigin-RevId: bfbbdac30530d859da0e8b5673357ba805b100ab --- .../web/frontend/extracted-translations.json | 5 + .../components/review-mode-switcher.tsx | 171 ++++++++++++++++++ .../components/review-panel-container.tsx | 7 +- .../context/track-changes-state-context.tsx | 44 +++-- .../js/shared/context/editor-context.tsx | 2 +- .../bootstrap-5/components/dropdown-menu.scss | 1 + .../pages/editor/review-panel-new.scss | 72 ++++++++ services/web/locales/en.json | 5 + 8 files changed, 294 insertions(+), 13 deletions(-) create mode 100644 services/web/frontend/js/features/review-panel-new/components/review-mode-switcher.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 3c50ef96ef..85d472ed56 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -171,12 +171,15 @@ "bulk_reject_confirm": "", "buy_overleaf_assist": "", "by_subscribing_you_agree_to_our_terms_of_service": "", + "can_add_tracked_changes_and_comments": "", "can_edit": "", + "can_edit_content": "", "can_link_institution_email_acct_to_institution_acct": "", "can_link_your_institution_acct_2": "", "can_now_relink_dropbox": "", "can_review": "", "can_view": "", + "can_view_content": "", "cancel": "", "cancel_add_on": "", "cancel_anytime": "", @@ -1300,6 +1303,7 @@ "review": "", "review_your_peers_work": "", "reviewer": "", + "reviewing": "", "revoke": "", "revoke_invite": "", "right": "", @@ -1839,6 +1843,7 @@ "view_pdf": "", "view_your_invoices": "", "viewer": "", + "viewing": "", "viewing_x": "", "visual_editor": "", "visual_editor_is_only_available_for_tex_files": "", diff --git a/services/web/frontend/js/features/review-panel-new/components/review-mode-switcher.tsx b/services/web/frontend/js/features/review-panel-new/components/review-mode-switcher.tsx new file mode 100644 index 0000000000..d1956d8d2f --- /dev/null +++ b/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 ( +
+ + + + { + saveTrackChangesForCurrentUser(false) + }} + description={t('can_edit_content')} + leadingIcon="edit" + active={enableEditing && mode === 'editing'} + > + {t('editing')} + + { + saveTrackChangesForCurrentUser(true) + }} + description={t('can_add_tracked_changes_and_comments')} + leadingIcon="rate_review" + active={enableReviewing && mode === 'reviewing'} + > + {t('reviewing')} + + {showViewOption && ( + { + saveTrackChangesForCurrentUser(true) + }} + description={t('can_view_content')} + leadingIcon="visibility" + active={mode === 'viewing'} + > + {t('viewing')} + + )} + + +
+ ) +} + +const ModeSwitcherToggleButton = forwardRef< + HTMLButtonElement, + { onClick: MouseEventHandler; 'aria-expanded': boolean } +>(({ onClick, 'aria-expanded': ariaExpanded }, ref) => { + const { t } = useTranslation() + const mode = useCurrentMode() + + if (mode === 'editing') { + return ( + + ) + } else if (mode === 'reviewing') { + return ( + + ) + } + + return ( + + ) +}) + +const ModeSwitcherToggleButtonContent = forwardRef< + HTMLButtonElement, + { + onClick: MouseEventHandler + className: string + iconType: string + label: string + ariaExpanded: boolean + } +>(({ onClick, className, iconType, label, ariaExpanded }, ref) => { + return ( + + ) +}) + +ModeSwitcherToggleButton.displayName = 'ModeSwitcherToggleButton' +ModeSwitcherToggleButtonContent.displayName = 'ModeSwitcherToggleButtonContent' + +export default memo(ReviewModeSwitcher) diff --git a/services/web/frontend/js/features/review-panel-new/components/review-panel-container.tsx b/services/web/frontend/js/features/review-panel-new/components/review-panel-container.tsx index 93be6ab439..9ee8bc4062 100644 --- a/services/web/frontend/js/features/review-panel-new/components/review-panel-container.tsx +++ b/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 TrackChangesOnWidget from './track-changes-on-widget' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' +import ReviewModeSwitcher from './review-mode-switcher' +import getMeta from '@/utils/meta' function ReviewPanelContainer() { const view = useCodeMirrorViewContext() @@ -15,6 +17,7 @@ function ReviewPanelContainer() { const threads = useThreadsContext() const { reviewPanelOpen } = useLayoutContext() const { wantTrackChanges } = useEditorManagerContext() + const enableReviewerRole = getMeta('ol-isReviewerRoleEnabled') if (!view) { return null @@ -22,11 +25,13 @@ function ReviewPanelContainer() { const hasCommentOrChange = hasActiveRange(ranges, threads) const showPanel = reviewPanelOpen || hasCommentOrChange - const showTrackChangesWidget = wantTrackChanges && !reviewPanelOpen + const showTrackChangesWidget = + !enableReviewerRole && wantTrackChanges && !reviewPanelOpen return ReactDOM.createPortal( <> {showTrackChangesWidget && } + {enableReviewerRole && } {showPanel && } , view.scrollDOM diff --git a/services/web/frontend/js/features/review-panel-new/context/track-changes-state-context.tsx b/services/web/frontend/js/features/review-panel-new/context/track-changes-state-context.tsx index e6f1783be8..d47da8c10a 100644 --- a/services/web/frontend/js/features/review-panel-new/context/track-changes-state-context.tsx +++ b/services/web/frontend/js/features/review-panel-new/context/track-changes-state-context.tsx @@ -36,6 +36,7 @@ type SaveTrackChangesRequestBody = { type TrackChangesStateActions = { saveTrackChanges: (trackChangesBody: SaveTrackChangesRequestBody) => void + saveTrackChangesForCurrentUser: (trackChanges: boolean) => void } const TrackChangesStateActionsContext = createContext< @@ -64,17 +65,6 @@ export const TrackChangesStateProvider: FC = ({ children }) => { ) }, [setWantTrackChanges, trackChangesValue, user.id]) - const actions = useMemo( - () => ({ - async saveTrackChanges(trackChangesBody: SaveTrackChangesRequestBody) { - postJSON(`/project/${project._id}/track_changes`, { - body: trackChangesBody, - }) - }, - }), - [project._id] - ) - const trackChangesIsObject = trackChangesValue !== true && trackChangesValue !== false const onForEveryone = trackChangesValue === true @@ -94,6 +84,38 @@ export const TrackChangesStateProvider: FC = ({ children }) => { return onForMembers }, [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( 'toggle-track-changes', useCallback(() => { diff --git a/services/web/frontend/js/shared/context/editor-context.tsx b/services/web/frontend/js/shared/context/editor-context.tsx index ac1c698ec3..beeb155358 100644 --- a/services/web/frontend/js/shared/context/editor-context.tsx +++ b/services/web/frontend/js/shared/context/editor-context.tsx @@ -43,7 +43,7 @@ export const EditorContext = createContext< isProjectOwner: boolean isRestrictedTokenMember?: boolean isPendingEditor: boolean - permissionsLevel: 'readOnly' | 'readAndWrite' | 'owner' + permissionsLevel: PermissionsLevel deactivateTutorial: (tutorial: string) => void inactiveTutorials: string[] currentPopup: string | null diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss index b603c0093e..f312630bef 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss @@ -109,6 +109,7 @@ color: var(--content-secondary); margin-top: var(--spacing-01); + text-wrap: wrap; } .dropdown-item-description-container { diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss index 279407c2bb..9803329db7 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/review-panel-new.scss @@ -768,3 +768,75 @@ 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; + } +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index d6259fbd96..bc2ed19165 100644 --- a/services/web/locales/en.json +++ b/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 and <1>privacy notice.", "by_registering_you_agree_to_our_terms_of_service": "By registering, you agree to our <0>terms of service and <1>privacy notice.", "by_subscribing_you_agree_to_our_terms_of_service": "By subscribing, you agree to our <0>terms of service.", + "can_add_tracked_changes_and_comments": "Can add tracked changes and comments", "can_edit": "Can edit", + "can_edit_content": "Can edit content", "can_link_institution_email_acct_to_institution_acct": "You can now link your __email__ __appName__ account to your __institutionName__ institutional account.", "can_link_institution_email_by_clicking": "You can link your __email__ __appName__ account to your __institutionName__ account by clicking __clickText__.", "can_link_institution_email_to_login": "You can link your __email__ __appName__ account to your __institutionName__ account, which will allow you to log in to __appName__ 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.", "can_review": "Can review", "can_view": "Can view", + "can_view_content": "Can view content", "cancel": "Cancel", "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.", @@ -1825,6 +1828,7 @@ "review": "Review", "review_your_peers_work": "Review your peers’ work", "reviewer": "Reviewer", + "reviewing": "Reviewing", "revoke": "Revoke", "revoke_invite": "Revoke Invite", "right": "Right", @@ -2482,6 +2486,7 @@ "view_source": "View Source", "view_your_invoices": "View your invoices", "viewer": "Viewer", + "viewing": "Viewing", "viewing_x": "Viewing <0>__endTime__", "visual_editor": "Visual Editor", "visual_editor_is_only_available_for_tex_files": "Visual Editor is only available for TeX files",