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

Add ReviewModeSwitcher

GitOrigin-RevId: bfbbdac30530d859da0e8b5673357ba805b100ab
This commit is contained in:
David
2024-12-11 11:01:41 +00:00
committed by Copybot
parent f655473f3f
commit d457aa8239
8 changed files with 294 additions and 13 deletions

View File

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

View File

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

View File

@@ -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 && <TrackChangesOnWidget />}
{enableReviewerRole && <ReviewModeSwitcher />}
{showPanel && <ReviewPanel mini={!reviewPanelOpen} />}
</>,
view.scrollDOM

View File

@@ -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(() => {

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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_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>.",
"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 <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_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_review": "Can review",
"can_view": "Can view",
"can_view_content": "Can view content",
"cancel": "Cancel",
"cancel_add_on": "Cancel add-on",
"cancel_anytime": "Were confident that youll love __appName__, but if not you can cancel anytime. Well 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__</0>",
"visual_editor": "Visual Editor",
"visual_editor_is_only_available_for_tex_files": "Visual Editor is only available for TeX files",