From 1830d41eba0d2f1caf7e30b3116aaac218a69e19 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 8 Dec 2016 14:09:06 +0000 Subject: [PATCH 01/98] Proxy ranges between doc updater and docstore --- .../Features/Docstore/DocstoreManager.coffee | 5 +++-- .../DocumentUpdaterHandler.coffee | 4 ++-- .../Documents/DocumentController.coffee | 7 ++++--- .../Project/ProjectEntityHandler.coffee | 6 +++--- .../coffee/Docstore/DocstoreManagerTests.coffee | 11 +++++++---- .../DocumentUpdaterHandlerTests.coffee | 3 ++- .../Documents/DocumentControllerTests.coffee | 7 +++++-- .../Project/ProjectEntityHandlerTests.coffee | 17 ++++++++++------- 8 files changed, 36 insertions(+), 24 deletions(-) diff --git a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee index 772d927d78..cf48dfe07b 100644 --- a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee +++ b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee @@ -45,13 +45,13 @@ module.exports = DocstoreManager = return callback(error) if error? if 200 <= res.statusCode < 300 logger.log doc_id: doc_id, project_id: project_id, version: doc.version, rev: doc.rev, "got doc from docstore api" - callback(null, doc.lines, doc.rev, doc.version) + callback(null, doc.lines, doc.rev, doc.version, doc.ranges) else error = new Error("docstore api responded with non-success code: #{res.statusCode}") logger.error err: error, project_id: project_id, doc_id: doc_id, "error getting doc from docstore" callback(error) - updateDoc: (project_id, doc_id, lines, version, callback = (error, modified, rev) ->) -> + updateDoc: (project_id, doc_id, lines, version, ranges, callback = (error, modified, rev) ->) -> logger.log project_id: project_id, doc_id: doc_id, "updating doc in docstore api" url = "#{settings.apis.docstore.url}/project/#{project_id}/doc/#{doc_id}" request.post { @@ -59,6 +59,7 @@ module.exports = DocstoreManager = json: lines: lines version: version + ranges: ranges }, (error, res, result) -> return callback(error) if error? if 200 <= res.statusCode < 300 diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee index dcf0615b25..9b3364f8ad 100644 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -95,7 +95,7 @@ module.exports = DocumentUpdaterHandler = logger.error err: error, project_id: project_id, doc_id: doc_id, "document updater returned failure status code: #{res.statusCode}" return callback(error) - getDocument: (project_id, doc_id, fromVersion, callback = (error, exists, doclines, version) ->) -> + getDocument: (project_id, doc_id, fromVersion, callback = (error, doclines, version, ranges, ops) ->) -> timer = new metrics.Timer("get-document") url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}" logger.log project_id:project_id, doc_id: doc_id, "getting doc from document updater" @@ -110,7 +110,7 @@ module.exports = DocumentUpdaterHandler = body = JSON.parse(body) catch error return callback(error) - callback null, body.lines, body.version, body.ops + callback null, body.lines, body.version, body.ranges, body.ops else logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}" callback new Error("doc updater returned a non-success status code: #{res.statusCode}") diff --git a/services/web/app/coffee/Features/Documents/DocumentController.coffee b/services/web/app/coffee/Features/Documents/DocumentController.coffee index 560f232ba1..2042f6a218 100644 --- a/services/web/app/coffee/Features/Documents/DocumentController.coffee +++ b/services/web/app/coffee/Features/Documents/DocumentController.coffee @@ -7,7 +7,7 @@ module.exports = doc_id = req.params.doc_id plain = req?.query?.plain == 'true' logger.log doc_id:doc_id, project_id:project_id, "receiving get document request from api (docupdater)" - ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version) -> + ProjectEntityHandler.getDoc project_id, doc_id, (error, lines, rev, version, ranges) -> if error? logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument" return next(error) @@ -19,14 +19,15 @@ module.exports = res.send JSON.stringify { lines: lines version: version + ranges: ranges } setDocument: (req, res, next = (error) ->) -> project_id = req.params.Project_id doc_id = req.params.doc_id - {lines, version} = req.body + {lines, version, ranges} = req.body logger.log doc_id:doc_id, project_id:project_id, "receiving set document request from api (docupdater)" - ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, (error) -> + ProjectEntityHandler.updateDocLines project_id, doc_id, lines, version, ranges, (error) -> if error? logger.err err:error, doc_id:doc_id, project_id:project_id, "error finding element for getDocument" return next(error) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 21932cefc9..eefaeab6ab 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -126,7 +126,7 @@ module.exports = ProjectEntityHandler = doc = new Doc name: docName # Put doc in docstore first, so that if it errors, we don't have a doc_id in the project # which hasn't been created in docstore. - DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, (err, modified, rev) -> + DocstoreManager.updateDoc project_id.toString(), doc._id.toString(), docLines, 0, {}, (err, modified, rev) -> return callback(err) if err? ProjectEntityHandler._putElement project, folder_id, doc, "doc", (err, result)=> @@ -292,7 +292,7 @@ module.exports = ProjectEntityHandler = return callback(err) callback(err, folder, parentFolder_id) - updateDocLines : (project_id, doc_id, lines, version, callback = (error) ->)-> + updateDocLines : (project_id, doc_id, lines, version, ranges, callback = (error) ->)-> ProjectGetter.getProjectWithoutDocLines project_id, (err, project)-> return callback(err) if err? return callback(new Errors.NotFoundError("project not found")) if !project? @@ -307,7 +307,7 @@ module.exports = ProjectEntityHandler = return callback(error) logger.log project_id: project_id, doc_id: doc_id, "telling docstore manager to update doc" - DocstoreManager.updateDoc project_id, doc_id, lines, version, (err, modified, rev) -> + DocstoreManager.updateDoc project_id, doc_id, lines, version, ranges, (err, modified, rev) -> if err? logger.error err: err, doc_id: doc_id, project_id:project_id, lines: lines, "error sending doc to docstore" return callback(err) diff --git a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee index e288c46aea..52603c28bb 100644 --- a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee @@ -57,12 +57,13 @@ describe "DocstoreManager", -> @lines = ["mock", "doc", "lines"] @rev = 5 @version = 42 + @ranges = { "mock": "ranges" } @modified = true describe "with a successful response code", -> beforeEach -> @request.post = sinon.stub().callsArgWith(1, null, statusCode: 204, { modified: @modified, rev: @rev }) - @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @callback + @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @ranges, @callback it "should update the doc in the docstore api", -> @request.post @@ -71,6 +72,7 @@ describe "DocstoreManager", -> json: lines: @lines version: @version + ranges: @ranges }) .should.equal true @@ -80,7 +82,7 @@ describe "DocstoreManager", -> describe "with a failed response code", -> beforeEach -> @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500, "") - @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @callback + @DocstoreManager.updateDoc @project_id, @doc_id, @lines, @version, @ranges, @callback it "should call the callback with an error", -> @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true @@ -100,6 +102,7 @@ describe "DocstoreManager", -> lines: @lines = ["mock", "doc", "lines"] rev: @rev = 5 version: @version = 42 + ranges: @ranges = { "mock": "ranges" } describe "with a successful response code", -> beforeEach -> @@ -115,7 +118,7 @@ describe "DocstoreManager", -> .should.equal true it "should call the callback with the lines, version and rev", -> - @callback.calledWith(null, @lines, @rev, @version).should.equal true + @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true describe "with a failed response code", -> beforeEach -> @@ -148,7 +151,7 @@ describe "DocstoreManager", -> .should.equal true it "should call the callback with the lines, version and rev", -> - @callback.calledWith(null, @lines, @rev, @version).should.equal true + @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true describe "getAllDocs", -> describe "with a successful response code", -> diff --git a/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee index aaae05219b..eca005f295 100644 --- a/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee @@ -267,6 +267,7 @@ describe 'DocumentUpdaterHandler - Flushing documents :', -> lines: @lines version: @version ops: @ops = ["mock-op-1", "mock-op-2"] + ranges: @ranges = {"mock":"ranges"} @fromVersion = 2 @request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body) @handler.getDocument @project_id, @doc_id, @fromVersion, @callback @@ -276,7 +277,7 @@ describe 'DocumentUpdaterHandler - Flushing documents :', -> @request.get.calledWith(url).should.equal true it "should call the callback with the lines and version", -> - @callback.calledWith(null, @lines, @version, @ops).should.equal true + @callback.calledWith(null, @lines, @version, @ranges, @ops).should.equal true describe "when the document updater API returns an error", -> beforeEach -> diff --git a/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee b/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee index a554319baa..fedfa1c1b3 100644 --- a/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Documents/DocumentControllerTests.coffee @@ -23,6 +23,7 @@ describe "DocumentController", -> @doc_id = "doc-id-123" @doc_lines = ["one", "two", "three"] @version = 42 + @ranges = {"mock": "ranges"} @rev = 5 describe "getDocument", -> @@ -33,7 +34,7 @@ describe "DocumentController", -> describe "when the document exists", -> beforeEach -> - @ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(2, null, @doc_lines, @rev, @version) + @ProjectEntityHandler.getDoc = sinon.stub().callsArgWith(2, null, @doc_lines, @rev, @version, @ranges) @DocumentController.getDocument(@req, @res, @next) it "should get the document from Mongo", -> @@ -46,6 +47,7 @@ describe "DocumentController", -> @res.body.should.equal JSON.stringify lines: @doc_lines version: @version + ranges: @ranges describe "when the document doesn't exist", -> beforeEach -> @@ -68,11 +70,12 @@ describe "DocumentController", -> @req.body = lines: @doc_lines version: @version + ranges: @ranges @DocumentController.setDocument(@req, @res, @next) it "should update the document in Mongo", -> @ProjectEntityHandler.updateDocLines - .calledWith(@project_id, @doc_id, @doc_lines, @version) + .calledWith(@project_id, @doc_id, @doc_lines, @version, @ranges) .should.equal true it "should return a successful response", -> diff --git a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee index 5a0c860ab2..f3dcda07cf 100644 --- a/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/Project/ProjectEntityHandlerTests.coffee @@ -382,7 +382,9 @@ describe 'ProjectEntityHandler', -> beforeEach -> @lines = ["mock", "doc", "lines"] @rev = 5 - @DocstoreManager.getDoc = sinon.stub().callsArgWith(3, null, @lines, @rev) + @version = 42 + @ranges = {"mock": "ranges"} + @DocstoreManager.getDoc = sinon.stub().callsArgWith(3, null, @lines, @rev, @version, @ranges) @ProjectEntityHandler.getDoc project_id, doc_id, @callback it "should call the docstore", -> @@ -391,7 +393,7 @@ describe 'ProjectEntityHandler', -> .should.equal true it "should call the callback with the lines, version and rev", -> - @callback.calledWith(null, @lines, @rev).should.equal true + @callback.calledWith(null, @lines, @rev, @version, @ranges).should.equal true describe 'addDoc', -> beforeEach -> @@ -590,6 +592,7 @@ describe 'ProjectEntityHandler', -> _id: doc_id } @version = 42 + @ranges = {"mock":"ranges"} @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, @project) @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @doc, {fileSystem: @path}) @tpdsUpdateSender.addDoc = sinon.stub().callsArg(1) @@ -599,7 +602,7 @@ describe 'ProjectEntityHandler', -> describe "when the doc has been modified", -> beforeEach -> @DocstoreManager.updateDoc = sinon.stub().yields(null, true, @rev = 5) - @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @callback + @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @ranges, @callback it "should get the project without doc lines", -> @ProjectGetter.getProjectWithoutDocLines @@ -617,7 +620,7 @@ describe 'ProjectEntityHandler', -> it "should update the doc in the docstore", -> @DocstoreManager.updateDoc - .calledWith(project_id, doc_id, @lines, @version) + .calledWith(project_id, doc_id, @lines, @version, @ranges) .should.equal true it "should mark the project as updated", -> @@ -642,7 +645,7 @@ describe 'ProjectEntityHandler', -> describe "when the doc has not been modified", -> beforeEach -> @DocstoreManager.updateDoc = sinon.stub().yields(null, false, @rev = 5) - @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @callback + @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @ranges, @callback it "should not mark the project as updated", -> @projectUpdater.markAsUpdated.called.should.equal false @@ -656,7 +659,7 @@ describe 'ProjectEntityHandler', -> describe "when the project is not found", -> beforeEach -> @ProjectGetter.getProjectWithoutDocLines = sinon.stub().callsArgWith(1, null, null) - @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @callback + @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @ranges, @version, @callback it "should return a not found error", -> @callback.calledWith(new Errors.NotFoundError()).should.equal true @@ -664,7 +667,7 @@ describe 'ProjectEntityHandler', -> describe "when the doc is not found", -> beforeEach -> @projectLocator.findElement = sinon.stub().callsArgWith(1, null, null, null) - @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @version, @callback + @ProjectEntityHandler.updateDocLines project_id, doc_id, @lines, @ranges, @version, @callback it "should log out the error", -> @logger.error From 1d426e538f732e785338f74df7c7d25fc45f33a9 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 8 Dec 2016 14:10:30 +0000 Subject: [PATCH 02/98] Load ranges from docupdater and toggle track changes when possible --- .../web/app/views/project/editor/editor.jade | 4 +- .../views/project/editor/review-panel.jade | 8 +- .../public/coffee/ide/editor/Document.coffee | 10 +- .../coffee/ide/editor/EditorManager.coffee | 33 ++++++ .../coffee/ide/editor/ShareJsDoc.coffee | 21 ++-- .../ide/editor/directives/aceEditor.coffee | 4 +- .../track-changes/TrackChangesManager.coffee | 101 ++++++++++-------- ...gesTracker.coffee => RangesTracker.coffee} | 34 ++++-- .../controllers/ReviewPanelController.coffee | 29 +++-- 9 files changed, 150 insertions(+), 94 deletions(-) rename services/web/public/coffee/ide/review-panel/{ChangesTracker.coffee => RangesTracker.coffee} (94%) diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index 50da35d08a..3126d63df2 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -52,8 +52,8 @@ div.full-size( review-panel="reviewPanel", events-bridge="reviewPanelEventsBridge" track-changes-enabled="trackChangesFeatureFlag", - track-new-changes= "reviewPanel.trackNewChanges", - changes-tracker="reviewPanel.changesTracker", + track-changes= "editor.trackChanges", + ranges-tracker="reviewPanel.rangesTracker", doc-id="editor.open_doc_id" ) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index dbb3a34631..be41e80e2c 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -1,10 +1,12 @@ #review-panel .review-panel-toolbar - span.review-panel-toolbar-label(ng-click="reviewPanel.trackNewChanges = true;", ng-if="reviewPanel.trackNewChanges === false") Track Changes is + span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = true;", ng-if="editor.wantTrackChanges === false") Track Changes is strong off - span.review-panel-toolbar-label(ng-click="reviewPanel.trackNewChanges = false;", ng-if="reviewPanel.trackNewChanges === true") Track Changes is + span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = false;", ng-if="editor.wantTrackChanges === true") Track Changes is strong on - review-panel-toggle(ng-model="reviewPanel.trackNewChanges") + review-panel-toggle(ng-if="editor.wantTrackChanges == editor.trackChanges", ng-model="editor.wantTrackChanges") + span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges") + i.fa.fa-spin.fa-spinner .rp-entry-list( review-panel-sorted diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index 17b1d9e28f..213fbe3514 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -77,6 +77,9 @@ define [ hasBufferedOps: () -> @doc?.hasBufferedOps() + + setTrackingChanges: (track_changes) -> + @doc.track_changes = track_changes _bindToSocketEvents: () -> @_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update) @@ -239,16 +242,19 @@ define [ _joinDoc: (callback = (error) ->) -> if @doc? - @ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), (error, docLines, version, updates) => + @ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), (error, docLines, version, updates, ranges) => return callback(error) if error? @joined = true @doc.catchUp( updates ) + # TODO: Worry about whether these ranges are consistent with the doc still + @opening_ranges = ranges callback() else - @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version) => + @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version, updates, ranges) => return callback(error) if error? @joined = true @doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket + @opening_ranges = ranges @_bindToShareJsDocEvents() callback() diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index eb063c9c6a..b7fccc29af 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -10,6 +10,8 @@ define [ open_doc_id: null open_doc_name: null opening: true + trackChanges: false + wantTrackChanges: false } @$scope.$on "entity:selected", (event, entity) => @@ -31,6 +33,10 @@ define [ @$scope.$on "flush-changes", () => Document.flushAll() + + @$scope.$watch "editor.wantTrackChanges", (value) => + return if !value? + @_syncTrackChangesState(@$scope.editor.sharejs_doc) autoOpenDoc: () -> open_doc_id = @@ -83,6 +89,8 @@ define [ "Sorry, something went wrong opening this document. Please try again." ) return + + @_syncTrackChangesState(sharejs_doc) @$scope.$broadcast "doc:opened" @@ -144,3 +152,28 @@ define [ stopIgnoringExternalUpdates: () -> @_ignoreExternalUpdates = false + + _syncTimeout: null + _syncTrackChangesState: (doc) -> + return if !doc? + + if @_syncTimeout? + clearTimeout @_syncTimeout + @_syncTimeout = null + + want = @$scope.editor.wantTrackChanges + have = @$scope.editor.trackChanges + if want == have + return + + console.log "Trying to set track changes to:", want + do tryToggle = () => + saved = !doc.getInflightOp()? and !doc.getPendingOp()? + if saved + console.log "SUCCESS, changing value", want + doc.setTrackingChanges(want) + @$scope.$apply () => + @$scope.editor.trackChanges = want + else + console.log "Still in flight, will try soon" + @_syncTimeout = setTimeout tryToggle, 100 diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee index 5d8b4ef11a..bb2d6b3e38 100644 --- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee @@ -9,21 +9,9 @@ define [ # Dencode any binary bits of data # See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html @type = "text" - docLines = for line in docLines - if line.text? - @type = "json" - line.text = decodeURIComponent(escape(line.text)) - else - @type = "text" - line = decodeURIComponent(escape(line)) - line - - if @type == "text" - snapshot = docLines.join("\n") - else if @type == "json" - snapshot = { lines: docLines } - else - throw new Error("Unknown type: #{@type}") + docLines = (decodeURIComponent(escape(line)) for line in docLines) + snapshot = docLines.join("\n") + @track_changes = false @connection = { send: (update) => @@ -34,6 +22,9 @@ define [ if window.dropUpdates? and Math.random() < window.dropUpdates sl_console.log "Simulating a lost update", update return + if @track_changes + update.meta ?= {} + update.meta.tc = 1 @socket.emit "applyOtUpdate", @doc_id, update, (error) => return @_handleError(error) if error? state: "ok" diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 885deec2f8..bbbcff5a85 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -54,9 +54,9 @@ define [ syntaxValidation: "=" reviewPanel: "=" eventsBridge: "=" - trackNewChanges: "=" + trackChanges: "=" trackChangesEnabled: "=" - changesTracker: "=" + rangesTracker: "=" docId: "=" } link: (scope, element, attrs) -> diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index af9815b2cb..54b20f7018 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -10,15 +10,22 @@ define [ constructor: (@$scope, @editor, @element) -> window.trackChangesManager ?= @ - @$scope.$watch "changesTracker", (changesTracker) => - return if !changesTracker? - @disconnectFromChangesTracker() - @changesTracker = changesTracker - @connectToChangesTracker() + @$scope.$watch "rangesTracker", (rangesTracker) => + return if !rangesTracker? + @disconnectFromRangesTracker() + @rangesTracker = rangesTracker + @connectToRangesTracker() - @$scope.$watch "trackNewChanges", (track_new_changes) => - return if !track_new_changes? - @changesTracker?.track_changes = track_new_changes + @$scope.$watch "trackChanges", (track_changes) => + return if !track_changes? + @rangesTracker?.track_changes = track_changes + + @$scope.$watch "sharejsDoc", (doc) => + return if !doc? + if doc.opening_ranges?.changes? + @rangesTracker.changes = doc.opening_ranges.changes + if doc.opening_ranges?.comments? + @rangesTracker.comments = doc.opening_ranges.comments @$scope.$on "comment:add", (e, comment) => @addCommentToSelection(comment) @@ -75,12 +82,12 @@ define [ else user_id = window.user.id - was_tracking = @changesTracker.track_changes + was_tracking = @rangesTracker.track_changes if @dont_track_next_update - @changesTracker.track_changes = false + @rangesTracker.track_changes = false @dont_track_next_update = false @applyChange(e, { user_id }) - @changesTracker.track_changes = was_tracking + @rangesTracker.track_changes = was_tracking # TODO: Just for debugging, remove before going live. setTimeout () => @@ -111,68 +118,68 @@ define [ else unbindFromAce() - disconnectFromChangesTracker: () -> + disconnectFromRangesTracker: () -> @changeIdToMarkerIdMap = {} - if @changesTracker? - @changesTracker.off "insert:added" - @changesTracker.off "insert:removed" - @changesTracker.off "delete:added" - @changesTracker.off "delete:removed" - @changesTracker.off "changes:moved" - @changesTracker.off "comment:added" - @changesTracker.off "comment:moved" - @changesTracker.off "comment:removed" - @changesTracker.off "comment:resolved" - @changesTracker.off "comment:unresolved" + if @rangesTracker? + @rangesTracker.off "insert:added" + @rangesTracker.off "insert:removed" + @rangesTracker.off "delete:added" + @rangesTracker.off "delete:removed" + @rangesTracker.off "changes:moved" + @rangesTracker.off "comment:added" + @rangesTracker.off "comment:moved" + @rangesTracker.off "comment:removed" + @rangesTracker.off "comment:resolved" + @rangesTracker.off "comment:unresolved" - connectToChangesTracker: () -> - @changesTracker.track_changes = @$scope.trackNewChanges + connectToRangesTracker: () -> + @rangesTracker.track_changes = @$scope.trackChanges - @changesTracker.on "insert:added", (change) => + @rangesTracker.on "insert:added", (change) => sl_console.log "[insert:added]", change @_onInsertAdded(change) - @changesTracker.on "insert:removed", (change) => + @rangesTracker.on "insert:removed", (change) => sl_console.log "[insert:removed]", change @_onInsertRemoved(change) - @changesTracker.on "delete:added", (change) => + @rangesTracker.on "delete:added", (change) => sl_console.log "[delete:added]", change @_onDeleteAdded(change) - @changesTracker.on "delete:removed", (change) => + @rangesTracker.on "delete:removed", (change) => sl_console.log "[delete:removed]", change @_onDeleteRemoved(change) - @changesTracker.on "changes:moved", (changes) => + @rangesTracker.on "changes:moved", (changes) => sl_console.log "[changes:moved]", changes @_onChangesMoved(changes) - @changesTracker.on "comment:added", (comment) => + @rangesTracker.on "comment:added", (comment) => sl_console.log "[comment:added]", comment @_onCommentAdded(comment) - @changesTracker.on "comment:moved", (comment) => + @rangesTracker.on "comment:moved", (comment) => sl_console.log "[comment:moved]", comment @_onCommentMoved(comment) - @changesTracker.on "comment:removed", (comment) => + @rangesTracker.on "comment:removed", (comment) => sl_console.log "[comment:removed]", comment @_onCommentRemoved(comment) - @changesTracker.on "comment:resolved", (comment) => + @rangesTracker.on "comment:resolved", (comment) => sl_console.log "[comment:resolved]", comment @_onCommentRemoved(comment) - @changesTracker.on "comment:unresolved", (comment) => + @rangesTracker.on "comment:unresolved", (comment) => sl_console.log "[comment:unresolved]", comment @_onCommentAdded(comment) redrawAnnotations: () -> - for change in @changesTracker.changes + for change in @rangesTracker.changes if change.op.i? @_onInsertAdded(change) else if change.op.d? @_onDeleteAdded(change) - for comment in @changesTracker.comments + for comment in @rangesTracker.comments @_onCommentAdded(comment) addComment: (offset, length, content) -> - @changesTracker.addComment offset, length, { + @rangesTracker.addComment offset, length, { thread: [{ content: content user_id: window.user_id @@ -192,12 +199,12 @@ define [ @editor.selection.selectLine() acceptChangeId: (change_id) -> - @changesTracker.removeChangeId(change_id) + @rangesTracker.removeChangeId(change_id) rejectChangeId: (change_id) -> - change = @changesTracker.getChange(change_id) + change = @rangesTracker.getChange(change_id) return if !change? - @changesTracker.removeChangeId(change_id) + @rangesTracker.removeChangeId(change_id) @dont_track_next_update = true session = @editor.getSession() if change.op.d? @@ -215,15 +222,15 @@ define [ throw new Error("unknown change: #{JSON.stringify(change)}") removeCommentId: (comment_id) -> - @changesTracker.removeCommentId(comment_id) + @rangesTracker.removeCommentId(comment_id) resolveCommentId: (comment_id, user_id) -> - @changesTracker.resolveCommentId(comment_id, { + @rangesTracker.resolveCommentId(comment_id, { user_id, ts: new Date() }) unresolveCommentId: (comment_id) -> - @changesTracker.unresolveCommentId(comment_id) + @rangesTracker.unresolveCommentId(comment_id) checkMapping: () -> session = @editor.getSession() @@ -234,7 +241,7 @@ define [ markers[marker_id] = marker expected_markers = [] - for change in @changesTracker.changes + for change in @rangesTracker.changes if @changeIdToMarkerIdMap[change.id]? op = change.op {background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id] @@ -246,7 +253,7 @@ define [ expected_markers.push { marker_id: background_marker_id, start, end } expected_markers.push { marker_id: callout_marker_id, start, end: start } - for comment in @changesTracker.comments + for comment in @rangesTracker.comments if @changeIdToMarkerIdMap[comment.id]? {background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[comment.id] start = @_shareJsOffsetToAcePosition(comment.offset) @@ -269,7 +276,7 @@ define [ applyChange: (delta, metadata) -> op = @_aceChangeToShareJs(delta) - @changesTracker.applyOp(op, metadata) + @rangesTracker.applyOp(op, metadata) updateFocus: () -> selection = @editor.getSelectionRange() diff --git a/services/web/public/coffee/ide/review-panel/ChangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee similarity index 94% rename from services/web/public/coffee/ide/review-panel/ChangesTracker.coffee rename to services/web/public/coffee/ide/review-panel/RangesTracker.coffee index 0b668c90dd..6a3625fd09 100644 --- a/services/web/public/coffee/ide/review-panel/ChangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -1,7 +1,5 @@ -define [ - "utils/EventEmitter" -], (EventEmitter) -> - class ChangesTracker extends EventEmitter +load = (EventEmitter) -> + class RangesTracker extends EventEmitter # The purpose of this class is to track a set of inserts and deletes to a document, like # track changes in Word. We store these as a set of ShareJs style ranges: # {i: "foo", p: 42} # Insert 'foo' at offset 42 @@ -36,7 +34,7 @@ define [ # * Deletes by another user will consume deletes by the first user # * Inserts by another user will not combine with inserts by the first user. If they are in the # middle of a previous insert by the first user, the original insert will be split into two. - constructor: () -> + constructor: (@changes = [], @comments = []) -> # Change objects have the following structure: # { # id: ... # Uniquely generated by us @@ -48,8 +46,6 @@ define [ # # Ids are used to uniquely identify a change, e.g. for updating it in the database, or keeping in # sync with Ace ranges. - @changes = [] - @comments = [] @id = 0 addComment: (offset, length, metadata) -> @@ -375,7 +371,23 @@ define [ @emit "changes:moved", moved_changes _newId: () -> - (@id++).toString() + # Generate a Mongo ObjectId + # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js + @_pid ?= Math.floor(Math.random() * (32767)) + @_machine ?= Math.floor(Math.random() * (16777216)) + timestamp = Math.floor(new Date().valueOf() / 1000) + @_increment ?= 0 + @_increment++ + + timestamp = timestamp.toString(16) + machine = @_machine.toString(16) + pid = @_pid.toString(16) + increment = @_increment.toString(16) + + return '00000000'.substr(0, 8 - timestamp.length) + timestamp + + '000000'.substr(0, 6 - machine.length) + machine + + '0000'.substr(0, 4 - pid.length) + pid + + '000000'.substr(0, 6 - increment.length) + increment; _addOp: (op, metadata) -> change = { @@ -453,3 +465,9 @@ define [ else # Only update to the current change if we haven't removed it. previous_change = change return { moved_changes, remove_changes } + +if define? + define ["utils/EventEmitter"], load +else + EventEmitter = require("events").EventEmitter + module.exports = load(EventEmitter) \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 9623e2af9a..d581c388a9 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -2,8 +2,8 @@ define [ "base", "utils/EventEmitter" "ide/colors/ColorManager" - "ide/review-panel/ChangesTracker" -], (App, EventEmitter, ColorManager, ChangesTracker) -> + "ide/review-panel/RangesTracker" +], (App, EventEmitter, ColorManager, RangesTracker) -> App.controller "ReviewPanelController", ($scope, $element, ide, $timeout) -> $reviewPanelEl = $element.find "#review-panel" @@ -13,7 +13,6 @@ define [ $scope.reviewPanel = entries: {} - trackNewChanges: false hasEntries: false subView: $scope.SubViews.CUR_FILE openSubView: $scope.SubViews.CUR_FILE @@ -24,15 +23,15 @@ define [ $scope.reviewPanelEventsBridge = new EventEmitter() - changesTrackers = {} + rangesTrackers = {} getDocEntries = (doc_id) -> $scope.reviewPanel.entries[doc_id] ?= {} return $scope.reviewPanel.entries[doc_id] getChangeTracker = (doc_id) -> - changesTrackers[doc_id] ?= new ChangesTracker() - return changesTrackers[doc_id] + rangesTrackers[doc_id] ?= new RangesTracker() + return rangesTrackers[doc_id] # TODO Just for prototyping purposes; remove afterwards. mockedUserId = 'mock_user_id_1' @@ -115,12 +114,12 @@ define [ ide.$scope.$on "file-tree:initialized", () -> ide.fileTreeManager.forEachEntity (entity) -> if mock_changes[entity.name]? - changesTracker = getChangeTracker(entity.id) + rangesTracker = getChangeTracker(entity.id) for change in mock_changes[entity.name].changes - changesTracker._addOp change.op, change.metadata + rangesTracker._addOp change.op, change.metadata for comment in mock_changes[entity.name].comments - changesTracker.addComment comment.offset, comment.length, comment.metadata - for doc_id, changesTracker of changesTrackers + rangesTracker.addComment comment.offset, comment.length, comment.metadata + for doc_id, rangesTracker of rangesTrackers updateEntries(doc_id) scrollbar = {} @@ -150,8 +149,8 @@ define [ $scope.$watch "editor.open_doc_id", (open_doc_id) -> return if !open_doc_id? - changesTrackers[open_doc_id] ?= new ChangesTracker() - $scope.reviewPanel.changesTracker = changesTrackers[open_doc_id] + rangesTrackers[open_doc_id] ?= new RangesTracker() + $scope.reviewPanel.rangesTracker = rangesTrackers[open_doc_id] $scope.$watch (() -> entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {} @@ -166,14 +165,14 @@ define [ $scope.$broadcast "review-panel:layout" updateEntries = (doc_id) -> - changesTracker = getChangeTracker(doc_id) + rangesTracker = getChangeTracker(doc_id) entries = getDocEntries(doc_id) # Assume we'll delete everything until we see it, then we'll remove it from this object delete_changes = {} delete_changes[change_id] = true for change_id, change of entries - for change in changesTracker.changes + for change in rangesTracker.changes delete delete_changes[change.id] entries[change.id] ?= {} @@ -189,7 +188,7 @@ define [ for key, value of new_entry entries[change.id][key] = value - for comment in changesTracker.comments + for comment in rangesTracker.comments delete delete_changes[comment.id] entries[comment.id] ?= {} new_entry = { From 293ba1fc4c4cc2244a126e864a823bad77de33b3 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 9 Dec 2016 15:43:08 +0000 Subject: [PATCH 03/98] Fetch all ranges from docstore when viewing overview panel --- .../Features/Docstore/DocstoreManager.coffee | 15 +++++++++ .../Features/Editor/EditorController.coffee | 1 - .../HistoryController.coffee} | 4 +-- .../HistoryManager.coffee} | 2 +- .../InactiveProjectManager.coffee | 3 -- .../Features/Ranges/RangesController.coffee | 11 +++++++ .../Features/Ranges/RangesManager.coffee | 8 +++++ services/web/app/coffee/router.coffee | 11 ++++--- .../views/project/editor/review-panel.jade | 3 ++ .../controllers/ReviewPanelController.coffee | 24 +++++++++++++- .../Docstore/DocstoreManagerTests.coffee | 32 +++++++++++++++++++ .../HistoryControllerTests.coffee} | 10 +++--- .../HistoryManagerTests.coffee} | 14 ++++---- 13 files changed, 114 insertions(+), 24 deletions(-) rename services/web/app/coffee/Features/{TrackChanges/TrackChangesController.coffee => History/HistoryController.coffee} (84%) rename services/web/app/coffee/Features/{TrackChanges/TrackChangesManager.coffee => History/HistoryManager.coffee} (95%) create mode 100644 services/web/app/coffee/Features/Ranges/RangesController.coffee create mode 100644 services/web/app/coffee/Features/Ranges/RangesManager.coffee rename services/web/test/UnitTests/coffee/{TrackChanges/TrackChangesControllerTests.coffee => History/HistoryControllerTests.coffee} (83%) rename services/web/test/UnitTests/coffee/{TrackChanges/TrackChangesManagerTests.coffee => History/HistoryManagerTests.coffee} (81%) diff --git a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee index cf48dfe07b..06dd14c17b 100644 --- a/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee +++ b/services/web/app/coffee/Features/Docstore/DocstoreManager.coffee @@ -29,6 +29,21 @@ module.exports = DocstoreManager = error = new Error("docstore api responded with non-success code: #{res.statusCode}") logger.error err: error, project_id: project_id, "error getting all docs from docstore" callback(error) + + getAllRanges: (project_id, callback = (error) ->) -> + logger.log { project_id }, "getting all doc ranges for project in docstore api" + url = "#{settings.apis.docstore.url}/project/#{project_id}/ranges" + request.get { + url: url + json: true + }, (error, res, docs) -> + return callback(error) if error? + if 200 <= res.statusCode < 300 + callback(null, docs) + else + error = new Error("docstore api responded with non-success code: #{res.statusCode}") + logger.error err: error, project_id: project_id, "error getting all doc ranges from docstore" + callback(error) getDoc: (project_id, doc_id, options = {}, callback = (error, lines, rev, version) ->) -> if typeof(options) == "function" diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index 476ba96174..b5abab3bf9 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -7,7 +7,6 @@ ProjectDetailsHandler = require('../Project/ProjectDetailsHandler') ProjectDeleter = require("../Project/ProjectDeleter") DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler') EditorRealTimeController = require("./EditorRealTimeController") -TrackChangesManager = require("../TrackChanges/TrackChangesManager") async = require('async') LockManager = require("../../infrastructure/LockManager") _ = require('underscore') diff --git a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee similarity index 84% rename from services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee rename to services/web/app/coffee/Features/History/HistoryController.coffee index bc6e00a29a..d4f42b38b1 100644 --- a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee +++ b/services/web/app/coffee/Features/History/HistoryController.coffee @@ -3,8 +3,8 @@ request = require "request" settings = require "settings-sharelatex" AuthenticationController = require "../Authentication/AuthenticationController" -module.exports = TrackChangesController = - proxyToTrackChangesApi: (req, res, next = (error) ->) -> +module.exports = HistoryController = + proxyToHistoryApi: (req, res, next = (error) ->) -> user_id = AuthenticationController.getLoggedInUserId req url = settings.apis.trackchanges.url + req.url logger.log url: url, "proxying to track-changes api" diff --git a/services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee b/services/web/app/coffee/Features/History/HistoryManager.coffee similarity index 95% rename from services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee rename to services/web/app/coffee/Features/History/HistoryManager.coffee index ddcfe3e44a..ea3f492613 100644 --- a/services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee +++ b/services/web/app/coffee/Features/History/HistoryManager.coffee @@ -2,7 +2,7 @@ settings = require "settings-sharelatex" request = require "request" logger = require "logger-sharelatex" -module.exports = TrackChangesManager = +module.exports = HistoryManager = flushProject: (project_id, callback = (error) ->) -> logger.log project_id: project_id, "flushing project in track-changes api" url = "#{settings.apis.trackchanges.url}/project/#{project_id}/flush" diff --git a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee index a2afee0573..5c984dcb5d 100644 --- a/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee +++ b/services/web/app/coffee/Features/InactiveData/InactiveProjectManager.coffee @@ -5,8 +5,6 @@ DocstoreManager = require("../Docstore/DocstoreManager") ProjectGetter = require("../Project/ProjectGetter") ProjectUpdateHandler = require("../Project/ProjectUpdateHandler") Project = require("../../models/Project").Project -TrackChangesManager = require("../TrackChanges/TrackChangesManager") - MILISECONDS_IN_DAY = 86400000 module.exports = InactiveProjectManager = @@ -52,7 +50,6 @@ module.exports = InactiveProjectManager = logger.log project_id:project_id, "deactivating inactive project" jobs = [ (cb)-> DocstoreManager.archiveProject project_id, cb - # (cb)-> TrackChangesManager.archiveProject project_id, cb (cb)-> ProjectUpdateHandler.markAsInactive project_id, cb ] async.series jobs, (err)-> diff --git a/services/web/app/coffee/Features/Ranges/RangesController.coffee b/services/web/app/coffee/Features/Ranges/RangesController.coffee new file mode 100644 index 0000000000..96a42588ac --- /dev/null +++ b/services/web/app/coffee/Features/Ranges/RangesController.coffee @@ -0,0 +1,11 @@ +RangesManager = require "./RangesManager" +logger = require "logger-sharelatex" + +module.exports = RangesController = + getAllRanges: (req, res, next) -> + project_id = req.params.project_id + logger.log {project_id}, "request for project ranges" + RangesManager.getAllRanges project_id, (error, docs = []) -> + return next(error) if error? + docs = ({id: d._id, ranges: d.ranges} for d in docs) + res.json docs diff --git a/services/web/app/coffee/Features/Ranges/RangesManager.coffee b/services/web/app/coffee/Features/Ranges/RangesManager.coffee new file mode 100644 index 0000000000..1fdf55b4a8 --- /dev/null +++ b/services/web/app/coffee/Features/Ranges/RangesManager.coffee @@ -0,0 +1,8 @@ +DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" +DocstoreManager = require "../Docstore/DocstoreManager" + +module.exports = RangesManager = + getAllRanges: (project_id, callback = (error, docs) ->) -> + DocumentUpdaterHandler.flushProjectToMongo project_id, (error) -> + return callback(error) if error? + DocstoreManager.getAllRanges project_id, callback \ No newline at end of file diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 14ac3b8d22..36e26782ba 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -25,7 +25,7 @@ ClsiCookieManager = require("./Features/Compile/ClsiCookieManager") HealthCheckController = require("./Features/HealthCheck/HealthCheckController") ProjectDownloadsController = require "./Features/Downloads/ProjectDownloadsController" FileStoreController = require("./Features/FileStore/FileStoreController") -TrackChangesController = require("./Features/TrackChanges/TrackChangesController") +HistoryController = require("./Features/History/HistoryController") PasswordResetRouter = require("./Features/PasswordReset/PasswordResetRouter") StaticPagesRouter = require("./Features/StaticPages/StaticPagesRouter") ChatController = require("./Features/Chat/ChatController") @@ -40,6 +40,7 @@ AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlew BetaProgramController = require('./Features/BetaProgram/BetaProgramController') AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') AnnouncementsController = require("./Features/Announcements/AnnouncementsController") +RangesController = require("./Features/Ranges/RangesController") logger = require("logger-sharelatex") _ = require("underscore") @@ -171,9 +172,11 @@ module.exports = class Router webRouter.post '/project/:Project_id/rename', AuthorizationMiddlewear.ensureUserCanAdminProject, ProjectController.renameProject - webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi - webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi - webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.proxyToTrackChangesApi + webRouter.get "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi + webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi + webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi + + webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, RangesController.getAllRanges webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index be41e80e2c..2da1277572 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -49,8 +49,11 @@ .rp-entry-list( ng-if="reviewPanel.subView === SubViews.OVERVIEW" ) + .rp-overview-loading(ng-if="reviewPanel.overview.loading") + i.fa.fa-spinner.fa-spin .rp-overview-file( ng-repeat="(doc_id, entries) in reviewPanel.entries" + ng-if="!reviewPanel.overview.loading" ) .rp-overview-file-header | {{ getFileName(doc_id) }} diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index d581c388a9..8543e86402 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -4,7 +4,7 @@ define [ "ide/colors/ColorManager" "ide/review-panel/RangesTracker" ], (App, EventEmitter, ColorManager, RangesTracker) -> - App.controller "ReviewPanelController", ($scope, $element, ide, $timeout) -> + App.controller "ReviewPanelController", ($scope, $element, ide, $timeout, $http) -> $reviewPanelEl = $element.find "#review-panel" $scope.SubViews = @@ -16,6 +16,8 @@ define [ hasEntries: false subView: $scope.SubViews.CUR_FILE openSubView: $scope.SubViews.CUR_FILE + overview: + loading: false $scope.commentState = adding: false @@ -146,6 +148,11 @@ define [ else # Reset back to what we had when previously open $scope.reviewPanel.subView = $scope.reviewPanel.openSubView + + $scope.$watch "reviewPanel.subView", (view) -> + return if !view? + if view == $scope.SubViews.OVERVIEW + refreshOverviewPanel() $scope.$watch "editor.open_doc_id", (open_doc_id) -> return if !open_doc_id? @@ -164,6 +171,21 @@ define [ $scope.$broadcast "review-panel:toggle" $scope.$broadcast "review-panel:layout" + refreshOverviewPanel = () -> + $scope.reviewPanel.overview.loading = true + $http.get "/project/#{$scope.project_id}/ranges" + .success (docs) -> + for doc in docs + if doc.id != $scope.editor.open_doc_id # this is kept up to date in real-time, don't overwrite + rangesTrackers[doc.id] ?= new RangesTracker() + rangesTrackers[doc.id].comments = doc.ranges?.comments or [] + rangesTrackers[doc.id].changes = doc.ranges?.changes or [] + updateEntries(doc.id) + $scope.reviewPanel.overview.loading = false + .error (error) -> + console.log "loading ranges errored", error + $scope.reviewPanel.overview.loading = false + updateEntries = (doc_id) -> rangesTracker = getChangeTracker(doc_id) entries = getDocEntries(doc_id) diff --git a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee index 52603c28bb..abcc55a0b9 100644 --- a/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/Docstore/DocstoreManagerTests.coffee @@ -186,6 +186,38 @@ describe "DocstoreManager", -> }, "error getting all docs from docstore") .should.equal true + describe "getAllRanges", -> + describe "with a successful response code", -> + beforeEach -> + @request.get = sinon.stub().callsArgWith(1, null, statusCode: 204, @docs = [{ _id: "mock-doc-id", ranges: "mock-ranges" }]) + @DocstoreManager.getAllRanges @project_id, @callback + + it "should get all the project doc ranges in the docstore api", -> + @request.get + .calledWith({ + url: "#{@settings.apis.docstore.url}/project/#{@project_id}/ranges" + json: true + }) + .should.equal true + + it "should call the callback with the docs", -> + @callback.calledWith(null, @docs).should.equal true + + describe "with a failed response code", -> + beforeEach -> + @request.get = sinon.stub().callsArgWith(1, null, statusCode: 500, "") + @DocstoreManager.getAllRanges @project_id, @callback + + it "should call the callback with an error", -> + @callback.calledWith(new Error("docstore api responded with non-success code: 500")).should.equal true + + it "should log the error", -> + @logger.error + .calledWith({ + err: new Error("docstore api responded with a non-success code: 500") + project_id: @project_id + }, "error getting all doc ranges from docstore") + .should.equal true describe "archiveProject", -> describe "with a successful response code", -> diff --git a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee b/services/web/test/UnitTests/coffee/History/HistoryControllerTests.coffee similarity index 83% rename from services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee rename to services/web/test/UnitTests/coffee/History/HistoryControllerTests.coffee index bcc57b58b8..577aae6a9d 100644 --- a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/History/HistoryControllerTests.coffee @@ -1,21 +1,21 @@ chai = require('chai') chai.should() sinon = require("sinon") -modulePath = "../../../../app/js/Features/TrackChanges/TrackChangesController" +modulePath = "../../../../app/js/Features/History/HistoryController" SandboxedModule = require('sandboxed-module') -describe "TrackChangesController", -> +describe "HistoryController", -> beforeEach -> @user_id = "user-id-123" @AuthenticationController = getLoggedInUserId: sinon.stub().returns(@user_id) - @TrackChangesController = SandboxedModule.require modulePath, requires: + @HistoryController = SandboxedModule.require modulePath, requires: "request" : @request = sinon.stub() "settings-sharelatex": @settings = {} "logger-sharelatex": @logger = {log: sinon.stub(), error: sinon.stub()} "../Authentication/AuthenticationController": @AuthenticationController - describe "proxyToTrackChangesApi", -> + describe "proxyToHistoryApi", -> beforeEach -> @req = { url: "/mock/url", method: "POST" } @res = "mock-res" @@ -28,7 +28,7 @@ describe "TrackChangesController", -> pipe: sinon.stub() on: (event, handler) -> @events[event] = handler @request.returns @proxy - @TrackChangesController.proxyToTrackChangesApi @req, @res, @next + @HistoryController.proxyToHistoryApi @req, @res, @next describe "successfully", -> it "should get the user id", -> diff --git a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesManagerTests.coffee b/services/web/test/UnitTests/coffee/History/HistoryManagerTests.coffee similarity index 81% rename from services/web/test/UnitTests/coffee/TrackChanges/TrackChangesManagerTests.coffee rename to services/web/test/UnitTests/coffee/History/HistoryManagerTests.coffee index 90b36f89c5..65b22812ea 100644 --- a/services/web/test/UnitTests/coffee/TrackChanges/TrackChangesManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/History/HistoryManagerTests.coffee @@ -2,12 +2,12 @@ chai = require('chai') expect = chai.expect chai.should() sinon = require("sinon") -modulePath = "../../../../app/js/Features/TrackChanges/TrackChangesManager" +modulePath = "../../../../app/js/Features/History/HistoryManager" SandboxedModule = require('sandboxed-module') -describe "TrackChangesManager", -> +describe "HistoryManager", -> beforeEach -> - @TrackChangesManager = SandboxedModule.require modulePath, requires: + @HistoryManager = SandboxedModule.require modulePath, requires: "request" : @request = sinon.stub() "settings-sharelatex": @settings = apis: @@ -22,7 +22,7 @@ describe "TrackChangesManager", -> describe "with a successful response code", -> beforeEach -> @request.post = sinon.stub().callsArgWith(1, null, statusCode: 204, "") - @TrackChangesManager.flushProject @project_id, @callback + @HistoryManager.flushProject @project_id, @callback it "should flush the project in the track changes api", -> @request.post @@ -35,7 +35,7 @@ describe "TrackChangesManager", -> describe "with a failed response code", -> beforeEach -> @request.post = sinon.stub().callsArgWith(1, null, statusCode: 500, "") - @TrackChangesManager.flushProject @project_id, @callback + @HistoryManager.flushProject @project_id, @callback it "should call the callback with an error", -> @callback.calledWith(new Error("track-changes api responded with a non-success code: 500")).should.equal true @@ -52,12 +52,12 @@ describe "TrackChangesManager", -> it "should call the post endpoint", (done)-> @request.post.callsArgWith(1, null, {}) - @TrackChangesManager.archiveProject @project_id, (err)=> + @HistoryManager.archiveProject @project_id, (err)=> @request.post.calledWith("#{@settings.apis.trackchanges.url}/project/#{@project_id}/archive") done() it "should return an error on a non success", (done)-> @request.post.callsArgWith(1, null, {statusCode:500}) - @TrackChangesManager.archiveProject @project_id, (err)=> + @HistoryManager.archiveProject @project_id, (err)=> expect(err).to.exist done() \ No newline at end of file From 0a6a6c3c2831b30706da1a3916d23fad3ed2b9e5 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 9 Dec 2016 16:17:28 +0000 Subject: [PATCH 04/98] Keep files ordered in overview panel in the same was as the file tree --- .../web/app/views/project/editor/review-panel.jade | 10 ++++++---- .../public/coffee/ide/file-tree/FileTreeManager.coffee | 10 ++++++++++ .../coffee/ide/review-panel/ReviewPanelManager.coffee | 1 + .../controllers/ReviewPanelController.coffee | 9 --------- .../coffee/ide/review-panel/filters/notEmpty.coffee | 5 +++++ 5 files changed, 22 insertions(+), 13 deletions(-) create mode 100644 services/web/public/coffee/ide/review-panel/filters/notEmpty.coffee diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 2da1277572..f6e43f93f2 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -52,13 +52,15 @@ .rp-overview-loading(ng-if="reviewPanel.overview.loading") i.fa.fa-spinner.fa-spin .rp-overview-file( - ng-repeat="(doc_id, entries) in reviewPanel.entries" + ng-repeat="doc in docs" ng-if="!reviewPanel.overview.loading" ) - .rp-overview-file-header - | {{ getFileName(doc_id) }} + .rp-overview-file-header( + ng-if="reviewPanel.entries[doc.doc.id] | notEmpty" + ) + | {{ doc.path }} .rp-entry-wrapper( - ng-repeat="(entry_id, entry) in entries | orderOverviewEntries" + ng-repeat="(entry_id, entry) in reviewPanel.entries[doc.doc.id] | orderOverviewEntries" ) div(ng-if="entry.type === 'insert' || entry.type === 'delete'") change-entry( diff --git a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee index 8c49d54c23..c4ad4b30a4 100644 --- a/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee +++ b/services/web/public/coffee/ide/file-tree/FileTreeManager.coffee @@ -275,6 +275,16 @@ define [ doc: entity path: path } + # Keep list ordered by folders, then name + @$scope.docs.sort (a,b) -> + aDepth = (a.path.match(/\//g) || []).length + bDepth = (b.path.match(/\//g) || []).length + if aDepth - bDepth != 0 + return -(aDepth - bDepth) # Deeper path == folder first + else if a.path < b.path + return -1 + else + return 1 getEntityPath: (entity) -> @_getEntityPathInFolder @$scope.rootFolder, entity diff --git a/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee b/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee index 6a23d15016..cd1231c798 100644 --- a/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee +++ b/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee @@ -5,5 +5,6 @@ define [ "ide/review-panel/directives/changeEntry" "ide/review-panel/directives/commentEntry" "ide/review-panel/directives/addCommentEntry" + "ide/review-panel/filters/notEmpty" "ide/review-panel/filters/orderOverviewEntries" ], () -> \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 8543e86402..0990baac4d 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -354,15 +354,6 @@ define [ $scope.gotoEntry = (doc_id, entry) -> ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset }) - DOC_ID_NAMES = {} - $scope.getFileName = (doc_id) -> - # This is called a lot and is relatively expensive, so cache the result - if !DOC_ID_NAMES[doc_id]? - entity = ide.fileTreeManager.findEntityById(doc_id) - return if !entity? - DOC_ID_NAMES[doc_id] = ide.fileTreeManager.getEntityPath(entity) - return DOC_ID_NAMES[doc_id] - # TODO: Eventually we need to get this from the server, and update it # when we get an id we don't know. This'll do for client side testing refreshUsers = () -> diff --git a/services/web/public/coffee/ide/review-panel/filters/notEmpty.coffee b/services/web/public/coffee/ide/review-panel/filters/notEmpty.coffee new file mode 100644 index 0000000000..52100c7ff1 --- /dev/null +++ b/services/web/public/coffee/ide/review-panel/filters/notEmpty.coffee @@ -0,0 +1,5 @@ +define [ + "base" +], (App) -> + app.filter 'notEmpty', () -> + (object) -> !angular.equals({}, object) From 898277b4afee2bf973747ccf4239a357665d6bfb Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 13 Dec 2016 17:34:29 +0000 Subject: [PATCH 05/98] Refactor ops model so it all happens in Document --- .../web/app/views/project/editor/editor.jade | 1 - .../public/coffee/ide/editor/Document.coffee | 23 +++++- .../coffee/ide/editor/ShareJsDoc.coffee | 4 +- .../ide/editor/directives/aceEditor.coffee | 1 - .../track-changes/TrackChangesManager.coffee | 78 ++++++------------- .../editor/sharejs/vendor/client/doc.coffee | 5 +- .../editor/sharejs/vendor/types/text.coffee | 66 ++++++++++++++-- .../controllers/ReviewPanelController.coffee | 11 ++- 8 files changed, 115 insertions(+), 74 deletions(-) diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index 3126d63df2..8d58e35f2e 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -53,7 +53,6 @@ div.full-size( events-bridge="reviewPanelEventsBridge" track-changes-enabled="trackChangesFeatureFlag", track-changes= "editor.trackChanges", - ranges-tracker="reviewPanel.rangesTracker", doc-id="editor.open_doc_id" ) diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index 213fbe3514..a0c07443c8 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -1,7 +1,8 @@ define [ "utils/EventEmitter" "ide/editor/ShareJsDoc" -], (EventEmitter, ShareJsDoc) -> + "ide/review-panel/RangesTracker" +], (EventEmitter, ShareJsDoc, RangesTracker) -> class Document extends EventEmitter @getDocument: (ide, doc_id) -> @openDocs ||= {} @@ -247,14 +248,15 @@ define [ @joined = true @doc.catchUp( updates ) # TODO: Worry about whether these ranges are consistent with the doc still - @opening_ranges = ranges + @ranges?.changes = ranges?.changes + @ranges?.comments = ranges?.comments callback() else @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version, updates, ranges) => return callback(error) if error? @joined = true @doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket - @opening_ranges = ranges + @ranges = new RangesTracker(ranges?.changes, ranges?.comments) @_bindToShareJsDocEvents() callback() @@ -313,6 +315,8 @@ define [ inflightOp: inflightOp, pendingOp: pendingOp v: version + @doc.on "change", (ops, oldSnapshot, msg) => + @_applyOpsToRanges(ops, oldSnapshot, msg) _onError: (error, meta = {}) -> meta.doc_id = @doc_id @@ -325,3 +329,16 @@ define [ # the disconnect event, which means we try to leaveDoc when the connection comes back. # This could intefere with the new connection of a new instance of this document. @_cleanUp() + + _applyOpsToRanges: (ops = [], oldSnapshot, msg) -> + track_changes_as = null + remote_op = msg? + if remote_op and msg.meta?.tc + track_changes_as = msg.meta.user_id + else if !remote_op and @track_changes_as? + track_changes_as = @track_changes_as + console.log "CHANGED", oldSnapshot, ops, track_changes_as + @ranges.track_changes = track_changes_as? + for op in ops + console.log "APPLYING OP", op, @ranges.track_changes + @ranges.applyOp op, { user_id: track_changes_as } diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee index bb2d6b3e38..feb82c1e3f 100644 --- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee @@ -34,8 +34,8 @@ define [ @_doc = new ShareJs.Doc @connection, @doc_id, type: @type @_doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY) - @_doc.on "change", () => - @trigger "change" + @_doc.on "change", (args...) => + @trigger "change", args... @_doc.on "acknowledge", () => @lastAcked = new Date() # note time of last ack from server for an op we sent @trigger "acknowledge" diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index bbbcff5a85..88ce71fcd2 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -56,7 +56,6 @@ define [ eventsBridge: "=" trackChanges: "=" trackChangesEnabled: "=" - rangesTracker: "=" docId: "=" } link: (scope, element, attrs) -> diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index 54b20f7018..ca8452f5c0 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -10,22 +10,15 @@ define [ constructor: (@$scope, @editor, @element) -> window.trackChangesManager ?= @ - @$scope.$watch "rangesTracker", (rangesTracker) => - return if !rangesTracker? - @disconnectFromRangesTracker() - @rangesTracker = rangesTracker - @connectToRangesTracker() - @$scope.$watch "trackChanges", (track_changes) => return if !track_changes? - @rangesTracker?.track_changes = track_changes + @setTrackChanges(track_changes) @$scope.$watch "sharejsDoc", (doc) => return if !doc? - if doc.opening_ranges?.changes? - @rangesTracker.changes = doc.opening_ranges.changes - if doc.opening_ranges?.comments? - @rangesTracker.comments = doc.opening_ranges.comments + @disconnectFromRangesTracker() + @rangesTracker = doc.ranges + @connectToRangesTracker() @$scope.$on "comment:add", (e, comment) => @addCommentToSelection(comment) @@ -65,48 +58,15 @@ define [ onResize = () => @recalculateReviewEntriesScreenPositions() - onChange = (e) => - if !@editor.initing - # This change is trigger by a sharejs 'change' event, which is before the - # sharejs 'remoteop' event. So wait until the next event loop when the 'remoteop' - # will have fired, before we decide if it was a remote op. - setTimeout () => - if @nextUpdateMetaData? - user_id = @nextUpdateMetaData.user_id - # The remote op may have contained multiple atomic ops, each of which is an Ace - # 'change' event (i.e. bulk commenting out of lines is a single remote op - # but gives us one event for each % inserted). These all come in a single event loop - # though, so wait until the next one before clearing the metadata. - setTimeout () => - @nextUpdateMetaData = null - else - user_id = window.user.id - - was_tracking = @rangesTracker.track_changes - if @dont_track_next_update - @rangesTracker.track_changes = false - @dont_track_next_update = false - @applyChange(e, { user_id }) - @rangesTracker.track_changes = was_tracking - - # TODO: Just for debugging, remove before going live. - setTimeout () => - @checkMapping() - , 100 - onChangeSession = (e) => - e.oldSession?.getDocument().off "change", onChange - e.session.getDocument().on "change", onChange @redrawAnnotations() bindToAce = () => - @editor.getSession().getDocument().on "change", onChange @editor.on "changeSelection", onChangeSelection @editor.on "changeSession", onChangeSession @editor.renderer.on "resize", onResize unbindFromAce = () => - @editor.getSession().getDocument().off "change", onChange @editor.off "changeSelection", onChangeSelection @editor.off "changeSession", onChangeSession @editor.renderer.off "resize", onResize @@ -132,41 +92,49 @@ define [ @rangesTracker.off "comment:removed" @rangesTracker.off "comment:resolved" @rangesTracker.off "comment:unresolved" + + setTrackChanges: (value) -> + if value + @$scope.sharejsDoc?.track_changes_as = window.user.id + else + @$scope.sharejsDoc?.track_changes_as = null connectToRangesTracker: () -> - @rangesTracker.track_changes = @$scope.trackChanges + @setTrackChanges(@$scope.trackChanges) + # Add a timeout because on remote ops, we get these notifications before + # ace has updated @rangesTracker.on "insert:added", (change) => sl_console.log "[insert:added]", change - @_onInsertAdded(change) + setTimeout () => @_onInsertAdded(change) @rangesTracker.on "insert:removed", (change) => sl_console.log "[insert:removed]", change - @_onInsertRemoved(change) + setTimeout () => @_onInsertRemoved(change) @rangesTracker.on "delete:added", (change) => sl_console.log "[delete:added]", change - @_onDeleteAdded(change) + setTimeout () => @_onDeleteAdded(change) @rangesTracker.on "delete:removed", (change) => sl_console.log "[delete:removed]", change - @_onDeleteRemoved(change) + setTimeout () => @_onDeleteRemoved(change) @rangesTracker.on "changes:moved", (changes) => sl_console.log "[changes:moved]", changes - @_onChangesMoved(changes) + setTimeout () => @_onChangesMoved(changes) @rangesTracker.on "comment:added", (comment) => sl_console.log "[comment:added]", comment - @_onCommentAdded(comment) + setTimeout () => @_onCommentAdded(comment) @rangesTracker.on "comment:moved", (comment) => sl_console.log "[comment:moved]", comment - @_onCommentMoved(comment) + setTimeout () => @_onCommentMoved(comment) @rangesTracker.on "comment:removed", (comment) => sl_console.log "[comment:removed]", comment - @_onCommentRemoved(comment) + setTimeout () => @_onCommentRemoved(comment) @rangesTracker.on "comment:resolved", (comment) => sl_console.log "[comment:resolved]", comment - @_onCommentRemoved(comment) + setTimeout () => @_onCommentRemoved(comment) @rangesTracker.on "comment:unresolved", (comment) => sl_console.log "[comment:unresolved]", comment - @_onCommentAdded(comment) + setTimeout () => @_onCommentAdded(comment) redrawAnnotations: () -> for change in @rangesTracker.changes diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee index d25baf89d5..301c4d5e04 100644 --- a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee +++ b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee @@ -71,7 +71,7 @@ class Doc # Its important that these event handlers are called with oldSnapshot. # The reason is that the OT type APIs might need to access the snapshots to # determine information about the received op. - @emit 'change', docOp, oldSnapshot + @emit 'change', docOp, oldSnapshot, msg @emit 'remoteop', docOp, oldSnapshot, msg if isRemote _connectionStateChanged: (state, data) -> @@ -274,6 +274,7 @@ class Doc submitOp: (op, callback) -> op = @type.normalize(op) if @type.normalize? + oldSnapshot = @snapshot # If this throws an exception, no changes should have been made to the doc @snapshot = @type.apply @snapshot, op @@ -284,7 +285,7 @@ class Doc @pendingCallbacks.push callback if callback - @emit 'change', op + @emit 'change', op, oldSnapshot @delayedFlush() diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/types/text.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/types/text.coffee index c64b4dfa68..2a3b79997d 100644 --- a/services/web/public/coffee/ide/editor/sharejs/vendor/types/text.coffee +++ b/services/web/public/coffee/ide/editor/sharejs/vendor/types/text.coffee @@ -31,7 +31,8 @@ checkValidComponent = (c) -> i_type = typeof c.i d_type = typeof c.d - throw new Error 'component needs an i or d field' unless (i_type == 'string') ^ (d_type == 'string') + c_type = typeof c.c + throw new Error 'component needs an i, d or c field' unless (i_type == 'string') ^ (d_type == 'string') ^ (c_type == 'string') throw new Error 'position cannot be negative' unless c.p >= 0 @@ -44,11 +45,15 @@ text.apply = (snapshot, op) -> for component in op if component.i? snapshot = strInject snapshot, component.p, component.i - else + else if component.d? deleted = snapshot[component.p...(component.p + component.d.length)] throw new Error "Delete component '#{component.d}' does not match deleted text '#{deleted}'" unless component.d == deleted snapshot = snapshot[...component.p] + snapshot[(component.p + component.d.length)..] - + else if component.c? + comment = snapshot[component.p...(component.p + component.c.length)] + throw new Error "Comment component '#{component.c}' does not match commented text '#{comment}'" unless component.c == comment + else + throw new Error "Unknown op type" snapshot @@ -112,7 +117,7 @@ transformPosition = (pos, c, insertAfter) -> pos + c.i.length else pos - else + else if c.d? # I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length)) # but I think its harder to read that way, and it compiles using ternary operators anyway # so its no slower written like this. @@ -122,6 +127,10 @@ transformPosition = (pos, c, insertAfter) -> c.p else pos - c.d.length + else if c.c? + pos + else + throw new Error("unknown op type") # Helper method to transform a cursor position as a result of an op. # @@ -143,7 +152,7 @@ text._tc = transformComponent = (dest, c, otherC, side) -> if c.i? append dest, {i:c.i, p:transformPosition(c.p, otherC, side == 'right')} - else # Delete + else if c.d? # Delete if otherC.i? # delete vs insert s = c.d if c.p < otherC.p @@ -152,7 +161,7 @@ text._tc = transformComponent = (dest, c, otherC, side) -> if s != '' append dest, {d:s, p:c.p + otherC.i.length} - else # Delete vs delete + else if otherC.d? # Delete vs delete if c.p >= otherC.p + otherC.d.length append dest, {d:c.d, p:c.p - otherC.d.length} else if c.p + c.d.length <= otherC.p @@ -177,6 +186,51 @@ text._tc = transformComponent = (dest, c, otherC, side) -> # This could be rewritten similarly to insert v delete, above. newC.p = transformPosition newC.p, otherC append dest, newC + + else if otherC.c? + append dest, c + + else + throw new Error("unknown op type") + + else if c.c? # Comment + if otherC.i? + if c.p < otherC.p < c.p + c.c.length + offset = otherC.p - c.p + new_c = (c.c[0..(offset-1)] + otherC.i + c.c[offset...]) + append dest, {c:new_c, p:c.p, t: c.t} + else + append dest, {c:c.c, p:transformPosition(c.p, otherC, true), t: c.t} + + else if otherC.d? + if c.p >= otherC.p + otherC.d.length + append dest, {c:c.c, p:c.p - otherC.d.length, t: c.t} + else if c.p + c.c.length <= otherC.p + append dest, c + else # Delete overlaps comment + # They overlap somewhere. + newC = {c:'', p:c.p, t: c.t} + if c.p < otherC.p + newC.c = c.c[...(otherC.p - c.p)] + if c.p + c.c.length > otherC.p + otherC.d.length + newC.c += c.c[(otherC.p + otherC.d.length - c.p)..] + + # This is entirely optional - just for a check that the deleted + # text in the two ops matches + intersectStart = Math.max c.p, otherC.p + intersectEnd = Math.min c.p + c.c.length, otherC.p + otherC.d.length + cIntersect = c.c[intersectStart - c.p...intersectEnd - c.p] + otherIntersect = otherC.d[intersectStart - otherC.p...intersectEnd - otherC.p] + throw new Error 'Delete ops delete different text in the same region of the document' unless cIntersect == otherIntersect + + newC.p = transformPosition newC.p, otherC + append dest, newC + + else if otherC.c? + append dest, c + + else + throw new Error("unknown op type") dest diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 0990baac4d..df4afe306a 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -154,10 +154,13 @@ define [ if view == $scope.SubViews.OVERVIEW refreshOverviewPanel() - $scope.$watch "editor.open_doc_id", (open_doc_id) -> - return if !open_doc_id? - rangesTrackers[open_doc_id] ?= new RangesTracker() - $scope.reviewPanel.rangesTracker = rangesTrackers[open_doc_id] + $scope.$watch "editor.sharejs_doc", (doc) -> + return if !doc? + console.log "DOC changed", doc + # The open doc range tracker is kept up to date in real-time so + # replace any outdated info with this + rangesTrackers[doc.doc_id] = doc.ranges + $scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id] $scope.$watch (() -> entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {} From 5717cafcec6e0dc7929409995496ce0eaf961d53 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 13 Dec 2016 17:57:46 +0000 Subject: [PATCH 06/98] Create comments via comment ops --- .../public/coffee/ide/editor/Document.coffee | 2 + .../coffee/ide/editor/ShareJsDoc.coffee | 1 + .../track-changes/TrackChangesManager.coffee | 36 +++++----- .../sharejs/vendor/types/text-api.coffee | 2 +- .../ide/review-panel/RangesTracker.coffee | 68 +++++++++++++------ .../controllers/ReviewPanelController.coffee | 10 +-- 6 files changed, 71 insertions(+), 48 deletions(-) diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index a0c07443c8..1287f207a5 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -41,6 +41,8 @@ define [ editorDoc = @ace?.getSession().getDocument() editorDoc?.off "change", @_checkConsistency @ide.$scope.$emit 'document:closed', @doc + + submitOp: (args...) -> @doc?.submitOp(args...) _checkConsistency: () -> # We've been seeing a lot of errors when I think there shouldn't be diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee index feb82c1e3f..48a7bbf3c6 100644 --- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee @@ -61,6 +61,7 @@ define [ @_doc._onMessage message catch error # Version mismatches are thrown as errors + console.log error @_handleError(error) if message?.meta?.type == "external" diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index ca8452f5c0..f8aa9099f1 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -20,8 +20,8 @@ define [ @rangesTracker = doc.ranges @connectToRangesTracker() - @$scope.$on "comment:add", (e, comment) => - @addCommentToSelection(comment) + @$scope.$on "comment:add", (e) => + @addCommentToSelection() @$scope.$on "comment:select_line", (e) => @selectLineIfNoSelection() @@ -146,21 +146,16 @@ define [ for comment in @rangesTracker.comments @_onCommentAdded(comment) - addComment: (offset, length, content) -> - @rangesTracker.addComment offset, length, { - thread: [{ - content: content - user_id: window.user_id - ts: new Date() - }] - } + addComment: (offset, content) -> + op = { c: content, p: offset } + # @rangesTracker.applyOp op # Will apply via sharejs + @$scope.sharejsDoc.submitOp op - addCommentToSelection: (content) -> + addCommentToSelection: () -> range = @editor.getSelectionRange() + content = @editor.getSelectedText() offset = @_aceRangeToShareJs(range.start) - end = @_aceRangeToShareJs(range.end) - length = end - offset - @addComment(offset, length, content) + @addComment(offset, content) selectLineIfNoSelection: () -> if @editor.selection.isEmpty() @@ -201,6 +196,7 @@ define [ @rangesTracker.unresolveCommentId(comment_id) checkMapping: () -> + # TODO: reintroduce this check session = @editor.getSession() # Make a copy of session.getMarkers() so we can modify it @@ -224,8 +220,8 @@ define [ for comment in @rangesTracker.comments if @changeIdToMarkerIdMap[comment.id]? {background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[comment.id] - start = @_shareJsOffsetToAcePosition(comment.offset) - end = @_shareJsOffsetToAcePosition(comment.offset + comment.length) + start = @_shareJsOffsetToAcePosition(comment.op.p) + end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length) expected_markers.push { marker_id: background_marker_id, start, end } expected_markers.push { marker_id: callout_marker_id, start, end: start } @@ -341,8 +337,8 @@ define [ _onCommentAdded: (comment) -> if !@changeIdToMarkerIdMap[comment.id]? # Only create new markers if they don't already exist - start = @_shareJsOffsetToAcePosition(comment.offset) - end = @_shareJsOffsetToAcePosition(comment.offset + comment.length) + start = @_shareJsOffsetToAcePosition(comment.op.p) + end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length) session = @editor.getSession() doc = session.getDocument() background_range = new Range(start.row, start.column, end.row, end.column) @@ -387,8 +383,8 @@ define [ @broadcastChange() _onCommentMoved: (comment) -> - start = @_shareJsOffsetToAcePosition(comment.offset) - end = @_shareJsOffsetToAcePosition(comment.offset + comment.length) + start = @_shareJsOffsetToAcePosition(comment.op.p) + end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length) @_updateMarker(comment.id, start, end) @editor.renderer.updateBackMarkers() @broadcastChange() diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee index 96243ceffb..274b6019c5 100644 --- a/services/web/public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee +++ b/services/web/public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee @@ -28,5 +28,5 @@ text.api = for component in op if component.i != undefined @emit 'insert', component.p, component.i - else + else if component.d != undefined @emit 'delete', component.p, component.d diff --git a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee index 6a3625fd09..550c7da585 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -48,15 +48,6 @@ load = (EventEmitter) -> # sync with Ace ranges. @id = 0 - addComment: (offset, length, metadata) -> - # TODO: Don't allow overlapping comments? - @comments.push comment = { - id: @_newId() - offset, length, metadata - } - @emit "comment:added", comment - return comment - getComment: (comment_id) -> comment = null for c in @comments @@ -97,7 +88,7 @@ load = (EventEmitter) -> return if !change? @_removeChange(change) - applyOp: (op, metadata) -> + applyOp: (op, metadata = {}) -> metadata.ts ?= new Date() # Apply an op that has been applied to the document to our changes to keep them up to date if op.i? @@ -106,14 +97,32 @@ load = (EventEmitter) -> else if op.d? @applyDeleteToChanges(op, metadata) @applyDeleteToComments(op) + else if op.c? + @addComment(op, metadata) + else + throw new Error("unknown op type") + + addComment: (op, metadata) -> + # TODO: Don't allow overlapping comments? + @comments.push comment = { + id: @_newId() + op: # Copy because we'll modify in place + c: op.c + p: op.p + t: op.t + metadata + } + @emit "comment:added", comment + return comment applyInsertToComments: (op) -> for comment in @comments - if op.p <= comment.offset - comment.offset += op.i.length + if op.p <= comment.op.p + comment.op.p += op.i.length @emit "comment:moved", comment - else if op.p < comment.offset + comment.length - comment.length += op.i.length + else if op.p < comment.op.p + comment.op.c.length + offset = op.p - comment.op.p + comment.op.c = comment.op.c[0..(offset-1)] + op.i + comment.op.c[offset...] @emit "comment:moved", comment applyDeleteToComments: (op) -> @@ -121,20 +130,35 @@ load = (EventEmitter) -> op_length = op.d.length op_end = op.p + op_length for comment in @comments - comment_end = comment.offset + comment.length - if op_end <= comment.offset + comment_start = comment.op.p + comment_end = comment.op.p + comment.op.c.length + comment_length = comment_end - comment_start + if op_end <= comment_start # delete is fully before comment - comment.offset -= op_length + comment.op.p -= op_length @emit "comment:moved", comment else if op_start >= comment_end # delete is fully after comment, nothing to do else # delete and comment overlap - delete_length_before = Math.max(0, comment.offset - op_start) - delete_length_after = Math.max(0, op_end - comment_end) - delete_length_overlapping = op_length - delete_length_before - delete_length_after - comment.offset = Math.min(comment.offset, op_start) - comment.length -= delete_length_overlapping + if op_start <= comment_start + remaining_before = "" + else + remaining_before = comment.op.c.slice(0, op_start - comment_start) + if op_end >= comment_end + remaining_after = "" + else + remaining_after = comment.op.c.slice(op_end - comment_start) + + # Check deleted content matches delete op + deleted_comment = comment.op.c.slice(remaining_before.length, comment_length - remaining_after.length) + offset = Math.max(0, comment_start - op_start) + deleted_op_content = op.d.slice(offset).slice(0, deleted_comment.length) + if deleted_comment != deleted_op_content + throw new Error("deleted content does not match comment content") + + comment.op.p = Math.min(comment_start, op_start) + comment.op.c = remaining_before + remaining_after @emit "comment:moved", comment applyInsertToChanges: (op, metadata) -> diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index df4afe306a..cc48079b8e 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -218,11 +218,11 @@ define [ entries[comment.id] ?= {} new_entry = { type: "comment" - thread: comment.metadata.thread - resolved: comment.metadata.resolved - resolved_data: comment.metadata.resolved_data - offset: comment.offset - length: comment.length + thread: comment.metadata.thread or [] + resolved: comment.metadata?.resolved + resolved_data: comment.metadata?.resolved_data + content: comment.op.c + offset: comment.op.p } for key, value of new_entry entries[comment.id][key] = value From 988005e92901f2fd6678402a8ce558e8526dea0d Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 16 Dec 2016 16:42:41 +0000 Subject: [PATCH 07/98] Send and get comments via the chat api --- .../Features/Chat/ChatApiHandler.coffee | 48 ++++ .../Features/Chat/ChatController.coffee | 33 ++- .../coffee/Features/Chat/ChatHandler.coffee | 32 --- .../Comments/CommentsController.coffee | 25 +++ services/web/app/coffee/router.coffee | 8 +- .../views/project/editor/review-panel.jade | 18 +- .../public/coffee/ide/editor/Document.coffee | 2 - .../track-changes/TrackChangesManager.coffee | 12 +- .../ide/review-panel/RangesTracker.coffee | 54 ++--- .../controllers/ReviewPanelController.coffee | 211 +++++------------- .../directives/commentEntry.coffee | 2 +- .../coffee/Chat/ChatApiHandlerTests.coffee | 92 ++++++++ .../coffee/Chat/ChatControllerTests.coffee | 88 +++----- .../coffee/Chat/ChatHandlerTests.coffee | 89 -------- .../Comments/CommentsControllerTests.coffee | 65 ++++++ 15 files changed, 380 insertions(+), 399 deletions(-) create mode 100644 services/web/app/coffee/Features/Chat/ChatApiHandler.coffee delete mode 100644 services/web/app/coffee/Features/Chat/ChatHandler.coffee create mode 100644 services/web/app/coffee/Features/Comments/CommentsController.coffee create mode 100644 services/web/test/UnitTests/coffee/Chat/ChatApiHandlerTests.coffee delete mode 100644 services/web/test/UnitTests/coffee/Chat/ChatHandlerTests.coffee create mode 100644 services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee diff --git a/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee b/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee new file mode 100644 index 0000000000..e21e94d232 --- /dev/null +++ b/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee @@ -0,0 +1,48 @@ +request = require("request") +settings = require("settings-sharelatex") +logger = require("logger-sharelatex") + +module.exports = ChatApiHandler = + _apiRequest: (opts, callback = (error, data) ->) -> + request opts, (error, response, data) -> + return callback(error) if error? + if 200 <= response.statusCode < 300 + return callback null, data + else + error = new Error("chat api returned non-success code: #{response.statusCode}") + error.statusCode = response.statusCode + logger.error {err: error, opts}, "error sending request to chat api" + return callback error + + sendGlobalMessage: (project_id, user_id, content, callback)-> + ChatApiHandler._apiRequest { + url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages" + method: "POST" + json: {user_id, content} + }, callback + + getGlobalMessages: (project_id, limit, before, callback)-> + qs = {} + qs.limit = limit if limit? + qs.before = before if before? + + ChatApiHandler._apiRequest { + url: "#{settings.apis.chat.internal_url}/project/#{project_id}/messages" + method: "GET" + qs: qs + json: true + }, callback + + sendComment: (project_id, thread_id, user_id, content, callback = (error) ->) -> + ChatApiHandler._apiRequest { + url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/messages" + method: "POST" + json: {user_id, content} + }, callback + + getThreads: (project_id, callback = (error) ->) -> + ChatApiHandler._apiRequest { + url: "#{settings.apis.chat.internal_url}/project/#{project_id}/threads" + method: "GET" + json: true + }, callback \ No newline at end of file diff --git a/services/web/app/coffee/Features/Chat/ChatController.coffee b/services/web/app/coffee/Features/Chat/ChatController.coffee index 35c280712a..d9a5d7db70 100644 --- a/services/web/app/coffee/Features/Chat/ChatController.coffee +++ b/services/web/app/coffee/Features/Chat/ChatController.coffee @@ -1,33 +1,26 @@ -ChatHandler = require("./ChatHandler") +ChatApiHandler = require("./ChatApiHandler") EditorRealTimeController = require("../Editor/EditorRealTimeController") logger = require("logger-sharelatex") AuthenticationController = require('../Authentication/AuthenticationController') module.exports = - - sendMessage: (req, res, next)-> - project_id = req.params.Project_id - messageContent = req.body.content + project_id = req.params.project_id + content = req.body.content user_id = AuthenticationController.getLoggedInUserId(req) if !user_id? err = new Error('no logged-in user') return next(err) - ChatHandler.sendMessage project_id, user_id, messageContent, (err, builtMessge)-> - if err? - logger.err err:err, project_id:project_id, user_id:user_id, messageContent:messageContent, "problem sending message to chat api" - return res.sendStatus(500) - EditorRealTimeController.emitToRoom project_id, "new-chat-message", builtMessge, (err)-> - res.send() + ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) -> + return next(err) if err? + EditorRealTimeController.emitToRoom project_id, "new-chat-message", message, (err)-> + res.send(204) - getMessages: (req, res)-> - project_id = req.params.Project_id + getMessages: (req, res, next)-> + project_id = req.params.project_id query = req.query logger.log project_id:project_id, query:query, "getting messages" - ChatHandler.getMessages project_id, query, (err, messages)-> - if err? - logger.err err:err, query:query, "problem getting messages from chat api" - return res.sendStatus 500 - logger.log length:messages?.length, "sending messages to client" - res.set 'Content-Type', 'application/json' - res.send messages + ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) -> + return next(err) if err? + logger.log length: messages?.length, "sending messages to client" + res.json messages diff --git a/services/web/app/coffee/Features/Chat/ChatHandler.coffee b/services/web/app/coffee/Features/Chat/ChatHandler.coffee deleted file mode 100644 index b77652bc39..0000000000 --- a/services/web/app/coffee/Features/Chat/ChatHandler.coffee +++ /dev/null @@ -1,32 +0,0 @@ -request = require("request") -settings = require("settings-sharelatex") -logger = require("logger-sharelatex") - -module.exports = - - sendMessage: (project_id, user_id, messageContent, callback)-> - opts = - method:"post" - json: - content:messageContent - user_id:user_id - uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages" - request opts, (err, response, body)-> - if err? - logger.err err:err, "problem sending new message to chat" - callback(err, body) - - - - getMessages: (project_id, query, callback)-> - qs = {} - qs.limit = query.limit if query?.limit? - qs.before = query.before if query?.before? - - opts = - uri:"#{settings.apis.chat.internal_url}/room/#{project_id}/messages" - method:"get" - qs: qs - - request opts, (err, response, body)-> - callback(err, body) \ No newline at end of file diff --git a/services/web/app/coffee/Features/Comments/CommentsController.coffee b/services/web/app/coffee/Features/Comments/CommentsController.coffee new file mode 100644 index 0000000000..0e9658f1d3 --- /dev/null +++ b/services/web/app/coffee/Features/Comments/CommentsController.coffee @@ -0,0 +1,25 @@ +ChatApiHandler = require("../Chat/ChatApiHandler") +EditorRealTimeController = require("../Editor/EditorRealTimeController") +logger = require("logger-sharelatex") +AuthenticationController = require('../Authentication/AuthenticationController') + +module.exports = CommentsController = + sendComment: (req, res, next) -> + {project_id, thread_id} = req.params + content = req.body.content + user_id = AuthenticationController.getLoggedInUserId(req) + if !user_id? + err = new Error('no logged-in user') + return next(err) + logger.log {project_id, thread_id, user_id, content}, "sending comment" + ChatApiHandler.sendComment project_id, thread_id, user_id, content, (err, comment) -> + return next(err) if err? + EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err)-> + res.send 204 + + getThreads: (req, res, next) -> + {project_id} = req.params + logger.log {project_id}, "getting comment threads for project" + ChatApiHandler.getThreads project_id, (err, threads) -> + return next(err) if err? + res.json threads \ No newline at end of file diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 36e26782ba..0ad7f74c4f 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -41,6 +41,7 @@ BetaProgramController = require('./Features/BetaProgram/BetaProgramController') AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') AnnouncementsController = require("./Features/Announcements/AnnouncementsController") RangesController = require("./Features/Ranges/RangesController") +CommentsController = require "./Features/Comments/CommentsController" logger = require("logger-sharelatex") _ = require("underscore") @@ -226,8 +227,11 @@ module.exports = class Router webRouter.post "/spelling/check", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi webRouter.post "/spelling/learn", AuthenticationController.requireLogin(), SpellingController.proxyRequestToSpellingApi - webRouter.get "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages - webRouter.post "/project/:Project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage + webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages + webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage + + webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.sendComment + webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index f6e43f93f2..6ffc1bdbb9 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -28,7 +28,7 @@ div(ng-if="entry.type === 'comment'") comment-entry( entry="entry" - users="users" + threads="reviewPanel.commentThreads" on-resolve="resolveComment(entry, entry_id)" on-unresolve="unresolveComment(entry_id)" on-show-thread="showThread(entry)" @@ -150,19 +150,19 @@ script(type='text/ng-template', id='commentEntryTemplate') ) .rp-comment( ng-if="!entry.resolved || entry.showWhenResolved" - ng-repeat="comment in entry.thread" - ng-class="users[comment.user_id].isSelf ? 'rp-comment-self' : '';" + ng-repeat="comment in threads[entry.thread_id]" + ng-class="comment.user.isSelf ? 'rp-comment-self' : '';" ) .rp-avatar( - ng-if="!users[comment.user_id].isSelf;" - style="background-color: hsl({{ users[comment.user_id].hue }}, 70%, 50%);" - ) {{ users[comment.user_id].avatar_text | limitTo : 1 }} - .rp-comment-body(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 90%);") + ng-if="!comment.user.isSelf;" + style="background-color: hsl({{ comment.user.hue }}, 70%, 50%);" + ) {{ comment.user.avatar_text | limitTo : 1 }} + .rp-comment-body(style="color: hsl({{ comment.user.hue }}, 70%, 90%);") p.rp-comment-content {{ comment.content }} p.rp-comment-metadata - | {{ comment.ts | date : 'MMM d, y h:mm a' }} + | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} |  •  - span(style="color: hsl({{ users[comment.user_id].hue }}, 70%, 40%);") {{ users[comment.user_id].name }} + span(style="color: hsl({{ comment.user.hue }}, 70%, 40%);") {{ comment.user.name }} .rp-comment-reply(ng-if="!entry.resolved || entry.showWhenResolved") textarea.rp-comment-input( ng-model="entry.replyContent" diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index 1287f207a5..229e281dd8 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -339,8 +339,6 @@ define [ track_changes_as = msg.meta.user_id else if !remote_op and @track_changes_as? track_changes_as = @track_changes_as - console.log "CHANGED", oldSnapshot, ops, track_changes_as @ranges.track_changes = track_changes_as? for op in ops - console.log "APPLYING OP", op, @ranges.track_changes @ranges.applyOp op, { user_id: track_changes_as } diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index f8aa9099f1..cd2f79f798 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -20,8 +20,8 @@ define [ @rangesTracker = doc.ranges @connectToRangesTracker() - @$scope.$on "comment:add", (e) => - @addCommentToSelection() + @$scope.$on "comment:add", (e, thread_id) => + @addCommentToSelection(thread_id) @$scope.$on "comment:select_line", (e) => @selectLineIfNoSelection() @@ -146,16 +146,16 @@ define [ for comment in @rangesTracker.comments @_onCommentAdded(comment) - addComment: (offset, content) -> - op = { c: content, p: offset } + addComment: (offset, content, thread_id) -> + op = { c: content, p: offset, t: thread_id } # @rangesTracker.applyOp op # Will apply via sharejs @$scope.sharejsDoc.submitOp op - addCommentToSelection: () -> + addCommentToSelection: (thread_id) -> range = @editor.getSelectionRange() content = @editor.getSelectedText() offset = @_aceRangeToShareJs(range.start) - @addComment(offset, content) + @addComment(offset, content, thread_id) selectLineIfNoSelection: () -> if @editor.selection.isEmpty() diff --git a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee index 550c7da585..8e9607d00a 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -35,18 +35,25 @@ load = (EventEmitter) -> # * Inserts by another user will not combine with inserts by the first user. If they are in the # middle of a previous insert by the first user, the original insert will be split into two. constructor: (@changes = [], @comments = []) -> - # Change objects have the following structure: - # { - # id: ... # Uniquely generated by us - # op: { # ShareJs style op tracking the offset (p) and content inserted (i) or deleted (d) - # i: "..." - # p: 42 - # } - # } - # - # Ids are used to uniquely identify a change, e.g. for updating it in the database, or keeping in - # sync with Ace ranges. - @id = 0 + + @_increment: 0 + @newId: () -> + # Generate a Mongo ObjectId + # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js + @_pid ?= Math.floor(Math.random() * (32767)) + @_machine ?= Math.floor(Math.random() * (16777216)) + timestamp = Math.floor(new Date().valueOf() / 1000) + @_increment++ + + timestamp = timestamp.toString(16) + machine = @_machine.toString(16) + pid = @_pid.toString(16) + increment = @_increment.toString(16) + + return '00000000'.substr(0, 8 - timestamp.length) + timestamp + + '000000'.substr(0, 6 - machine.length) + machine + + '0000'.substr(0, 4 - pid.length) + pid + + '000000'.substr(0, 6 - increment.length) + increment; getComment: (comment_id) -> comment = null @@ -105,7 +112,7 @@ load = (EventEmitter) -> addComment: (op, metadata) -> # TODO: Don't allow overlapping comments? @comments.push comment = { - id: @_newId() + id: RangesTracker.newId() op: # Copy because we'll modify in place c: op.c p: op.p @@ -394,28 +401,9 @@ load = (EventEmitter) -> if moved_changes.length > 0 @emit "changes:moved", moved_changes - _newId: () -> - # Generate a Mongo ObjectId - # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js - @_pid ?= Math.floor(Math.random() * (32767)) - @_machine ?= Math.floor(Math.random() * (16777216)) - timestamp = Math.floor(new Date().valueOf() / 1000) - @_increment ?= 0 - @_increment++ - - timestamp = timestamp.toString(16) - machine = @_machine.toString(16) - pid = @_pid.toString(16) - increment = @_increment.toString(16) - - return '00000000'.substr(0, 8 - timestamp.length) + timestamp + - '000000'.substr(0, 6 - machine.length) + machine + - '0000'.substr(0, 4 - pid.length) + pid + - '000000'.substr(0, 6 - increment.length) + increment; - _addOp: (op, metadata) -> change = { - id: @_newId() + id: RangesTracker.newId() op: op metadata: metadata } diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index cc48079b8e..cff77f5e1e 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -18,12 +18,27 @@ define [ openSubView: $scope.SubViews.CUR_FILE overview: loading: false + commentThreads: {} $scope.commentState = adding: false content: "" $scope.reviewPanelEventsBridge = new EventEmitter() + + $http.get "/project/#{$scope.project_id}/threads" + .success (threads) -> + for thread_id, comments of threads + for comment in comments + formatComment(comment) + $scope.reviewPanel.commentThreads = threads + + ide.socket.on "new-comment", (thread_id, comment) -> + $scope.reviewPanel.commentThreads[thread_id] ?= [] + $scope.reviewPanel.commentThreads[thread_id].push(formatComment(comment)) + $scope.$apply() + $timeout () -> + $scope.$broadcast "review-panel:layout" rangesTrackers = {} @@ -35,95 +50,6 @@ define [ rangesTrackers[doc_id] ?= new RangesTracker() return rangesTrackers[doc_id] - # TODO Just for prototyping purposes; remove afterwards. - mockedUserId = 'mock_user_id_1' - mockedUserId2 = 'mock_user_id_2' - - if window.location.search.match /mocktc=true/ - mock_changes = { - "main.tex": - changes: [{ - op: { i: "Habitat loss and conflicts with humans are the greatest causes of concern.", p: 925 - 38 } - metadata: { user_id: mockedUserId, ts: new Date(Date.now() - 30 * 60 * 1000) } - }, { - op: { d: "The lion is now a vulnerable species. ", p: 778 } - metadata: { user_id: mockedUserId, ts: new Date(Date.now() - 31 * 60 * 1000) } - }] - comments: [{ - offset: 1375 - 38 - length: 79 - metadata: - thread: [{ - content: "Do we have a source for this?" - user_id: mockedUserId - ts: new Date(Date.now() - 45 * 60 * 1000) - }] - }] - "chapter_1.tex": - changes: [{ - "op":{"p":740,"d":", to take down large animals"}, - "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 15 * 60 * 1000)} - }, { - "op":{"i":", to keep hold of the prey","p":920}, - "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 130 * 60 * 1000)} - }, { - "op":{"i":" being","p":1057}, - "metadata":{"user_id":mockedUserId2, ts: new Date(Date.now() - 72 * 60 * 1000)} - }] - comments:[{ - "offset":111,"length":5, - "metadata":{ - "thread": [ - {"content":"Have we used 'pride' too much here?","user_id":mockedUserId, ts: new Date(Date.now() - 12 * 60 * 1000)}, - {"content":"No, I think this is OK","user_id":mockedUserId2, ts: new Date(Date.now() - 9 * 60 * 1000)} - ] - } - },{ - "offset":452,"length":21, - "metadata":{ - "thread":[ - {"content":"TODO: Don't use as many parentheses!","user_id":mockedUserId2, ts: new Date(Date.now() - 99 * 60 * 1000)} - ] - } - }] - "chapter_2.tex": - changes: [{ - "op":{"p":458,"d":"other"}, - "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 133 * 60 * 1000)} - },{ - "op":{"i":"usually 2-3, ","p":928}, - "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 27 * 60 * 1000)} - },{ - "op":{"i":"If the parents are a male lion and a female tiger, it is called a liger. A tigon comes from a male tiger and a female lion.","p":1126}, - "metadata":{"user_id":mockedUserId, ts: new Date(Date.now() - 152 * 60 * 1000)} - }] - comments: [{ - "offset":299,"length":10, - "metadata":{ - "thread":[{ - "content":"Should we use a different word here if 'den' needs clarifying?","user_id":mockedUserId,"ts": new Date(Date.now() - 430 * 60 * 1000) - }] - } - },{ - "offset":843,"length":66, - "metadata":{ - "thread":[{ - "content":"This sentence is a little ambiguous","user_id":mockedUserId,"ts": new Date(Date.now() - 430 * 60 * 1000) - }] - } - }] - } - ide.$scope.$on "file-tree:initialized", () -> - ide.fileTreeManager.forEachEntity (entity) -> - if mock_changes[entity.name]? - rangesTracker = getChangeTracker(entity.id) - for change in mock_changes[entity.name].changes - rangesTracker._addOp change.op, change.metadata - for comment in mock_changes[entity.name].comments - rangesTracker.addComment comment.offset, comment.length, comment.metadata - for doc_id, rangesTracker of rangesTrackers - updateEntries(doc_id) - scrollbar = {} $scope.reviewPanelEventsBridge.on "aceScrollbarVisibilityChanged", (isVisible, scrollbarWidth) -> scrollbar = {isVisible, scrollbarWidth} @@ -156,7 +82,6 @@ define [ $scope.$watch "editor.sharejs_doc", (doc) -> return if !doc? - console.log "DOC changed", doc # The open doc range tracker is kept up to date in real-time so # replace any outdated info with this rangesTrackers[doc.doc_id] = doc.ranges @@ -218,7 +143,7 @@ define [ entries[comment.id] ?= {} new_entry = { type: "comment" - thread: comment.metadata.thread or [] + thread_id: comment.op.t resolved: comment.metadata?.resolved resolved_data: comment.metadata?.resolved_data content: comment.op.c @@ -250,7 +175,7 @@ define [ for id, entry of entries if entry.type == "comment" and not entry.resolved - entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.length) + entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.content.length) else if entry.type == "insert" entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.content.length) else if entry.type == "delete" @@ -268,21 +193,20 @@ define [ $scope.$broadcast "change:reject", entry_id $scope.startNewComment = () -> - # $scope.commentState.adding = true $scope.$broadcast "comment:select_line" $timeout () -> $scope.$broadcast "review-panel:layout" $scope.submitNewComment = (content) -> - # $scope.commentState.adding = false - $scope.$broadcast "comment:add", content - # $scope.commentState.content = "" + thread_id = RangesTracker.newId() + $scope.$broadcast "comment:add", thread_id + $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) + .error (error) -> + ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment") $timeout () -> $scope.$broadcast "review-panel:layout" $scope.cancelNewComment = (entry) -> - # $scope.commentState.adding = false - # $scope.commentState.content = "" $timeout () -> $scope.$broadcast "review-panel:layout" @@ -291,40 +215,19 @@ define [ $timeout () -> $scope.$broadcast "review-panel:layout" - # $scope.handleCommentReplyKeyPress = (ev, entry) -> - # if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey - # ev.preventDefault() - # ev.target.blur() - # $scope.submitReply(entry) - $scope.submitReply = (entry, entry_id) -> $scope.unresolveComment(entry_id) - entry.thread.push { - content: entry.replyContent - ts: new Date() - user_id: window.user_id - } - entry.replyContent = "" - entry.replying = false - $timeout () -> - $scope.$broadcast "review-panel:layout" - # TODO Just for prototyping purposes; remove afterwards - window.setTimeout((() -> - $scope.$applyAsync(() -> submitMockedReply(entry)) - ), 1000 * 2) + thread_id = entry.thread_id + content = entry.replyContent + $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) + .error (error) -> + ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment") - # TODO Just for prototyping purposes; remove afterwards. - submitMockedReply = (entry) -> - entry.thread.push { - content: 'Sounds good!' - ts: new Date() - user_id: mockedUserId - } entry.replyContent = "" entry.replying = false $timeout () -> $scope.$broadcast "review-panel:layout" - + $scope.cancelReply = (entry) -> entry.replying = false entry.replyContent = "" @@ -361,37 +264,39 @@ define [ # when we get an id we don't know. This'll do for client side testing refreshUsers = () -> $scope.users = {} - # TODO Just for prototyping purposes; remove afterwards. - $scope.users[mockedUserId] = { - email: "paulo@sharelatex.com" - name: "Paulo Reis" - isSelf: false - hue: 70 - avatar_text: "PR" - } - $scope.users[mockedUserId2] = { - email: "james@sharelatex.com" - name: "James Allen" - isSelf: false - hue: 320 - avatar_text: "JA" - } - for member in $scope.project.members.concat($scope.project.owner) - if member._id == window.user_id - name = "You" - isSelf = true - else - name = "#{member.first_name} #{member.last_name}" - isSelf = false + $scope.users[member._id] = formatUser(member) - $scope.users[member._id] = { - email: member.email - name: name - isSelf: isSelf - hue: ColorManager.getHueForUserId(member._id) - avatar_text: [member.first_name, member.last_name].filter((n) -> n?).map((n) -> n[0]).join "" + formatComment = (comment) -> + comment.user = formatUser(user) + comment.timestamp = new Date(comment.timestamp) + return comment + + formatUser = (user) -> + if !user? + return { + email: null + name: "Anonymous" + isSelf: false + hue: ColorManager.ANONYMOUS_HUE + avatar_text: "A" } + + id = user._id or user.id + if id == window.user_id + name = "You" + isSelf = true + else + name = "#{user.first_name} #{user.last_name}" + isSelf = false + return { + id: id + email: user.email + name: name + isSelf: isSelf + hue: ColorManager.getHueForUserId(id) + avatar_text: [user.first_name, user.last_name].filter((n) -> n?).map((n) -> n[0]).join "" + } $scope.$watch "project.members", (members) -> return if !members? diff --git a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee index 6938062e2b..76798b1935 100644 --- a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee @@ -6,7 +6,7 @@ define [ templateUrl: "commentEntryTemplate" scope: entry: "=" - users: "=" + threads: "=" onResolve: "&" onReply: "&" onIndicatorClick: "&" diff --git a/services/web/test/UnitTests/coffee/Chat/ChatApiHandlerTests.coffee b/services/web/test/UnitTests/coffee/Chat/ChatApiHandlerTests.coffee new file mode 100644 index 0000000000..ea569b8a53 --- /dev/null +++ b/services/web/test/UnitTests/coffee/Chat/ChatApiHandlerTests.coffee @@ -0,0 +1,92 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +sinon = require('sinon') +modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatApiHandler" +expect = require("chai").expect + +describe "ChatApiHandler", -> + beforeEach -> + @settings = + apis: + chat: + internal_url:"chat.sharelatex.env" + @request = sinon.stub() + @ChatApiHandler = SandboxedModule.require modulePath, requires: + "settings-sharelatex": @settings + "logger-sharelatex": { log: sinon.stub(), error: sinon.stub() } + "request": @request + @project_id = "3213213kl12j" + @user_id = "2k3jlkjs9" + @content = "my message here" + @callback = sinon.stub() + + describe "sendGlobalMessage", -> + describe "successfully", -> + beforeEach -> + @message = { "mock": "message" } + @request.callsArgWith(1, null, {statusCode: 200}, @message) + @ChatApiHandler.sendGlobalMessage @project_id, @user_id, @content, @callback + + it "should post the data to the chat api", -> + @request.calledWith({ + url: "#{@settings.apis.chat.internal_url}/project/#{@project_id}/messages" + method: "POST" + json: + content: @content + user_id: @user_id + }).should.equal true + + it "should return the message from the post", -> + @callback.calledWith(null, @message).should.equal true + + describe "with a non-success status code", -> + beforeEach -> + @request.callsArgWith(1, null, {statusCode: 500}) + @ChatApiHandler.sendGlobalMessage @project_id, @user_id, @content, @callback + + it "should return an error", -> + error = new Error() + error.statusCode = 500 + @callback.calledWith(error).should.equal true + + describe "getGlobalMessages", -> + beforeEach -> + @messages = [{ "mock": "message" }] + @limit = 30 + @before = "1234" + + describe "successfully", -> + beforeEach -> + @request.callsArgWith(1, null, {statusCode: 200}, @messages) + @ChatApiHandler.getGlobalMessages @project_id, @limit, @before, @callback + + it "should make get request for room to chat api", -> + @request.calledWith({ + method: "GET" + url: "#{@settings.apis.chat.internal_url}/project/#{@project_id}/messages" + qs: + limit: @limit + before: @before + json: true + }).should.equal true + + it "should return the messages from the request", -> + @callback.calledWith(null, @messages).should.equal true + + describe "with failure error code", -> + beforeEach -> + @request.callsArgWith(1, null, {statusCode: 500}, null) + @ChatApiHandler.getGlobalMessages @project_id, @limit, @before, @callback + + it "should return an error", -> + error = new Error() + error.statusCode = 500 + @callback.calledWith(error).should.equal true + + + + + + diff --git a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee index a491e4b499..3db5dadd30 100644 --- a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee @@ -7,75 +7,59 @@ modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatControll expect = require("chai").expect describe "ChatController", -> - beforeEach -> - - @user_id = 'ier_' + @user_id = 'mock-user-id' @settings = {} - @ChatHandler = - sendMessage:sinon.stub() - getMessages:sinon.stub() - + @ChatApiHandler = {} @EditorRealTimeController = emitToRoom:sinon.stub().callsArgWith(3) - @AuthenticationController = getLoggedInUserId: sinon.stub().returns(@user_id) @ChatController = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": log:-> - "./ChatHandler":@ChatHandler - "../Editor/EditorRealTimeController":@EditorRealTimeController + "settings-sharelatex": @settings + "logger-sharelatex": log: -> + "./ChatApiHandler": @ChatApiHandler + "../Editor/EditorRealTimeController": @EditorRealTimeController '../Authentication/AuthenticationController': @AuthenticationController - @query = - before:"some time" - @req = params: - Project_id:@project_id - session: - user: - _id:@user_id - body: - content:@messageContent + project_id: @project_id @res = - set:sinon.stub() + json: sinon.stub() + send: sinon.stub() describe "sendMessage", -> - - it "should tell the chat handler about the message", (done)-> - @ChatHandler.sendMessage.callsArgWith(3) - @res.send = => - @ChatHandler.sendMessage.calledWith(@project_id, @user_id, @messageContent).should.equal true - done() + beforeEach -> + @req.body = + content: @content = "message-content" + @ChatApiHandler.sendGlobalMessage = sinon.stub().yields(null, @message = {"mock": "message"}) @ChatController.sendMessage @req, @res - it "should tell the editor real time controller about the update with the data from the chat handler", (done)-> - @chatMessage = - content:"hello world" - @ChatHandler.sendMessage.callsArgWith(3, null, @chatMessage) - @res.send = => - @EditorRealTimeController.emitToRoom.calledWith(@project_id, "new-chat-message", @chatMessage).should.equal true - done() - @ChatController.sendMessage @req, @res + it "should tell the chat handler about the message", -> + @ChatApiHandler.sendGlobalMessage + .calledWith(@project_id, @user_id, @content) + .should.equal true + + it "should tell the editor real time controller about the update with the data from the chat handler", -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "new-chat-message", @message) + .should.equal true + + it "should return a 204 status code", -> + @res.send.calledWith(204).should.equal true describe "getMessages", -> beforeEach -> - @req.query = @query - - it "should ask the chat handler about the request", (done)-> - - @ChatHandler.getMessages.callsArgWith(2) - @res.send = => - @ChatHandler.getMessages.calledWith(@project_id, @query).should.equal true - done() + @req.query = + limit: @limit = "30" + before: @before = "12345" + @ChatApiHandler.getGlobalMessages = sinon.stub().yields(null, @messages = ["mock", "messages"]) @ChatController.getMessages @req, @res - it "should return the messages", (done)-> - messages = [{content:"hello"}] - @ChatHandler.getMessages.callsArgWith(2, null, messages) - @res.send = (sentMessages)=> - @res.set.calledWith('Content-Type', 'application/json').should.equal true - sentMessages.should.deep.equal messages - done() - @ChatController.getMessages @req, @res + it "should ask the chat handler about the request", -> + @ChatApiHandler.getGlobalMessages + .calledWith(@project_id, @limit, @before) + .should.equal true + + it "should return the messages", -> + @res.json.calledWith(@messages).should.equal true \ No newline at end of file diff --git a/services/web/test/UnitTests/coffee/Chat/ChatHandlerTests.coffee b/services/web/test/UnitTests/coffee/Chat/ChatHandlerTests.coffee deleted file mode 100644 index 22b6a575cc..0000000000 --- a/services/web/test/UnitTests/coffee/Chat/ChatHandlerTests.coffee +++ /dev/null @@ -1,89 +0,0 @@ -should = require('chai').should() -SandboxedModule = require('sandboxed-module') -assert = require('assert') -path = require('path') -sinon = require('sinon') -modulePath = path.join __dirname, "../../../../app/js/Features/Chat/ChatHandler" -expect = require("chai").expect - -describe "ChatHandler", -> - - beforeEach -> - - @settings = - apis: - chat: - internal_url:"chat.sharelatex.env" - @request = sinon.stub() - @ChatHandler = SandboxedModule.require modulePath, requires: - "settings-sharelatex":@settings - "logger-sharelatex": log:-> - "request": @request - @project_id = "3213213kl12j" - @user_id = "2k3jlkjs9" - @messageContent = "my message here" - - describe "sending message", -> - - beforeEach -> - @messageResponse = - message:"Details" - @request.callsArgWith(1, null, null, @messageResponse) - - it "should post the data to the chat api", (done)-> - - @ChatHandler.sendMessage @project_id, @user_id, @messageContent, (err)=> - @opts = - method:"post" - json: - content:@messageContent - user_id:@user_id - uri:"#{@settings.apis.chat.internal_url}/room/#{@project_id}/messages" - @request.calledWith(@opts).should.equal true - done() - - it "should return the message from the post", (done)-> - @ChatHandler.sendMessage @project_id, @user_id, @messageContent, (err, returnedMessage)=> - returnedMessage.should.equal @messageResponse - done() - - describe "get messages", -> - - beforeEach -> - @returnedMessages = [{content:"hello world"}] - @request.callsArgWith(1, null, null, @returnedMessages) - @query = {} - - it "should make get request for room to chat api", (done)-> - - @ChatHandler.getMessages @project_id, @query, (err)=> - @opts = - method:"get" - uri:"#{@settings.apis.chat.internal_url}/room/#{@project_id}/messages" - qs:{} - @request.calledWith(@opts).should.equal true - done() - - it "should make get request for room to chat api with query string", (done)-> - @query = {limit:5, before:12345, ignore:"this"} - - @ChatHandler.getMessages @project_id, @query, (err)=> - @opts = - method:"get" - uri:"#{@settings.apis.chat.internal_url}/room/#{@project_id}/messages" - qs: - limit:5 - before:12345 - @request.calledWith(@opts).should.equal true - done() - - it "should return the messages from the request", (done)-> - @ChatHandler.getMessages @project_id, @query, (err, returnedMessages)=> - returnedMessages.should.equal @returnedMessages - done() - - - - - - diff --git a/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee new file mode 100644 index 0000000000..8acaa66886 --- /dev/null +++ b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee @@ -0,0 +1,65 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +path = require('path') +sinon = require('sinon') +modulePath = path.join __dirname, "../../../../app/js/Features/Comments/CommentsController" +expect = require("chai").expect + +describe "CommentsController", -> + beforeEach -> + @user_id = 'mock-user-id' + @settings = {} + @ChatApiHandler = {} + @EditorRealTimeController = + emitToRoom:sinon.stub() + @AuthenticationController = + getLoggedInUserId: sinon.stub().returns(@user_id) + @CommentsController = SandboxedModule.require modulePath, requires: + "settings-sharelatex": @settings + "logger-sharelatex": log: -> + "../Chat/ChatApiHandler": @ChatApiHandler + "../Editor/EditorRealTimeController": @EditorRealTimeController + '../Authentication/AuthenticationController': @AuthenticationController + @req = {} + @res = + json: sinon.stub() + send: sinon.stub() + + describe "sendComment", -> + beforeEach -> + @req.params = + project_id: @project_id + thread_id: @thread_id + @req.body = + content: @content = "message-content" + @ChatApiHandler.sendComment = sinon.stub().yields(null, @message = {"mock": "message"}) + @CommentsController.sendComment @req, @res + + it "should tell the chat handler about the message", -> + @ChatApiHandler.sendComment + .calledWith(@project_id, @thread_id, @user_id, @content) + .should.equal true + + it "should tell the editor real time controller about the update with the data from the chat handler", -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "new-comment", @thread_id, @message) + .should.equal true + + it "should return a 204 status code", -> + @res.send.calledWith(204).should.equal true + + describe "getThreads", -> + beforeEach -> + @req.params = + project_id: @project_id + @ChatApiHandler.getThreads = sinon.stub().yields(null, @threads = {"mock", "threads"}) + @CommentsController.getThreads @req, @res + + it "should ask the chat handler about the request", -> + @ChatApiHandler.getThreads + .calledWith(@project_id) + .should.equal true + + it "should return the messages", -> + @res.json.calledWith(@threads).should.equal true \ No newline at end of file From fdafcf9677477495361b855146c2c09ebc0bd4db Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 4 Jan 2017 15:24:49 +0000 Subject: [PATCH 08/98] Restyle insertions and deletions - increasing info density. --- .../views/project/editor/review-panel.jade | 20 +-- .../stylesheets/app/editor/review-panel.less | 122 +++++++----------- 2 files changed, 54 insertions(+), 88 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 6ffc1bdbb9..44fb82d310 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -116,19 +116,19 @@ script(type='text/ng-template', id='changeEntryTemplate') .rp-entry( ng-class="[ 'rp-entry-' + entry.type, (entry.focused ? 'rp-entry-focused' : '')]" ) - .rp-entry-header + .rp-entry-body .rp-entry-action-icon(ng-switch="entry.type") i.fa.fa-pencil(ng-switch-when="insert") i.rp-icon-delete(ng-switch-when="delete") - .rp-entry-metadata - p.rp-entry-metadata-line(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }} - p.rp-entry-metadata-line {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} - .rp-avatar(style="background-color: hsl({{ user.hue }}, 70%, 50%);") {{ user.avatar_text | limitTo : 1 }} - .rp-entry-body(ng-switch="entry.type") - span(ng-switch-when="insert") Added  - ins.rp-content-highlight {{ entry.content }} - span(ng-switch-when="delete") Deleted  - del.rp-content-highlight {{ entry.content }} + .rp-entry-details + .rp-entry-description(ng-switch="entry.type") + span(ng-switch-when="insert") Added  + ins.rp-content-highlight {{ entry.content }} + span(ng-switch-when="delete") Deleted  + del.rp-content-highlight {{ entry.content }} + .rp-entry-metadata + span {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •  + span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }} .rp-entry-actions a.rp-entry-button(href, ng-click="onReject();") i.fa.fa-times diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index d14e843591..72efa23a03 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -1,31 +1,31 @@ -@rp-base-font-size : 12px; -@rp-small-font-size : 10px; -@rp-icon-large-size : 22px; +@rp-base-font-size : 12px; +@rp-small-font-size : 10px; +@rp-icon-large-size : 22px; -@rp-bg-blue : #dadfed; -@rp-bg-dim-blue : #fafafa; -@rp-highlight-blue : #8a96b5; +@rp-bg-blue : #dadfed; +@rp-bg-dim-blue : #fafafa; +@rp-highlight-blue : #8a96b5; -@rp-border-grey : #d9d9d9; +@rp-border-grey : #d9d9d9; -@rp-green : #2c8e30; -@rp-dim-green : #cae3cb; -@rp-red : #c5060b; -@rp-dim-red : #f3cdce; -@rp-yellow : #f3b111; -@rp-dim-yellow : #ffe9b2; -@rp-grey : #aaaaaa; +@rp-green : #2c8e30; +@rp-dim-green : #cae3cb; +@rp-red : #c5060b; +@rp-dim-red : #f3cdce; +@rp-yellow : #f3b111; +@rp-dim-yellow : #ffe9b2; +@rp-grey : #aaaaaa; -@rp-type-blue : #6b7797; -@rp-type-darkgrey : #3f3f3f; +@rp-type-blue : #6b7797; +@rp-type-darkgrey : #3f3f3f; -@rp-entry-ribbon-width : 4px; -@rp-entry-arrow-width : 6px; -@rp-semibold-weight : 600; -@review-panel-width : 230px; -@review-off-width : 22px; +@rp-entry-ribbon-width : 4px; +@rp-entry-arrow-width : 6px; +@rp-semibold-weight : 600; +@review-panel-width : 230px; +@review-off-width : 22px; -@rp-toolbar-height: 32px; +@rp-toolbar-height : 32px; .rp-button() { background-color: @rp-highlight-blue; @@ -264,14 +264,13 @@ } } } - - .rp-entry-header { + .rp-entry-body { display: flex; align-items: center; - padding: 5px; + padding: 3px 5px; .rp-state-overview & { - padding: 0px; + padding: 0; } } .rp-entry-action-icon { @@ -280,48 +279,33 @@ line-height: 0; .rp-state-overview & { - font-size: @rp-base-font-size; - padding: 0px; - margin-right: 5px; + display: none; } } - .rp-entry-metadata { - flex-grow: 1; - padding: 0 5px; - line-height: 1.2; - - .rp-state-overview & { - display: flex; - line-height: inherit; - padding: 0; - } + .rp-entry-details { + line-height: 1.4; + margin-left: 5px; } - .rp-entry-metadata-line { - margin: 0; - .rp-state-overview &:last-of-type { - flex-grow: 1; - text-align: right; + .rp-entry-metadata { + font-size: @rp-small-font-size; + .rp-state-overview & { } } + .rp-entry-user { + font-weight: @rp-semibold-weight; + } - .rp-entry-body { - padding: 5px; + .rp-content-highlight { + color: @rp-type-darkgrey; + font-weight: @rp-semibold-weight; + text-decoration: none; - .rp-state-overview & { - padding: 0; - } - } - .rp-content-highlight { - color: @rp-type-darkgrey; - font-weight: @rp-semibold-weight; - text-decoration: none; - - .rp-entry-delete & { - text-decoration: line-through; + .rp-entry-delete & { + text-decoration: line-through; + } } - } .rp-entry-actions { display: flex; @@ -426,24 +410,6 @@ resize: vertical; } -.rp-avatar { - border-radius: 3px; - font-weight: @rp-semibold-weight; - font-size: @rp-icon-large-size; - line-height: 1.2; - text-transform: uppercase; - color: #FFF; - width: 1.3em; - height: 1.3em; - text-align: center; - flex-grow: 0; - flex-shrink: 0; - - .rp-state-overview & { - display: none; - } -} - .rp-icon-delete { display: inline-block; line-height: 1; @@ -524,10 +490,10 @@ padding: 2px 5px; border-top: solid 1px @rp-border-grey; border-bottom: solid 1px @rp-border-grey; - background-color: #FFF; + background-color: @rp-bg-dim-blue; margin-top: 10px; font-weight: @rp-semibold-weight; - border-left: solid @rp-entry-ribbon-width currentColor; + text-align: center; } .rp-nav { From 84d9b2aa49f69726b627b64ba4b285c37a90c946 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 4 Jan 2017 16:26:02 +0100 Subject: [PATCH 09/98] Save resolve and reopened state --- .../Features/Chat/ChatApiHandler.coffee | 13 ++++++++ .../Comments/CommentsController.coffee | 21 +++++++++++-- services/web/app/coffee/router.coffee | 2 ++ .../views/project/editor/review-panel.jade | 30 +++++++++---------- .../controllers/ReviewPanelController.coffee | 22 ++++++++++---- 5 files changed, 66 insertions(+), 22 deletions(-) diff --git a/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee b/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee index e21e94d232..aa4b75ce11 100644 --- a/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee +++ b/services/web/app/coffee/Features/Chat/ChatApiHandler.coffee @@ -45,4 +45,17 @@ module.exports = ChatApiHandler = url: "#{settings.apis.chat.internal_url}/project/#{project_id}/threads" method: "GET" json: true + }, callback + + resolveThread: (project_id, thread_id, user_id, callback = (error) ->) -> + ChatApiHandler._apiRequest { + url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/resolve" + method: "POST" + json: {user_id} + }, callback + + reopenThread: (project_id, thread_id, callback = (error) ->) -> + ChatApiHandler._apiRequest { + url: "#{settings.apis.chat.internal_url}/project/#{project_id}/thread/#{thread_id}/reopen" + method: "POST" }, callback \ No newline at end of file diff --git a/services/web/app/coffee/Features/Comments/CommentsController.coffee b/services/web/app/coffee/Features/Comments/CommentsController.coffee index 0e9658f1d3..07974d6872 100644 --- a/services/web/app/coffee/Features/Comments/CommentsController.coffee +++ b/services/web/app/coffee/Features/Comments/CommentsController.coffee @@ -14,7 +14,7 @@ module.exports = CommentsController = logger.log {project_id, thread_id, user_id, content}, "sending comment" ChatApiHandler.sendComment project_id, thread_id, user_id, content, (err, comment) -> return next(err) if err? - EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err)-> + EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err) -> res.send 204 getThreads: (req, res, next) -> @@ -22,4 +22,21 @@ module.exports = CommentsController = logger.log {project_id}, "getting comment threads for project" ChatApiHandler.getThreads project_id, (err, threads) -> return next(err) if err? - res.json threads \ No newline at end of file + res.json threads + + resolveThread: (req, res, next) -> + {project_id, thread_id} = req.params + user_id = AuthenticationController.getLoggedInUserId(req) + logger.log {project_id, thread_id, user_id}, "resolving comment thread" + ChatApiHandler.resolveThread project_id, thread_id, user_id, (err, threads) -> + return next(err) if err? + EditorRealTimeController.emitToRoom project_id, "resolve-thread", thread_id, user_id, (err)-> + res.send 204 + + reopenThread: (req, res, next) -> + {project_id, thread_id} = req.params + logger.log {project_id, thread_id}, "reopening comment thread" + ChatApiHandler.reopenThread project_id, thread_id, (err, threads) -> + return next(err) if err? + EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)-> + res.send 204 \ No newline at end of file diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 0ad7f74c4f..13c8568f17 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -232,6 +232,8 @@ module.exports = class Router webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.sendComment webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads + webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread + webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread webRouter.post "/project/:Project_id/references/index", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.index webRouter.post "/project/:Project_id/references/indexAll", AuthorizationMiddlewear.ensureUserCanReadProject, ReferencesController.indexAll diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 6ffc1bdbb9..16f003633a 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -30,7 +30,7 @@ entry="entry" threads="reviewPanel.commentThreads" on-resolve="resolveComment(entry, entry_id)" - on-unresolve="unresolveComment(entry_id)" + on-unresolve="unresolveComment(entry, entry_id)" on-show-thread="showThread(entry)" on-hide-thread="hideThread(entry)" on-delete="deleteComment(entry_id)" @@ -77,7 +77,7 @@ entry="entry" users="users" on-resolve="resolveComment(entry, entry.id)" - on-unresolve="unresolveComment(entry.id)" + on-unresolve="unresolveComment(entry, entry.id)" on-delete="deleteComment(entry.id)" on-reply="submitReply(entry, entry_id);" on-indicator-click="toggleReviewPanel();" @@ -139,18 +139,18 @@ script(type='text/ng-template', id='changeEntryTemplate') script(type='text/ng-template', id='commentEntryTemplate') div - .rp-entry-callout.rp-entry-callout-comment(ng-if="!entry.resolved") + .rp-entry-callout.rp-entry-callout-comment(ng-if="!threads[entry.thread_id].resolved") .rp-entry-indicator( ng-class="{ 'rp-entry-indicator-focused': entry.focused }" ng-click="onIndicatorClick();" ) i.fa.fa-comment .rp-entry.rp-entry-comment( - ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolved': entry.resolved}" + ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolved': threads[entry.thread_id].resolved}" ) .rp-comment( - ng-if="!entry.resolved || entry.showWhenResolved" - ng-repeat="comment in threads[entry.thread_id]" + ng-if="!threads[entry.thread_id].resolved || entry.showWhenResolved" + ng-repeat="comment in threads[entry.thread_id].messages" ng-class="comment.user.isSelf ? 'rp-comment-self' : '';" ) .rp-avatar( @@ -163,29 +163,29 @@ script(type='text/ng-template', id='commentEntryTemplate') | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} |  •  span(style="color: hsl({{ comment.user.hue }}, 70%, 40%);") {{ comment.user.name }} - .rp-comment-reply(ng-if="!entry.resolved || entry.showWhenResolved") + .rp-comment-reply(ng-if="!threads[entry.thread_id].resolved || entry.showWhenResolved") textarea.rp-comment-input( ng-model="entry.replyContent" ng-keypress="handleCommentReplyKeyPress($event);" stop-propagation="click" placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}" ) - .rp-comment-resolved-description(ng-if="entry.resolved && !entry.showWhenResolved") + .rp-comment-resolved-description(ng-if="threads[entry.thread_id].resolved && !entry.showWhenResolved") div | Comment resolved by - span(style="color: hsl({{ users[entry.resolved_data.user_id].hue }}, 70%, 40%);") {{ users[entry.resolved_data.user_id].name }} - div {{ entry.resolved_data.ts | date : 'MMM d, y h:mm a' }} + span(style="color: hsl({{ threads[entry.thread_id].resolved_by_user.hue }}, 70%, 40%);") {{ threads[entry.thread_id].resolved_by_user.name }} + div {{ threads[entry.thread_id].resolved_at | date : 'MMM d, y h:mm a' }} .rp-entry-actions - a.rp-entry-button(href, ng-click="onResolve();", ng-if="!entry.resolved") + a.rp-entry-button(href, ng-click="onResolve();", ng-if="!threads[entry.thread_id].resolved") i.fa.fa-check |  Mark as resolved - a.rp-entry-button(href, ng-click="onShowThread();", ng-if="entry.resolved && !entry.showWhenResolved") + a.rp-entry-button(href, ng-click="onShowThread();", ng-if="threads[entry.thread_id].resolved && !entry.showWhenResolved") |  Show - a.rp-entry-button(href, ng-click="onHideThread();", ng-if="entry.resolved && entry.showWhenResolved") + a.rp-entry-button(href, ng-click="onHideThread();", ng-if="threads[entry.thread_id].resolved && entry.showWhenResolved") |  Hide - a.rp-entry-button(href, ng-click="onUnresolve();", ng-if="entry.resolved") + a.rp-entry-button(href, ng-click="onUnresolve();", ng-if="threads[entry.thread_id].resolved") |  Re-open - a.rp-entry-button(href, ng-click="onDelete();", ng-if="entry.resolved") + a.rp-entry-button(href, ng-click="onDelete();", ng-if="threads[entry.thread_id].resolved") |  Delete diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index cff77f5e1e..55ea4a46e1 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -28,14 +28,16 @@ define [ $http.get "/project/#{$scope.project_id}/threads" .success (threads) -> - for thread_id, comments of threads - for comment in comments + for thread_id, thread of threads + for comment in thread.messages formatComment(comment) + if thread.resolved_by_user? + formatUser(thread.resolved_by_user) $scope.reviewPanel.commentThreads = threads ide.socket.on "new-comment", (thread_id, comment) -> - $scope.reviewPanel.commentThreads[thread_id] ?= [] - $scope.reviewPanel.commentThreads[thread_id].push(formatComment(comment)) + $scope.reviewPanel.commentThreads[thread_id] ?= { messages: [] } + $scope.reviewPanel.commentThreads[thread_id].messages.push(formatComment(comment)) $scope.$apply() $timeout () -> $scope.$broadcast "review-panel:layout" @@ -236,9 +238,19 @@ define [ $scope.resolveComment = (entry, entry_id) -> entry.showWhenResolved = false entry.focused = false + thread = $scope.reviewPanel.commentThreads[entry.thread_id] + thread.resolved = true + thread.resolved_by_user = $scope.users[window.user_id] + thread.resolved_at = new Date() + $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken} $scope.$broadcast "comment:resolve", entry_id, window.user_id - $scope.unresolveComment = (entry_id) -> + $scope.unresolveComment = (entry, entry_id) -> + thread = $scope.reviewPanel.commentThreads[entry.thread_id] + delete thread.resolved + delete thread.resolved_by_user + delete thread.resolved_at + $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/reopen", {_csrf: window.csrfToken} $scope.$broadcast "comment:unresolve", entry_id $scope.deleteComment = (entry_id) -> From 1599c4167087ee3e0803c9b986582df0ba42c7da Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 4 Jan 2017 15:34:24 +0000 Subject: [PATCH 10/98] Sizing adjustments. --- .../web/public/stylesheets/app/editor/review-panel.less | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 72efa23a03..c2dd9f314c 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -1,6 +1,6 @@ @rp-base-font-size : 12px; @rp-small-font-size : 10px; -@rp-icon-large-size : 22px; +@rp-icon-large-size : 18px; @rp-bg-blue : #dadfed; @rp-bg-dim-blue : #fafafa; @@ -31,6 +31,7 @@ background-color: @rp-highlight-blue; color: #FFF; text-align: center; + line-height: 1.3; &:hover, &:focus { background-color: darken(@rp-highlight-blue, 5%); @@ -267,7 +268,7 @@ .rp-entry-body { display: flex; align-items: center; - padding: 3px 5px; + padding: 4px 5px; .rp-state-overview & { padding: 0; @@ -275,7 +276,7 @@ } .rp-entry-action-icon { font-size: @rp-icon-large-size; - padding: 0 5px; + padding: 0 3px; line-height: 0; .rp-state-overview & { From 898d20a6fcc155f7bfe2d5cd26ec1806fece435b Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 5 Jan 2017 10:34:28 +0100 Subject: [PATCH 11/98] Fix signature of call to unresolveComment --- .../ide/review-panel/controllers/ReviewPanelController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 55ea4a46e1..aa993183d7 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -218,7 +218,7 @@ define [ $scope.$broadcast "review-panel:layout" $scope.submitReply = (entry, entry_id) -> - $scope.unresolveComment(entry_id) + $scope.unresolveComment(entry, entry_id) thread_id = entry.thread_id content = entry.replyContent $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) From 5155ebaeec2082a0deb2472bf665c194b3d6d5e6 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 5 Jan 2017 10:55:16 +0100 Subject: [PATCH 12/98] Don't show resolved comments when loading editor --- .../track-changes/TrackChangesManager.coffee | 38 +++++++++---------- .../ide/review-panel/RangesTracker.coffee | 13 ------- .../controllers/ReviewPanelController.coffee | 7 ++-- 3 files changed, 22 insertions(+), 36 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index cd2f79f798..030aa8c252 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -35,11 +35,11 @@ define [ @$scope.$on "comment:remove", (e, comment_id) => @removeCommentId(comment_id) - @$scope.$on "comment:resolve", (e, comment_id, user_id) => - @resolveCommentId(comment_id, user_id) + @$scope.$on "comment:resolve_thread", (e, thread_id) => + @resolveCommentByThreadId(thread_id) - @$scope.$on "comment:unresolve", (e, comment_id) => - @unresolveCommentId(comment_id) + @$scope.$on "comment:unresolve_thread", (e, thread_id) => + @unresolveCommentByThreadId(thread_id) @$scope.$on "review-panel:recalculate-screen-positions", () => @recalculateReviewEntriesScreenPositions() @@ -90,9 +90,7 @@ define [ @rangesTracker.off "comment:added" @rangesTracker.off "comment:moved" @rangesTracker.off "comment:removed" - @rangesTracker.off "comment:resolved" - @rangesTracker.off "comment:unresolved" - + setTrackChanges: (value) -> if value @$scope.sharejsDoc?.track_changes_as = window.user.id @@ -129,12 +127,6 @@ define [ @rangesTracker.on "comment:removed", (comment) => sl_console.log "[comment:removed]", comment setTimeout () => @_onCommentRemoved(comment) - @rangesTracker.on "comment:resolved", (comment) => - sl_console.log "[comment:resolved]", comment - setTimeout () => @_onCommentRemoved(comment) - @rangesTracker.on "comment:unresolved", (comment) => - sl_console.log "[comment:unresolved]", comment - setTimeout () => @_onCommentAdded(comment) redrawAnnotations: () -> for change in @rangesTracker.changes @@ -187,13 +179,18 @@ define [ removeCommentId: (comment_id) -> @rangesTracker.removeCommentId(comment_id) - resolveCommentId: (comment_id, user_id) -> - @rangesTracker.resolveCommentId(comment_id, { - user_id, ts: new Date() - }) + RESOLVED_THREADS: {} + resolveCommentByThreadId: (thread_id) -> + @RESOLVED_THREADS[thread_id] = true + for comment in @rangesTracker?.comments or [] + if comment.op.t == thread_id + @_onCommentRemoved(comment) - unresolveCommentId: (comment_id) -> - @rangesTracker.unresolveCommentId(comment_id) + unresolveCommentByThreadId: (thread_id) -> + @RESOLVED_THREADS[thread_id] = false + for comment in @rangesTracker?.comments or [] + if comment.op.t == thread_id + @_onCommentAdded(comment) checkMapping: () -> # TODO: reintroduce this check @@ -335,6 +332,9 @@ define [ @broadcastChange() _onCommentAdded: (comment) -> + if @RESOLVED_THREADS[comment.op.t] + # Comment is resolved so shouldn't be displayed. + return if !@changeIdToMarkerIdMap[comment.id]? # Only create new markers if they don't already exist start = @_shareJsOffsetToAcePosition(comment.op.p) diff --git a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee index 8e9607d00a..6ff301e803 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -63,19 +63,6 @@ load = (EventEmitter) -> break return comment - resolveCommentId: (comment_id, resolved_data) -> - comment = @getComment(comment_id) - return if !comment? - comment.metadata.resolved = true - comment.metadata.resolved_data = resolved_data - @emit "comment:resolved", comment - - unresolveCommentId: (comment_id) -> - comment = @getComment(comment_id) - return if !comment? - comment.metadata.resolved = false - @emit "comment:unresolved", comment - removeCommentId: (comment_id) -> comment = @getComment(comment_id) return if !comment? diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index aa993183d7..ccfb012f2a 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -32,6 +32,7 @@ define [ for comment in thread.messages formatComment(comment) if thread.resolved_by_user? + $scope.$broadcast "comment:resolve_thread", thread_id formatUser(thread.resolved_by_user) $scope.reviewPanel.commentThreads = threads @@ -146,8 +147,6 @@ define [ new_entry = { type: "comment" thread_id: comment.op.t - resolved: comment.metadata?.resolved - resolved_data: comment.metadata?.resolved_data content: comment.op.c offset: comment.op.p } @@ -243,7 +242,7 @@ define [ thread.resolved_by_user = $scope.users[window.user_id] thread.resolved_at = new Date() $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken} - $scope.$broadcast "comment:resolve", entry_id, window.user_id + $scope.$broadcast "comment:resolve_thread", entry.thread_id $scope.unresolveComment = (entry, entry_id) -> thread = $scope.reviewPanel.commentThreads[entry.thread_id] @@ -251,7 +250,7 @@ define [ delete thread.resolved_by_user delete thread.resolved_at $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/reopen", {_csrf: window.csrfToken} - $scope.$broadcast "comment:unresolve", entry_id + $scope.$broadcast "comment:unresolve_thread", entry.thread_id $scope.deleteComment = (entry_id) -> $scope.$broadcast "comment:remove", entry_id From d13035a4f474cb4e87605d5296e595e2dfa7101b Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 5 Jan 2017 11:50:43 +0000 Subject: [PATCH 13/98] Simpler UI for comments; remove some unused code. --- .../views/project/editor/review-panel.jade | 69 +++++++--------- .../directives/commentEntry.coffee | 4 - .../stylesheets/app/editor/review-panel.less | 82 ++++++------------- 3 files changed, 54 insertions(+), 101 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index aade8bde51..6139d5b52f 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -30,10 +30,6 @@ entry="entry" threads="reviewPanel.commentThreads" on-resolve="resolveComment(entry, entry_id)" - on-unresolve="unresolveComment(entry, entry_id)" - on-show-thread="showThread(entry)" - on-hide-thread="hideThread(entry)" - on-delete="deleteComment(entry_id)" on-reply="submitReply(entry, entry_id);" on-indicator-click="toggleReviewPanel();" ) @@ -66,8 +62,6 @@ change-entry( entry="entry" user="users[entry.metadata.user_id]" - on-reject="rejectChange(entry.id);" - on-accept="acceptChange(entry.id);" on-indicator-click="toggleReviewPanel();" ng-click="gotoEntry(doc_id, entry)" ) @@ -76,9 +70,6 @@ comment-entry( entry="entry" users="users" - on-resolve="resolveComment(entry, entry.id)" - on-unresolve="unresolveComment(entry, entry.id)" - on-delete="deleteComment(entry.id)" on-reply="submitReply(entry, entry_id);" on-indicator-click="toggleReviewPanel();" ng-click="gotoEntry(doc_id, entry)" @@ -127,7 +118,8 @@ script(type='text/ng-template', id='changeEntryTemplate') span(ng-switch-when="delete") Deleted  del.rp-content-highlight {{ entry.content }} .rp-entry-metadata - span {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •  + | {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} + |  •  span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }} .rp-entry-actions a.rp-entry-button(href, ng-click="onReject();") @@ -139,54 +131,51 @@ script(type='text/ng-template', id='changeEntryTemplate') script(type='text/ng-template', id='commentEntryTemplate') div - .rp-entry-callout.rp-entry-callout-comment(ng-if="!threads[entry.thread_id].resolved") + .rp-entry-callout.rp-entry-callout-comment .rp-entry-indicator( ng-class="{ 'rp-entry-indicator-focused': entry.focused }" ng-click="onIndicatorClick();" ) i.fa.fa-comment .rp-entry.rp-entry-comment( - ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolved': threads[entry.thread_id].resolved}" + ng-class="{ 'rp-entry-focused': entry.focused }" ) - .rp-comment( - ng-if="!threads[entry.thread_id].resolved || entry.showWhenResolved" - ng-repeat="comment in threads[entry.thread_id].messages" - ng-class="comment.user.isSelf ? 'rp-comment-self' : '';" - ) - .rp-avatar( - ng-if="!comment.user.isSelf;" - style="background-color: hsl({{ comment.user.hue }}, 70%, 50%);" - ) {{ comment.user.avatar_text | limitTo : 1 }} - .rp-comment-body(style="color: hsl({{ comment.user.hue }}, 70%, 90%);") + div + .rp-comment( + ng-repeat="comment in threads[entry.thread_id].messages" + ) p.rp-comment-content {{ comment.content }} - p.rp-comment-metadata + .rp-entry-metadata | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} |  •  - span(style="color: hsl({{ comment.user.hue }}, 70%, 40%);") {{ comment.user.name }} - .rp-comment-reply(ng-if="!threads[entry.thread_id].resolved || entry.showWhenResolved") + span.rp-entry-user(style="color: hsl({{ comment.user.hue }}, 70%, 40%);") {{ comment.user.name }} + .rp-comment-reply textarea.rp-comment-input( ng-model="entry.replyContent" ng-keypress="handleCommentReplyKeyPress($event);" stop-propagation="click" placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}" ) - .rp-comment-resolved-description(ng-if="threads[entry.thread_id].resolved && !entry.showWhenResolved") - div - | Comment resolved by - span(style="color: hsl({{ threads[entry.thread_id].resolved_by_user.hue }}, 70%, 40%);") {{ threads[entry.thread_id].resolved_by_user.name }} - div {{ threads[entry.thread_id].resolved_at | date : 'MMM d, y h:mm a' }} + //- .rp-comment-resolved-description(ng-if="threads[entry.thread_id].resolved && !entry.showWhenResolved") + //- div + //- | Comment resolved by + //- span(style="color: hsl({{ threads[entry.thread_id].resolved_by_user.hue }}, 70%, 40%);") {{ threads[entry.thread_id].resolved_by_user.name }} + //- div {{ threads[entry.thread_id].resolved_at | date : 'MMM d, y h:mm a' }} .rp-entry-actions - a.rp-entry-button(href, ng-click="onResolve();", ng-if="!threads[entry.thread_id].resolved") + a.rp-entry-button(href, ng-click="onResolve();") i.fa.fa-check - |  Mark as resolved - a.rp-entry-button(href, ng-click="onShowThread();", ng-if="threads[entry.thread_id].resolved && !entry.showWhenResolved") - |  Show - a.rp-entry-button(href, ng-click="onHideThread();", ng-if="threads[entry.thread_id].resolved && entry.showWhenResolved") - |  Hide - a.rp-entry-button(href, ng-click="onUnresolve();", ng-if="threads[entry.thread_id].resolved") - |  Re-open - a.rp-entry-button(href, ng-click="onDelete();", ng-if="threads[entry.thread_id].resolved") - |  Delete + |  Resolve + a.rp-entry-button(href, ng-click="onReply();") + i.fa.fa-reply + |  Reply + //- a.rp-entry-button(href, ng-click="onShowThread();", ng-if="threads[entry.thread_id].resolved && !entry.showWhenResolved") + //- |  Show + //- a.rp-entry-button(href, ng-click="onHideThread();", ng-if="threads[entry.thread_id].resolved && entry.showWhenResolved") + //- |  Hide + //- a.rp-entry-button(href, ng-click="onUnresolve();", ng-if="threads[entry.thread_id].resolved") + //- |  Re-open + //- a.rp-entry-button(href, ng-click="onDelete();", ng-if="threads[entry.thread_id].resolved") + //- |  Delete script(type='text/ng-template', id='addCommentEntryTemplate') diff --git a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee index 76798b1935..2ee7862379 100644 --- a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee @@ -10,10 +10,6 @@ define [ onResolve: "&" onReply: "&" onIndicatorClick: "&" - onDelete: "&" - onUnresolve: "&" - onShowThread: "&" - onHideThread: "&" link: (scope, element, attrs) -> scope.handleCommentReplyKeyPress = (ev) -> if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index c2dd9f314c..a55c82220d 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -291,8 +291,6 @@ .rp-entry-metadata { font-size: @rp-small-font-size; - .rp-state-overview & { - } } .rp-entry-user { font-weight: @rp-semibold-weight; @@ -328,69 +326,39 @@ } .rp-comment { - display: flex; - align-items: flex-start; - padding: 5px; + margin: 2px 5px; + padding-bottom: 3px; + line-height: 1.4; + border-bottom: solid 1px @rp-border-grey; + + &:last-child { + margin-bottom: 2px; + border-bottom-width: 0; + } .rp-state-overview & { - padding: 3px 0; - line-height: 1.2; + padding: 0; } } - .rp-comment-body { - position: relative; - background-color: currentColor; - flex-grow: 1; - padding: 2px 5px; - margin-left: @rp-entry-arrow-width; - border-radius: 3px; - - .rp-comment-self & { - margin-left: 0; - margin-right: @rp-entry-arrow-width; - } - - &::after { - .triangle(left, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit); - top: (@review-off-width / 2) - @rp-entry-arrow-width; - left: -@rp-entry-arrow-width; - content: ''; - - .rp-comment-self & { - .triangle(right, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit); - right: -@rp-entry-arrow-width; - left: auto; - } - - } - } - .rp-comment-content { - margin: 0; - color: @rp-type-darkgrey; - } - - .rp-comment-metadata { - color: @rp-type-blue; - font-size: @rp-small-font-size; - margin: 0; - } - - .rp-comment-reply { - padding: 0 5px; - - .rp-state-overview & { - padding: 3px 0 0; - } + .rp-comment-content { + margin: 0; + color: @rp-type-darkgrey; } - .rp-comment-resolved-description { - padding: 5px; - - .rp-state-overview & { - padding: 0px; - } + .rp-comment-metadata { + color: @rp-type-blue; + font-size: @rp-small-font-size; + margin: 0; } + .rp-comment-reply { + padding: 0 5px; + + .rp-state-overview & { + padding: 3px 0 0; + } + } + .rp-add-comment-btn { .rp-button(); display: block; From a1435d13a35e5c5de2761cb32b8d5e7e25785a5c Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 5 Jan 2017 17:15:27 +0000 Subject: [PATCH 14/98] Basic structure for the resolved comments dropdown. --- .../views/project/editor/review-panel.jade | 34 ++++++++++++++----- .../review-panel/ReviewPanelManager.coffee | 1 + .../resolvedCommentsDropdown.coffee | 28 +++++++++++++++ .../stylesheets/app/editor/review-panel.less | 25 +++++++++++++- 4 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 6139d5b52f..a2f8d01efd 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -1,5 +1,9 @@ #review-panel .review-panel-toolbar + resolved-comments-dropdown( + entries="reviewPanel.entries" + threads="reviewPanel.commentThreads" + ) span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = true;", ng-if="editor.wantTrackChanges === false") Track Changes is strong off span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = false;", ng-if="editor.wantTrackChanges === true") Track Changes is @@ -118,8 +122,7 @@ script(type='text/ng-template', id='changeEntryTemplate') span(ng-switch-when="delete") Deleted  del.rp-content-highlight {{ entry.content }} .rp-entry-metadata - | {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} - |  •  + | {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •  span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }} .rp-entry-actions a.rp-entry-button(href, ng-click="onReject();") @@ -142,13 +145,16 @@ script(type='text/ng-template', id='commentEntryTemplate') ) div .rp-comment( - ng-repeat="comment in threads[entry.thread_id].messages" + ng-repeat="comment in threads[entry.thread_id].messages track by comment.id" ) - p.rp-comment-content {{ comment.content }} + p.rp-comment-content + span.rp-entry-user( + style="color: hsl({{ comment.user.hue }}, 70%, 40%);" + ng-if="$first || comment.user.id !== threads[entry.thread_id].messages[$index - 1].user.id" + ) {{ comment.user.name }}:  + | {{ comment.content }} .rp-entry-metadata | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} - |  •  - span.rp-entry-user(style="color: hsl({{ comment.user.hue }}, 70%, 40%);") {{ comment.user.name }} .rp-comment-reply textarea.rp-comment-input( ng-model="entry.replyContent" @@ -163,7 +169,7 @@ script(type='text/ng-template', id='commentEntryTemplate') //- div {{ threads[entry.thread_id].resolved_at | date : 'MMM d, y h:mm a' }} .rp-entry-actions a.rp-entry-button(href, ng-click="onResolve();") - i.fa.fa-check + i.fa.fa-inbox |  Resolve a.rp-entry-button(href, ng-click="onReply();") i.fa.fa-reply @@ -212,4 +218,16 @@ script(type='text/ng-template', id='addCommentEntryTemplate') |  Cancel a.rp-entry-button(href, ng-click="submitNewComment()") i.fa.fa-comment - |  Comment \ No newline at end of file + |  Comment + +script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') + .resolved-comments + a.resolved-comments-toggle( + href + ng-click="state.isOpen = !state.isOpen" + ) + i.fa.fa-inbox + .resolved-comments-dropdown( + ng-class="{ 'resolved-comments-dropdown-open' : state.isOpen }" + ) + div wut diff --git a/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee b/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee index cd1231c798..51358f8bc7 100644 --- a/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee +++ b/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee @@ -5,6 +5,7 @@ define [ "ide/review-panel/directives/changeEntry" "ide/review-panel/directives/commentEntry" "ide/review-panel/directives/addCommentEntry" + "ide/review-panel/directives/resolvedCommentsDropdown" "ide/review-panel/filters/notEmpty" "ide/review-panel/filters/orderOverviewEntries" ], () -> \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee new file mode 100644 index 0000000000..61a187be5b --- /dev/null +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -0,0 +1,28 @@ +define [ + "base" +], (App) -> + App.directive "resolvedCommentsDropdown", () -> + restrict: "E" + templateUrl: "resolvedCommentsDropdownTemplate" + scope: + entries: "=" + threads: "=" + link: (scope, element, attrs) -> + scope.state = + isOpen: false + + scope.resolvedComments = {} + + filterResolvedComments = () -> + scope.resolvedComments = {} + + for fileId, fileEntries of scope.entries + scope.resolvedComments[fileId] = {} + for entryId, entry of fileEntries + if entry.type == "comment" and scope.threads[entry.thread_id].resolved? + scope.resolvedComments[fileId][entryId] = scope.threads[entry.thread_id] + + scope.$watchCollection "entries", filterResolvedComments + scope.$watchCollection "threads", filterResolvedComments + + diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index a55c82220d..fc6193deb7 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -100,7 +100,6 @@ .rp-size-expanded & { display: flex; align-items: center; - justify-content: space-between; padding: 0 5px; } .rp-state-current-file & { @@ -120,6 +119,8 @@ .review-panel-toolbar-label { cursor: pointer; margin-right: 5px; + text-align: right; + flex-grow: 1; } .rp-entry-list { @@ -633,3 +634,25 @@ } } + +.resolved-comments-toggle { + font-size: @rp-icon-large-size; + color: lighten(@rp-type-blue, 25%); + border: solid 1px @rp-border-grey; + border-radius: 3px; + padding: 0 4px; + + &:hover, + &:focus { + text-decoration: none; + color: @rp-type-blue; + } +} + +.resolved-comments-dropdown { + display: none; + + &-open { + display: block; + } +} From f2e6e69df67d9ae087ad29ee9c65ddba97ac446a Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 6 Jan 2017 11:59:49 +0000 Subject: [PATCH 15/98] Add an entry directive for resolved comments, with file and quoted text. --- .../directives/resolvedCommentEntry.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee new file mode 100644 index 0000000000..e3691c1962 --- /dev/null +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee @@ -0,0 +1,11 @@ +define [ + "base" +], (App) -> + App.directive "resolvedCommentEntry", () -> + restrict: "E" + templateUrl: "resolvedCommentEntryTemplate" + scope: + thread: "=" + doc: "=" + onReopen: "&" + onDelete: "&" \ No newline at end of file From 42585085c08659cfa305b04947a010d18568dd6a Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 6 Jan 2017 12:00:17 +0000 Subject: [PATCH 16/98] Integrate new directive, plumb data into it. More styling. --- .../views/project/editor/review-panel.jade | 54 +++++++++++++------ .../review-panel/ReviewPanelManager.coffee | 1 + .../resolvedCommentsDropdown.coffee | 17 +++--- .../stylesheets/app/editor/review-panel.less | 26 ++++++++- 4 files changed, 76 insertions(+), 22 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index a2f8d01efd..3e4962f676 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -3,6 +3,7 @@ resolved-comments-dropdown( entries="reviewPanel.entries" threads="reviewPanel.commentThreads" + docs="docs" ) span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = true;", ng-if="editor.wantTrackChanges === false") Track Changes is strong off @@ -162,11 +163,6 @@ script(type='text/ng-template', id='commentEntryTemplate') stop-propagation="click" placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}" ) - //- .rp-comment-resolved-description(ng-if="threads[entry.thread_id].resolved && !entry.showWhenResolved") - //- div - //- | Comment resolved by - //- span(style="color: hsl({{ threads[entry.thread_id].resolved_by_user.hue }}, 70%, 40%);") {{ threads[entry.thread_id].resolved_by_user.name }} - //- div {{ threads[entry.thread_id].resolved_at | date : 'MMM d, y h:mm a' }} .rp-entry-actions a.rp-entry-button(href, ng-click="onResolve();") i.fa.fa-inbox @@ -174,15 +170,31 @@ script(type='text/ng-template', id='commentEntryTemplate') a.rp-entry-button(href, ng-click="onReply();") i.fa.fa-reply |  Reply - //- a.rp-entry-button(href, ng-click="onShowThread();", ng-if="threads[entry.thread_id].resolved && !entry.showWhenResolved") - //- |  Show - //- a.rp-entry-button(href, ng-click="onHideThread();", ng-if="threads[entry.thread_id].resolved && entry.showWhenResolved") - //- |  Hide - //- a.rp-entry-button(href, ng-click="onUnresolve();", ng-if="threads[entry.thread_id].resolved") - //- |  Re-open - //- a.rp-entry-button(href, ng-click="onDelete();", ng-if="threads[entry.thread_id].resolved") - //- |  Delete - + +script(type='text/ng-template', id='resolvedCommentEntryTemplate') + .rp-entry.rp-entry-comment + div + .rp-comment-context + | Quoted text on  + .rp-comment-context-file {{ doc.doc.name }} + .rp-comment-context-quote {{ thread.content }} + .rp-comment( + ng-repeat="comment in thread.messages track by comment.id" + ) + p.rp-comment-content + span.rp-entry-user( + style="color: hsl({{ comment.user.hue }}, 70%, 40%);" + ng-if="$first || comment.user.id !== thread.messages[$index - 1].user.id" + ) {{ comment.user.name }}:  + | {{ comment.content }} + .rp-entry-metadata + | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} + .rp-entry-actions + a.rp-entry-button(href, ng-click="onUnresolve();") + |  Re-open + a.rp-entry-button(href, ng-click="onDelete();") + |  Delete + script(type='text/ng-template', id='addCommentEntryTemplate') div @@ -222,6 +234,10 @@ script(type='text/ng-template', id='addCommentEntryTemplate') script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') .resolved-comments + .resolved-comments-backdrop( + ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }" + ng-click="state.isOpen = !state.isOpen" + ) a.resolved-comments-toggle( href ng-click="state.isOpen = !state.isOpen" @@ -230,4 +246,12 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') .resolved-comments-dropdown( ng-class="{ 'resolved-comments-dropdown-open' : state.isOpen }" ) - div wut + div( + ng-repeat="doc in docs" + ) + resolved-comment-entry( + ng-repeat="thread in resolvedCommentsPerFile[doc.doc.id]" + thread="thread" + doc="doc" + ) + diff --git a/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee b/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee index 51358f8bc7..2ad425b737 100644 --- a/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee +++ b/services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee @@ -5,6 +5,7 @@ define [ "ide/review-panel/directives/changeEntry" "ide/review-panel/directives/commentEntry" "ide/review-panel/directives/addCommentEntry" + "ide/review-panel/directives/resolvedCommentEntry" "ide/review-panel/directives/resolvedCommentsDropdown" "ide/review-panel/filters/notEmpty" "ide/review-panel/filters/orderOverviewEntries" diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index 61a187be5b..5bdd85e71e 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -5,22 +5,27 @@ define [ restrict: "E" templateUrl: "resolvedCommentsDropdownTemplate" scope: - entries: "=" - threads: "=" + entries : "=" + threads : "=" + docs : "=" link: (scope, element, attrs) -> scope.state = isOpen: false - scope.resolvedComments = {} + scope.resolvedCommentsPerFile = {} filterResolvedComments = () -> - scope.resolvedComments = {} + scope.resolvedCommentsPerFile = {} for fileId, fileEntries of scope.entries - scope.resolvedComments[fileId] = {} + scope.resolvedCommentsPerFile[fileId] = {} for entryId, entry of fileEntries if entry.type == "comment" and scope.threads[entry.thread_id].resolved? - scope.resolvedComments[fileId][entryId] = scope.threads[entry.thread_id] + scope.resolvedCommentsPerFile[fileId][entryId] = angular.copy scope.threads[entry.thread_id] + scope.resolvedCommentsPerFile[fileId][entryId].content = entry.content + + console.log scope.resolvedCommentsPerFile + scope.$watchCollection "entries", filterResolvedComments scope.$watchCollection "threads", filterResolvedComments diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index fc6193deb7..48c37e0159 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -230,6 +230,10 @@ border-bottom: solid 1px @rp-border-grey; cursor: pointer; } + .resolved-comments-dropdown & { + position: static; + margin-bottom: 5px; + } border-left: solid @rp-entry-ribbon-width transparent; border-radius: 3px; @@ -649,9 +653,29 @@ } } +.resolved-comments-backdrop { + display: none; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba(0, 0, 0, .5); + + &-visible { + display: block; + } +} + .resolved-comments-dropdown { display: none; - + position: absolute; + width: 300px; + background-color: @rp-bg-blue; + text-align: left; + padding: 5px; + border-radius: 3px; + &-open { display: block; } From 3a5d45fa32731ce104edb549a271ca7814cd30fc Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 6 Jan 2017 13:41:58 +0100 Subject: [PATCH 17/98] Get user info via web, not chat --- .../Features/Chat/ChatController.coffee | 16 +- .../Comments/CommentsController.coffee | 59 ++++++- .../Features/Ranges/RangesController.coffee | 9 + .../Features/Ranges/RangesManager.coffee | 17 +- .../Features/User/UserInfoController.coffee | 21 +-- .../Features/User/UserInfoManager.coffee | 5 + services/web/app/coffee/router.coffee | 1 + .../ide/chat/services/chatMessages.coffee | 10 +- .../controllers/ReviewPanelController.coffee | 62 ++++--- .../coffee/Chat/ChatControllerTests.coffee | 19 ++- .../Comments/CommentsControllerTests.coffee | 154 +++++++++++++++++- .../coffee/Ranges/RangesManagerTests.coffee | 55 +++++++ .../User/UserInfoControllerTests.coffee | 25 ++- 13 files changed, 384 insertions(+), 69 deletions(-) create mode 100644 services/web/app/coffee/Features/User/UserInfoManager.coffee create mode 100644 services/web/test/UnitTests/coffee/Ranges/RangesManagerTests.coffee diff --git a/services/web/app/coffee/Features/Chat/ChatController.coffee b/services/web/app/coffee/Features/Chat/ChatController.coffee index d9a5d7db70..7e4e190290 100644 --- a/services/web/app/coffee/Features/Chat/ChatController.coffee +++ b/services/web/app/coffee/Features/Chat/ChatController.coffee @@ -2,6 +2,9 @@ ChatApiHandler = require("./ChatApiHandler") EditorRealTimeController = require("../Editor/EditorRealTimeController") logger = require("logger-sharelatex") AuthenticationController = require('../Authentication/AuthenticationController') +UserInfoManager = require('../User/UserInfoManager') +UserInfoController = require('../User/UserInfoController') +CommentsController = require('../Comments/CommentsController') module.exports = sendMessage: (req, res, next)-> @@ -13,8 +16,11 @@ module.exports = return next(err) ChatApiHandler.sendGlobalMessage project_id, user_id, content, (err, message) -> return next(err) if err? - EditorRealTimeController.emitToRoom project_id, "new-chat-message", message, (err)-> - res.send(204) + UserInfoManager.getPersonalInfo message.user_id, (err, user) -> + return next(err) if err? + message.user = UserInfoController.formatPersonalInfo(user) + EditorRealTimeController.emitToRoom project_id, "new-chat-message", message, (err)-> + res.send(204) getMessages: (req, res, next)-> project_id = req.params.project_id @@ -22,5 +28,7 @@ module.exports = logger.log project_id:project_id, query:query, "getting messages" ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) -> return next(err) if err? - logger.log length: messages?.length, "sending messages to client" - res.json messages + CommentsController._injectUserInfoIntoThreads [{ messages: messages }], (err) -> + return next(err) if err? + logger.log length: messages?.length, "sending messages to client" + res.json messages diff --git a/services/web/app/coffee/Features/Comments/CommentsController.coffee b/services/web/app/coffee/Features/Comments/CommentsController.coffee index 07974d6872..20346dccbb 100644 --- a/services/web/app/coffee/Features/Comments/CommentsController.coffee +++ b/services/web/app/coffee/Features/Comments/CommentsController.coffee @@ -2,6 +2,9 @@ ChatApiHandler = require("../Chat/ChatApiHandler") EditorRealTimeController = require("../Editor/EditorRealTimeController") logger = require("logger-sharelatex") AuthenticationController = require('../Authentication/AuthenticationController') +UserInfoManager = require('../User/UserInfoManager') +UserInfoController = require('../User/UserInfoController') +async = require "async" module.exports = CommentsController = sendComment: (req, res, next) -> @@ -14,29 +17,67 @@ module.exports = CommentsController = logger.log {project_id, thread_id, user_id, content}, "sending comment" ChatApiHandler.sendComment project_id, thread_id, user_id, content, (err, comment) -> return next(err) if err? - EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err) -> - res.send 204 + UserInfoManager.getPersonalInfo comment.user_id, (err, user) -> + return next(err) if err? + comment.user = UserInfoController.formatPersonalInfo(user) + EditorRealTimeController.emitToRoom project_id, "new-comment", thread_id, comment, (err) -> + res.send 204 getThreads: (req, res, next) -> {project_id} = req.params logger.log {project_id}, "getting comment threads for project" ChatApiHandler.getThreads project_id, (err, threads) -> return next(err) if err? - res.json threads - + CommentsController._injectUserInfoIntoThreads threads, (error, threads) -> + return next(err) if err? + res.json threads + resolveThread: (req, res, next) -> {project_id, thread_id} = req.params user_id = AuthenticationController.getLoggedInUserId(req) logger.log {project_id, thread_id, user_id}, "resolving comment thread" - ChatApiHandler.resolveThread project_id, thread_id, user_id, (err, threads) -> + ChatApiHandler.resolveThread project_id, thread_id, user_id, (err) -> return next(err) if err? - EditorRealTimeController.emitToRoom project_id, "resolve-thread", thread_id, user_id, (err)-> - res.send 204 - + UserInfoManager.getPersonalInfo user_id, (err, user) -> + return next(err) if err? + EditorRealTimeController.emitToRoom project_id, "resolve-thread", thread_id, UserInfoController.formatPersonalInfo(user), (err)-> + res.send 204 + reopenThread: (req, res, next) -> {project_id, thread_id} = req.params logger.log {project_id, thread_id}, "reopening comment thread" ChatApiHandler.reopenThread project_id, thread_id, (err, threads) -> return next(err) if err? EditorRealTimeController.emitToRoom project_id, "reopen-thread", thread_id, (err)-> - res.send 204 \ No newline at end of file + res.send 204 + + _injectUserInfoIntoThreads: (threads, callback = (error, threads) ->) -> + userCache = {} + getUserDetails = (user_id, callback = (error, user) ->) -> + return callback(null, userCache[user_id]) if userCache[user_id]? + UserInfoManager.getPersonalInfo user_id, (err, user) -> + return callback(error) if error? + user = UserInfoController.formatPersonalInfo user + userCache[user_id] = user + callback null, user + + jobs = [] + for thread in threads + do (thread) -> + if thread.resolved + jobs.push (cb) -> + getUserDetails thread.resolved_by_user_id, (error, user) -> + cb(error) if error? + thread.resolved_by_user = user + cb() + for message in thread.messages + do (message) -> + jobs.push (cb) -> + getUserDetails message.user_id, (error, user) -> + cb(error) if error? + message.user = user + cb() + + async.series jobs, (error) -> + return callback(error) if error? + return callback null, threads \ No newline at end of file diff --git a/services/web/app/coffee/Features/Ranges/RangesController.coffee b/services/web/app/coffee/Features/Ranges/RangesController.coffee index 96a42588ac..78457c2e64 100644 --- a/services/web/app/coffee/Features/Ranges/RangesController.coffee +++ b/services/web/app/coffee/Features/Ranges/RangesController.coffee @@ -1,5 +1,6 @@ RangesManager = require "./RangesManager" logger = require "logger-sharelatex" +UserInfoController = require "../User/UserInfoController" module.exports = RangesController = getAllRanges: (req, res, next) -> @@ -9,3 +10,11 @@ module.exports = RangesController = return next(error) if error? docs = ({id: d._id, ranges: d.ranges} for d in docs) res.json docs + + getAllRangesUsers: (req, res, next) -> + project_id = req.params.project_id + logger.log {project_id}, "request for project range users" + RangesManager.getAllRangesUsers project_id, (error, users) -> + return next(error) if error? + users = (UserInfoController.formatPersonalInfo(user) for user in users) + res.json users diff --git a/services/web/app/coffee/Features/Ranges/RangesManager.coffee b/services/web/app/coffee/Features/Ranges/RangesManager.coffee index 1fdf55b4a8..b538c1f443 100644 --- a/services/web/app/coffee/Features/Ranges/RangesManager.coffee +++ b/services/web/app/coffee/Features/Ranges/RangesManager.coffee @@ -1,8 +1,23 @@ DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" DocstoreManager = require "../Docstore/DocstoreManager" +UserInfoManager = require "../User/UserInfoManager" +async = require "async" module.exports = RangesManager = getAllRanges: (project_id, callback = (error, docs) ->) -> DocumentUpdaterHandler.flushProjectToMongo project_id, (error) -> return callback(error) if error? - DocstoreManager.getAllRanges project_id, callback \ No newline at end of file + DocstoreManager.getAllRanges project_id, callback + + getAllRangesUsers: (project_id, callback = (error, users) ->) -> + user_ids = {} + RangesManager.getAllRanges project_id, (error, docs) -> + return callback(error) if error? + jobs = [] + for doc in docs + for change in doc.ranges?.changes or [] + user_ids[change.metadata.user_id] = true + + async.mapSeries Object.keys(user_ids), (user_id, cb) -> + UserInfoManager.getPersonalInfo user_id, cb + , callback \ No newline at end of file diff --git a/services/web/app/coffee/Features/User/UserInfoController.coffee b/services/web/app/coffee/Features/User/UserInfoController.coffee index 92f111bda7..a77f575a48 100644 --- a/services/web/app/coffee/Features/User/UserInfoController.coffee +++ b/services/web/app/coffee/Features/User/UserInfoController.coffee @@ -26,17 +26,12 @@ module.exports = UserController = UserController.sendFormattedPersonalInfo(user, res, next) sendFormattedPersonalInfo: (user, res, next = (error) ->) -> - UserController._formatPersonalInfo user, (error, info) -> - return next(error) if error? - res.send JSON.stringify(info) + info = UserController.formatPersonalInfo(user) + res.send JSON.stringify(info) - _formatPersonalInfo: (user, callback = (error, info) ->) -> - callback null, { - id: user._id.toString() - first_name: user.first_name - last_name: user.last_name - email: user.email - signUpDate: user.signUpDate - role: user.role - institution: user.institution - } + formatPersonalInfo: (user, callback = (error, info) ->) -> + formatted_user = { id: user._id.toString() } + for key in ["first_name", "last_name", "email", "signUpDate", "role", "institution"] + if user[key]? + formatted_user[key] = user[key] + return formatted_user diff --git a/services/web/app/coffee/Features/User/UserInfoManager.coffee b/services/web/app/coffee/Features/User/UserInfoManager.coffee new file mode 100644 index 0000000000..90971e31a5 --- /dev/null +++ b/services/web/app/coffee/Features/User/UserInfoManager.coffee @@ -0,0 +1,5 @@ +UserGetter = require "./UserGetter" + +module.exports = UserInfoManager = + getPersonalInfo: (user_id, callback = (error) ->) -> + UserGetter.getUser user_id, { _id: true, first_name: true, last_name: true, email: true }, callback \ No newline at end of file diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 13c8568f17..1332a2d7e5 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -178,6 +178,7 @@ module.exports = class Router webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, RangesController.getAllRanges + webRouter.get "/project/:project_id/ranges/users", AuthorizationMiddlewear.ensureUserCanReadProject, RangesController.getAllRangesUsers webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects diff --git a/services/web/public/coffee/ide/chat/services/chatMessages.coffee b/services/web/public/coffee/ide/chat/services/chatMessages.coffee index 1205a51267..6c33f95ea7 100644 --- a/services/web/public/coffee/ide/chat/services/chatMessages.coffee +++ b/services/web/public/coffee/ide/chat/services/chatMessages.coffee @@ -1,5 +1,6 @@ define [ "base" + "libs/md5" ], (App) -> App.factory "chatMessages", ($http, ide) -> MESSAGES_URL = "/project/#{ide.project_id}/messages" @@ -72,7 +73,7 @@ define [ firstMessage.contents.unshift message.content else chat.state.messages.unshift({ - user: message.user + user: formatUser(message.user) timestamp: message.timestamp contents: [message.content] }) @@ -93,9 +94,14 @@ define [ lastMessage.contents.push message.content else chat.state.messages.push({ - user: message.user + user: formatUser(message.user) timestamp: message.timestamp contents: [message.content] }) + + formatUser = (user) -> + hash = CryptoJS.MD5(user.email.toLowerCase()) + user.gravatar_url = "//www.gravatar.com/avatar/#{hash}" + return user return chat \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index ccfb012f2a..103f566617 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -24,18 +24,10 @@ define [ adding: false content: "" + $scope.users = {} + $scope.reviewPanelEventsBridge = new EventEmitter() - $http.get "/project/#{$scope.project_id}/threads" - .success (threads) -> - for thread_id, thread of threads - for comment in thread.messages - formatComment(comment) - if thread.resolved_by_user? - $scope.$broadcast "comment:resolve_thread", thread_id - formatUser(thread.resolved_by_user) - $scope.reviewPanel.commentThreads = threads - ide.socket.on "new-comment", (thread_id, comment) -> $scope.reviewPanel.commentThreads[thread_id] ?= { messages: [] } $scope.reviewPanel.commentThreads[thread_id].messages.push(formatComment(comment)) @@ -141,6 +133,9 @@ define [ for key, value of new_entry entries[change.id][key] = value + if !$scope.users[change.metadata.user_id]? + refreshChangeUsers(change.metadata.user_id) + for comment in rangesTracker.comments delete delete_changes[comment.id] entries[comment.id] ?= {} @@ -239,7 +234,7 @@ define [ entry.focused = false thread = $scope.reviewPanel.commentThreads[entry.thread_id] thread.resolved = true - thread.resolved_by_user = $scope.users[window.user_id] + thread.resolved_by_user = formatUser(ide.$scope.user) thread.resolved_at = new Date() $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken} $scope.$broadcast "comment:resolve_thread", entry.thread_id @@ -271,12 +266,41 @@ define [ $scope.gotoEntry = (doc_id, entry) -> ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset }) - # TODO: Eventually we need to get this from the server, and update it - # when we get an id we don't know. This'll do for client side testing - refreshUsers = () -> - $scope.users = {} - for member in $scope.project.members.concat($scope.project.owner) - $scope.users[member._id] = formatUser(member) + _refreshingRangeUsers = false + _refreshedForUserIds = {} + refreshChangeUsers = (refresh_for_user_id) -> + if refresh_for_user_id? + if _refreshedForUserIds[refresh_for_user_id]? + # We've already tried to refresh to get this user id, so stop it looping + return + _refreshedForUserIds[refresh_for_user_id] = true + + # Only do one refresh at once + if _refreshingRangeUsers + return + _refreshingRangeUsers = true + + $http.get "/project/#{$scope.project_id}/ranges/users" + .success (users) -> + _refreshingRangeUsers = false + $scope.users = {} + for user in users + $scope.users[user.id] = formatUser(user) + .error () -> + _refreshingRangeUsers = false + + refreshThreads = () -> + $http.get "/project/#{$scope.project_id}/threads" + .success (threads) -> + for thread_id, thread of threads + for comment in thread.messages + formatComment(comment) + if thread.resolved_by_user? + $scope.$broadcast "comment:resolve_thread", thread_id + formatUser(thread.resolved_by_user) + $scope.reviewPanel.commentThreads = threads + + refreshThreads() formatComment = (comment) -> comment.user = formatUser(user) @@ -308,7 +332,3 @@ define [ hue: ColorManager.getHueForUserId(id) avatar_text: [user.first_name, user.last_name].filter((n) -> n?).map((n) -> n[0]).join "" } - - $scope.$watch "project.members", (members) -> - return if !members? - refreshUsers() diff --git a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee index 3db5dadd30..851eb47f09 100644 --- a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee @@ -21,6 +21,9 @@ describe "ChatController", -> "./ChatApiHandler": @ChatApiHandler "../Editor/EditorRealTimeController": @EditorRealTimeController '../Authentication/AuthenticationController': @AuthenticationController + '../User/UserInfoManager': @UserInfoManager = {} + '../User/UserInfoController': @UserInfoController = {} + '../Comments/CommentsController': @CommentsController = {} @req = params: project_id: @project_id @@ -32,9 +35,22 @@ describe "ChatController", -> beforeEach -> @req.body = content: @content = "message-content" - @ChatApiHandler.sendGlobalMessage = sinon.stub().yields(null, @message = {"mock": "message"}) + @UserInfoManager.getPersonalInfo = sinon.stub().yields(null, @user = {"unformatted": "user"}) + @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formatted_user = {"formatted": "user"}) + @ChatApiHandler.sendGlobalMessage = sinon.stub().yields(null, @message = {"mock": "message", user_id: @user_id}) @ChatController.sendMessage @req, @res + it "should look up the user", -> + @UserInfoManager.getPersonalInfo + .calledWith(@user_id) + .should.equal true + + it "should format and inject the user into the message", -> + @UserInfoController.formatPersonalInfo + .calledWith(@user) + .should.equal true + @message.user.should.deep.equal @formatted_user + it "should tell the chat handler about the message", -> @ChatApiHandler.sendGlobalMessage .calledWith(@project_id, @user_id, @content) @@ -53,6 +69,7 @@ describe "ChatController", -> @req.query = limit: @limit = "30" before: @before = "12345" + @CommentsController._injectUserInfoIntoThreads = sinon.stub().yields() @ChatApiHandler.getGlobalMessages = sinon.stub().yields(null, @messages = ["mock", "messages"]) @ChatController.getMessages @req, @res diff --git a/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee index 8acaa66886..ecf57abd2e 100644 --- a/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee @@ -21,6 +21,8 @@ describe "CommentsController", -> "../Chat/ChatApiHandler": @ChatApiHandler "../Editor/EditorRealTimeController": @EditorRealTimeController '../Authentication/AuthenticationController': @AuthenticationController + '../User/UserInfoManager': @UserInfoManager = {} + '../User/UserInfoController': @UserInfoController = {} @req = {} @res = json: sinon.stub() @@ -29,12 +31,25 @@ describe "CommentsController", -> describe "sendComment", -> beforeEach -> @req.params = - project_id: @project_id - thread_id: @thread_id + project_id: @project_id = "mock-project-id" + thread_id: @thread_id = "mock-thread-id" @req.body = content: @content = "message-content" - @ChatApiHandler.sendComment = sinon.stub().yields(null, @message = {"mock": "message"}) + @UserInfoManager.getPersonalInfo = sinon.stub().yields(null, @user = {"unformatted": "user"}) + @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formatted_user = {"formatted": "user"}) + @ChatApiHandler.sendComment = sinon.stub().yields(null, @message = {"mock": "message", user_id: @user_id}) @CommentsController.sendComment @req, @res + + it "should look up the user", -> + @UserInfoManager.getPersonalInfo + .calledWith(@user_id) + .should.equal true + + it "should format and inject the user into the comment", -> + @UserInfoController.formatPersonalInfo + .calledWith(@user) + .should.equal true + @message.user.should.deep.equal @formatted_user it "should tell the chat handler about the message", -> @ChatApiHandler.sendComment @@ -52,14 +67,143 @@ describe "CommentsController", -> describe "getThreads", -> beforeEach -> @req.params = - project_id: @project_id + project_id: @project_id = "mock-project-id" @ChatApiHandler.getThreads = sinon.stub().yields(null, @threads = {"mock", "threads"}) + @CommentsController._injectUserInfoIntoThreads = sinon.stub().yields(null, @threads) @CommentsController.getThreads @req, @res it "should ask the chat handler about the request", -> @ChatApiHandler.getThreads .calledWith(@project_id) .should.equal true + + it "should inject the user details into the threads", -> + @CommentsController._injectUserInfoIntoThreads + .calledWith(@threads) + .should.equal true it "should return the messages", -> - @res.json.calledWith(@threads).should.equal true \ No newline at end of file + @res.json.calledWith(@threads).should.equal true + + describe "resolveThread", -> + beforeEach -> + @req.params = + project_id: @project_id = "mock-project-id" + thread_id: @thread_id = "mock-thread-id" + @ChatApiHandler.resolveThread = sinon.stub().yields() + @UserInfoManager.getPersonalInfo = sinon.stub().yields(null, @user = {"unformatted": "user"}) + @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formatted_user = {"formatted": "user"}) + @CommentsController.resolveThread @req, @res + + it "should ask the chat handler to resolve the thread", -> + @ChatApiHandler.resolveThread + .calledWith(@project_id, @thread_id) + .should.equal true + + it "should look up the user", -> + @UserInfoManager.getPersonalInfo + .calledWith(@user_id) + .should.equal true + + it "should tell the client the comment was resolved", -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "resolve-thread", @thread_id, @formatted_user) + .should.equal true + + it "should return a success code", -> + @res.send.calledWith(204).should.equal + + describe "reopenThread", -> + beforeEach -> + @req.params = + project_id: @project_id = "mock-project-id" + thread_id: @thread_id = "mock-thread-id" + @ChatApiHandler.reopenThread = sinon.stub().yields() + @CommentsController.reopenThread @req, @res + + it "should ask the chat handler to reopen the thread", -> + @ChatApiHandler.reopenThread + .calledWith(@project_id, @thread_id) + .should.equal true + + it "should tell the client the comment was resolved", -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "reopen-thread", @thread_id) + .should.equal true + + it "should return a success code", -> + @res.send.calledWith(204).should.equal + + describe "_injectUserInfoIntoThreads", -> + beforeEach -> + @users = { + "user_id_1": { + "mock": "user_1" + } + "user_id_2": { + "mock": "user_2" + } + } + @UserInfoManager.getPersonalInfo = (user_id, callback) => + return callback(null, @users[user_id]) + sinon.spy @UserInfoManager, "getPersonalInfo" + @UserInfoController.formatPersonalInfo = (user) -> + return { "formatted": user["mock"] } + + it "should inject a user object into messaged and resolved data", (done) -> + @CommentsController._injectUserInfoIntoThreads [ + { + resolved: true + resolved_by_user_id: "user_id_1" + messages: [{ + user_id: "user_id_1" + content: "foo" + }, { + user_id: "user_id_2" + content: "bar" + }] + }, + { + messages: [{ + user_id: "user_id_1" + content: "baz" + }] + } + ], (error, threads) -> + expect(threads).to.deep.equal [ + { + resolved: true + resolved_by_user_id: "user_id_1" + resolved_by_user: { "formatted": "user_1" } + messages: [{ + user_id: "user_id_1" + user: { "formatted": "user_1" } + content: "foo" + }, { + user_id: "user_id_2" + user: { "formatted": "user_2" } + content: "bar" + }] + }, + { + messages: [{ + user_id: "user_id_1" + user: { "formatted": "user_1" } + content: "baz" + }] + } + ] + done() + + it "should only need to look up each user once", (done) -> + @CommentsController._injectUserInfoIntoThreads [{ + messages: [{ + user_id: "user_id_1" + content: "foo" + }, { + user_id: "user_id_1" + content: "bar" + }] + }], (error, threads) => + @UserInfoManager.getPersonalInfo.calledOnce.should.equal true + done() \ No newline at end of file diff --git a/services/web/test/UnitTests/coffee/Ranges/RangesManagerTests.coffee b/services/web/test/UnitTests/coffee/Ranges/RangesManagerTests.coffee new file mode 100644 index 0000000000..5f5c09d402 --- /dev/null +++ b/services/web/test/UnitTests/coffee/Ranges/RangesManagerTests.coffee @@ -0,0 +1,55 @@ +should = require('chai').should() +SandboxedModule = require('sandboxed-module') +assert = require('assert') +sinon = require('sinon') +path = require "path" +modulePath = path.join __dirname, "../../../../app/js/Features/Ranges/RangesManager" +expect = require("chai").expect + +describe "RangesManager", -> + beforeEach -> + @RangesManager = SandboxedModule.require modulePath, requires: + "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler = {} + "../Docstore/DocstoreManager": @DocstoreManager = {} + "../User/UserInfoManager": @UserInfoManager = {} + + describe "getAllRangesUsers", -> + beforeEach -> + @project_id = "mock-project-id" + @user_id1 = "mock-user-id-1" + @user_id1 = "mock-user-id-2" + @docs = [{ + ranges: + changes: [{ + op: { i: "foo", p: 42 } + metadata: + user_id: @user_id1 + }, { + op: { i: "bar", p: 102 } + metadata: + user_id: @user_id2 + }] + }, { + ranges: + changes: [{ + op: { i: "baz", p: 3 } + metadata: + user_id: @user_id1 + }] + }] + @users = {} + @users[@user_id1] = {"mock": "user-1"} + @users[@user_id2] = {"mock": "user-2"} + @UserInfoManager.getPersonalInfo = (user_id, callback) => callback null, @users[user_id] + sinon.spy @UserInfoManager, "getPersonalInfo" + @RangesManager.getAllRanges = sinon.stub().yields(null, @docs) + + it "should return an array of unique users", (done) -> + @RangesManager.getAllRangesUsers @project_id, (error, users) => + users.should.deep.equal [{"mock": "user-1"}, {"mock": "user-2"}] + done() + + it "should only call getPersonalInfo once for each user", (done) -> + @RangesManager.getAllRangesUsers @project_id, (error, users) => + @UserInfoManager.getPersonalInfo.calledTwice.should.equal true + done() \ No newline at end of file diff --git a/services/web/test/UnitTests/coffee/User/UserInfoControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserInfoControllerTests.coffee index df895e630d..37a1c034f0 100644 --- a/services/web/test/UnitTests/coffee/User/UserInfoControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/User/UserInfoControllerTests.coffee @@ -93,18 +93,18 @@ describe "UserInfoController", -> first_name: @user.first_name last_name: @user.last_name email: @user.email - @UserInfoController._formatPersonalInfo = sinon.stub().callsArgWith(1, null, @formattedInfo) + @UserInfoController.formatPersonalInfo = sinon.stub().returns(@formattedInfo) @UserInfoController.sendFormattedPersonalInfo @user, @res it "should format the user details for the response", -> - @UserInfoController._formatPersonalInfo + @UserInfoController.formatPersonalInfo .calledWith(@user) .should.equal true it "should send the formatted details back to the client", -> @res.body.should.equal JSON.stringify(@formattedInfo) - describe "_formatPersonalInfo", -> + describe "formatPersonalInfo", -> it "should return the correctly formatted data", -> @user = _id: ObjectId() @@ -115,14 +115,13 @@ describe "UserInfoController", -> signUpDate: new Date() role:"student" institution:"sheffield" - @UserInfoController._formatPersonalInfo @user, (error, info) => - expect(info).to.deep.equal { - id: @user._id.toString() - first_name: @user.first_name - last_name: @user.last_name - email: @user.email - signUpDate: @user.signUpDate - role: @user.role - institution: @user.institution - } + expect(@UserInfoController.formatPersonalInfo(@user)).to.deep.equal { + id: @user._id.toString() + first_name: @user.first_name + last_name: @user.last_name + email: @user.email + signUpDate: @user.signUpDate + role: @user.role + institution: @user.institution + } From cb24e9390a1e48a32e7724630ab15145b367580d Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 6 Jan 2017 14:17:57 +0100 Subject: [PATCH 18/98] Fix comments in overview panel --- services/web/app/views/project/editor/review-panel.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 3e4962f676..dcaefeea12 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -74,7 +74,7 @@ div(ng-if="entry.type === 'comment'") comment-entry( entry="entry" - users="users" + threads="reviewPanel.commentThreads" on-reply="submitReply(entry, entry_id);" on-indicator-click="toggleReviewPanel();" ng-click="gotoEntry(doc_id, entry)" From 0478fcd9258199249127b9175865cd33f0ceb5f9 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 6 Jan 2017 15:24:33 +0000 Subject: [PATCH 19/98] Add comment resolution info. --- .../views/project/editor/review-panel.jade | 25 ++++-- .../controllers/ReviewPanelController.coffee | 2 +- .../resolvedCommentsDropdown.coffee | 5 +- .../stylesheets/app/editor/review-panel.less | 77 +++++++++++++------ 4 files changed, 73 insertions(+), 36 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index dcaefeea12..e8bddadf49 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -172,12 +172,12 @@ script(type='text/ng-template', id='commentEntryTemplate') |  Reply script(type='text/ng-template', id='resolvedCommentEntryTemplate') - .rp-entry.rp-entry-comment + .rp-resolved-comment div - .rp-comment-context + .rp-resolved-comment-context | Quoted text on  - .rp-comment-context-file {{ doc.doc.name }} - .rp-comment-context-quote {{ thread.content }} + span.rp-resolved-comment-context-file {{ doc.doc.name }} + p.rp-resolved-comment-context-quote {{ thread.content }} .rp-comment( ng-repeat="comment in thread.messages track by comment.id" ) @@ -189,6 +189,15 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') | {{ comment.content }} .rp-entry-metadata | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} + .rp-comment.rp-comment-resolver + p.rp-comment-resolver-content + span.rp-entry-user( + style="color: hsl({{ thread.resolved_by_user.hue }}, 70%, 40%);" + ) {{ thread.resolved_by_user.name }}:  + | Marked as resolved. + .rp-entry-metadata + | {{ thread.resolved_at | date : 'MMM d, y h:mm a' }} + .rp-entry-actions a.rp-entry-button(href, ng-click="onUnresolve();") |  Re-open @@ -234,10 +243,10 @@ script(type='text/ng-template', id='addCommentEntryTemplate') script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') .resolved-comments - .resolved-comments-backdrop( - ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }" - ng-click="state.isOpen = !state.isOpen" - ) + //- .resolved-comments-backdrop( + //- ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }" + //- ng-click="state.isOpen = !state.isOpen" + //- ) a.resolved-comments-toggle( href ng-click="state.isOpen = !state.isOpen" diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 103f566617..a41bb07958 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -297,7 +297,7 @@ define [ formatComment(comment) if thread.resolved_by_user? $scope.$broadcast "comment:resolve_thread", thread_id - formatUser(thread.resolved_by_user) + thread.resolved_by_user = formatUser(thread.resolved_by_user) $scope.reviewPanel.commentThreads = threads refreshThreads() diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index 5bdd85e71e..7f9457bcdf 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -20,13 +20,10 @@ define [ for fileId, fileEntries of scope.entries scope.resolvedCommentsPerFile[fileId] = {} for entryId, entry of fileEntries - if entry.type == "comment" and scope.threads[entry.thread_id].resolved? + if entry.type == "comment" and scope.threads[entry.thread_id]?.resolved? scope.resolvedCommentsPerFile[fileId][entryId] = angular.copy scope.threads[entry.thread_id] scope.resolvedCommentsPerFile[fileId][entryId].content = entry.content - console.log scope.resolvedCommentsPerFile - - scope.$watchCollection "entries", filterResolvedComments scope.$watchCollection "threads", filterResolvedComments diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 48c37e0159..16a52fbabe 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -226,7 +226,6 @@ } .rp-state-overview & { border-radius: 0; - padding: 2px 5px; border-bottom: solid 1px @rp-border-grey; cursor: pointer; } @@ -274,10 +273,6 @@ display: flex; align-items: center; padding: 4px 5px; - - .rp-state-overview & { - padding: 0; - } } .rp-entry-action-icon { font-size: @rp-icon-large-size; @@ -292,6 +287,10 @@ .rp-entry-details { line-height: 1.4; margin-left: 5px; + + .rp-state-overview & { + margin-left: 0; + } } .rp-entry-metadata { @@ -299,6 +298,7 @@ } .rp-entry-user { font-weight: @rp-semibold-weight; + font-style: normal; } .rp-content-highlight { @@ -336,13 +336,20 @@ line-height: 1.4; border-bottom: solid 1px @rp-border-grey; + + &:last-child { margin-bottom: 2px; border-bottom-width: 0; } .rp-state-overview & { - padding: 0; + margin: 4px 5px; + + &:first-child { + margin-top: 0; + padding-top: 4px; + } } } .rp-comment-content { @@ -355,13 +362,17 @@ font-size: @rp-small-font-size; margin: 0; } + + .rp-comment-resolver { + color: @rp-type-blue; + font-style: italic; + } + .rp-comment-resolver-content { + margin: 0; + } .rp-comment-reply { padding: 0 5px; - - .rp-state-overview & { - padding: 3px 0 0; - } } .rp-add-comment-btn { @@ -396,6 +407,26 @@ } } +.rp-resolved-comment { + border-left: solid @rp-entry-ribbon-width @rp-yellow; + border-radius: 3px; + background-color: #FFF; + margin-bottom: 5px; +} + .rp-resolved-comment-context { + background-color: lighten(@rp-yellow, 35%); + padding: 4px 5px; + } + .rp-resolved-comment-context-file { + font-weight: @rp-semibold-weight; + } + + .rp-resolved-comment-context-quote { + color: #000; + font-family: @font-family-monospace; + margin: 0; + } + .rp-entry-callout { .rp-state-current-file & { position: absolute; @@ -653,19 +684,19 @@ } } -.resolved-comments-backdrop { - display: none; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: rgba(0, 0, 0, .5); +// .resolved-comments-backdrop { +// display: none; +// position: fixed; +// top: 0; +// right: 0; +// bottom: 0; +// left: 0; +// background-color: rgba(0, 0, 0, .5); - &-visible { - display: block; - } -} +// &-visible { +// display: block; +// } +// } .resolved-comments-dropdown { display: none; @@ -673,7 +704,7 @@ width: 300px; background-color: @rp-bg-blue; text-align: left; - padding: 5px; + padding: 5px 5px 0; border-radius: 3px; &-open { From 037b61cbba7d29c577c88a7b7ff9b3db096638be Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 6 Jan 2017 17:22:22 +0000 Subject: [PATCH 20/98] Further styling. --- .../stylesheets/app/editor/review-panel.less | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 16a52fbabe..204191f418 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -365,9 +365,9 @@ .rp-comment-resolver { color: @rp-type-blue; - font-style: italic; } .rp-comment-resolver-content { + font-style: italic; margin: 0; } @@ -702,10 +702,22 @@ display: none; position: absolute; width: 300px; + left: -150px; + margin-top: @rp-entry-arrow-width * 1.5; + margin-left: 1em; background-color: @rp-bg-blue; text-align: left; padding: 5px 5px 0; border-radius: 3px; + box-shadow: 0 0 20px 10px rgba(0, 0, 0, .3); + + &::before { + content: ''; + .triangle(top, @rp-entry-arrow-width * 3, @rp-entry-arrow-width * 1.5, @rp-bg-blue); + top: -@rp-entry-ribbon-width * 2; + left: 50%; + margin-left: -@rp-entry-arrow-width * .75; + } &-open { display: block; From ae30f32481ca7ebbeeed34c260409aa3cec41a3a Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 9 Jan 2017 10:49:03 +0100 Subject: [PATCH 21/98] Use deterministic ids based on a seed --- .../public/coffee/ide/editor/Document.coffee | 7 ++++ .../coffee/ide/editor/EditorManager.coffee | 3 -- .../coffee/ide/editor/ShareJsDoc.coffee | 6 ++-- .../editor/sharejs/vendor/client/doc.coffee | 2 ++ .../ide/review-panel/RangesTracker.coffee | 34 ++++++++----------- .../controllers/ReviewPanelController.coffee | 21 +++++++++++- 6 files changed, 47 insertions(+), 26 deletions(-) diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index 229e281dd8..6984479b21 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -83,6 +83,9 @@ define [ setTrackingChanges: (track_changes) -> @doc.track_changes = track_changes + + setTrackChangesIdSeeds: (id_seeds) -> + @doc.track_changes_id_seeds = id_seeds _bindToSocketEvents: () -> @_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update) @@ -319,6 +322,8 @@ define [ v: version @doc.on "change", (ops, oldSnapshot, msg) => @_applyOpsToRanges(ops, oldSnapshot, msg) + @doc.on "flipped_pending_to_inflight", () => + @trigger "flipped_pending_to_inflight" _onError: (error, meta = {}) -> meta.doc_id = @doc_id @@ -335,6 +340,8 @@ define [ _applyOpsToRanges: (ops = [], oldSnapshot, msg) -> track_changes_as = null remote_op = msg? + if msg.meta?.tc? + @ranges.setIdSeed(msg.meta.tc) if remote_op and msg.meta?.tc track_changes_as = msg.meta.user_id else if !remote_op and @track_changes_as? diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index b7fccc29af..629e1a4cb3 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -166,14 +166,11 @@ define [ if want == have return - console.log "Trying to set track changes to:", want do tryToggle = () => saved = !doc.getInflightOp()? and !doc.getPendingOp()? if saved - console.log "SUCCESS, changing value", want doc.setTrackingChanges(want) @$scope.$apply () => @$scope.editor.trackChanges = want else - console.log "Still in flight, will try soon" @_syncTimeout = setTimeout tryToggle, 100 diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee index 48a7bbf3c6..f580c56f77 100644 --- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee +++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee @@ -24,7 +24,7 @@ define [ return if @track_changes update.meta ?= {} - update.meta.tc = 1 + update.meta.tc = @track_changes_id_seeds.inflight @socket.emit "applyOtUpdate", @doc_id, update, (error) => return @_handleError(error) if error? state: "ok" @@ -44,6 +44,8 @@ define [ # ops as quickly as possible for low latency. @_doc.setFlushDelay(0) @trigger "remoteop", args... + @_doc.on "flipped_pending_to_inflight", () => + @trigger "flipped_pending_to_inflight" @_doc.on "error", (e) => @_handleError(e) @@ -117,7 +119,7 @@ define [ attachToAce: (ace) -> @_doc.attach_ace(ace, false, window.maxDocLength) detachFromAce: () -> @_doc.detach_ace?() - + INFLIGHT_OP_TIMEOUT: 5000 # Retry sending ops after 5 seconds without an ack WAIT_FOR_CONNECTION_TIMEOUT: 500 # If we're waiting for the project to join, try again in 0.5 seconds _startInflightOpTimeout: (update) -> diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee index 301c4d5e04..aca3560b49 100644 --- a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee +++ b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee @@ -266,6 +266,8 @@ class Doc @pendingOp = null @pendingCallbacks = [] + @emit "flipped_pending_to_inflight" + #console.log "SENDING OP TO SERVER", @inflightOp, @version @connection.send {doc:@name, op:@inflightOp, v:@version} diff --git a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee index 6ff301e803..36ef621493 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -36,24 +36,18 @@ load = (EventEmitter) -> # middle of a previous insert by the first user, the original insert will be split into two. constructor: (@changes = [], @comments = []) -> - @_increment: 0 - @newId: () -> - # Generate a Mongo ObjectId - # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js - @_pid ?= Math.floor(Math.random() * (32767)) - @_machine ?= Math.floor(Math.random() * (16777216)) - timestamp = Math.floor(new Date().valueOf() / 1000) - @_increment++ - - timestamp = timestamp.toString(16) - machine = @_machine.toString(16) - pid = @_pid.toString(16) - increment = @_increment.toString(16) - - return '00000000'.substr(0, 8 - timestamp.length) + timestamp + - '000000'.substr(0, 6 - machine.length) + machine + - '0000'.substr(0, 4 - pid.length) + pid + - '000000'.substr(0, 6 - increment.length) + increment; + getIdSeed: () -> + return @id_seed + + setIdSeed: (seed) -> + @id_seed = seed + @id_increment = 0 + + newId: () -> + @id_increment++ + increment = @id_increment.toString(16) + id = @id_seed + '000000'.substr(0, 6 - increment.length) + increment; + return id getComment: (comment_id) -> comment = null @@ -99,7 +93,7 @@ load = (EventEmitter) -> addComment: (op, metadata) -> # TODO: Don't allow overlapping comments? @comments.push comment = { - id: RangesTracker.newId() + id: @newId() op: # Copy because we'll modify in place c: op.c p: op.p @@ -390,7 +384,7 @@ load = (EventEmitter) -> _addOp: (op, metadata) -> change = { - id: RangesTracker.newId() + id: @newId() op: op metadata: metadata } diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 103f566617..4a5c02ba01 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -75,12 +75,17 @@ define [ if view == $scope.SubViews.OVERVIEW refreshOverviewPanel() - $scope.$watch "editor.sharejs_doc", (doc) -> + $scope.$watch "editor.sharejs_doc", (doc, old_doc) -> return if !doc? # The open doc range tracker is kept up to date in real-time so # replace any outdated info with this rangesTrackers[doc.doc_id] = doc.ranges $scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id] + if old_doc? + old_doc.off "flipped_pending_to_inflight" + doc.on "flipped_pending_to_inflight", () -> + regenerateTrackChangesId(doc) + regenerateTrackChangesId(doc) $scope.$watch (() -> entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {} @@ -94,6 +99,20 @@ define [ $scope.$broadcast "review-panel:toggle" $scope.$broadcast "review-panel:layout" + regenerateTrackChangesId = (doc) -> + old_id = getChangeTracker(doc.doc_id).getIdSeed() + # Generate a the first 18 characters of Mongo ObjectId, leaving 6 for the increment part + # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js + pid = Math.floor(Math.random() * (32767)).toString(16) + machine = Math.floor(Math.random() * (16777216)).toString(16) + timestamp = Math.floor(new Date().valueOf() / 1000).toString(16) + new_id = '00000000'.substr(0, 8 - timestamp.length) + timestamp + + '000000'.substr(0, 6 - machine.length) + machine + + '0000'.substr(0, 4 - pid.length) + pid + + getChangeTracker(doc.doc_id).setIdSeed(new_id) + doc.setTrackChangesIdSeeds({pending: new_id, inflight: old_id}) + refreshOverviewPanel = () -> $scope.reviewPanel.overview.loading = true $http.get "/project/#{$scope.project_id}/ranges" From 14c624614d695c9e71661c51a55d0158638568fb Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 9 Jan 2017 10:51:55 +0100 Subject: [PATCH 22/98] Update RangesTracker --- services/web/public/coffee/ide/review-panel/RangesTracker.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee index 36ef621493..09d471d476 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -35,6 +35,7 @@ load = (EventEmitter) -> # * Inserts by another user will not combine with inserts by the first user. If they are in the # middle of a previous insert by the first user, the original insert will be split into two. constructor: (@changes = [], @comments = []) -> + @setIdSeed("") getIdSeed: () -> return @id_seed From 9379cff89d3f5dff680478c12f4d8b1776f9eab8 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 9 Jan 2017 15:25:27 +0100 Subject: [PATCH 23/98] Add end point for accepting change in doc updater --- .../DocumentUpdaterHandler.coffee | 24 +++++++---- .../Features/Ranges/RangesController.coffee | 8 ++++ services/web/app/coffee/router.coffee | 1 + .../public/coffee/ide/editor/Document.coffee | 2 +- .../controllers/ReviewPanelController.coffee | 3 +- .../DocumentUpdaterHandlerTests.coffee | 38 +++++++++++++++- .../GetNumberOfDocsInMemoryTests.coffee | 43 ------------------- 7 files changed, 63 insertions(+), 56 deletions(-) delete mode 100644 services/web/test/UnitTests/coffee/DocumentUpdater/GetNumberOfDocsInMemoryTests.coffee diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee index 9b3364f8ad..bb4922704f 100644 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -137,15 +137,21 @@ module.exports = DocumentUpdaterHandler = logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}" callback new Error("doc updater returned a non-success status code: #{res.statusCode}") - getNumberOfDocsInMemory : (callback)-> - request.get "#{settings.apis.documentupdater.url}/total", (err, req, body)-> - try - body = JSON.parse body - catch err - logger.err err:err, "error parsing response from doc updater about the total number of docs" - callback(err, body?.total) - - + acceptChange: (project_id, doc_id, change_id, callback = (error) ->) -> + timer = new metrics.Timer("accept-change") + url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept" + logger.log {project_id, doc_id, change_id}, "accepting change in document updater" + request.post url, (error, res, body)-> + timer.done() + if error? + logger.error {err:error, project_id, doc_id, change_id}, "error accepting change in doc updater" + return callback(error) + if res.statusCode >= 200 and res.statusCode < 300 + logger.log {project_id, doc_id, change_id}, "accepted change in document updater" + return callback(null) + else + logger.error {project_id, doc_id, change_id}, "doc updater returned a non-success status code: #{res.statusCode}" + callback new Error("doc updater returned a non-success status code: #{res.statusCode}") PENDINGUPDATESKEY = "PendingUpdates" DOCLINESKEY = "doclines" diff --git a/services/web/app/coffee/Features/Ranges/RangesController.coffee b/services/web/app/coffee/Features/Ranges/RangesController.coffee index 78457c2e64..0c740cf892 100644 --- a/services/web/app/coffee/Features/Ranges/RangesController.coffee +++ b/services/web/app/coffee/Features/Ranges/RangesController.coffee @@ -1,6 +1,7 @@ RangesManager = require "./RangesManager" logger = require "logger-sharelatex" UserInfoController = require "../User/UserInfoController" +DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" module.exports = RangesController = getAllRanges: (req, res, next) -> @@ -18,3 +19,10 @@ module.exports = RangesController = return next(error) if error? users = (UserInfoController.formatPersonalInfo(user) for user in users) res.json users + + acceptChange: (req, res, next) -> + {project_id, doc_id, change_id} = req.params + logger.log {project_id, doc_id, change_id}, "request to accept change" + DocumentUpdaterHandler.acceptChange project_id, doc_id, change_id, (error) -> + return next(error) if error? + res.send 204 diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 1332a2d7e5..1d032c74ac 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -179,6 +179,7 @@ module.exports = class Router webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, RangesController.getAllRanges webRouter.get "/project/:project_id/ranges/users", AuthorizationMiddlewear.ensureUserCanReadProject, RangesController.getAllRangesUsers + webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, RangesController.acceptChange webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index 6984479b21..d037aeb165 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -340,7 +340,7 @@ define [ _applyOpsToRanges: (ops = [], oldSnapshot, msg) -> track_changes_as = null remote_op = msg? - if msg.meta?.tc? + if msg?.meta?.tc? @ranges.setIdSeed(msg.meta.tc) if remote_op and msg.meta?.tc track_changes_as = msg.meta.user_id diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 70edf939ad..5fdc14f5e5 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -200,8 +200,9 @@ define [ $scope.$broadcast "review-panel:recalculate-screen-positions" $scope.$broadcast "review-panel:layout" - + $scope.acceptChange = (entry_id) -> + $http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/#{entry_id}/accept", {_csrf: window.csrfToken} $scope.$broadcast "change:accept", entry_id $scope.rejectChange = (entry_id) -> diff --git a/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee index eca005f295..3bde5e991a 100644 --- a/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee +++ b/services/web/test/UnitTests/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee @@ -8,8 +8,7 @@ path = require 'path' _ = require 'underscore' modulePath = path.join __dirname, '../../../../app/js/Features/DocumentUpdater/DocumentUpdaterHandler' -describe 'DocumentUpdaterHandler - Flushing documents :', -> - +describe 'DocumentUpdaterHandler', -> beforeEach -> @project_id = "project-id-923" @doc_id = "doc-id-394" @@ -296,3 +295,38 @@ describe 'DocumentUpdaterHandler - Flushing documents :', -> @callback .calledWith(new Error("doc updater returned failure status code: 500")) .should.equal true + + describe "acceptChange", -> + beforeEach -> + @change_id = "mock-change-id-1" + @callback = sinon.stub() + + describe "successfully", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, {statusCode: 200}, @body) + @handler.acceptChange @project_id, @doc_id, @change_id, @callback + + it 'should accept the change in the document updater', -> + url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}/doc/#{@doc_id}/change/#{@change_id}/accept" + @request.post.calledWith(url).should.equal true + + it "should call the callback", -> + @callback.calledWith(null).should.equal true + + describe "when the document updater API returns an error", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, @error = new Error("something went wrong"), null, null) + @handler.acceptChange @project_id, @doc_id, @change_id, @callback + + it "should return an error to the callback", -> + @callback.calledWith(@error).should.equal true + + describe "when the document updater returns a failure error code", -> + beforeEach -> + @request.post = sinon.stub().callsArgWith(1, null, { statusCode: 500 }, "") + @handler.acceptChange @project_id, @doc_id, @change_id, @callback + + it "should return the callback with an error", -> + @callback + .calledWith(new Error("doc updater returned failure status code: 500")) + .should.equal true diff --git a/services/web/test/UnitTests/coffee/DocumentUpdater/GetNumberOfDocsInMemoryTests.coffee b/services/web/test/UnitTests/coffee/DocumentUpdater/GetNumberOfDocsInMemoryTests.coffee deleted file mode 100644 index 213fd2257b..0000000000 --- a/services/web/test/UnitTests/coffee/DocumentUpdater/GetNumberOfDocsInMemoryTests.coffee +++ /dev/null @@ -1,43 +0,0 @@ -path = require("path") -sinon = require("sinon") -SandboxedModule = require('sandboxed-module') - -modulePath = path.join __dirname, '../../../../app/js/Features/DocumentUpdater/DocumentUpdaterHandler' - -describe "getNumberOfDocsInMemory", -> - beforeEach -> - @host = "doc.updater" - @noOfDocs = 42 - @callback = sinon.stub() - @DocumentUpdateHandler = SandboxedModule.require modulePath, requires: - "redis-sharelatex" : - createClient: () -> - auth:-> - "soa-req-id": null - "logger-sharelatex": @logger = - log: sinon.stub() - error: sinon.stub() - "../../infrastructure/Metrics" : @metrics - "../../Features/Project/ProjectLocator": @ProjectLocator = {} - "../../models/Project":Project:{} - "request" : defaults: () => @request = {} - "settings-sharelatex": - apis: documentupdater: url: @host - redis: web:{} - - - @request.get = sinon.stub().callsArgWith(1, null, {statusCode: 200}, JSON.stringify(total: @noOfDocs)) - @DocumentUpdateHandler.getNumberOfDocsInMemory @callback - - it "should call the doc updater", -> - @request.get - .calledWith("#{@host}/total") - .should.equal true - - it "should return the number of docs", -> - @callback - .calledWith(null, @noOfDocs) - .should.equal true - - - From 4871d56725dc75a2e50360e370c517bb2ba3076e Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 9 Jan 2017 14:30:29 +0000 Subject: [PATCH 24/98] Isolate overview panel overrides. --- .../review-panel/controllers/ReviewPanelController.coffee | 7 ++----- .../web/public/stylesheets/app/editor/review-panel.less | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 70edf939ad..b311a5e0f1 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -55,11 +55,7 @@ define [ $reviewPanelEl.css "right", "#{ scrollbar.scrollbarWidth }px" else $reviewPanelEl.css "right", "0" - - $scope.$watch "reviewPanel.subView", (subView) -> - return if !subView? - updateScrollbar() - + $scope.$watch "ui.reviewPanelOpen", (open) -> return if !open? if !open @@ -72,6 +68,7 @@ define [ $scope.$watch "reviewPanel.subView", (view) -> return if !view? + updateScrollbar() if view == $scope.SubViews.OVERVIEW refreshOverviewPanel() diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 204191f418..6219ec2e59 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -137,7 +137,6 @@ bottom: 0; } - .rp-state-overview & { flex-grow: 2; overflow-y: auto; @@ -314,7 +313,7 @@ .rp-entry-actions { display: flex; - .rp-state-overview & { + .rp-entry-list .rp-state-overview & { display: none; } } @@ -336,14 +335,12 @@ line-height: 1.4; border-bottom: solid 1px @rp-border-grey; - - &:last-child { margin-bottom: 2px; border-bottom-width: 0; } - .rp-state-overview & { + .rp-entry-list .rp-state-overview & { margin: 4px 5px; &:first-child { From 7e33d1a24e15b0b27c03bb4831a981ccbf4e389d Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 9 Jan 2017 15:30:48 +0100 Subject: [PATCH 25/98] Fix generation of thread id --- .../controllers/ReviewPanelController.coffee | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 5fdc14f5e5..6fbbc6139d 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -99,17 +99,22 @@ define [ $scope.$broadcast "review-panel:toggle" $scope.$broadcast "review-panel:layout" - regenerateTrackChangesId = (doc) -> - old_id = getChangeTracker(doc.doc_id).getIdSeed() + generatePartialMongoId = () -> # Generate a the first 18 characters of Mongo ObjectId, leaving 6 for the increment part # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js pid = Math.floor(Math.random() * (32767)).toString(16) machine = Math.floor(Math.random() * (16777216)).toString(16) timestamp = Math.floor(new Date().valueOf() / 1000).toString(16) - new_id = '00000000'.substr(0, 8 - timestamp.length) + timestamp + + return '00000000'.substr(0, 8 - timestamp.length) + timestamp + '000000'.substr(0, 6 - machine.length) + machine + '0000'.substr(0, 4 - pid.length) + pid - + + generateFullMongoId = () -> + return generatePartialMongoId() + "000000" + + regenerateTrackChangesId = (doc) -> + old_id = getChangeTracker(doc.doc_id).getIdSeed() + new_id = generatePartialMongoId() getChangeTracker(doc.doc_id).setIdSeed(new_id) doc.setTrackChangesIdSeeds({pending: new_id, inflight: old_id}) @@ -214,7 +219,7 @@ define [ $scope.$broadcast "review-panel:layout" $scope.submitNewComment = (content) -> - thread_id = RangesTracker.newId() + thread_id = generateFullMongoId() $scope.$broadcast "comment:add", thread_id $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) .error (error) -> From 43f6b9de7c4e6b5d751cff82a23b5ec788371ce1 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 9 Jan 2017 15:54:12 +0100 Subject: [PATCH 26/98] Inform other clients when we accept a change --- .../Features/Ranges/RangesController.coffee | 2 ++ .../controllers/ReviewPanelController.coffee | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/Ranges/RangesController.coffee b/services/web/app/coffee/Features/Ranges/RangesController.coffee index 0c740cf892..4b2c1449a3 100644 --- a/services/web/app/coffee/Features/Ranges/RangesController.coffee +++ b/services/web/app/coffee/Features/Ranges/RangesController.coffee @@ -2,6 +2,7 @@ RangesManager = require "./RangesManager" logger = require "logger-sharelatex" UserInfoController = require "../User/UserInfoController" DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" +EditorRealTimeController = require("../Editor/EditorRealTimeController") module.exports = RangesController = getAllRanges: (req, res, next) -> @@ -25,4 +26,5 @@ module.exports = RangesController = logger.log {project_id, doc_id, change_id}, "request to accept change" DocumentUpdaterHandler.acceptChange project_id, doc_id, change_id, (error) -> return next(error) if error? + EditorRealTimeController.emitToRoom project_id, "accept-change", doc_id, change_id, (err)-> res.send 204 diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 6fbbc6139d..6793c1fc81 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -34,6 +34,15 @@ define [ $scope.$apply() $timeout () -> $scope.$broadcast "review-panel:layout" + + ide.socket.on "accept-change", (doc_id, change_id) -> + console.log "Got remote accept change", doc_id, change_id + if doc_id != $scope.editor.open_doc_id + getChangeTracker(doc_id).removeChangeId(change_id) + else + $scope.$broadcast "change:accept", change_id + updateEntries(doc_id) + $scope.$apply () -> rangesTrackers = {} @@ -124,13 +133,12 @@ define [ .success (docs) -> for doc in docs if doc.id != $scope.editor.open_doc_id # this is kept up to date in real-time, don't overwrite - rangesTrackers[doc.id] ?= new RangesTracker() - rangesTrackers[doc.id].comments = doc.ranges?.comments or [] - rangesTrackers[doc.id].changes = doc.ranges?.changes or [] + rangesTracker = getChangeTracker(doc.id) + rangesTracker.comments = doc.ranges?.comments or [] + rangesTracker.changes = doc.ranges?.changes or [] updateEntries(doc.id) $scope.reviewPanel.overview.loading = false .error (error) -> - console.log "loading ranges errored", error $scope.reviewPanel.overview.loading = false updateEntries = (doc_id) -> From 037389b7dd071185625c1f6d4afc20c540061822 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 9 Jan 2017 14:59:01 +0000 Subject: [PATCH 27/98] Decouple ranges update from the overview panel; integrate it with the dropdown. --- .../views/project/editor/review-panel.jade | 9 +++++-- .../controllers/ReviewPanelController.coffee | 25 ++++++++++++++++--- .../resolvedCommentsDropdown.coffee | 15 ++++++++--- .../stylesheets/app/editor/review-panel.less | 9 +++++-- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index e8bddadf49..40c54b6888 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -4,6 +4,8 @@ entries="reviewPanel.entries" threads="reviewPanel.commentThreads" docs="docs" + on-open="refreshResolvedCommentsDropdown();" + is-loading="reviewPanel.dropdown.loading" ) span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = true;", ng-if="editor.wantTrackChanges === false") Track Changes is strong off @@ -50,7 +52,7 @@ .rp-entry-list( ng-if="reviewPanel.subView === SubViews.OVERVIEW" ) - .rp-overview-loading(ng-if="reviewPanel.overview.loading") + .rp-loading(ng-if="reviewPanel.overview.loading") i.fa.fa-spinner.fa-spin .rp-overview-file( ng-repeat="doc in docs" @@ -249,13 +251,16 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') //- ) a.resolved-comments-toggle( href - ng-click="state.isOpen = !state.isOpen" + ng-click="toggleOpenState();" ) i.fa.fa-inbox .resolved-comments-dropdown( ng-class="{ 'resolved-comments-dropdown-open' : state.isOpen }" ) + .rp-loading(ng-if="isLoading") + i.fa.fa-spinner.fa-spin div( + ng-if="!isLoading" ng-repeat="doc in docs" ) resolved-comment-entry( diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index f7aa4ae19c..87d4b2eeb1 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -18,6 +18,8 @@ define [ openSubView: $scope.SubViews.CUR_FILE overview: loading: false + dropdown: + loading: false commentThreads: {} $scope.commentState = @@ -115,8 +117,7 @@ define [ getChangeTracker(doc.doc_id).setIdSeed(new_id) doc.setTrackChangesIdSeeds({pending: new_id, inflight: old_id}) - refreshOverviewPanel = () -> - $scope.reviewPanel.overview.loading = true + refreshRanges = () -> $http.get "/project/#{$scope.project_id}/ranges" .success (docs) -> for doc in docs @@ -125,11 +126,27 @@ define [ rangesTrackers[doc.id].comments = doc.ranges?.comments or [] rangesTrackers[doc.id].changes = doc.ranges?.changes or [] updateEntries(doc.id) - $scope.reviewPanel.overview.loading = false + # $scope.reviewPanel.overview.loading = false .error (error) -> console.log "loading ranges errored", error + # $scope.reviewPanel.overview.loading = false + + refreshOverviewPanel = () -> + $scope.reviewPanel.overview.loading = true + refreshRanges() + .then () -> $scope.reviewPanel.overview.loading = false - + .catch () -> + $scope.reviewPanel.overview.loading = false + + $scope.refreshResolvedCommentsDropdown = () -> + $scope.reviewPanel.dropdown.loading = true + refreshRanges() + .then () -> + $scope.reviewPanel.dropdown.loading = false + .catch () -> + $scope.reviewPanel.dropdown.loading = false + updateEntries = (doc_id) -> rangesTracker = getChangeTracker(doc_id) entries = getDocEntries(doc_id) diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index 7f9457bcdf..b909948d55 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -5,13 +5,22 @@ define [ restrict: "E" templateUrl: "resolvedCommentsDropdownTemplate" scope: - entries : "=" - threads : "=" - docs : "=" + entries : "=" + threads : "=" + docs : "=" + onOpen : "&" + isLoading : "=" + link: (scope, element, attrs) -> scope.state = isOpen: false + scope.toggleOpenState = () -> + scope.state.isOpen = !scope.state.isOpen + if (scope.state.isOpen) + console.log('will call stuff') + scope.onOpen() + scope.resolvedCommentsPerFile = {} filterResolvedComments = () -> diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 6219ec2e59..2d02361b54 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -313,7 +313,7 @@ .rp-entry-actions { display: flex; - .rp-entry-list .rp-state-overview & { + .rp-state-overview .rp-entry-list & { display: none; } } @@ -340,7 +340,7 @@ border-bottom-width: 0; } - .rp-entry-list .rp-state-overview & { + .rp-state-overview .rp-entry-list & { margin: 4px 5px; &:first-child { @@ -498,6 +498,11 @@ text-align: center; } +.rp-loading { + text-align: center; + padding: 5px; +} + .rp-nav { display: flex; flex-shrink: 0; From dbe5331566dd4280f16c6a22289550b71e9a8c58 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 9 Jan 2017 16:09:29 +0100 Subject: [PATCH 28/98] Tell other clients when comments are resolved and reopened --- .../controllers/ReviewPanelController.coffee | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 6793c1fc81..4bc7266feb 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -43,6 +43,12 @@ define [ $scope.$broadcast "change:accept", change_id updateEntries(doc_id) $scope.$apply () -> + + ide.socket.on "resolve-thread", (thread_id, user) -> + _onCommentResolved(thread_id, user) + + ide.socket.on "reopen-thread", (thread_id) -> + _onCommentReopened(thread_id) rangesTrackers = {} @@ -265,20 +271,26 @@ define [ $scope.resolveComment = (entry, entry_id) -> entry.showWhenResolved = false entry.focused = false - thread = $scope.reviewPanel.commentThreads[entry.thread_id] - thread.resolved = true - thread.resolved_by_user = formatUser(ide.$scope.user) - thread.resolved_at = new Date() $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken} - $scope.$broadcast "comment:resolve_thread", entry.thread_id - + _onCommentResolved(entry.thread_id, ide.$scope.user) + $scope.unresolveComment = (entry, entry_id) -> - thread = $scope.reviewPanel.commentThreads[entry.thread_id] + _onCommentReopened(entry.thread_id) + $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/reopen", {_csrf: window.csrfToken} + + _onCommentResolved = (thread_id, user) -> + thread = $scope.reviewPanel.commentThreads[thread_id] + thread.resolved = true + thread.resolved_by_user = formatUser(user) + thread.resolved_at = new Date() + $scope.$broadcast "comment:resolve_thread", thread_id + + _onCommentReopened = (thread_id) -> + thread = $scope.reviewPanel.commentThreads[thread_id] delete thread.resolved delete thread.resolved_by_user delete thread.resolved_at - $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/reopen", {_csrf: window.csrfToken} - $scope.$broadcast "comment:unresolve_thread", entry.thread_id + $scope.$broadcast "comment:unresolve_thread", thread_id $scope.deleteComment = (entry_id) -> $scope.$broadcast "comment:remove", entry_id From 1a100b77df85ca4e53de60eec9cbf7fa54582931 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 9 Jan 2017 15:15:03 +0000 Subject: [PATCH 29/98] Hide resolved comments from the review panel. --- services/web/app/views/project/editor/review-panel.jade | 4 ++-- .../review-panel/directives/resolvedCommentsDropdown.coffee | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 40c54b6888..91f0b762f7 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -32,7 +32,7 @@ on-indicator-click="toggleReviewPanel();" ) - div(ng-if="entry.type === 'comment'") + div(ng-if="entry.type === 'comment' && !(reviewPanel.commentThreads[entry.thread_id].resolved === true)") comment-entry( entry="entry" threads="reviewPanel.commentThreads" @@ -73,7 +73,7 @@ ng-click="gotoEntry(doc_id, entry)" ) - div(ng-if="entry.type === 'comment'") + div(ng-if="entry.type === 'comment' && !(reviewPanel.commentThreads[entry.thread_id].resolved === true)") comment-entry( entry="entry" threads="reviewPanel.commentThreads" diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index b909948d55..df3299de51 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -18,7 +18,6 @@ define [ scope.toggleOpenState = () -> scope.state.isOpen = !scope.state.isOpen if (scope.state.isOpen) - console.log('will call stuff') scope.onOpen() scope.resolvedCommentsPerFile = {} From 4e128b6ab7e008eefdb2821edd06d2c442af6a3c Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 9 Jan 2017 17:25:06 +0100 Subject: [PATCH 30/98] Send user data in /threads dat --- .../coffee/Features/Chat/ChatController.coffee | 2 +- .../Features/Comments/CommentsController.coffee | 2 +- .../Comments/CommentsControllerTests.coffee | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/services/web/app/coffee/Features/Chat/ChatController.coffee b/services/web/app/coffee/Features/Chat/ChatController.coffee index 7e4e190290..3090f4f108 100644 --- a/services/web/app/coffee/Features/Chat/ChatController.coffee +++ b/services/web/app/coffee/Features/Chat/ChatController.coffee @@ -28,7 +28,7 @@ module.exports = logger.log project_id:project_id, query:query, "getting messages" ChatApiHandler.getGlobalMessages project_id, query.limit, query.before, (err, messages) -> return next(err) if err? - CommentsController._injectUserInfoIntoThreads [{ messages: messages }], (err) -> + CommentsController._injectUserInfoIntoThreads {global: { messages: messages }}, (err) -> return next(err) if err? logger.log length: messages?.length, "sending messages to client" res.json messages diff --git a/services/web/app/coffee/Features/Comments/CommentsController.coffee b/services/web/app/coffee/Features/Comments/CommentsController.coffee index 20346dccbb..ee9b8b9f84 100644 --- a/services/web/app/coffee/Features/Comments/CommentsController.coffee +++ b/services/web/app/coffee/Features/Comments/CommentsController.coffee @@ -62,7 +62,7 @@ module.exports = CommentsController = callback null, user jobs = [] - for thread in threads + for thread_id, thread of threads do (thread) -> if thread.resolved jobs.push (cb) -> diff --git a/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee index ecf57abd2e..cbc24bca1f 100644 --- a/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Comments/CommentsControllerTests.coffee @@ -151,8 +151,8 @@ describe "CommentsController", -> return { "formatted": user["mock"] } it "should inject a user object into messaged and resolved data", (done) -> - @CommentsController._injectUserInfoIntoThreads [ - { + @CommentsController._injectUserInfoIntoThreads { + thread1: { resolved: true resolved_by_user_id: "user_id_1" messages: [{ @@ -163,15 +163,15 @@ describe "CommentsController", -> content: "bar" }] }, - { + thread2: { messages: [{ user_id: "user_id_1" content: "baz" }] } - ], (error, threads) -> - expect(threads).to.deep.equal [ - { + }, (error, threads) -> + expect(threads).to.deep.equal { + thread1: { resolved: true resolved_by_user_id: "user_id_1" resolved_by_user: { "formatted": "user_1" } @@ -185,14 +185,14 @@ describe "CommentsController", -> content: "bar" }] }, - { + thread2: { messages: [{ user_id: "user_id_1" user: { "formatted": "user_1" } content: "baz" }] } - ] + } done() it "should only need to look up each user once", (done) -> From 70134d44d06839f08f1dc01d353ac59812e6bc5d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 9 Jan 2017 17:22:01 +0000 Subject: [PATCH 31/98] Add unresolve action to the dropdown. --- services/web/app/views/project/editor/review-panel.jade | 8 +++++++- .../review-panel/controllers/ReviewPanelController.coffee | 6 +++--- .../review-panel/directives/resolvedCommentEntry.coffee | 4 ++-- .../directives/resolvedCommentsDropdown.coffee | 6 ++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 91f0b762f7..886f99ffe3 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -5,6 +5,7 @@ threads="reviewPanel.commentThreads" docs="docs" on-open="refreshResolvedCommentsDropdown();" + on-unresolve="unresolveComment(threadId);" is-loading="reviewPanel.dropdown.loading" ) span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = true;", ng-if="editor.wantTrackChanges === false") Track Changes is @@ -201,7 +202,10 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') | {{ thread.resolved_at | date : 'MMM d, y h:mm a' }} .rp-entry-actions - a.rp-entry-button(href, ng-click="onUnresolve();") + a.rp-entry-button( + href + ng-click="onUnresolve({ 'threadId': threadId });" + ) |  Re-open a.rp-entry-button(href, ng-click="onDelete();") |  Delete @@ -265,7 +269,9 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') ) resolved-comment-entry( ng-repeat="thread in resolvedCommentsPerFile[doc.doc.id]" + thread-id="thread.threadId" thread="thread" doc="doc" + on-unresolve="handleUnresolve(threadId);" ) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 8de7822ecd..d84c0cb354 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -285,9 +285,9 @@ define [ $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken} _onCommentResolved(entry.thread_id, ide.$scope.user) - $scope.unresolveComment = (entry, entry_id) -> - _onCommentReopened(entry.thread_id) - $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/reopen", {_csrf: window.csrfToken} + $scope.unresolveComment = (thread_id) -> + _onCommentReopened(thread_id) + $http.post "/project/#{$scope.project_id}/thread/#{thread_id}/reopen", {_csrf: window.csrfToken} _onCommentResolved = (thread_id, user) -> thread = $scope.reviewPanel.commentThreads[thread_id] diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee index e3691c1962..28acad8b99 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee @@ -5,7 +5,7 @@ define [ restrict: "E" templateUrl: "resolvedCommentEntryTemplate" scope: + threadId: "=" thread: "=" doc: "=" - onReopen: "&" - onDelete: "&" \ No newline at end of file + onUnresolve: "&" \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index df3299de51..e0144f9d94 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -9,6 +9,7 @@ define [ threads : "=" docs : "=" onOpen : "&" + onUnresolve : "&" isLoading : "=" link: (scope, element, attrs) -> @@ -22,6 +23,10 @@ define [ scope.resolvedCommentsPerFile = {} + scope.handleUnresolve = (threadId) -> + scope.onUnresolve({ threadId }) + filterResolvedComments() + filterResolvedComments = () -> scope.resolvedCommentsPerFile = {} @@ -31,6 +36,7 @@ define [ if entry.type == "comment" and scope.threads[entry.thread_id]?.resolved? scope.resolvedCommentsPerFile[fileId][entryId] = angular.copy scope.threads[entry.thread_id] scope.resolvedCommentsPerFile[fileId][entryId].content = entry.content + scope.resolvedCommentsPerFile[fileId][entryId].threadId = entry.thread_id scope.$watchCollection "entries", filterResolvedComments scope.$watchCollection "threads", filterResolvedComments From 32fb3178206bfb2e8014e6acbdcb0d4ebdc6b9b7 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 10 Jan 2017 10:33:54 +0000 Subject: [PATCH 32/98] Add delete action to the dropdown. --- services/web/app/views/project/editor/review-panel.jade | 8 +++++++- .../review-panel/controllers/ReviewPanelController.coffee | 3 +++ .../review-panel/directives/resolvedCommentEntry.coffee | 6 ++++-- .../directives/resolvedCommentsDropdown.coffee | 8 ++++++++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 886f99ffe3..4ad558ef90 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -6,6 +6,7 @@ docs="docs" on-open="refreshResolvedCommentsDropdown();" on-unresolve="unresolveComment(threadId);" + on-delete="deleteComment(entryId);" is-loading="reviewPanel.dropdown.loading" ) span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = true;", ng-if="editor.wantTrackChanges === false") Track Changes is @@ -207,7 +208,10 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') ng-click="onUnresolve({ 'threadId': threadId });" ) |  Re-open - a.rp-entry-button(href, ng-click="onDelete();") + a.rp-entry-button( + href + ng-click="onDelete({ 'entryId': entryId });" + ) |  Delete @@ -269,9 +273,11 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') ) resolved-comment-entry( ng-repeat="thread in resolvedCommentsPerFile[doc.doc.id]" + entry-id="thread.entryId" thread-id="thread.threadId" thread="thread" doc="doc" on-unresolve="handleUnresolve(threadId);" + on-delete="handleDelete(entryId);" ) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index d84c0cb354..a42915e648 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -231,6 +231,9 @@ define [ $scope.$broadcast "review-panel:recalculate-screen-positions" $scope.$broadcast "review-panel:layout" + $scope.$on "comment:removed", (comment) -> + console.log comment + $scope.acceptChange = (entry_id) -> $http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/#{entry_id}/accept", {_csrf: window.csrfToken} $scope.$broadcast "change:accept", entry_id diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee index 28acad8b99..ad6e9f2099 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee @@ -4,8 +4,10 @@ define [ App.directive "resolvedCommentEntry", () -> restrict: "E" templateUrl: "resolvedCommentEntryTemplate" - scope: + scope: + entryId: "=" threadId: "=" thread: "=" doc: "=" - onUnresolve: "&" \ No newline at end of file + onUnresolve: "&" + onDelete: "&" \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index e0144f9d94..df7302d1d1 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -10,6 +10,7 @@ define [ docs : "=" onOpen : "&" onUnresolve : "&" + onDelete : "&" isLoading : "=" link: (scope, element, attrs) -> @@ -19,6 +20,7 @@ define [ scope.toggleOpenState = () -> scope.state.isOpen = !scope.state.isOpen if (scope.state.isOpen) + filterResolvedComments() scope.onOpen() scope.resolvedCommentsPerFile = {} @@ -27,6 +29,11 @@ define [ scope.onUnresolve({ threadId }) filterResolvedComments() + scope.handleDelete = (entryId) -> + scope.onDelete({ entryId }) + filterResolvedComments() + + filterResolvedComments = () -> scope.resolvedCommentsPerFile = {} @@ -37,6 +44,7 @@ define [ scope.resolvedCommentsPerFile[fileId][entryId] = angular.copy scope.threads[entry.thread_id] scope.resolvedCommentsPerFile[fileId][entryId].content = entry.content scope.resolvedCommentsPerFile[fileId][entryId].threadId = entry.thread_id + scope.resolvedCommentsPerFile[fileId][entryId].entryId = entryId scope.$watchCollection "entries", filterResolvedComments scope.$watchCollection "threads", filterResolvedComments From 12e1b2bc3dddb4f56b437931d3796b8768ba3085 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jan 2017 11:55:59 +0100 Subject: [PATCH 33/98] Don't show resolved comments highlighted in the text --- .../track-changes/TrackChangesManager.coffee | 5 +-- .../ide/review-panel/RangesTracker.coffee | 15 ++++++++- .../controllers/ReviewPanelController.coffee | 32 ++++++++----------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index 030aa8c252..836bcf7b9a 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -179,15 +179,12 @@ define [ removeCommentId: (comment_id) -> @rangesTracker.removeCommentId(comment_id) - RESOLVED_THREADS: {} resolveCommentByThreadId: (thread_id) -> - @RESOLVED_THREADS[thread_id] = true for comment in @rangesTracker?.comments or [] if comment.op.t == thread_id @_onCommentRemoved(comment) unresolveCommentByThreadId: (thread_id) -> - @RESOLVED_THREADS[thread_id] = false for comment in @rangesTracker?.comments or [] if comment.op.t == thread_id @_onCommentAdded(comment) @@ -332,7 +329,7 @@ define [ @broadcastChange() _onCommentAdded: (comment) -> - if @RESOLVED_THREADS[comment.op.t] + if @rangesTracker.resolvedThreadIds[comment.op.t] # Comment is resolved so shouldn't be displayed. return if !@changeIdToMarkerIdMap[comment.id]? diff --git a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee index 09d471d476..f2794f2c07 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -35,7 +35,7 @@ load = (EventEmitter) -> # * Inserts by another user will not combine with inserts by the first user. If they are in the # middle of a previous insert by the first user, the original insert will be split into two. constructor: (@changes = [], @comments = []) -> - @setIdSeed("") + @setIdSeed(RangesTracker.generateIdSeed()) getIdSeed: () -> return @id_seed @@ -43,6 +43,19 @@ load = (EventEmitter) -> setIdSeed: (seed) -> @id_seed = seed @id_increment = 0 + + @generateIdSeed: () -> + # Generate a the first 18 characters of Mongo ObjectId, leaving 6 for the increment part + # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js + pid = Math.floor(Math.random() * (32767)).toString(16) + machine = Math.floor(Math.random() * (16777216)).toString(16) + timestamp = Math.floor(new Date().valueOf() / 1000).toString(16) + return '00000000'.substr(0, 8 - timestamp.length) + timestamp + + '000000'.substr(0, 6 - machine.length) + machine + + '0000'.substr(0, 4 - pid.length) + pid + + @generateId: () -> + @generateId() + "000001" newId: () -> @id_increment++ diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index a42915e648..94e5d7809b 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -11,7 +11,7 @@ define [ CUR_FILE : "cur_file" OVERVIEW : "overview" - $scope.reviewPanel = + window.reviewPanel = $scope.reviewPanel = entries: {} hasEntries: false subView: $scope.SubViews.CUR_FILE @@ -53,13 +53,16 @@ define [ _onCommentReopened(thread_id) rangesTrackers = {} + resolvedThreadIds = {} getDocEntries = (doc_id) -> $scope.reviewPanel.entries[doc_id] ?= {} return $scope.reviewPanel.entries[doc_id] getChangeTracker = (doc_id) -> - rangesTrackers[doc_id] ?= new RangesTracker() + if !rangesTrackers[doc_id]? + rangesTrackers[doc_id] = new RangesTracker() + rangesTrackers[doc_id].resolvedThreadIds = resolvedThreadIds return rangesTrackers[doc_id] scrollbar = {} @@ -94,6 +97,7 @@ define [ # The open doc range tracker is kept up to date in real-time so # replace any outdated info with this rangesTrackers[doc.doc_id] = doc.ranges + rangesTrackers[doc.doc_id].resolvedThreadIds = resolvedThreadIds $scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id] if old_doc? old_doc.off "flipped_pending_to_inflight" @@ -112,23 +116,10 @@ define [ $timeout () -> $scope.$broadcast "review-panel:toggle" $scope.$broadcast "review-panel:layout" - - generatePartialMongoId = () -> - # Generate a the first 18 characters of Mongo ObjectId, leaving 6 for the increment part - # Reference: https://github.com/dreampulse/ObjectId.js/blob/master/src/main/javascript/Objectid.js - pid = Math.floor(Math.random() * (32767)).toString(16) - machine = Math.floor(Math.random() * (16777216)).toString(16) - timestamp = Math.floor(new Date().valueOf() / 1000).toString(16) - return '00000000'.substr(0, 8 - timestamp.length) + timestamp + - '000000'.substr(0, 6 - machine.length) + machine + - '0000'.substr(0, 4 - pid.length) + pid - - generateFullMongoId = () -> - return generatePartialMongoId() + "000000" - + regenerateTrackChangesId = (doc) -> old_id = getChangeTracker(doc.doc_id).getIdSeed() - new_id = generatePartialMongoId() + new_id = RangesTracker.generateIdSeed() getChangeTracker(doc.doc_id).setIdSeed(new_id) doc.setTrackChangesIdSeeds({pending: new_id, inflight: old_id}) @@ -247,7 +238,7 @@ define [ $scope.$broadcast "review-panel:layout" $scope.submitNewComment = (content) -> - thread_id = generateFullMongoId() + thread_id = RangesTracker.generateId() $scope.$broadcast "comment:add", thread_id $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) .error (error) -> @@ -297,6 +288,7 @@ define [ thread.resolved = true thread.resolved_by_user = formatUser(user) thread.resolved_at = new Date() + resolvedThreadIds[thread_id] = true $scope.$broadcast "comment:resolve_thread", thread_id _onCommentReopened = (thread_id) -> @@ -304,6 +296,7 @@ define [ delete thread.resolved delete thread.resolved_by_user delete thread.resolved_at + delete resolvedThreadIds[thread_id] $scope.$broadcast "comment:unresolve_thread", thread_id $scope.deleteComment = (entry_id) -> @@ -351,12 +344,15 @@ define [ refreshThreads = () -> $http.get "/project/#{$scope.project_id}/threads" .success (threads) -> + for thread_id, _ of resolvedThreadIds + delete resolvedThreadIds[thread_id] for thread_id, thread of threads for comment in thread.messages formatComment(comment) if thread.resolved_by_user? $scope.$broadcast "comment:resolve_thread", thread_id thread.resolved_by_user = formatUser(thread.resolved_by_user) + resolvedThreadIds[thread_id] = true $scope.reviewPanel.commentThreads = threads refreshThreads() From 7ae33041b5356f253de9e92d9026bd3806d7ad77 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jan 2017 11:58:55 +0100 Subject: [PATCH 34/98] Don't enter infinite loop generating id --- .../web/public/coffee/ide/review-panel/RangesTracker.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee index f2794f2c07..722eab1aa5 100644 --- a/services/web/public/coffee/ide/review-panel/RangesTracker.coffee +++ b/services/web/public/coffee/ide/review-panel/RangesTracker.coffee @@ -55,7 +55,7 @@ load = (EventEmitter) -> '0000'.substr(0, 4 - pid.length) + pid @generateId: () -> - @generateId() + "000001" + @generateIdSeed() + "000001" newId: () -> @id_increment++ From 072044ee90b9c231aa168c1ea75a29d96901d6c0 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 10 Jan 2017 11:23:06 +0000 Subject: [PATCH 35/98] Remove thread from view collection; some clean-up. --- .../views/project/editor/review-panel.jade | 6 ++--- .../controllers/ReviewPanelController.coffee | 23 ++++++------------- .../resolvedCommentsDropdown.coffee | 4 ++-- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 4ad558ef90..91c2b84c41 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -6,7 +6,7 @@ docs="docs" on-open="refreshResolvedCommentsDropdown();" on-unresolve="unresolveComment(threadId);" - on-delete="deleteComment(entryId);" + on-delete="deleteComment(entryId, threadId);" is-loading="reviewPanel.dropdown.loading" ) span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = true;", ng-if="editor.wantTrackChanges === false") Track Changes is @@ -210,7 +210,7 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') |  Re-open a.rp-entry-button( href - ng-click="onDelete({ 'entryId': entryId });" + ng-click="onDelete({ 'entryId': entryId, 'threadId': threadId });" ) |  Delete @@ -278,6 +278,6 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') thread="thread" doc="doc" on-unresolve="handleUnresolve(threadId);" - on-delete="handleDelete(entryId);" + on-delete="handleDelete(entryId, threadId);" ) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index a42915e648..52825ba5e8 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -66,7 +66,7 @@ define [ $scope.reviewPanelEventsBridge.on "aceScrollbarVisibilityChanged", (isVisible, scrollbarWidth) -> scrollbar = {isVisible, scrollbarWidth} updateScrollbar() - + updateScrollbar = () -> if scrollbar.isVisible and $scope.reviewPanel.subView == $scope.SubViews.CUR_FILE $reviewPanelEl.css "right", "#{ scrollbar.scrollbarWidth }px" @@ -231,9 +231,6 @@ define [ $scope.$broadcast "review-panel:recalculate-screen-positions" $scope.$broadcast "review-panel:layout" - $scope.$on "comment:removed", (comment) -> - console.log comment - $scope.acceptChange = (entry_id) -> $http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/#{entry_id}/accept", {_csrf: window.csrfToken} $scope.$broadcast "change:accept", entry_id @@ -283,7 +280,6 @@ define [ $scope.$broadcast "review-panel:layout" $scope.resolveComment = (entry, entry_id) -> - entry.showWhenResolved = false entry.focused = false $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken} _onCommentResolved(entry.thread_id, ide.$scope.user) @@ -305,20 +301,15 @@ define [ delete thread.resolved_by_user delete thread.resolved_at $scope.$broadcast "comment:unresolve_thread", thread_id + + _onCommentDeleted = (thread_id) -> + delete $scope.reviewPanel.commentThreads[thread_id] - $scope.deleteComment = (entry_id) -> + $scope.deleteComment = (entry_id, thread_id) -> + console.log thread_id + _onCommentDeleted(thread_id) $scope.$broadcast "comment:remove", entry_id - $scope.showThread = (entry) -> - entry.showWhenResolved = true - $timeout () -> - $scope.$broadcast "review-panel:layout" - - $scope.hideThread = (entry) -> - entry.showWhenResolved = false - $timeout () -> - $scope.$broadcast "review-panel:layout" - $scope.setSubView = (subView) -> $scope.reviewPanel.subView = subView diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index df7302d1d1..a440be0585 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -29,8 +29,8 @@ define [ scope.onUnresolve({ threadId }) filterResolvedComments() - scope.handleDelete = (entryId) -> - scope.onDelete({ entryId }) + scope.handleDelete = (entryId, threadId) -> + scope.onDelete({ entryId, threadId }) filterResolvedComments() From 5bd49c08f817b4537fdfd56154d986bfe1c02586 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 10 Jan 2017 12:16:58 +0000 Subject: [PATCH 36/98] Simpler resolved comments dropdown data structure. --- .../views/project/editor/review-panel.jade | 14 ++++----- .../directives/resolvedCommentEntry.coffee | 3 -- .../resolvedCommentsDropdown.coffee | 30 ++++++++++++------- .../stylesheets/app/editor/review-panel.less | 5 ++-- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 91c2b84c41..4ec1d24f6e 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -180,7 +180,7 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') div .rp-resolved-comment-context | Quoted text on  - span.rp-resolved-comment-context-file {{ doc.doc.name }} + span.rp-resolved-comment-context-file {{ thread.docName }} p.rp-resolved-comment-context-quote {{ thread.content }} .rp-comment( ng-repeat="comment in thread.messages track by comment.id" @@ -205,12 +205,12 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') .rp-entry-actions a.rp-entry-button( href - ng-click="onUnresolve({ 'threadId': threadId });" + ng-click="onUnresolve({ 'threadId': thread.threadId });" ) |  Re-open a.rp-entry-button( href - ng-click="onDelete({ 'entryId': entryId, 'threadId': threadId });" + ng-click="onDelete({ 'entryId': thread.entryId, 'threadId': thread.threadId });" ) |  Delete @@ -269,15 +269,13 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') i.fa.fa-spinner.fa-spin div( ng-if="!isLoading" - ng-repeat="doc in docs" ) resolved-comment-entry( - ng-repeat="thread in resolvedCommentsPerFile[doc.doc.id]" - entry-id="thread.entryId" - thread-id="thread.threadId" + ng-repeat="thread in resolvedComments" thread="thread" - doc="doc" on-unresolve="handleUnresolve(threadId);" on-delete="handleDelete(entryId, threadId);" ) + .rp-loading(ng-if="!resolvedComments.length") + | No resolved threads. diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee index ad6e9f2099..76da5b2913 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee @@ -5,9 +5,6 @@ define [ restrict: "E" templateUrl: "resolvedCommentEntryTemplate" scope: - entryId: "=" - threadId: "=" thread: "=" - doc: "=" onUnresolve: "&" onDelete: "&" \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index a440be0585..38a440dfa0 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.directive "resolvedCommentsDropdown", () -> + App.directive "resolvedCommentsDropdown", (_) -> restrict: "E" templateUrl: "resolvedCommentsDropdownTemplate" scope: @@ -23,7 +23,7 @@ define [ filterResolvedComments() scope.onOpen() - scope.resolvedCommentsPerFile = {} + scope.resolvedComments = [] scope.handleUnresolve = (threadId) -> scope.onUnresolve({ threadId }) @@ -33,18 +33,28 @@ define [ scope.onDelete({ entryId, threadId }) filterResolvedComments() + getDocNameById = (docId) -> + doc = _.find(scope.docs, (doc) -> doc.doc.id = docId) + if doc? + return doc.path + else + return null filterResolvedComments = () -> - scope.resolvedCommentsPerFile = {} + scope.resolvedComments = [] - for fileId, fileEntries of scope.entries - scope.resolvedCommentsPerFile[fileId] = {} - for entryId, entry of fileEntries + for docId, docEntries of scope.entries + for entryId, entry of docEntries if entry.type == "comment" and scope.threads[entry.thread_id]?.resolved? - scope.resolvedCommentsPerFile[fileId][entryId] = angular.copy scope.threads[entry.thread_id] - scope.resolvedCommentsPerFile[fileId][entryId].content = entry.content - scope.resolvedCommentsPerFile[fileId][entryId].threadId = entry.thread_id - scope.resolvedCommentsPerFile[fileId][entryId].entryId = entryId + resolvedComment = angular.copy scope.threads[entry.thread_id] + + resolvedComment.content = entry.content + resolvedComment.threadId = entry.thread_id + resolvedComment.entryId = entryId + resolvedComment.docId = docId + resolvedComment.docName = getDocNameById(docId) + + scope.resolvedComments.push(resolvedComment) scope.$watchCollection "entries", filterResolvedComments scope.$watchCollection "threads", filterResolvedComments diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 2d02361b54..aa14e2c2d4 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -498,7 +498,8 @@ text-align: center; } -.rp-loading { +.rp-loading, +.rp-empty { text-align: center; padding: 5px; } @@ -709,7 +710,7 @@ margin-left: 1em; background-color: @rp-bg-blue; text-align: left; - padding: 5px 5px 0; + padding: 5px; border-radius: 3px; box-shadow: 0 0 20px 10px rgba(0, 0, 0, .3); From d03aa7056e8f7147cb487a86b97937dca83823f4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jan 2017 13:43:53 +0100 Subject: [PATCH 37/98] Rename /ranges/users end point to /changes/users --- .../{Ranges => TrackChanges}/RangesManager.coffee | 2 +- .../TrackChangesController.coffee} | 7 ++++--- services/web/app/coffee/router.coffee | 8 ++++---- .../review-panel/controllers/ReviewPanelController.coffee | 2 +- .../{Ranges => TrackChanges}/RangesManagerTests.coffee | 8 ++++---- 5 files changed, 14 insertions(+), 13 deletions(-) rename services/web/app/coffee/Features/{Ranges => TrackChanges}/RangesManager.coffee (90%) rename services/web/app/coffee/Features/{Ranges/RangesController.coffee => TrackChanges/TrackChangesController.coffee} (88%) rename services/web/test/UnitTests/coffee/{Ranges => TrackChanges}/RangesManagerTests.coffee (84%) diff --git a/services/web/app/coffee/Features/Ranges/RangesManager.coffee b/services/web/app/coffee/Features/TrackChanges/RangesManager.coffee similarity index 90% rename from services/web/app/coffee/Features/Ranges/RangesManager.coffee rename to services/web/app/coffee/Features/TrackChanges/RangesManager.coffee index b538c1f443..09e6b52ed1 100644 --- a/services/web/app/coffee/Features/Ranges/RangesManager.coffee +++ b/services/web/app/coffee/Features/TrackChanges/RangesManager.coffee @@ -9,7 +9,7 @@ module.exports = RangesManager = return callback(error) if error? DocstoreManager.getAllRanges project_id, callback - getAllRangesUsers: (project_id, callback = (error, users) ->) -> + getAllChangesUsers: (project_id, callback = (error, users) ->) -> user_ids = {} RangesManager.getAllRanges project_id, (error, docs) -> return callback(error) if error? diff --git a/services/web/app/coffee/Features/Ranges/RangesController.coffee b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee similarity index 88% rename from services/web/app/coffee/Features/Ranges/RangesController.coffee rename to services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee index 4b2c1449a3..7fb873a209 100644 --- a/services/web/app/coffee/Features/Ranges/RangesController.coffee +++ b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee @@ -4,7 +4,7 @@ UserInfoController = require "../User/UserInfoController" DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" EditorRealTimeController = require("../Editor/EditorRealTimeController") -module.exports = RangesController = +module.exports = TrackChangesController = getAllRanges: (req, res, next) -> project_id = req.params.project_id logger.log {project_id}, "request for project ranges" @@ -13,10 +13,10 @@ module.exports = RangesController = docs = ({id: d._id, ranges: d.ranges} for d in docs) res.json docs - getAllRangesUsers: (req, res, next) -> + getAllChangesUsers: (req, res, next) -> project_id = req.params.project_id logger.log {project_id}, "request for project range users" - RangesManager.getAllRangesUsers project_id, (error, users) -> + RangesManager.getAllChangesUsers project_id, (error, users) -> return next(error) if error? users = (UserInfoController.formatPersonalInfo(user) for user in users) res.json users @@ -28,3 +28,4 @@ module.exports = RangesController = return next(error) if error? EditorRealTimeController.emitToRoom project_id, "accept-change", doc_id, change_id, (err)-> res.send 204 + diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 1d032c74ac..05c6c7194b 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -40,7 +40,7 @@ AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlew BetaProgramController = require('./Features/BetaProgram/BetaProgramController') AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') AnnouncementsController = require("./Features/Announcements/AnnouncementsController") -RangesController = require("./Features/Ranges/RangesController") +TrackChangesController = require("./Features/TrackChanges/TrackChangesController") CommentsController = require "./Features/Comments/CommentsController" logger = require("logger-sharelatex") @@ -177,9 +177,9 @@ module.exports = class Router webRouter.get "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.proxyToHistoryApi - webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, RangesController.getAllRanges - webRouter.get "/project/:project_id/ranges/users", AuthorizationMiddlewear.ensureUserCanReadProject, RangesController.getAllRangesUsers - webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, RangesController.acceptChange + webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllRanges + webRouter.get "/project/:project_id/changes/users", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllChangesUsers + webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.acceptChange webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 94e5d7809b..e4c6fc3436 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -332,7 +332,7 @@ define [ return _refreshingRangeUsers = true - $http.get "/project/#{$scope.project_id}/ranges/users" + $http.get "/project/#{$scope.project_id}/changes/users" .success (users) -> _refreshingRangeUsers = false $scope.users = {} diff --git a/services/web/test/UnitTests/coffee/Ranges/RangesManagerTests.coffee b/services/web/test/UnitTests/coffee/TrackChanges/RangesManagerTests.coffee similarity index 84% rename from services/web/test/UnitTests/coffee/Ranges/RangesManagerTests.coffee rename to services/web/test/UnitTests/coffee/TrackChanges/RangesManagerTests.coffee index 5f5c09d402..b9c95040c1 100644 --- a/services/web/test/UnitTests/coffee/Ranges/RangesManagerTests.coffee +++ b/services/web/test/UnitTests/coffee/TrackChanges/RangesManagerTests.coffee @@ -3,7 +3,7 @@ SandboxedModule = require('sandboxed-module') assert = require('assert') sinon = require('sinon') path = require "path" -modulePath = path.join __dirname, "../../../../app/js/Features/Ranges/RangesManager" +modulePath = path.join __dirname, "../../../../app/js/Features/TrackChanges/RangesManager" expect = require("chai").expect describe "RangesManager", -> @@ -13,7 +13,7 @@ describe "RangesManager", -> "../Docstore/DocstoreManager": @DocstoreManager = {} "../User/UserInfoManager": @UserInfoManager = {} - describe "getAllRangesUsers", -> + describe "getAllChangesUsers", -> beforeEach -> @project_id = "mock-project-id" @user_id1 = "mock-user-id-1" @@ -45,11 +45,11 @@ describe "RangesManager", -> @RangesManager.getAllRanges = sinon.stub().yields(null, @docs) it "should return an array of unique users", (done) -> - @RangesManager.getAllRangesUsers @project_id, (error, users) => + @RangesManager.getAllChangesUsers @project_id, (error, users) => users.should.deep.equal [{"mock": "user-1"}, {"mock": "user-2"}] done() it "should only call getPersonalInfo once for each user", (done) -> - @RangesManager.getAllRangesUsers @project_id, (error, users) => + @RangesManager.getAllChangesUsers @project_id, (error, users) => @UserInfoManager.getPersonalInfo.calledTwice.should.equal true done() \ No newline at end of file From f3af44fab0b5d59f33e2aebfd75f06a2fd230589 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 10 Jan 2017 14:46:09 +0000 Subject: [PATCH 38/98] add on-success and on-error handlers to async-form. --- services/web/public/coffee/directives/asyncForm.coffee | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/services/web/public/coffee/directives/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee index 2c6345d878..b3e13e8d61 100644 --- a/services/web/public/coffee/directives/asyncForm.coffee +++ b/services/web/public/coffee/directives/asyncForm.coffee @@ -33,6 +33,10 @@ define [ response.success = true response.error = false + if onSuccessHandler = scope[attrs.onSuccess] + onSuccessHandler(data, status, headers, config) + return + if data.redir? ga('send', 'event', formName, 'success') window.location = data.redir @@ -50,6 +54,11 @@ define [ scope[attrs.name].inflight = false response.success = false response.error = true + + if onErrorHandler = scope[attrs.onError] + onErrorHandler(data, status, headers, config) + return + if status == 403 # Forbidden response.message = text: "Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies." From f31fa986790ee46e29e11b49c6221f9accaf6d5d Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jan 2017 16:11:12 +0100 Subject: [PATCH 39/98] Sync track changes state between sessions and client --- .../coffee/Features/Project/ProjectController.coffee | 3 ++- .../TrackChanges/TrackChangesController.coffee | 9 +++++++++ .../Features/TrackChanges/TrackChangesManager.coffee | 5 +++++ services/web/app/coffee/models/Project.coffee | 1 + services/web/app/coffee/router.coffee | 1 + services/web/app/views/project/editor.jade | 1 + .../web/app/views/project/editor/review-panel.jade | 6 +++--- .../public/coffee/ide/editor/EditorManager.coffee | 2 +- .../controllers/ReviewPanelController.coffee | 9 +++++++++ .../review-panel/directives/reviewPanelToggle.coffee | 12 ++++++++++-- 10 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 1d975ea5b3..9713a1b1e2 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -201,7 +201,7 @@ module.exports = ProjectController = async.parallel { project: (cb)-> - ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1}, cb + ProjectGetter.getProject project_id, { name: 1, lastUpdated: 1, track_changes: 1 }, cb user: (cb)-> if !user_id? cb null, defaultSettingsForAnonymousUser(user_id) @@ -267,6 +267,7 @@ module.exports = ProjectController = pdfViewer : user.ace.pdfViewer syntaxValidation: user.ace.syntaxValidation } + trackChangesEnabled: !!project.track_changes privilegeLevel: privilegeLevel chatUrl: Settings.apis.chat.url anonymous: anonymous diff --git a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee index 7fb873a209..71adadb213 100644 --- a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee +++ b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee @@ -3,6 +3,7 @@ logger = require "logger-sharelatex" UserInfoController = require "../User/UserInfoController" DocumentUpdaterHandler = require "../DocumentUpdater/DocumentUpdaterHandler" EditorRealTimeController = require("../Editor/EditorRealTimeController") +TrackChangesManager = require "./TrackChangesManager" module.exports = TrackChangesController = getAllRanges: (req, res, next) -> @@ -29,3 +30,11 @@ module.exports = TrackChangesController = EditorRealTimeController.emitToRoom project_id, "accept-change", doc_id, change_id, (err)-> res.send 204 + toggleTrackChanges: (req, res, next) -> + {project_id} = req.params + track_changes_on = !!req.body.on + logger.log {project_id, track_changes_on}, "request to toggle track changes" + TrackChangesManager.toggleTrackChanges project_id, track_changes_on, (error) -> + return next(error) if error? + EditorRealTimeController.emitToRoom project_id, "toggle-track-changes", track_changes_on, (err)-> + res.send 204 diff --git a/services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee b/services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee new file mode 100644 index 0000000000..8eb7c10c29 --- /dev/null +++ b/services/web/app/coffee/Features/TrackChanges/TrackChangesManager.coffee @@ -0,0 +1,5 @@ +Project = require("../../models/Project").Project + +module.exports = TrackChangesManager = + toggleTrackChanges: (project_id, track_changes_on, callback = (error) ->) -> + Project.update {_id: project_id}, {track_changes: track_changes_on}, callback diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee index 1d53999bd9..18387bdc0b 100644 --- a/services/web/app/coffee/models/Project.coffee +++ b/services/web/app/coffee/models/Project.coffee @@ -32,6 +32,7 @@ ProjectSchema = new Schema archived : { type: Boolean } deletedDocs : [DeletedDocSchema] imageName : { type: String } + track_changes : { type: Boolean } ProjectSchema.statics.getProject = (project_or_id, fields, callback)-> if project_or_id._id? diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 05c6c7194b..d98f9579e1 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -180,6 +180,7 @@ module.exports = class Router webRouter.get "/project/:project_id/ranges", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllRanges webRouter.get "/project/:project_id/changes/users", AuthorizationMiddlewear.ensureUserCanReadProject, TrackChangesController.getAllChangesUsers webRouter.post "/project/:project_id/doc/:doc_id/changes/:change_id/accept", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.acceptChange + webRouter.post "/project/:project_id/track_changes", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, TrackChangesController.toggleTrackChanges webRouter.get '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject webRouter.get '/project/download/zip', AuthorizationMiddlewear.ensureUserCanReadMultipleProjects, ProjectDownloadsController.downloadMultipleProjects diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index 01e1a8b88f..54c742fd15 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -107,6 +107,7 @@ block requirejs window.csrfToken = "!{csrfToken}"; window.anonymous = #{anonymous}; window.maxDocLength = #{maxDocLength}; + window.trackChangesEnabled = #{trackChangesEnabled}; window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.requirejs = { "paths" : { diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 4ec1d24f6e..34e49f03f0 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -9,11 +9,11 @@ on-delete="deleteComment(entryId, threadId);" is-loading="reviewPanel.dropdown.loading" ) - span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = true;", ng-if="editor.wantTrackChanges === false") Track Changes is + span.review-panel-toolbar-label(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") Track Changes is strong off - span.review-panel-toolbar-label(ng-click="editor.wantTrackChanges = false;", ng-if="editor.wantTrackChanges === true") Track Changes is + span.review-panel-toolbar-label(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") Track Changes is strong on - review-panel-toggle(ng-if="editor.wantTrackChanges == editor.trackChanges", ng-model="editor.wantTrackChanges") + review-panel-toggle(ng-if="editor.wantTrackChanges == editor.trackChanges", ng-model="editor.wantTrackChanges", on-toggle="toggleTrackChanges") span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges") i.fa.fa-spin.fa-spinner diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee index 629e1a4cb3..22fcef1a69 100644 --- a/services/web/public/coffee/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/ide/editor/EditorManager.coffee @@ -11,7 +11,7 @@ define [ open_doc_name: null opening: true trackChanges: false - wantTrackChanges: false + wantTrackChanges: window.trackChangesEnabled } @$scope.$on "entity:selected", (event, entity) => diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 96cc0fc86f..9a28c4995e 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -308,6 +308,15 @@ define [ $scope.gotoEntry = (doc_id, entry) -> ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset }) + + $scope.toggleTrackChanges = (value) -> + console.log "Toggling track changes", value + $scope.editor.wantTrackChanges = value + $http.post "/project/#{$scope.project_id}/track_changes", {_csrf: window.csrfToken, on: value} + + ide.socket.on "toggle-track-changes", (value) -> + $scope.$apply () -> + $scope.editor.wantTrackChanges = value _refreshingRangeUsers = false _refreshedForUserIds = {} diff --git a/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee b/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee index e3844d1b12..24b7070d07 100644 --- a/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/reviewPanelToggle.coffee @@ -4,10 +4,18 @@ define [ App.directive "reviewPanelToggle", () -> restrict: "E" scope: - innerModel: '=ngModel' + onToggle: '=' + ngModel: '=' + link: (scope) -> + scope.onChange = (args...) -> + scope.onToggle(scope.localModel) + scope.localModel = scope.ngModel + scope.$watch "ngModel", (value) -> + scope.localModel = value + template: """
- +
""" From 3668d7dd1fe58f1b2f9d421c69a4a6e7616a39fa Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jan 2017 16:58:59 +0100 Subject: [PATCH 40/98] Remove global debugging hook --- .../ide/review-panel/controllers/ReviewPanelController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 9a28c4995e..15ead40e84 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -11,7 +11,7 @@ define [ CUR_FILE : "cur_file" OVERVIEW : "overview" - window.reviewPanel = $scope.reviewPanel = + $scope.reviewPanel = entries: {} hasEntries: false subView: $scope.SubViews.CUR_FILE From f17ce2705c3837810c9a7845c1df7949a1e9533e Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 10 Jan 2017 17:17:07 +0100 Subject: [PATCH 41/98] Don't include wrapper for resolved comments, since it throws the review panel layout off --- services/web/app/views/project/editor/review-panel.jade | 6 ++++-- .../review-panel/controllers/ReviewPanelController.coffee | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 34e49f03f0..0b914a3443 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -24,6 +24,7 @@ .rp-entry-list-inner .rp-entry-wrapper( ng-repeat="(entry_id, entry) in reviewPanel.entries[editor.open_doc_id]" + ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)" ) div(ng-if="entry.type === 'insert' || entry.type === 'delete'") change-entry( @@ -34,7 +35,7 @@ on-indicator-click="toggleReviewPanel();" ) - div(ng-if="entry.type === 'comment' && !(reviewPanel.commentThreads[entry.thread_id].resolved === true)") + div(ng-if="entry.type === 'comment'") comment-entry( entry="entry" threads="reviewPanel.commentThreads" @@ -66,6 +67,7 @@ | {{ doc.path }} .rp-entry-wrapper( ng-repeat="(entry_id, entry) in reviewPanel.entries[doc.doc.id] | orderOverviewEntries" + ng-if="!(entry.type === 'comment' && reviewPanel.commentThreads[entry.thread_id].resolved === true)" ) div(ng-if="entry.type === 'insert' || entry.type === 'delete'") change-entry( @@ -75,7 +77,7 @@ ng-click="gotoEntry(doc_id, entry)" ) - div(ng-if="entry.type === 'comment' && !(reviewPanel.commentThreads[entry.thread_id].resolved === true)") + div(ng-if="entry.type === 'comment'") comment-entry( entry="entry" threads="reviewPanel.commentThreads" diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 15ead40e84..d8c307d66e 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -38,7 +38,6 @@ define [ $scope.$broadcast "review-panel:layout" ide.socket.on "accept-change", (doc_id, change_id) -> - console.log "Got remote accept change", doc_id, change_id if doc_id != $scope.editor.open_doc_id getChangeTracker(doc_id).removeChangeId(change_id) else From 010612aaccbf8312e459c8cf48a03f711945ed44 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 10 Jan 2017 16:29:27 +0000 Subject: [PATCH 42/98] Keep track of resolved threads in the dropdown. --- .../views/project/editor/review-panel.jade | 3 ++- .../controllers/ReviewPanelController.coffee | 20 ++++++++++--------- .../resolvedCommentsDropdown.coffee | 8 ++------ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 34e49f03f0..753941702c 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -3,6 +3,7 @@ resolved-comments-dropdown( entries="reviewPanel.entries" threads="reviewPanel.commentThreads" + resolved-ids="reviewPanel.resolvedThreadIds" docs="docs" on-open="refreshResolvedCommentsDropdown();" on-unresolve="unresolveComment(threadId);" @@ -271,7 +272,7 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') ng-if="!isLoading" ) resolved-comment-entry( - ng-repeat="thread in resolvedComments" + ng-repeat="thread in resolvedComments | orderBy:'-resolved_at'" thread="thread" on-unresolve="handleUnresolve(threadId);" on-delete="handleDelete(entryId, threadId);" diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 15ead40e84..4b45ef3b2d 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -21,6 +21,7 @@ define [ dropdown: loading: false commentThreads: {} + resolvedThreadIds: {} $scope.commentState = adding: false @@ -53,7 +54,6 @@ define [ _onCommentReopened(thread_id) rangesTrackers = {} - resolvedThreadIds = {} getDocEntries = (doc_id) -> $scope.reviewPanel.entries[doc_id] ?= {} @@ -62,7 +62,7 @@ define [ getChangeTracker = (doc_id) -> if !rangesTrackers[doc_id]? rangesTrackers[doc_id] = new RangesTracker() - rangesTrackers[doc_id].resolvedThreadIds = resolvedThreadIds + rangesTrackers[doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds return rangesTrackers[doc_id] scrollbar = {} @@ -97,7 +97,7 @@ define [ # The open doc range tracker is kept up to date in real-time so # replace any outdated info with this rangesTrackers[doc.doc_id] = doc.ranges - rangesTrackers[doc.doc_id].resolvedThreadIds = resolvedThreadIds + rangesTrackers[doc.doc_id].resolvedThreadIds = $scope.reviewPanel.resolvedThreadIds $scope.reviewPanel.rangesTracker = rangesTrackers[doc.doc_id] if old_doc? old_doc.off "flipped_pending_to_inflight" @@ -284,7 +284,7 @@ define [ thread.resolved = true thread.resolved_by_user = formatUser(user) thread.resolved_at = new Date() - resolvedThreadIds[thread_id] = true + $scope.reviewPanel.resolvedThreadIds[thread_id] = true $scope.$broadcast "comment:resolve_thread", thread_id _onCommentReopened = (thread_id) -> @@ -292,14 +292,16 @@ define [ delete thread.resolved delete thread.resolved_by_user delete thread.resolved_at - delete resolvedThreadIds[thread_id] + delete $scope.reviewPanel.resolvedThreadIds[thread_id] $scope.$broadcast "comment:unresolve_thread", thread_id _onCommentDeleted = (thread_id) -> + if $scope.reviewPanel.resolvedThreadIds[thread_id]? + delete $scope.reviewPanel.resolvedThreadIds[thread_id] + delete $scope.reviewPanel.commentThreads[thread_id] $scope.deleteComment = (entry_id, thread_id) -> - console.log thread_id _onCommentDeleted(thread_id) $scope.$broadcast "comment:remove", entry_id @@ -344,15 +346,15 @@ define [ refreshThreads = () -> $http.get "/project/#{$scope.project_id}/threads" .success (threads) -> - for thread_id, _ of resolvedThreadIds - delete resolvedThreadIds[thread_id] + for thread_id, _ of $scope.reviewPanel.resolvedThreadIds + delete $scope.reviewPanel.resolvedThreadIds[thread_id] for thread_id, thread of threads for comment in thread.messages formatComment(comment) if thread.resolved_by_user? $scope.$broadcast "comment:resolve_thread", thread_id thread.resolved_by_user = formatUser(thread.resolved_by_user) - resolvedThreadIds[thread_id] = true + $scope.reviewPanel.resolvedThreadIds[thread_id] = true $scope.reviewPanel.commentThreads = threads refreshThreads() diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index 38a440dfa0..1c798de0e3 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -7,6 +7,7 @@ define [ scope: entries : "=" threads : "=" + resolvedIds : "=" docs : "=" onOpen : "&" onUnresolve : "&" @@ -27,11 +28,9 @@ define [ scope.handleUnresolve = (threadId) -> scope.onUnresolve({ threadId }) - filterResolvedComments() scope.handleDelete = (entryId, threadId) -> scope.onDelete({ entryId, threadId }) - filterResolvedComments() getDocNameById = (docId) -> doc = _.find(scope.docs, (doc) -> doc.doc.id = docId) @@ -56,7 +55,4 @@ define [ scope.resolvedComments.push(resolvedComment) - scope.$watchCollection "entries", filterResolvedComments - scope.$watchCollection "threads", filterResolvedComments - - + scope.$watchCollection "resolvedIds", filterResolvedComments From 931187bd8b2b5c41b35be01179f91134411709bf Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 11 Jan 2017 10:53:46 +0000 Subject: [PATCH 43/98] Add a backdrop to the dropdown (to close when clicked outside). --- .../views/project/editor/review-panel.jade | 8 +++--- .../controllers/ReviewPanelController.coffee | 3 +-- .../stylesheets/app/editor/review-panel.less | 26 +++++++++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 633c6ccdcb..5da0fe5eaf 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -256,10 +256,10 @@ script(type='text/ng-template', id='addCommentEntryTemplate') script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') .resolved-comments - //- .resolved-comments-backdrop( - //- ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }" - //- ng-click="state.isOpen = !state.isOpen" - //- ) + .resolved-comments-backdrop( + ng-class="{ 'resolved-comments-backdrop-visible' : state.isOpen }" + ng-click="state.isOpen = false" + ) a.resolved-comments-toggle( href ng-click="toggleOpenState();" diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index fb521ed9f8..cbdfd62ef3 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -251,8 +251,7 @@ define [ $timeout () -> $scope.$broadcast "review-panel:layout" - $scope.submitReply = (entry, entry_id) -> - $scope.unresolveComment(entry, entry_id) + $scope.submitReply = (entry, entry_id) -> thread_id = entry.thread_id content = entry.replyContent $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index aa14e2c2d4..1092fc2eb2 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -93,6 +93,7 @@ border-left: solid 1px @rp-border-grey; font-size: @rp-base-font-size; color: @rp-type-blue; + z-index: 4; // 3 is Ace's gutter z-index value. } .review-panel-toolbar { @@ -112,7 +113,7 @@ border-bottom: 1px solid @rp-border-grey; background-color: @rp-bg-dim-blue; text-align: center; - z-index: 2; + z-index: 3; flex-basis: 32px; flex-shrink: 0; } @@ -687,19 +688,18 @@ } } -// .resolved-comments-backdrop { -// display: none; -// position: fixed; -// top: 0; -// right: 0; -// bottom: 0; -// left: 0; -// background-color: rgba(0, 0, 0, .5); +.resolved-comments-backdrop { + display: none; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; -// &-visible { -// display: block; -// } -// } + &-visible { + display: block; + } +} .resolved-comments-dropdown { display: none; From d701c1ef5ec26a5242ebefe74a884c44037865dc Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 11 Jan 2017 11:25:18 +0000 Subject: [PATCH 44/98] Make the dropdown scrollable. --- .../views/project/editor/review-panel.jade | 2 +- .../stylesheets/app/editor/review-panel.less | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 5da0fe5eaf..8d92ede03f 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -270,7 +270,7 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') ) .rp-loading(ng-if="isLoading") i.fa.fa-spinner.fa-spin - div( + .resolved-comments-scroller( ng-if="!isLoading" ) resolved-comment-entry( diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 1092fc2eb2..47784df152 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -103,12 +103,12 @@ align-items: center; padding: 0 5px; } - .rp-state-current-file & { - position: absolute; - top: 0; - left: 0; - right: 0; - } + // .rp-state-current-file & { + // position: absolute; + // top: 0; + // left: 0; + // right: 0; + // } height: @rp-toolbar-height; border-bottom: 1px solid @rp-border-grey; background-color: @rp-bg-dim-blue; @@ -706,11 +706,13 @@ position: absolute; width: 300px; left: -150px; + max-height: 90%; margin-top: @rp-entry-arrow-width * 1.5; margin-left: 1em; background-color: @rp-bg-blue; text-align: left; - padding: 5px; + align-items: stretch; + justify-content: center; border-radius: 3px; box-shadow: 0 0 20px 10px rgba(0, 0, 0, .3); @@ -723,6 +725,11 @@ } &-open { - display: block; + display: flex; } } + .resolved-comments-scroller { + flex: 0 0 100%; + padding: 5px; + overflow-y: auto; + } From 2e5c6d4570ae7fab1111ac5dda558b5a4faf0ac9 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 11 Jan 2017 13:41:07 +0100 Subject: [PATCH 45/98] Don't remove add comment marker when updating entries --- .../ide/review-panel/controllers/ReviewPanelController.coffee | 4 +++- .../coffee/ide/review-panel/directives/addCommentEntry.coffee | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index d8c307d66e..67bc813da4 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -154,7 +154,9 @@ define [ # Assume we'll delete everything until we see it, then we'll remove it from this object delete_changes = {} - delete_changes[change_id] = true for change_id, change of entries + for change_id, change of entries + if change_id != "add-comment" + delete_changes[change_id] = true for change in rangesTracker.changes delete delete_changes[change.id] diff --git a/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee index fd3edd09ca..de10524b70 100644 --- a/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee @@ -29,7 +29,6 @@ define [ scope.submitNewComment() scope.submitNewComment = () -> - console.log scope.state.content scope.onSubmit { content: scope.state.content } scope.state.isAdding = false scope.state.content = "" \ No newline at end of file From a081ae7307e0ab80494fdf37070b90dde0867552 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 11 Jan 2017 14:13:49 +0100 Subject: [PATCH 46/98] Properly sync resolved comments dropdown to entries --- .../controllers/ReviewPanelController.coffee | 10 +++++++++- .../directives/resolvedCommentsDropdown.coffee | 3 +-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index a3dcc1e2d8..27ab2ea7d4 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -130,7 +130,7 @@ define [ rangesTracker = getChangeTracker(doc.id) rangesTracker.comments = doc.ranges?.comments or [] rangesTracker.changes = doc.ranges?.changes or [] - updateEntries(doc.id) + updateEntries(doc.id) refreshOverviewPanel = () -> $scope.reviewPanel.overview.loading = true @@ -152,6 +152,8 @@ define [ rangesTracker = getChangeTracker(doc_id) entries = getDocEntries(doc_id) + changed = false + # Assume we'll delete everything until we see it, then we'll remove it from this object delete_changes = {} for change_id, change of entries @@ -159,6 +161,7 @@ define [ delete_changes[change_id] = true for change in rangesTracker.changes + changed = true delete delete_changes[change.id] entries[change.id] ?= {} @@ -178,6 +181,7 @@ define [ refreshChangeUsers(change.metadata.user_id) for comment in rangesTracker.comments + changed = true delete delete_changes[comment.id] entries[comment.id] ?= {} new_entry = { @@ -190,7 +194,11 @@ define [ entries[comment.id][key] = value for change_id, _ of delete_changes + changed = true delete entries[change_id] + + if changed + $scope.$broadcast "entries:changed" $scope.$on "editor:track-changes:changed", () -> doc_id = $scope.editor.open_doc_id diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index 1c798de0e3..c7d1813f34 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -21,7 +21,6 @@ define [ scope.toggleOpenState = () -> scope.state.isOpen = !scope.state.isOpen if (scope.state.isOpen) - filterResolvedComments() scope.onOpen() scope.resolvedComments = [] @@ -55,4 +54,4 @@ define [ scope.resolvedComments.push(resolvedComment) - scope.$watchCollection "resolvedIds", filterResolvedComments + scope.$on "entries:changed", filterResolvedComments \ No newline at end of file From 0264efccaed6e1054c6c9dd6a56270e97488cb3f Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 11 Jan 2017 14:57:00 +0100 Subject: [PATCH 47/98] Don't let review panel entries go off the top of the screen --- .../directives/reviewPanelSorted.coffee | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee b/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee index 82435faf44..5aa8e53295 100644 --- a/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee @@ -43,15 +43,36 @@ define [ previous_focused_entry_index = focused_entry_index sl_console.log "focused_entry_index", focused_entry_index - + + # As we go backwards, we run the risk of pushing things off the top of the editor. + # If we go through the entries before and assume they are as pushed together as they + # could be, we can work out the 'ceiling' that each one can't go through. I.e. the first + # on can't go beyond the toolbar height, the next one can't go beyond the bottom of the first + # one at this minimum height, etc. + heights = (entry.$layout_el.height() for entry in entries_before) + previousMinTop = TOOLBAR_HEIGHT + min_tops = [] + for height in heights + min_tops.push previousMinTop + previousMinTop += PADDING + height + min_tops.reverse() + line_height = 15 - # Put the focused entry exactly where it wants to be - focused_entry_top = Math.max(TOOLBAR_HEIGHT, focused_entry.scope.entry.screenPos.y) + # Put the focused entry as close to where it wants to be as possible + focused_entry_top = Math.max(previousMinTop, focused_entry.scope.entry.screenPos.y) focused_entry.$box_el.css(top: focused_entry_top) focused_entry.$indicator_el.css(top: focused_entry_top) focused_entry.$callout_el.css(top: focused_entry_top + line_height, height: 0) + positionLayoutEl = ($callout_el, original_top, top) -> + if original_top <= top + entry.$callout_el.removeClass("rp-entry-callout-inverted") + entry.$callout_el.css(top: original_top + line_height, height: top - original_top) + else + entry.$callout_el.addClass("rp-entry-callout-inverted") + entry.$callout_el.css(top: top + line_height + 1, height: original_top - top) + previousBottom = focused_entry_top + focused_entry.$layout_el.height() for entry in entries_after original_top = entry.scope.entry.screenPos.y @@ -60,23 +81,21 @@ define [ previousBottom = top + height entry.$box_el.css(top: top) entry.$indicator_el.css(top: top) - entry.$callout_el.removeClass("rp-entry-callout-inverted") - entry.$callout_el.css(top: original_top + line_height, height: top - original_top) + positionLayoutEl(entry.$callout_el, original_top, top) sl_console.log "ENTRY", {entry: entry.scope.entry, top} - + previousTop = focused_entry_top entries_before.reverse() # Work through backwards, starting with the one just above - for entry in entries_before + for entry, i in entries_before original_top = entry.scope.entry.screenPos.y height = entry.$layout_el.height() original_bottom = original_top + height bottom = Math.min(original_bottom, previousTop - PADDING) - top = bottom - height + top = Math.max(bottom - height, min_tops[i]) previousTop = top entry.$box_el.css(top: top) entry.$indicator_el.css(top: top) - entry.$callout_el.addClass("rp-entry-callout-inverted") - entry.$callout_el.css(top: top + line_height + 1, height: original_top - top) + positionLayoutEl(entry.$callout_el, original_top, top) sl_console.log "ENTRY", {entry: entry.scope.entry, top} scope.$applyAsync () -> From 953371ad2dfbb8ce7791346bf14b5f974a982ddd Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 10:59:16 +0100 Subject: [PATCH 48/98] Don't double track a reject --- .../aceEditor/track-changes/TrackChangesManager.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index 836bcf7b9a..20c7fb0731 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -159,8 +159,6 @@ define [ rejectChangeId: (change_id) -> change = @rangesTracker.getChange(change_id) return if !change? - @rangesTracker.removeChangeId(change_id) - @dont_track_next_update = true session = @editor.getSession() if change.op.d? content = change.op.d From e7ab92b7c99c7a3a687616af3770ee3f9d853a53 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 11:52:39 +0100 Subject: [PATCH 49/98] Only show users controls they have permission to use --- services/web/app/coffee/router.coffee | 3 +- .../views/project/editor/review-panel.jade | 34 +++++++++++++------ .../ide/permissions/PermissionsManager.coffee | 7 ++++ .../directives/changeEntry.coffee | 1 + .../directives/commentEntry.coffee | 1 + .../directives/resolvedCommentEntry.coffee | 1 + .../resolvedCommentsDropdown.coffee | 1 + .../stylesheets/app/editor/review-panel.less | 6 +++- 8 files changed, 41 insertions(+), 13 deletions(-) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index d98f9579e1..a9105a1d46 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -233,7 +233,8 @@ module.exports = class Router webRouter.get "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.getMessages webRouter.post "/project/:project_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, ChatController.sendMessage - webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.sendComment + # Note: Read only users can still comment + webRouter.post "/project/:project_id/thread/:thread_id/messages", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.sendComment webRouter.get "/project/:project_id/threads", AuthorizationMiddlewear.ensureUserCanReadProject, CommentsController.getThreads webRouter.post "/project/:project_id/thread/:thread_id/resolve", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.resolveThread webRouter.post "/project/:project_id/thread/:thread_id/reopen", AuthorizationMiddlewear.ensureUserCanWriteProjectContent, CommentsController.reopenThread diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 8d92ede03f..98d9dd1e3e 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -9,12 +9,19 @@ on-unresolve="unresolveComment(threadId);" on-delete="deleteComment(entryId, threadId);" is-loading="reviewPanel.dropdown.loading" + permissions="permissions" ) - span.review-panel-toolbar-label(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") Track Changes is - strong off - span.review-panel-toolbar-label(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") Track Changes is - strong on - review-panel-toggle(ng-if="editor.wantTrackChanges == editor.trackChanges", ng-model="editor.wantTrackChanges", on-toggle="toggleTrackChanges") + span.review-panel-toolbar-label(ng-if="permissions.write") + span(ng-click="toggleTrackChanges(true)", ng-if="editor.wantTrackChanges === false") Track Changes is + strong off + span(ng-click="toggleTrackChanges(false)", ng-if="editor.wantTrackChanges === true") Track Changes is + strong on + review-panel-toggle(ng-if="editor.wantTrackChanges == editor.trackChanges", ng-model="editor.wantTrackChanges", on-toggle="toggleTrackChanges") + span.review-panel-toolbar-label.review-panel-toolbar-label-disabled(ng-if="!permissions.write") + span(ng-if="editor.wantTrackChanges === false") Track Changes is + strong off + span(ng-if="editor.wantTrackChanges === true") Track Changes is + strong on span.review-panel-toolbar-spinner(ng-if="editor.wantTrackChanges != editor.trackChanges") i.fa.fa-spin.fa-spinner @@ -34,6 +41,7 @@ on-reject="rejectChange(entry_id);" on-accept="acceptChange(entry_id);" on-indicator-click="toggleReviewPanel();" + permissions="permissions" ) div(ng-if="entry.type === 'comment'") @@ -43,9 +51,10 @@ on-resolve="resolveComment(entry, entry_id)" on-reply="submitReply(entry, entry_id);" on-indicator-click="toggleReviewPanel();" + permissions="permissions" ) - div(ng-if="entry.type === 'add-comment'") + div(ng-if="entry.type === 'add-comment' && permissions.comment") add-comment-entry( on-start-new="startNewComment();" on-submit="submitNewComment(content);" @@ -76,6 +85,7 @@ user="users[entry.metadata.user_id]" on-indicator-click="toggleReviewPanel();" ng-click="gotoEntry(doc_id, entry)" + permissions="permissions" ) div(ng-if="entry.type === 'comment'") @@ -85,6 +95,7 @@ on-reply="submitReply(entry, entry_id);" on-indicator-click="toggleReviewPanel();" ng-click="gotoEntry(doc_id, entry)" + permissions="permissions" ) .rp-nav @@ -132,7 +143,7 @@ script(type='text/ng-template', id='changeEntryTemplate') .rp-entry-metadata | {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •  span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }} - .rp-entry-actions + .rp-entry-actions(ng-if="permissions.write") a.rp-entry-button(href, ng-click="onReject();") i.fa.fa-times |  Reject @@ -163,7 +174,7 @@ script(type='text/ng-template', id='commentEntryTemplate') | {{ comment.content }} .rp-entry-metadata | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} - .rp-comment-reply + .rp-comment-reply(ng-if="permissions.comment") textarea.rp-comment-input( ng-model="entry.replyContent" ng-keypress="handleCommentReplyKeyPress($event);" @@ -171,10 +182,10 @@ script(type='text/ng-template', id='commentEntryTemplate') placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}" ) .rp-entry-actions - a.rp-entry-button(href, ng-click="onResolve();") + a.rp-entry-button(href, ng-click="onResolve();", ng-if="permissions.comment && permissions.write") i.fa.fa-inbox |  Resolve - a.rp-entry-button(href, ng-click="onReply();") + a.rp-entry-button(href, ng-click="onReply();", ng-if="permissions.comment") i.fa.fa-reply |  Reply @@ -205,7 +216,7 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') .rp-entry-metadata | {{ thread.resolved_at | date : 'MMM d, y h:mm a' }} - .rp-entry-actions + .rp-entry-actions(ng-if="permissions.comment && permissions.write") a.rp-entry-button( href ng-click="onUnresolve({ 'threadId': thread.threadId });" @@ -278,6 +289,7 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') thread="thread" on-unresolve="handleUnresolve(threadId);" on-delete="handleDelete(entryId, threadId);" + permissions="permissions" ) .rp-loading(ng-if="!resolvedComments.length") | No resolved threads. diff --git a/services/web/public/coffee/ide/permissions/PermissionsManager.coffee b/services/web/public/coffee/ide/permissions/PermissionsManager.coffee index 096f15babe..88dea13084 100644 --- a/services/web/public/coffee/ide/permissions/PermissionsManager.coffee +++ b/services/web/public/coffee/ide/permissions/PermissionsManager.coffee @@ -5,15 +5,22 @@ define [], () -> read: false write: false admin: false + comment: false @$scope.$watch "permissionsLevel", (permissionsLevel) => if permissionsLevel? if permissionsLevel == "readOnly" @$scope.permissions.read = true + @$scope.permissions.comment = true else if permissionsLevel == "readAndWrite" @$scope.permissions.read = true @$scope.permissions.write = true + @$scope.permissions.comment = true else if permissionsLevel == "owner" @$scope.permissions.read = true @$scope.permissions.write = true @$scope.permissions.admin = true + @$scope.permissions.comment = true + + if @$scope.anonymous + @$scope.permissions.comment = false diff --git a/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee index d436a34b2c..0ff205a7ec 100644 --- a/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee @@ -7,6 +7,7 @@ define [ scope: entry: "=" user: "=" + permissions: "=" onAccept: "&" onReject: "&" onIndicatorClick: "&" diff --git a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee index 2ee7862379..b74180b719 100644 --- a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee @@ -7,6 +7,7 @@ define [ scope: entry: "=" threads: "=" + permissions: "=" onResolve: "&" onReply: "&" onIndicatorClick: "&" diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee index 76da5b2913..fedf17bb94 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee @@ -6,5 +6,6 @@ define [ templateUrl: "resolvedCommentEntryTemplate" scope: thread: "=" + permissions: "=" onUnresolve: "&" onDelete: "&" \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index c7d1813f34..251db60df1 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -9,6 +9,7 @@ define [ threads : "=" resolvedIds : "=" docs : "=" + permissions: "=" onOpen : "&" onUnresolve : "&" onDelete : "&" diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 47784df152..849c6736d3 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -119,10 +119,13 @@ } .review-panel-toolbar-label { cursor: pointer; - margin-right: 5px; text-align: right; flex-grow: 1; } + .review-panel-toolbar-label-disabled { + cursor: auto; + margin-right: 5px; + } .rp-entry-list { .rp-size-expanded & { @@ -574,6 +577,7 @@ .rp-toggle { display: inline-block; vertical-align: middle; + margin-left: 5px; } .rp-toggle-hidden-input { display: none; From 6cd8e2a9351d4615a87efaaea6a30bd1ab7da167 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 12:05:35 +0100 Subject: [PATCH 50/98] Add user data to comments correctly --- .../ide/review-panel/controllers/ReviewPanelController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 27ab2ea7d4..f68e6518c2 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -368,7 +368,7 @@ define [ refreshThreads() formatComment = (comment) -> - comment.user = formatUser(user) + comment.user = formatUser(comment.user) comment.timestamp = new Date(comment.timestamp) return comment From ea7f1abb6b69334228af1b3821f883ab349c0ed3 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 12:05:50 +0100 Subject: [PATCH 51/98] Darken comment reply text area --- services/web/public/stylesheets/app/editor/review-panel.less | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 849c6736d3..ebafa888cc 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -394,6 +394,7 @@ border-radius: 3px; border: solid 1px @rp-border-grey; resize: vertical; + color: @rp-type-darkgrey; } .rp-icon-delete { From 9a867438b21a98482951438f551d2b28c9b4f163 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 12:06:02 +0100 Subject: [PATCH 52/98] Always show user name next to comment --- services/web/app/views/project/editor/review-panel.jade | 1 - 1 file changed, 1 deletion(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 98d9dd1e3e..df4051b1d9 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -169,7 +169,6 @@ script(type='text/ng-template', id='commentEntryTemplate') p.rp-comment-content span.rp-entry-user( style="color: hsl({{ comment.user.hue }}, 70%, 40%);" - ng-if="$first || comment.user.id !== threads[entry.thread_id].messages[$index - 1].user.id" ) {{ comment.user.name }}:  | {{ comment.content }} .rp-entry-metadata From 88b694f8940ff33ee0a434a754ce4a37ccc9e829 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 12 Jan 2017 11:25:36 +0000 Subject: [PATCH 53/98] Animate comment resolving. --- .../views/project/editor/review-panel.jade | 10 ++++++++-- .../directives/commentEntry.coffee | 12 +++++++++-- .../stylesheets/app/editor/review-panel.less | 20 +++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index df4051b1d9..b695f5f3cd 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -152,7 +152,9 @@ script(type='text/ng-template', id='changeEntryTemplate') |  Accept script(type='text/ng-template', id='commentEntryTemplate') - div + .rp-comment-wrapper( + ng-class="{ 'rp-comment-wrapper-resolving': state.animating }" + ) .rp-entry-callout.rp-entry-callout-comment .rp-entry-indicator( ng-class="{ 'rp-entry-indicator-focused': entry.focused }" @@ -160,7 +162,7 @@ script(type='text/ng-template', id='commentEntryTemplate') ) i.fa.fa-comment .rp-entry.rp-entry-comment( - ng-class="{ 'rp-entry-focused': entry.focused }" + ng-class="{ 'rp-entry-focused': entry.focused, 'rp-entry-comment-resolving': state.animating }" ) div .rp-comment( @@ -181,7 +183,11 @@ script(type='text/ng-template', id='commentEntryTemplate') placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}" ) .rp-entry-actions +<<<<<<< Updated upstream a.rp-entry-button(href, ng-click="onResolve();", ng-if="permissions.comment && permissions.write") +======= + a.rp-entry-button(href, ng-click="animateAndCallOnResolve();") +>>>>>>> Stashed changes i.fa.fa-inbox |  Resolve a.rp-entry-button(href, ng-click="onReply();", ng-if="permissions.comment") diff --git a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee index b74180b719..7ceed51bbe 100644 --- a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.directive "commentEntry", () -> + App.directive "commentEntry", ($timeout) -> restrict: "E" templateUrl: "commentEntryTemplate" scope: @@ -12,9 +12,17 @@ define [ onReply: "&" onIndicatorClick: "&" link: (scope, element, attrs) -> + scope.state = + animating: false + scope.handleCommentReplyKeyPress = (ev) -> if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey ev.preventDefault() ev.target.blur() scope.onReply() - \ No newline at end of file + + scope.animateAndCallOnResolve = () -> + scope.state.animating = true + element.find(".rp-entry").css("top", 0) + $timeout((() -> scope.onResolve()), 200) + return true \ No newline at end of file diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index ebafa888cc..c9c7b21359 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -27,6 +27,8 @@ @rp-toolbar-height : 32px; + + .rp-button() { background-color: @rp-highlight-blue; color: #FFF; @@ -254,6 +256,16 @@ border-color: @rp-yellow; } + &-comment-resolving { + top: 4px; + left: 6px; + opacity: 0; + z-index: 3; + transform: scale(.1); + transform-origin: 0 0; + transition: top .2s ease-out, left .2s ease-out, transform .2s ease-out, opacity .2s ease-out .1s; + } + &-comment-resolved { border-color: @rp-grey; background-color: #efefef; @@ -503,6 +515,14 @@ text-align: center; } +.rp-comment-wrapper { + transition: .2s opacity ease-out; + + &-resolving { + opacity: 0; + } +} + .rp-loading, .rp-empty { text-align: center; From 64b9fe52dd44ac2b5768e6aa952ce3a949da77d3 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 12:31:01 +0100 Subject: [PATCH 54/98] Handle deleted users in comments gracefully --- .../app/coffee/Features/User/UserInfoController.coffee | 2 ++ .../controllers/ReviewPanelController.coffee | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/services/web/app/coffee/Features/User/UserInfoController.coffee b/services/web/app/coffee/Features/User/UserInfoController.coffee index a77f575a48..8054f48afe 100644 --- a/services/web/app/coffee/Features/User/UserInfoController.coffee +++ b/services/web/app/coffee/Features/User/UserInfoController.coffee @@ -30,6 +30,8 @@ module.exports = UserController = res.send JSON.stringify(info) formatPersonalInfo: (user, callback = (error, info) ->) -> + if !user? + return {} formatted_user = { id: user._id.toString() } for key in ["first_name", "last_name", "email", "signUpDate", "role", "institution"] if user[key]? diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index f68e6518c2..3b9990564e 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -373,7 +373,9 @@ define [ return comment formatUser = (user) -> - if !user? + id = user?._id or user?.id + + if !id? return { email: null name: "Anonymous" @@ -381,13 +383,13 @@ define [ hue: ColorManager.ANONYMOUS_HUE avatar_text: "A" } - - id = user._id or user.id if id == window.user_id name = "You" isSelf = true else - name = "#{user.first_name} #{user.last_name}" + name = [user.first_name, user.last_name].filter((n) -> n?).join(" ") + if name == "" + name = "Unknown" isSelf = false return { id: id From 0b57cc650b1ccabeef92d2a53b2db3a5b95279b6 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 12 Jan 2017 11:56:04 +0000 Subject: [PATCH 55/98] Fix merge mistake; adjust animation timings. --- services/web/app/views/project/editor/review-panel.jade | 6 +----- .../coffee/ide/review-panel/directives/commentEntry.coffee | 2 +- .../web/public/stylesheets/app/editor/review-panel.less | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index b695f5f3cd..ce1f7aa48e 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -183,11 +183,7 @@ script(type='text/ng-template', id='commentEntryTemplate') placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}" ) .rp-entry-actions -<<<<<<< Updated upstream - a.rp-entry-button(href, ng-click="onResolve();", ng-if="permissions.comment && permissions.write") -======= - a.rp-entry-button(href, ng-click="animateAndCallOnResolve();") ->>>>>>> Stashed changes + a.rp-entry-button(href, ng-click="animateAndCallOnResolve();", ng-if="permissions.comment && permissions.write") i.fa.fa-inbox |  Resolve a.rp-entry-button(href, ng-click="onReply();", ng-if="permissions.comment") diff --git a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee index 7ceed51bbe..2d32f7f20e 100644 --- a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee @@ -24,5 +24,5 @@ define [ scope.animateAndCallOnResolve = () -> scope.state.animating = true element.find(".rp-entry").css("top", 0) - $timeout((() -> scope.onResolve()), 200) + $timeout((() -> scope.onResolve()), 350) return true \ No newline at end of file diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index c9c7b21359..96ed8377ad 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -263,7 +263,7 @@ z-index: 3; transform: scale(.1); transform-origin: 0 0; - transition: top .2s ease-out, left .2s ease-out, transform .2s ease-out, opacity .2s ease-out .1s; + transition: top .35s ease-out, left .35s ease-out, transform .35s ease-out, opacity .35s ease-out .2s; } &-comment-resolved { @@ -516,7 +516,7 @@ } .rp-comment-wrapper { - transition: .2s opacity ease-out; + transition: .35s opacity ease-out .2s; &-resolving { opacity: 0; From f9b8ada2153f22a8e7b0e4bcf2940905788e9413 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 13:29:57 +0100 Subject: [PATCH 56/98] track changes for anonymous users --- .../Features/TrackChanges/TrackChangesController.coffee | 2 ++ .../aceEditor/track-changes/TrackChangesManager.coffee | 6 +----- .../review-panel/controllers/ReviewPanelController.coffee | 7 ++++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee index 71adadb213..d71481a7fd 100644 --- a/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee +++ b/services/web/app/coffee/Features/TrackChanges/TrackChangesController.coffee @@ -20,6 +20,8 @@ module.exports = TrackChangesController = RangesManager.getAllChangesUsers project_id, (error, users) -> return next(error) if error? users = (UserInfoController.formatPersonalInfo(user) for user in users) + # Get rid of any anonymous/deleted user objects + users = users.filter (u) -> u?.id? res.json users acceptChange: (req, res, next) -> diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index 20c7fb0731..279542475d 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -93,7 +93,7 @@ define [ setTrackChanges: (value) -> if value - @$scope.sharejsDoc?.track_changes_as = window.user.id + @$scope.sharejsDoc?.track_changes_as = window.user.id or "anonymous" else @$scope.sharejsDoc?.track_changes_as = null @@ -230,10 +230,6 @@ define [ if marker.clazz.match("track-changes") console.error "Orphaned ace marker", marker - applyChange: (delta, metadata) -> - op = @_aceChangeToShareJs(delta) - @rangesTracker.applyOp(op, metadata) - updateFocus: () -> selection = @editor.getSelectionRange() cursor_offset = @_aceRangeToShareJs(selection.start) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 3b9990564e..b5cc4d580c 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -346,8 +346,13 @@ define [ .success (users) -> _refreshingRangeUsers = false $scope.users = {} + # Always include ourself, since if we submit an op, we might need to display info + # about it locally before it has been flushed through the server + if ide.$scope.user?.id? + $scope.users[ide.$scope.user.id] = formatUser(ide.$scope.user) for user in users - $scope.users[user.id] = formatUser(user) + if user.id? + $scope.users[user.id] = formatUser(user) .error () -> _refreshingRangeUsers = false From 8e0c2ff0b3b558803241f9ef45e4e7f037946524 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 13:37:44 +0100 Subject: [PATCH 57/98] Adjust resolved comments buttons and add tooltip --- services/web/app/views/project/editor/review-panel.jade | 5 ++++- .../web/public/stylesheets/app/editor/review-panel.less | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index ce1f7aa48e..fca2022bfa 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -275,7 +275,10 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') a.resolved-comments-toggle( href ng-click="toggleOpenState();" - ) + tooltip="Resolved Comments" + tooltip-placement="bottom" + tooltip-append-to-body="true" + ) i.fa.fa-inbox .resolved-comments-dropdown( ng-class="{ 'resolved-comments-dropdown-open' : state.isOpen }" diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 96ed8377ad..206446a7f7 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -700,11 +700,15 @@ } .resolved-comments-toggle { - font-size: @rp-icon-large-size; + font-size: 14px; color: lighten(@rp-type-blue, 25%); border: solid 1px @rp-border-grey; border-radius: 3px; padding: 0 4px; + display: block; + height: 22px; + width: 22px; + line-height: 1.4; &:hover, &:focus { From a42cc48be8f959faef34be71f138511beafd6db1 Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 13:44:54 +0100 Subject: [PATCH 58/98] Fix doc names and sort order in resolved comments dropdown --- services/web/app/views/project/editor/review-panel.jade | 2 +- .../ide/review-panel/directives/resolvedCommentsDropdown.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index fca2022bfa..f3e9457558 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -289,7 +289,7 @@ script(type='text/ng-template', id='resolvedCommentsDropdownTemplate') ng-if="!isLoading" ) resolved-comment-entry( - ng-repeat="thread in resolvedComments | orderBy:'-resolved_at'" + ng-repeat="thread in resolvedComments | orderBy:'resolved_at':true" thread="thread" on-unresolve="handleUnresolve(threadId);" on-delete="handleDelete(entryId, threadId);" diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index 251db60df1..f161338324 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -33,7 +33,7 @@ define [ scope.onDelete({ entryId, threadId }) getDocNameById = (docId) -> - doc = _.find(scope.docs, (doc) -> doc.doc.id = docId) + doc = _.find(scope.docs, (doc) -> doc.doc.id == docId) if doc? return doc.path else From b97ce52c6f4bce2312fd87421e511b44834cd48d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 12 Jan 2017 14:20:41 +0000 Subject: [PATCH 59/98] Handle z-index issues with mini review panel. --- .../stylesheets/app/editor/review-panel.less | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 206446a7f7..5aff5ed322 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -85,6 +85,7 @@ .rp-size-mini & { display: block; width: @review-off-width; + z-index: 6; } position: absolute; @@ -157,7 +158,6 @@ display: none; .rp-size-mini & { display: block; - z-index: 12; } position: absolute; left: 2px; @@ -193,21 +193,22 @@ display: none; left: @review-off-width + @rp-entry-arrow-width; box-shadow: 0 0 10px 5px rgba(0, 0, 0, .2); - z-index: 11; + z-index: 1; &::before { - .triangle(left, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit); - top: (@review-off-width / 2) - @rp-entry-arrow-width; - left: -(@rp-entry-ribbon-width + @rp-entry-arrow-width); - content: ''; - } - &::after { content: ''; position: absolute; top: -(@review-off-width + @rp-entry-arrow-width); right: -(@review-off-width + @rp-entry-arrow-width); bottom: -(@review-off-width + @rp-entry-arrow-width); left: -(@review-off-width + @rp-entry-arrow-width); + z-index: -1; + } + &::after { + .triangle(left, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit); + top: (@review-off-width / 2) - @rp-entry-arrow-width; + left: -(@rp-entry-ribbon-width + @rp-entry-arrow-width); + content: ''; } } .rp-state-current-file-expanded & { From 2052ee63a35b528a71dec0c54618a7473046bf4c Mon Sep 17 00:00:00 2001 From: James Allen Date: Thu, 12 Jan 2017 17:19:19 +0100 Subject: [PATCH 60/98] Recover tracked changes after a disconnect --- .../public/coffee/ide/editor/Document.coffee | 22 ++++++++++++++++--- .../track-changes/TrackChangesManager.coffee | 13 +++++++++++ .../controllers/ReviewPanelController.coffee | 1 - 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index d037aeb165..fbef95e67b 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -252,9 +252,7 @@ define [ return callback(error) if error? @joined = true @doc.catchUp( updates ) - # TODO: Worry about whether these ranges are consistent with the doc still - @ranges?.changes = ranges?.changes - @ranges?.comments = ranges?.comments + @_catchUpRanges( ranges?.changes, ranges?.comments ) callback() else @ide.socket.emit 'joinDoc', @doc_id, (error, docLines, version, updates, ranges) => @@ -341,6 +339,7 @@ define [ track_changes_as = null remote_op = msg? if msg?.meta?.tc? + old_id_seed = @ranges.getIdSeed() @ranges.setIdSeed(msg.meta.tc) if remote_op and msg.meta?.tc track_changes_as = msg.meta.user_id @@ -349,3 +348,20 @@ define [ @ranges.track_changes = track_changes_as? for op in ops @ranges.applyOp op, { user_id: track_changes_as } + if old_id_seed? + @ranges.setIdSeed(old_id_seed) + + _catchUpRanges: (changes = [], comments = []) -> + # We've just been given the current server's ranges, but need to apply any local ops we have. + # Reset to the server state then apply our local ops again. + @ranges.emit "clear" + @ranges.changes = changes + @ranges.comments = comments + @ranges.track_changes = @doc.track_changes + for op in @doc.getInflightOp() or [] + @ranges.setIdSeed(@doc.track_changes_id_seeds.inflight) + @ranges.applyOp(op) + for op in @doc.getPendingOp() or [] + @ranges.setIdSeed(@doc.track_changes_id_seeds.pending) + @ranges.applyOp(op) + @ranges.emit "redraw" diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index 279542475d..422e1978a6 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -59,6 +59,7 @@ define [ @recalculateReviewEntriesScreenPositions() onChangeSession = (e) => + @clearAnnotations() @redrawAnnotations() bindToAce = () => @@ -128,6 +129,18 @@ define [ sl_console.log "[comment:removed]", comment setTimeout () => @_onCommentRemoved(comment) + @rangesTracker.on "clear", () => + @clearAnnotations() + @rangesTracker.on "redraw", () => + @redrawAnnotations() + + clearAnnotations: () -> + session = @editor.getSession() + for change_id, markers of @changeIdToMarkerIdMap + for marker_name, marker_id of markers + session.removeMarker marker_id + @changeIdToMarkerIdMap = {} + redrawAnnotations: () -> for change in @rangesTracker.changes if change.op.i? diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index b5cc4d580c..942178cf8b 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -320,7 +320,6 @@ define [ ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset }) $scope.toggleTrackChanges = (value) -> - console.log "Toggling track changes", value $scope.editor.wantTrackChanges = value $http.post "/project/#{$scope.project_id}/track_changes", {_csrf: window.csrfToken, on: value} From 9f008318cc0b779768f03a53f02dca7db26253e0 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 12 Jan 2017 16:49:29 +0000 Subject: [PATCH 61/98] Emit event after linking the layout directive. --- services/web/public/coffee/ide/directives/layout.coffee | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/web/public/coffee/ide/directives/layout.coffee b/services/web/public/coffee/ide/directives/layout.coffee index 7fc459a539..f20a37b342 100644 --- a/services/web/public/coffee/ide/directives/layout.coffee +++ b/services/web/public/coffee/ide/directives/layout.coffee @@ -117,5 +117,10 @@ define [ element.layout().hide("east") else element.layout().show("east") + + post: (scope, element, attrs) -> + name = attrs.layout + state = element.layout().readState() + scope.$broadcast "layout:#{name}:linked", state } ] From e3e8541a6302bcdbe1f488bc1571ad093a96ae2d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 12 Jan 2017 16:49:47 +0000 Subject: [PATCH 62/98] Add class when review panel should layout to the left. --- services/web/app/views/project/editor/editor.jade | 1 + .../controllers/ReviewPanelController.coffee | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index 8d58e35f2e..bd7d26616f 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -18,6 +18,7 @@ div.full-size( 'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\ 'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\ 'rp-size-expanded': ui.reviewPanelOpen\ + 'rp-layout-left': reviewPanel.layoutToLeft }" ) .loading-panel(ng-show="!editor.sharejs_doc || editor.opening") diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index b5cc4d580c..bbabd6c2c5 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -22,6 +22,16 @@ define [ loading: false commentThreads: {} resolvedThreadIds: {} + layoutToLeft: false + + $scope.$on "layout:pdf:linked", (event, state) -> + $scope.reviewPanel.layoutToLeft = (state.east?.size < 220 || state.east?.initClosed) + + $scope.$on "layout:pdf:resize", (event, state) -> + $scope.reviewPanel.layoutToLeft = (state.east?.size < 220 || state.east?.initClosed) + + $scope.$watch "ui.pdfLayout", (layout) -> + $scope.reviewPanel.layoutToLeft = (layout == "flat") $scope.commentState = adding: false From 823c2c854af1494bb4d8fcd4e8eddc073e9a4f04 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 12 Jan 2017 17:06:08 +0000 Subject: [PATCH 63/98] Add styling to have left-based layout when there is no space on the right. --- .../web/app/views/project/editor/editor.jade | 4 ++-- .../stylesheets/app/editor/review-panel.less | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index bd7d26616f..4988e56dc4 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -17,8 +17,8 @@ div.full-size( 'rp-state-current-file-mini': (reviewPanel.subView === SubViews.CUR_FILE && !ui.reviewPanelOpen),\ 'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\ 'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\ - 'rp-size-expanded': ui.reviewPanelOpen\ - 'rp-layout-left': reviewPanel.layoutToLeft + 'rp-size-expanded': ui.reviewPanelOpen,\ + 'rp-layout-left': reviewPanel.layoutToLeft\ }" ) .loading-panel(ng-show="!editor.sharejs_doc || editor.opening") diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 5aff5ed322..652bf55e5b 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -211,6 +211,19 @@ content: ''; } } + .rp-state-current-file-mini.rp-layout-left & { + left: auto; + right: @review-off-width + @rp-entry-arrow-width; + border-left-width: 0; + border-right-width: @rp-entry-ribbon-width; + border-right-style: solid; + + &::after { + .triangle(right, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit); + right: -(@rp-entry-ribbon-width + @rp-entry-arrow-width); + left: auto; + } + } .rp-state-current-file-expanded & { left: 5px; right: 5px; @@ -344,6 +357,15 @@ border-bottom-right-radius: 3px; border-right-width: 0; } + + .rp-layout-left & { + &:first-child { + border-bottom-left-radius: 3px; + } + &:last-child { + border-bottom-right-radius: 0; + } + } } .rp-comment { From 6c49b955386ced925a5a2843e3415a0177c1a5b5 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 13 Jan 2017 14:17:47 +0100 Subject: [PATCH 64/98] Don't allow overlapping comments Note that this is only a 'soft' don't allow. You could resolve a comment, comment in the same area, and get them to overlap. It's not a problem if they overlap, just a bit ugly UI wise --- .../track-changes/TrackChangesManager.coffee | 7 +++-- .../controllers/ReviewPanelController.coffee | 30 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index 422e1978a6..5de7e3be2d 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -245,10 +245,11 @@ define [ updateFocus: () -> selection = @editor.getSelectionRange() - cursor_offset = @_aceRangeToShareJs(selection.start) + selection_start = @_aceRangeToShareJs(selection.start) + selection_end = @_aceRangeToShareJs(selection.end) entries = @_getCurrentDocEntries() - selection = !(selection.start.column == selection.end.column and selection.start.row == selection.end.row) - @$scope.$emit "editor:focus:changed", cursor_offset, selection + is_selection = (selection_start != selection_end) + @$scope.$emit "editor:focus:changed", selection_start, selection_end, is_selection broadcastChange: () -> @$scope.$emit "editor:track-changes:changed", @$scope.docId diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index ddedda525b..6dae528436 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -216,25 +216,31 @@ define [ $scope.$broadcast "review-panel:recalculate-screen-positions" $scope.$broadcast "review-panel:layout" - $scope.$on "editor:focus:changed", (e, cursor_offset, selection) -> + $scope.$on "editor:focus:changed", (e, selection_offset_start, selection_offset_end, selection) -> doc_id = $scope.editor.open_doc_id entries = getDocEntries(doc_id) - if !selection - delete entries["add-comment"] - else - entries["add-comment"] = { - type: "add-comment" - offset: cursor_offset - } + delete entries["add-comment"] + if selection + # Only show add comment if we're not already overlapping one + overlapping_comment = false + for id, entry of entries + if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id] + unless entry.offset >= selection_offset_end or entry.offset + entry.content.length <= selection_offset_start + overlapping_comment = true + if !overlapping_comment + entries["add-comment"] = { + type: "add-comment" + offset: selection_offset_start + } for id, entry of entries - if entry.type == "comment" and not entry.resolved - entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.content.length) + if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id] + entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length) else if entry.type == "insert" - entry.focused = (entry.offset <= cursor_offset <= entry.offset + entry.content.length) + entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length) else if entry.type == "delete" - entry.focused = (entry.offset == cursor_offset) + entry.focused = (entry.offset == selection_offset_start) else if entry.type == "add-comment" and selection entry.focused = true From 718f3dc3888380e8cb097cdabd488b424b125074 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 13 Jan 2017 14:30:04 +0100 Subject: [PATCH 65/98] Fix slightly broken callout positioning behaviour --- .../directives/reviewPanelSorted.coffee | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee b/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee index 5aa8e53295..72fd0f1a58 100644 --- a/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee @@ -59,19 +59,19 @@ define [ line_height = 15 + positionLayoutEl = ($callout_el, original_top, top) -> + if original_top <= top + $callout_el.removeClass("rp-entry-callout-inverted") + $callout_el.css(top: original_top + line_height, height: top - original_top) + else + $callout_el.addClass("rp-entry-callout-inverted") + $callout_el.css(top: top + line_height + 1, height: original_top - top) + # Put the focused entry as close to where it wants to be as possible focused_entry_top = Math.max(previousMinTop, focused_entry.scope.entry.screenPos.y) focused_entry.$box_el.css(top: focused_entry_top) focused_entry.$indicator_el.css(top: focused_entry_top) - focused_entry.$callout_el.css(top: focused_entry_top + line_height, height: 0) - - positionLayoutEl = ($callout_el, original_top, top) -> - if original_top <= top - entry.$callout_el.removeClass("rp-entry-callout-inverted") - entry.$callout_el.css(top: original_top + line_height, height: top - original_top) - else - entry.$callout_el.addClass("rp-entry-callout-inverted") - entry.$callout_el.css(top: top + line_height + 1, height: original_top - top) + positionLayoutEl(focused_entry.$callout_el, focused_entry.scope.entry.screenPos.y, focused_entry_top) previousBottom = focused_entry_top + focused_entry.$layout_el.height() for entry in entries_after From bf7038f5061b13779fed80a7ccbd9a4c791fd122 Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 13 Jan 2017 14:52:08 +0100 Subject: [PATCH 66/98] Adapt review panel line height to editor --- services/web/app/views/project/editor/editor.jade | 1 + .../coffee/ide/editor/directives/aceEditor.coffee | 9 +++++++++ .../controllers/ReviewPanelController.coffee | 1 + .../review-panel/directives/reviewPanelSorted.coffee | 11 +++++++---- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index 4988e56dc4..d31be444af 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -55,6 +55,7 @@ div.full-size( track-changes-enabled="trackChangesFeatureFlag", track-changes= "editor.trackChanges", doc-id="editor.open_doc_id" + renderer-data="reviewPanel.rendererData" ) include ./review-panel diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 8a3b00a61e..41c70b4dee 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -57,6 +57,7 @@ define [ trackChanges: "=" trackChangesEnabled: "=" docId: "=" + rendererData: "=" } link: (scope, element, attrs) -> # Don't freak out if we're already in an apply callback @@ -317,6 +318,14 @@ define [ doc = session.getDocument() doc.off "change", onChange + + editor.renderer.on "changeCharacterSize", () -> + scope.$apply () -> + scope.rendererData.lineHeight = editor.renderer.lineHeight + + scope.$watch "rendererData", (rendererData) -> + if rendererData? + rendererData.lineHeight = editor.renderer.lineHeight template: """
diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 6dae528436..1db13c291e 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -23,6 +23,7 @@ define [ commentThreads: {} resolvedThreadIds: {} layoutToLeft: false + rendererData: {} $scope.$on "layout:pdf:linked", (event, state) -> $scope.reviewPanel.layoutToLeft = (state.east?.size < 220 || state.east?.initClosed) diff --git a/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee b/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee index 72fd0f1a58..2bd66c723e 100644 --- a/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/reviewPanelSorted.coffee @@ -32,6 +32,8 @@ define [ return if entries.length == 0 + line_height = scope.reviewPanel.rendererData.lineHeight + focused_entry_index = Math.min(previous_focused_entry_index, entries.length - 1) for entry, i in entries if entry.scope.entry.focused @@ -57,15 +59,13 @@ define [ previousMinTop += PADDING + height min_tops.reverse() - line_height = 15 - positionLayoutEl = ($callout_el, original_top, top) -> if original_top <= top $callout_el.removeClass("rp-entry-callout-inverted") - $callout_el.css(top: original_top + line_height, height: top - original_top) + $callout_el.css(top: original_top + line_height - 1, height: top - original_top) else $callout_el.addClass("rp-entry-callout-inverted") - $callout_el.css(top: top + line_height + 1, height: original_top - top) + $callout_el.css(top: top + line_height, height: original_top - top) # Put the focused entry as close to where it wants to be as possible focused_entry_top = Math.max(previousMinTop, focused_entry.scope.entry.screenPos.y) @@ -104,6 +104,9 @@ define [ scope.$on "review-panel:layout", () -> scope.$applyAsync () -> layout() + + scope.$watch "reviewPanel.rendererData.lineHeight", () -> + layout() ## Scroll lock with Ace scroller = element From b6ea338ff84ee92da1b41eb05960f5e08c62325f Mon Sep 17 00:00:00 2001 From: James Allen Date: Fri, 13 Jan 2017 15:27:45 +0100 Subject: [PATCH 67/98] Separate resolved comments so they don't affect review panel --- .../app/views/project/editor/review-panel.jade | 2 +- .../controllers/ReviewPanelController.coffee | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index f3e9457558..a994a87de2 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -1,7 +1,7 @@ #review-panel .review-panel-toolbar resolved-comments-dropdown( - entries="reviewPanel.entries" + entries="reviewPanel.resolvedComments" threads="reviewPanel.commentThreads" resolved-ids="reviewPanel.resolvedThreadIds" docs="docs" diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 1db13c291e..cdb66c52b9 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -13,6 +13,7 @@ define [ $scope.reviewPanel = entries: {} + resolvedComments: {} hasEntries: false subView: $scope.SubViews.CUR_FILE openSubView: $scope.SubViews.CUR_FILE @@ -69,6 +70,10 @@ define [ $scope.reviewPanel.entries[doc_id] ?= {} return $scope.reviewPanel.entries[doc_id] + getDocResolvedComments = (doc_id) -> + $scope.reviewPanel.resolvedComments[doc_id] ?= {} + return $scope.reviewPanel.resolvedComments[doc_id] + getChangeTracker = (doc_id) -> if !rangesTrackers[doc_id]? rangesTrackers[doc_id] = new RangesTracker() @@ -162,6 +167,7 @@ define [ updateEntries = (doc_id) -> rangesTracker = getChangeTracker(doc_id) entries = getDocEntries(doc_id) + resolvedComments = getDocResolvedComments(doc_id) changed = false @@ -170,6 +176,8 @@ define [ for change_id, change of entries if change_id != "add-comment" delete_changes[change_id] = true + for change_id, change of resolvedComments + delete_changes[change_id] = true for change in rangesTracker.changes changed = true @@ -194,7 +202,10 @@ define [ for comment in rangesTracker.comments changed = true delete delete_changes[comment.id] - entries[comment.id] ?= {} + if $scope.reviewPanel.resolvedThreadIds[comment.op.t] + new_comment = resolvedComments[comment.id] ?= {} + else + new_comment = entries[comment.id] ?= {} new_entry = { type: "comment" thread_id: comment.op.t @@ -202,11 +213,12 @@ define [ offset: comment.op.p } for key, value of new_entry - entries[comment.id][key] = value + new_comment[key] = value for change_id, _ of delete_changes changed = true delete entries[change_id] + delete resolvedComments[change_id] if changed $scope.$broadcast "entries:changed" From 5eece9f5d9ede5a58ba3e9737f06cf4a814975e1 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 13 Jan 2017 15:30:31 +0000 Subject: [PATCH 68/98] Add collapsing behaviour to long entries. --- .../web/app/views/project/editor/review-panel.jade | 14 ++++++++++++-- .../ide/review-panel/directives/changeEntry.coffee | 12 ++++++++++-- .../stylesheets/app/editor/review-panel.less | 11 +++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index a994a87de2..a664c02e23 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -137,9 +137,19 @@ script(type='text/ng-template', id='changeEntryTemplate') .rp-entry-details .rp-entry-description(ng-switch="entry.type") span(ng-switch-when="insert") Added  - ins.rp-content-highlight {{ entry.content }} + ins.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }} + a.rp-collapse-toggle( + href + ng-if="needsCollapsing" + ng-click="toggleCollapse();" + )  {{ isCollapsed ? '(more)' : '(less)' }} span(ng-switch-when="delete") Deleted  - del.rp-content-highlight {{ entry.content }} + del.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }} + a.rp-collapse-toggle( + href + ng-if="needsCollapsing" + ng-click="toggleCollapse();" + )  {{ isCollapsed ? '(more)' : '(less)' }} .rp-entry-metadata | {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •  span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }} diff --git a/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee index 0ff205a7ec..d21cdc10b2 100644 --- a/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee @@ -1,7 +1,7 @@ define [ "base" ], (App) -> - App.directive "changeEntry", () -> + App.directive "changeEntry", ($timeout) -> restrict: "E" templateUrl: "changeEntryTemplate" scope: @@ -11,4 +11,12 @@ define [ onAccept: "&" onReject: "&" onIndicatorClick: "&" - \ No newline at end of file + link: (scope, element, attrs) -> + scope.contentLimit = 40 + scope.needsCollapsing = scope.entry.content.length > scope.contentLimit + scope.isCollapsed = true + + scope.toggleCollapse = () -> + scope.isCollapsed = !scope.isCollapsed + $timeout () -> + scope.$emit "review-panel:layout" \ No newline at end of file diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 652bf55e5b..79006446ab 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -785,3 +785,14 @@ padding: 5px; overflow-y: auto; } + +.rp-collapse-toggle { + color: @rp-type-blue; + font-weight: @rp-semibold-weight; + + &:hover, + &:focus { + color: darken(@rp-type-blue, 5%); + text-decoration: none; + } +} \ No newline at end of file From 3968761ac805ea8e75e5b998f7c449cadcc00618 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 13 Jan 2017 15:37:24 +0000 Subject: [PATCH 69/98] Add collapsing behaviour to resolved comments. --- .../web/app/views/project/editor/review-panel.jade | 8 +++++++- .../directives/resolvedCommentEntry.coffee | 11 ++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index a664c02e23..d7c6bc9e52 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -206,7 +206,13 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') .rp-resolved-comment-context | Quoted text on  span.rp-resolved-comment-context-file {{ thread.docName }} - p.rp-resolved-comment-context-quote {{ thread.content }} + p.rp-resolved-comment-context-quote + span {{ thread.content | limitTo:(isCollapsed ? contentLimit : thread.content.length)}} + a.rp-collapse-toggle( + href + ng-if="needsCollapsing" + ng-click="toggleCollapse();" + )  {{ isCollapsed ? '(more)' : '(less)' }} .rp-comment( ng-repeat="comment in thread.messages track by comment.id" ) diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee index fedf17bb94..8b933e05f2 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee @@ -8,4 +8,13 @@ define [ thread: "=" permissions: "=" onUnresolve: "&" - onDelete: "&" \ No newline at end of file + onDelete: "&" + link: (scope, element, attrs) -> + scope.contentLimit = 40 + scope.needsCollapsing = scope.thread.content.length > scope.contentLimit + scope.isCollapsed = true + + scope.toggleCollapse = () -> + scope.isCollapsed = !scope.isCollapsed + $timeout () -> + scope.$emit "review-panel:layout" \ No newline at end of file From 1ed0ccf85464f31f46c60159aaa601d1bf0c1d4b Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Fri, 13 Jan 2017 16:07:08 +0000 Subject: [PATCH 70/98] Improve hovering behaviour for entries in mini mode. --- .../web/public/stylesheets/app/editor/review-panel.less | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 79006446ab..646de2fc3a 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -201,7 +201,7 @@ top: -(@review-off-width + @rp-entry-arrow-width); right: -(@review-off-width + @rp-entry-arrow-width); bottom: -(@review-off-width + @rp-entry-arrow-width); - left: -(@review-off-width + @rp-entry-arrow-width); + left: -(2 * @rp-entry-arrow-width + 2); z-index: -1; } &::after { @@ -218,6 +218,10 @@ border-right-width: @rp-entry-ribbon-width; border-right-style: solid; + &::before { + left: -(@review-off-width + @rp-entry-arrow-width); + right: -(2 * @rp-entry-arrow-width + 2); + } &::after { .triangle(right, @rp-entry-arrow-width, @rp-entry-arrow-width * 1.5, inherit); right: -(@rp-entry-ribbon-width + @rp-entry-arrow-width); @@ -789,7 +793,7 @@ .rp-collapse-toggle { color: @rp-type-blue; font-weight: @rp-semibold-weight; - + &:hover, &:focus { color: darken(@rp-type-blue, 5%); From 278bfc1f503afa27093e576b6add9fb59077487a Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 16 Jan 2017 15:06:47 +0100 Subject: [PATCH 71/98] Use 'show all' instead of 'more' --- services/web/app/views/project/editor/review-panel.jade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index d7c6bc9e52..499f01a3a7 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -142,14 +142,14 @@ script(type='text/ng-template', id='changeEntryTemplate') href ng-if="needsCollapsing" ng-click="toggleCollapse();" - )  {{ isCollapsed ? '(more)' : '(less)' }} + ) {{ isCollapsed ? '... (show all)' : ' (show less)' }} span(ng-switch-when="delete") Deleted  del.rp-content-highlight {{ entry.content | limitTo:(isCollapsed ? contentLimit : entry.content.length) }} a.rp-collapse-toggle( href ng-if="needsCollapsing" ng-click="toggleCollapse();" - )  {{ isCollapsed ? '(more)' : '(less)' }} + ) {{ isCollapsed ? '... (show all)' : ' (show less)' }} .rp-entry-metadata | {{ entry.metadata.ts | date : 'MMM d, y h:mm a' }} •  span.rp-entry-user(style="color: hsl({{ user.hue }}, 70%, 40%);") {{ user.name }} From e5c96eb6190806ccddfa125c7a9a74506603ee4c Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 16 Jan 2017 15:17:56 +0100 Subject: [PATCH 72/98] Focus on text area when adding a comment --- services/web/app/views/project/editor/review-panel.jade | 1 + .../coffee/ide/review-panel/directives/addCommentEntry.coffee | 2 ++ 2 files changed, 3 insertions(+) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 499f01a3a7..7fe12a4c2b 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -273,6 +273,7 @@ script(type='text/ng-template', id='addCommentEntryTemplate') ng-model="state.content" ng-keypress="handleCommentKeyPress($event);" placeholder="Add your comment here" + focus-on="comment:new:open" ) .rp-entry-actions a.rp-entry-button(href, ng-click="cancelNewComment();") diff --git a/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee index de10524b70..7f41f00fa5 100644 --- a/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee @@ -17,6 +17,8 @@ define [ scope.startNewComment = () -> scope.state.isAdding = true scope.onStartNew() + setTimeout () -> + scope.$broadcast "comment:new:open" scope.cancelNewComment = () -> scope.state.isAdding = false From d4f60977fd4f56149a895c597fbae6f539c89bd4 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 16 Jan 2017 15:25:10 +0100 Subject: [PATCH 73/98] Add loading indicator while submitting new comments --- .../web/app/views/project/editor/review-panel.jade | 2 ++ .../controllers/ReviewPanelController.coffee | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 7fe12a4c2b..0efd5add10 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -185,6 +185,8 @@ script(type='text/ng-template', id='commentEntryTemplate') | {{ comment.content }} .rp-entry-metadata | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} + .rp-loading(ng-if="threads[entry.thread_id].submitting") + i.fa.fa-spinner.fa-spin .rp-comment-reply(ng-if="permissions.comment") textarea.rp-comment-input( ng-model="entry.replyContent" diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index cdb66c52b9..4c65208ad9 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -44,8 +44,9 @@ define [ $scope.reviewPanelEventsBridge = new EventEmitter() ide.socket.on "new-comment", (thread_id, comment) -> - $scope.reviewPanel.commentThreads[thread_id] ?= { messages: [] } - $scope.reviewPanel.commentThreads[thread_id].messages.push(formatComment(comment)) + thread = getThread(thread_id) + delete thread.submitting + thread.messages.push(formatComment(comment)) $scope.$apply() $timeout () -> $scope.$broadcast "review-panel:layout" @@ -73,6 +74,10 @@ define [ getDocResolvedComments = (doc_id) -> $scope.reviewPanel.resolvedComments[doc_id] ?= {} return $scope.reviewPanel.resolvedComments[doc_id] + + getThread = (thread_id) -> + $scope.reviewPanel.commentThreads[thread_id] ?= { messages: [] } + return $scope.reviewPanel.commentThreads[thread_id] getChangeTracker = (doc_id) -> if !rangesTrackers[doc_id]? @@ -274,6 +279,8 @@ define [ $scope.submitNewComment = (content) -> thread_id = RangesTracker.generateId() + thread = getThread(thread_id) + thread.submitting = true $scope.$broadcast "comment:add", thread_id $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) .error (error) -> @@ -297,6 +304,8 @@ define [ .error (error) -> ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment") + thread = getThread(thread_id) + thread.submitting = true entry.replyContent = "" entry.replying = false $timeout () -> From bf56952f88b6cfb4f32ffe4f1fd789b046b63120 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 16 Jan 2017 15:31:51 +0100 Subject: [PATCH 74/98] Make overview panel entries jump to place in doc again --- services/web/app/views/project/editor/review-panel.jade | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 0efd5add10..f0296a1d23 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -84,7 +84,7 @@ entry="entry" user="users[entry.metadata.user_id]" on-indicator-click="toggleReviewPanel();" - ng-click="gotoEntry(doc_id, entry)" + ng-click="gotoEntry(doc.doc.id, entry)" permissions="permissions" ) @@ -94,7 +94,7 @@ threads="reviewPanel.commentThreads" on-reply="submitReply(entry, entry_id);" on-indicator-click="toggleReviewPanel();" - ng-click="gotoEntry(doc_id, entry)" + ng-click="gotoEntry(doc.doc.id, entry)" permissions="permissions" ) From 5fb54d8447d59783e93f54313e2c59558055ada5 Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 16 Jan 2017 16:55:14 +0100 Subject: [PATCH 75/98] Don't refresh resolved comments every times an entry changes --- .../controllers/ReviewPanelController.coffee | 11 ++++++----- .../directives/resolvedCommentsDropdown.coffee | 3 +-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 4c65208ad9..656cb6c9f0 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -163,11 +163,12 @@ define [ $scope.refreshResolvedCommentsDropdown = () -> $scope.reviewPanel.dropdown.loading = true - refreshRanges() - .then () -> - $scope.reviewPanel.dropdown.loading = false - .catch () -> - $scope.reviewPanel.dropdown.loading = false + q = refreshRanges() + q.then () -> + $scope.reviewPanel.dropdown.loading = false + q.catch () -> + $scope.reviewPanel.dropdown.loading = false + return q updateEntries = (doc_id) -> rangesTracker = getChangeTracker(doc_id) diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index f161338324..e70dcaf1ec 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -23,6 +23,7 @@ define [ scope.state.isOpen = !scope.state.isOpen if (scope.state.isOpen) scope.onOpen() + .then () -> filterResolvedComments() scope.resolvedComments = [] @@ -54,5 +55,3 @@ define [ resolvedComment.docName = getDocNameById(docId) scope.resolvedComments.push(resolvedComment) - - scope.$on "entries:changed", filterResolvedComments \ No newline at end of file From f12aeedb35a05535076a03d1bbe4218c8ccb349d Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 16 Jan 2017 16:57:20 +0100 Subject: [PATCH 76/98] Tweak padding around track changes toggle loading indicator --- services/web/public/stylesheets/app/editor/review-panel.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 646de2fc3a..707ac62b15 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -129,6 +129,9 @@ cursor: auto; margin-right: 5px; } + .review-panel-toolbar-spinner { + margin-left: 5px; + } .rp-entry-list { .rp-size-expanded & { From 65e257ca37157f4f645e200f0bef2c718e7bc789 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 16 Jan 2017 16:25:38 +0000 Subject: [PATCH 77/98] Change text marker colors on Ace dark themes. --- .../public/stylesheets/app/editor/review-panel.less | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 646de2fc3a..e7277e6be8 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -10,9 +10,11 @@ @rp-green : #2c8e30; @rp-dim-green : #cae3cb; +@rp-green-on-dark : rgba(37, 107, 41, 0.5); @rp-red : #c5060b; @rp-dim-red : #f3cdce; @rp-yellow : #f3b111; +@rp-yellow-on-dark : rgba(194, 93, 11, 0.5); @rp-dim-yellow : #ffe9b2; @rp-grey : #aaaaaa; @@ -703,6 +705,15 @@ border-left: 2px dotted @rp-red; margin-left: -1px; } + + .ace_dark { + .track-changes-comment-marker { + background-color: @rp-yellow-on-dark + } + .track-changes-added-marker { + background-color: @rp-green-on-dark; + } + } } .review-icon { From 121629426e971c71fc1d8a46b1896ddf341e3c0b Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 16 Jan 2017 17:06:57 +0000 Subject: [PATCH 78/98] Avoid empty comment replies. --- .../web/app/views/project/editor/review-panel.jade | 12 +++++++++--- .../ide/review-panel/directives/commentEntry.coffee | 5 +++-- .../public/stylesheets/app/editor/review-panel.less | 13 +++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index f0296a1d23..2646932ad2 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -195,10 +195,16 @@ script(type='text/ng-template', id='commentEntryTemplate') placeholder="{{ 'Hit \"Enter\" to reply' + (entry.resolved ? ' and re-open' : '') }}" ) .rp-entry-actions - a.rp-entry-button(href, ng-click="animateAndCallOnResolve();", ng-if="permissions.comment && permissions.write") + button.rp-entry-button( + ng-click="animateAndCallOnResolve();" + ng-if="permissions.comment && permissions.write" + ) i.fa.fa-inbox |  Resolve - a.rp-entry-button(href, ng-click="onReply();", ng-if="permissions.comment") + button.rp-entry-button( + ng-click="onReply();" + ng-if="permissions.comment" + ng-disabled="!entry.replyContent.length") i.fa.fa-reply |  Reply @@ -214,7 +220,7 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') href ng-if="needsCollapsing" ng-click="toggleCollapse();" - )  {{ isCollapsed ? '(more)' : '(less)' }} + )  {{ isCollapsed ? '... (show all)' : ' (show less)' }} .rp-comment( ng-repeat="comment in thread.messages track by comment.id" ) diff --git a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee index 2d32f7f20e..63a20c2243 100644 --- a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee @@ -18,8 +18,9 @@ define [ scope.handleCommentReplyKeyPress = (ev) -> if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey ev.preventDefault() - ev.target.blur() - scope.onReply() + if scope.entry.length > 0 + ev.target.blur() + scope.onReply() scope.animateAndCallOnResolve = () -> scope.state.animating = true diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 3c3d4dc250..36e420633b 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -36,12 +36,25 @@ color: #FFF; text-align: center; line-height: 1.3; + user-select: none; + border: 0; + &:hover, &:focus { + outline: 0; background-color: darken(@rp-highlight-blue, 5%); text-decoration: none; color: #FFF; } + + &[disabled] { + opacity: 0.5; + + &:hover, + &:focus { + background-color: @rp-highlight-blue; + } + } } .triangle(@_, @width, @height, @color) { From 36f15ff56ec70b846796456242c593d7c4d29818 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Mon, 16 Jan 2017 17:14:06 +0000 Subject: [PATCH 79/98] Avoid empty comment submission. --- .../web/app/views/project/editor/review-panel.jade | 12 +++++++++--- .../review-panel/directives/addCommentEntry.coffee | 5 +++-- .../ide/review-panel/directives/commentEntry.coffee | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 2646932ad2..03454b92a8 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -204,7 +204,8 @@ script(type='text/ng-template', id='commentEntryTemplate') button.rp-entry-button( ng-click="onReply();" ng-if="permissions.comment" - ng-disabled="!entry.replyContent.length") + ng-disabled="!entry.replyContent.length" + ) i.fa.fa-reply |  Reply @@ -284,10 +285,15 @@ script(type='text/ng-template', id='addCommentEntryTemplate') focus-on="comment:new:open" ) .rp-entry-actions - a.rp-entry-button(href, ng-click="cancelNewComment();") + button.rp-entry-button( + ng-click="cancelNewComment();" + ) i.fa.fa-times |  Cancel - a.rp-entry-button(href, ng-click="submitNewComment()") + button.rp-entry-button( + ng-click="submitNewComment()" + ng-disabled="!state.content.length" + ) i.fa.fa-comment |  Comment diff --git a/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee index 7f41f00fa5..124794e7b8 100644 --- a/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/addCommentEntry.coffee @@ -27,8 +27,9 @@ define [ scope.handleCommentKeyPress = (ev) -> if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey ev.preventDefault() - ev.target.blur() - scope.submitNewComment() + if scope.state.content.length > 0 + ev.target.blur() + scope.submitNewComment() scope.submitNewComment = () -> scope.onSubmit { content: scope.state.content } diff --git a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee index 63a20c2243..db54574d27 100644 --- a/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/commentEntry.coffee @@ -18,7 +18,7 @@ define [ scope.handleCommentReplyKeyPress = (ev) -> if ev.keyCode == 13 and !ev.shiftKey and !ev.ctrlKey and !ev.metaKey ev.preventDefault() - if scope.entry.length > 0 + if scope.entry.replyContent.length > 0 ev.target.blur() scope.onReply() From d66f2033bbdd5f0ef9db9e1359fea0da67e0793c Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 17 Jan 2017 10:16:10 +0000 Subject: [PATCH 80/98] Ensure that review panel elements are above, z-index-wise, Ace elements. --- services/web/public/stylesheets/app/editor/review-panel.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 36e420633b..c65e0e8240 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -111,7 +111,7 @@ border-left: solid 1px @rp-border-grey; font-size: @rp-base-font-size; color: @rp-type-blue; - z-index: 4; // 3 is Ace's gutter z-index value. + z-index: 6; } .review-panel-toolbar { From 1b63db91d053823682809b0397d044879d42eabd Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jan 2017 11:51:02 +0100 Subject: [PATCH 81/98] Add in a null check --- .../aceEditor/track-changes/TrackChangesManager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee index 5de7e3be2d..2d57100cc5 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -399,11 +399,11 @@ define [ session = @editor.getSession() markers = session.getMarkers() {background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change_id] - if background_marker_id? + if background_marker_id? and markers[background_marker_id]? background_marker = markers[background_marker_id] background_marker.range.start = start background_marker.range.end = end - if callout_marker_id? + if callout_marker_id? and markers[callout_marker_id]? callout_marker = markers[callout_marker_id] callout_marker.range.start = start callout_marker.range.end = start From 70c7e32e589e4f5f567a04fb2b5e2ff8351ef3f9 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 17 Jan 2017 10:52:04 +0000 Subject: [PATCH 82/98] Add a 'testEmail' email type --- .../coffee/Features/Email/EmailBuilder.coffee | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 306aad3d2a..1aa6f2e413 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -123,8 +123,6 @@ Thank you description: "Join #{ opts.project.name } at ShareLaTeX" }) - - templates.completeJoinGroupAccount = subject: _.template "Verify Email to join <%= group_name %> group" layout: BaseWithHeaderEmailLayout @@ -149,6 +147,21 @@ Thank You gmailGoToAction: null }) +templates.testEmail = + subject: _.template "Test Email from ShareLaTeX" + layout: PersonalEmailLayout + type: 'notification' + plainTextTemplate: _.template """ +Hi, + +This is a test email, from ShareLaTeX. + +#{settings.appName} - <%= siteUrl %> +""" + compiledTemplate: _.template """ +

This is a test email, from ShareLaTeX.

+ """ + module.exports = templates: templates @@ -163,4 +176,4 @@ module.exports = html: template.layout(opts) text: template?.plainTextTemplate?(opts) type:template.type - } \ No newline at end of file + } From 4a47d135c7159053681daf5e2c7af9a400df8fdb Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jan 2017 14:54:22 +0100 Subject: [PATCH 83/98] Reapply ops to track changes as current user when reconnecting --- services/web/public/coffee/ide/editor/Document.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee index fbef95e67b..9d7eca813a 100644 --- a/services/web/public/coffee/ide/editor/Document.coffee +++ b/services/web/public/coffee/ide/editor/Document.coffee @@ -360,8 +360,8 @@ define [ @ranges.track_changes = @doc.track_changes for op in @doc.getInflightOp() or [] @ranges.setIdSeed(@doc.track_changes_id_seeds.inflight) - @ranges.applyOp(op) + @ranges.applyOp(op, { user_id: @track_changes_as }) for op in @doc.getPendingOp() or [] @ranges.setIdSeed(@doc.track_changes_id_seeds.pending) - @ranges.applyOp(op) + @ranges.applyOp(op, { user_id: @track_changes_as }) @ranges.emit "redraw" From 960d1e8b2fe83d0af30d973560fc84066883159a Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jan 2017 15:04:49 +0100 Subject: [PATCH 84/98] Only call /project/.../threads if we need the threads --- .../web/app/views/project/editor/review-panel.jade | 2 +- .../controllers/ReviewPanelController.coffee | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 03454b92a8..7982a5f991 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -185,7 +185,7 @@ script(type='text/ng-template', id='commentEntryTemplate') | {{ comment.content }} .rp-entry-metadata | {{ comment.timestamp | date : 'MMM d, y h:mm a' }} - .rp-loading(ng-if="threads[entry.thread_id].submitting") + .rp-loading(ng-if="!threads[entry.thread_id] || threads[entry.thread_id].submitting") i.fa.fa-spinner.fa-spin .rp-comment-reply(ng-if="permissions.comment") textarea.rp-comment-input( diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 656cb6c9f0..675c8644ec 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -205,6 +205,9 @@ define [ if !$scope.users[change.metadata.user_id]? refreshChangeUsers(change.metadata.user_id) + if rangesTracker.comments.length > 0 + ensureThreadsAreLoaded() + for comment in rangesTracker.comments changed = true delete delete_changes[comment.id] @@ -394,7 +397,12 @@ define [ .error () -> _refreshingRangeUsers = false - refreshThreads = () -> + _threadsLoaded = false + ensureThreadsAreLoaded = () -> + if _threadsLoaded + # We get any updates in real time so only need to load them once. + return + _threadsLoaded = true $http.get "/project/#{$scope.project_id}/threads" .success (threads) -> for thread_id, _ of $scope.reviewPanel.resolvedThreadIds @@ -408,8 +416,6 @@ define [ $scope.reviewPanel.resolvedThreadIds[thread_id] = true $scope.reviewPanel.commentThreads = threads - refreshThreads() - formatComment = (comment) -> comment.user = formatUser(comment.user) comment.timestamp = new Date(comment.timestamp) From dec9b5bf1e5828d505e3092989ab1ae05e4dd5fc Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jan 2017 15:10:44 +0100 Subject: [PATCH 85/98] Use email if name is blank --- .../review-panel/controllers/ReviewPanelController.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 675c8644ec..91792255de 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -415,6 +415,8 @@ define [ thread.resolved_by_user = formatUser(thread.resolved_by_user) $scope.reviewPanel.resolvedThreadIds[thread_id] = true $scope.reviewPanel.commentThreads = threads + $timeout () -> + $scope.$broadcast "review-panel:layout" formatComment = (comment) -> comment.user = formatUser(comment.user) @@ -436,9 +438,9 @@ define [ name = "You" isSelf = true else - name = [user.first_name, user.last_name].filter((n) -> n?).join(" ") + name = [user.first_name, user.last_name].filter((n) -> n? and n != "").join(" ") if name == "" - name = "Unknown" + name = user.email?.split("@")[0] or "Unknown" isSelf = false return { id: id From e15d01874d35dbd3efc0dfa8b4a19eff3e3a37ef Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jan 2017 15:13:50 +0100 Subject: [PATCH 86/98] Remove comment from dropdown when it is reopened --- .../ide/review-panel/directives/resolvedCommentsDropdown.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee index e70dcaf1ec..fa556e2939 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentsDropdown.coffee @@ -29,6 +29,7 @@ define [ scope.handleUnresolve = (threadId) -> scope.onUnresolve({ threadId }) + scope.resolvedComments = scope.resolvedComments.filter (c) -> c.threadId != threadId scope.handleDelete = (entryId, threadId) -> scope.onDelete({ entryId, threadId }) From 1137ab0715e4b1c0ad7c41061fa056f3b05ee3a3 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 17 Jan 2017 14:35:37 +0000 Subject: [PATCH 87/98] Don't record redirect to static asset paths --- .../Authentication/AuthenticationController.coffee | 4 ++-- .../Authentication/AuthenticationControllerTests.coffee | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee index e8e3db4f93..485b046a85 100644 --- a/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee +++ b/services/web/app/coffee/Features/Authentication/AuthenticationController.coffee @@ -194,8 +194,8 @@ module.exports = AuthenticationController = _setRedirectInSession: (req, value) -> if !value? - value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else req.path - if req.session? + value = if Object.keys(req.query).length > 0 then "#{req.path}?#{querystring.stringify(req.query)}" else "#{req.path}" + if req.session? && !value.match(new RegExp('^\/(socket.io|js|stylesheets|img)\/.*$')) req.session.postLoginRedirect = value _getRedirectFromSession: (req) -> diff --git a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee index 515b888911..94e930c7b1 100644 --- a/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Authentication/AuthenticationControllerTests.coffee @@ -550,6 +550,15 @@ describe "AuthenticationController", -> @AuthenticationController._setRedirectInSession(@req, '/somewhere/specific') expect(@req.session.postLoginRedirect).to.equal "/somewhere/specific" + describe 'with a js path', -> + + beforeEach -> + @req = {session: {}} + + it 'should not set the redirect', -> + @AuthenticationController._setRedirectInSession(@req, '/js/something.js') + expect(@req.session.postLoginRedirect).to.equal undefined + describe '_getRedirectFromSession', -> beforeEach -> @req = {session: {postLoginRedirect: "/a?b=c"}} From 2f119e5787e0778f63e341df0e6b24ec93f17e7b Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 17 Jan 2017 14:39:31 +0000 Subject: [PATCH 88/98] Track review features usage. --- services/web/public/coffee/ide.coffee | 1 + .../controllers/ReviewPanelController.coffee | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 370f144ee1..b4a1d7b689 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -86,6 +86,7 @@ define [ ide.toggleReviewPanel = $scope.toggleReviewPanel = () -> $scope.ui.reviewPanelOpen = !$scope.ui.reviewPanelOpen + event_tracking.sendMB "rp-toggle-panel", { value : $scope.ui.reviewPanelOpen } $scope.$watch "ui.reviewPanelOpen", (value) -> if value? diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 91792255de..f3ade87b14 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -4,7 +4,7 @@ define [ "ide/colors/ColorManager" "ide/review-panel/RangesTracker" ], (App, EventEmitter, ColorManager, RangesTracker) -> - App.controller "ReviewPanelController", ($scope, $element, ide, $timeout, $http) -> + App.controller "ReviewPanelController", ($scope, $element, ide, $timeout, $http, event_tracking) -> $reviewPanelEl = $element.find "#review-panel" $scope.SubViews = @@ -272,9 +272,11 @@ define [ $scope.acceptChange = (entry_id) -> $http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/#{entry_id}/accept", {_csrf: window.csrfToken} $scope.$broadcast "change:accept", entry_id + event_tracking.sendMB "rp-change-accepted", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' } $scope.rejectChange = (entry_id) -> $scope.$broadcast "change:reject", entry_id + event_tracking.sendMB "rp-change-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' } $scope.startNewComment = () -> $scope.$broadcast "comment:select_line" @@ -291,7 +293,8 @@ define [ ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment") $timeout () -> $scope.$broadcast "review-panel:layout" - + event_tracking.sendMB "rp-new-comment", { size: content.length } + $scope.cancelNewComment = (entry) -> $timeout () -> $scope.$broadcast "review-panel:layout" @@ -307,6 +310,11 @@ define [ $http.post("/project/#{$scope.project_id}/thread/#{thread_id}/messages", {content, _csrf: window.csrfToken}) .error (error) -> ide.showGenericMessageModal("Error submitting comment", "Sorry, there was a problem submitting your comment") + + trackingMetadata = + view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' + size: entry.replyContent.length + thread: thread_id thread = getThread(thread_id) thread.submitting = true @@ -314,6 +322,7 @@ define [ entry.replying = false $timeout () -> $scope.$broadcast "review-panel:layout" + event_tracking.sendMB "rp-comment-reply", trackingMetadata $scope.cancelReply = (entry) -> entry.replying = false @@ -324,10 +333,12 @@ define [ entry.focused = false $http.post "/project/#{$scope.project_id}/thread/#{entry.thread_id}/resolve", {_csrf: window.csrfToken} _onCommentResolved(entry.thread_id, ide.$scope.user) + event_tracking.sendMB "rp-comment-resolve", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' } $scope.unresolveComment = (thread_id) -> _onCommentReopened(thread_id) $http.post "/project/#{$scope.project_id}/thread/#{thread_id}/reopen", {_csrf: window.csrfToken} + event_tracking.sendMB "rp-comment-reopen" _onCommentResolved = (thread_id, user) -> thread = $scope.reviewPanel.commentThreads[thread_id] @@ -354,9 +365,11 @@ define [ $scope.deleteComment = (entry_id, thread_id) -> _onCommentDeleted(thread_id) $scope.$broadcast "comment:remove", entry_id + event_tracking.sendMB "rp-comment-delete" $scope.setSubView = (subView) -> $scope.reviewPanel.subView = subView + event_tracking.sendMB "rp-subview-change", { subView } $scope.gotoEntry = (doc_id, entry) -> ide.editorManager.openDocId(doc_id, { gotoOffset: entry.offset }) @@ -364,6 +377,7 @@ define [ $scope.toggleTrackChanges = (value) -> $scope.editor.wantTrackChanges = value $http.post "/project/#{$scope.project_id}/track_changes", {_csrf: window.csrfToken, on: value} + event_tracking.sendMB "rp-trackchanges-toggle", { value } ide.socket.on "toggle-track-changes", (value) -> $scope.$apply () -> From 5a34d17947341419f2d77f89db48ce49eae91fc9 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jan 2017 15:59:18 +0100 Subject: [PATCH 89/98] Toggle track changes in a project based on the owner's feature switch --- .../app/coffee/Features/Project/ProjectEditorHandler.coffee | 6 ++++++ services/web/app/coffee/models/User.coffee | 2 +- services/web/app/views/project/editor/editor.jade | 2 +- services/web/app/views/project/editor/header.jade | 2 +- services/web/public/coffee/ide.coffee | 5 +---- .../review-panel/controllers/ReviewPanelController.coffee | 2 +- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee index 1a10321544..e25ff29b28 100644 --- a/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEditorHandler.coffee @@ -19,6 +19,11 @@ module.exports = ProjectEditorHandler = if !result.invites? result.invites = [] + + hasTrackChanges = false + for member in members + if member.privilegeLevel == "owner" and member.user?.featureSwitches?.track_changes + hasTrackChanges = true {owner, ownerFeatures, members} = @buildOwnerAndMembersViews(members) result.owner = owner @@ -32,6 +37,7 @@ module.exports = ProjectEditorHandler = compileGroup:"standard" templates: false references: false + trackChanges: hasTrackChanges }) return result diff --git a/services/web/app/coffee/models/User.coffee b/services/web/app/coffee/models/User.coffee index f120422128..c6e469a1f4 100644 --- a/services/web/app/coffee/models/User.coffee +++ b/services/web/app/coffee/models/User.coffee @@ -39,7 +39,7 @@ UserSchema = new Schema references: { type:Boolean, default: Settings.defaultFeatures.references } } featureSwitches : { - pdfng: { type: Boolean } + track_changes: { type: Boolean } } referal_id : {type:String, default:() -> uuid.v4().split("-")[0]} refered_users: [ type:ObjectId, ref:'User' ] diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index d31be444af..f57eb4dd2a 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -52,7 +52,7 @@ div.full-size( syntax-validation="settings.syntaxValidation", review-panel="reviewPanel", events-bridge="reviewPanelEventsBridge" - track-changes-enabled="trackChangesFeatureFlag", + track-changes-enabled="project.features.trackChanges", track-changes= "editor.trackChanges", doc-id="editor.open_doc_id" renderer-data="reviewPanel.rendererData" diff --git a/services/web/app/views/project/editor/header.jade b/services/web/app/views/project/editor/header.jade index d1305d28b9..601b18e9cf 100644 --- a/services/web/app/views/project/editor/header.jade +++ b/services/web/app/views/project/editor/header.jade @@ -87,7 +87,7 @@ header.toolbar.toolbar-header.toolbar-with-labels( a.btn.btn-full-height( href, - ng-if="trackChangesFeatureFlag", + ng-if="project.features.trackChanges", ng-class="{ active: ui.reviewPanelOpen }" ng-click="toggleReviewPanel()" ) diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 370f144ee1..9baaf1c66f 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -57,9 +57,6 @@ define [ else this.$originalApply(fn); - if window.location.search.match /tcon=true/ # track changes on - $scope.trackChangesFeatureFlag = true - $scope.state = { loading: true load_progress: 40 @@ -70,7 +67,7 @@ define [ view: "editor" chatOpen: false pdfLayout: 'sideBySide' - reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}") and $scope.trackChangesFeatureFlag + reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}") and $scope.project.features.trackChanges showCodeCheckerOnboarding: !window.userSettings.syntaxValidation? } $scope.user = window.user diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 91792255de..717513e884 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -129,7 +129,7 @@ define [ entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {} Object.keys(entries).length ), (nEntries) -> - $scope.reviewPanel.hasEntries = nEntries > 0 and $scope.trackChangesFeatureFlag + $scope.reviewPanel.hasEntries = nEntries > 0 and $scope.project.features.trackChanges $scope.$watch "ui.reviewPanelOpen", (reviewPanelOpen) -> return if !reviewPanelOpen? From 4d15fc30baa744c979cfec2db729d677bcfba26c Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jan 2017 16:55:18 +0100 Subject: [PATCH 90/98] Don't flash resolved comments when editor is loading --- services/web/app/views/project/editor/editor.jade | 3 ++- services/web/app/views/project/editor/review-panel.jade | 1 + services/web/public/coffee/ide.coffee | 2 +- .../review-panel/controllers/ReviewPanelController.coffee | 3 +++ services/web/public/stylesheets/app/editor/review-panel.less | 5 ++++- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index f57eb4dd2a..935ce8bb28 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -18,7 +18,8 @@ div.full-size( 'rp-state-overview': (reviewPanel.subView === SubViews.OVERVIEW),\ 'rp-size-mini': (!ui.reviewPanelOpen && reviewPanel.hasEntries),\ 'rp-size-expanded': ui.reviewPanelOpen,\ - 'rp-layout-left': reviewPanel.layoutToLeft\ + 'rp-layout-left': reviewPanel.layoutToLeft,\ + 'rp-loading-threads': reviewPanel.loadingThreads\ }" ) .loading-panel(ng-show="!editor.sharejs_doc || editor.opening") diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 7982a5f991..95e54368c9 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -52,6 +52,7 @@ on-reply="submitReply(entry, entry_id);" on-indicator-click="toggleReviewPanel();" permissions="permissions" + ng-if="!reviewPanel.loadingThreads" ) div(ng-if="entry.type === 'add-comment' && permissions.comment") diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 9baaf1c66f..b17b9b05f4 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -67,7 +67,7 @@ define [ view: "editor" chatOpen: false pdfLayout: 'sideBySide' - reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}") and $scope.project.features.trackChanges + reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}") showCodeCheckerOnboarding: !window.userSettings.syntaxValidation? } $scope.user = window.user diff --git a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee index 717513e884..d00e7078aa 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee @@ -25,6 +25,7 @@ define [ resolvedThreadIds: {} layoutToLeft: false rendererData: {} + loadingThreads: false $scope.$on "layout:pdf:linked", (event, state) -> $scope.reviewPanel.layoutToLeft = (state.east?.size < 220 || state.east?.initClosed) @@ -403,8 +404,10 @@ define [ # We get any updates in real time so only need to load them once. return _threadsLoaded = true + $scope.reviewPanel.loadingThreads = true $http.get "/project/#{$scope.project_id}/threads" .success (threads) -> + $scope.reviewPanel.loadingThreads = false for thread_id, _ of $scope.reviewPanel.resolvedThreadIds delete $scope.reviewPanel.resolvedThreadIds[thread_id] for thread_id, thread of threads diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index c65e0e8240..511c9b25b1 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -692,7 +692,7 @@ .track-changes-marker-callout { border-radius: 0; position: absolute; - .rp-state-overview & { + .rp-state-overview &, .rp-loading-threads & { display: none; } } @@ -709,6 +709,9 @@ .track-changes-marker { border-radius: 0; position: absolute; + .rp-loading-threads & { + display: none; + } } .track-changes-comment-marker { From f27118084ca895f0fb826e8ba7e2e86f5b7fd070 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 17 Jan 2017 16:03:41 +0000 Subject: [PATCH 91/98] Make review header icon behave like font-awesome icons. --- .../public/stylesheets/app/editor/review-panel.less | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 511c9b25b1..759c7705a6 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -736,10 +736,13 @@ } .review-icon { - position: absolute; + display: inline-block; background: url('/img/review-icon-sprite.png') top/30px no-repeat; width: 30px; - height: 30px; + + &::before { + content: '\00a0'; // Non-breakable space. A non-breakable character here makes this icon work like font-awesome. + } .toolbar .btn-full-height:hover & { background-position-y: -30px; @@ -749,11 +752,6 @@ .toolbar .btn-full-height:active & { background-position-y: -60px; } - - & + .toolbar-label { - margin-left: 34px; - } - } .resolved-comments-toggle { From 59ab66fca28af34a54a35b2111d7abb7a0823651 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 17 Jan 2017 16:25:18 +0000 Subject: [PATCH 92/98] Fix double-callback --- services/web/app/coffee/Features/Email/EmailSender.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Email/EmailSender.coffee b/services/web/app/coffee/Features/Email/EmailSender.coffee index 7a909e083e..69574c8276 100644 --- a/services/web/app/coffee/Features/Email/EmailSender.coffee +++ b/services/web/app/coffee/Features/Email/EmailSender.coffee @@ -41,7 +41,7 @@ else checkCanSendEmail = (options, callback)-> if !options.sendingUser_id? #email not sent from user, not rate limited - callback(null, true) + return callback(null, true) opts = endpointName: "send_email" timeInterval: 60 * 60 * 3 From 786c92b2fcd5e2d62b9e79291953a99d682a1a50 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 17 Jan 2017 17:34:11 +0100 Subject: [PATCH 93/98] Don't show delete comment button (until it works) --- .../web/app/views/project/editor/review-panel.jade | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/app/views/project/editor/review-panel.jade b/services/web/app/views/project/editor/review-panel.jade index 95e54368c9..0419884502 100644 --- a/services/web/app/views/project/editor/review-panel.jade +++ b/services/web/app/views/project/editor/review-panel.jade @@ -249,11 +249,11 @@ script(type='text/ng-template', id='resolvedCommentEntryTemplate') ng-click="onUnresolve({ 'threadId': thread.threadId });" ) |  Re-open - a.rp-entry-button( - href - ng-click="onDelete({ 'entryId': thread.entryId, 'threadId': thread.threadId });" - ) - |  Delete + //- a.rp-entry-button( + //- href + //- ng-click="onDelete({ 'entryId': thread.entryId, 'threadId': thread.threadId });" + //- ) + //- |  Delete script(type='text/ng-template', id='addCommentEntryTemplate') From 329c6af1682924c1467f916026ff95da856b0127 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Tue, 17 Jan 2017 17:20:51 +0000 Subject: [PATCH 94/98] Add a track-changes indicator in the editor, for when the review panel is closed. --- .../web/app/views/project/editor/editor.jade | 9 +++++ .../stylesheets/app/editor/review-panel.less | 39 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor/editor.jade b/services/web/app/views/project/editor/editor.jade index 935ce8bb28..e02b9ec3d0 100644 --- a/services/web/app/views/project/editor/editor.jade +++ b/services/web/app/views/project/editor/editor.jade @@ -58,6 +58,15 @@ div.full-size( doc-id="editor.open_doc_id" renderer-data="reviewPanel.rendererData" ) + + a.rp-track-changes-indicator( + href + ng-if="editor.wantTrackChanges" + ng-click="toggleReviewPanel();" + ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }" + ) Track changes is + strong on + include ./review-panel diff --git a/services/web/public/stylesheets/app/editor/review-panel.less b/services/web/public/stylesheets/app/editor/review-panel.less index 759c7705a6..54142f2dcc 100644 --- a/services/web/public/stylesheets/app/editor/review-panel.less +++ b/services/web/public/stylesheets/app/editor/review-panel.less @@ -827,4 +827,41 @@ color: darken(@rp-type-blue, 5%); text-decoration: none; } -} \ No newline at end of file +} + +.rp-track-changes-indicator { + display: none; + position: absolute; + top: 0; + right: @review-off-width; + padding: 5px 10px; + background-color: rgba(240, 240, 240, 0.9); + color: @rp-type-blue; + text-align: center; + border-bottom-left-radius: 3px; + font-size: 10px; + z-index: 2; + + &.rp-track-changes-indicator-on-dark { + background-color: rgba(88, 88, 88, .8); + color: #FFF; + + &:hover, + &:focus { + background-color: rgba(88, 88, 88, 1); + color: #FFF; + } + } + + &:hover, + &:focus { + outline: 0; + text-decoration: none; + background-color: rgba(240, 240, 240, 1); + color: @rp-type-blue; + } + + .rp-size-mini & { + display: block; + } +} From 49fe8ef3a3bdf1d6afb280a41bb0d3eb4cfd044c Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 18 Jan 2017 10:53:59 +0000 Subject: [PATCH 95/98] Update entry truncating logic while typing. --- .../coffee/ide/review-panel/directives/changeEntry.coffee | 7 +++++-- .../review-panel/directives/resolvedCommentEntry.coffee | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee index d21cdc10b2..96f5e50016 100644 --- a/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/changeEntry.coffee @@ -13,10 +13,13 @@ define [ onIndicatorClick: "&" link: (scope, element, attrs) -> scope.contentLimit = 40 - scope.needsCollapsing = scope.entry.content.length > scope.contentLimit scope.isCollapsed = true + scope.needsCollapsing = false scope.toggleCollapse = () -> scope.isCollapsed = !scope.isCollapsed $timeout () -> - scope.$emit "review-panel:layout" \ No newline at end of file + scope.$emit "review-panel:layout" + + scope.$watch "entry.content.length", (contentLength) -> + scope.needsCollapsing = contentLength > scope.contentLimit \ No newline at end of file diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee index 8b933e05f2..ac945a927b 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee @@ -11,10 +11,13 @@ define [ onDelete: "&" link: (scope, element, attrs) -> scope.contentLimit = 40 - scope.needsCollapsing = scope.thread.content.length > scope.contentLimit + scope.needsCollapsing = false scope.isCollapsed = true scope.toggleCollapse = () -> scope.isCollapsed = !scope.isCollapsed $timeout () -> - scope.$emit "review-panel:layout" \ No newline at end of file + scope.$emit "review-panel:layout" + + scope.$watch "thread.content.length", (contentLength) -> + scope.needsCollapsing = contentLength > scope.contentLimit \ No newline at end of file From 2589ac7d58f41a5e437cd3992dcdebb50c87c297 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 18 Jan 2017 10:55:51 +0000 Subject: [PATCH 96/98] Remove unneeded review panel layout event. --- .../ide/review-panel/directives/resolvedCommentEntry.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee index ac945a927b..8a1d42990b 100644 --- a/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee +++ b/services/web/public/coffee/ide/review-panel/directives/resolvedCommentEntry.coffee @@ -16,8 +16,6 @@ define [ scope.toggleCollapse = () -> scope.isCollapsed = !scope.isCollapsed - $timeout () -> - scope.$emit "review-panel:layout" scope.$watch "thread.content.length", (contentLength) -> scope.needsCollapsing = contentLength > scope.contentLimit \ No newline at end of file From 8922c9dbf57d8fbe55650d467457a3706c041ae0 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 18 Jan 2017 15:28:51 +0000 Subject: [PATCH 97/98] New email layout for "testEmail" --- .../coffee/Features/Email/EmailBuilder.coffee | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee index 1aa6f2e413..0a06a2a175 100644 --- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee +++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee @@ -147,20 +147,29 @@ Thank You gmailGoToAction: null }) + templates.testEmail = - subject: _.template "Test Email from ShareLaTeX" - layout: PersonalEmailLayout - type: 'notification' + subject: _.template "A Test Email from ShareLaTeX" + layout: BaseWithHeaderEmailLayout + type:"notification" plainTextTemplate: _.template """ Hi, -This is a test email, from ShareLaTeX. +This is a test email sent from ShareLaTeX. #{settings.appName} - <%= siteUrl %> """ - compiledTemplate: _.template """ -

This is a test email, from ShareLaTeX.

- """ + compiledTemplate: (opts) -> + SingleCTAEmailBody({ + title: "A Test Email from ShareLaTeX" + greeting: "Hi," + message: "This is a test email sent from ShareLaTeX" + secondaryMessage: null + ctaText: "Open ShareLaTeX" + ctaURL: "/" + gmailGoToAction: null + }) + module.exports = templates: templates From 36eb453aed4b3cc7fd407429116f94926cdca2ad Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 20 Jan 2017 13:52:31 +0000 Subject: [PATCH 98/98] Clarify logic of asyncform onsuccess and onerror handlers --- services/web/public/coffee/directives/asyncForm.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/web/public/coffee/directives/asyncForm.coffee b/services/web/public/coffee/directives/asyncForm.coffee index b3e13e8d61..d9ca11231b 100644 --- a/services/web/public/coffee/directives/asyncForm.coffee +++ b/services/web/public/coffee/directives/asyncForm.coffee @@ -33,7 +33,8 @@ define [ response.success = true response.error = false - if onSuccessHandler = scope[attrs.onSuccess] + onSuccessHandler = scope[attrs.onSuccess] + if onSuccessHandler onSuccessHandler(data, status, headers, config) return @@ -55,7 +56,8 @@ define [ response.success = false response.error = true - if onErrorHandler = scope[attrs.onError] + onErrorHandler = scope[attrs.onError] + if onErrorHandler onErrorHandler(data, status, headers, config) return