Add support for handling deleted root document in RestoreManager (#28008)

* Skip opening root document if delete originated from a file-restore

* handle project-restore origin

* Refactor isFileRestore logic

* Add support for handling deleted root document in RestoreManager

GitOrigin-RevId: 837144aa6e269cbffebf82624f58e8219fe654c4
This commit is contained in:
Domagoj Kriskovic
2025-08-26 13:01:37 +02:00
committed by Copybot
parent d82dcc382a
commit 511d2d104b
2 changed files with 117 additions and 1 deletions

View File

@@ -79,6 +79,7 @@ const RestoreManager = {
) {
const project = await ProjectGetter.promises.getProject(projectId, {
overleaf: true,
rootDoc_id: true,
})
if (!project?.overleaf?.history?.rangesSupportEnabled) {
throw new OError('project does not have ranges support', { projectId })
@@ -125,6 +126,7 @@ const RestoreManager = {
pathname
)
let hadDeletedRootFile = false
if (file) {
if (file.type !== 'doc' && file.type !== 'file') {
throw new OError('unexpected file type', { type: file.type })
@@ -140,6 +142,11 @@ const RestoreManager = {
origin,
userId
)
if (file.element._id.toString() === project.rootDoc_id.toString()) {
hadDeletedRootFile = true
}
threadIds.delete(file.element._id.toString())
}
@@ -274,6 +281,11 @@ const RestoreManager = {
origin,
userId
)
if (hadDeletedRootFile) {
await EditorController.promises.setRootDoc(projectId, _id)
}
// For revertProject: The next doc that gets reverted will need to duplicate all the threads seen here.
threadIds.set(
_id.toString(),

View File

@@ -225,7 +225,10 @@ describe('RestoreManager', function () {
this.ProjectGetter.promises.getProject = sinon.stub()
this.ProjectGetter.promises.getProject
.withArgs(this.project_id)
.resolves({ overleaf: { history: { rangesSupportEnabled: true } } })
.resolves({
overleaf: { history: { rangesSupportEnabled: true } },
rootDoc_id: 'root-doc-id',
})
this.RestoreManager.promises._writeFileVersionToDisk = sinon
.stub()
.resolves((this.fsPath = '/tmp/path/on/disk'))
@@ -405,6 +408,13 @@ describe('RestoreManager', function () {
describe('with an existing file in the current project', function () {
beforeEach(async function () {
this.ProjectGetter.promises.getProject = sinon.stub()
this.ProjectGetter.promises.getProject
.withArgs(this.project_id)
.resolves({
overleaf: { history: { rangesSupportEnabled: true } },
rootDoc_id: 'root-doc-id',
})
this.ProjectLocator.promises.findElementByPath = sinon
.stub()
.resolves({ type: 'file', element: { _id: 'mock-file-id' } })
@@ -438,6 +448,13 @@ describe('RestoreManager', function () {
describe('with an existing document in the current project', function () {
beforeEach(async function () {
this.ProjectGetter.promises.getProject = sinon.stub()
this.ProjectGetter.promises.getProject
.withArgs(this.project_id)
.resolves({
overleaf: { history: { rangesSupportEnabled: true } },
rootDoc_id: 'root-doc-id',
})
this.ProjectLocator.promises.findElementByPath = sinon
.stub()
.resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
@@ -512,6 +529,13 @@ describe('RestoreManager', function () {
describe('with comments in same doc', function () {
// copy of the above, addition: inject and later inspect threadIds set
beforeEach(async function () {
this.ProjectGetter.promises.getProject = sinon.stub()
this.ProjectGetter.promises.getProject
.withArgs(this.project_id)
.resolves({
overleaf: { history: { rangesSupportEnabled: true } },
rootDoc_id: 'root-doc-id',
})
this.ProjectLocator.promises.findElementByPath = sinon
.stub()
.resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
@@ -595,6 +619,13 @@ describe('RestoreManager', function () {
describe('with remapped comments during revertProject', function () {
// copy of the above, addition: inject and later inspect threadIds set
beforeEach(async function () {
this.ProjectGetter.promises.getProject = sinon.stub()
this.ProjectGetter.promises.getProject
.withArgs(this.project_id)
.resolves({
overleaf: { history: { rangesSupportEnabled: true } },
rootDoc_id: 'root-doc-id',
})
this.ProjectLocator.promises.findElementByPath = sinon
.stub()
.resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
@@ -645,6 +676,79 @@ describe('RestoreManager', function () {
)
})
})
describe('when restored file has the same id as root doc', function () {
beforeEach(async function () {
this.ProjectGetter.promises.getProject = sinon.stub()
this.ProjectGetter.promises.getProject
.withArgs(this.project_id)
.resolves({
overleaf: { history: { rangesSupportEnabled: true } },
rootDoc_id: 'root-doc-id',
})
this.ProjectLocator.promises.findElementByPath = sinon
.stub()
.resolves({ type: 'doc', element: { _id: 'root-doc-id' } })
this.EditorController.promises.deleteEntity = sinon.stub().resolves()
this.EditorController.promises.addDocWithRanges = sinon
.stub()
.resolves((this.addedFile = { _id: 'new-doc-id', type: 'doc' }))
this.EditorController.promises.setRootDoc = sinon.stub().resolves()
this.data = await this.RestoreManager.promises.revertFile(
this.user_id,
this.project_id,
this.version,
this.pathname
)
})
it('should delete the existing root document', async function () {
expect(
this.EditorController.promises.deleteEntity
).to.have.been.calledWith(
this.project_id,
'root-doc-id',
'doc',
{
kind: 'file-restore',
path: this.pathname,
version: this.version,
timestamp: new Date(this.endTs).toISOString(),
},
this.user_id
)
})
it('should import the file', function () {
expect(
this.EditorController.promises.addDocWithRanges
).to.have.been.calledWith(
this.project_id,
this.folder_id,
'foo.tex',
['foo', 'bar', 'baz'],
{ changes: this.tracked_changes, comments: this.remappedComments },
{
kind: 'file-restore',
path: this.pathname,
version: this.version,
timestamp: new Date(this.endTs).toISOString(),
}
)
})
it('should return the created entity with root doc id', function () {
expect(this.data).to.deep.equal(this.addedFile)
expect(this.data._id).to.equal('new-doc-id')
})
it('should set the restored document as the new root doc', function () {
expect(
this.EditorController.promises.setRootDoc
).to.have.been.calledWith(this.project_id, this.addedFile._id)
})
})
})
describe('reverting a file or document with metadata', function () {