mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Send operations to project-history when resolving/unresolving comments (#17540)
* Send operations to project-history when resolving/unresolving comments * small fixes * added doc_id in web unit test * Revert "added doc_id in web unit test" This reverts commit f0b8251abfce17965d5e1b0e45d8784fcf1d9eed. * fix mocked dependency in test * wip: web unit tests * document updater, reopen test * document-updater tests * format fix * fix typo * fix callsArgWith * fix reopenThread calls in doc updater tests * fix typos * log error if chat api resolve failes * log error when reopening thread * sendStatus calls done() in tests * using OError instead of logging * removed timers * preserve legacy endpoints * update after merge * Remove timer check in HttpControllerTest * prettier * added "legacy" in log * remove metrics.timer * fix promisify issues * remove unused cb GitOrigin-RevId: 849538c86996973a065c727835e93028e5429344
This commit is contained in:
parent
1557338775
commit
8bde496da4
12 changed files with 485 additions and 10 deletions
|
@ -171,6 +171,14 @@ app.post(
|
|||
'/project/:project_id/doc/:doc_id/change/accept',
|
||||
HttpController.acceptChanges
|
||||
)
|
||||
app.post(
|
||||
'/project/:project_id/doc/:doc_id/comment/:comment_id/resolve',
|
||||
HttpController.resolveComment
|
||||
)
|
||||
app.post(
|
||||
'/project/:project_id/doc/:doc_id/comment/:comment_id/reopen',
|
||||
HttpController.reopenComment
|
||||
)
|
||||
app.delete(
|
||||
'/project/:project_id/doc/:doc_id/comment/:comment_id',
|
||||
HttpController.deleteComment
|
||||
|
|
|
@ -258,6 +258,30 @@ const DocumentManager = {
|
|||
)
|
||||
},
|
||||
|
||||
async updateCommentState(projectId, docId, commentId, userId, resolved) {
|
||||
const { lines, version, pathname, historyRangesSupport } =
|
||||
await DocumentManager.getDoc(projectId, docId)
|
||||
|
||||
if (lines == null || version == null) {
|
||||
throw new Errors.NotFoundError(`document not found: ${docId}`)
|
||||
}
|
||||
|
||||
if (historyRangesSupport) {
|
||||
await ProjectHistoryRedisManager.promises.queueOps(
|
||||
projectId,
|
||||
JSON.stringify({
|
||||
pathname,
|
||||
commentId,
|
||||
resolved,
|
||||
meta: {
|
||||
ts: new Date(),
|
||||
user_id: userId,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
async deleteComment(projectId, docId, commentId, userId) {
|
||||
const { lines, version, ranges, pathname, historyRangesSupport } =
|
||||
await DocumentManager.getDoc(projectId, docId)
|
||||
|
@ -427,6 +451,24 @@ const DocumentManager = {
|
|||
)
|
||||
},
|
||||
|
||||
async updateCommentStateWithLock(
|
||||
projectId,
|
||||
docId,
|
||||
threadId,
|
||||
userId,
|
||||
resolved
|
||||
) {
|
||||
const UpdateManager = require('./UpdateManager')
|
||||
await UpdateManager.promises.lockUpdatesAndDo(
|
||||
DocumentManager.updateCommentState,
|
||||
projectId,
|
||||
docId,
|
||||
threadId,
|
||||
userId,
|
||||
resolved
|
||||
)
|
||||
},
|
||||
|
||||
async deleteCommentWithLock(projectId, docId, threadId, userId) {
|
||||
const UpdateManager = require('./UpdateManager')
|
||||
await UpdateManager.promises.lockUpdatesAndDo(
|
||||
|
|
|
@ -23,6 +23,8 @@ module.exports = {
|
|||
deleteProject,
|
||||
deleteMultipleProjects,
|
||||
acceptChanges,
|
||||
resolveComment,
|
||||
reopenComment,
|
||||
deleteComment,
|
||||
updateProject,
|
||||
resyncProjectHistory,
|
||||
|
@ -301,6 +303,54 @@ function acceptChanges(req, res, next) {
|
|||
})
|
||||
}
|
||||
|
||||
function resolveComment(req, res, next) {
|
||||
const {
|
||||
project_id: projectId,
|
||||
doc_id: docId,
|
||||
comment_id: commentId,
|
||||
} = req.params
|
||||
const userId = req.body.user_id
|
||||
logger.debug({ projectId, docId, commentId }, 'resolving comment via http')
|
||||
DocumentManager.updateCommentStateWithLock(
|
||||
projectId,
|
||||
docId,
|
||||
commentId,
|
||||
userId,
|
||||
true,
|
||||
error => {
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.debug({ projectId, docId, commentId }, 'resolved comment via http')
|
||||
res.sendStatus(204) // No Content
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function reopenComment(req, res, next) {
|
||||
const {
|
||||
project_id: projectId,
|
||||
doc_id: docId,
|
||||
comment_id: commentId,
|
||||
} = req.params
|
||||
const userId = req.body.user_id
|
||||
logger.debug({ projectId, docId, commentId }, 'reopening comment via http')
|
||||
DocumentManager.updateCommentStateWithLock(
|
||||
projectId,
|
||||
docId,
|
||||
commentId,
|
||||
userId,
|
||||
false,
|
||||
error => {
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
logger.debug({ projectId, docId, commentId }, 'reopened comment via http')
|
||||
res.sendStatus(204) // No Content
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function deleteComment(req, res, next) {
|
||||
const {
|
||||
project_id: projectId,
|
||||
|
|
|
@ -650,6 +650,149 @@ describe('HttpController', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('resolveComment', function () {
|
||||
beforeEach(function () {
|
||||
this.user_id = 'user-id-123'
|
||||
this.req = {
|
||||
params: {
|
||||
project_id: this.project_id,
|
||||
doc_id: this.doc_id,
|
||||
comment_id: (this.comment_id = 'mock-comment-id'),
|
||||
},
|
||||
query: {},
|
||||
body: {
|
||||
user_id: this.user_id,
|
||||
},
|
||||
}
|
||||
this.resolved = true
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function (done) {
|
||||
this.DocumentManager.updateCommentStateWithLock = sinon
|
||||
.stub()
|
||||
.callsArgWith(5)
|
||||
|
||||
this.ProjectHistoryRedisManager.queueOps = sinon.stub()
|
||||
this.res.sendStatus.callsFake(() => done())
|
||||
this.HttpController.resolveComment(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should accept the change', function () {
|
||||
this.DocumentManager.updateCommentStateWithLock
|
||||
.calledWith(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.comment_id,
|
||||
this.user_id,
|
||||
this.resolved
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return a successful No Content response', function () {
|
||||
this.res.sendStatus.calledWith(204).should.equal(true)
|
||||
})
|
||||
|
||||
it('should log the request', function () {
|
||||
this.logger.debug
|
||||
.calledWith(
|
||||
{
|
||||
projectId: this.project_id,
|
||||
docId: this.doc_id,
|
||||
commentId: this.comment_id,
|
||||
},
|
||||
'resolving comment via http'
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when an errors occurs', function () {
|
||||
beforeEach(function () {
|
||||
this.DocumentManager.updateCommentStateWithLock = sinon
|
||||
.stub()
|
||||
.callsArgWith(5, new Error('oops'))
|
||||
this.HttpController.resolveComment(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should call next with the error', function () {
|
||||
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('reopenComment', function () {
|
||||
beforeEach(function () {
|
||||
this.user_id = 'user-id-123'
|
||||
this.req = {
|
||||
params: {
|
||||
project_id: this.project_id,
|
||||
doc_id: this.doc_id,
|
||||
comment_id: (this.comment_id = 'mock-comment-id'),
|
||||
},
|
||||
query: {},
|
||||
body: {
|
||||
user_id: this.user_id,
|
||||
},
|
||||
}
|
||||
this.resolved = false
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function () {
|
||||
this.DocumentManager.updateCommentStateWithLock = sinon
|
||||
.stub()
|
||||
.callsArgWith(5)
|
||||
|
||||
this.ProjectHistoryRedisManager.queueOps = sinon.stub()
|
||||
this.HttpController.reopenComment(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should accept the change', function () {
|
||||
this.DocumentManager.updateCommentStateWithLock
|
||||
.calledWith(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.comment_id,
|
||||
this.user_id,
|
||||
this.resolved
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should return a successful No Content response', function () {
|
||||
this.res.sendStatus.calledWith(204).should.equal(true)
|
||||
})
|
||||
|
||||
it('should log the request', function () {
|
||||
this.logger.debug
|
||||
.calledWith(
|
||||
{
|
||||
projectId: this.project_id,
|
||||
docId: this.doc_id,
|
||||
commentId: this.comment_id,
|
||||
},
|
||||
'reopening comment via http'
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when an errors occurs', function () {
|
||||
beforeEach(function () {
|
||||
this.DocumentManager.updateCommentStateWithLock = sinon
|
||||
.stub()
|
||||
.callsArgWith(5, new Error('oops'))
|
||||
this.HttpController.reopenComment(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should call next with the error', function () {
|
||||
this.next.calledWith(sinon.match.instanceOf(Error)).should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteComment', function () {
|
||||
beforeEach(function () {
|
||||
this.user_id = 'user-id-123'
|
||||
|
|
|
@ -17,6 +17,7 @@ import * as OperationsCompressor from './OperationsCompressor.js'
|
|||
* @typedef {import('./types').RenameUpdate} RenameUpdate
|
||||
* @typedef {import('./types').TextUpdate} TextUpdate
|
||||
* @typedef {import('./types').TrackingProps} TrackingProps
|
||||
* @typedef {import('./types').SetCommentStateUpdate} SetCommentStateUpdate
|
||||
* @typedef {import('./types').Update} Update
|
||||
* @typedef {import('./types').UpdateWithBlob} UpdateWithBlob
|
||||
*/
|
||||
|
@ -79,6 +80,14 @@ function _convertToChange(projectId, updateWithBlob) {
|
|||
if (update.v != null) {
|
||||
v2DocVersions[update.doc] = { pathname, v: update.v }
|
||||
}
|
||||
} else if (isSetCommentStateUpdate(update)) {
|
||||
operations = [
|
||||
{
|
||||
pathname: _convertPathname(update.pathname),
|
||||
commentId: update.commentId,
|
||||
resolved: update.resolved,
|
||||
},
|
||||
]
|
||||
} else if (isDeleteCommentUpdate(update)) {
|
||||
operations = [
|
||||
{
|
||||
|
@ -189,6 +198,14 @@ export function isAddUpdate(update) {
|
|||
return _isAddDocUpdate(update) || _isAddFileUpdate(update)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Update} update
|
||||
* @returns {update is SetCommentStateUpdate}
|
||||
*/
|
||||
export function isSetCommentStateUpdate(update) {
|
||||
return 'commentId' in update && 'resolved' in update
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Update} update
|
||||
* @returns {update is DeleteCommentUpdate}
|
||||
|
|
|
@ -4,6 +4,7 @@ export type Update =
|
|||
| AddFileUpdate
|
||||
| RenameUpdate
|
||||
| DeleteCommentUpdate
|
||||
| SetCommentStateUpdate
|
||||
|
||||
export type UpdateMeta = {
|
||||
user_id: string
|
||||
|
@ -25,6 +26,13 @@ export type TextUpdate = {
|
|||
}
|
||||
}
|
||||
|
||||
export type SetCommentStateUpdate = {
|
||||
pathname: string
|
||||
commentId: string
|
||||
resolved: boolean
|
||||
meta: UpdateMeta
|
||||
}
|
||||
|
||||
export type DeleteCommentUpdate = {
|
||||
pathname: string
|
||||
deleteComment: string
|
||||
|
|
|
@ -19,6 +19,8 @@ module.exports = {
|
|||
getProjectDocsIfMatch,
|
||||
clearProjectState,
|
||||
acceptChanges,
|
||||
resolveThread,
|
||||
reopenThread,
|
||||
deleteThread,
|
||||
resyncProjectHistory,
|
||||
updateProjectStructure,
|
||||
|
@ -38,6 +40,8 @@ module.exports = {
|
|||
getProjectDocsIfMatch: promisify(getProjectDocsIfMatch),
|
||||
clearProjectState: promisify(clearProjectState),
|
||||
acceptChanges: promisify(acceptChanges),
|
||||
resolveThread: promisify(resolveThread),
|
||||
reopenThread: promisify(reopenThread),
|
||||
deleteThread: promisify(deleteThread),
|
||||
resyncProjectHistory: promisify(resyncProjectHistory),
|
||||
updateProjectStructure: promisify(updateProjectStructure),
|
||||
|
@ -212,6 +216,36 @@ function acceptChanges(projectId, docId, changeIds, callback) {
|
|||
)
|
||||
}
|
||||
|
||||
function resolveThread(projectId, docId, threadId, userId, callback) {
|
||||
_makeRequest(
|
||||
{
|
||||
path: `/project/${projectId}/doc/${docId}/comment/${threadId}/resolve`,
|
||||
method: 'POST',
|
||||
json: {
|
||||
user_id: userId,
|
||||
},
|
||||
},
|
||||
projectId,
|
||||
'resolve-thread',
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
function reopenThread(projectId, docId, threadId, userId, callback) {
|
||||
_makeRequest(
|
||||
{
|
||||
path: `/project/${projectId}/doc/${docId}/comment/${threadId}/reopen`,
|
||||
method: 'POST',
|
||||
json: {
|
||||
user_id: userId,
|
||||
},
|
||||
},
|
||||
projectId,
|
||||
'reopen-thread',
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
function deleteThread(projectId, docId, threadId, userId, callback) {
|
||||
_makeRequest(
|
||||
{
|
||||
|
|
|
@ -898,7 +898,9 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
|||
},
|
||||
}))
|
||||
|
||||
postJSON(`/project/${projectId}/thread/${entry.thread_id}/resolve`)
|
||||
postJSON(
|
||||
`/project/${projectId}/doc/${docId}/thread/${entry.thread_id}/resolve`
|
||||
)
|
||||
onCommentResolved(entry.thread_id, user)
|
||||
sendMB('rp-comment-resolve', { view })
|
||||
},
|
||||
|
@ -925,9 +927,9 @@ function useReviewPanelState(): ReviewPanel.ReviewPanelState {
|
|||
)
|
||||
|
||||
const unresolveComment = useCallback(
|
||||
(threadId: ThreadId) => {
|
||||
(docId: DocId, threadId: ThreadId) => {
|
||||
onCommentReopened(threadId)
|
||||
const url = `/project/${projectId}/thread/${threadId}/reopen`
|
||||
const url = `/project/${projectId}/doc/${docId}/thread/${threadId}/reopen`
|
||||
postJSON(url).catch(debugConsole.error)
|
||||
sendMB('rp-comment-reopen')
|
||||
},
|
||||
|
|
|
@ -38,7 +38,7 @@ function ResolvedCommentEntry({
|
|||
: thread.content
|
||||
|
||||
const handleUnresolve = () => {
|
||||
unresolveComment(thread.threadId)
|
||||
unresolveComment(thread.docId, thread.threadId)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
|
|
|
@ -71,7 +71,7 @@ export interface ReviewPanelState {
|
|||
commentId: CommentId,
|
||||
content: string
|
||||
) => void
|
||||
unresolveComment: (threadId: ThreadId) => void
|
||||
unresolveComment: (docId: DocId, threadId: ThreadId) => void
|
||||
deleteThread: (docId: DocId, threadId: ThreadId) => void
|
||||
refreshResolvedCommentsDropdown: () => Promise<
|
||||
void | ReviewPanelDocEntries[]
|
||||
|
|
|
@ -849,7 +849,7 @@ export default App.controller('ReviewPanelController', [
|
|||
const entry = getDocEntries(doc_id)[entry_id]
|
||||
entry.focused = false
|
||||
$http.post(
|
||||
`/project/${$scope.project_id}/thread/${entry.thread_id}/resolve`,
|
||||
`/project/${$scope.project_id}/doc/${doc_id}/thread/${entry.thread_id}/resolve`,
|
||||
{ _csrf: window.csrfToken }
|
||||
)
|
||||
_onCommentResolved(entry.thread_id, ide.$scope.user)
|
||||
|
@ -860,11 +860,14 @@ export default App.controller('ReviewPanelController', [
|
|||
})
|
||||
}
|
||||
|
||||
ide.$scope.unresolveComment = function (thread_id) {
|
||||
ide.$scope.unresolveComment = function (doc_id, thread_id) {
|
||||
_onCommentReopened(thread_id)
|
||||
$http.post(`/project/${$scope.project_id}/thread/${thread_id}/reopen`, {
|
||||
_csrf: window.csrfToken,
|
||||
})
|
||||
$http.post(
|
||||
`/project/${$scope.project_id}/doc/${doc_id}/thread/${thread_id}/reopen`,
|
||||
{
|
||||
_csrf: window.csrfToken,
|
||||
}
|
||||
)
|
||||
eventTracking.sendMB('rp-comment-reopen')
|
||||
}
|
||||
|
||||
|
|
|
@ -838,6 +838,174 @@ describe('DocumentUpdaterHandler', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('resolveThread', function () {
|
||||
beforeEach(function () {
|
||||
this.thread_id = 'mock-thread-id-1'
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function () {
|
||||
this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
|
||||
this.handler.resolveThread(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.thread_id,
|
||||
this.user_id,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should resolve the thread in the document updater', function () {
|
||||
this.request
|
||||
.calledWithMatch({
|
||||
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/resolve`,
|
||||
method: 'POST',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
this.callback.calledWith(null).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the document updater API returns an error', function () {
|
||||
beforeEach(function () {
|
||||
this.request.callsArgWith(
|
||||
1,
|
||||
new Error('something went wrong'),
|
||||
null,
|
||||
null
|
||||
)
|
||||
this.handler.resolveThread(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.thread_id,
|
||||
this.user_id,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should return an error to the callback', function () {
|
||||
this.callback
|
||||
.calledWith(sinon.match.instanceOf(Error))
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the document updater returns a failure error code', function () {
|
||||
beforeEach(function () {
|
||||
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
||||
this.handler.resolveThread(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.thread_id,
|
||||
this.user_id,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the callback with an error', function () {
|
||||
this.callback
|
||||
.calledWith(
|
||||
sinon.match
|
||||
.instanceOf(Error)
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'document updater returned a failure status code: 500'
|
||||
)
|
||||
)
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('reopenThread', function () {
|
||||
beforeEach(function () {
|
||||
this.thread_id = 'mock-thread-id-1'
|
||||
})
|
||||
|
||||
describe('successfully', function () {
|
||||
beforeEach(function () {
|
||||
this.request.callsArgWith(1, null, { statusCode: 200 }, this.body)
|
||||
this.handler.reopenThread(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.thread_id,
|
||||
this.user_id,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should reopen the thread in the document updater', function () {
|
||||
this.request
|
||||
.calledWithMatch({
|
||||
url: `${this.settings.apis.documentupdater.url}/project/${this.project_id}/doc/${this.doc_id}/comment/${this.thread_id}/reopen`,
|
||||
method: 'POST',
|
||||
})
|
||||
.should.equal(true)
|
||||
})
|
||||
|
||||
it('should call the callback', function () {
|
||||
this.callback.calledWith(null).should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the document updater API returns an error', function () {
|
||||
beforeEach(function () {
|
||||
this.request.callsArgWith(
|
||||
1,
|
||||
new Error('something went wrong'),
|
||||
null,
|
||||
null
|
||||
)
|
||||
this.handler.reopenThread(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.thread_id,
|
||||
this.user_id,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should return an error to the callback', function () {
|
||||
this.callback
|
||||
.calledWith(sinon.match.instanceOf(Error))
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the document updater returns a failure error code', function () {
|
||||
beforeEach(function () {
|
||||
this.request.callsArgWith(1, null, { statusCode: 500 }, '')
|
||||
this.handler.reopenThread(
|
||||
this.project_id,
|
||||
this.doc_id,
|
||||
this.thread_id,
|
||||
this.user_id,
|
||||
this.callback
|
||||
)
|
||||
})
|
||||
|
||||
it('should return the callback with an error', function () {
|
||||
this.callback
|
||||
.calledWith(
|
||||
sinon.match
|
||||
.instanceOf(Error)
|
||||
.and(
|
||||
sinon.match.has(
|
||||
'message',
|
||||
'document updater returned a failure status code: 500'
|
||||
)
|
||||
)
|
||||
)
|
||||
.should.equal(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateProjectStructure ', function () {
|
||||
beforeEach(function () {
|
||||
this.user_id = 1234
|
||||
|
|
Loading…
Reference in a new issue