mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #19867 from overleaf/em-hide-history-resync-details
Hide history resync changes GitOrigin-RevId: a36876ff637d0a4c34150b21f4b4e623dff0ab03
This commit is contained in:
parent
40f3db3772
commit
e10478f256
9 changed files with 227 additions and 163 deletions
|
@ -137,7 +137,7 @@ class UpdateSetBuilder {
|
|||
}
|
||||
|
||||
for (const op of change.operations) {
|
||||
this.applyOperation(op, timestamp, authors)
|
||||
this.applyOperation(op, timestamp, authors, change.origin)
|
||||
}
|
||||
|
||||
this.currentUpdate.pathnames = Array.from(this.currentUpdate.pathnames)
|
||||
|
@ -146,9 +146,9 @@ class UpdateSetBuilder {
|
|||
this.version += 1
|
||||
}
|
||||
|
||||
applyOperation(op, timestamp, authors) {
|
||||
applyOperation(op, timestamp, authors, origin) {
|
||||
if (UpdateSetBuilder._isTextOperation(op)) {
|
||||
this.applyTextOperation(op, timestamp, authors)
|
||||
this.applyTextOperation(op, timestamp, authors, origin)
|
||||
} else if (UpdateSetBuilder._isRenameOperation(op)) {
|
||||
this.applyRenameOperation(op, timestamp, authors)
|
||||
} else if (UpdateSetBuilder._isRemoveFileOperation(op)) {
|
||||
|
@ -158,7 +158,7 @@ class UpdateSetBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
applyTextOperation(operation, timestamp, authors) {
|
||||
applyTextOperation(operation, timestamp, authors, origin) {
|
||||
const { pathname } = operation
|
||||
if (pathname === '') {
|
||||
// this shouldn't happen, but we continue to allow the user to see the history
|
||||
|
@ -180,7 +180,7 @@ class UpdateSetBuilder {
|
|||
return
|
||||
}
|
||||
|
||||
file.applyTextOperation(authors, timestamp, this.version, operation)
|
||||
file.applyTextOperation(authors, timestamp, this.version, operation, origin)
|
||||
this.currentUpdate.pathnames.add(pathname)
|
||||
}
|
||||
|
||||
|
@ -285,8 +285,8 @@ class File {
|
|||
this.operations = []
|
||||
}
|
||||
|
||||
applyTextOperation(authors, timestamp, version, operation) {
|
||||
this.operations.push({ authors, timestamp, version, operation })
|
||||
applyTextOperation(authors, timestamp, version, operation, origin) {
|
||||
this.operations.push({ authors, timestamp, version, operation, origin })
|
||||
}
|
||||
|
||||
rename(pathname) {
|
||||
|
@ -309,13 +309,12 @@ class File {
|
|||
let initialContent
|
||||
const updates = []
|
||||
|
||||
for (let operation of this.operations) {
|
||||
if (!('textOperation' in operation.operation)) {
|
||||
for (const operationInfo of this.operations) {
|
||||
if (!('textOperation' in operationInfo.operation)) {
|
||||
// We only care about text operations
|
||||
continue
|
||||
}
|
||||
let authors, ops, timestamp, version
|
||||
;({ authors, timestamp, version, operation } = operation)
|
||||
const { authors, timestamp, version, operation } = operationInfo
|
||||
// Set the initialContent to the latest version we have before the diff
|
||||
// begins. 'version' here refers to the document version as we are
|
||||
// applying the updates. So we store the content *before* applying the
|
||||
|
@ -327,6 +326,7 @@ class File {
|
|||
)
|
||||
}
|
||||
|
||||
let ops
|
||||
;({ content, ops } = this._convertTextOperation(
|
||||
content,
|
||||
operation,
|
||||
|
@ -335,7 +335,7 @@ class File {
|
|||
|
||||
// We only need to return the updates between fromVersion and toVersion
|
||||
if (fromVersion <= version && version < toVersion) {
|
||||
updates.push({
|
||||
const update = {
|
||||
meta: {
|
||||
users: authors,
|
||||
start_ts: timestamp.getTime(),
|
||||
|
@ -343,7 +343,11 @@ class File {
|
|||
},
|
||||
v: version,
|
||||
op: ops,
|
||||
})
|
||||
}
|
||||
if (operationInfo.origin) {
|
||||
update.meta.origin = operationInfo.origin
|
||||
}
|
||||
updates.push(update)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,4 @@
|
|||
/* eslint-disable
|
||||
no-proto,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
|
||||
import logger from '@overleaf/logger'
|
||||
import _ from 'lodash'
|
||||
import OError from '@overleaf/o-error'
|
||||
|
||||
export class ConsistencyError extends OError {}
|
||||
|
@ -26,7 +12,7 @@ export const _mocks = {}
|
|||
|
||||
export function buildDiff(initialContent, updates) {
|
||||
let diff = [{ u: initialContent }]
|
||||
for (const update of Array.from(updates)) {
|
||||
for (const update of updates) {
|
||||
diff = applyUpdateToDiff(diff, update)
|
||||
}
|
||||
diff = compressDiff(diff)
|
||||
|
@ -35,29 +21,48 @@ export function buildDiff(initialContent, updates) {
|
|||
|
||||
_mocks.compressDiff = diff => {
|
||||
const newDiff = []
|
||||
for (const part of Array.from(diff)) {
|
||||
for (const part of diff) {
|
||||
const users = part.meta?.users ?? []
|
||||
|
||||
if (part.meta?.origin?.kind === 'history-resync') {
|
||||
// Skip history resync updates. Inserts are converted to unchanged text
|
||||
// and deletes are skipped, so that they effectively don't appear in the
|
||||
// diff.
|
||||
if (part.u != null) {
|
||||
newDiff.push(part)
|
||||
} else if (part.i != null) {
|
||||
newDiff.push({ u: part.i })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (newDiff.length === 0) {
|
||||
// If we haven't seen other parts yet, we have nothing to merge.
|
||||
newDiff.push(part)
|
||||
continue
|
||||
}
|
||||
|
||||
const lastPart = newDiff[newDiff.length - 1]
|
||||
if (
|
||||
lastPart != null &&
|
||||
(lastPart.meta != null ? lastPart.meta.user : undefined) != null &&
|
||||
(part.meta != null ? part.meta.user : undefined) != null
|
||||
) {
|
||||
if (
|
||||
lastPart.i != null &&
|
||||
part.i != null &&
|
||||
lastPart.meta.user.id === part.meta.user.id
|
||||
) {
|
||||
const lastUsers = lastPart.meta?.users ?? []
|
||||
const usersNotInBothParts = _.xor(users, lastUsers)
|
||||
|
||||
if (usersNotInBothParts.length > 0) {
|
||||
// If the set of users in the last part and this part are not the same, we
|
||||
// can't merge.
|
||||
newDiff.push(part)
|
||||
continue
|
||||
}
|
||||
|
||||
if (lastPart.i != null && part.i != null) {
|
||||
// Merge two inserts
|
||||
lastPart.i += part.i
|
||||
lastPart.meta.start_ts = Math.min(
|
||||
lastPart.meta.start_ts,
|
||||
part.meta.start_ts
|
||||
)
|
||||
lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
|
||||
} else if (
|
||||
lastPart.d != null &&
|
||||
part.d != null &&
|
||||
lastPart.meta.user.id === part.meta.user.id
|
||||
) {
|
||||
} else if (lastPart.d != null && part.d != null) {
|
||||
// Merge two deletes
|
||||
lastPart.d += part.d
|
||||
lastPart.meta.start_ts = Math.min(
|
||||
lastPart.meta.start_ts,
|
||||
|
@ -67,9 +72,6 @@ _mocks.compressDiff = diff => {
|
|||
} else {
|
||||
newDiff.push(part)
|
||||
}
|
||||
} else {
|
||||
newDiff.push(part)
|
||||
}
|
||||
}
|
||||
return newDiff
|
||||
}
|
||||
|
@ -80,7 +82,6 @@ export function compressDiff(...args) {
|
|||
|
||||
export function applyOpToDiff(diff, op, meta) {
|
||||
let consumedDiff
|
||||
const position = 0
|
||||
|
||||
let remainingDiff = diff.slice()
|
||||
;({ consumedDiff, remainingDiff } = _consumeToOffset(remainingDiff, op.p))
|
||||
|
@ -97,16 +98,16 @@ export function applyOpToDiff(diff, op, meta) {
|
|||
op,
|
||||
meta
|
||||
))
|
||||
newDiff.push(...Array.from(consumedDiff || []))
|
||||
newDiff.push(...(consumedDiff || []))
|
||||
}
|
||||
|
||||
newDiff.push(...Array.from(remainingDiff || []))
|
||||
newDiff.push(...(remainingDiff || []))
|
||||
|
||||
return newDiff
|
||||
}
|
||||
|
||||
_mocks.applyUpdateToDiff = (diff, update) => {
|
||||
for (const op of Array.from(update.op)) {
|
||||
for (const op of update.op) {
|
||||
if (op.broken !== true) {
|
||||
diff = applyOpToDiff(diff, op, update.meta)
|
||||
}
|
||||
|
|
|
@ -189,10 +189,10 @@ function _summarizeUpdates(updates, labels, existingSummarizedUpdates, toV) {
|
|||
toV = update.v + 1
|
||||
}
|
||||
|
||||
// Skip empty and history-resync updates (only record their version).
|
||||
// Empty updates are updates that only contain comment operations. We don't have a UI for
|
||||
// Skip empty updates (only record their version). Empty updates are
|
||||
// updates that only contain comment operations. We don't have a UI for
|
||||
// these yet.
|
||||
if (isUpdateEmpty(update) || isHistoryResyncUpdate(update)) {
|
||||
if (isUpdateEmpty(update)) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -282,8 +282,11 @@ function _shouldMergeUpdate(update, summarizedUpdate, labels) {
|
|||
const updateHasFileOps = update.project_ops.length > 0
|
||||
const summarizedUpdateHasTextOps = summarizedUpdate.pathnames.size > 0
|
||||
const summarizedUpdateHasFileOps = summarizedUpdate.project_ops.length > 0
|
||||
const isHistoryResync =
|
||||
update.meta.origin &&
|
||||
['history-resync', 'history-migration'].includes(update.meta.origin.kind)
|
||||
if (
|
||||
!isHistoryResyncUpdate(update) &&
|
||||
!isHistoryResync &&
|
||||
((updateHasTextOps && summarizedUpdateHasFileOps) ||
|
||||
(updateHasFileOps && summarizedUpdateHasTextOps))
|
||||
) {
|
||||
|
@ -343,10 +346,3 @@ function _mergeUpdate(update, summarizedUpdate) {
|
|||
function isUpdateEmpty(update) {
|
||||
return update.project_ops.length === 0 && update.pathnames.length === 0
|
||||
}
|
||||
|
||||
function isHistoryResyncUpdate(update) {
|
||||
return (
|
||||
update.meta.origin?.kind === 'history-resync' ||
|
||||
update.meta.origin?.kind === 'history-migration'
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,16 +1,3 @@
|
|||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-undef,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import sinon from 'sinon'
|
||||
import { expect } from 'chai'
|
||||
import { strict as esmock } from 'esmock'
|
||||
|
@ -23,11 +10,11 @@ describe('DiffGenerator', function () {
|
|||
this.ts = Date.now()
|
||||
this.user_id = 'mock-user-id'
|
||||
this.user_id_2 = 'mock-user-id-2'
|
||||
return (this.meta = {
|
||||
this.meta = {
|
||||
start_ts: this.ts,
|
||||
end_ts: this.ts,
|
||||
user_id: this.user_id,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('buildDiff', function () {
|
||||
|
@ -43,18 +30,15 @@ describe('DiffGenerator', function () {
|
|||
.stub()
|
||||
.returns(this.diff)
|
||||
this.DiffGenerator._mocks.compressDiff = sinon.stub().returns(this.diff)
|
||||
return (this.result = this.DiffGenerator.buildDiff(
|
||||
this.content,
|
||||
this.updates
|
||||
))
|
||||
this.result = this.DiffGenerator.buildDiff(this.content, this.updates)
|
||||
})
|
||||
|
||||
it('should return the diff', function () {
|
||||
return this.result.should.deep.equal(this.diff)
|
||||
this.result.should.deep.equal(this.diff)
|
||||
})
|
||||
|
||||
it('should build the content into an initial diff', function () {
|
||||
return this.DiffGenerator._mocks.applyUpdateToDiff
|
||||
this.DiffGenerator._mocks.applyUpdateToDiff
|
||||
.calledWith(
|
||||
[
|
||||
{
|
||||
|
@ -67,106 +51,127 @@ describe('DiffGenerator', function () {
|
|||
})
|
||||
|
||||
it('should apply each update', function () {
|
||||
return Array.from(this.updates).map(update =>
|
||||
this.updates.map(update =>
|
||||
this.DiffGenerator._mocks.applyUpdateToDiff
|
||||
.calledWith(sinon.match.any, update)
|
||||
.should.equal(true)
|
||||
)
|
||||
})
|
||||
|
||||
return it('should compress the diff', function () {
|
||||
return this.DiffGenerator._mocks.compressDiff
|
||||
it('should compress the diff', function () {
|
||||
this.DiffGenerator._mocks.compressDiff
|
||||
.calledWith(this.diff)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('compressDiff', function () {
|
||||
describe('with adjacent inserts with the same user_id', function () {
|
||||
return it('should create one update with combined meta data and min/max timestamps', function () {
|
||||
describe('with adjacent inserts with the same user id', function () {
|
||||
it('should create one update with combined meta data and min/max timestamps', function () {
|
||||
const diff = this.DiffGenerator.compressDiff([
|
||||
{
|
||||
i: 'foo',
|
||||
meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } },
|
||||
meta: { start_ts: 10, end_ts: 20, users: [this.user_id] },
|
||||
},
|
||||
{
|
||||
i: 'bar',
|
||||
meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } },
|
||||
meta: { start_ts: 5, end_ts: 15, users: [this.user_id] },
|
||||
},
|
||||
])
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{
|
||||
i: 'foobar',
|
||||
meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } },
|
||||
meta: { start_ts: 5, end_ts: 20, users: [this.user_id] },
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with adjacent inserts with different user_ids', function () {
|
||||
return it('should leave the inserts unchanged', function () {
|
||||
describe('with adjacent inserts with different user ids', function () {
|
||||
it('should leave the inserts unchanged', function () {
|
||||
const input = [
|
||||
{
|
||||
i: 'foo',
|
||||
meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } },
|
||||
meta: { start_ts: 10, end_ts: 20, users: [this.user_id] },
|
||||
},
|
||||
{
|
||||
i: 'bar',
|
||||
meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } },
|
||||
meta: { start_ts: 5, end_ts: 15, users: [this.user_id_2] },
|
||||
},
|
||||
]
|
||||
const output = this.DiffGenerator.compressDiff(input)
|
||||
return expect(output).to.deep.equal(input)
|
||||
expect(output).to.deep.equal(input)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with adjacent deletes with the same user_id', function () {
|
||||
return it('should create one update with combined meta data and min/max timestamps', function () {
|
||||
describe('with adjacent deletes with the same user id', function () {
|
||||
it('should create one update with combined meta data and min/max timestamps', function () {
|
||||
const diff = this.DiffGenerator.compressDiff([
|
||||
{
|
||||
d: 'foo',
|
||||
meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } },
|
||||
meta: { start_ts: 10, end_ts: 20, users: [this.user_id] },
|
||||
},
|
||||
{
|
||||
d: 'bar',
|
||||
meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id } },
|
||||
meta: { start_ts: 5, end_ts: 15, users: [this.user_id] },
|
||||
},
|
||||
])
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{
|
||||
d: 'foobar',
|
||||
meta: { start_ts: 5, end_ts: 20, user: { id: this.user_id } },
|
||||
meta: { start_ts: 5, end_ts: 20, users: [this.user_id] },
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
return describe('with adjacent deletes with different user_ids', function () {
|
||||
return it('should leave the deletes unchanged', function () {
|
||||
describe('with adjacent deletes with different user ids', function () {
|
||||
it('should leave the deletes unchanged', function () {
|
||||
const input = [
|
||||
{
|
||||
d: 'foo',
|
||||
meta: { start_ts: 10, end_ts: 20, user: { id: this.user_id } },
|
||||
meta: { start_ts: 10, end_ts: 20, users: [this.user_id] },
|
||||
},
|
||||
{
|
||||
d: 'bar',
|
||||
meta: { start_ts: 5, end_ts: 15, user: { id: this.user_id_2 } },
|
||||
meta: { start_ts: 5, end_ts: 15, users: [this.user_id_2] },
|
||||
},
|
||||
]
|
||||
const output = this.DiffGenerator.compressDiff(input)
|
||||
return expect(output).to.deep.equal(input)
|
||||
expect(output).to.deep.equal(input)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with history resync updates', function () {
|
||||
it('should keep only inserts and mark them as unchanged text', function () {
|
||||
const input = [
|
||||
{ u: 'untracked text' },
|
||||
{
|
||||
i: 'inserted anonymously',
|
||||
meta: { origin: { kind: 'history-resync' } },
|
||||
},
|
||||
{
|
||||
d: 'deleted anonymously',
|
||||
meta: { origin: { kind: 'history-resync' } },
|
||||
},
|
||||
]
|
||||
const output = this.DiffGenerator.compressDiff(input)
|
||||
expect(output).to.deep.equal([
|
||||
{ u: 'untracked text' },
|
||||
{ u: 'inserted anonymously' },
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return describe('applyUpdateToDiff', function () {
|
||||
describe('applyUpdateToDiff', function () {
|
||||
describe('an insert', function () {
|
||||
it('should insert into the middle of (u)nchanged text', function () {
|
||||
const diff = this.DiffGenerator.applyUpdateToDiff([{ u: 'foobar' }], {
|
||||
op: [{ p: 3, i: 'baz' }],
|
||||
meta: this.meta,
|
||||
})
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: 'foo' },
|
||||
{ i: 'baz', meta: this.meta },
|
||||
{ u: 'bar' },
|
||||
|
@ -178,7 +183,7 @@ describe('DiffGenerator', function () {
|
|||
op: [{ p: 0, i: 'baz' }],
|
||||
meta: this.meta,
|
||||
})
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ i: 'baz', meta: this.meta },
|
||||
{ u: 'foobar' },
|
||||
])
|
||||
|
@ -189,7 +194,7 @@ describe('DiffGenerator', function () {
|
|||
op: [{ p: 6, i: 'baz' }],
|
||||
meta: this.meta,
|
||||
})
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: 'foobar' },
|
||||
{ i: 'baz', meta: this.meta },
|
||||
])
|
||||
|
@ -200,19 +205,19 @@ describe('DiffGenerator', function () {
|
|||
[{ i: 'foobar', meta: this.meta }],
|
||||
{ op: [{ p: 3, i: 'baz' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ i: 'foo', meta: this.meta },
|
||||
{ i: 'baz', meta: this.meta },
|
||||
{ i: 'bar', meta: this.meta },
|
||||
])
|
||||
})
|
||||
|
||||
return it('should not count deletes in the running length total', function () {
|
||||
it('should not count deletes in the running length total', function () {
|
||||
const diff = this.DiffGenerator.applyUpdateToDiff(
|
||||
[{ d: 'deleted', meta: this.meta }, { u: 'foobar' }],
|
||||
{ op: [{ p: 3, i: 'baz' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ d: 'deleted', meta: this.meta },
|
||||
{ u: 'foo' },
|
||||
{ i: 'baz', meta: this.meta },
|
||||
|
@ -221,14 +226,14 @@ describe('DiffGenerator', function () {
|
|||
})
|
||||
})
|
||||
|
||||
return describe('a delete', function () {
|
||||
describe('a delete', function () {
|
||||
describe('deleting unchanged text', function () {
|
||||
it('should delete from the middle of (u)nchanged text', function () {
|
||||
const diff = this.DiffGenerator.applyUpdateToDiff(
|
||||
[{ u: 'foobazbar' }],
|
||||
{ op: [{ p: 3, d: 'baz' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: 'foo' },
|
||||
{ d: 'baz', meta: this.meta },
|
||||
{ u: 'bar' },
|
||||
|
@ -240,7 +245,7 @@ describe('DiffGenerator', function () {
|
|||
[{ u: 'foobazbar' }],
|
||||
{ op: [{ p: 0, d: 'foo' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ d: 'foo', meta: this.meta },
|
||||
{ u: 'bazbar' },
|
||||
])
|
||||
|
@ -251,18 +256,18 @@ describe('DiffGenerator', function () {
|
|||
[{ u: 'foobazbar' }],
|
||||
{ op: [{ p: 6, d: 'bar' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: 'foobaz' },
|
||||
{ d: 'bar', meta: this.meta },
|
||||
])
|
||||
})
|
||||
|
||||
return it('should delete across multiple (u)changed text parts', function () {
|
||||
it('should delete across multiple (u)changed text parts', function () {
|
||||
const diff = this.DiffGenerator.applyUpdateToDiff(
|
||||
[{ u: 'foo' }, { u: 'baz' }, { u: 'bar' }],
|
||||
{ op: [{ p: 2, d: 'obazb' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: 'fo' },
|
||||
{ d: 'o', meta: this.meta },
|
||||
{ d: 'baz', meta: this.meta },
|
||||
|
@ -278,7 +283,7 @@ describe('DiffGenerator', function () {
|
|||
[{ i: 'foobazbar', meta: this.meta }],
|
||||
{ op: [{ p: 3, d: 'baz' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ i: 'foo', meta: this.meta },
|
||||
{ i: 'bar', meta: this.meta },
|
||||
])
|
||||
|
@ -289,7 +294,7 @@ describe('DiffGenerator', function () {
|
|||
[{ i: 'foobazbar', meta: this.meta }],
|
||||
{ op: [{ p: 0, d: 'foo' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([{ i: 'bazbar', meta: this.meta }])
|
||||
expect(diff).to.deep.equal([{ i: 'bazbar', meta: this.meta }])
|
||||
})
|
||||
|
||||
it('should delete from the end of (u)nchanged text', function () {
|
||||
|
@ -297,15 +302,15 @@ describe('DiffGenerator', function () {
|
|||
[{ i: 'foobazbar', meta: this.meta }],
|
||||
{ op: [{ p: 6, d: 'bar' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([{ i: 'foobaz', meta: this.meta }])
|
||||
expect(diff).to.deep.equal([{ i: 'foobaz', meta: this.meta }])
|
||||
})
|
||||
|
||||
return it('should delete across multiple (u)changed and (i)nserted text parts', function () {
|
||||
it('should delete across multiple (u)changed and (i)nserted text parts', function () {
|
||||
const diff = this.DiffGenerator.applyUpdateToDiff(
|
||||
[{ u: 'foo' }, { i: 'baz', meta: this.meta }, { u: 'bar' }],
|
||||
{ op: [{ p: 2, d: 'obazb' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: 'fo' },
|
||||
{ d: 'o', meta: this.meta },
|
||||
{ d: 'b', meta: this.meta },
|
||||
|
@ -315,12 +320,12 @@ describe('DiffGenerator', function () {
|
|||
})
|
||||
|
||||
describe('deleting over existing deletes', function () {
|
||||
return it('should delete across multiple (u)changed and (d)deleted text parts', function () {
|
||||
it('should delete across multiple (u)changed and (d)deleted text parts', function () {
|
||||
const diff = this.DiffGenerator.applyUpdateToDiff(
|
||||
[{ u: 'foo' }, { d: 'baz', meta: this.meta }, { u: 'bar' }],
|
||||
{ op: [{ p: 2, d: 'ob' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: 'fo' },
|
||||
{ d: 'o', meta: this.meta },
|
||||
{ d: 'baz', meta: this.meta },
|
||||
|
@ -332,7 +337,7 @@ describe('DiffGenerator', function () {
|
|||
|
||||
describe("deleting when the text doesn't match", function () {
|
||||
it('should throw an error when deleting from the middle of (u)nchanged text', function () {
|
||||
return expect(() =>
|
||||
expect(() =>
|
||||
this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
|
||||
op: [{ p: 3, d: 'xxx' }],
|
||||
meta: this.meta,
|
||||
|
@ -341,7 +346,7 @@ describe('DiffGenerator', function () {
|
|||
})
|
||||
|
||||
it('should throw an error when deleting from the start of (u)nchanged text', function () {
|
||||
return expect(() =>
|
||||
expect(() =>
|
||||
this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
|
||||
op: [{ p: 0, d: 'xxx' }],
|
||||
meta: this.meta,
|
||||
|
@ -349,8 +354,8 @@ describe('DiffGenerator', function () {
|
|||
).to.throw(this.DiffGenerator.ConsistencyError)
|
||||
})
|
||||
|
||||
return it('should throw an error when deleting from the end of (u)nchanged text', function () {
|
||||
return expect(() =>
|
||||
it('should throw an error when deleting from the end of (u)nchanged text', function () {
|
||||
expect(() =>
|
||||
this.DiffGenerator.applyUpdateToDiff([{ u: 'foobazbar' }], {
|
||||
op: [{ p: 6, d: 'xxx' }],
|
||||
meta: this.meta,
|
||||
|
@ -360,12 +365,12 @@ describe('DiffGenerator', function () {
|
|||
})
|
||||
|
||||
describe('when the last update in the existing diff is a delete', function () {
|
||||
return it('should insert the new update before the delete', function () {
|
||||
it('should insert the new update before the delete', function () {
|
||||
const diff = this.DiffGenerator.applyUpdateToDiff(
|
||||
[{ u: 'foo' }, { d: 'bar', meta: this.meta }],
|
||||
{ op: [{ p: 3, i: 'baz' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: 'foo' },
|
||||
{ i: 'baz', meta: this.meta },
|
||||
{ d: 'bar', meta: this.meta },
|
||||
|
@ -373,13 +378,13 @@ describe('DiffGenerator', function () {
|
|||
})
|
||||
})
|
||||
|
||||
return describe('when the only update in the existing diff is a delete', function () {
|
||||
return it('should insert the new update after the delete', function () {
|
||||
describe('when the only update in the existing diff is a delete', function () {
|
||||
it('should insert the new update after the delete', function () {
|
||||
const diff = this.DiffGenerator.applyUpdateToDiff(
|
||||
[{ d: 'bar', meta: this.meta }],
|
||||
{ op: [{ p: 0, i: 'baz' }], meta: this.meta }
|
||||
)
|
||||
return expect(diff).to.deep.equal([
|
||||
expect(diff).to.deep.equal([
|
||||
{ d: 'bar', meta: this.meta },
|
||||
{ i: 'baz', meta: this.meta },
|
||||
])
|
||||
|
|
|
@ -595,12 +595,12 @@ describe('SummarizedUpdatesManager', function () {
|
|||
makeUpdate({
|
||||
startTs: 20,
|
||||
v: 3,
|
||||
origin: { kind: 'origin-a' },
|
||||
origin: { kind: 'history-resync' },
|
||||
}),
|
||||
makeUpdate({
|
||||
startTs: 30,
|
||||
v: 4,
|
||||
origin: { kind: 'origin-a' },
|
||||
origin: { kind: 'history-resync' },
|
||||
}),
|
||||
makeUpdate({ startTs: 40, v: 5 }),
|
||||
makeUpdate({ startTs: 50, v: 6 }),
|
||||
|
@ -617,7 +617,7 @@ describe('SummarizedUpdatesManager', function () {
|
|||
endTs: 40,
|
||||
fromV: 3,
|
||||
toV: 5,
|
||||
origin: { kind: 'origin-a' },
|
||||
origin: { kind: 'history-resync' },
|
||||
}),
|
||||
makeSummary({ startTs: 0, endTs: 20, fromV: 1, toV: 3 }),
|
||||
]
|
||||
|
@ -687,7 +687,13 @@ describe('SummarizedUpdatesManager', function () {
|
|||
describe('history resync updates', function () {
|
||||
setupChunks([
|
||||
[
|
||||
makeUpdate({ startTs: 0, v: 1 }),
|
||||
makeUpdate({
|
||||
startTs: 0,
|
||||
v: 1,
|
||||
origin: { kind: 'history-resync' },
|
||||
projectOps: [{ add: { pathname: 'file1.tex' } }],
|
||||
pathnames: [],
|
||||
}),
|
||||
makeUpdate({
|
||||
startTs: 20,
|
||||
v: 2,
|
||||
|
@ -703,13 +709,47 @@ describe('SummarizedUpdatesManager', function () {
|
|||
v: 3,
|
||||
origin: { kind: 'history-resync' },
|
||||
projectOps: [{ add: { pathname: 'file4.tex' } }],
|
||||
pathnames: [],
|
||||
}),
|
||||
makeUpdate({ startTs: 60, v: 4 }),
|
||||
makeUpdate({ startTs: 80, v: 5 }),
|
||||
makeUpdate({
|
||||
startTs: 60,
|
||||
v: 4,
|
||||
origin: { kind: 'history-resync' },
|
||||
projectOps: [],
|
||||
pathnames: ['file1.tex', 'file2.tex', 'file5.tex'],
|
||||
}),
|
||||
makeUpdate({
|
||||
startTs: 80,
|
||||
v: 5,
|
||||
origin: { kind: 'history-resync' },
|
||||
projectOps: [],
|
||||
pathnames: ['file4.tex'],
|
||||
}),
|
||||
makeUpdate({ startTs: 100, v: 6, pathnames: ['file1.tex'] }),
|
||||
],
|
||||
])
|
||||
expectSummaries('should skip history-resync updates', {}, [
|
||||
makeSummary({ startTs: 0, endTs: 90, fromV: 1, toV: 6 }),
|
||||
expectSummaries('should merge creates and edits', {}, [
|
||||
makeSummary({
|
||||
startTs: 100,
|
||||
endTs: 110,
|
||||
fromV: 6,
|
||||
toV: 7,
|
||||
pathnames: ['file1.tex'],
|
||||
}),
|
||||
makeSummary({
|
||||
startTs: 0,
|
||||
endTs: 90,
|
||||
fromV: 1,
|
||||
toV: 6,
|
||||
origin: { kind: 'history-resync' },
|
||||
pathnames: ['file5.tex'],
|
||||
projectOps: [
|
||||
{ add: { pathname: 'file4.tex' }, atV: 3 },
|
||||
{ add: { pathname: 'file2.tex' }, atV: 2 },
|
||||
{ add: { pathname: 'file3.tex' }, atV: 2 },
|
||||
{ add: { pathname: 'file1.tex' }, atV: 1 },
|
||||
],
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -580,6 +580,7 @@
|
|||
"history_label_project_current_state": "",
|
||||
"history_label_this_version": "",
|
||||
"history_new_label_name": "",
|
||||
"history_resync": "",
|
||||
"history_view_a11y_description": "",
|
||||
"history_view_all": "",
|
||||
"history_view_labels": "",
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function HistoryResyncChange() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return <div>{t('history_resync')}</div>
|
||||
}
|
||||
|
||||
export default HistoryResyncChange
|
|
@ -20,6 +20,7 @@ import CompareItems from './dropdown/menu-item/compare-items'
|
|||
import CompareVersionDropdown from './dropdown/compare-version-dropdown'
|
||||
import { CompareVersionDropdownContentAllHistory } from './dropdown/compare-version-dropdown-content'
|
||||
import FileRestoreChange from './file-restore-change'
|
||||
import HistoryResyncChange from './history-resync-change'
|
||||
|
||||
type HistoryVersionProps = {
|
||||
update: LoadedUpdate
|
||||
|
@ -167,18 +168,24 @@ function HistoryVersion({
|
|||
))}
|
||||
{update.meta.origin?.kind === 'file-restore' ? (
|
||||
<FileRestoreChange origin={update.meta.origin} />
|
||||
) : update.meta.origin?.kind === 'history-resync' ? (
|
||||
<HistoryResyncChange />
|
||||
) : (
|
||||
<Changes
|
||||
pathnames={update.pathnames}
|
||||
projectOps={update.project_ops}
|
||||
/>
|
||||
)}
|
||||
{update.meta.origin?.kind !== 'history-resync' ? (
|
||||
<>
|
||||
<MetadataUsersList
|
||||
users={update.meta.users}
|
||||
origin={update.meta.origin}
|
||||
currentUserId={currentUserId}
|
||||
/>
|
||||
<Origin origin={update.meta.origin} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</HistoryVersionDetails>
|
||||
</div>
|
||||
|
|
|
@ -855,6 +855,7 @@
|
|||
"history_label_project_current_state": "Current state",
|
||||
"history_label_this_version": "Label this version",
|
||||
"history_new_label_name": "New label name",
|
||||
"history_resync": "History resync",
|
||||
"history_view_a11y_description": "Show all of the project history or only labelled versions.",
|
||||
"history_view_all": "All history",
|
||||
"history_view_labels": "Labels",
|
||||
|
|
Loading…
Reference in a new issue