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