overleaf-cep/services/web/frontend/js/features/review-panel-new/components/review-mode-switcher.tsx
Tim Down 905cc5d45f Move project context out of scope value store (#26615)
* Refactor project context to not use scope store

* Fix Cypress tests for project context changes

* Fix frontend React Testing Library tests for project context changes

* Remove redundant code

* Fix some project types in tests

* Remove unused import and fix a type

* Throw an error if updating the project in the project context before joining the project

* Fix some review panel tests

* Remove unused imports

GitOrigin-RevId: 2f0c928b651f387aa980c29aef7d1ba0649790a7
2025-07-10 08:06:31 +00:00

274 lines
8.5 KiB
TypeScript

import { forwardRef, memo, MouseEventHandler, useRef, useState } 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 { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import usePersistedState from '@/shared/hooks/use-persisted-state'
import { sendMB } from '@/infrastructure/event-tracking'
import { useEditorContext } from '@/shared/context/editor-context'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { useProjectContext } from '@/shared/context/project-context'
import UpgradeTrackChangesModal from './upgrade-track-changes-modal'
import { ReviewModePromo } from '@/features/review-panel-new/components/review-mode-promo'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useCodeMirrorViewContext } from '@/features/source-editor/components/codemirror-context'
type Mode = 'view' | 'review' | 'edit'
const useCurrentMode = (): Mode => {
const trackChanges = useTrackChangesStateContext()
const user = useUserContext()
const trackChangesForCurrentUser =
trackChanges?.onForEveryone ||
(user?.id && trackChanges?.onForMembers[user.id]) ||
(!user?.id && trackChanges?.onForGuests)
const { permissionsLevel } = useIdeReactContext()
if (permissionsLevel === 'readOnly') {
return 'view'
} else if (permissionsLevel === 'review') {
return 'review'
} else if (trackChangesForCurrentUser) {
return 'review'
} else {
return 'edit'
}
}
function ReviewModeSwitcher() {
const { t } = useTranslation()
const user = useUserContext()
const { saveTrackChangesForCurrentUser, saveTrackChanges } =
useTrackChangesStateActionsContext()
const mode = useCurrentMode()
const { permissionsLevel } = useIdeReactContext()
const { write, trackedWrite } = usePermissionsContext()
const { features } = useProjectContext()
const [showUpgradeModal, setShowUpgradeModal] = useState(false)
const showViewOption = permissionsLevel === 'readOnly'
const view = useCodeMirrorViewContext()
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={!write}
onClick={() => {
if (mode === 'edit') {
view.focus()
return
}
sendMB('editing-mode-change', {
role: permissionsLevel,
previousMode: mode,
newMode: 'edit',
})
if (user?.id) {
saveTrackChangesForCurrentUser(false)
} else {
saveTrackChanges({ on_for_guests: false })
}
view.focus()
}}
description={t('edit_content_directly')}
leadingIcon="edit"
active={write && mode === 'edit'}
>
{t('editing')}
</OLDropdownMenuItem>
<OLDropdownMenuItem
disabled={permissionsLevel === 'readOnly'}
onClick={() => {
if (mode === 'review') {
view.focus()
return
}
if (!features.trackChanges) {
setShowUpgradeModal(true)
} else {
sendMB('editing-mode-change', {
role: permissionsLevel,
previousMode: mode,
newMode: 'review',
})
if (user?.id) {
saveTrackChangesForCurrentUser(true)
} else {
saveTrackChanges({ on_for_guests: true })
}
view.focus()
}
}}
description={
permissionsLevel === 'review' && !trackedWrite
? t('comment_only')
: t('edits_become_suggestions')
}
leadingIcon="rate_review"
active={trackedWrite && mode === 'review'}
>
{t('reviewing')}
</OLDropdownMenuItem>
{showViewOption && (
<OLDropdownMenuItem
description={t('can_view_content')}
leadingIcon="visibility"
active={mode === 'view'}
>
{t('viewing')}
</OLDropdownMenuItem>
)}
</DropdownMenu>
</Dropdown>
<UpgradeTrackChangesModal
show={showUpgradeModal}
setShow={setShowUpgradeModal}
/>
</div>
)
}
const ModeSwitcherToggleButton = forwardRef<
HTMLButtonElement,
{ onClick: MouseEventHandler<HTMLButtonElement>; 'aria-expanded': boolean }
>(({ onClick, 'aria-expanded': ariaExpanded }, ref) => {
const { t } = useTranslation()
const mode = useCurrentMode()
if (mode === 'edit') {
return (
<ModeSwitcherToggleButtonContent
ref={ref}
onClick={onClick}
className="editing"
iconType="edit"
label={t('editing')}
ariaExpanded={ariaExpanded}
currentMode={mode}
/>
)
} else if (mode === 'review') {
return (
<ModeSwitcherToggleButtonContent
ref={ref}
onClick={onClick}
className="reviewing"
iconType="rate_review"
label={t('reviewing')}
ariaExpanded={ariaExpanded}
currentMode={mode}
/>
)
}
return (
<ModeSwitcherToggleButtonContent
ref={ref}
onClick={onClick}
className="viewing"
iconType="visibility"
label={t('viewing')}
ariaExpanded={ariaExpanded}
currentMode={mode}
/>
)
})
const ModeSwitcherToggleButtonContent = forwardRef<
HTMLButtonElement,
{
onClick: MouseEventHandler<HTMLButtonElement>
className: string
iconType: string
label: string
ariaExpanded: boolean
currentMode: string
}
>(({ onClick, className, iconType, label, ariaExpanded, currentMode }, ref) => {
const [isFirstTimeUsed, setIsFirstTimeUsed] = usePersistedState(
`modeSwitcherFirstTimeUsed`,
true
)
const tutorialProps = useTutorial('review-mode', {
name: 'review-mode-notification',
})
const user = useUserContext()
const { features } = useProjectContext()
const { reviewPanelOpen } = useLayoutContext()
const { inactiveTutorials } = useEditorContext()
const hasCompletedReviewModeTutorial =
inactiveTutorials.includes('review-mode')
const canShowReviewModePromo =
reviewPanelOpen &&
currentMode !== 'review' &&
features.trackChanges &&
user.signUpDate &&
user.signUpDate < '2025-03-15' &&
!hasCompletedReviewModeTutorial
const containerRef = useRef<HTMLSpanElement | null>(null)
return (
<>
<span ref={containerRef}>
<button
className={classNames(
'review-mode-switcher-toggle-button',
className,
{
'review-mode-switcher-toggle-button-expanded': isFirstTimeUsed,
}
)}
ref={ref}
onClick={event => {
setIsFirstTimeUsed(false)
if (!hasCompletedReviewModeTutorial) {
tutorialProps.completeTutorial({
action: 'complete',
event: 'promo-click',
})
}
onClick(event)
}}
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>
</span>
{canShowReviewModePromo && (
<ReviewModePromo target={containerRef} {...tutorialProps} />
)}
</>
)
})
ModeSwitcherToggleButton.displayName = 'ModeSwitcherToggleButton'
ModeSwitcherToggleButtonContent.displayName = 'ModeSwitcherToggleButtonContent'
export default memo(ReviewModeSwitcher)