mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Merge pull request #35293 from overleaf/mj-zod-history-ot
[real-time] Allow history-ot operations in real-time GitOrigin-RevId: 4b1b9e4c1a214edf49c5eface46a4d309ba41b0d
This commit is contained in:
committed by
Copybot
parent
9d57689112
commit
d8f167dec2
@@ -0,0 +1,89 @@
|
|||||||
|
// @ts-check
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { z, zz } = require('@overleaf/validation-tools')
|
||||||
|
|
||||||
|
const rawTrackingProps = z.strictObject({
|
||||||
|
type: z.enum(['insert', 'delete']),
|
||||||
|
userId: zz.objectId(),
|
||||||
|
ts: z.iso.datetime(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawClearTrackingProps = z.strictObject({
|
||||||
|
type: z.literal('none'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawInsertOp = z.union([
|
||||||
|
z.strictObject({
|
||||||
|
i: z.string(),
|
||||||
|
commentIds: z.array(zz.objectId()).optional(),
|
||||||
|
tracking: rawTrackingProps.optional(),
|
||||||
|
}),
|
||||||
|
z.string(),
|
||||||
|
])
|
||||||
|
|
||||||
|
const rawRemoveOp = z.number().int().max(-1)
|
||||||
|
|
||||||
|
const rawRetainOp = z.union([
|
||||||
|
z.strictObject({
|
||||||
|
r: z.number().int().min(1),
|
||||||
|
commentIds: z.array(zz.objectId()).optional(),
|
||||||
|
tracking: rawTrackingProps.or(rawClearTrackingProps).optional(),
|
||||||
|
}),
|
||||||
|
z.number().int().min(1),
|
||||||
|
])
|
||||||
|
|
||||||
|
const rawScanOp = z.union([rawInsertOp, rawRemoveOp, rawRetainOp])
|
||||||
|
|
||||||
|
const rawTextOperation = z.strictObject({
|
||||||
|
textOperation: z.array(rawScanOp).min(1),
|
||||||
|
contentHash: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawRange = z.strictObject({
|
||||||
|
pos: z.number().int().min(0),
|
||||||
|
length: z.number().int().min(0),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawAddCommentOperation = z.strictObject({
|
||||||
|
commentId: zz.objectId(),
|
||||||
|
ranges: z.array(rawRange).min(1),
|
||||||
|
resolved: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawSetCommentStateOperation = z.strictObject({
|
||||||
|
commentId: zz.objectId(),
|
||||||
|
resolved: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawDeleteCommentOperation = z.strictObject({
|
||||||
|
deleteComment: zz.objectId(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawEditNoOperation = z.strictObject({
|
||||||
|
noOp: z.literal(true),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rawEditOperation = z.union([
|
||||||
|
rawTextOperation,
|
||||||
|
rawAddCommentOperation,
|
||||||
|
rawDeleteCommentOperation,
|
||||||
|
rawSetCommentStateOperation,
|
||||||
|
rawEditNoOperation,
|
||||||
|
])
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
rawTrackingProps,
|
||||||
|
rawClearTrackingProps,
|
||||||
|
rawInsertOp,
|
||||||
|
rawRemoveOp,
|
||||||
|
rawRetainOp,
|
||||||
|
rawScanOp,
|
||||||
|
rawTextOperation,
|
||||||
|
rawRange,
|
||||||
|
rawAddCommentOperation,
|
||||||
|
rawSetCommentStateOperation,
|
||||||
|
rawDeleteCommentOperation,
|
||||||
|
rawEditNoOperation,
|
||||||
|
rawEditOperation,
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import Blob from './blob'
|
import type Blob from './blob'
|
||||||
import TrackingProps from './file_data/tracking_props'
|
import type TrackingProps from './file_data/tracking_props'
|
||||||
import ClearTrackingProps from './file_data/clear_tracking_props'
|
import type ClearTrackingProps from './file_data/clear_tracking_props'
|
||||||
|
import type { z } from '@overleaf/validation-tools'
|
||||||
|
import type * as schemas from './schemas'
|
||||||
|
|
||||||
export type BlobStore = {
|
export type BlobStore = {
|
||||||
getBlob(hash: string): Promise<Blob | null>
|
getBlob(hash: string): Promise<Blob | null>
|
||||||
@@ -17,10 +19,7 @@ export type RangesBlob = {
|
|||||||
trackedChanges: TrackedChangeRawData[]
|
trackedChanges: TrackedChangeRawData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RawRange = {
|
export type RawRange = z.infer<typeof schemas.rawRange>
|
||||||
pos: number
|
|
||||||
length: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommentRawData = {
|
export type CommentRawData = {
|
||||||
id: string
|
id: string
|
||||||
@@ -33,15 +32,11 @@ export type TrackedChangeRawData = {
|
|||||||
tracking: TrackingPropsRawData
|
tracking: TrackingPropsRawData
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TrackingPropsRawData = {
|
export type TrackingPropsRawData = z.infer<typeof schemas.rawTrackingProps>
|
||||||
type: 'insert' | 'delete'
|
|
||||||
userId: string
|
|
||||||
ts: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ClearTrackingPropsRawData = {
|
export type ClearTrackingPropsRawData = z.infer<
|
||||||
type: 'none'
|
typeof schemas.rawClearTrackingProps
|
||||||
}
|
>
|
||||||
|
|
||||||
export type TrackingDirective = TrackingProps | ClearTrackingProps
|
export type TrackingDirective = TrackingProps | ClearTrackingProps
|
||||||
|
|
||||||
@@ -112,55 +107,32 @@ export type RawHollowStringFileData = { stringLength: number }
|
|||||||
|
|
||||||
export type RawV2DocVersions = Record<string, { pathname: string; v: number }>
|
export type RawV2DocVersions = Record<string, { pathname: string; v: number }>
|
||||||
|
|
||||||
export type RawInsertOp =
|
export type RawInsertOp = z.infer<typeof schemas.rawInsertOp>
|
||||||
| {
|
|
||||||
i: string
|
|
||||||
commentIds?: string[]
|
|
||||||
tracking?: TrackingPropsRawData
|
|
||||||
}
|
|
||||||
| string
|
|
||||||
|
|
||||||
export type RawRemoveOp = number
|
export type RawRemoveOp = z.infer<typeof schemas.rawRemoveOp>
|
||||||
export type RawRetainOp =
|
export type RawRetainOp = z.infer<typeof schemas.rawRetainOp>
|
||||||
| {
|
|
||||||
r: number
|
|
||||||
commentIds?: string[]
|
|
||||||
tracking?: TrackingPropsRawData | ClearTrackingPropsRawData
|
|
||||||
}
|
|
||||||
| number
|
|
||||||
|
|
||||||
export type RawScanOp = RawInsertOp | RawRemoveOp | RawRetainOp
|
export type RawScanOp = z.infer<typeof schemas.rawScanOp>
|
||||||
|
|
||||||
export type RawTextOperation = {
|
export type RawTextOperation = z.infer<typeof schemas.rawTextOperation>
|
||||||
textOperation: RawScanOp[]
|
|
||||||
contentHash?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RawAddCommentOperation = {
|
export type RawAddCommentOperation = z.infer<
|
||||||
commentId: string
|
typeof schemas.rawAddCommentOperation
|
||||||
ranges: RawRange[]
|
>
|
||||||
resolved?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RawDeleteCommentOperation = { deleteComment: string }
|
export type RawDeleteCommentOperation = z.infer<
|
||||||
|
typeof schemas.rawDeleteCommentOperation
|
||||||
|
>
|
||||||
|
|
||||||
export type RawSetCommentStateOperation = {
|
export type RawSetCommentStateOperation = z.infer<
|
||||||
commentId: string
|
typeof schemas.rawSetCommentStateOperation
|
||||||
resolved: boolean
|
>
|
||||||
}
|
|
||||||
|
|
||||||
export type RawEditNoOperation = {
|
export type RawEditNoOperation = z.infer<typeof schemas.rawEditNoOperation>
|
||||||
noOp: true
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RawEditFileOperation = RawEditOperation & { pathname: string }
|
export type RawEditFileOperation = RawEditOperation & { pathname: string }
|
||||||
|
|
||||||
export type RawEditOperation =
|
export type RawEditOperation = z.infer<typeof schemas.rawEditOperation>
|
||||||
| RawTextOperation
|
|
||||||
| RawAddCommentOperation
|
|
||||||
| RawDeleteCommentOperation
|
|
||||||
| RawSetCommentStateOperation
|
|
||||||
| RawEditNoOperation
|
|
||||||
|
|
||||||
export type LinkedFileData = {
|
export type LinkedFileData = {
|
||||||
importedAt: string
|
importedAt: string
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@overleaf/o-error": "workspace:*",
|
"@overleaf/o-error": "workspace:*",
|
||||||
|
"@overleaf/validation-tools": "workspace:*",
|
||||||
"check-types": "^5.1.0",
|
"check-types": "^5.1.0",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"p-map": "^4.0.0",
|
"p-map": "^4.0.0",
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ const operationSchema = z
|
|||||||
hash: z.string(),
|
hash: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
// TODO: Use rawEditOperation from overleaf-editor-core/lib/schemas.js
|
||||||
|
// instead
|
||||||
textOperation: z.array(z.any()).optional(),
|
textOperation: z.array(z.any()).optional(),
|
||||||
file: fileSchema.optional(),
|
file: fileSchema.optional(),
|
||||||
contentHash: z.string().optional(),
|
contentHash: z.string().optional(),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Errors from './Errors.js'
|
|||||||
import { z, zz } from '@overleaf/validation-tools'
|
import { z, zz } from '@overleaf/validation-tools'
|
||||||
import { isZodErrorLike } from 'zod-validation-error'
|
import { isZodErrorLike } from 'zod-validation-error'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
|
import schemas from 'overleaf-editor-core/lib/schemas.js'
|
||||||
|
|
||||||
const { UnexpectedArgumentsError } = Errors
|
const { UnexpectedArgumentsError } = Errors
|
||||||
|
|
||||||
@@ -60,7 +61,8 @@ const applyOtUpdateSchema = z.object({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.min(1),
|
.min(1)
|
||||||
|
.or(z.array(schemas.rawEditOperation).min(1)),
|
||||||
v: z.number().int().min(0),
|
v: z.number().int().min(0),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ConnectedUsersManager from './ConnectedUsersManager.js'
|
|||||||
import WebsocketLoadBalancer from './WebsocketLoadBalancer.js'
|
import WebsocketLoadBalancer from './WebsocketLoadBalancer.js'
|
||||||
import RoomManager from './RoomManager.js'
|
import RoomManager from './RoomManager.js'
|
||||||
import Errors from './Errors.js'
|
import Errors from './Errors.js'
|
||||||
|
import { EditOperationBuilder } from 'overleaf-editor-core'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
CodedError,
|
CodedError,
|
||||||
@@ -658,14 +659,42 @@ export default WebsocketController = {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_isHistoryOTUpdate(update) {
|
||||||
|
for (const op of update.op) {
|
||||||
|
if (!EditOperationBuilder.isValid(op)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
_assertClientCanApplyUpdate(client, docId, update, callback) {
|
_assertClientCanApplyUpdate(client, docId, update, callback) {
|
||||||
if (WebsocketController._isCommentUpdate(update)) {
|
if (WebsocketController._isHistoryOTUpdate(update)) {
|
||||||
|
return WebsocketController._assertClientCanApplyHistoryUpdate(
|
||||||
|
client,
|
||||||
|
docId,
|
||||||
|
update,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return WebsocketController._assertClientCanApplyShareJsUpdate(
|
||||||
|
client,
|
||||||
|
docId,
|
||||||
|
update,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_assertClientCanApplyShareJsUpdate(client, docId, update, callback) {
|
||||||
|
if (WebsocketController._isShareJsCommentUpdate(update)) {
|
||||||
return AuthorizationManager.assertClientCanViewProjectAndDoc(
|
return AuthorizationManager.assertClientCanViewProjectAndDoc(
|
||||||
client,
|
client,
|
||||||
docId,
|
docId,
|
||||||
callback
|
callback
|
||||||
)
|
)
|
||||||
} else if (update.meta?.tc) {
|
} else if (WebsocketController._isShareJsTrackedChangesUpdate(update)) {
|
||||||
return AuthorizationManager.assertClientCanReviewProjectAndDoc(
|
return AuthorizationManager.assertClientCanReviewProjectAndDoc(
|
||||||
client,
|
client,
|
||||||
docId,
|
docId,
|
||||||
@@ -680,10 +709,7 @@ export default WebsocketController = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_isCommentUpdate(update) {
|
_isShareJsCommentUpdate(update) {
|
||||||
if (!(update && update.op instanceof Array)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for (const op of update.op) {
|
for (const op of update.op) {
|
||||||
if (!op.c) {
|
if (!op.c) {
|
||||||
return false
|
return false
|
||||||
@@ -691,4 +717,73 @@ export default WebsocketController = {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_isShareJsTrackedChangesUpdate(update) {
|
||||||
|
return Boolean(update.meta?.tc)
|
||||||
|
},
|
||||||
|
|
||||||
|
_assertClientCanApplyHistoryUpdate(client, docId, update, callback) {
|
||||||
|
if (WebsocketController._isHistoryOTCommentUpdate(update)) {
|
||||||
|
return AuthorizationManager.assertClientCanViewProjectAndDoc(
|
||||||
|
client,
|
||||||
|
docId,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
} else if (WebsocketController._isHistoryOTTrackedChangesUpdate(update)) {
|
||||||
|
return AuthorizationManager.assertClientCanReviewProjectAndDoc(
|
||||||
|
client,
|
||||||
|
docId,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return AuthorizationManager.assertClientCanEditProjectAndDoc(
|
||||||
|
client,
|
||||||
|
docId,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_isHistoryOTCommentUpdate(update) {
|
||||||
|
for (const op of update.op) {
|
||||||
|
if (!WebsocketController._isHistoryOTAddCommentOperation(op)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
_isHistoryOTAddCommentOperation(op) {
|
||||||
|
return typeof op === 'object' && 'commentId' in op && 'ranges' in op
|
||||||
|
},
|
||||||
|
|
||||||
|
_isHistoryOTTrackedChangesUpdate(update) {
|
||||||
|
for (const op of update.op) {
|
||||||
|
if (!('textOperation' in op)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (const scanOp of op.textOperation) {
|
||||||
|
// Plain retains
|
||||||
|
if (typeof scanOp === 'number' && scanOp > 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Plain insertions
|
||||||
|
if (typeof scanOp !== 'object') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Tracked insertions
|
||||||
|
if ('i' in scanOp && 'tracking' in scanOp) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Tracked ranges (we don't accepting by clearing tracking)
|
||||||
|
if ('r' in scanOp && scanOp.tracking?.type !== 'none') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"express": "4.22.1",
|
"express": "4.22.1",
|
||||||
"express-session": "^1.17.1",
|
"express-session": "^1.17.1",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
|
"overleaf-editor-core": "workspace:*",
|
||||||
"proxy-addr": "^2.0.7",
|
"proxy-addr": "^2.0.7",
|
||||||
"socket.io": "github:overleaf/socket.io#0.9.19-overleaf-12",
|
"socket.io": "github:overleaf/socket.io#0.9.19-overleaf-12",
|
||||||
"socket.io-client": "https://github.com/overleaf/socket.io-client/archive/refs/tags/0.9.17-overleaf-6.tar.gz",
|
"socket.io-client": "https://github.com/overleaf/socket.io-client/archive/refs/tags/0.9.17-overleaf-6.tar.gz",
|
||||||
|
|||||||
@@ -1788,5 +1788,107 @@ describe('WebsocketController', function () {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('with a history-OT client', function () {
|
||||||
|
const tracking = {
|
||||||
|
insert: { type: 'insert', userId: 'user-id-123', ts: '2026-07-15' },
|
||||||
|
delete: { type: 'delete', userId: 'user-id-123', ts: '2026-07-15' },
|
||||||
|
none: { type: 'none' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectRoutedTo(ctx, target, update) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
for (const name of [
|
||||||
|
'assertClientCanViewProjectAndDoc',
|
||||||
|
'assertClientCanReviewProjectAndDoc',
|
||||||
|
'assertClientCanEditProjectAndDoc',
|
||||||
|
]) {
|
||||||
|
ctx.AuthorizationManager[name].yields(new Error('not authorized'))
|
||||||
|
}
|
||||||
|
ctx.AuthorizationManager[target].yields(null)
|
||||||
|
ctx.WebsocketController._assertClientCanApplyUpdate(
|
||||||
|
ctx.client,
|
||||||
|
ctx.doc_id,
|
||||||
|
update,
|
||||||
|
error => {
|
||||||
|
expect(error).to.be.null
|
||||||
|
expect(ctx.AuthorizationManager[target].calledOnce).to.be.true
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should route an add-comment op to view access', async function (ctx) {
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanViewProjectAndDoc', {
|
||||||
|
op: [{ commentId: 'comment-id', ranges: [{ pos: 0, length: 3 }] }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should route a tracked insertion to review access', async function (ctx) {
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanReviewProjectAndDoc', {
|
||||||
|
op: [{ textOperation: [5, { i: 'x', tracking: tracking.insert }] }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should route a tracked deletion to review access', async function (ctx) {
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanReviewProjectAndDoc', {
|
||||||
|
op: [{ textOperation: [{ r: 3, tracking: tracking.delete }] }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should route accepting a tracked change (clearing tracking) to edit access', async function (ctx) {
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanEditProjectAndDoc', {
|
||||||
|
op: [{ textOperation: [{ r: 3, tracking: tracking.none }] }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should route an untracked insertion to edit access', async function (ctx) {
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanEditProjectAndDoc', {
|
||||||
|
op: [{ textOperation: ['plain'] }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should route an untracked deletion to edit access', async function (ctx) {
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanEditProjectAndDoc', {
|
||||||
|
op: [{ textOperation: [-3] }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should route a mix of tracked and untracked ops to edit access', async function (ctx) {
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanEditProjectAndDoc', {
|
||||||
|
op: [
|
||||||
|
{ textOperation: [{ i: 'x', tracking: tracking.insert }] },
|
||||||
|
{ textOperation: ['plain'] },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should route a resolve-comment op to edit access', async function (ctx) {
|
||||||
|
// Only add-comment ops (commentId + ranges) are recognised as comment
|
||||||
|
// updates; resolving a comment currently requires edit access.
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanEditProjectAndDoc', {
|
||||||
|
op: [{ commentId: 'comment-id', resolved: true }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should route a permanent history-OT insertion carrying meta.tc to edit access', async function (ctx) {
|
||||||
|
// meta.tc is a legacy (sharejs) tracked-changes id seed and is
|
||||||
|
// meaningless for history-OT, where tracking is carried by the op.
|
||||||
|
// A review-only client must not be able to make a permanent edit by
|
||||||
|
// attaching it.
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanEditProjectAndDoc', {
|
||||||
|
op: [{ textOperation: ['permanent untracked text'] }],
|
||||||
|
meta: { tc: 'tracked-change-id' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should route a permanent history-OT deletion carrying meta.tc to edit access', async function (ctx) {
|
||||||
|
await expectRoutedTo(ctx, 'assertClientCanEditProjectAndDoc', {
|
||||||
|
op: [{ textOperation: [-3] }],
|
||||||
|
meta: { tc: 'tracked-change-id' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
EditOperationBuilder,
|
EditOperationBuilder,
|
||||||
CommentList,
|
CommentList,
|
||||||
} from 'overleaf-editor-core'
|
} from 'overleaf-editor-core'
|
||||||
import {
|
import type {
|
||||||
StringFileRawData,
|
StringFileRawData,
|
||||||
RawEditOperation,
|
RawEditOperation,
|
||||||
} from 'overleaf-editor-core/lib/types'
|
} from 'overleaf-editor-core/lib/types'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { StringFileData } from 'overleaf-editor-core'
|
import { StringFileData } from 'overleaf-editor-core'
|
||||||
import { AnyOperation } from '../../../../../../types/change'
|
import { AnyOperation } from '../../../../../../types/change'
|
||||||
import { RawEditOperation } from 'overleaf-editor-core/lib/types'
|
import type { RawEditOperation } from 'overleaf-editor-core/lib/types'
|
||||||
|
|
||||||
export type Version = number
|
export type Version = number
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pLimit from 'p-limit'
|
import pLimit from 'p-limit'
|
||||||
import { Change, Chunk, Snapshot, File } from 'overleaf-editor-core'
|
import { Change, Chunk, Snapshot, File } from 'overleaf-editor-core'
|
||||||
import { RawChange, RawChunk } from 'overleaf-editor-core/lib/types'
|
import type { RawChange, RawChunk } from 'overleaf-editor-core/lib/types'
|
||||||
import { FetchError, getJSON, postJSON } from '@/infrastructure/fetch-json'
|
import { FetchError, getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||||
import path from 'path-browserify'
|
import path from 'path-browserify'
|
||||||
|
|
||||||
|
|||||||
@@ -7187,6 +7187,7 @@ __metadata:
|
|||||||
mocha: "npm:^11.1.0"
|
mocha: "npm:^11.1.0"
|
||||||
mocha-junit-reporter: "npm:^2.2.1"
|
mocha-junit-reporter: "npm:^2.2.1"
|
||||||
mocha-multi-reporters: "npm:^1.5.1"
|
mocha-multi-reporters: "npm:^1.5.1"
|
||||||
|
overleaf-editor-core: "workspace:*"
|
||||||
proxy-addr: "npm:^2.0.7"
|
proxy-addr: "npm:^2.0.7"
|
||||||
sinon: "npm:^9.2.4"
|
sinon: "npm:^9.2.4"
|
||||||
sinon-chai: "npm:3.7.0"
|
sinon-chai: "npm:3.7.0"
|
||||||
@@ -25255,6 +25256,7 @@ __metadata:
|
|||||||
resolution: "overleaf-editor-core@workspace:libraries/overleaf-editor-core"
|
resolution: "overleaf-editor-core@workspace:libraries/overleaf-editor-core"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@overleaf/o-error": "workspace:*"
|
"@overleaf/o-error": "workspace:*"
|
||||||
|
"@overleaf/validation-tools": "workspace:*"
|
||||||
"@types/check-types": "npm:^7.3.7"
|
"@types/check-types": "npm:^7.3.7"
|
||||||
"@types/path-browserify": "npm:^1.0.3"
|
"@types/path-browserify": "npm:^1.0.3"
|
||||||
chai: "npm:^3.3.0"
|
chai: "npm:^3.3.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user