mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-27 02:51:57 +02:00
Merge pull request #22398 from overleaf/dp-change-edit-mode-ui-part-2
Add ReviewModeSwitcher GitOrigin-RevId: bfbbdac30530d859da0e8b5673357ba805b100ab
This commit is contained in:
@@ -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": "",
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
|
||||
color: var(--content-secondary);
|
||||
margin-top: var(--spacing-01);
|
||||
text-wrap: wrap;
|
||||
}
|
||||
|
||||
.dropdown-item-description-container {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "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__</0>",
|
||||
"visual_editor": "Visual Editor",
|
||||
"visual_editor_is_only_available_for_tex_files": "Visual Editor is only available for TeX files",
|
||||
|
||||
Reference in New Issue
Block a user