Sfoglia il codice sorgente

[web] Add mainBibliographyDocId to projects (#20842)

GitOrigin-RevId: 5358ef5cf0b9aaeadfe360c1bdc575fd1bf7344d
Mathias Jakobsen 1 anno fa
parent
commit
c9ed5f6a79

+ 18 - 0
services/web/app/src/Features/Editor/EditorController.js

@@ -614,6 +614,24 @@ const EditorController = {
     )
   },
 
+  setMainBibliographyDoc(projectId, newBibliographyDocId, callback) {
+    ProjectEntityUpdateHandler.setMainBibliographyDoc(
+      projectId,
+      newBibliographyDocId,
+      function (err) {
+        if (err) {
+          return callback(err)
+        }
+        EditorRealTimeController.emitToRoom(
+          projectId,
+          'mainBibliographyDocUpdated',
+          newBibliographyDocId
+        )
+        callback()
+      }
+    )
+  },
+
   _notifyProjectUsersOfNewFolders(projectId, folders, callback) {
     async.eachSeries(
       folders,

+ 7 - 0
services/web/app/src/Features/Project/ProjectController.js

@@ -90,6 +90,13 @@ const _ProjectController = {
       await EditorController.promises.setRootDoc(projectId, req.body.rootDocId)
     }
 
+    if (req.body.mainBibliographyDocId != null) {
+      await EditorController.promises.setMainBibliographyDoc(
+        projectId,
+        req.body.mainBibliographyDocId
+      )
+    }
+
     res.sendStatus(204)
   },
 

+ 1 - 0
services/web/app/src/Features/Project/ProjectEditorHandler.js

@@ -23,6 +23,7 @@ module.exports = ProjectEditorHandler = {
       _id: project._id,
       name: project.name,
       rootDoc_id: project.rootDoc_id,
+      mainBibliographyDoc_id: project.mainBibliographyDoc_id,
       rootFolder: [this.buildFolderModelView(project.rootFolder[0])],
       publicAccesLevel: project.publicAccesLevel,
       dropboxEnabled: !!project.existsInDropbox,

+ 32 - 0
services/web/app/src/Features/Project/ProjectEntityUpdateHandler.js

@@ -1112,6 +1112,31 @@ const convertDocToFile = wrapWithLock({
   },
 })
 
+async function setMainBibliographyDoc(projectId, newBibliographyDocId) {
+  logger.debug(
+    { projectId, mainBibliographyDocId: newBibliographyDocId },
+    'setting main bibliography doc'
+  )
+  if (projectId == null || newBibliographyDocId == null) {
+    throw new Errors.InvalidError('missing arguments (project or doc)')
+  }
+  const docPath =
+    await ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId(
+      projectId,
+      newBibliographyDocId
+    )
+  if (ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc(docPath)) {
+    await Project.updateOne(
+      { _id: projectId },
+      { mainBibliographyDoc_id: newBibliographyDocId }
+    ).exec()
+  } else {
+    throw new Errors.UnsupportedFileTypeError(
+      'invalid file extension for main bibliography doc'
+    )
+  }
+}
+
 const ProjectEntityUpdateHandler = {
   LOCK_NAMESPACE,
 
@@ -1154,6 +1179,8 @@ const ProjectEntityUpdateHandler = {
 
   unsetRootDoc: callbackify(unsetRootDoc),
 
+  setMainBibliographyDoc: callbackify(setMainBibliographyDoc),
+
   updateDocLines: callbackify(updateDocLines),
 
   upsertDoc: callbackifyMultiResult(upsertDoc, ['doc', 'isNew']),
@@ -1502,6 +1529,11 @@ const ProjectEntityUpdateHandler = {
     return VALID_ROOT_DOC_REGEXP.test(docExtension)
   },
 
+  isPathValidForMainBibliographyDoc(docPath) {
+    const docExtension = Path.extname(docPath).toLowerCase()
+    return docExtension === '.bib'
+  },
+
   async _cleanUpEntity(
     project,
     newProject,

+ 1 - 0
services/web/app/src/models/Project.js

@@ -42,6 +42,7 @@ const ProjectSchema = new Schema(
     pendingEditor_refs: [{ type: ObjectId, ref: 'User' }],
     rootDoc_id: { type: ObjectId },
     rootFolder: [FolderSchema],
+    mainBibliographyDoc_id: { type: ObjectId },
     version: { type: Number }, // incremented for every change in the project structure (folders and filenames)
     publicAccesLevel: { type: String, default: 'private' },
     compiler: { type: String, default: 'pdflatex' },

+ 9 - 0
services/web/frontend/js/features/ide-react/context/ide-react-context.tsx

@@ -147,10 +147,19 @@ export const IdeReactProvider: FC = ({ children }) => {
       setProjectJoined(true)
     }
 
+    function handleMainBibliographyDocUpdated(payload: string) {
+      scopeStore.set('project.mainBibliographyDoc_id', payload)
+    }
+
     socket.on('joinProjectResponse', handleJoinProjectResponse)
+    socket.on('mainBibliographyDocUpdated', handleMainBibliographyDocUpdated)
 
     return () => {
       socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
+      socket.removeListener(
+        'mainBibliographyDocUpdated',
+        handleMainBibliographyDocUpdated
+      )
     }
   }, [socket, eventEmitter, scopeStore])
 

+ 3 - 0
services/web/frontend/js/shared/context/project-context.tsx

@@ -40,6 +40,7 @@ export const ProjectProvider: FC = ({ children }) => {
     publicAccesLevel: publicAccessLevel,
     owner,
     trackChangesState,
+    mainBibliographyDoc_id: mainBibliographyDocId,
   } = project || projectFallback
 
   const tags = useMemo(
@@ -63,6 +64,7 @@ export const ProjectProvider: FC = ({ children }) => {
       owner,
       tags,
       trackChangesState,
+      mainBibliographyDocId,
     }
   }, [
     _id,
@@ -76,6 +78,7 @@ export const ProjectProvider: FC = ({ children }) => {
     owner,
     tags,
     trackChangesState,
+    mainBibliographyDocId,
   ])
 
   return (

+ 1 - 0
services/web/frontend/js/shared/context/types/project-context.tsx

@@ -14,6 +14,7 @@ export type ProjectContextValue = {
   _id: string
   name: string
   rootDocId?: string
+  mainBibliographyDocId?: string
   compiler: string
   members: ProjectContextMember[]
   invites: ProjectContextMember[]

+ 69 - 0
services/web/test/unit/src/Editor/EditorControllerTests.js

@@ -957,4 +957,73 @@ describe('EditorController', function () {
         .should.equal(true)
     })
   })
+
+  describe('setMainBibliographyDoc', function () {
+    describe('on success', function () {
+      beforeEach(function (done) {
+        this.mainBibliographyId = 'bib-doc-id'
+        this.ProjectEntityUpdateHandler.setMainBibliographyDoc = sinon
+          .stub()
+          .yields()
+
+        this.callback = sinon.stub().callsFake(done)
+        this.EditorController.setMainBibliographyDoc(
+          this.project_id,
+          this.mainBibliographyId,
+          this.callback
+        )
+      })
+
+      it('should forward the call to the ProjectEntityUpdateHandler', function () {
+        expect(
+          this.ProjectEntityUpdateHandler.setMainBibliographyDoc
+        ).to.have.been.calledWith(this.project_id, this.mainBibliographyId)
+      })
+
+      it('should emit the update to the room', function () {
+        expect(
+          this.EditorRealTimeController.emitToRoom
+        ).to.have.been.calledWith(
+          this.project_id,
+          'mainBibliographyDocUpdated',
+          this.mainBibliographyId
+        )
+      })
+
+      it('should return nothing', function () {
+        expect(this.callback).to.have.been.calledWithExactly()
+      })
+    })
+
+    describe('on error', function () {
+      beforeEach(function (done) {
+        this.mainBibliographyId = 'bib-doc-id'
+        this.error = new Error('oh no')
+        this.ProjectEntityUpdateHandler.setMainBibliographyDoc = sinon
+          .stub()
+          .yields(this.error)
+
+        this.callback = sinon.stub().callsFake(() => done())
+        this.EditorController.setMainBibliographyDoc(
+          this.project_id,
+          this.mainBibliographyId,
+          this.callback
+        )
+      })
+
+      it('should forward the call to the ProjectEntityUpdateHandler', function () {
+        expect(
+          this.ProjectEntityUpdateHandler.setMainBibliographyDoc
+        ).to.have.been.calledWith(this.project_id, this.mainBibliographyId)
+      })
+
+      it('should return the error', function () {
+        expect(this.callback).to.have.been.calledWithExactly(this.error)
+      })
+
+      it('should not emit the update to the room', function () {
+        expect(this.EditorRealTimeController.emitToRoom).to.not.have.been.called
+      })
+    })
+  })
 })

+ 132 - 0
services/web/test/unit/src/Project/ProjectEntityUpdateHandlerTests.js

@@ -3001,4 +3001,136 @@ describe('ProjectEntityUpdateHandler', function () {
       })
     })
   })
