Merge pull request #34698 from overleaf/mj-context-menu-tracked-changes

[web] Add tracked changes options to context menu

GitOrigin-RevId: b49198c6c2c63063d96ace69f10b617ac819bb04
This commit is contained in:
Mathias Jakobsen
2026-06-23 08:05:33 +00:00
committed by Copybot
parent a53b164b45
commit e0cfb07f60
6 changed files with 264 additions and 81 deletions
@@ -3,9 +3,11 @@ import {
EditorProviders,
makeEditorPropertiesProvider,
makeProjectProvider,
USER_ID,
} from '../../../helpers/editor-providers'
import CodeMirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
import { TestContainer } from '../helpers/test-container'
import { docId } from '../helpers/mock-doc'
import { FC } from 'react'
import { PermissionsContext } from '@/features/ide-react/context/permissions-context'
import { Permissions } from '@/features/ide-react/types/permissions'
@@ -1324,4 +1326,137 @@ describe('editor context menu', { scrollBehavior: false }, function () {
cy.findByRole('menu').should('be.visible')
})
})
describe('tracked-change actions', function () {
function mountEditorWithChanges() {
cy.intercept('POST', `/project/*/doc/${docId}/changes/accept`, {}).as(
'acceptChange'
)
const changes = [
{
metadata: {
user_id: USER_ID,
ts: new Date('2025-01-01T00:00:00.000Z'),
},
id: 'inserted-op-id',
op: { p: 166, t: 'inserted-op-id', i: 'introduction' },
},
{
metadata: {
user_id: USER_ID,
ts: new Date('2025-01-01T01:00:00.000Z'),
},
id: 'deleted-op-id',
op: { p: 110, t: 'deleted-op-id', d: 'beautiful ' },
},
]
const getChanges = cy.stub().as('getChanges').returns([])
const removeChangeIds = cy.stub().as('removeChangeIds')
const scope = mockScope(undefined, {
docOptions: {
rangesOptions: { changes, getChanges, removeChangeIds },
},
})
cy.mount(
<TestContainer>
<EditorProviders
scope={scope}
features={{ trackChangesVisible: true }}
>
<CodeMirrorEditor />
</EditorProviders>
</TestContainer>
)
}
// Select a deletion and an insertion so the bulk-action items appear, then
// right-click inside the selection (which preserves it).
function openMenuOverChanges() {
cy.findByText('\\maketitle').type(
'{home}{shift}' + '{downArrow}'.repeat(10),
{ scrollBehavior: false }
)
cy.findByText('\\maketitle').rightclick({ scrollBehavior: false })
}
it('shows accept and reject items when the selection covers changes', function () {
mountEditorWithChanges()
openMenuOverChanges()
cy.findByRole('menu').within(() => {
cy.findByRole('menuitem', { name: 'Accept selected changes' }).should(
'be.visible'
)
cy.findByRole('menuitem', { name: 'Reject selected changes' }).should(
'be.visible'
)
})
})
it('hides accept and reject items when the selection covers no changes', function () {
mountEditorWithChanges()
cy.get('.cm-line').eq(2).rightclick({ scrollBehavior: false })
cy.findByRole('menu').should('be.visible')
cy.findByRole('menu').within(() => {
cy.findByRole('menuitem', { name: 'Accept selected changes' }).should(
'not.exist'
)
cy.findByRole('menuitem', { name: 'Reject selected changes' }).should(
'not.exist'
)
})
})
it('accepts the selected changes', function () {
mountEditorWithChanges()
openMenuOverChanges()
cy.findByRole('menu').within(() => {
cy.findByRole('menuitem', { name: 'Accept selected changes' }).click({
scrollBehavior: false,
})
})
cy.findByRole('dialog').within(() => {
cy.findByText(
'Are you sure you want to accept the selected 2 changes?'
).should('exist')
cy.findByRole('button', { name: 'OK' }).click({ scrollBehavior: false })
})
cy.wait('@acceptChange')
cy.get('@removeChangeIds').should('have.been.calledWith', [
'inserted-op-id',
'deleted-op-id',
])
})
it('rejects the selected changes', function () {
mountEditorWithChanges()
openMenuOverChanges()
cy.findByRole('menu').within(() => {
cy.findByRole('menuitem', { name: 'Reject selected changes' }).click({
scrollBehavior: false,
})
})
cy.findByRole('dialog').within(() => {
cy.findByText(
'Are you sure you want to reject the selected 2 changes?'
).should('exist')
cy.findByRole('button', { name: 'OK' }).click({ scrollBehavior: false })
})
cy.get('@getChanges').should('have.been.calledWith', [
'inserted-op-id',
'deleted-op-id',
])
})
})
})