diff --git a/services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx b/services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx index 369c964c86..f2f2c67733 100644 --- a/services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx +++ b/services/web/frontend/js/features/review-panel/components/review-mode-switcher.tsx @@ -1,4 +1,4 @@ -import { forwardRef, memo, MouseEventHandler, useState } from 'react' +import { forwardRef, memo, MouseEventHandler } from 'react' import { Dropdown, DropdownMenu, @@ -7,10 +7,7 @@ import { import OLDropdownMenuItem from '@/shared/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 { useTrackChangesStateActionsContext } 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' @@ -18,41 +15,20 @@ import usePersistedState from '@/shared/hooks/use-persisted-state' import { sendMB } from '@/infrastructure/event-tracking' 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 { 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' - } -} +import { useEditorContext } from '@/shared/context/editor-context' +import { useTrackingChangesMode } from '@/shared/hooks/use-tracking-changes-mode' function ReviewModeSwitcher() { const { t } = useTranslation() const user = useUserContext() const { saveTrackChangesForCurrentUser, saveTrackChanges } = useTrackChangesStateActionsContext() - const mode = useCurrentMode() + const mode = useTrackingChangesMode() const { permissionsLevel } = useIdeReactContext() const { write, trackedWrite } = usePermissionsContext() const { features } = useProjectContext() - const [showUpgradeModal, setShowUpgradeModal] = useState(false) + const { setShowUpgradeModal } = useEditorContext() const showViewOption = permissionsLevel === 'readOnly' const view = useCodeMirrorViewContext() @@ -133,10 +109,6 @@ function ReviewModeSwitcher() { )} - ) } @@ -146,7 +118,7 @@ const ModeSwitcherToggleButton = forwardRef< { onClick: MouseEventHandler; 'aria-expanded': boolean } >(({ onClick, 'aria-expanded': ariaExpanded }, ref) => { const { t } = useTranslation() - const mode = useCurrentMode() + const mode = useTrackingChangesMode() if (mode === 'edit') { return ( diff --git a/services/web/frontend/js/features/review-panel/components/upgrade-track-changes-modal.tsx b/services/web/frontend/js/features/review-panel/components/upgrade-track-changes-modal.tsx index 6130ec64db..12002fc5c9 100644 --- a/services/web/frontend/js/features/review-panel/components/upgrade-track-changes-modal.tsx +++ b/services/web/frontend/js/features/review-panel/components/upgrade-track-changes-modal.tsx @@ -16,22 +16,20 @@ import OLButton from '@/shared/components/ol/ol-button' import OLRow from '@/shared/components/ol/ol-row' import OLCol from '@/shared/components/ol/ol-col' import MaterialIcon from '@/shared/components/material-icon' +import { useEditorContext } from '@/shared/context/editor-context' -type UpgradeTrackChangesModalProps = { - show: boolean - setShow: React.Dispatch> -} - -function UpgradeTrackChangesModal({ - show, - setShow, -}: UpgradeTrackChangesModalProps) { +function UpgradeTrackChangesModal() { const { t } = useTranslation() const { project } = useProjectContext() const user = useUserContext() + const { showUpgradeModal, setShowUpgradeModal } = useEditorContext() + + if (!showUpgradeModal) { + return null + } return ( - setShow(false)}> + setShowUpgradeModal(false)}> {t('upgrade_to_review')} @@ -94,7 +92,10 @@ function UpgradeTrackChangesModal({ )} - setShow(false)}> + setShowUpgradeModal(false)} + > {t('close')} diff --git a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx index 753079f6be..2726990738 100644 --- a/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx +++ b/services/web/frontend/js/features/source-editor/components/codemirror-editor.tsx @@ -24,6 +24,7 @@ import { useProjectContext } from '@/shared/context/project-context' import { useFeatureFlag } from '@/shared/context/split-test-context' import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context' +import UpgradeTrackChangesModal from '@/features/review-panel/components/upgrade-track-changes-modal' // TODO: remove this when definitely no longer used export * from './codemirror-context' @@ -100,6 +101,7 @@ function CodeMirrorEditorComponents({ {features.trackChangesVisible && } {features.trackChangesVisible && } + {features.trackChangesVisible && } {sourceEditorComponents.map( ({ import: { default: Component }, path }) => ( diff --git a/services/web/frontend/js/features/source-editor/hooks/use-context-menu-items.tsx b/services/web/frontend/js/features/source-editor/hooks/use-context-menu-items.tsx index cdb4923c63..40d1a29bbb 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-context-menu-items.tsx +++ b/services/web/frontend/js/features/source-editor/hooks/use-context-menu-items.tsx @@ -24,6 +24,8 @@ import { pasteWithFormatting, } from '../commands/clipboard' import { isVisual } from '../extensions/visual/visual' +import { useEditorContext } from '@/shared/context/editor-context' +import { useTrackingChangesMode } from '@/shared/hooks/use-tracking-changes-mode' export const useContextMenuItems = () => { const view = useCodeMirrorViewContext() @@ -38,6 +40,9 @@ export const useContextMenuItems = () => { const { shortcuts } = useCommandRegistry() const { features } = useProjectContext() const requestedPdfSyncRef = useRef(false) + const { setShowUpgradeModal } = useEditorContext() + const trackingChangesMode = useTrackingChangesMode() + const isReview = trackingChangesMode === 'review' const closeMenu = useCallback(() => { view.dispatch({ effects: closeContextMenuEffect.of(null) }) @@ -97,6 +102,11 @@ export const useContextMenuItems = () => { const handleDelete = wrapForContextMenu(() => commands.deleteSelection(view)) const handleToggleTrackChanges = wrapForContextMenu(() => { + // Matching the logic in review toggle to ensure consistency for server pro + if (!features.trackChanges && !isReview) { + setShowUpgradeModal(true) + return true + } window.dispatchEvent(new Event('toggle-track-changes')) return true }) @@ -173,10 +183,9 @@ export const useContextMenuItems = () => { { label: wantTrackChanges ? t('back_to_editing') : t('suggest_edits'), handler: handleToggleTrackChanges, - // disable for now, future work opens upgrade modal - disabled: !features.trackChanges, + disabled: false, separatorAbove: true, - show: canEdit, + show: canEdit && features.trackChangesVisible, shortcut: getShortcut('toggle-track-changes'), }, { diff --git a/services/web/frontend/js/shared/context/editor-context.tsx b/services/web/frontend/js/shared/context/editor-context.tsx index 96f87fe1a1..c545161a62 100644 --- a/services/web/frontend/js/shared/context/editor-context.tsx +++ b/services/web/frontend/js/shared/context/editor-context.tsx @@ -39,6 +39,8 @@ export const EditorContext = createContext< premiumSuggestionResetDate: Date writefullInstance: WritefullAPI | null setWritefullInstance: (instance: WritefullAPI) => void + showUpgradeModal: boolean + setShowUpgradeModal: Dispatch> } | undefined >(undefined) @@ -95,6 +97,8 @@ export const EditorProvider: FC = ({ children }) => { : new Date() }) + const [showUpgradeModal, setShowUpgradeModal] = useState(false) + const isPendingEditor = useMemo( () => Boolean( @@ -186,6 +190,8 @@ export const EditorProvider: FC = ({ children }) => { setPremiumSuggestionResetDate, writefullInstance, setWritefullInstance, + showUpgradeModal, + setShowUpgradeModal, }), [ cobranding, @@ -205,6 +211,8 @@ export const EditorProvider: FC = ({ children }) => { setPremiumSuggestionResetDate, writefullInstance, setWritefullInstance, + showUpgradeModal, + setShowUpgradeModal, ] ) diff --git a/services/web/frontend/js/shared/hooks/use-tracking-changes-mode.ts b/services/web/frontend/js/shared/hooks/use-tracking-changes-mode.ts new file mode 100644 index 0000000000..55d6898fed --- /dev/null +++ b/services/web/frontend/js/shared/hooks/use-tracking-changes-mode.ts @@ -0,0 +1,28 @@ +import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' +import { useTrackChangesStateContext } from '@/features/review-panel/context/track-changes-state-context' +import { useUserContext } from '../context/user-context' + +type Mode = 'view' | 'review' | 'edit' + +export const useTrackingChangesMode = (): Mode => { + const trackChanges = useTrackChangesStateContext() + const user = useUserContext() + const { permissionsLevel } = useIdeReactContext() + + if (permissionsLevel === 'readOnly') { + return 'view' + } else if (permissionsLevel === 'review') { + return 'review' + } + + const trackChangesForCurrentUser = + trackChanges?.onForEveryone || + (user?.id && trackChanges?.onForMembers[user.id]) || + (!user?.id && trackChanges?.onForGuests) + + if (trackChangesForCurrentUser) { + return 'review' + } else { + return 'edit' + } +} diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx index a183eb0126..8c9eccec9d 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-context-menu.spec.tsx @@ -90,6 +90,10 @@ describe('editor context menu', { scrollBehavior: false }, function () { window.metaAttributesCache.set('ol-splitTestVariants', { 'editor-context-menu': 'enabled', }) + cy.intercept('POST', '/project/*/track_changes', { + statusCode: 200, + body: {}, + }).as('trackChanges') cy.interceptEvents() cy.interceptMetadata() }) @@ -184,7 +188,10 @@ describe('editor context menu', { scrollBehavior: false }, function () { cy.mount( - + @@ -224,7 +231,10 @@ describe('editor context menu', { scrollBehavior: false }, function () { cy.mount( - + @@ -369,7 +379,7 @@ describe('editor context menu', { scrollBehavior: false }, function () { }) }) - describe('track changes toggle', function () { + describe('when clicking the track changes buttons', function () { let toggleTrackChangesListener: Cypress.Agent beforeEach(function () { @@ -394,11 +404,19 @@ describe('editor context menu', { scrollBehavior: false }, function () { @@ -432,11 +450,17 @@ describe('editor context menu', { scrollBehavior: false }, function () { @@ -463,14 +487,40 @@ describe('editor context menu', { scrollBehavior: false }, function () { cy.get('@toggleTrackChanges').should('have.been.calledOnce') }) - it('should disable suggest edits when project does not support track changes', function () { + it('should open upgrade modal when user does not support track changes', function () { const scope = mockScope() cy.mount( + + + + ) + + cy.get('.cm-line').eq(10).rightclick() + + cy.findByRole('menu').within(() => { + cy.findByRole('menuitem', { name: /suggest edits/i }).click() + }) + + cy.findByRole('dialog').should('be.visible') + cy.findByRole('dialog').should('contain.text', 'Upgrade to Review') + }) + }) + + describe('when trackChangesVisible feature is disabled', function () { + it('should hide the track changes button', function () { + const scope = mockScope() + + cy.mount( + + @@ -481,9 +531,10 @@ describe('editor context menu', { scrollBehavior: false }, function () { cy.findByRole('menu').within(() => { cy.findByRole('menuitem', { name: /suggest edits/i }).should( - 'have.attr', - 'aria-disabled', - 'true' + 'not.exist' + ) + cy.findByRole('menuitem', { name: /back to editing/i }).should( + 'not.exist' ) }) }) @@ -559,6 +610,9 @@ describe('editor context menu', { scrollBehavior: false }, function () { cy.findByRole('menuitem', { name: /suggest edits/i }).should( 'not.exist' ) + cy.findByRole('menuitem', { name: /back to editing/i }).should( + 'not.exist' + ) cy.findByRole('menuitem', { name: /comment/i }).should('be.enabled') }) }) @@ -821,7 +875,6 @@ describe('editor context menu', { scrollBehavior: false }, function () { @@ -964,7 +1017,10 @@ describe('editor context menu', { scrollBehavior: false }, function () { cy.mount( - + diff --git a/services/web/test/frontend/features/source-editor/helpers/mock-project.ts b/services/web/test/frontend/features/source-editor/helpers/mock-project.ts index f9848d1a96..948e4b0033 100644 --- a/services/web/test/frontend/features/source-editor/helpers/mock-project.ts +++ b/services/web/test/frontend/features/source-editor/helpers/mock-project.ts @@ -11,6 +11,7 @@ export const mockProject = ({ projectOwner = undefined, spellCheckLanguage = 'en', rootFolder = null, + trackChangesState = false, }: any = {}) => { return { _id: 'test-project', @@ -63,7 +64,7 @@ export const mockProject = ({ }, compiler: 'pdflatex' as ProjectCompiler, imageName: 'texlive-full:2024.1', - trackChangesState: false, + trackChangesState, invites: [], members: [], owner: projectOwner || {