mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
[web] Add mainBibliographyDocId to projects (#20842)
GitOrigin-RevId: 5358ef5cf0b9aaeadfe360c1bdc575fd1bf7344d
This commit is contained in:
parent
9cafe8735d
commit
c9ed5f6a79
10 changed files with 273 additions and 0 deletions
|
@ -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) {
|
_notifyProjectUsersOfNewFolders(projectId, folders, callback) {
|
||||||
async.eachSeries(
|
async.eachSeries(
|
||||||
folders,
|
folders,
|
||||||
|
|
|
@ -90,6 +90,13 @@ const _ProjectController = {
|
||||||
await EditorController.promises.setRootDoc(projectId, req.body.rootDocId)
|
await EditorController.promises.setRootDoc(projectId, req.body.rootDocId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.body.mainBibliographyDocId != null) {
|
||||||
|
await EditorController.promises.setMainBibliographyDoc(
|
||||||
|
projectId,
|
||||||
|
req.body.mainBibliographyDocId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
res.sendStatus(204)
|
res.sendStatus(204)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ module.exports = ProjectEditorHandler = {
|
||||||
_id: project._id,
|
_id: project._id,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
rootDoc_id: project.rootDoc_id,
|
rootDoc_id: project.rootDoc_id,
|
||||||
|
mainBibliographyDoc_id: project.mainBibliographyDoc_id,
|
||||||
rootFolder: [this.buildFolderModelView(project.rootFolder[0])],
|
rootFolder: [this.buildFolderModelView(project.rootFolder[0])],
|
||||||
publicAccesLevel: project.publicAccesLevel,
|
publicAccesLevel: project.publicAccesLevel,
|
||||||
dropboxEnabled: !!project.existsInDropbox,
|
dropboxEnabled: !!project.existsInDropbox,
|
||||||
|
|
|
@ -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 = {
|
const ProjectEntityUpdateHandler = {
|
||||||
LOCK_NAMESPACE,
|
LOCK_NAMESPACE,
|
||||||
|
|
||||||
|
@ -1154,6 +1179,8 @@ const ProjectEntityUpdateHandler = {
|
||||||
|
|
||||||
unsetRootDoc: callbackify(unsetRootDoc),
|
unsetRootDoc: callbackify(unsetRootDoc),
|
||||||
|
|
||||||
|
setMainBibliographyDoc: callbackify(setMainBibliographyDoc),
|
||||||
|
|
||||||
updateDocLines: callbackify(updateDocLines),
|
updateDocLines: callbackify(updateDocLines),
|
||||||
|
|
||||||
upsertDoc: callbackifyMultiResult(upsertDoc, ['doc', 'isNew']),
|
upsertDoc: callbackifyMultiResult(upsertDoc, ['doc', 'isNew']),
|
||||||
|
@ -1502,6 +1529,11 @@ const ProjectEntityUpdateHandler = {
|
||||||
return VALID_ROOT_DOC_REGEXP.test(docExtension)
|
return VALID_ROOT_DOC_REGEXP.test(docExtension)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isPathValidForMainBibliographyDoc(docPath) {
|
||||||
|
const docExtension = Path.extname(docPath).toLowerCase()
|
||||||
|
return docExtension === '.bib'
|
||||||
|
},
|
||||||
|
|
||||||
async _cleanUpEntity(
|
async _cleanUpEntity(
|
||||||
project,
|
project,
|
||||||
newProject,
|
newProject,
|
||||||
|
|
|
@ -42,6 +42,7 @@ const ProjectSchema = new Schema(
|
||||||
pendingEditor_refs: [{ type: ObjectId, ref: 'User' }],
|
pendingEditor_refs: [{ type: ObjectId, ref: 'User' }],
|
||||||
rootDoc_id: { type: ObjectId },
|
rootDoc_id: { type: ObjectId },
|
||||||
rootFolder: [FolderSchema],
|
rootFolder: [FolderSchema],
|
||||||
|
mainBibliographyDoc_id: { type: ObjectId },
|
||||||
version: { type: Number }, // incremented for every change in the project structure (folders and filenames)
|
version: { type: Number }, // incremented for every change in the project structure (folders and filenames)
|
||||||
publicAccesLevel: { type: String, default: 'private' },
|
publicAccesLevel: { type: String, default: 'private' },
|
||||||
compiler: { type: String, default: 'pdflatex' },
|
compiler: { type: String, default: 'pdflatex' },
|
||||||
|
|
|
@ -147,10 +147,19 @@ export const IdeReactProvider: FC = ({ children }) => {
|
||||||
setProjectJoined(true)
|
setProjectJoined(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMainBibliographyDocUpdated(payload: string) {
|
||||||
|
scopeStore.set('project.mainBibliographyDoc_id', payload)
|
||||||
|
}
|
||||||
|
|
||||||
socket.on('joinProjectResponse', handleJoinProjectResponse)
|
socket.on('joinProjectResponse', handleJoinProjectResponse)
|
||||||
|
socket.on('mainBibliographyDocUpdated', handleMainBibliographyDocUpdated)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
|
socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
|
||||||
|
socket.removeListener(
|
||||||
|
'mainBibliographyDocUpdated',
|
||||||
|
handleMainBibliographyDocUpdated
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [socket, eventEmitter, scopeStore])
|
}, [socket, eventEmitter, scopeStore])
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@ export const ProjectProvider: FC = ({ children }) => {
|
||||||
publicAccesLevel: publicAccessLevel,
|
publicAccesLevel: publicAccessLevel,
|
||||||
owner,
|
owner,
|
||||||
trackChangesState,
|
trackChangesState,
|
||||||
|
mainBibliographyDoc_id: mainBibliographyDocId,
|
||||||
} = project || projectFallback
|
} = project || projectFallback
|
||||||
|
|
||||||
const tags = useMemo(
|
const tags = useMemo(
|
||||||
|
@ -63,6 +64,7 @@ export const ProjectProvider: FC = ({ children }) => {
|
||||||
owner,
|
owner,
|
||||||
tags,
|
tags,
|
||||||
trackChangesState,
|
trackChangesState,
|
||||||
|
mainBibliographyDocId,
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
_id,
|
_id,
|
||||||
|
@ -76,6 +78,7 @@ export const ProjectProvider: FC = ({ children }) => {
|
||||||
owner,
|
owner,
|
||||||
tags,
|
tags,
|
||||||
trackChangesState,
|
trackChangesState,
|
||||||
|
mainBibliographyDocId,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -14,6 +14,7 @@ export type ProjectContextValue = {
|
||||||
_id: string
|
_id: string
|
||||||
name: string
|
name: string
|
||||||
rootDocId?: string
|
rootDocId?: string
|
||||||
|
mainBibliographyDocId?: string
|
||||||
compiler: string
|
compiler: string
|
||||||
members: ProjectContextMember[]
|
members: ProjectContextMember[]
|
||||||
invites: ProjectContextMember[]
|
invites: ProjectContextMember[]
|
||||||
|
|
|
@ -957,4 +957,73 @@ describe('EditorController', function () {
|
||||||
.should.equal(true)
|
.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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue