Merge pull request #19867 from overleaf/em-hide-history-resync-details

Hide history resync changes

GitOrigin-RevId: a36876ff637d0a4c34150b21f4b4e623dff0ab03
This commit is contained in:
Eric Mc Sween 2024-08-13 08:53:39 -04:00 committed by Copybot
parent 40f3db3772
commit e10478f256
9 changed files with 227 additions and 163 deletions

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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'
)
}

View file

@ -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 },
])

View file

@ -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 },
],
}),
])
})
})

View file

@ -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": "",

View file

@ -0,0 +1,9 @@
import { useTranslation } from 'react-i18next'
function HistoryResyncChange() {
const { t } = useTranslation()
return <div>{t('history_resync')}</div>
}
export default HistoryResyncChange

View file

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

View file

@ -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",