+
+  describe('isPathValidForMainBibliographyDoc', function () {
+    it('should not allow other endings than .bib', function () {
+      const endings = ['.tex', '.png', '.jpg', '.pdf', '.docx', '.doc']
+      endings.forEach(ending => {
+        expect(
+          this.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc(
+            `/foo/bar/baz${ending}`
+          )
+        ).to.be.false
+      })
+    })
+
+    it('should allow a mix of lower and uppercase letters', function () {
+      const endings = ['.bib', '.BiB', '.BIB', '.bIB']
+      endings.forEach(ending => {
+        expect(
+          this.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc(
+            `/foo/bar/baz.${ending}`
+          )
+        ).to.be.true
+      })
+    })
+
+    it('should not allow a path without an extension', function () {
+      expect(
+        this.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc(
+          '/foo/bar/baz'
+        )
+      ).to.be.false
+    })
+
+    it('should not allow the empty path', function () {
+      expect(
+        this.ProjectEntityUpdateHandler.isPathValidForMainBibliographyDoc('')
+      ).to.be.false
+    })
+  })
+
+  describe('setMainBibliographyDoc', function () {
+    describe('on success', function () {
+      beforeEach(function (done) {
+        this.doc = {
+          _id: new ObjectId(),
+          name: 'test.bib',
+        }
+        this.path = '/path/to/test.bib'
+        this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId
+          .withArgs(this.project._id, this.doc._id)
+          .resolves(this.path)
+
+        this.callback = sinon.stub().callsFake(() => done())
+
+        this.ProjectEntityUpdateHandler.setMainBibliographyDoc(
+          this.project._id,
+          this.doc._id,
+          this.callback
+        )
+      })
+
+      it('should update the project with the new main bibliography doc', function () {
+        expect(this.ProjectModel.updateOne).to.have.been.calledWith(
+          { _id: this.project._id },
+          { mainBibliographyDoc_id: this.doc._id }
+        )
+      })
+    })
+
+    describe('on failure', function () {
+      describe("when document can't be found", function () {
+        beforeEach(function (done) {
+          this.doc = {
+            _id: new ObjectId(),
+            name: 'test.bib',
+          }
+          this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId
+            .withArgs(this.project._id, this.doc._id)
+            .rejects(new Error('error'))
+
+          this.callback = sinon.stub().callsFake(() => done())
+
+          this.ProjectEntityUpdateHandler.setMainBibliographyDoc(
+            this.project._id,
+            this.doc._id,
+            this.callback
+          )
+        })
+
+        it('should call the callback with an error', function () {
+          expect(this.callback).to.have.been.calledWith(
+            sinon.match.instanceOf(Error)
+          )
+        })
+
+        it('should not update the project with the new main bibliography doc', function () {
+          expect(this.ProjectModel.updateOne).to.not.have.been.called
+        })
+      })
+
+      describe("when path is not a bib file can't be found", function () {
+        beforeEach(function (done) {
+          this.doc = {
+            _id: new ObjectId(),
+            name: 'test.bib',
+          }
+
+          this.path = '/path/to/test.tex'
+          this.ProjectEntityHandler.promises.getDocPathByProjectIdAndDocId
+            .withArgs(this.project._id, this.doc._id)
+            .resolves(this.path)
+
+          this.callback = sinon.stub().callsFake(() => done())
+
+          this.ProjectEntityUpdateHandler.setMainBibliographyDoc(
+            this.project._id,
+            this.doc._id,
+            this.callback
+          )
+        })
+
+        it('should call the callback with an error', function () {
+          expect(this.callback).to.have.been.calledWith(
+            sinon.match.instanceOf(Error)
+          )
+        })
+
+        it('should not update the project with the new main bibliography doc', function () {
+          expect(this.ProjectModel.updateOne).to.not.have.been.called
+        })
+      })
+    })
+  })
 })