Просмотр исходного кода

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
Domagoj Kriskovic 1 год назад
Родитель
Сommit
cffad44264

+ 12 - 0
services/web/app/src/Features/History/RestoreManager.js

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

+ 105 - 1
services/web/test/unit/src/History/RestoreManagerTests.js

@@ -225,7 +225,10 @@ describe('RestoreManager', function () {
       this.ProjectGetter.promises.getProject = sinon.stub()
       this.ProjectGetter.promises.getProject = sinon.stub()
       this.ProjectGetter.promises.getProject
       this.ProjectGetter.promises.getProject
         .withArgs(this.project_id)
         .withArgs(this.project_id)
-        .resolves({ overleaf: { history: { rangesSupportEnabled: true } } })
+        .resolves({
+          overleaf: { history: { rangesSupportEnabled: true } },
+          rootDoc_id: 'root-doc-id',
+        })
       this.RestoreManager.promises._writeFileVersionToDisk = sinon
       this.RestoreManager.promises._writeFileVersionToDisk = sinon
         .stub()
         .stub()
         .resolves((this.fsPath = '/tmp/path/on/disk'))
         .resolves((this.fsPath = '/tmp/path/on/disk'))
@@ -405,6 +408,13 @@ describe('RestoreManager', function () {
 
 
       describe('with an existing file in the current project', function () {
       describe('with an existing file in the current project', function () {
         beforeEach(async 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
           this.ProjectLocator.promises.findElementByPath = sinon
             .stub()
             .stub()
             .resolves({ type: 'file', element: { _id: 'mock-file-id' } })
             .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 () {
       describe('with an existing document in the current project', function () {
         beforeEach(async 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
           this.ProjectLocator.promises.findElementByPath = sinon
             .stub()
             .stub()
             .resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
             .resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
@@ -512,6 +529,13 @@ describe('RestoreManager', function () {
       describe('with comments in same doc', function () {
       describe('with comments in same doc', function () {
         // copy of the above, addition: inject and later inspect threadIds set
         // copy of the above, addition: inject and later inspect threadIds set
         beforeEach(async 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
           this.ProjectLocator.promises.findElementByPath = sinon
             .stub()
             .stub()
             .resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
             .resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
@@ -595,6 +619,13 @@ describe('RestoreManager', function () {
       describe('with remapped comments during revertProject', function () {
       describe('with remapped comments during revertProject', function () {
         // copy of the above, addition: inject and later inspect threadIds set
         // copy of the above, addition: inject and later inspect threadIds set
         beforeEach(async 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
           this.ProjectLocator.promises.findElementByPath = sinon
             .stub()
             .stub()
             .resolves({ type: 'doc', element: { _id: 'mock-file-id' } })
             .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 () {
     describe('reverting a file or document with metadata', function () {