From 5ef7bad60fef4806f861c277189be58a6bcb918a Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 24 Nov 2017 14:58:35 +0000 Subject: [PATCH 001/109] removed groove --- services/web/public/coffee/libs.coffee | 1 - services/web/public/js/libs/groove.js | 84 -------------------------- 2 files changed, 85 deletions(-) delete mode 100644 services/web/public/js/libs/groove.js diff --git a/services/web/public/coffee/libs.coffee b/services/web/public/coffee/libs.coffee index 533891548b..5b3f2a165a 100644 --- a/services/web/public/coffee/libs.coffee +++ b/services/web/public/coffee/libs.coffee @@ -11,7 +11,6 @@ define [ "libs/angular-cookie" "libs/passfield" "libs/sixpack" - "libs/groove" "libs/angular-sixpack" "libs/ng-tags-input-3.0.0" ], () -> diff --git a/services/web/public/js/libs/groove.js b/services/web/public/js/libs/groove.js deleted file mode 100644 index 983b2a73dc..0000000000 --- a/services/web/public/js/libs/groove.js +++ /dev/null @@ -1,84 +0,0 @@ -!function(window) { - - window.Groove = { - - init: function(options) { - this._options = options; - if (typeof grooveOnReady != 'undefined') {grooveOnReady();} - }, - - createTicket: function(params, callback) { - var postData = serialize({ - "ticket[enduser_name]": params["name"], - "ticket[enduser_email]": params["email"], - "ticket[title]": params["subject"], - "ticket[enduser_about]": params["about"], - "ticket[label_string]": params["labels"], - "ticket[comments_attributes][0][body]": params["message"] - }); - - sendRequest(this._options.widget_ticket_url, function(req) { - if (callback) {callback(req);} - }, postData); - } - }; - - // http://www.quirksmode.org/js/xmlhttp.html - function sendRequest(url, callback, postData) { - var req = createXMLHTTPObject(); - if (!req) return; - var method = (postData) ? "POST" : "GET"; - req.open(method, url, true); - if (postData){ - try { - req.setRequestHeader('Content-type','application/x-www-form-urlencoded'); - } - catch(e) { - req.contentType = 'application/x-www-form-urlencoded'; - }; - }; - req.onreadystatechange = function () { - if (req.readyState != 4) return; - callback(req); - } - if (req.readyState == 4) return; - req.send(postData); - } - - var XMLHttpFactories = [ - function () {return new XDomainRequest()}, - function () {return new XMLHttpRequest()}, - function () {return new ActiveXObject("Msxml2.XMLHTTP")}, - function () {return new ActiveXObject("Msxml3.XMLHTTP")}, - function () {return new ActiveXObject("Microsoft.XMLHTTP")} - ]; - - function createXMLHTTPObject() { - var xmlhttp = false; - for (var i = 0; i < XMLHttpFactories.length; i++) { - try { - xmlhttp = XMLHttpFactories[i](); - } - catch (e) { - continue; - } - break; - } - return xmlhttp; - } - - function serialize(obj) { - var str = []; - for(var p in obj) { - if (obj[p]) { - str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); - } - } - return str.join("&"); -} - -if (typeof grooveOnLoad != 'undefined') {grooveOnLoad();} -}(window); - -Groove.init({"widget_ticket_url":"https://sharelatex-accounts.groovehq.com/widgets/f5ad3b09-7d99-431b-8af5-c5725e3760ce/ticket.json"}); - From 7eb2f0d917214643f44ec33c59e237ec5f626b79 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 27 Nov 2017 12:49:12 +0000 Subject: [PATCH 002/109] move generic sales modal to use new support endpoint --- services/web/app/views/subscriptions/plans.pug | 2 +- services/web/public/coffee/main/contact-us.coffee | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index 88584dba76..f096fef051 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -228,7 +228,7 @@ block content .form-group input#Field13.field.text.medium.span8.form-control(ng-model="form.position", name='Field13', type='text', value='', maxlength='255', tabindex='4', onkeyup='') .form-group - input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'ShareLaTeX for Universities';") + input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';") .form-group.text-center input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote') span(ng-show="sent") diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 7bb86a6b93..d370dd6b0f 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -74,7 +74,7 @@ define [ $modalInstance.close() - App.controller 'UniverstiesContactController', ($scope, $modal) -> + App.controller 'UniverstiesContactController', ($scope, $modal, $http) -> $scope.form = {} $scope.sent = false @@ -85,16 +85,16 @@ define [ return $scope.sending = true ticketNumber = Math.floor((1 + Math.random()) * 0x10000).toString(32) - params = + data = + _csrf : window.csrfToken name: $scope.form.name || $scope.form.email email: $scope.form.email labels: "#{$scope.form.source} accounts" message: "Please contact me with more details" - subject: $scope.form.subject + " - [#{ticketNumber}]" - about : "#{$scope.form.position || ''} #{$scope.form.university || ''}" + subject: "#{$scope.form.subject} - #{$scope.form.position} #{$scope.form.university}" + inbox: "accounts" - Groove.createTicket params, (err, json)-> + request = $http.post "/support", data + request.then (response)-> $scope.sent = true $scope.$apply() - - From 3f2ec422f84f04e4bbe6009e0ae35fec42d10c8e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 27 Nov 2017 13:52:31 +0000 Subject: [PATCH 003/109] change subject line to put details in subject --- services/web/public/coffee/main/contact-us.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index d370dd6b0f..21e8100863 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -91,7 +91,7 @@ define [ email: $scope.form.email labels: "#{$scope.form.source} accounts" message: "Please contact me with more details" - subject: "#{$scope.form.subject} - #{$scope.form.position} #{$scope.form.university}" + subject: "#{$scope.form.name} - General Enquiry - #{$scope.form.position} - #{$scope.form.university}" inbox: "accounts" request = $http.post "/support", data From b16862fa31d770d8325feffd8592344074a0d50e Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Mon, 27 Nov 2017 18:03:42 +0000 Subject: [PATCH 004/109] add error handling for form --- services/web/app/views/subscriptions/plans.pug | 4 +++- services/web/public/coffee/main/contact-us.coffee | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index f096fef051..8a179fa323 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -231,7 +231,9 @@ block content input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';") .form-group.text-center input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote') - span(ng-show="sent") + span(ng-show="sent && error") + p Error sending request. + span(ng-show="sent && error == false") p Request Sent, Thank you. .row diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 21e8100863..1eb67145e5 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -79,6 +79,7 @@ define [ $scope.form = {} $scope.sent = false $scope.sending = false + $scope.error = false $scope.contactUs = -> if !$scope.form.email? console.log "email not set" @@ -97,4 +98,5 @@ define [ request = $http.post "/support", data request.then (response)-> $scope.sent = true + $scope.error = (response.status != 200) $scope.$apply() From 7ce56aa3b63c9b9f44f6bff0c31b1b62877cf172 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 28 Nov 2017 10:13:54 +0000 Subject: [PATCH 005/109] add http catch onto form --- services/web/app/views/subscriptions/plans.pug | 8 ++++---- services/web/public/coffee/main/contact-us.coffee | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/subscriptions/plans.pug b/services/web/app/views/subscriptions/plans.pug index 8a179fa323..89a07abe1c 100644 --- a/services/web/app/views/subscriptions/plans.pug +++ b/services/web/app/views/subscriptions/plans.pug @@ -210,7 +210,7 @@ block content h3 #{translate("group_plan_enquiry")} .modal-body form.text-left.form(ng-controller="UniverstiesContactController", ng-submit="contactUs()", ng-cloak) - span(ng-show="sent == false") + span(ng-show="sent == false && error == false") .form-group label#title9(for='Field9') | Name @@ -231,10 +231,10 @@ block content input(ng-model="form.source", type="hidden", ng-init="form.source = '__ref__'; form.subject = 'General enquiry for larger ShareLaTeX use';") .form-group.text-center input#saveForm.btn-success.btn.btn-lg(name='saveForm', type='submit', ng-disabled="sending", value='Request a quote') - span(ng-show="sent && error") - p Error sending request. - span(ng-show="sent && error == false") + span(ng-show="sent == true && error == false") p Request Sent, Thank you. + span(ng-show="error") + p Error sending request. .row .col-md-12 diff --git a/services/web/public/coffee/main/contact-us.coffee b/services/web/public/coffee/main/contact-us.coffee index 1eb67145e5..138f459890 100644 --- a/services/web/public/coffee/main/contact-us.coffee +++ b/services/web/public/coffee/main/contact-us.coffee @@ -96,6 +96,11 @@ define [ inbox: "accounts" request = $http.post "/support", data + + request.catch ()-> + $scope.error = true + $scope.$apply() + request.then (response)-> $scope.sent = true $scope.error = (response.status != 200) From 9a073cdaf06c1f7145a8f73cdb3fb64c2e6970ff Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 1 Dec 2017 16:58:19 +0000 Subject: [PATCH 006/109] Add link to V1 dash --- services/web/app/views/project/list.pug | 4 +++ .../public/stylesheets/app/project-list.less | 30 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index deecc46911..48d2cf205a 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -63,6 +63,10 @@ block content aside.project-list-sidebar.col-md-2.col-xs-3 include ./list/side-bar + .project-list-sidebar-v1-link.col-md-2.col-xs-3 + span Want to go back to the V1 dashboard? + a.btn.btn-default(href=settings.overleaf.host + "/dash") Go back to V1 + .project-list-main.col-md-10.col-xs-9 include ./list/notifications include ./list/project-list diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 1ca170ff89..55dcd30643 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -1,4 +1,5 @@ @announcements-shadow: 0 2px 20px rgba(0, 0, 0, 0.5); +@v1-dash-link-height: 110px; @keyframes pulse { 0% { @@ -55,7 +56,7 @@ } .project-list-sidebar when (@is-overleaf) { - height: 100%; + height: calc(~"100% -" @v1-dash-link-height); overflow-x: hidden; overflow-y: auto; -ms-overflow-style: -ms-autohiding-scrollbar; @@ -110,6 +111,33 @@ } } +.project-list-sidebar-v1-link { + position: absolute; + bottom: 0; + height: @v1-dash-link-height; + background-color: @sidebar-hover-bg; // TODO: Fix var names + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + color: white; +} + + .project-list-sidebar-v1-link a { + display: block; + margin-left: auto; + margin-right: auto; + margin-top: 4px; + padding-top: 0; + padding-bottom: 0; + background-color: @sidebar-bg; // TODO: Fix var names + color: #fff; + } + + .project-list-sidebar-v1-link a:hover { + background-color: @sidebar-active-bg; // TODO: Fix var names + } + .userNotifications { ul { margin-bottom:0px; From 7f622e2e6b49220666a9423bbf75af512a09cd86 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 1 Dec 2017 17:14:53 +0000 Subject: [PATCH 007/109] Conditionally show V1 dash link --- .../app/coffee/Features/Project/ProjectController.coffee | 1 + services/web/app/views/project/list.pug | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 9d9b7d9a13..38b2eb66c3 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -182,6 +182,7 @@ module.exports = ProjectController = hasSubscription: results.hasSubscription[0] isShowingV1Projects: results.v1Projects? noV1Connection: results.v1Projects?.noConnection + showV1DashLink: true # TODO: false if SL } if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key? diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index 48d2cf205a..6702527612 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -63,9 +63,10 @@ block content aside.project-list-sidebar.col-md-2.col-xs-3 include ./list/side-bar - .project-list-sidebar-v1-link.col-md-2.col-xs-3 - span Want to go back to the V1 dashboard? - a.btn.btn-default(href=settings.overleaf.host + "/dash") Go back to V1 + if showV1DashLink + .project-list-sidebar-v1-link.col-md-2.col-xs-3 + span Want to go back to the V1 dashboard? + a.btn.btn-default(href=settings.overleaf.host + "/dash") Go back to V1 .project-list-main.col-md-10.col-xs-9 include ./list/notifications From d6e50ac940f33c7d140d21bd19c00420292df476 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 1 Dec 2017 17:16:37 +0000 Subject: [PATCH 008/109] Extract var to allowed to be overridden --- services/web/public/stylesheets/app/project-list.less | 1 - services/web/public/stylesheets/core/_common-variables.less | 1 + services/web/public/stylesheets/core/ol-variables.less | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 55dcd30643..a3ad20dd94 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -1,5 +1,4 @@ @announcements-shadow: 0 2px 20px rgba(0, 0, 0, 0.5); -@v1-dash-link-height: 110px; @keyframes pulse { 0% { diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 0b5d3bf744..4defe7bc9f 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -852,6 +852,7 @@ @sidebar-active-bg : @link-color; @sidebar-hover-bg : transparent; @sidebar-hover-text-decoration : underline; +@v1-dash-link-height : 0; @folders-menu-margin : 0; @folders-menu-line-height : 1.2; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 43646ec542..b72143c29b 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -110,6 +110,7 @@ @sidebar-active-bg : @ol-blue-gray-6; @sidebar-hover-bg : @ol-blue-gray-4; @sidebar-hover-text-decoration : none; +@v1-dash-link-height : 110px; @folders-menu-margin : 0 -(@grid-gutter-width / 2); @folders-menu-line-height : @structured-list-line-height; From e7f83c73ae58f83acb5d3258fc9bb5ea4a5ed859 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 1 Dec 2017 17:53:47 +0000 Subject: [PATCH 009/109] Extract v1 dash link styles to file so can define OL only vars --- .../stylesheets/_ol_style_includes.less | 1 + .../public/stylesheets/app/project-list.less | 27 ------------------- .../stylesheets/app/sidebar-v1-dash-link.less | 26 ++++++++++++++++++ .../public/stylesheets/core/ol-variables.less | 3 +++ services/web/public/stylesheets/ol-style.less | 3 ++- 5 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 services/web/public/stylesheets/_ol_style_includes.less create mode 100644 services/web/public/stylesheets/app/sidebar-v1-dash-link.less diff --git a/services/web/public/stylesheets/_ol_style_includes.less b/services/web/public/stylesheets/_ol_style_includes.less new file mode 100644 index 0000000000..c8505c2e56 --- /dev/null +++ b/services/web/public/stylesheets/_ol_style_includes.less @@ -0,0 +1 @@ +@import "app/sidebar-v1-dash-link.less"; diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index a3ad20dd94..9fe3dc2238 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -110,33 +110,6 @@ } } -.project-list-sidebar-v1-link { - position: absolute; - bottom: 0; - height: @v1-dash-link-height; - background-color: @sidebar-hover-bg; // TODO: Fix var names - text-align: center; - display: flex; - flex-direction: column; - justify-content: center; - color: white; -} - - .project-list-sidebar-v1-link a { - display: block; - margin-left: auto; - margin-right: auto; - margin-top: 4px; - padding-top: 0; - padding-bottom: 0; - background-color: @sidebar-bg; // TODO: Fix var names - color: #fff; - } - - .project-list-sidebar-v1-link a:hover { - background-color: @sidebar-active-bg; // TODO: Fix var names - } - .userNotifications { ul { margin-bottom:0px; diff --git a/services/web/public/stylesheets/app/sidebar-v1-dash-link.less b/services/web/public/stylesheets/app/sidebar-v1-dash-link.less new file mode 100644 index 0000000000..008d47662e --- /dev/null +++ b/services/web/public/stylesheets/app/sidebar-v1-dash-link.less @@ -0,0 +1,26 @@ +.project-list-sidebar-v1-link { + position: absolute; + bottom: 0; + height: @v1-dash-link-height; + background-color: @v1-dash-link-bg; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + color: white; +} + + .project-list-sidebar-v1-link a { + display: block; + margin-left: auto; + margin-right: auto; + margin-top: 4px; + padding-top: 0; + padding-bottom: 0; + background-color: @v1-dash-link-btn-bg; + color: #fff; + } + + .project-list-sidebar-v1-link a:hover { + background-color: @v1-dash-link-btn-hover-bg; + } \ No newline at end of file diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index b72143c29b..49e4196ebf 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -111,6 +111,9 @@ @sidebar-hover-bg : @ol-blue-gray-4; @sidebar-hover-text-decoration : none; @v1-dash-link-height : 110px; +@v1-dash-link-bg : @ol-blue-gray-4; +@v1-dash-link-btn-bg : @ol-blue-gray-5; +@v1-dash-link-btn-hover-bg : @ol-blue-gray-6; @folders-menu-margin : 0 -(@grid-gutter-width / 2); @folders-menu-line-height : @structured-list-line-height; diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less index 1e48b7284d..2a9a140611 100644 --- a/services/web/public/stylesheets/ol-style.less +++ b/services/web/public/stylesheets/ol-style.less @@ -1,4 +1,5 @@ // Core variables and mixins @import "core/ol-variables.less"; @import "app/ol-style-guide.less"; -@import "_style_includes.less"; \ No newline at end of file +@import "_style_includes.less"; +@import "_ol_style_includes.less"; \ No newline at end of file From bf23a096442978e383235a8ae1ffce67b1cfe5a1 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 1 Dec 2017 17:58:35 +0000 Subject: [PATCH 010/109] Move setting sidebar height to OL only file so var can be moved to specific scope --- services/web/public/stylesheets/app/project-list.less | 1 - .../web/public/stylesheets/app/sidebar-v1-dash-link.less | 6 ++++++ services/web/public/stylesheets/core/_common-variables.less | 1 - services/web/public/stylesheets/core/ol-variables.less | 1 - 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/web/public/stylesheets/app/project-list.less b/services/web/public/stylesheets/app/project-list.less index 9fe3dc2238..687cd545fc 100644 --- a/services/web/public/stylesheets/app/project-list.less +++ b/services/web/public/stylesheets/app/project-list.less @@ -55,7 +55,6 @@ } .project-list-sidebar when (@is-overleaf) { - height: calc(~"100% -" @v1-dash-link-height); overflow-x: hidden; overflow-y: auto; -ms-overflow-style: -ms-autohiding-scrollbar; diff --git a/services/web/public/stylesheets/app/sidebar-v1-dash-link.less b/services/web/public/stylesheets/app/sidebar-v1-dash-link.less index 008d47662e..b297920e5b 100644 --- a/services/web/public/stylesheets/app/sidebar-v1-dash-link.less +++ b/services/web/public/stylesheets/app/sidebar-v1-dash-link.less @@ -1,3 +1,9 @@ +@v1-dash-link-height: 110px; + +.project-list-sidebar { + height: calc(~"100% -" @v1-dash-link-height); +} + .project-list-sidebar-v1-link { position: absolute; bottom: 0; diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 4defe7bc9f..0b5d3bf744 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -852,7 +852,6 @@ @sidebar-active-bg : @link-color; @sidebar-hover-bg : transparent; @sidebar-hover-text-decoration : underline; -@v1-dash-link-height : 0; @folders-menu-margin : 0; @folders-menu-line-height : 1.2; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 49e4196ebf..61c74d0230 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -110,7 +110,6 @@ @sidebar-active-bg : @ol-blue-gray-6; @sidebar-hover-bg : @ol-blue-gray-4; @sidebar-hover-text-decoration : none; -@v1-dash-link-height : 110px; @v1-dash-link-bg : @ol-blue-gray-4; @v1-dash-link-btn-bg : @ol-blue-gray-5; @v1-dash-link-btn-hover-bg : @ol-blue-gray-6; From f5950099110275318d5458f8ddde4c9f62b67111 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 4 Dec 2017 11:23:09 +0000 Subject: [PATCH 011/109] Switch to existing v1 dash flag --- .../web/app/coffee/Features/Project/ProjectController.coffee | 1 - services/web/app/views/project/list.pug | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index 38b2eb66c3..9d9b7d9a13 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -182,7 +182,6 @@ module.exports = ProjectController = hasSubscription: results.hasSubscription[0] isShowingV1Projects: results.v1Projects? noV1Connection: results.v1Projects?.noConnection - showV1DashLink: true # TODO: false if SL } if Settings?.algolia?.app_id? and Settings?.algolia?.read_only_api_key? diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index 6702527612..c1c708de1f 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -63,7 +63,7 @@ block content aside.project-list-sidebar.col-md-2.col-xs-3 include ./list/side-bar - if showV1DashLink + if isShowingV1Projects .project-list-sidebar-v1-link.col-md-2.col-xs-3 span Want to go back to the V1 dashboard? a.btn.btn-default(href=settings.overleaf.host + "/dash") Go back to V1 From a25464d9256654e549ef22415d7cce4174a2a439 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 4 Dec 2017 11:34:00 +0000 Subject: [PATCH 012/109] Add check for OL host --- services/web/app/views/project/list.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index c1c708de1f..26c2082968 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -63,7 +63,7 @@ block content aside.project-list-sidebar.col-md-2.col-xs-3 include ./list/side-bar - if isShowingV1Projects + if isShowingV1Projects && settings.overleaf && settings.overleaf.host .project-list-sidebar-v1-link.col-md-2.col-xs-3 span Want to go back to the V1 dashboard? a.btn.btn-default(href=settings.overleaf.host + "/dash") Go back to V1 From 1fd518ec5819b50ff8189872bea9bcfa75388535 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 4 Dec 2017 12:22:27 +0000 Subject: [PATCH 013/109] Add query param to mark as explicitly using V1 dash --- services/web/app/views/project/list.pug | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index 26c2082968..ab8d42751c 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -66,7 +66,8 @@ block content if isShowingV1Projects && settings.overleaf && settings.overleaf.host .project-list-sidebar-v1-link.col-md-2.col-xs-3 span Want to go back to the V1 dashboard? - a.btn.btn-default(href=settings.overleaf.host + "/dash") Go back to V1 + a.btn.btn-default(href=settings.overleaf.host + "/dash?remember-dash=1") + | Go back to V1 .project-list-main.col-md-10.col-xs-9 include ./list/notifications From ab78cea48ddb6653c84fd6ac02d3f81b686bcbb7 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 4 Dec 2017 12:24:21 +0000 Subject: [PATCH 014/109] Move to sidebar template --- services/web/app/views/project/list.pug | 9 +- .../web/app/views/project/list/side-bar.pug | 275 +++++++++--------- 2 files changed, 142 insertions(+), 142 deletions(-) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index ab8d42751c..bcfd7954cb 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -60,14 +60,7 @@ block content .row.project-list-row(ng-cloak) .project-list-container(ng-if="projects.length > 0") - aside.project-list-sidebar.col-md-2.col-xs-3 - include ./list/side-bar - - if isShowingV1Projects && settings.overleaf && settings.overleaf.host - .project-list-sidebar-v1-link.col-md-2.col-xs-3 - span Want to go back to the V1 dashboard? - a.btn.btn-default(href=settings.overleaf.host + "/dash?remember-dash=1") - | Go back to V1 + include ./list/side-bar .project-list-main.col-md-10.col-xs-9 include ./list/notifications diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug index c1ffb6e30c..f01223293b 100644 --- a/services/web/app/views/project/list/side-bar.pug +++ b/services/web/app/views/project/list/side-bar.pug @@ -1,144 +1,151 @@ -.dropdown(dropdown) - a.btn.btn-primary.sidebar-new-proj-btn.dropdown-toggle( - href="#", - data-toggle="dropdown", - dropdown-toggle - ) - | #{translate("new_project")} +if isShowingV1Projects && settings.overleaf && settings.overleaf.host + .project-list-sidebar-v1-link.col-md-2.col-xs-3 + span Want to go back to the V1 dashboard? + a.btn.btn-default(href=settings.overleaf.host + "/dash?remember-dash=1") + | Go back to V1 - ul.dropdown-menu(role="menu") - li - a( - href, - sixpack-convert="first_sign_up", - ng-click="openCreateProjectModal()" - ) #{translate("blank_project")} - li - a( - href, - sixpack-convert="first_sign_up", - ng-click="openCreateProjectModal('example')" - ) #{translate("example_project")} - li - a( - href, - sixpack-convert="first_sign_up", - ng-click="openUploadProjectModal()" - ) #{translate("upload_project")} - != moduleIncludes("newProjectMenu", locals) - if (templates) - li.divider - li.dropdown-header #{translate("templates")} - each item in templates - li - a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)} - -.row-spaced(ng-if="projects.length > 0", ng-cloak) - ul.list-unstyled.folders-menu( - ng-controller="TagListController" - ) - li(ng-class="{active: (filter == 'all')}", ng-click="filterProjects('all')") - a(href) #{translate("all_projects")} - li(ng-class="{active: (filter == 'owned')}", ng-click="filterProjects('owned')") - a(href) #{translate("your_projects")} - li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')") - a(href) #{translate("shared_with_you")} - li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')") - a(href) #{translate("deleted_projects")} - if isShowingV1Projects - li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')") - a(href) #{translate("v1_projects")} - li.separator - h2 #{translate("folders")} - li.tag( - ng-repeat="tag in tags | orderBy:'name'", - ng-class="{active: tag.selected}", - ng-cloak, - ng-click="selectTag(tag)" +aside.project-list-sidebar.col-md-2.col-xs-3 + .dropdown(dropdown) + a.btn.btn-primary.sidebar-new-proj-btn.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle ) - a.tag-name(href) - i.icon.fa.fa-fw( - ng-class="{\ - 'fa-folder-open-o': tag.selected,\ - 'fa-folder-o': !tag.selected\ - }" - ) - span.name {{tag.name}} - span.subdued ({{tag.project_ids.length}}) - span.v1-badge( - ng-if="tag.isV1", - ng-cloak, - aria-label=translate("v1_badge") - tooltip-template="'v1TagTooltipTemplate'" - tooltip-append-to-body="true" + | #{translate("new_project")} + + ul.dropdown-menu(role="menu") + li + a( + href, + sixpack-convert="first_sign_up", + ng-click="openCreateProjectModal()" + ) #{translate("blank_project")} + li + a( + href, + sixpack-convert="first_sign_up", + ng-click="openCreateProjectModal('example')" + ) #{translate("example_project")} + li + a( + href, + sixpack-convert="first_sign_up", + ng-click="openUploadProjectModal()" + ) #{translate("upload_project")} + != moduleIncludes("newProjectMenu", locals) + if (templates) + li.divider + li.dropdown-header #{translate("templates")} + each item in templates + li + a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)} + + .row-spaced(ng-if="projects.length > 0", ng-cloak) + ul.list-unstyled.folders-menu( + ng-controller="TagListController" + ) + li(ng-class="{active: (filter == 'all')}", ng-click="filterProjects('all')") + a(href) #{translate("all_projects")} + li(ng-class="{active: (filter == 'owned')}", ng-click="filterProjects('owned')") + a(href) #{translate("your_projects")} + li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')") + a(href) #{translate("shared_with_you")} + li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')") + a(href) #{translate("deleted_projects")} + if isShowingV1Projects + li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')") + a(href) #{translate("v1_projects")} + li.separator + h2 #{translate("folders")} + li.tag( + ng-repeat="tag in tags | orderBy:'name'", + ng-class="{active: tag.selected}", + ng-cloak, + ng-click="selectTag(tag)" + ) + a.tag-name(href) + i.icon.fa.fa-fw( + ng-class="{\ + 'fa-folder-open-o': tag.selected,\ + 'fa-folder-o': !tag.selected\ + }" ) - span.dropdown.tag-menu(dropdown) - a.dropdown-toggle( - href="#", - data-toggle="dropdown", - dropdown-toggle, - stop-propagation="click" - ) - span.caret - ul.dropdown-menu.dropdown-menu-right( - role="menu" - ) - li - a(href, ng-click="renameTag(tag)", stop-propagation="click") - | #{translate("rename")} - li - a(href, ng-click="deleteTag(tag)", stop-propagation="click") - | #{translate("delete")} - li.tag.untagged( - ng-if="tags.length", - ng-cloak, - ng-click="selectUntagged()" - ng-class="{active: filter === 'untagged'}", - ) - a.tag-name(href) - | #{translate("uncategorized")} - span.subdued ({{ nUntagged }}) - li(ng-cloak) - a.tag(href, ng-click="openNewTagModal()") - i.fa.fa-fw.fa-plus - span.name #{translate("new_folder")} + span.name {{tag.name}} + span.subdued ({{tag.project_ids.length}}) + span.v1-badge( + ng-if="tag.isV1", + ng-cloak, + aria-label=translate("v1_badge") + tooltip-template="'v1TagTooltipTemplate'" + tooltip-append-to-body="true" + ) + span.dropdown.tag-menu(dropdown) + a.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle, + stop-propagation="click" + ) + span.caret + ul.dropdown-menu.dropdown-menu-right( + role="menu" + ) + li + a(href, ng-click="renameTag(tag)", stop-propagation="click") + | #{translate("rename")} + li + a(href, ng-click="deleteTag(tag)", stop-propagation="click") + | #{translate("delete")} + li.tag.untagged( + ng-if="tags.length", + ng-cloak, + ng-click="selectUntagged()" + ng-class="{active: filter === 'untagged'}", + ) + a.tag-name(href) + | #{translate("uncategorized")} + span.subdued ({{ nUntagged }}) + li(ng-cloak) + a.tag(href, ng-click="openNewTagModal()") + i.fa.fa-fw.fa-plus + span.name #{translate("new_folder")} -.row-spaced(ng-if="projects.length == 0", ng-cloak) - .first-project - div - i.fa.fa-arrow-up.fa-2x - div - strong #{translate("create_your_first_project")} - -- if (showUserDetailsArea) - span(ng-controller="LeftHandMenuPromoController", ng-cloak) + .row-spaced(ng-if="projects.length == 0", ng-cloak) + .first-project + div + i.fa.fa-arrow-up.fa-2x + div + strong #{translate("create_your_first_project")} - .row-spaced#userProfileInformation(ng-if="hasProjects") - div(ng-controller="UserProfileController") - hr(ng-show="percentComplete < 100") - .text-centered.user-profile(ng-show="percentComplete < 100") - .progress - .progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}") + - if (showUserDetailsArea) + span(ng-controller="LeftHandMenuPromoController", ng-cloak) - p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})} - - button#completeUserProfileInformation.btn.btn-info( - ng-hide="formVisable", - ng-click="openUserProfileModal()" - ) #{translate("complete")} + .row-spaced#userProfileInformation(ng-if="hasProjects") + div(ng-controller="UserProfileController") + hr(ng-show="percentComplete < 100") + .text-centered.user-profile(ng-show="percentComplete < 100") + .progress + .progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}") + + p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})} + + button#completeUserProfileInformation.btn.btn-info( + ng-hide="formVisable", + ng-click="openUserProfileModal()" + ) #{translate("complete")} - .row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered - hr - p.small #{translate("on_free_sl")} - p - a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")} - p.small.text-centered - | #{translate("or_unlock_features_bonus")} - a(href="/user/bonus") #{translate("sharing_sl")} . - - -script. - window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)} + .row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered + hr + p.small #{translate("on_free_sl")} + p + a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")} + p.small.text-centered + | #{translate("or_unlock_features_bonus")} + a(href="/user/bonus") #{translate("sharing_sl")} . + + + script. + window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)} From 77dc7adf1f1af25840da3b27977da10648d080a1 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 4 Dec 2017 14:53:18 +0000 Subject: [PATCH 015/109] Revert "Move to sidebar template" This reverts commit 15dc1a35184aade8912d129ea75248ffc8b1b4bd. --- services/web/app/views/project/list.pug | 9 +- .../web/app/views/project/list/side-bar.pug | 275 +++++++++--------- 2 files changed, 142 insertions(+), 142 deletions(-) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index bcfd7954cb..ab8d42751c 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -60,7 +60,14 @@ block content .row.project-list-row(ng-cloak) .project-list-container(ng-if="projects.length > 0") - include ./list/side-bar + aside.project-list-sidebar.col-md-2.col-xs-3 + include ./list/side-bar + + if isShowingV1Projects && settings.overleaf && settings.overleaf.host + .project-list-sidebar-v1-link.col-md-2.col-xs-3 + span Want to go back to the V1 dashboard? + a.btn.btn-default(href=settings.overleaf.host + "/dash?remember-dash=1") + | Go back to V1 .project-list-main.col-md-10.col-xs-9 include ./list/notifications diff --git a/services/web/app/views/project/list/side-bar.pug b/services/web/app/views/project/list/side-bar.pug index f01223293b..c1ffb6e30c 100644 --- a/services/web/app/views/project/list/side-bar.pug +++ b/services/web/app/views/project/list/side-bar.pug @@ -1,151 +1,144 @@ -if isShowingV1Projects && settings.overleaf && settings.overleaf.host - .project-list-sidebar-v1-link.col-md-2.col-xs-3 - span Want to go back to the V1 dashboard? - a.btn.btn-default(href=settings.overleaf.host + "/dash?remember-dash=1") - | Go back to V1 +.dropdown(dropdown) + a.btn.btn-primary.sidebar-new-proj-btn.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle + ) + | #{translate("new_project")} -aside.project-list-sidebar.col-md-2.col-xs-3 - .dropdown(dropdown) - a.btn.btn-primary.sidebar-new-proj-btn.dropdown-toggle( - href="#", - data-toggle="dropdown", - dropdown-toggle + ul.dropdown-menu(role="menu") + li + a( + href, + sixpack-convert="first_sign_up", + ng-click="openCreateProjectModal()" + ) #{translate("blank_project")} + li + a( + href, + sixpack-convert="first_sign_up", + ng-click="openCreateProjectModal('example')" + ) #{translate("example_project")} + li + a( + href, + sixpack-convert="first_sign_up", + ng-click="openUploadProjectModal()" + ) #{translate("upload_project")} + != moduleIncludes("newProjectMenu", locals) + if (templates) + li.divider + li.dropdown-header #{translate("templates")} + each item in templates + li + a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)} + +.row-spaced(ng-if="projects.length > 0", ng-cloak) + ul.list-unstyled.folders-menu( + ng-controller="TagListController" + ) + li(ng-class="{active: (filter == 'all')}", ng-click="filterProjects('all')") + a(href) #{translate("all_projects")} + li(ng-class="{active: (filter == 'owned')}", ng-click="filterProjects('owned')") + a(href) #{translate("your_projects")} + li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')") + a(href) #{translate("shared_with_you")} + li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')") + a(href) #{translate("deleted_projects")} + if isShowingV1Projects + li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')") + a(href) #{translate("v1_projects")} + li.separator + h2 #{translate("folders")} + li.tag( + ng-repeat="tag in tags | orderBy:'name'", + ng-class="{active: tag.selected}", + ng-cloak, + ng-click="selectTag(tag)" ) - | #{translate("new_project")} - - ul.dropdown-menu(role="menu") - li - a( - href, - sixpack-convert="first_sign_up", - ng-click="openCreateProjectModal()" - ) #{translate("blank_project")} - li - a( - href, - sixpack-convert="first_sign_up", - ng-click="openCreateProjectModal('example')" - ) #{translate("example_project")} - li - a( - href, - sixpack-convert="first_sign_up", - ng-click="openUploadProjectModal()" - ) #{translate("upload_project")} - != moduleIncludes("newProjectMenu", locals) - if (templates) - li.divider - li.dropdown-header #{translate("templates")} - each item in templates + a.tag-name(href) + i.icon.fa.fa-fw( + ng-class="{\ + 'fa-folder-open-o': tag.selected,\ + 'fa-folder-o': !tag.selected\ + }" + ) + span.name {{tag.name}} + span.subdued ({{tag.project_ids.length}}) + span.v1-badge( + ng-if="tag.isV1", + ng-cloak, + aria-label=translate("v1_badge") + tooltip-template="'v1TagTooltipTemplate'" + tooltip-append-to-body="true" + ) + span.dropdown.tag-menu(dropdown) + a.dropdown-toggle( + href="#", + data-toggle="dropdown", + dropdown-toggle, + stop-propagation="click" + ) + span.caret + ul.dropdown-menu.dropdown-menu-right( + role="menu" + ) li - a.menu-indent(href=item.url, sixpack-convert="first_sign_up") #{translate(item.name)} + a(href, ng-click="renameTag(tag)", stop-propagation="click") + | #{translate("rename")} + li + a(href, ng-click="deleteTag(tag)", stop-propagation="click") + | #{translate("delete")} + li.tag.untagged( + ng-if="tags.length", + ng-cloak, + ng-click="selectUntagged()" + ng-class="{active: filter === 'untagged'}", + ) + a.tag-name(href) + | #{translate("uncategorized")} + span.subdued ({{ nUntagged }}) + li(ng-cloak) + a.tag(href, ng-click="openNewTagModal()") + i.fa.fa-fw.fa-plus + span.name #{translate("new_folder")} - .row-spaced(ng-if="projects.length > 0", ng-cloak) - ul.list-unstyled.folders-menu( - ng-controller="TagListController" - ) - li(ng-class="{active: (filter == 'all')}", ng-click="filterProjects('all')") - a(href) #{translate("all_projects")} - li(ng-class="{active: (filter == 'owned')}", ng-click="filterProjects('owned')") - a(href) #{translate("your_projects")} - li(ng-class="{active: (filter == 'shared')}", ng-click="filterProjects('shared')") - a(href) #{translate("shared_with_you")} - li(ng-class="{active: (filter == 'archived')}", ng-click="filterProjects('archived')") - a(href) #{translate("deleted_projects")} - if isShowingV1Projects - li(ng-class="{active: (filter == 'v1')}", ng-click="filterProjects('v1')") - a(href) #{translate("v1_projects")} - li.separator - h2 #{translate("folders")} - li.tag( - ng-repeat="tag in tags | orderBy:'name'", - ng-class="{active: tag.selected}", - ng-cloak, - ng-click="selectTag(tag)" - ) - a.tag-name(href) - i.icon.fa.fa-fw( - ng-class="{\ - 'fa-folder-open-o': tag.selected,\ - 'fa-folder-o': !tag.selected\ - }" - ) - span.name {{tag.name}} - span.subdued ({{tag.project_ids.length}}) - span.v1-badge( - ng-if="tag.isV1", - ng-cloak, - aria-label=translate("v1_badge") - tooltip-template="'v1TagTooltipTemplate'" - tooltip-append-to-body="true" - ) - span.dropdown.tag-menu(dropdown) - a.dropdown-toggle( - href="#", - data-toggle="dropdown", - dropdown-toggle, - stop-propagation="click" - ) - span.caret - ul.dropdown-menu.dropdown-menu-right( - role="menu" - ) - li - a(href, ng-click="renameTag(tag)", stop-propagation="click") - | #{translate("rename")} - li - a(href, ng-click="deleteTag(tag)", stop-propagation="click") - | #{translate("delete")} - li.tag.untagged( - ng-if="tags.length", - ng-cloak, - ng-click="selectUntagged()" - ng-class="{active: filter === 'untagged'}", - ) - a.tag-name(href) - | #{translate("uncategorized")} - span.subdued ({{ nUntagged }}) - li(ng-cloak) - a.tag(href, ng-click="openNewTagModal()") - i.fa.fa-fw.fa-plus - span.name #{translate("new_folder")} +.row-spaced(ng-if="projects.length == 0", ng-cloak) + .first-project + div + i.fa.fa-arrow-up.fa-2x + div + strong #{translate("create_your_first_project")} + +- if (showUserDetailsArea) + span(ng-controller="LeftHandMenuPromoController", ng-cloak) - .row-spaced(ng-if="projects.length == 0", ng-cloak) - .first-project - div - i.fa.fa-arrow-up.fa-2x - div - strong #{translate("create_your_first_project")} + .row-spaced#userProfileInformation(ng-if="hasProjects") + div(ng-controller="UserProfileController") + hr(ng-show="percentComplete < 100") + .text-centered.user-profile(ng-show="percentComplete < 100") + .progress + .progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}") - - if (showUserDetailsArea) - span(ng-controller="LeftHandMenuPromoController", ng-cloak) - - .row-spaced#userProfileInformation(ng-if="hasProjects") - div(ng-controller="UserProfileController") - hr(ng-show="percentComplete < 100") - .text-centered.user-profile(ng-show="percentComplete < 100") - .progress - .progress-bar.progress-bar-info(ng-style="{'width' : (percentComplete+'%')}") - - p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})} - - button#completeUserProfileInformation.btn.btn-info( - ng-hide="formVisable", - ng-click="openUserProfileModal()" - ) #{translate("complete")} + p.small #{translate("profile_complete_percentage", {percentval:"{{percentComplete}}"})} + + button#completeUserProfileInformation.btn.btn-info( + ng-hide="formVisable", + ng-click="openUserProfileModal()" + ) #{translate("complete")} - .row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered - hr - p.small #{translate("on_free_sl")} - p - a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")} - p.small.text-centered - | #{translate("or_unlock_features_bonus")} - a(href="/user/bonus") #{translate("sharing_sl")} . - - - script. - window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)} + .row-spaced(ng-if="hasProjects && userHasNoSubscription", ng-cloak).text-centered + hr + p.small #{translate("on_free_sl")} + p + a(href="/user/subscription/plans", sixpack-convert="left-menu-upgraed-rotation").btn.btn-primary #{translate("upgrade")} + p.small.text-centered + | #{translate("or_unlock_features_bonus")} + a(href="/user/bonus") #{translate("sharing_sl")} . + + +script. + window.userHasNoSubscription = #{!!(settings.enableSubscriptions && !hasSubscription)} From e7d8848a6a74e43036a0306a89c8f6012ff56d5f Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 4 Dec 2017 15:12:26 +0000 Subject: [PATCH 016/109] Include possible overleaf subscription in feature setting --- .../Subscription/SubscriptionUpdater.coffee | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 12faf0e234..e26f39de07 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -8,6 +8,7 @@ Settings = require("settings-sharelatex") logger = require("logger-sharelatex") ObjectId = require('mongoose').Types.ObjectId ReferalAllocator = require("../Referal/ReferalAllocator") +Modules = require '../../infrastructure/Modules' oneMonthInSeconds = 60 * 60 * 24 * 30 @@ -106,17 +107,26 @@ module.exports = SubscriptionUpdater = SubscriptionLocator.getUsersSubscription user_id, cb groupSubscription: (cb)-> SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb + overleafPlanCode: (cb) -> + Modules.hooks.fire 'getOverleafPlanCode', user_id, cb async.series jobs, (err, results)-> if err? logger.err err:err, user_id:user, "error getting subscription or group for _setUsersMinimumFeatures" return callback(err) - {subscription, groupSubscription} = results - if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode - logger.log user_id:user_id, "using users subscription plan code for features" - UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback - else if groupSubscription? and groupSubscription.planCode? + {subscription, groupSubscription, overleafPlanCode} = results + # Group Subscription + if groupSubscription? and groupSubscription.planCode? logger.log user_id:user_id, "using group which user is memor of for features" UserFeaturesUpdater.updateFeatures user_id, groupSubscription.planCode, callback + # Personal Subscription + else if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode + logger.log user_id:user_id, "using users subscription plan code for features" + UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback + # Overleaf Subscription + else if overleafPlanCode? + logger.log user_id: user_id, "using the overleaf plan for features" + UserFeaturesUpdater.updateFeatures user_id, overleafPlanCode, callback + # Default else logger.log user_id:user_id, "using default features for user with no subscription or group" UserFeaturesUpdater.updateFeatures user_id, Settings.defaultPlanCode, (err)-> From c84c00d56d8c5334998118866cbfd83a519caf15 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 4 Dec 2017 15:19:03 +0000 Subject: [PATCH 017/109] Change query param for nicer api on OL side --- services/web/app/views/project/list.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index ab8d42751c..7ee28763a8 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -66,7 +66,7 @@ block content if isShowingV1Projects && settings.overleaf && settings.overleaf.host .project-list-sidebar-v1-link.col-md-2.col-xs-3 span Want to go back to the V1 dashboard? - a.btn.btn-default(href=settings.overleaf.host + "/dash?remember-dash=1") + a.btn.btn-default(href=settings.overleaf.host + "/dash?prefer-v1-dash=1") | Go back to V1 .project-list-main.col-md-10.col-xs-9 From 44c289d8dd28f5b1d7756524968cf413c71ddd1b Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 4 Dec 2017 15:34:46 +0000 Subject: [PATCH 018/109] Fix unit tests for SubscriptionUpdater --- .../Subscription/SubscriptionUpdaterTests.coffee | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee index 87ff474a0a..3f25465ae0 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee @@ -57,6 +57,7 @@ describe "SubscriptionUpdater", -> @ReferalAllocator = assignBonus:sinon.stub().callsArgWith(1) @ReferalAllocator.cock = true + @Modules = {hooks: {fire: sinon.stub().callsArgWith(2, null, null)}} @SubscriptionUpdater = SandboxedModule.require modulePath, requires: '../../models/Subscription': Subscription:@SubscriptionModel './UserFeaturesUpdater': @UserFeaturesUpdater @@ -65,6 +66,7 @@ describe "SubscriptionUpdater", -> "logger-sharelatex": log:-> 'settings-sharelatex': @Settings "../Referal/ReferalAllocator" : @ReferalAllocator + '../../infrastructure/Modules': @Modules describe "syncSubscription", -> @@ -204,10 +206,22 @@ describe "SubscriptionUpdater", -> assert.equal args[1], @groupSubscription.planCode done() + it "should call updateFeatures with the overleaf subscription if set", (done)-> + @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null) + @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, null) + @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, 'ol_pro') + + @SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=> + args = @UserFeaturesUpdater.updateFeatures.args[0] + assert.equal args[0], @adminUser._id + assert.equal args[1], 'ol_pro' + done() + it "should call not call updateFeatures with users subscription if the subscription plan code is the default one (downgraded)", (done)-> @subscription.planCode = @Settings.defaultPlanCode @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null, @subscription) @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, @groupSubscription) + @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null) @SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=> args = @UserFeaturesUpdater.updateFeatures.args[0] assert.equal args[0], @adminUser._id @@ -218,6 +232,7 @@ describe "SubscriptionUpdater", -> it "should call updateFeatures with default if there are no subscriptions for user", (done)-> @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null) @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null) + @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, null) @SubscriptionUpdater._setUsersMinimumFeatures @adminuser_id, (err)=> args = @UserFeaturesUpdater.updateFeatures.args[0] assert.equal args[0], @adminUser._id From 6a185a838dee8b46948c9d4dee0b4bbb2c4ffe6b Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 4 Dec 2017 17:20:49 +0000 Subject: [PATCH 019/109] Increase height & margin --- services/web/public/stylesheets/app/sidebar-v1-dash-link.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/public/stylesheets/app/sidebar-v1-dash-link.less b/services/web/public/stylesheets/app/sidebar-v1-dash-link.less index b297920e5b..072042226e 100644 --- a/services/web/public/stylesheets/app/sidebar-v1-dash-link.less +++ b/services/web/public/stylesheets/app/sidebar-v1-dash-link.less @@ -1,4 +1,4 @@ -@v1-dash-link-height: 110px; +@v1-dash-link-height: 130px; .project-list-sidebar { height: calc(~"100% -" @v1-dash-link-height); @@ -20,7 +20,7 @@ display: block; margin-left: auto; margin-right: auto; - margin-top: 4px; + margin-top: 12.5px; padding-top: 0; padding-bottom: 0; background-color: @v1-dash-link-btn-bg; From 1a6e831b9edcc964d73dd6757994549ec2745019 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 7 Dec 2017 14:14:42 +0000 Subject: [PATCH 020/109] Basic PDF viewer colors for v2. --- services/web/public/stylesheets/app/editor/pdf.less | 10 +++++++--- .../web/public/stylesheets/app/editor/toolbar.less | 4 ++-- .../web/public/stylesheets/core/_common-variables.less | 9 +++++++++ services/web/public/stylesheets/core/ol-variables.less | 6 ++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less index a873cd5e8f..aab748d17f 100644 --- a/services/web/public/stylesheets/app/editor/pdf.less +++ b/services/web/public/stylesheets/app/editor/pdf.less @@ -10,9 +10,13 @@ padding: 0 (@line-height-computed / 2); } +.pdf §{ + background-color: @pdf-bg; +} + .pdf-viewer, .pdf-logs, .pdf-errors, .pdf-uncompiled { .full-size; - top: 58px; + top: @pdf-top-offset; } .pdf-logs, .pdf-errors, .pdf-uncompiled, .pdf-validation-problems{ @@ -69,11 +73,11 @@ } .pdfjs-viewer { .full-size; - background-color: @gray-lighter; + background-color: @pdfjs-bg; overflow: scroll; canvas, div.pdf-canvas { background: white; - box-shadow: black 0px 0px 10px; + box-shadow: @pdf-page-shadow-color 0px 0px 10px; } div.pdf-canvas.pdfng-empty { background-color: white; diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less index 5c00f00567..1243c07634 100644 --- a/services/web/public/stylesheets/app/editor/toolbar.less +++ b/services/web/public/stylesheets/app/editor/toolbar.less @@ -129,11 +129,11 @@ } .toolbar-small-mixin() { - height: 32px; + height: @toolbar-small-height; } .toolbar-tall-mixin() { - height: 58px; + height: @toolbar-tall-height; padding-top: 10px; } .toolbar-alt-mixin() { diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index acf6bcc91f..a0342c5534 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -904,6 +904,8 @@ @toolbar-icon-btn-hover-shadow : 0 1px 0 rgba(0, 0, 0, 0.25); @toolbar-icon-btn-hover-boxshadow : inset 0 3px 5px rgba(0, 0, 0, 0.225); @toolbar-border-bottom : 1px solid @toolbar-border-color; +@toolbar-small-height : 32px; +@toolbar-tall-height : 58px; // Editor file-tree @file-tree-bg : transparent; @@ -917,6 +919,13 @@ @file-tree-item-selected-bg : transparent; @file-tree-multiselect-bg : lighten(@brand-info, 40%); @file-tree-multiselect-hover-bg : lighten(@brand-info, 30%); + +// PDF +@pdf-top-offset : @toolbar-tall-height; +@pdf-bg : transparent; +@pdfjs-bg : @gray-lighter; +@pdf-page-shadow-color : #000; + // Tags @tag-border-radius : 0.25em; @tag-bg-color : @label-default-bg; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 5c15ad2d3d..8116c79a6a 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -186,6 +186,12 @@ @file-tree-multiselect-bg : @ol-blue; @file-tree-multiselect-hover-bg : @ol-dark-blue; @file-tree-droppable-bg-color : tint(@ol-green, 5%); + +// PDF +@pdf-top-offset : @toolbar-small-height; +@pdf-bg : transparent; +@pdfjs-bg : @ol-blue-gray-1; +@pdf-page-shadow-color : rgba(0, 0, 0, 0.5); //== Colors // //## Gray and brand colors for use across Bootstrap. From 4e86a03b4632e70567ecd1f8765c5865a56e4c20 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 7 Dec 2017 15:06:32 +0000 Subject: [PATCH 021/109] Fix a problem with the order of module initialization --- .../app/coffee/Features/Subscription/SubscriptionUpdater.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index e26f39de07..33ade7a5ea 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -8,7 +8,6 @@ Settings = require("settings-sharelatex") logger = require("logger-sharelatex") ObjectId = require('mongoose').Types.ObjectId ReferalAllocator = require("../Referal/ReferalAllocator") -Modules = require '../../infrastructure/Modules' oneMonthInSeconds = 60 * 60 * 24 * 30 @@ -108,6 +107,7 @@ module.exports = SubscriptionUpdater = groupSubscription: (cb)-> SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb overleafPlanCode: (cb) -> + Modules = require '../../infrastructure/Modules' Modules.hooks.fire 'getOverleafPlanCode', user_id, cb async.series jobs, (err, results)-> if err? From 3adbc52089fc4af1a5d547b9d61642e610794f24 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 7 Dec 2017 16:28:07 +0000 Subject: [PATCH 022/109] Add a method to explicitely refresh subscription feature --- .../coffee/Features/Subscription/SubscriptionUpdater.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 33ade7a5ea..98d241fbd3 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -62,6 +62,9 @@ module.exports = SubscriptionUpdater = invited_emails: email }, callback + refreshSubscription: (user_id, callback=(err)->) -> + SubscriptionUpdater._setUsersMinimumFeatures user_id, callback + deleteSubscription: (subscription_id, callback = (error) ->) -> SubscriptionLocator.getSubscription subscription_id, (err, subscription) -> return callback(err) if err? From 5bea196b8faceadd5e6eb4f9289b88d217881d5d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 7 Dec 2017 16:35:39 +0000 Subject: [PATCH 023/109] Style logs, log hints and alerts in general. --- .../public/stylesheets/app/editor/pdf.less | 22 ++++----- .../public/stylesheets/components/alerts.less | 2 +- .../public/stylesheets/components/card.less | 1 - .../stylesheets/core/_common-variables.less | 5 +- .../public/stylesheets/core/ol-variables.less | 46 +++++++++++++++++-- 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less index aab748d17f..eadf39cb9e 100644 --- a/services/web/public/stylesheets/app/editor/pdf.less +++ b/services/web/public/stylesheets/app/editor/pdf.less @@ -10,7 +10,7 @@ padding: 0 (@line-height-computed / 2); } -.pdf §{ +.pdf { background-color: @pdf-bg; } @@ -183,7 +183,7 @@ cursor: pointer; .line-no { float: right; - color: @gray; + color: @log-line-no-color; font-weight: 700; .fa { @@ -303,22 +303,22 @@ } .alert-danger & { - color: @alert-danger-border; + color: @state-danger-border; } .alert-warning & { - color: @alert-warning-border; + color: @state-warning-border; } .alert-info & { - color: @alert-info-border; + color: @state-info-border; } } &-text, &-feedback-label { - color: @gray-dark; + color: @log-hints-color; font-size: 0.9rem; margin-bottom: 20px; } @@ -343,25 +343,25 @@ &-actions a, &-text a { .alert-danger & { - color: @alert-danger-text; + color: @state-danger-text; } .alert-warning & { - color: @alert-warning-text; + color: @state-warning-text; } .alert-info & { - color: @alert-info-text; + color: @state-info-text; } } &-feedback { - color: @gray-dark; + color: @log-hints-color; float: right; } &-extra-feedback { - color: @gray-dark; + color: @log-hints-color; font-size: 0.8rem; margin-top: 10px; padding-bottom: 5px; diff --git a/services/web/public/stylesheets/components/alerts.less b/services/web/public/stylesheets/components/alerts.less index 527c97b151..6ed93d29d4 100755 --- a/services/web/public/stylesheets/components/alerts.less +++ b/services/web/public/stylesheets/components/alerts.less @@ -10,7 +10,7 @@ padding: @alert-padding; margin-bottom: @line-height-computed; border-left: 3px solid transparent; - // border-radius: @alert-border-radius; + border-radius: @alert-border-radius; // Headings for larger alerts h4 { diff --git a/services/web/public/stylesheets/components/card.less b/services/web/public/stylesheets/components/card.less index 1e06fbe3b4..ac43c038b7 100644 --- a/services/web/public/stylesheets/components/card.less +++ b/services/web/public/stylesheets/components/card.less @@ -1,7 +1,6 @@ .card { background-color: white; border-radius: @border-radius-base; - -webkit-box-shadow: @card-box-shadow; box-shadow: @card-box-shadow; padding: @line-height-computed; .page-header { diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index a0342c5534..23db1a202b 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -550,7 +550,7 @@ //## Define alert colors, border radius, and padding. @alert-padding: 15px; -@alert-border-radius: @border-radius-base; +@alert-border-radius: 0; @alert-link-font-weight: bold; @alert-success-bg: @state-success-bg; @@ -925,7 +925,8 @@ @pdf-bg : transparent; @pdfjs-bg : @gray-lighter; @pdf-page-shadow-color : #000; - +@log-line-no-color : @gray; +@log-hints-color : @gray-dark; // Tags @tag-border-radius : 0.25em; @tag-bg-color : @label-default-bg; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 8116c79a6a..bb3ba436b7 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -64,6 +64,44 @@ @btn-info-bg : @ol-blue; @btn-info-border : transparent; +// Alerts +@alert-padding : 15px; +@alert-border-radius : @border-radius-base; +@alert-link-font-weight : bold; + +@alert-success-bg : @brand-success; +@alert-success-text : #FFF; +@alert-success-border: transparent; + +@alert-info-bg : @brand-info; +@alert-info-text : #FFF; +@alert-info-border : transparent; + +@alert-warning-bg : @brand-warning; +@alert-warning-text : #FFF; +@alert-warning-border: transparent; + +@alert-danger-bg : @brand-danger; +@alert-danger-text : #FFF; +@alert-danger-border : transparent; + + +// @state-success-text: darken(@brand-success, 20%); +// @state-success-bg: lighten(@brand-success, 50%); +// @state-success-border: darken(@brand-success, 5%); + +// @state-info-text: darken(@brand-info, 20%); +// @state-info-bg: lighten(@brand-info, 47%); +// @state-info-border: darken(@brand-info, 7%); + +// @state-warning-text: darken(@brand-warning, 10%); +// @state-warning-bg: lighten(@brand-warning, 45%); +// @state-warning-border: @brand-warning; + +// @state-danger-text: darken(@brand-danger, 10%); +// @state-danger-bg: lighten(@brand-danger, 50%); +// @state-danger-border: darken(@brand-danger, 5%); + // Tags @tag-border-radius : 9999px; @tag-bg-color : @ol-green; @@ -189,9 +227,11 @@ // PDF @pdf-top-offset : @toolbar-small-height; -@pdf-bg : transparent; -@pdfjs-bg : @ol-blue-gray-1; +@pdf-bg : @ol-blue-gray-1; +@pdfjs-bg : transparent; @pdf-page-shadow-color : rgba(0, 0, 0, 0.5); +@log-line-no-color : #FFF; +@log-hints-color : @ol-blue-gray-4; //== Colors // //## Gray and brand colors for use across Bootstrap. @@ -213,7 +253,7 @@ @brand-primary: @ol-green; @brand-success: @green; -@brand-info: @ol-dark-green; +@brand-info: @ol-blue; @brand-warning: @orange; @brand-danger: #E03A06; From a675e774b8f5828c869db4a066515feae2e8bca9 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 8 Dec 2017 11:29:11 +0000 Subject: [PATCH 024/109] Fix how the `getOverleafPlanCode` integration is done --- .../coffee/Features/Subscription/SubscriptionUpdater.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 98d241fbd3..9b5fe83f88 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -111,7 +111,8 @@ module.exports = SubscriptionUpdater = SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb overleafPlanCode: (cb) -> Modules = require '../../infrastructure/Modules' - Modules.hooks.fire 'getOverleafPlanCode', user_id, cb + Modules.hooks.fire 'getOverleafPlanCode', user_id, (err, results) -> + cb(err, results[0]) async.series jobs, (err, results)-> if err? logger.err err:err, user_id:user, "error getting subscription or group for _setUsersMinimumFeatures" From 6198491e5f38eece274d927a02ca1fafd4fd46ff Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 8 Dec 2017 11:31:44 +0000 Subject: [PATCH 025/109] Add unsupported file type error --- services/web/app/coffee/Features/Errors/Errors.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index 2e46dd692d..ce236d5968 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -26,8 +26,16 @@ InvalidNameError = (message) -> return error InvalidNameError.prototype.__proto__ = Error.prototype +UnsupportedFileType = (message) -> + error = new Error(message) + error.name = "UnsupportedFileType" + error.__proto__ = UnsupportedFileType.prototype + return error +UnsupportedFileType.prototype.__proto___ = Error.prototype + module.exports = Errors = NotFoundError: NotFoundError ServiceNotConfiguredError: ServiceNotConfiguredError TooManyRequestsError: TooManyRequestsError InvalidNameError: InvalidNameError + UnsupportedFileType: UnsupportedFileType From eefe56d0f77c40a0b794ea6b9e8de1687c071ae8 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Fri, 8 Dec 2017 11:37:24 +0000 Subject: [PATCH 026/109] Fix up error handling in SubscriptionUpdater --- .../coffee/Features/Subscription/SubscriptionUpdater.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index 9b5fe83f88..d3b1a0feb7 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -112,10 +112,11 @@ module.exports = SubscriptionUpdater = overleafPlanCode: (cb) -> Modules = require '../../infrastructure/Modules' Modules.hooks.fire 'getOverleafPlanCode', user_id, (err, results) -> - cb(err, results[0]) + cb(err, results?[0] || null) async.series jobs, (err, results)-> if err? - logger.err err:err, user_id:user, "error getting subscription or group for _setUsersMinimumFeatures" + logger.err err:err, user_id:user_id, + "error getting subscription or group for _setUsersMinimumFeatures" return callback(err) {subscription, groupSubscription, overleafPlanCode} = results # Group Subscription From 89bdb6eff921b69c9f49456bbb64f7305671029f Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 11 Dec 2017 11:45:53 +0000 Subject: [PATCH 027/109] Increase autocompile rollout to 60% --- .../web/app/coffee/Features/Project/ProjectController.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee index c7fad83bf6..da22373870 100644 --- a/services/web/app/coffee/Features/Project/ProjectController.coffee +++ b/services/web/app/coffee/Features/Project/ProjectController.coffee @@ -253,7 +253,7 @@ module.exports = ProjectController = # Extract data from user's ObjectId timestamp = parseInt(user_id.toString().substring(0, 8), 16) - rolloutPercentage = 40 # Percentage of users to roll out to + rolloutPercentage = 60 # Percentage of users to roll out to if !ProjectController._isInPercentageRollout('autocompile', user_id, rolloutPercentage) # Don't show if user is not part of roll out return cb(null, { enabled: false, showOnboarding: false }) From f5c914ed6cbd345f984cf4ba946ad536485f6ea1 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Mon, 11 Dec 2017 18:03:09 +0000 Subject: [PATCH 028/109] When upgrading track changes on V2, redirect to V1 trial page --- services/web/app/views/layout.pug | 4 ++++ .../web/public/coffee/main/account-upgrade.coffee | 11 +++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index e12d7d6f81..fc14c2030b 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -95,6 +95,10 @@ html(itemscope, itemtype='http://schema.org/Product') cdnDomain : '!{settings.templates.cdnDomain}', indexName : '!{settings.templates.indexName}' } + + - if (settings.overleaf && settings.overleaf.host) + script. + window.v1BaseUrl = '!{settings.overleaf.host}' body if(settings.recaptcha) diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee index 6144bea9ef..29bc472505 100644 --- a/services/web/public/coffee/main/account-upgrade.coffee +++ b/services/web/public/coffee/main/account-upgrade.coffee @@ -11,9 +11,12 @@ define [ w = window.open() go = () -> ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source) - url = "/user/subscription/new?planCode=#{plan}&ssp=true" - if couponCode? - url = "#{url}&cc=#{couponCode}" + if window.v1BaseUrl? + url = "#{window.v1BaseUrl}/users/trial" + else + url = "/user/subscription/new?planCode=#{plan}&ssp=true" + if couponCode? + url = "#{url}&cc=#{couponCode}" $scope.startedFreeTrial = true switch source @@ -27,7 +30,7 @@ define [ else event_tracking.sendMB "subscription-start-trial", { source, plan } - + w.location = url if $scope.shouldABTestPlans From 5c62ac4f8aed26112aafbbbb8ee5ac9816dbb4a3 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Tue, 12 Dec 2017 12:50:20 +0000 Subject: [PATCH 029/109] Change error type for consistency --- services/web/app/coffee/Features/Errors/Errors.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/services/web/app/coffee/Features/Errors/Errors.coffee b/services/web/app/coffee/Features/Errors/Errors.coffee index ce236d5968..d88b243418 100644 --- a/services/web/app/coffee/Features/Errors/Errors.coffee +++ b/services/web/app/coffee/Features/Errors/Errors.coffee @@ -26,16 +26,16 @@ InvalidNameError = (message) -> return error InvalidNameError.prototype.__proto__ = Error.prototype -UnsupportedFileType = (message) -> +UnsupportedFileTypeError = (message) -> error = new Error(message) - error.name = "UnsupportedFileType" - error.__proto__ = UnsupportedFileType.prototype + error.name = "UnsupportedFileTypeError" + error.__proto__ = UnsupportedFileTypeError.prototype return error -UnsupportedFileType.prototype.__proto___ = Error.prototype +UnsupportedFileTypeError.prototype.__proto___ = Error.prototype module.exports = Errors = NotFoundError: NotFoundError ServiceNotConfiguredError: ServiceNotConfiguredError TooManyRequestsError: TooManyRequestsError InvalidNameError: InvalidNameError - UnsupportedFileType: UnsupportedFileType + UnsupportedFileTypeError: UnsupportedFileTypeError From 603252f3b4c6d216fa657ec59d52796e8f943130 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Tue, 12 Dec 2017 16:23:24 +0000 Subject: [PATCH 030/109] Prevent user from trying to delete archived project they don't own This change disables the select-project checkbox if the user is on the 'archived' project pane and they don't own the project. The request to delete would fail anyway, but this prevents UI confusion --- services/web/app/views/project/list/item.pug | 1 + .../web/public/coffee/main/project-list/project-list.coffee | 3 +++ 2 files changed, 4 insertions(+) diff --git a/services/web/app/views/project/list/item.pug b/services/web/app/views/project/list/item.pug index f246ad34d4..a4bb240c4b 100644 --- a/services/web/app/views/project/list/item.pug +++ b/services/web/app/views/project/list/item.pug @@ -2,6 +2,7 @@ input.select-item( select-individual, type="checkbox", + ng-disabled="shouldDisableCheckbox(project)", ng-model="project.selected" stop-propagation="click" aria-label=translate('select_project') + " '{{ project.name }}'" diff --git a/services/web/public/coffee/main/project-list/project-list.coffee b/services/web/public/coffee/main/project-list/project-list.coffee index f4a6951e4a..75d4767ec0 100644 --- a/services/web/public/coffee/main/project-list/project-list.coffee +++ b/services/web/public/coffee/main/project-list/project-list.coffee @@ -462,6 +462,9 @@ define [ App.controller "ProjectListItemController", ($scope) -> + $scope.shouldDisableCheckbox = (project) -> + $scope.filter == 'archived' && project.accessLevel != 'owner' + $scope.projectLink = (project) -> if project.accessLevel == 'readAndWrite' and project.source == 'token' "/#{project.tokens.readAndWrite}" From 80e6a660be2f3fb938b938541206c94ba160b994 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Tue, 12 Dec 2017 17:21:01 +0000 Subject: [PATCH 031/109] wip, this does not work minified yet for some reason lib.js is not being requested no, console errors or network errors --- services/web/.gitignore | 9 ++++++++ .../infrastructure/ExpressLocals.coffee | 22 ++++++++++++++++++- services/web/app/views/layout.pug | 4 ++-- services/web/app/views/project/editor.pug | 4 ++-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/services/web/.gitignore b/services/web/.gitignore index a48481690a..8df4ab2fa6 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -67,6 +67,15 @@ public/minjs/ Gemfile.lock +public/js/libs/fineuploader-5.15.4.*.js +public/js/libs/fineuploader-*.js +public/js/libs/pdfjs-1.7.225/compatibility.*.js +public/js/libs/pdfjs-1.7.225/pdf.*.js +public/js/libs/pdfjs-1.7.225/pdf.worker.*.js +public/js/libs/require.*.js +public/stylesheets/ol-style.*.css +public/stylesheets/style.*.css + *.swp .DS_Store diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index b126819f56..d2aeae655c 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -13,6 +13,7 @@ Url = require "url" PackageVersions = require "./PackageVersions" htmlEncoder = new require("node-html-encoder").Encoder("numerical") fingerprints = {} +hashedFiles = {} Path = require 'path' Features = require "./Features" @@ -59,6 +60,19 @@ for paths in pathList logger.log "#{filePath}: #{hash}" fingerprints[filePath] = hash + if paths.length == 1 + #todo deal with ace multi file hash + path = paths[0] + splitPath = path.split("/") + filenameSplit = splitPath.pop().split(".") + filenameSplit.splice(filenameSplit.length-1, 0, hash) + splitPath.push(filenameSplit.join(".")) + hashPath = splitPath.join("/") + fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}" + fs.writeFileSync(fsHashPath, content) + hashedFiles[paths] = hashPath + + getFingerprint = (path) -> if fingerprints[path]? return fingerprints[path] @@ -123,6 +137,9 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> res.locals.buildJsPath = (jsFile, opts = {})-> path = Path.join(jsPath, jsFile) + if opts.hashedPath + return hashedFiles[path] + doFingerPrint = opts.fingerprint != false if !opts.qs? @@ -140,8 +157,11 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> path = path + "?" + qs return path - res.locals.buildCssPath = (cssFile)-> + res.locals.buildCssPath = (cssFile, opts)-> path = Path.join("/stylesheets/", cssFile) + if opts.hashedPath + hashedPath = hashedFiles[path] + return Url.resolve(staticFilesBase, hashedPath) return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path) res.locals.buildImgPath = (imgFile)-> diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index e12d7d6f81..537417dce3 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -23,7 +23,7 @@ html(itemscope, itemtype='http://schema.org/Product') link(rel="apple-touch-icon-precomposed", href="/" + settings.brandPrefix + "apple-touch-icon-precomposed.png") link(rel="mask-icon", href="/" + settings.brandPrefix + "mask-favicon.svg", color="#a93529") - link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css")) + link(rel='stylesheet', href=buildCssPath("/" + settings.brandPrefix + "style.css", {hashedPath:true})) block _headLinks @@ -152,7 +152,7 @@ html(itemscope, itemtype='http://schema.org/Product') } }; script( - data-main=buildJsPath('main.js', {fingerprint:false}), + data-main=buildJsPath('main.js', {hashedPath:true}), baseurl=fullJsPath, src=buildJsPath('libs/require.js') ) diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index ec1d129714..c49afee0b2 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -161,8 +161,8 @@ block requirejs window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}'); script( - data-main=buildJsPath("ide.js", {fingerprint:false}), + data-main=buildJsPath("ide.js", {hashedPath:true}), baseurl=fullJsPath, data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}), - src=buildJsPath('libs/require.js') + src=buildJsPath('libs/require.js', {hashedPath:true}) ) From 6fe937bcadcc9ff895381785e88f9cda8f39fcf4 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 13 Dec 2017 10:57:56 +0000 Subject: [PATCH 032/109] Finish unit tests for SubscriptionUpdater --- .../Subscription/SubscriptionUpdaterTests.coffee | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee index 3f25465ae0..93479a03a1 100644 --- a/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee +++ b/services/web/test/unit/coffee/Subscription/SubscriptionUpdaterTests.coffee @@ -209,7 +209,7 @@ describe "SubscriptionUpdater", -> it "should call updateFeatures with the overleaf subscription if set", (done)-> @SubscriptionLocator.getUsersSubscription.callsArgWith(1, null) @SubscriptionLocator.getGroupSubscriptionMemberOf.callsArgWith(1, null, null) - @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, 'ol_pro') + @Modules.hooks.fire = sinon.stub().callsArgWith(2, null, ['ol_pro']) @SubscriptionUpdater._setUsersMinimumFeatures @adminUser._id, (err)=> args = @UserFeaturesUpdater.updateFeatures.args[0] @@ -278,3 +278,13 @@ describe "SubscriptionUpdater", -> @SubscriptionUpdater._setUsersMinimumFeatures .calledWith(user_id) .should.equal true + + describe 'refreshSubscription', -> + beforeEach -> + @SubscriptionUpdater._setUsersMinimumFeatures = sinon.stub() + .callsArgWith(1, null) + + it 'should call to _setUsersMinimumFeatures', -> + @SubscriptionUpdater.refreshSubscription(@adminUser._id, ()->) + @SubscriptionUpdater._setUsersMinimumFeatures.callCount.should.equal 1 + @SubscriptionUpdater._setUsersMinimumFeatures.calledWith(@adminUser._id).should.equal true From 4c81cd874fb81d8c3581217f1efe48779d869a67 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 13 Dec 2017 11:09:43 +0000 Subject: [PATCH 033/109] Remove unnecessary method - handled by FreeTrialModalController --- .../controllers/TrackChangesUpgradeModalController.coffee | 5 ----- 1 file changed, 5 deletions(-) diff --git a/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee b/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee index ae8c049f69..3fcc5e416f 100644 --- a/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee +++ b/services/web/public/coffee/ide/review-panel/controllers/TrackChangesUpgradeModalController.coffee @@ -4,8 +4,3 @@ define [ App.controller "TrackChangesUpgradeModalController", ($scope, $modalInstance) -> $scope.cancel = () -> $modalInstance.dismiss() - - $scope.startFreeTrial = (source) -> - ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source) - window.open("/user/subscription/new?planCode=student_free_trial_7_days") - $scope.startedFreeTrial = true \ No newline at end of file From 65efbbce537bfe4a5428b85848c06a71c8ee1ca0 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 13 Dec 2017 13:06:38 +0000 Subject: [PATCH 034/109] seems to work now. --- .../web/app/coffee/infrastructure/ExpressLocals.coffee | 8 ++++++-- services/web/app/views/layout.pug | 6 +++--- services/web/app/views/project/editor.pug | 6 ++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index d2aeae655c..7aef6e5424 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -134,11 +134,15 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> res.locals.fullJsPath = Url.resolve(staticFilesBase, jsPath) res.locals.lib = PackageVersions.lib + + res.locals.buildJsPath = (jsFile, opts = {})-> path = Path.join(jsPath, jsFile) + + # TODO CDN? if opts.hashedPath - return hashedFiles[path] + path = hashedFiles[path] doFingerPrint = opts.fingerprint != false @@ -159,7 +163,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> res.locals.buildCssPath = (cssFile, opts)-> path = Path.join("/stylesheets/", cssFile) - if opts.hashedPath + if opts?.hashedPath hashedPath = hashedFiles[path] return Url.resolve(staticFilesBase, hashedPath) return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 537417dce3..d9bba35f0b 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -142,9 +142,9 @@ html(itemscope, itemtype='http://schema.org/Product') window.requirejs = { "paths" : { "moment": "libs/#{lib('moment')}", - "fineuploader": "libs/#{lib('fineuploader')}" + "fineuploader": "libs/#{lib('fineuploader')}", + "main": "#{buildJsPath('main.js', {hashedPath:true, fingerprint:false}).slice(0,-3)}", }, - "urlArgs": "fingerprint=#{fingerprint(jsPath + 'main.js')}-#{fingerprint(jsPath + 'libs.js')}", "config":{ "moment":{ "noGlobal": true @@ -152,7 +152,7 @@ html(itemscope, itemtype='http://schema.org/Product') } }; script( - data-main=buildJsPath('main.js', {hashedPath:true}), + data-main=buildJsPath('main.js', {hashedPath:false, fingerprint:false}), baseurl=fullJsPath, src=buildJsPath('libs/require.js') ) diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index c49afee0b2..a37ac6e6d5 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -131,7 +131,9 @@ block requirejs "pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf", "pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}", "ace": "#{lib('ace')}", - "fineuploader": "libs/#{lib('fineuploader')}" + "fineuploader": "libs/#{lib('fineuploader')}", + "ide": "#{buildJsPath('ide.js', {hashedPath:true, fingerprint:false}).slice(0,-3)}", + }, "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}", "waitSeconds": 0, @@ -161,7 +163,7 @@ block requirejs window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}'); script( - data-main=buildJsPath("ide.js", {hashedPath:true}), + data-main=buildJsPath("ide.js", {hashedPath:false}), baseurl=fullJsPath, data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}), src=buildJsPath('libs/require.js', {hashedPath:true}) From 88334959d708c2cef12b7fc5556d9a39823b8300 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 13 Dec 2017 14:13:45 +0000 Subject: [PATCH 035/109] added remove extension --- services/web/app/coffee/infrastructure/ExpressLocals.coffee | 3 +++ services/web/app/views/layout.pug | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 7aef6e5424..33dbe2f34a 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -157,6 +157,9 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> qs = querystring.stringify(opts.qs) + if opts.removeExtension == true + path = path.slice(0,-3) + if qs? and qs.length > 0 path = path + "?" + qs return path diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index d9bba35f0b..0220a5af71 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -143,7 +143,7 @@ html(itemscope, itemtype='http://schema.org/Product') "paths" : { "moment": "libs/#{lib('moment')}", "fineuploader": "libs/#{lib('fineuploader')}", - "main": "#{buildJsPath('main.js', {hashedPath:true, fingerprint:false}).slice(0,-3)}", + "main": "#{buildJsPath('main.js', {hashedPath:true, fingerprint:false, removeExtension:true})}" }, "config":{ "moment":{ From 357617d952f15dfb679470fa6e6d19a416899601 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 13 Dec 2017 14:19:19 +0000 Subject: [PATCH 036/109] works with libs compiled into it --- services/web/Gruntfile.coffee | 5 ++--- services/web/app/views/project/editor.pug | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index c49add4a91..374177e7d2 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -205,11 +205,10 @@ module.exports = (grunt) -> modules: [ { name: "main", - exclude: ["libs"] }, { name: "ide", - exclude: ["libs", "pdfjs-dist/build/pdf"] - }, { + exclude: ["pdfjs-dist/build/pdf"] + },{ name: "libs" },{ name: "ace/mode-latex" diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index a37ac6e6d5..19b2478f48 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -132,7 +132,7 @@ block requirejs "pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}", "ace": "#{lib('ace')}", "fineuploader": "libs/#{lib('fineuploader')}", - "ide": "#{buildJsPath('ide.js', {hashedPath:true, fingerprint:false}).slice(0,-3)}", + "ide": "#{buildJsPath('ide.js', {hashedPath:true, fingerprint:false, removeExtension:true})}" }, "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}", @@ -163,7 +163,7 @@ block requirejs window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}'); script( - data-main=buildJsPath("ide.js", {hashedPath:false}), + data-main=buildJsPath("ide.js", {hashedPath:false, fingerprint:false}), baseurl=fullJsPath, data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}), src=buildJsPath('libs/require.js', {hashedPath:true}) From ccbb15c82ef038bc8a3a23eeb7c6402fa47dd8e0 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 13 Dec 2017 14:54:08 +0000 Subject: [PATCH 037/109] remove fingerprint from ide --- services/web/app/views/project/editor.pug | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 19b2478f48..38ffdd3402 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -133,9 +133,7 @@ block requirejs "ace": "#{lib('ace')}", "fineuploader": "libs/#{lib('fineuploader')}", "ide": "#{buildJsPath('ide.js', {hashedPath:true, fingerprint:false, removeExtension:true})}" - }, - "urlArgs" : "fingerprint=#{fingerprint(jsPath + 'ide.js')}-#{fingerprint(jsPath + 'libs.js')}", "waitSeconds": 0, "shim": { "pdfjs-dist/build/pdf": { From b7a43d95e0e54035c55c8cd35eb49a661da4874e Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Wed, 13 Dec 2017 15:55:32 +0000 Subject: [PATCH 038/109] Remove unused method, now handled by FreeTrialModalController --- .../coffee/ide/pdf/controllers/PdfController.coffee | 8 -------- 1 file changed, 8 deletions(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 17afd8bbcb..90e5eca145 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -533,14 +533,6 @@ define [ else $scope.switchToSideBySideLayout() - $scope.startFreeTrial = (source) -> - ga?('send', 'event', 'subscription-funnel', 'compile-timeout', source) - - event_tracking.sendMB "subscription-start-trial", { source } - - window.open("/user/subscription/new?planCode=#{$scope.startTrialPlanCode}") - $scope.startedFreeTrial = true - App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) -> # enable per-user containers by default perUserCompile = true From f021f21f11c3c5049a555fc84edb70f71a3733ad Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 13 Dec 2017 16:37:51 +0000 Subject: [PATCH 039/109] only hash when minified --- services/web/app/views/layout.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 0220a5af71..f32e90a6ed 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -143,7 +143,7 @@ html(itemscope, itemtype='http://schema.org/Product') "paths" : { "moment": "libs/#{lib('moment')}", "fineuploader": "libs/#{lib('fineuploader')}", - "main": "#{buildJsPath('main.js', {hashedPath:true, fingerprint:false, removeExtension:true})}" + "main": "#{buildJsPath('main.js', {hashedPath:settings.useMinifiedJs, fingerprint:false, removeExtension:true})}", }, "config":{ "moment":{ From 716b3092088ee98e5891b36d437d7a468c7eddf2 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 13 Dec 2017 16:50:18 +0000 Subject: [PATCH 040/109] remove todo statments --- services/web/app/coffee/infrastructure/ExpressLocals.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 33dbe2f34a..7cac6743e5 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -61,7 +61,6 @@ for paths in pathList fingerprints[filePath] = hash if paths.length == 1 - #todo deal with ace multi file hash path = paths[0] splitPath = path.split("/") filenameSplit = splitPath.pop().split(".") @@ -139,8 +138,6 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> res.locals.buildJsPath = (jsFile, opts = {})-> path = Path.join(jsPath, jsFile) - - # TODO CDN? if opts.hashedPath path = hashedFiles[path] From 490c56c692cae2e856f86fca659bbf724f6a1cc2 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Wed, 13 Dec 2017 17:07:58 +0000 Subject: [PATCH 041/109] libs -> libraries don't use hash in non minjs version --- services/web/Gruntfile.coffee | 5 +++-- services/web/app/coffee/infrastructure/ExpressLocals.coffee | 2 +- services/web/app/views/layout.pug | 1 + services/web/app/views/project/editor.pug | 3 ++- services/web/public/coffee/base.coffee | 2 +- services/web/public/coffee/{libs.coffee => libraries.coffee} | 0 services/web/public/coffee/utils/underscore.coffee | 2 +- 7 files changed, 9 insertions(+), 6 deletions(-) rename services/web/public/coffee/{libs.coffee => libraries.coffee} (100%) diff --git a/services/web/Gruntfile.coffee b/services/web/Gruntfile.coffee index 374177e7d2..a0000360b3 100644 --- a/services/web/Gruntfile.coffee +++ b/services/web/Gruntfile.coffee @@ -205,11 +205,12 @@ module.exports = (grunt) -> modules: [ { name: "main", + exclude: ["libraries"] }, { name: "ide", - exclude: ["pdfjs-dist/build/pdf"] + exclude: ["pdfjs-dist/build/pdf", "libraries"] },{ - name: "libs" + name: "libraries" },{ name: "ace/mode-latex" },{ diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 7cac6743e5..46f9eefb86 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -43,7 +43,7 @@ pathList = [ ["#{jsPath}libs/require.js"] ["#{jsPath}ide.js"] ["#{jsPath}main.js"] - ["#{jsPath}libs.js"] + ["#{jsPath}libraries.js"] ["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js","#{jsPath}#{ace}/worker-latex.js","#{jsPath}#{ace}/snippets/latex.js"] ["#{jsPath}libs/#{pdfjs}/pdf.js"] ["#{jsPath}libs/#{pdfjs}/pdf.worker.js"] diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index f32e90a6ed..5aa41c56c7 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -144,6 +144,7 @@ html(itemscope, itemtype='http://schema.org/Product') "moment": "libs/#{lib('moment')}", "fineuploader": "libs/#{lib('fineuploader')}", "main": "#{buildJsPath('main.js', {hashedPath:settings.useMinifiedJs, fingerprint:false, removeExtension:true})}", + "libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, fingerprint:false, removeExtension:true})}", }, "config":{ "moment":{ diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 38ffdd3402..c3fd408e10 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -132,7 +132,8 @@ block requirejs "pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}", "ace": "#{lib('ace')}", "fineuploader": "libs/#{lib('fineuploader')}", - "ide": "#{buildJsPath('ide.js', {hashedPath:true, fingerprint:false, removeExtension:true})}" + "ide": "#{buildJsPath('ide.js', {hashedPath:settings.useMinifiedJs, fingerprint:false, removeExtension:true})}", + "libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, fingerprint:false, removeExtension:true})}", }, "waitSeconds": 0, "shim": { diff --git a/services/web/public/coffee/base.coffee b/services/web/public/coffee/base.coffee index 03d0ada365..c3847c9342 100644 --- a/services/web/public/coffee/base.coffee +++ b/services/web/public/coffee/base.coffee @@ -1,5 +1,5 @@ define [ - "libs" + "libraries" "modules/recursionHelper" "modules/errorCatcher" "modules/localStorage" diff --git a/services/web/public/coffee/libs.coffee b/services/web/public/coffee/libraries.coffee similarity index 100% rename from services/web/public/coffee/libs.coffee rename to services/web/public/coffee/libraries.coffee diff --git a/services/web/public/coffee/utils/underscore.coffee b/services/web/public/coffee/utils/underscore.coffee index 7a1551f90b..944abccb5d 100644 --- a/services/web/public/coffee/utils/underscore.coffee +++ b/services/web/public/coffee/utils/underscore.coffee @@ -1,5 +1,5 @@ define [ - "libs" + "libraries" ], () -> angular.module('underscore', []).factory '_', -> return window._ From bbbba701b019682b026634ab20ff6b3a827c99b7 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 14 Dec 2017 10:21:53 +0000 Subject: [PATCH 042/109] Update editor.pug --- services/web/app/views/project/editor.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index c3fd408e10..582e0f3541 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -165,5 +165,5 @@ block requirejs data-main=buildJsPath("ide.js", {hashedPath:false, fingerprint:false}), baseurl=fullJsPath, data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}), - src=buildJsPath('libs/require.js', {hashedPath:true}) + src=buildJsPath('libs/require.js', {hashedPath:false}) ) From 288af6772afcc2dcf3398ec927e5263965fdff50 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 14 Dec 2017 10:59:46 +0000 Subject: [PATCH 043/109] write hashes for all files we fingerprint --- services/web/.gitignore | 10 +++++++--- .../web/app/coffee/infrastructure/ExpressLocals.coffee | 3 +-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/services/web/.gitignore b/services/web/.gitignore index 8df4ab2fa6..d0053e47c8 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -69,12 +69,16 @@ Gemfile.lock public/js/libs/fineuploader-5.15.4.*.js public/js/libs/fineuploader-*.js -public/js/libs/pdfjs-1.7.225/compatibility.*.js -public/js/libs/pdfjs-1.7.225/pdf.*.js -public/js/libs/pdfjs-1.7.225/pdf.worker.*.js +public/js/libs/pdfjs-*/compatibility.*.js +public/js/libs/pdfjs-*/pdf.*.js +public/js/libs/pdfjs-*/pdf.worker.*.js public/js/libs/require.*.js public/stylesheets/ol-style.*.css public/stylesheets/style.*.css +public/js/ace-*/ace.*.js +public/js/ace-*/mode-latex.*.js +public/js/ace-*/snippets/latex.*.js +public/js/ace-*/worker-latex.*.js *.swp .DS_Store diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 46f9eefb86..51b44a8996 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -60,8 +60,7 @@ for paths in pathList logger.log "#{filePath}: #{hash}" fingerprints[filePath] = hash - if paths.length == 1 - path = paths[0] + for path in paths splitPath = path.split("/") filenameSplit = splitPath.pop().split(".") filenameSplit.splice(filenameSplit.length-1, 0, hash) From c5e2560b435503807868435e453c0faa2148a2d4 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 13 Dec 2017 11:38:57 +0000 Subject: [PATCH 044/109] Basic colors for chat. --- services/web/public/stylesheets/app/editor/chat.less | 10 ++++++---- .../web/public/stylesheets/core/_common-variables.less | 8 ++++++++ services/web/public/stylesheets/core/ol-variables.less | 9 +++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less index c782384cb7..a03999cc37 100644 --- a/services/web/public/stylesheets/app/editor/chat.less +++ b/services/web/public/stylesheets/app/editor/chat.less @@ -31,11 +31,13 @@ right: 0; bottom: @new-message-height; overflow-x: hidden; + background-color: @chat-bg; + li.message { margin: @line-height-computed / 2; .date { font-size: 12px; - color: @gray-light; + color: @chat-color; border-bottom: 1px solid @gray-lightest; margin-bottom: @line-height-computed / 2; text-align: center; @@ -63,8 +65,8 @@ .message { border-left: 3px solid transparent; font-size: 14px; - box-shadow: -1px 2px 3px #ddd; - border-raduis: @border-radius-base; + box-shadow: @chat-message-box-shadow; + border-radius: @chat-message-border-radius; position: relative; .message-content { @@ -124,7 +126,7 @@ .full-size; top: auto; height: @new-message-height; - background-color: @gray-lightest; + background-color: @chat-new-message-bg; padding: @line-height-computed / 4; border-top: 1px solid @editor-border-color; textarea { diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index ab3ef2ccc9..57833767be 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -925,6 +925,14 @@ @editor-toggler-hover-bg-color : #DDD; @synctex-controls-z-index : 3; @synctex-controls-padding : 0 2px; + +// Chat +@chat-bg : transparent; +@chat-color : @gray-light; +@chat-new-message-bg : @gray-lightest; +@chat-message-box-shadow : -1px 2px 3px #ddd; +@chat-message-border-radius : 0; + // Tags @tag-border-radius : 0.25em; @tag-bg-color : @label-default-bg; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 508ae6ed37..72281ad48c 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -196,6 +196,15 @@ @editor-toggler-hover-bg-color : @ol-green; @synctex-controls-z-index : 6; @synctex-controls-padding : 0; +@editor-border-color : @ol-blue-gray-5; + +// Chat +@chat-bg : @ol-blue-gray-2; +@chat-color : #FFF; +@chat-new-message-bg : @ol-blue-gray-3; +@chat-message-box-shadow : none; +@chat-message-border-radius : @border-radius-base; + //## Gray and brand colors for use across Bootstrap. @gray-darker: #252525; @gray-dark: #505050; From 9deddcf2a8b9c1b215d73ddd23e10d439458aeec Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 13 Dec 2017 11:39:17 +0000 Subject: [PATCH 045/109] Disable dark theme handling for v2, for now. --- services/web/public/stylesheets/app/editor/chat.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less index a03999cc37..9c1b2038ec 100644 --- a/services/web/public/stylesheets/app/editor/chat.less +++ b/services/web/public/stylesheets/app/editor/chat.less @@ -147,7 +147,7 @@ word-break: break-all; } -.editor-dark { +.editor-dark when (@is-overleaf = false) { .chat { .new-message { background-color: lighten(@editor-dark-background-color, 10%); From ca68f01a9dbdb097501b1251c1b57deb167cd62d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 13 Dec 2017 12:16:21 +0000 Subject: [PATCH 046/109] Build chat message styles on the controller side (allowing for extra parameters). --- services/web/app/views/project/editor/chat.pug | 7 ++----- .../ide/chat/controllers/ChatMessageController.coffee | 9 ++++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/services/web/app/views/project/editor/chat.pug b/services/web/app/views/project/editor/chat.pug index fcd47a81e3..cdfbcfc097 100644 --- a/services/web/app/views/project/editor/chat.pug +++ b/services/web/app/views/project/editor/chat.pug @@ -35,12 +35,9 @@ aside.chat( span(ng-if="message.user.first_name") {{ message.user.first_name }} span(ng-if="!message.user.first_name") {{ message.user.email }} .message( - ng-style="{\ - 'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)',\ - 'background-color': 'hsl({{ hue(message.user) }}, 60%, 97%)'\ - }" + ng-style="getMessageStyle(message.user);" ) - .arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 70%, 70%)'}") + .arrow(ng-style="getArrowStyle(message.user)") .message-content p( mathjax, diff --git a/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee b/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee index 30fbccd05a..302aa1fd94 100644 --- a/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee +++ b/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee @@ -3,9 +3,16 @@ define [ "ide/colors/ColorManager" ], (App, ColorManager) -> App.controller "ChatMessageController", ["$scope", "ide", ($scope, ide) -> - $scope.hue = (user) -> + hue = (user) -> if !user? return 0 else return ColorManager.getHueForUserId(user.id) + + $scope.getMessageStyle = (user) -> + "border-color" : "hsl(#{ hue(user) }, 70%, 70%)" + "background-color" : "hsl(#{ hue(user) }, 60%, 97%)" + + $scope.getArrowStyle = (user) -> + "border-color" : "hsl(#{ hue(user) }, 70%, 70%)" ] \ No newline at end of file From e47b20390b5cdbe918823f241b662bd4b60429aa Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 13 Dec 2017 12:16:44 +0000 Subject: [PATCH 047/109] Add per brand UI specific chat message styles. --- .../coffee/infrastructure/ExpressLocals.coffee | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index b126819f56..51e1209cdc 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -296,10 +296,14 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> webRouter.use (req, res, next) -> isOl = (Settings.brandPrefix == 'ol-') res.locals.uiConfig = - defaultResizerSizeOpen : if isOl then 2 else 24 - defaultResizerSizeClosed : if isOl then 2 else 24 - eastResizerCursor : if isOl then "ew-resize" else null - westResizerCursor : if isOl then "ew-resize" else null - chatResizerSizeOpen : if isOl then 2 else 12 - chatResizerSizeClosed : 0 + defaultResizerSizeOpen : if isOl then 2 else 24 + defaultResizerSizeClosed : if isOl then 2 else 24 + eastResizerCursor : if isOl then "ew-resize" else null + westResizerCursor : if isOl then "ew-resize" else null + chatResizerSizeOpen : if isOl then 2 else 12 + chatResizerSizeClosed : 0 + chatMessageBorderSaturation: if isOl then 90 else 70 + chatMessageBorderLightness : if isOl then 50 else 70 + chatMessageBgSaturation : if isOl then 90 else 60 + chatMessageBgLightness : if isOl then 50 else 97 next() From c03ce6fdf7aafacb27a31660f86ce6abd507f326 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 13 Dec 2017 13:21:42 +0000 Subject: [PATCH 048/109] Configure chat messages colors per brand. --- .../coffee/infrastructure/ExpressLocals.coffee | 8 ++++---- .../controllers/ChatMessageController.coffee | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 51e1209cdc..9b7e9197b4 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -302,8 +302,8 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> westResizerCursor : if isOl then "ew-resize" else null chatResizerSizeOpen : if isOl then 2 else 12 chatResizerSizeClosed : 0 - chatMessageBorderSaturation: if isOl then 90 else 70 - chatMessageBorderLightness : if isOl then 50 else 70 - chatMessageBgSaturation : if isOl then 90 else 60 - chatMessageBgLightness : if isOl then 50 else 97 + chatMessageBorderSaturation: if isOl then "90%" else "70%" + chatMessageBorderLightness : if isOl then "50%" else "70%" + chatMessageBgSaturation : if isOl then "90%" else "60%" + chatMessageBgLightness : if isOl then "50%" else "97%" next() diff --git a/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee b/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee index 302aa1fd94..f69e04cad2 100644 --- a/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee +++ b/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee @@ -3,6 +3,12 @@ define [ "ide/colors/ColorManager" ], (App, ColorManager) -> App.controller "ChatMessageController", ["$scope", "ide", ($scope, ide) -> + hslColorConfigs = + borderSaturation: window.uiConfig?.chatMessageBorderSaturation or "70%" + borderLightness : window.uiConfig?.chatMessageBorderLightness or "70%" + bgSaturation : window.uiConfig?.chatMessageBgSaturation or "60%" + bgLightness : window.uiConfig?.chatMessageBgLightness or "97%" + hue = (user) -> if !user? return 0 @@ -10,9 +16,15 @@ define [ return ColorManager.getHueForUserId(user.id) $scope.getMessageStyle = (user) -> - "border-color" : "hsl(#{ hue(user) }, 70%, 70%)" - "background-color" : "hsl(#{ hue(user) }, 60%, 97%)" + style = + "border-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })" + "background-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.bgSaturation }, #{ hslColorConfigs.bgLightness })" + console.log style + return style $scope.getArrowStyle = (user) -> - "border-color" : "hsl(#{ hue(user) }, 70%, 70%)" + style = + "border-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })" + console.log style + return style ] \ No newline at end of file From 36c79979bd2654315b224f0392c7f7c3caf0d8c2 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 13 Dec 2017 14:01:46 +0000 Subject: [PATCH 049/109] More color adjustments. --- .../web/app/coffee/infrastructure/ExpressLocals.coffee | 8 ++++---- services/web/public/stylesheets/app/editor/chat.less | 10 +++++++--- .../web/public/stylesheets/core/_common-variables.less | 6 ++++-- services/web/public/stylesheets/core/ol-variables.less | 8 +++++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 9b7e9197b4..6781aaffcd 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -302,8 +302,8 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> westResizerCursor : if isOl then "ew-resize" else null chatResizerSizeOpen : if isOl then 2 else 12 chatResizerSizeClosed : 0 - chatMessageBorderSaturation: if isOl then "90%" else "70%" - chatMessageBorderLightness : if isOl then "50%" else "70%" - chatMessageBgSaturation : if isOl then "90%" else "60%" - chatMessageBgLightness : if isOl then "50%" else "97%" + chatMessageBorderSaturation: if isOl then "100%" else "70%" + chatMessageBorderLightness : if isOl then "45%" else "70%" + chatMessageBgSaturation : if isOl then "100%" else "60%" + chatMessageBgLightness : if isOl then "45%" else "97%" next() diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less index 9c1b2038ec..96603df237 100644 --- a/services/web/public/stylesheets/app/editor/chat.less +++ b/services/web/public/stylesheets/app/editor/chat.less @@ -37,9 +37,12 @@ margin: @line-height-computed / 2; .date { font-size: 12px; - color: @chat-color; - border-bottom: 1px solid @gray-lightest; + color: @chat-message-date-color; margin-bottom: @line-height-computed / 2; + text-align: right; + } + .date when (@is-overleaf = false) { + border-bottom: 1px solid @gray-lightest; text-align: center; } .avatar { @@ -58,7 +61,7 @@ .name { font-size: 12px; - color: @gray-light; + color: @chat-message-name-color; margin-bottom: 4px; min-height: 16px; } @@ -72,6 +75,7 @@ .message-content { padding: @line-height-computed / 2; overflow-x: auto; + color: @chat-message-color; } .arrow { diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 57833767be..126c1a04fb 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -928,10 +928,12 @@ // Chat @chat-bg : transparent; -@chat-color : @gray-light; -@chat-new-message-bg : @gray-lightest; +@chat-message-color : @text-color; +@chat-message-date-color : @gray-light; +@chat-message-name-color : @gray-light; @chat-message-box-shadow : -1px 2px 3px #ddd; @chat-message-border-radius : 0; +@chat-new-message-bg : @gray-light; // Tags @tag-border-radius : 0.25em; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 72281ad48c..d705e3b00d 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -199,11 +199,13 @@ @editor-border-color : @ol-blue-gray-5; // Chat -@chat-bg : @ol-blue-gray-2; -@chat-color : #FFF; -@chat-new-message-bg : @ol-blue-gray-3; +@chat-bg : @ol-blue-gray-6; +@chat-message-color : #FFF; +@chat-message-name-color : #FFF; +@chat-message-date-color : @ol-blue-gray-2; @chat-message-box-shadow : none; @chat-message-border-radius : @border-radius-base; +@chat-new-message-bg : @gray-light; //## Gray and brand colors for use across Bootstrap. @gray-darker: #252525; From cbf656518f33ac6df64f488c7aec90d0b2a2f484 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 14 Dec 2017 11:24:47 +0000 Subject: [PATCH 050/109] remove versioned files from fingerprinting --- services/web/app/coffee/infrastructure/ExpressLocals.coffee | 5 ----- services/web/app/views/project/editor.pug | 1 - .../web/public/coffee/ide/editor/directives/aceEditor.coffee | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 51b44a8996..74c4b242fc 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -39,15 +39,10 @@ getFileContent = (filePath)-> logger.log "Generating file fingerprints..." pathList = [ - ["#{jsPath}libs/#{fineuploader}.js"] ["#{jsPath}libs/require.js"] ["#{jsPath}ide.js"] ["#{jsPath}main.js"] ["#{jsPath}libraries.js"] - ["#{jsPath}#{ace}/ace.js","#{jsPath}#{ace}/mode-latex.js","#{jsPath}#{ace}/worker-latex.js","#{jsPath}#{ace}/snippets/latex.js"] - ["#{jsPath}libs/#{pdfjs}/pdf.js"] - ["#{jsPath}libs/#{pdfjs}/pdf.worker.js"] - ["#{jsPath}libs/#{pdfjs}/compatibility.js"] ["/stylesheets/style.css"] ["/stylesheets/ol-style.css"] ] diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 582e0f3541..38ade1a6b2 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -156,7 +156,6 @@ block requirejs } } }; - window.aceFingerprint = "#{fingerprint(jsPath + lib('ace') + '/ace.js')}" window.aceWorkerPath = "#{aceWorkerPath}"; window.pdfCMapsPath = "#{pdfCMapsPath}" window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}'); diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 6908b25bce..ff5d1554e2 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -33,7 +33,7 @@ define [ if !ace.config._moduleUrl? ace.config._moduleUrl = ace.config.moduleUrl ace.config.moduleUrl = (args...) -> - url = ace.config._moduleUrl(args...) + "?fingerprint=#{window.aceFingerprint}" + url = ace.config._moduleUrl(args...) return url App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, metadata, graphics, preamble, files, $http, $q) -> From 907654747ba5665652ff6c84a975dac38c6def0d Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 13 Dec 2017 14:18:25 +0000 Subject: [PATCH 051/109] Style the chat input box. --- services/web/public/stylesheets/app/editor/chat.less | 3 ++- .../web/public/stylesheets/core/_common-variables.less | 10 ++++++++++ services/web/public/stylesheets/core/ol-variables.less | 6 ++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less index 96603df237..1f24f2e4b0 100644 --- a/services/web/public/stylesheets/app/editor/chat.less +++ b/services/web/public/stylesheets/app/editor/chat.less @@ -140,9 +140,10 @@ border: 1px solid @editor-border-color; height: 100%; width: 100%; - color: @gray-dark; + color: @chat-new-message-textarea-color; font-size: 14px; padding: @line-height-computed / 4; + background-color: @chat-new-message-textarea-bg; } } } diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 126c1a04fb..a49a288088 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -934,6 +934,16 @@ @chat-message-box-shadow : -1px 2px 3px #ddd; @chat-message-border-radius : 0; @chat-new-message-bg : @gray-light; +@chat-new-message-textarea-bg : #FFF; +@chat-new-message-textarea-color : @gray-dark; + +// PDF +@pdf-top-offset : @toolbar-tall-height; +@pdf-bg : transparent; +@pdfjs-bg : @gray-lighter; +@pdf-page-shadow-color : #000; +@log-line-no-color : @gray; +@log-hints-color : @gray-dark; // Tags @tag-border-radius : 0.25em; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index d705e3b00d..370e47d8d6 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -202,10 +202,12 @@ @chat-bg : @ol-blue-gray-6; @chat-message-color : #FFF; @chat-message-name-color : #FFF; -@chat-message-date-color : @ol-blue-gray-2; +@chat-message-date-color : @ol-blue-gray-3; @chat-message-box-shadow : none; @chat-message-border-radius : @border-radius-base; -@chat-new-message-bg : @gray-light; +@chat-new-message-bg : @ol-blue-gray-4; +@chat-new-message-textarea-bg : @ol-blue-gray-1; +@chat-new-message-textarea-color : @ol-blue-gray-6; //## Gray and brand colors for use across Bootstrap. @gray-darker: #252525; From d5c000d761931b2370ecadb3d88b3c318357c3ce Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 14 Dec 2017 10:37:11 +0000 Subject: [PATCH 052/109] Use correct SL color for the chat textarea. --- services/web/public/stylesheets/core/_common-variables.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index a49a288088..03f41e5c81 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -933,7 +933,7 @@ @chat-message-name-color : @gray-light; @chat-message-box-shadow : -1px 2px 3px #ddd; @chat-message-border-radius : 0; -@chat-new-message-bg : @gray-light; +@chat-new-message-bg : @gray-lightest; @chat-new-message-textarea-bg : #FFF; @chat-new-message-textarea-color : @gray-dark; From df5254d6032de989e2e73262b86562f71f6c9ac3 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 14 Dec 2017 11:29:41 +0000 Subject: [PATCH 053/109] Remove LESS vars from other branch. --- .../web/public/stylesheets/core/_common-variables.less | 8 -------- 1 file changed, 8 deletions(-) diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 03f41e5c81..5ddac36738 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -937,14 +937,6 @@ @chat-new-message-textarea-bg : #FFF; @chat-new-message-textarea-color : @gray-dark; -// PDF -@pdf-top-offset : @toolbar-tall-height; -@pdf-bg : transparent; -@pdfjs-bg : @gray-lighter; -@pdf-page-shadow-color : #000; -@log-line-no-color : @gray; -@log-hints-color : @gray-dark; - // Tags @tag-border-radius : 0.25em; @tag-bg-color : @label-default-bg; From 507503fe89ecae00f6976cac1fc0471d0c943c91 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 14 Dec 2017 11:36:33 +0000 Subject: [PATCH 054/109] remove old .gitignore paths --- services/web/.gitignore | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/services/web/.gitignore b/services/web/.gitignore index d0053e47c8..3830036f24 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -67,18 +67,9 @@ public/minjs/ Gemfile.lock -public/js/libs/fineuploader-5.15.4.*.js -public/js/libs/fineuploader-*.js -public/js/libs/pdfjs-*/compatibility.*.js -public/js/libs/pdfjs-*/pdf.*.js -public/js/libs/pdfjs-*/pdf.worker.*.js -public/js/libs/require.*.js public/stylesheets/ol-style.*.css public/stylesheets/style.*.css -public/js/ace-*/ace.*.js -public/js/ace-*/mode-latex.*.js -public/js/ace-*/snippets/latex.*.js -public/js/ace-*/worker-latex.*.js + *.swp .DS_Store From d89aa0ca02f7d3a6358466d6d430a602b090bf36 Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Thu, 14 Dec 2017 11:52:41 +0000 Subject: [PATCH 055/109] Remove commented out code. --- .../public/stylesheets/core/ol-variables.less | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index a1a7738102..aee331f3d2 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -86,22 +86,6 @@ @alert-danger-border : transparent; -// @state-success-text: darken(@brand-success, 20%); -// @state-success-bg: lighten(@brand-success, 50%); -// @state-success-border: darken(@brand-success, 5%); - -// @state-info-text: darken(@brand-info, 20%); -// @state-info-bg: lighten(@brand-info, 47%); -// @state-info-border: darken(@brand-info, 7%); - -// @state-warning-text: darken(@brand-warning, 10%); -// @state-warning-bg: lighten(@brand-warning, 45%); -// @state-warning-border: @brand-warning; - -// @state-danger-text: darken(@brand-danger, 10%); -// @state-danger-bg: lighten(@brand-danger, 50%); -// @state-danger-border: darken(@brand-danger, 5%); - // Tags @tag-border-radius : 9999px; @tag-bg-color : @ol-green; From bf276b08142ed3fbb752bee8ea3c9d2d4f26729f Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Thu, 14 Dec 2017 12:11:13 +0000 Subject: [PATCH 056/109] ripped file fingerprinting out --- .../infrastructure/ExpressLocals.coffee | 66 +++++++------------ services/web/app/views/layout.pug | 12 ++-- services/web/app/views/project/editor.pug | 18 ++--- 3 files changed, 38 insertions(+), 58 deletions(-) diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee index 74c4b242fc..002a10ca89 100644 --- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee +++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee @@ -12,7 +12,6 @@ Modules = require "./Modules" Url = require "url" PackageVersions = require "./PackageVersions" htmlEncoder = new require("node-html-encoder").Encoder("numerical") -fingerprints = {} hashedFiles = {} Path = require 'path' Features = require "./Features" @@ -31,49 +30,39 @@ getFileContent = (filePath)-> filePath = Path.join __dirname, "../../../", "public#{filePath}" exists = fs.existsSync filePath if exists - content = fs.readFileSync filePath + content = fs.readFileSync filePath, "UTF-8" return content else - logger.log filePath:filePath, "file does not exist for fingerprints" + logger.log filePath:filePath, "file does not exist for hashing" return "" -logger.log "Generating file fingerprints..." +logger.log "Generating file hashes..." pathList = [ - ["#{jsPath}libs/require.js"] - ["#{jsPath}ide.js"] - ["#{jsPath}main.js"] - ["#{jsPath}libraries.js"] - ["/stylesheets/style.css"] - ["/stylesheets/ol-style.css"] + "#{jsPath}libs/require.js" + "#{jsPath}ide.js" + "#{jsPath}main.js" + "#{jsPath}libraries.js" + "/stylesheets/style.css" + "/stylesheets/ol-style.css" ] -for paths in pathList - contentList = _.map(paths, getFileContent) - content = contentList.join("") +for path in pathList + content = getFileContent(path) hash = crypto.createHash("md5").update(content).digest("hex") - _.each paths, (filePath)-> - logger.log "#{filePath}: #{hash}" - fingerprints[filePath] = hash + + splitPath = path.split("/") + filenameSplit = splitPath.pop().split(".") + filenameSplit.splice(filenameSplit.length-1, 0, hash) + splitPath.push(filenameSplit.join(".")) - for path in paths - splitPath = path.split("/") - filenameSplit = splitPath.pop().split(".") - filenameSplit.splice(filenameSplit.length-1, 0, hash) - splitPath.push(filenameSplit.join(".")) - hashPath = splitPath.join("/") - fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}" - fs.writeFileSync(fsHashPath, content) - hashedFiles[paths] = hashPath + hashPath = splitPath.join("/") + hashedFiles[path] = hashPath + + fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}" + fs.writeFileSync(fsHashPath, content) -getFingerprint = (path) -> - if fingerprints[path]? - return fingerprints[path] - else - logger.err "No fingerprint for file: #{path}" - return "" - -logger.log "Finished generating file fingerprints" +logger.log "Finished hashing static content" cdnAvailable = Settings.cdn?.web?.host? darkCdnAvailable = Settings.cdn?.web?.darkHost? @@ -135,14 +124,9 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> if opts.hashedPath path = hashedFiles[path] - doFingerPrint = opts.fingerprint != false - if !opts.qs? opts.qs = {} - if !opts.qs?.fingerprint? and doFingerPrint - opts.qs.fingerprint = getFingerprint(path) - if opts.cdn != false path = Url.resolve(staticFilesBase, path) @@ -160,7 +144,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> if opts?.hashedPath hashedPath = hashedFiles[path] return Url.resolve(staticFilesBase, hashedPath) - return Url.resolve(staticFilesBase, path) + "?fingerprint=" + getFingerprint(path) + return Url.resolve(staticFilesBase, path) res.locals.buildImgPath = (imgFile)-> path = Path.join("/img/", imgFile) @@ -245,10 +229,6 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)-> return req.query?[field] next() - webRouter.use (req, res, next)-> - res.locals.fingerprint = getFingerprint - next() - webRouter.use (req, res, next)-> res.locals.formatPrice = SubscriptionFormatters.formatPrice next() diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index 5aa41c56c7..f203d29357 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -57,7 +57,7 @@ html(itemscope, itemtype='http://schema.org/Product') script(type="text/javascript"). window.csrfToken = "#{csrfToken}"; - script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false})) + script(src=buildJsPath("libs/jquery-1.11.1.min.js")) script(type="text/javascript"). var noCdnKey = "nocdn=true" var cdnBlocked = typeof jQuery === 'undefined' @@ -68,7 +68,7 @@ html(itemscope, itemtype='http://schema.org/Product') block scripts - script(src=buildJsPath("libs/angular-1.6.4.min.js", {fingerprint:false})) + script(src=buildJsPath("libs/angular-1.6.4.min.js")) script. window.sharelatex = { @@ -143,8 +143,8 @@ html(itemscope, itemtype='http://schema.org/Product') "paths" : { "moment": "libs/#{lib('moment')}", "fineuploader": "libs/#{lib('fineuploader')}", - "main": "#{buildJsPath('main.js', {hashedPath:settings.useMinifiedJs, fingerprint:false, removeExtension:true})}", - "libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, fingerprint:false, removeExtension:true})}", + "main": "#{buildJsPath('main.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}", + "libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}", }, "config":{ "moment":{ @@ -153,9 +153,9 @@ html(itemscope, itemtype='http://schema.org/Product') } }; script( - data-main=buildJsPath('main.js', {hashedPath:false, fingerprint:false}), + data-main=buildJsPath('main.js', {hashedPath:false}), baseurl=fullJsPath, - src=buildJsPath('libs/require.js') + src=buildJsPath('libs/require.js', {hashedPath:true}) ) include contact-us-modal diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 38ade1a6b2..d1d0d94cf3 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -98,9 +98,9 @@ block requirejs script(type="text/javascript" src='/socket.io/socket.io.js') //- don't use cdn for workers - - var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false,fingerprint:false}) - - var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false,fingerprint:false}) - - var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false,fingerprint:false}) + - var aceWorkerPath = buildJsPath(lib('ace'), {cdn:false}) + - var pdfWorkerPath = buildJsPath('/libs/' + lib('pdfjs') + '/pdf.worker', {cdn:false}) + - var pdfCMapsPath = buildJsPath('/libs/' + lib('pdfjs') + '/bcmaps/', {cdn:false}) //- We need to do .replace(/\//g, '\\/') do that '' -> '<\/script>' //- and doesn't prematurely end the script tag. @@ -126,14 +126,14 @@ block requirejs window.wikiEnabled = #{!!(settings.apis.wiki && settings.apis.wiki.url)}; window.requirejs = { "paths" : { - "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, fingerprint:false, qs:{config:'TeX-AMS_HTML'}})}", + "mathjax": "#{buildJsPath('/libs/mathjax/MathJax.js', {cdn:false, qs:{config:'TeX-AMS_HTML'}})}", "moment": "libs/#{lib('moment')}", "pdfjs-dist/build/pdf": "libs/#{lib('pdfjs')}/pdf", "pdfjs-dist/build/pdf.worker": "#{pdfWorkerPath}", "ace": "#{lib('ace')}", "fineuploader": "libs/#{lib('fineuploader')}", - "ide": "#{buildJsPath('ide.js', {hashedPath:settings.useMinifiedJs, fingerprint:false, removeExtension:true})}", - "libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, fingerprint:false, removeExtension:true})}", + "ide": "#{buildJsPath('ide.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}", + "libraries": "#{buildJsPath('libraries.js', {hashedPath:settings.useMinifiedJs, removeExtension:true})}", }, "waitSeconds": 0, "shim": { @@ -161,8 +161,8 @@ block requirejs window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}'); script( - data-main=buildJsPath("ide.js", {hashedPath:false, fingerprint:false}), + data-main=buildJsPath("ide.js", {hashedPath:false}), baseurl=fullJsPath, - data-ace-base=buildJsPath(lib('ace'), {fingerprint:false}), - src=buildJsPath('libs/require.js', {hashedPath:false}) + data-ace-base=buildJsPath(lib('ace')), + src=buildJsPath('libs/require.js', {hashedPath:true}) ) From b6d6ac8304cb1de36503342be95e09cd0b6f2afe Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Thu, 14 Dec 2017 16:18:01 +0000 Subject: [PATCH 057/109] Ignore select-all events for disabled check-boxes --- services/web/public/coffee/directives/selectAll.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/directives/selectAll.coffee b/services/web/public/coffee/directives/selectAll.coffee index 4194d050e8..eb0bd05e86 100644 --- a/services/web/public/coffee/directives/selectAll.coffee +++ b/services/web/public/coffee/directives/selectAll.coffee @@ -49,18 +49,21 @@ define [ selectAllListController.clearSelectAllState() scope.$on "select-all:select", () -> + return if element.prop('disabled') ignoreChanges = true scope.$apply () -> scope.ngModel = true ignoreChanges = false scope.$on "select-all:deselect", () -> + return if element.prop('disabled') ignoreChanges = true scope.$apply () -> scope.ngModel = false ignoreChanges = false scope.$on "select-all:row-clicked", () -> + return if element.prop('disabled') ignoreChanges = true scope.$apply () -> scope.ngModel = !scope.ngModel @@ -75,4 +78,4 @@ define [ link: (scope, element, attrs) -> element.on "click", (e) -> scope.$broadcast "select-all:row-clicked" - } \ No newline at end of file + } From 8d91cd274870829f8ed167afdcd5847552f548e0 Mon Sep 17 00:00:00 2001 From: Brian Gough Date: Thu, 14 Dec 2017 16:19:43 +0000 Subject: [PATCH 058/109] allow autocompile without code check --- .../public/coffee/ide/pdf/controllers/PdfController.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee index 17afd8bbcb..4ac48c20d0 100644 --- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee +++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee @@ -90,7 +90,9 @@ define [ # to block auto compiles. It also causes problems where server-provided # linting errors aren't cleared after typing if (ide.$scope.settings.syntaxValidation and !ide.$scope.hasLintingError) - $scope.recompile(isAutoCompileOnChange: true) + $scope.recompile(isAutoCompileOnChange: true) # compile if no linting errors + else if !ide.$scope.settings.syntaxValidation + $scope.recompile(isAutoCompileOnChange: true) # always recompile else # Extend remainder of timeout autoCompileTimeout = setTimeout () -> From 73225223236b25567bc574875786b09ffc3e3cc2 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 14 Dec 2017 17:05:12 +0000 Subject: [PATCH 059/109] Use freeTrialBaseUrl instead of potentially fragile v1BaseUrl --- services/web/app/views/layout.pug | 5 ++++- services/web/public/coffee/main/account-upgrade.coffee | 9 +++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index fc14c2030b..dd93b0b0fa 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -98,7 +98,10 @@ html(itemscope, itemtype='http://schema.org/Product') - if (settings.overleaf && settings.overleaf.host) script. - window.v1BaseUrl = '!{settings.overleaf.host}' + window.freeTrialBaseUrl = '!{settings.overleaf.host}/users/trial' + - else + script. + window.freeTrialBaseUrl = '/user/subscription/new' body if(settings.recaptcha) diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee index 29bc472505..8356458ef9 100644 --- a/services/web/public/coffee/main/account-upgrade.coffee +++ b/services/web/public/coffee/main/account-upgrade.coffee @@ -11,12 +11,9 @@ define [ w = window.open() go = () -> ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source) - if window.v1BaseUrl? - url = "#{window.v1BaseUrl}/users/trial" - else - url = "/user/subscription/new?planCode=#{plan}&ssp=true" - if couponCode? - url = "#{url}&cc=#{couponCode}" + url = "#{window.freeTrialBaseUrl}?planCode=#{plan}&ssp=true" + if couponCode? + url = "#{url}&cc=#{couponCode}" $scope.startedFreeTrial = true switch source From da2c0b3fead5de68e96b5e2cbf39ec577ecff004 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Thu, 14 Dec 2017 17:09:11 +0000 Subject: [PATCH 060/109] Switch redirectToOLFreeTrialUrl to calculate trial url Sending unnecessary query params to OL seems like a footgun, so switch to compromise that is more explicit but doesn't send unnecessary query params --- services/web/app/views/layout.pug | 7 ++----- services/web/public/coffee/main/account-upgrade.coffee | 9 ++++++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index dd93b0b0fa..14ebc3c576 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -98,11 +98,8 @@ html(itemscope, itemtype='http://schema.org/Product') - if (settings.overleaf && settings.overleaf.host) script. - window.freeTrialBaseUrl = '!{settings.overleaf.host}/users/trial' - - else - script. - window.freeTrialBaseUrl = '/user/subscription/new' - + window.redirectToOLFreeTrialUrl = '!{settings.overleaf.host}/users/trial' + body if(settings.recaptcha) script(src="https://www.google.com/recaptcha/api.js?render=explicit") diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee index 8356458ef9..0cabc224a0 100644 --- a/services/web/public/coffee/main/account-upgrade.coffee +++ b/services/web/public/coffee/main/account-upgrade.coffee @@ -11,9 +11,12 @@ define [ w = window.open() go = () -> ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source) - url = "#{window.freeTrialBaseUrl}?planCode=#{plan}&ssp=true" - if couponCode? - url = "#{url}&cc=#{couponCode}" + if window.redirectToOLFreeTrialUrl? + url = window.redirectToOLFreeTrialUrl + else + url = "/user/subscription/new?planCode=#{plan}&ssp=true" + if couponCode? + url = "#{url}&cc=#{couponCode}" $scope.startedFreeTrial = true switch source From 4955fb373bd342792f86b63d61f37b65c11d9083 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 15 Dec 2017 10:10:13 +0000 Subject: [PATCH 061/109] ignore hashed requirejs path --- services/web/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/services/web/.gitignore b/services/web/.gitignore index 3830036f24..83d4d28b65 100644 --- a/services/web/.gitignore +++ b/services/web/.gitignore @@ -69,6 +69,7 @@ Gemfile.lock public/stylesheets/ol-style.*.css public/stylesheets/style.*.css +public/js/libs/require.js *.swp From a6cd210ef6160f1dc9ec678ec77feabc3875c25f Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 15 Dec 2017 12:02:11 +0000 Subject: [PATCH 062/109] Update v1 dash pane with new copy & restyle to fit correctly --- services/web/app/views/project/list.pug | 10 +++-- .../stylesheets/_ol_style_includes.less | 2 +- .../stylesheets/app/sidebar-v1-dash-link.less | 32 -------------- .../stylesheets/app/sidebar-v2-dash-pane.less | 42 +++++++++++++++++++ .../public/stylesheets/core/ol-variables.less | 7 ++-- 5 files changed, 53 insertions(+), 40 deletions(-) delete mode 100644 services/web/public/stylesheets/app/sidebar-v1-dash-link.less create mode 100644 services/web/public/stylesheets/app/sidebar-v2-dash-pane.less diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index 7ee28763a8..710eeee028 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -64,10 +64,12 @@ block content include ./list/side-bar if isShowingV1Projects && settings.overleaf && settings.overleaf.host - .project-list-sidebar-v1-link.col-md-2.col-xs-3 - span Want to go back to the V1 dashboard? - a.btn.btn-default(href=settings.overleaf.host + "/dash?prefer-v1-dash=1") - | Go back to V1 + .project-list-sidebar-v2-pane.col-md-2.col-xs-3 + span Welcome to the Overleaf v2 alpha! #[a.project-list-sidebar-faq(href=settings.overleaf.host + "/help/v2") Find out more]. + span To tag or rename your v1 projects, please go back to Overleaf v1. + a.project-list-sidebar-v1-link.btn.btn-default( + href=settings.overleaf.host + "/dash?prefer-v1-dash=1" + ) Go back to v1 .project-list-main.col-md-10.col-xs-9 include ./list/notifications diff --git a/services/web/public/stylesheets/_ol_style_includes.less b/services/web/public/stylesheets/_ol_style_includes.less index c8505c2e56..f2832bd219 100644 --- a/services/web/public/stylesheets/_ol_style_includes.less +++ b/services/web/public/stylesheets/_ol_style_includes.less @@ -1 +1 @@ -@import "app/sidebar-v1-dash-link.less"; +@import "app/sidebar-v2-dash-pane.less"; diff --git a/services/web/public/stylesheets/app/sidebar-v1-dash-link.less b/services/web/public/stylesheets/app/sidebar-v1-dash-link.less deleted file mode 100644 index 072042226e..0000000000 --- a/services/web/public/stylesheets/app/sidebar-v1-dash-link.less +++ /dev/null @@ -1,32 +0,0 @@ -@v1-dash-link-height: 130px; - -.project-list-sidebar { - height: calc(~"100% -" @v1-dash-link-height); -} - -.project-list-sidebar-v1-link { - position: absolute; - bottom: 0; - height: @v1-dash-link-height; - background-color: @v1-dash-link-bg; - text-align: center; - display: flex; - flex-direction: column; - justify-content: center; - color: white; -} - - .project-list-sidebar-v1-link a { - display: block; - margin-left: auto; - margin-right: auto; - margin-top: 12.5px; - padding-top: 0; - padding-bottom: 0; - background-color: @v1-dash-link-btn-bg; - color: #fff; - } - - .project-list-sidebar-v1-link a:hover { - background-color: @v1-dash-link-btn-hover-bg; - } \ No newline at end of file diff --git a/services/web/public/stylesheets/app/sidebar-v2-dash-pane.less b/services/web/public/stylesheets/app/sidebar-v2-dash-pane.less new file mode 100644 index 0000000000..6adb952bed --- /dev/null +++ b/services/web/public/stylesheets/app/sidebar-v2-dash-pane.less @@ -0,0 +1,42 @@ +@v2-dash-pane-link-height: 130px; + +.project-list-sidebar { + height: calc(~"100% -" @v2-dash-pane-link-height); +} + +.project-list-sidebar-v2-pane { + position: absolute; + bottom: 0; + height: @v2-dash-pane-link-height; + background-color: @v2-dash-pane-bg; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + color: white; + font-size: 13px; +} + + .project-list-sidebar-faq { + color: @v2-dash-pane-link-color; + text-decoration: underline; + } + + .project-list-sidebar-faq:hover { + color: @v2-dash-pane-link-color; + text-decoration: none; + } + + .project-list-sidebar-v1-link { + display: block; + margin-left: auto; + margin-right: auto; + padding-top: 0; + padding-bottom: 0; + background-color: @v2-dash-pane-btn-bg; + font-size: 13px; + } + + .project-list-sidebar-v1-link:hover { + background-color: @v2-dash-pane-btn-hover-bg; + } \ No newline at end of file diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 508ae6ed37..329f1005f1 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -110,9 +110,10 @@ @sidebar-active-bg : @ol-blue-gray-6; @sidebar-hover-bg : @ol-blue-gray-4; @sidebar-hover-text-decoration : none; -@v1-dash-link-bg : @ol-blue-gray-4; -@v1-dash-link-btn-bg : @ol-blue-gray-5; -@v1-dash-link-btn-hover-bg : @ol-blue-gray-6; +@v2-dash-pane-bg : @ol-blue-gray-4; +@v2-dash-pane-link-color : #FFF; +@v2-dash-pane-btn-bg : @ol-blue-gray-5; +@v2-dash-pane-btn-hover-bg : @ol-blue-gray-6; @folders-menu-margin : 0 -(@grid-gutter-width / 2); @folders-menu-line-height : @structured-list-line-height; From 96af59d6b5c6f8422cf2cc6932c1f8ac9e767937 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 15 Dec 2017 12:17:08 +0000 Subject: [PATCH 063/109] Update v1 project/tag tooltips --- services/web/app/views/v1-tooltip.pug | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/v1-tooltip.pug b/services/web/app/views/v1-tooltip.pug index 2934adbe8b..728962338a 100644 --- a/services/web/app/views/v1-tooltip.pug +++ b/services/web/app/views/v1-tooltip.pug @@ -1,6 +1,6 @@ script(type="text/ng-template", id="v1ProjectTooltipTemplate") - span This project is from Overleaf version 1 and has not been imported to the beta yet + span This project is from Overleaf v1 and has not been imported to v2 yet. script(type="text/ng-template", id="v1TagTooltipTemplate") - span This folder is from Overleaf version 1 and has not been imported to the beta yet \ No newline at end of file + span This folder/tag is from Overleaf v1. Please go back to v1 to manage. \ No newline at end of file From 2b3b8b2c3b4c04d383f6a1c1f3f43be6c4aff0e5 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 15 Dec 2017 12:29:39 +0000 Subject: [PATCH 064/109] Use hardcoded OL url instead of settings - no point in linking to staging faq --- services/web/app/views/project/list.pug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index 710eeee028..69e791411c 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -65,7 +65,7 @@ block content if isShowingV1Projects && settings.overleaf && settings.overleaf.host .project-list-sidebar-v2-pane.col-md-2.col-xs-3 - span Welcome to the Overleaf v2 alpha! #[a.project-list-sidebar-faq(href=settings.overleaf.host + "/help/v2") Find out more]. + span Welcome to the Overleaf v2 alpha! #[a.project-list-sidebar-faq(href="https://www.overleaf.com/help/v2") Find out more]. span To tag or rename your v1 projects, please go back to Overleaf v1. a.project-list-sidebar-v1-link.btn.btn-default( href=settings.overleaf.host + "/dash?prefer-v1-dash=1" From 7130d0b27042447f11c0cdbf42aa1768401fe6f0 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 15 Dec 2017 12:49:17 +0000 Subject: [PATCH 065/109] Update v1 import modal with updated copy --- .../web/app/views/project/list/modals.pug | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 1e12fb2293..8969611621 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -317,7 +317,7 @@ script(type="text/ng-template", id="userProfileModalTemplate") script(type="text/ng-template", id="v1ImportModalTemplate") .modal-header button.close(ng-click="dismiss()") × - h3 #{translate("import_project_to_v2")} + h3 Move Project to Overleaf v2 .modal-body.v1-import-wrapper .v1-import-step-1(ng-show="step === 1") @@ -325,30 +325,42 @@ script(type="text/ng-template", id="v1ImportModalTemplate") src="/img/v1-import/v2-editor.png" alt="The new V2 Editor." ) - h2.v1-import-title Try importing your project to V2! - p Some exciting copy about the new features: + h2.v1-import-title Try the Overleaf v2 Editor + p The Overleaf v2 editor has many great new features including: ul - li Some stuff - li Some more stuff - li Yet more stuff + li Faster real-time collaboration + li See your coauthors’ cursors + li Chat with math support + li Tracked changes and commenting + li Improved LaTeX autocomplete + li Two-way Dropbox sync + p Would you like to move {{project.title}} into Overleaf v2? .v1-import-step-2(ng-show="step === 2") div.v1-import-warning(aria-label="Warning symbol.") i.fa.fa-exclamation-triangle - h2.v1-import-title #[strong Warning:] Overleaf V2 is in beta - p Once importing your project you will lose access to the some of the features of Overleaf V1. This includes the git bridge, journal integrations, WYSIWYG and linked files. We’re working on bringing these features to V2! - p Once you have imported a project to V2 you #[strong cannot go back to V1]. - p Are you sure you want to import to V2? + h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental + p We are still working hard to bring some Overleaf v1 features to the v2 editor. If you move this project to v2 now, you will: + ul + li Lose access your project via git + li Not be able to use the Journals and Services menu to submit directly to our partners + li Not be able to use the Rich Text (WYSIWYG) mode + li Not be able to use linked files (to URLs or to files in other Overleaf projects) + li Not be able to use some bibliography integrations (Zotero, CiteULike) + li Lose access to your labelled versions and not be able to create new labelled versions + p + strong Please note: you cannot move this project back to v1 once you have moved it to v2. If this is an important project, please consider making a clone in v1 before you move the project to v2. + p Are you sure you want to move {{project.title}} into Overleaf v2? .modal-footer.v1-import-footer div(ng-show="step === 1") if settings.overleaf && settings.overleaf.host a.btn.btn-primary.v1-import-btn( ng-href=settings.overleaf.host + "/{{project.id}}" - ) #{translate("open_in_v1")} + ) No thanks, open in v1 button.btn.btn-primary.v1-import-btn( ng-click="moveToConfirmation()" - ) #{translate("import_to_v2")} + ) Yes, move project to v2 div(ng-show="step === 2") form( async-form="v1Import", @@ -363,9 +375,9 @@ script(type="text/ng-template", id="v1ImportModalTemplate") a.btn.btn-primary.v1-import-btn( ng-href=settings.overleaf.host + "/{{project.id}}" ng-class="{disabled: v1ImportForm.inflight || v1ImportForm.success}" - ) #{translate("never_mind_open_in_v1")} + ) No thanks, open in v1 input.btn.btn-primary.v1-import-btn( type="submit", - value=translate('yes_im_sure') + value="Yes, move project to v2" ng-disabled="v1ImportForm.inflight || v1ImportForm.success" ) From 8862ed383cee285d210814ea2b9af21947eed696 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 15 Dec 2017 13:28:53 +0000 Subject: [PATCH 066/109] Fix project title & bold --- services/web/app/views/project/list/modals.pug | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 8969611621..3b07f4c933 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -334,7 +334,7 @@ script(type="text/ng-template", id="v1ImportModalTemplate") li Tracked changes and commenting li Improved LaTeX autocomplete li Two-way Dropbox sync - p Would you like to move {{project.title}} into Overleaf v2? + p Would you like to move #[strong {{project.name}}] into Overleaf v2? .v1-import-step-2(ng-show="step === 2") div.v1-import-warning(aria-label="Warning symbol.") @@ -350,7 +350,7 @@ script(type="text/ng-template", id="v1ImportModalTemplate") li Lose access to your labelled versions and not be able to create new labelled versions p strong Please note: you cannot move this project back to v1 once you have moved it to v2. If this is an important project, please consider making a clone in v1 before you move the project to v2. - p Are you sure you want to move {{project.title}} into Overleaf v2? + p Are you sure you want to move #[strong {{project.name}}] into Overleaf v2? .modal-footer.v1-import-footer div(ng-show="step === 1") From 7aaf08da48f986dd75f6ecd807ed3692df4b62b6 Mon Sep 17 00:00:00 2001 From: Henry Oswald Date: Fri, 15 Dec 2017 13:38:34 +0000 Subject: [PATCH 067/109] added no-cache endpoint --- services/web/app/coffee/router.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index f8ec65ac53..c89d9cdf5e 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -324,6 +324,10 @@ module.exports = class Router headers: req.headers }) + webRouter.get "/no-cache", (req, res, next)-> + res.header("Cache-Control", "max-age=0") + res.sendStatus(404) + webRouter.get '/oops-express', (req, res, next) -> next(new Error("Test error")) webRouter.get '/oops-internal', (req, res, next) -> throw new Error("Test error") webRouter.get '/oops-mongo', (req, res, next) -> From 75a34024bcdfa1d39f2dba03d929e7415c4dab12 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 15 Dec 2017 13:48:53 +0000 Subject: [PATCH 068/109] Restyle step 1 for side-by-side layout --- .../web/app/views/project/list/modals.pug | 31 ++++++++++--------- .../stylesheets/app/list/v1-import-modal.less | 6 ++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 3b07f4c933..6684246cb8 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -321,20 +321,23 @@ script(type="text/ng-template", id="v1ImportModalTemplate") .modal-body.v1-import-wrapper .v1-import-step-1(ng-show="step === 1") - img.v1-import-img( - src="/img/v1-import/v2-editor.png" - alt="The new V2 Editor." - ) - h2.v1-import-title Try the Overleaf v2 Editor - p The Overleaf v2 editor has many great new features including: - ul - li Faster real-time collaboration - li See your coauthors’ cursors - li Chat with math support - li Tracked changes and commenting - li Improved LaTeX autocomplete - li Two-way Dropbox sync - p Would you like to move #[strong {{project.name}}] into Overleaf v2? + .row + .col-xs-6 + img.v1-import-img( + src="/img/v1-import/v2-editor.png" + alt="The new V2 Editor." + ) + .col-xs-6.col-offset-6 + h2.v1-import-title Try the Overleaf v2 Editor + p The Overleaf v2 editor has many great new features including: + ul + li Faster real-time collaboration + li See your coauthors’ cursors + li Chat with math support + li Tracked changes and commenting + li Improved LaTeX autocomplete + li Two-way Dropbox sync + p.v1-import-cta Would you like to move #[strong {{project.name}}] into Overleaf v2? .v1-import-step-2(ng-show="step === 2") div.v1-import-warning(aria-label="Warning symbol.") diff --git a/services/web/public/stylesheets/app/list/v1-import-modal.less b/services/web/public/stylesheets/app/list/v1-import-modal.less index 64bb9fda37..cd015c1e4c 100644 --- a/services/web/public/stylesheets/app/list/v1-import-modal.less +++ b/services/web/public/stylesheets/app/list/v1-import-modal.less @@ -4,6 +4,12 @@ .v1-import-img { width: 100%; + margin-top: 30px; +} + +.v1-import-cta { + margin-top: 20px; + text-align: center; } .v1-import-warning { From 61900cc1eab033c66e109fe475ceae8b595987d8 Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 15 Dec 2017 16:01:00 +0000 Subject: [PATCH 069/109] Restyle step 2 for side-by-side layout --- .../web/app/views/project/list/modals.pug | 37 ++++++++++--------- .../stylesheets/app/list/v1-import-modal.less | 16 ++++++++ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/services/web/app/views/project/list/modals.pug b/services/web/app/views/project/list/modals.pug index 6684246cb8..c3d86feb70 100644 --- a/services/web/app/views/project/list/modals.pug +++ b/services/web/app/views/project/list/modals.pug @@ -321,13 +321,13 @@ script(type="text/ng-template", id="v1ImportModalTemplate") .modal-body.v1-import-wrapper .v1-import-step-1(ng-show="step === 1") - .row - .col-xs-6 + .v1-import-row + .v1-import-col img.v1-import-img( src="/img/v1-import/v2-editor.png" alt="The new V2 Editor." ) - .col-xs-6.col-offset-6 + .v1-import-col h2.v1-import-title Try the Overleaf v2 Editor p The Overleaf v2 editor has many great new features including: ul @@ -340,20 +340,23 @@ script(type="text/ng-template", id="v1ImportModalTemplate") p.v1-import-cta Would you like to move #[strong {{project.name}}] into Overleaf v2? .v1-import-step-2(ng-show="step === 2") - div.v1-import-warning(aria-label="Warning symbol.") - i.fa.fa-exclamation-triangle - h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental - p We are still working hard to bring some Overleaf v1 features to the v2 editor. If you move this project to v2 now, you will: - ul - li Lose access your project via git - li Not be able to use the Journals and Services menu to submit directly to our partners - li Not be able to use the Rich Text (WYSIWYG) mode - li Not be able to use linked files (to URLs or to files in other Overleaf projects) - li Not be able to use some bibliography integrations (Zotero, CiteULike) - li Lose access to your labelled versions and not be able to create new labelled versions - p - strong Please note: you cannot move this project back to v1 once you have moved it to v2. If this is an important project, please consider making a clone in v1 before you move the project to v2. - p Are you sure you want to move #[strong {{project.name}}] into Overleaf v2? + .v1-import-row + .v1-import-warning.v1-import-col(aria-label="Warning symbol.") + i.fa.fa-exclamation-triangle + .v1-import-col + h2.v1-import-title #[strong Warning:] Overleaf v2 is Experimental + p We are still working hard to bring some Overleaf v1 features to the v2 editor. If you move this project to v2 now, you will: + ul + li Lose access your project via git + li Not be able to use the Journals and Services menu to submit directly to our partners + li Not be able to use the Rich Text (WYSIWYG) mode + li Not be able to use linked files (to URLs or to files in other Overleaf projects) + li Not be able to use some bibliography integrations (Zotero, CiteULike) + li Lose access to your labelled versions and not be able to create new labelled versions + .v1-import-cta + p + strong Please note: you cannot move this project back to v1 once you have moved it to v2. If this is an important project, please consider making a clone in v1 before you move the project to v2. + p Are you sure you want to move #[strong {{project.name}}] into Overleaf v2? .modal-footer.v1-import-footer div(ng-show="step === 1") diff --git a/services/web/public/stylesheets/app/list/v1-import-modal.less b/services/web/public/stylesheets/app/list/v1-import-modal.less index cd015c1e4c..7c5d9b97e1 100644 --- a/services/web/public/stylesheets/app/list/v1-import-modal.less +++ b/services/web/public/stylesheets/app/list/v1-import-modal.less @@ -2,6 +2,19 @@ text-align: center; } +.v1-import-row { + display: flex; + align-items: center; +} + +.v1-import-col { + flex-basis: 50%; + flex-grow: 0; + flex-shrink: 0; + padding-left: 15px; + padding-right: 15px; +} + .v1-import-img { width: 100%; margin-top: 30px; @@ -9,6 +22,9 @@ .v1-import-cta { margin-top: 20px; + margin-left: auto; + margin-right: auto; + width: 80%; text-align: center; } From 72ca08ae2d8373b526f35ca340c78b3dcd9435fb Mon Sep 17 00:00:00 2001 From: Alasdair Smith Date: Fri, 15 Dec 2017 16:11:31 +0000 Subject: [PATCH 070/109] Reduce margin under list --- services/web/public/stylesheets/app/list/v1-import-modal.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/web/public/stylesheets/app/list/v1-import-modal.less b/services/web/public/stylesheets/app/list/v1-import-modal.less index 7c5d9b97e1..14a1813369 100644 --- a/services/web/public/stylesheets/app/list/v1-import-modal.less +++ b/services/web/public/stylesheets/app/list/v1-import-modal.less @@ -15,6 +15,10 @@ padding-right: 15px; } + .v1-import-col ul { + margin-bottom: 0; + } + .v1-import-img { width: 100%; margin-top: 30px; From 56ec81bb3efc4e67122cbb206b0137f9d3c51127 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Mon, 18 Dec 2017 13:52:37 +0000 Subject: [PATCH 071/109] Change 'oveleafPlanCode' to 'v1PlanCode' --- .../Subscription/SubscriptionUpdater.coffee | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee index d3b1a0feb7..649551b5b2 100644 --- a/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee +++ b/services/web/app/coffee/Features/Subscription/SubscriptionUpdater.coffee @@ -109,16 +109,16 @@ module.exports = SubscriptionUpdater = SubscriptionLocator.getUsersSubscription user_id, cb groupSubscription: (cb)-> SubscriptionLocator.getGroupSubscriptionMemberOf user_id, cb - overleafPlanCode: (cb) -> + v1PlanCode: (cb) -> Modules = require '../../infrastructure/Modules' - Modules.hooks.fire 'getOverleafPlanCode', user_id, (err, results) -> + Modules.hooks.fire 'getV1PlanCode', user_id, (err, results) -> cb(err, results?[0] || null) async.series jobs, (err, results)-> if err? logger.err err:err, user_id:user_id, "error getting subscription or group for _setUsersMinimumFeatures" return callback(err) - {subscription, groupSubscription, overleafPlanCode} = results + {subscription, groupSubscription, v1PlanCode} = results # Group Subscription if groupSubscription? and groupSubscription.planCode? logger.log user_id:user_id, "using group which user is memor of for features" @@ -127,10 +127,10 @@ module.exports = SubscriptionUpdater = else if subscription? and subscription.planCode? and subscription.planCode != Settings.defaultPlanCode logger.log user_id:user_id, "using users subscription plan code for features" UserFeaturesUpdater.updateFeatures user_id, subscription.planCode, callback - # Overleaf Subscription - else if overleafPlanCode? - logger.log user_id: user_id, "using the overleaf plan for features" - UserFeaturesUpdater.updateFeatures user_id, overleafPlanCode, callback + # V1 Subscription + else if v1PlanCode? + logger.log user_id: user_id, "using the V1 plan for features" + UserFeaturesUpdater.updateFeatures user_id, v1PlanCode, callback # Default else logger.log user_id:user_id, "using default features for user with no subscription or group" From e5e75a8ccb0685fa7a36b3c43cd9d36823c15705 Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Thu, 14 Dec 2017 13:06:27 +0000 Subject: [PATCH 072/109] update DocumentUpdaterHandler.updateProjectStructure to support entity deletions --- .../DocumentUpdater/DocumentUpdaterHandler.coffee | 15 ++++++++++++--- .../DocumentUpdaterHandlerTests.coffee | 13 ++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee index f7dd9f3e17..f1336a7f77 100644 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -207,8 +207,8 @@ module.exports = DocumentUpdaterHandler = updateProjectStructure : (project_id, userId, changes, callback = (error) ->)-> return callback() if !settings.apis.project_history?.enabled - docUpdates = DocumentUpdaterHandler._getRenameUpdates('doc', changes.oldDocs, changes.newDocs) - fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', changes.oldFiles, changes.newFiles) + docUpdates = DocumentUpdaterHandler._getUpdates('doc', changes.oldDocs, changes.newDocs) + fileUpdates = DocumentUpdaterHandler._getUpdates('file', changes.oldFiles, changes.newFiles) timer = new metrics.Timer("set-document") url = "#{settings.apis.documentupdater.url}/project/#{project_id}" @@ -230,7 +230,7 @@ module.exports = DocumentUpdaterHandler = logger.error {project_id, url}, "doc updater returned a non-success status code: #{res.statusCode}" callback new Error("doc updater returned a non-success status code: #{res.statusCode}") - _getRenameUpdates: (entityType, oldEntities, newEntities) -> + _getUpdates: (entityType, oldEntities, newEntities) -> oldEntities ||= [] newEntities ||= [] updates = [] @@ -255,6 +255,15 @@ module.exports = DocumentUpdaterHandler = pathname: oldEntity.path newPathname: newEntity.path + for id, oldEntity of oldEntitiesHash + newEntity = newEntitiesHash[id] + + if !newEntity? + # entity deleted + updates.push + id: id + pathname: oldEntity.path + updates PENDINGUPDATESKEY = "PendingUpdates" diff --git a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee index 14ccaa3a33..266de9bfb7 100644 --- a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee +++ b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee @@ -478,14 +478,21 @@ describe 'DocumentUpdaterHandler', -> .should.equal true done() - describe "when a doc has been deleted", -> - it 'should do nothing', (done) -> + describe "when an entity has been deleted", -> + it 'should end the structure update to the document updater', (done) -> @docId = new ObjectId() @changes = oldDocs: [ { path: '/foo', docLines: 'a\nb', doc: _id: @docId } ] + docUpdates = [ + id: @docId.toString(), + pathname: "/foo", + ] + @handler.updateProjectStructure @project_id, @user_id, @changes, () => - @request.post.called.should.equal false + @request.post + .calledWith(url: @url, json: {docUpdates, fileUpdates: [], userId: @user_id}) + .should.equal true done() From 81c061c6a7ce3236213d67ca491f5b925742602d Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Thu, 14 Dec 2017 14:39:27 +0000 Subject: [PATCH 073/109] acceptance test moving entities --- .../DocumentUpdaterHandler.coffee | 1 + .../coffee/ProjectStructureTests.coffee | 89 +++++++++++++++++++ .../DocumentUpdaterHandlerTests.coffee | 3 +- 3 files changed, 92 insertions(+), 1 deletion(-) diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee index f1336a7f77..bbd7ca4919 100644 --- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee +++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee @@ -263,6 +263,7 @@ module.exports = DocumentUpdaterHandler = updates.push id: id pathname: oldEntity.path + newPathname: '' updates diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee index e54d4fac9d..92fcd2e34e 100644 --- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee @@ -91,6 +91,7 @@ describe "ProjectStructureChanges", -> throw error if error? if res.statusCode < 200 || res.statusCode >= 300 throw new Error("failed to add doc #{res.statusCode}") + @example_doc_id = body._id done() it "should version the doc added", -> @@ -162,6 +163,8 @@ describe "ProjectStructureChanges", -> if res.statusCode < 200 || res.statusCode >= 300 throw new Error("failed to upload file #{res.statusCode}") + @example_file_id = JSON.parse(body).entity_id + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates expect(updates.length).to.equal(1) update = updates[0] @@ -199,6 +202,92 @@ describe "ProjectStructureChanges", -> done() + describe "moving entities", -> + before (done) -> + @owner.request.post { + uri: "project/#{@example_project_id}/folder", + formData: + name: 'foo' + }, (error, res, body) => + throw error if error? + @example_folder_id_1 = JSON.parse(body)._id + done() + + beforeEach () -> + MockDocUpdaterApi.clearProjectStructureUpdates() + + it "should version moving a doc", (done) -> + @owner.request.post { + uri: "project/#{@example_project_id}/Doc/#{@example_doc_id}/move", + json: + folder_id: @example_folder_id_1 + }, (error, res, body) => + throw error if error? + if res.statusCode < 200 || res.statusCode >= 300 + throw new Error("failed to move doc #{res.statusCode}") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/new.tex") + expect(update.newPathname).to.equal("/foo/new.tex") + + done() + + it "should version moving a file", (done) -> + @owner.request.post { + uri: "project/#{@example_project_id}/File/#{@example_file_id}/move", + json: + folder_id: @example_folder_id_1 + }, (error, res, body) => + throw error if error? + if res.statusCode < 200 || res.statusCode >= 300 + throw new Error("failed to move file #{res.statusCode}") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/1pixel.png") + expect(update.newPathname).to.equal("/foo/1pixel.png") + + done() + + it "should version moving a folder", (done) -> + @owner.request.post { + uri: "project/#{@example_project_id}/folder", + formData: + name: 'bar' + }, (error, res, body) => + throw error if error? + example_folder_id_2 = JSON.parse(body)._id + + @owner.request.post { + uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_1}/move", + json: + folder_id: example_folder_id_2 + }, (error, res, body) => + throw error if error? + if res.statusCode < 200 || res.statusCode >= 300 + throw new Error("failed to move folder #{res.statusCode}") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/foo/new.tex") + expect(update.newPathname).to.equal("/bar/foo/new.tex") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/foo/1pixel.png") + expect(update.newPathname).to.equal("/bar/foo/1pixel.png") + + done() + describe "tpds", -> before (done) -> @tpds_project_name = "tpds-project-#{new ObjectId().toString()}" diff --git a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee index 266de9bfb7..111d2ace68 100644 --- a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee +++ b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee @@ -487,7 +487,8 @@ describe 'DocumentUpdaterHandler', -> docUpdates = [ id: @docId.toString(), - pathname: "/foo", + pathname: '/foo', + newPathname: '' ] @handler.updateProjectStructure @project_id, @user_id, @changes, () => From 475e84b0390cc8c1b8872abf6c9fef9443aef6de Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Thu, 14 Dec 2017 16:03:43 +0000 Subject: [PATCH 074/109] version entity deletions in ProjectEntityHandler --- .../Project/ProjectEntityHandler.coffee | 33 ++++++----- .../Project/ProjectEntityHandlerTests.coffee | 57 ++++++++++++------- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 446786fa57..4ff3818516 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -423,7 +423,7 @@ module.exports = ProjectEntityHandler = return callback(error) if error? projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=> return callback(error) if error? - ProjectEntityHandler._cleanUpEntity project, entity, entityType, (error) -> + ProjectEntityHandler._cleanUpEntity project, entity, entityType, path.fileSystem, (error) -> return callback(error) if error? tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, (error) -> return callback(error) if error? @@ -456,17 +456,17 @@ module.exports = ProjectEntityHandler = return callback(error) if error? DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback - _cleanUpEntity: (project, entity, entityType, callback = (error) ->) -> + _cleanUpEntity: (project, entity, entityType, path, callback = (error) ->) -> if(entityType.indexOf("file") != -1) - ProjectEntityHandler._cleanUpFile project, entity, callback + ProjectEntityHandler._cleanUpFile project, entity, path, callback else if (entityType.indexOf("doc") != -1) - ProjectEntityHandler._cleanUpDoc project, entity, callback + ProjectEntityHandler._cleanUpDoc project, entity, path, callback else if (entityType.indexOf("folder") != -1) - ProjectEntityHandler._cleanUpFolder project, entity, callback + ProjectEntityHandler._cleanUpFolder project, entity, path, callback else callback() - _cleanUpDoc: (project, doc, callback = (error) ->) -> + _cleanUpDoc: (project, doc, path, callback = (error) ->) -> project_id = project._id.toString() doc_id = doc._id.toString() unsetRootDocIfRequired = (callback) => @@ -483,26 +483,33 @@ module.exports = ProjectEntityHandler = return callback(error) if error? DocstoreManager.deleteDoc project_id, doc_id, (error) -> return callback(error) if error? - callback() + changes = oldDocs: [ {doc, path} ] + DocumentUpdaterHandler.updateProjectStructure project_id, null, changes, callback - _cleanUpFile: (project, file, callback = (error) ->) -> + _cleanUpFile: (project, file, path, callback = (error) ->) -> project_id = project._id.toString() file_id = file._id.toString() - FileStoreHandler.deleteFile project_id, file_id, callback + FileStoreHandler.deleteFile project_id, file_id, (error) -> + return callback(error) if error? + changes = oldFiles: [ {file, path} ] + DocumentUpdaterHandler.updateProjectStructure project_id, null, changes, callback - _cleanUpFolder: (project, folder, callback = (error) ->) -> + _cleanUpFolder: (project, folder, folderPath, callback = (error) ->) -> jobs = [] for doc in folder.docs do (doc) -> - jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, callback + docPath = path.join(folderPath, doc.name) + jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, docPath, callback for file in folder.fileRefs do (file) -> - jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, callback + filePath = path.join(folderPath, file.name) + jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, filePath, callback for childFolder in folder.folders do (childFolder) -> - jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, callback + folderPath = path.join(folderPath, childFolder.name) + jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, folderPath, callback async.series jobs, callback diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee index f62690e226..faaaa146da 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee @@ -157,7 +157,7 @@ describe 'ProjectEntityHandler', -> @ProjectGetter.getProject.callsArgWith(2, null, @project) @tpdsUpdateSender.deleteEntity = sinon.stub().callsArg(1) @ProjectEntityHandler._removeElementFromMongoArray = sinon.stub().callsArg(3) - @ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(3) + @ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(4) @path = mongo: "mongo.path", fileSystem: "/file/system/path" @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: entity_id }, @path) @@ -182,7 +182,7 @@ describe 'ProjectEntityHandler', -> it "should clean up the entity from the rest of the system", -> @ProjectEntityHandler._cleanUpEntity - .calledWith(@project, @entity, @type) + .calledWith(@project, @entity, @type, @path.fileSystem) .should.equal true describe "_cleanUpEntity", -> @@ -193,7 +193,9 @@ describe 'ProjectEntityHandler', -> describe "a file", -> beforeEach (done) -> - @ProjectEntityHandler._cleanUpEntity @project, _id: @entity_id, 'file', done + @path = "/file/system/path.png" + @entity = _id: @entity_id + @ProjectEntityHandler._cleanUpEntity @project, @entity, 'file', @path, done it "should delete the file from FileStoreHandler", -> @FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true @@ -201,38 +203,48 @@ describe 'ProjectEntityHandler', -> it "should not attempt to delete from the document updater", -> @documentUpdaterHandler.deleteDoc.called.should.equal false + it "should should send the update to the doc updater", -> + oldFiles = [ file: @entity, path: @path ] + @documentUpdaterHandler.updateProjectStructure + .calledWith(project_id, null, {oldFiles}) + .should.equal true + describe "a doc", -> beforeEach (done) -> - @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2) - @ProjectEntityHandler._cleanUpEntity @project, @entity = {_id: @entity_id}, 'doc', done + @path = "/file/system/path.tex" + @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(3) + @entity = {_id: @entity_id} + @ProjectEntityHandler._cleanUpEntity @project, @entity, 'doc', @path, done it "should clean up the doc", -> @ProjectEntityHandler._cleanUpDoc - .calledWith(@project, @entity) + .calledWith(@project, @entity, @path) .should.equal true describe "a folder", -> beforeEach (done) -> @folder = folders: [ - fileRefs: [ @file1 = {_id: "file-id-1" } ] - docs: [ @doc1 = { _id: "doc-id-1" } ] + name: "subfolder" + fileRefs: [ @file1 = { _id: "file-id-1", name: "file-name-1"} ] + docs: [ @doc1 = { _id: "doc-id-1", name: "doc-name-1" } ] folders: [] ] - fileRefs: [ @file2 = { _id: "file-id-2" } ] - docs: [ @doc2 = { _id: "doc-id-2" } ] + fileRefs: [ @file2 = { _id: "file-id-2", name: "file-name-2" } ] + docs: [ @doc2 = { _id: "doc-id-2", name: "doc-name-2" } ] - @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(2) - @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(2) - @ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", done + @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(3) + @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(3) + path = "/folder" + @ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", path, done it "should clean up all sub files", -> - @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file1).should.equal true - @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file2).should.equal true + @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file1, "/folder/subfolder/file-name-1").should.equal true + @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file2, "/folder/file-name-2").should.equal true it "should clean up all sub docs", -> - @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc1).should.equal true - @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc2).should.equal true + @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc1, "/folder/subfolder/doc-name-1").should.equal true + @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc2, "/folder/doc-name-2").should.equal true describe 'moveEntity', -> beforeEach -> @@ -1116,6 +1128,7 @@ describe 'ProjectEntityHandler', -> @doc = _id: ObjectId() name: "test.tex" + @path = "/path/to/doc" @ProjectEntityHandler.unsetRootDoc = sinon.stub().callsArg(1) @ProjectEntityHandler._insertDeletedDocReference = sinon.stub().callsArg(2) @documentUpdaterHandler.deleteDoc = sinon.stub().callsArg(2) @@ -1125,7 +1138,7 @@ describe 'ProjectEntityHandler', -> describe "when the doc is the root doc", -> beforeEach -> @project.rootDoc_id = @doc._id - @ProjectEntityHandler._cleanUpDoc @project, @doc, @callback + @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, @callback it "should unset the root doc", -> @ProjectEntityHandler.unsetRootDoc @@ -1146,13 +1159,19 @@ describe 'ProjectEntityHandler', -> .calledWith(project_id, @doc._id.toString()) .should.equal true + it "should should send the update to the doc updater", -> + oldDocs = [ doc: @doc, path: @path ] + @documentUpdaterHandler.updateProjectStructure + .calledWith(project_id, null, {oldDocs}) + .should.equal true + it "should call the callback", -> @callback.called.should.equal true describe "when the doc is not the root doc", -> beforeEach -> @project.rootDoc_id = ObjectId() - @ProjectEntityHandler._cleanUpDoc @project, @doc, @callback + @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, @callback it "should not unset the root doc", -> @ProjectEntityHandler.unsetRootDoc.called.should.equal false From 99a52d48c8bc32c1685b2a411e98c5c929493f28 Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Thu, 14 Dec 2017 16:03:57 +0000 Subject: [PATCH 075/109] acceptance test versioning entity deletions --- .../coffee/ProjectStructureTests.coffee | 32 +++++++++++++++++-- .../coffee/helpers/MockDocUpdaterApi.coffee | 3 ++ .../coffee/helpers/MockDocstoreApi.coffee | 10 ++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee index 92fcd2e34e..0f8086647f 100644 --- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee @@ -261,12 +261,12 @@ describe "ProjectStructureChanges", -> name: 'bar' }, (error, res, body) => throw error if error? - example_folder_id_2 = JSON.parse(body)._id + @example_folder_id_2 = JSON.parse(body)._id @owner.request.post { uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_1}/move", json: - folder_id: example_folder_id_2 + folder_id: @example_folder_id_2 }, (error, res, body) => throw error if error? if res.statusCode < 200 || res.statusCode >= 300 @@ -288,6 +288,34 @@ describe "ProjectStructureChanges", -> done() + describe "deleting entities", -> + beforeEach () -> + MockDocUpdaterApi.clearProjectStructureUpdates() + + it "should version deleting a folder", (done) -> + @owner.request.delete { + uri: "project/#{@example_project_id}/Folder/#{@example_folder_id_2}", + }, (error, res, body) => + throw error if error? + if res.statusCode < 200 || res.statusCode >= 300 + throw new Error("failed to delete folder #{res.statusCode}") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates + expect(updates.length).to.equal(1) + update = updates[0] + #expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/bar/foo/new.tex") + expect(update.newPathname).to.equal("") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates + expect(updates.length).to.equal(1) + update = updates[0] + #expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/bar/foo/1pixel.png") + expect(update.newPathname).to.equal("") + + done() + describe "tpds", -> before (done) -> @tpds_project_name = "tpds-project-#{new ObjectId().toString()}" diff --git a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee index b00cd6b173..b21fb1adab 100644 --- a/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockDocUpdaterApi.coffee @@ -33,6 +33,9 @@ module.exports = MockDocUpdaterApi = @addProjectStructureUpdates(project_id, userId, docUpdates, fileUpdates) res.sendStatus 200 + app.delete "/project/:project_id/doc/:doc_id", (req, res, next) => + res.send 204 + app.listen 3003, (error) -> throw error if error? .on "error", (error) -> diff --git a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee index c5b003ac75..631538fd89 100644 --- a/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee +++ b/services/web/test/acceptance/coffee/helpers/MockDocstoreApi.coffee @@ -23,6 +23,16 @@ module.exports = MockDocStoreApi = docs = (doc for doc_id, doc of @docs[req.params.project_id]) res.send JSON.stringify docs + app.delete "/project/:project_id/doc/:doc_id", (req, res, next) => + {project_id, doc_id} = req.params + if !@docs[project_id]? + res.send 404 + else if !@docs[project_id][doc_id]? + res.send 404 + else + @docs[project_id][doc_id] = undefined + res.send 204 + app.listen 3016, (error) -> throw error if error? .on "error", (error) -> From 2ac74b9adcadb3bd9fb166346b6f43c376cc60e8 Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Fri, 15 Dec 2017 10:14:01 +0000 Subject: [PATCH 076/109] pass userId into _clean methods in ProjectEntityHandler --- .../Project/ProjectEntityHandler.coffee | 26 ++++++------ .../Project/ProjectEntityHandlerTests.coffee | 42 +++++++++++-------- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 4ff3818516..644f60349a 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -423,7 +423,7 @@ module.exports = ProjectEntityHandler = return callback(error) if error? projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=> return callback(error) if error? - ProjectEntityHandler._cleanUpEntity project, entity, entityType, path.fileSystem, (error) -> + ProjectEntityHandler._cleanUpEntity project, null, entity, entityType, path.fileSystem, (error) -> return callback(error) if error? tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, (error) -> return callback(error) if error? @@ -456,17 +456,17 @@ module.exports = ProjectEntityHandler = return callback(error) if error? DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback - _cleanUpEntity: (project, entity, entityType, path, callback = (error) ->) -> + _cleanUpEntity: (project, userId, entity, entityType, path, callback = (error) ->) -> if(entityType.indexOf("file") != -1) - ProjectEntityHandler._cleanUpFile project, entity, path, callback + ProjectEntityHandler._cleanUpFile project, userId, entity, path, callback else if (entityType.indexOf("doc") != -1) - ProjectEntityHandler._cleanUpDoc project, entity, path, callback + ProjectEntityHandler._cleanUpDoc project, userId, entity, path, callback else if (entityType.indexOf("folder") != -1) - ProjectEntityHandler._cleanUpFolder project, entity, path, callback + ProjectEntityHandler._cleanUpFolder project, userId, entity, path, callback else callback() - _cleanUpDoc: (project, doc, path, callback = (error) ->) -> + _cleanUpDoc: (project, userId, doc, path, callback = (error) ->) -> project_id = project._id.toString() doc_id = doc._id.toString() unsetRootDocIfRequired = (callback) => @@ -484,32 +484,32 @@ module.exports = ProjectEntityHandler = DocstoreManager.deleteDoc project_id, doc_id, (error) -> return callback(error) if error? changes = oldDocs: [ {doc, path} ] - DocumentUpdaterHandler.updateProjectStructure project_id, null, changes, callback + DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback - _cleanUpFile: (project, file, path, callback = (error) ->) -> + _cleanUpFile: (project, userId, file, path, callback = (error) ->) -> project_id = project._id.toString() file_id = file._id.toString() FileStoreHandler.deleteFile project_id, file_id, (error) -> return callback(error) if error? changes = oldFiles: [ {file, path} ] - DocumentUpdaterHandler.updateProjectStructure project_id, null, changes, callback + DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback - _cleanUpFolder: (project, folder, folderPath, callback = (error) ->) -> + _cleanUpFolder: (project, userId, folder, folderPath, callback = (error) ->) -> jobs = [] for doc in folder.docs do (doc) -> docPath = path.join(folderPath, doc.name) - jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, docPath, callback + jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, userId, doc, docPath, callback for file in folder.fileRefs do (file) -> filePath = path.join(folderPath, file.name) - jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, filePath, callback + jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, userId, file, filePath, callback for childFolder in folder.folders do (childFolder) -> folderPath = path.join(folderPath, childFolder.name) - jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, folderPath, callback + jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, userId, childFolder, folderPath, callback async.series jobs, callback diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee index faaaa146da..63240f670e 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee @@ -157,7 +157,7 @@ describe 'ProjectEntityHandler', -> @ProjectGetter.getProject.callsArgWith(2, null, @project) @tpdsUpdateSender.deleteEntity = sinon.stub().callsArg(1) @ProjectEntityHandler._removeElementFromMongoArray = sinon.stub().callsArg(3) - @ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(4) + @ProjectEntityHandler._cleanUpEntity = sinon.stub().callsArg(5) @path = mongo: "mongo.path", fileSystem: "/file/system/path" @projectLocator.findElement = sinon.stub().callsArgWith(1, null, @entity = { _id: entity_id }, @path) @@ -182,7 +182,7 @@ describe 'ProjectEntityHandler', -> it "should clean up the entity from the rest of the system", -> @ProjectEntityHandler._cleanUpEntity - .calledWith(@project, @entity, @type, @path.fileSystem) + .calledWith(@project, null, @entity, @type, @path.fileSystem) .should.equal true describe "_cleanUpEntity", -> @@ -195,7 +195,7 @@ describe 'ProjectEntityHandler', -> beforeEach (done) -> @path = "/file/system/path.png" @entity = _id: @entity_id - @ProjectEntityHandler._cleanUpEntity @project, @entity, 'file', @path, done + @ProjectEntityHandler._cleanUpEntity @project, userId, @entity, 'file', @path, done it "should delete the file from FileStoreHandler", -> @FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true @@ -206,19 +206,19 @@ describe 'ProjectEntityHandler', -> it "should should send the update to the doc updater", -> oldFiles = [ file: @entity, path: @path ] @documentUpdaterHandler.updateProjectStructure - .calledWith(project_id, null, {oldFiles}) + .calledWith(project_id, userId, {oldFiles}) .should.equal true describe "a doc", -> beforeEach (done) -> @path = "/file/system/path.tex" - @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(3) + @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4) @entity = {_id: @entity_id} - @ProjectEntityHandler._cleanUpEntity @project, @entity, 'doc', @path, done + @ProjectEntityHandler._cleanUpEntity @project, userId, @entity, 'doc', @path, done it "should clean up the doc", -> @ProjectEntityHandler._cleanUpDoc - .calledWith(@project, @entity, @path) + .calledWith(@project, userId, @entity, @path) .should.equal true describe "a folder", -> @@ -233,18 +233,26 @@ describe 'ProjectEntityHandler', -> fileRefs: [ @file2 = { _id: "file-id-2", name: "file-name-2" } ] docs: [ @doc2 = { _id: "doc-id-2", name: "doc-name-2" } ] - @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(3) - @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(3) + @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4) + @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(4) path = "/folder" - @ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", path, done + @ProjectEntityHandler._cleanUpEntity @project, userId, @folder, "folder", path, done it "should clean up all sub files", -> - @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file1, "/folder/subfolder/file-name-1").should.equal true - @ProjectEntityHandler._cleanUpFile.calledWith(@project, @file2, "/folder/file-name-2").should.equal true + @ProjectEntityHandler._cleanUpFile + .calledWith(@project, userId, @file1, "/folder/subfolder/file-name-1") + .should.equal true + @ProjectEntityHandler._cleanUpFile + .calledWith(@project, userId, @file2, "/folder/file-name-2") + .should.equal true it "should clean up all sub docs", -> - @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc1, "/folder/subfolder/doc-name-1").should.equal true - @ProjectEntityHandler._cleanUpDoc.calledWith(@project, @doc2, "/folder/doc-name-2").should.equal true + @ProjectEntityHandler._cleanUpDoc + .calledWith(@project, userId, @doc1, "/folder/subfolder/doc-name-1") + .should.equal true + @ProjectEntityHandler._cleanUpDoc + .calledWith(@project, userId, @doc2, "/folder/doc-name-2") + .should.equal true describe 'moveEntity', -> beforeEach -> @@ -1138,7 +1146,7 @@ describe 'ProjectEntityHandler', -> describe "when the doc is the root doc", -> beforeEach -> @project.rootDoc_id = @doc._id - @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, @callback + @ProjectEntityHandler._cleanUpDoc @project, userId, @doc, @path, @callback it "should unset the root doc", -> @ProjectEntityHandler.unsetRootDoc @@ -1162,7 +1170,7 @@ describe 'ProjectEntityHandler', -> it "should should send the update to the doc updater", -> oldDocs = [ doc: @doc, path: @path ] @documentUpdaterHandler.updateProjectStructure - .calledWith(project_id, null, {oldDocs}) + .calledWith(project_id, userId, {oldDocs}) .should.equal true it "should call the callback", -> @@ -1171,7 +1179,7 @@ describe 'ProjectEntityHandler', -> describe "when the doc is not the root doc", -> beforeEach -> @project.rootDoc_id = ObjectId() - @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, @callback + @ProjectEntityHandler._cleanUpDoc @project, userId, @doc, @path, @callback it "should not unset the root doc", -> @ProjectEntityHandler.unsetRootDoc.called.should.equal false From 5f6686ed3b5da84b4a67205bb8d2e9046084a64d Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Fri, 15 Dec 2017 10:23:10 +0000 Subject: [PATCH 077/109] pass userId to ProjectEntityHandler.deleteEntity --- .../Features/Editor/EditorController.coffee | 6 ++-- .../Project/ProjectEntityHandler.coffee | 4 +-- .../Editor/EditorControllerTests.coffee | 35 +++++++++---------- .../Project/ProjectEntityHandlerTests.coffee | 4 +-- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index f66094f9e8..75741ceef2 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -110,14 +110,14 @@ module.exports = EditorController = if err? logger.err err:err, project_id:project_id, "could not get lock to deleteEntity" return callback(err) - EditorController.deleteEntityWithoutLock project_id, entity_id, entityType, source, (err)-> + EditorController.deleteEntityWithoutLock project_id, entity_id, entityType, source, null, (err)-> LockManager.releaseLock project_id, ()-> callback(err) - deleteEntityWithoutLock: (project_id, entity_id, entityType, source, callback)-> + deleteEntityWithoutLock: (project_id, entity_id, entityType, source, userId, callback)-> logger.log {project_id, entity_id, entityType, source}, "start delete process of entity" Metrics.inc "editor.delete-entity" - ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, (err)-> + ProjectEntityHandler.deleteEntity project_id, entity_id, entityType, userId, (err)-> if err? logger.err err:err, project_id:project_id, entity_id:entity_id, entityType:entityType, "error deleting entity" return callback(err) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 644f60349a..0dec3f191b 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -412,7 +412,7 @@ module.exports = ProjectEntityHandler = callback() - deleteEntity: (project_id, entity_id, entityType, callback = (error) ->)-> + deleteEntity: (project_id, entity_id, entityType, userId, callback = (error) ->)-> self = @ logger.log entity_id:entity_id, entityType:entityType, project_id:project_id, "deleting project entity" if !entityType? @@ -423,7 +423,7 @@ module.exports = ProjectEntityHandler = return callback(error) if error? projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=> return callback(error) if error? - ProjectEntityHandler._cleanUpEntity project, null, entity, entityType, path.fileSystem, (error) -> + ProjectEntityHandler._cleanUpEntity project, userId, entity, entityType, path.fileSystem, (error) -> return callback(error) if error? tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, (error) -> return callback(error) if error? diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee index 4e1af79b46..1e84352d38 100644 --- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee @@ -382,11 +382,13 @@ describe "EditorController", -> beforeEach -> @LockManager.getLock.callsArgWith(1) @LockManager.releaseLock.callsArgWith(1) - @EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(4) + @EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(5) it "should call deleteEntityWithoutLock", (done)-> @EditorController.deleteEntity @project_id, @entity_id, @type, @source, => - @EditorController.deleteEntityWithoutLock.calledWith(@project_id, @entity_id, @type, @source).should.equal true + @EditorController.deleteEntityWithoutLock + .calledWith(@project_id, @entity_id, @type, @source, null) + .should.equal true done() it "should take the lock", (done)-> @@ -404,30 +406,25 @@ describe "EditorController", -> @EditorController.deleteEntity @project_id, @entity_id, @type, @source, (err)=> expect(err).to.exist err.should.equal "timed out" - done() - - + done() describe 'deleteEntityWithoutLock', -> - beforeEach -> - @ProjectEntityHandler.deleteEntity = (project_id, entity_id, type, callback)-> callback() + beforeEach (done) -> @entity_id = "entity_id_here" @type = "doc" @EditorRealTimeController.emitToRoom = sinon.stub() + @ProjectEntityHandler.deleteEntity = sinon.stub().callsArg(4) + @EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, @user_id, done - it 'should delete the folder using the project entity handler', (done)-> - mock = sinon.mock(@ProjectEntityHandler).expects("deleteEntity").withArgs(@project_id, @entity_id, @type).callsArg(3) + it 'should delete the folder using the project entity handler', -> + @ProjectEntityHandler.deleteEntity + .calledWith(@project_id, @entity_id, @type, @user_id) + .should.equal.true - @EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, -> - mock.verify() - done() - - it 'notify users an entity has been deleted', (done)-> - @EditorController.deleteEntityWithoutLock @project_id, @entity_id, @type, @source, => - @EditorRealTimeController.emitToRoom - .calledWith(@project_id, "removeEntity", @entity_id, @source) - .should.equal true - done() + it 'notify users an entity has been deleted', -> + @EditorRealTimeController.emitToRoom + .calledWith(@project_id, "removeEntity", @entity_id, @source) + .should.equal true describe "getting a list of project paths", -> diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee index 63240f670e..7a6b62d99f 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee @@ -163,7 +163,7 @@ describe 'ProjectEntityHandler', -> describe "deleting from Mongo", -> beforeEach (done) -> - @ProjectEntityHandler.deleteEntity project_id, entity_id, @type = 'file', done + @ProjectEntityHandler.deleteEntity project_id, entity_id, @type = 'file', userId, done it "should retreive the path", -> @projectLocator.findElement.called.should.equal true @@ -182,7 +182,7 @@ describe 'ProjectEntityHandler', -> it "should clean up the entity from the rest of the system", -> @ProjectEntityHandler._cleanUpEntity - .calledWith(@project, null, @entity, @type, @path.fileSystem) + .calledWith(@project, userId, @entity, @type, @path.fileSystem) .should.equal true describe "_cleanUpEntity", -> From ca15fdb6eb74f6873ad3c409bcb44282884b82fe Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Fri, 15 Dec 2017 10:53:38 +0000 Subject: [PATCH 078/109] pass userId to EditorHttpController.deleteEntity --- .../Features/Editor/EditorController.coffee | 4 ++-- .../Features/Editor/EditorHttpController.coffee | 3 ++- .../coffee/ProjectStructureTests.coffee | 4 ++-- .../coffee/Editor/EditorControllerTests.coffee | 16 +++++++--------- .../Editor/EditorHttpControllerTests.coffee | 4 ++-- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/services/web/app/coffee/Features/Editor/EditorController.coffee b/services/web/app/coffee/Features/Editor/EditorController.coffee index 75741ceef2..532f515a10 100644 --- a/services/web/app/coffee/Features/Editor/EditorController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorController.coffee @@ -105,12 +105,12 @@ module.exports = EditorController = async.series jobs, (err)-> callback err, newFolders, lastFolder - deleteEntity : (project_id, entity_id, entityType, source, callback)-> + deleteEntity : (project_id, entity_id, entityType, source, userId, callback)-> LockManager.getLock project_id, (err)-> if err? logger.err err:err, project_id:project_id, "could not get lock to deleteEntity" return callback(err) - EditorController.deleteEntityWithoutLock project_id, entity_id, entityType, source, null, (err)-> + EditorController.deleteEntityWithoutLock project_id, entity_id, entityType, source, userId, (err)-> LockManager.releaseLock project_id, ()-> callback(err) diff --git a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee index cbc296b69f..16b1a79e31 100644 --- a/services/web/app/coffee/Features/Editor/EditorHttpController.coffee +++ b/services/web/app/coffee/Features/Editor/EditorHttpController.coffee @@ -147,6 +147,7 @@ module.exports = EditorHttpController = project_id = req.params.Project_id entity_id = req.params.entity_id entity_type = req.params.entity_type - EditorController.deleteEntity project_id, entity_id, entity_type, "editor", (error) -> + user_id = AuthenticationController.getLoggedInUserId(req) + EditorController.deleteEntity project_id, entity_id, entity_type, "editor", user_id, (error) -> return next(error) if error? res.sendStatus 204 diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee index 0f8086647f..fa91f7aeff 100644 --- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee @@ -303,14 +303,14 @@ describe "ProjectStructureChanges", -> updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).docUpdates expect(updates.length).to.equal(1) update = updates[0] - #expect(update.userId).to.equal(@owner._id) + expect(update.userId).to.equal(@owner._id) expect(update.pathname).to.equal("/bar/foo/new.tex") expect(update.newPathname).to.equal("") updates = MockDocUpdaterApi.getProjectStructureUpdates(@example_project_id).fileUpdates expect(updates.length).to.equal(1) update = updates[0] - #expect(update.userId).to.equal(@owner._id) + expect(update.userId).to.equal(@owner._id) expect(update.pathname).to.equal("/bar/foo/1pixel.png") expect(update.newPathname).to.equal("") diff --git a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee index 1e84352d38..85760f510d 100644 --- a/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee +++ b/services/web/test/unit/coffee/Editor/EditorControllerTests.coffee @@ -376,36 +376,34 @@ describe "EditorController", -> err.should.equal "timed out" done() - describe "deleteEntity", -> - beforeEach -> @LockManager.getLock.callsArgWith(1) @LockManager.releaseLock.callsArgWith(1) @EditorController.deleteEntityWithoutLock = sinon.stub().callsArgWith(5) it "should call deleteEntityWithoutLock", (done)-> - @EditorController.deleteEntity @project_id, @entity_id, @type, @source, => + @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, => @EditorController.deleteEntityWithoutLock - .calledWith(@project_id, @entity_id, @type, @source, null) + .calledWith(@project_id, @entity_id, @type, @source, @user_id) .should.equal true done() it "should take the lock", (done)-> - @EditorController.deleteEntity @project_id, @entity_id, @type, @source, => + @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, => @LockManager.getLock.calledWith(@project_id).should.equal true done() it "should release the lock", (done)-> - @EditorController.deleteEntity @project_id, @entity_id, @type, @source, (error)=> + @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) => @LockManager.releaseLock.calledWith(@project_id).should.equal true done() it "should error if it can't cat the lock", (done)-> @LockManager.getLock = sinon.stub().callsArgWith(1, "timed out") - @EditorController.deleteEntity @project_id, @entity_id, @type, @source, (err)=> - expect(err).to.exist - err.should.equal "timed out" + @EditorController.deleteEntity @project_id, @entity_id, @type, @source, @user_id, (error) => + expect(error).to.exist + error.should.equal "timed out" done() describe 'deleteEntityWithoutLock', -> diff --git a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee index e05846c5d5..38419e6b46 100644 --- a/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee +++ b/services/web/test/unit/coffee/Editor/EditorHttpControllerTests.coffee @@ -331,12 +331,12 @@ describe "EditorHttpController", -> Project_id: @project_id entity_id: @entity_id = "entity-id-123" entity_type: @entity_type = "entity-type" - @EditorController.deleteEntity = sinon.stub().callsArg(4) + @EditorController.deleteEntity = sinon.stub().callsArg(5) @EditorHttpController.deleteEntity @req, @res it "should call EditorController.deleteEntity", -> @EditorController.deleteEntity - .calledWith(@project_id, @entity_id, @entity_type, "editor") + .calledWith(@project_id, @entity_id, @entity_type, "editor", @userId) .should.equal true it "should send back a success response", -> From 938caed4f72dd17d83bac61cab28e6895926f60c Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Fri, 15 Dec 2017 11:32:43 +0000 Subject: [PATCH 079/109] set userId when deleting entities via the tpds --- .../TpdsUpdateHandler.coffee | 2 +- .../ThirdPartyDataStore/UpdateMerger.coffee | 4 ++-- .../coffee/ProjectStructureTests.coffee | 22 +++++++++++++++++++ .../TpdsUpdateHandlerTests.coffee | 8 ++++--- .../UpdateMergerTests.coffee | 6 ++--- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee index c78b588e8a..78e3f12ed6 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/TpdsUpdateHandler.coffee @@ -47,7 +47,7 @@ module.exports = logger.log user_id:user_id, filePath:path, projectName:projectName, project_id:project._id, "project found for delete update, path is root so marking project as deleted" return projectDeleter.markAsDeletedByExternalSource project._id, callback else - updateMerger.deleteUpdate project._id, path, source, (err)-> + updateMerger.deleteUpdate user_id, project._id, path, source, (err)-> callback(err) diff --git a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee index 010594d16a..e49c52aab2 100644 --- a/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee +++ b/services/web/app/coffee/Features/ThirdPartyDataStore/UpdateMerger.coffee @@ -32,13 +32,13 @@ module.exports = else self.p.processDoc project_id, elementId, user_id, fsPath, path, source, callback - deleteUpdate: (project_id, path, source, callback)-> + deleteUpdate: (user_id, project_id, path, source, callback)-> projectLocator.findElementByPath project_id, path, (err, element, type)-> if err? || !element? logger.log element:element, project_id:project_id, path:path, "could not find entity for deleting, assuming it was already deleted" return callback() logger.log project_id:project_id, path:path, type:type, element:element, "processing update to delete entity from tpds" - editorController.deleteEntity project_id, element._id, type, source, (err)-> + editorController.deleteEntity project_id, element._id, type, source, user_id, (err)-> logger.log project_id:project_id, path:path, "finished processing update to delete entity from tpds" callback() diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee index fa91f7aeff..cb59efe1df 100644 --- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee +++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee @@ -422,3 +422,25 @@ describe "ProjectStructureChanges", -> done() image_file.pipe(req) + + it "should version deleting a doc", (done) -> + req = @owner.request.delete { + uri: "/user/#{@owner._id}/update/#{@tpds_project_name}/test.tex", + auth: + user: _.keys(Settings.httpAuthUsers)[0] + pass: _.values(Settings.httpAuthUsers)[0] + sendImmediately: true + }, (error, res, body) => + throw error if error? + if res.statusCode < 200 || res.statusCode >= 300 + throw new Error("failed to delete doc #{res.statusCode}") + + updates = MockDocUpdaterApi.getProjectStructureUpdates(@tpds_project_id).docUpdates + expect(updates.length).to.equal(1) + update = updates[0] + expect(update.userId).to.equal(@owner._id) + expect(update.pathname).to.equal("/test.tex") + expect(update.newPathname).to.equal("") + + done() + diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee index dedd3ea7c8..3984d6341f 100644 --- a/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee +++ b/services/web/test/unit/coffee/ThirdPartyDataStore/TpdsUpdateHandlerTests.coffee @@ -8,7 +8,7 @@ describe 'TpdsUpdateHandler', -> beforeEach -> @requestQueuer = {} @updateMerger = - deleteUpdate: (user_id, path, source, cb)->cb() + deleteUpdate: (user_id, project_id, path, source, cb)->cb() mergeUpdate:(user_id, project_id, path, update, source, cb)->cb() @editorController = {} @project_id = "dsjajilknaksdn" @@ -107,11 +107,13 @@ describe 'TpdsUpdateHandler', -> it 'should call deleteEntity in the collaberation manager', (done)-> path = "/delete/this" update = {} - @updateMerger.deleteUpdate = sinon.stub().callsArg(3) + @updateMerger.deleteUpdate = sinon.stub().callsArg(4) @handler.deleteUpdate @user_id, @project.name, path, @source, => @projectDeleter.markAsDeletedByExternalSource.calledWith(@project._id).should.equal false - @updateMerger.deleteUpdate.calledWith(@project_id, path, @source).should.equal true + @updateMerger.deleteUpdate + .calledWith(@user_id, @project_id, path, @source) + .should.equal true done() it 'should mark the project as deleted by external source if path is a single slash', (done)-> diff --git a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee index e20419765f..cb2aa059ea 100644 --- a/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee +++ b/services/web/test/unit/coffee/ThirdPartyDataStore/UpdateMergerTests.coffee @@ -145,13 +145,13 @@ describe 'UpdateMerger :', -> it 'should get the element id', -> @projectLocator.findElementByPath = sinon.spy() - @updateMerger.deleteUpdate @project_id, @path, @source, -> + @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, -> @projectLocator.findElementByPath.calledWith(@project_id, @path).should.equal true it 'should delete the entity in the editor controller with the correct type', (done)-> @entity.lines = [] - mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source).callsArg(4) - @updateMerger.deleteUpdate @project_id, @path, @source, -> + mock = sinon.mock(@editorController).expects("deleteEntity").withArgs(@project_id, @entity_id, @type, @source, @user_id).callsArg(5) + @updateMerger.deleteUpdate @user_id, @project_id, @path, @source, -> mock.verify() done() From ac36de962924e09f98008178b5911161adad7bed Mon Sep 17 00:00:00 2001 From: Hayden Faulds Date: Fri, 15 Dec 2017 13:17:26 +0000 Subject: [PATCH 080/109] make ProjectEntityHandler._clean* argument signatures consistent --- .../Project/ProjectEntityHandler.coffee | 22 +++++++++---------- .../Project/ProjectEntityHandlerTests.coffee | 22 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee index 0dec3f191b..a9d7c708ee 100644 --- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee +++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee @@ -423,7 +423,7 @@ module.exports = ProjectEntityHandler = return callback(error) if error? projectLocator.findElement {project: project, element_id: entity_id, type: entityType}, (error, entity, path)=> return callback(error) if error? - ProjectEntityHandler._cleanUpEntity project, userId, entity, entityType, path.fileSystem, (error) -> + ProjectEntityHandler._cleanUpEntity project, entity, entityType, path.fileSystem, userId, (error) -> return callback(error) if error? tpdsUpdateSender.deleteEntity project_id:project_id, path:path.fileSystem, project_name:project.name, (error) -> return callback(error) if error? @@ -456,17 +456,17 @@ module.exports = ProjectEntityHandler = return callback(error) if error? DocumentUpdaterHandler.updateProjectStructure project_id, userId, {oldDocs, newDocs, oldFiles, newFiles}, callback - _cleanUpEntity: (project, userId, entity, entityType, path, callback = (error) ->) -> + _cleanUpEntity: (project, entity, entityType, path, userId, callback = (error) ->) -> if(entityType.indexOf("file") != -1) - ProjectEntityHandler._cleanUpFile project, userId, entity, path, callback + ProjectEntityHandler._cleanUpFile project, entity, path, userId, callback else if (entityType.indexOf("doc") != -1) - ProjectEntityHandler._cleanUpDoc project, userId, entity, path, callback + ProjectEntityHandler._cleanUpDoc project, entity, path, userId, callback else if (entityType.indexOf("folder") != -1) - ProjectEntityHandler._cleanUpFolder project, userId, entity, path, callback + ProjectEntityHandler._cleanUpFolder project, entity, path, userId, callback else callback() - _cleanUpDoc: (project, userId, doc, path, callback = (error) ->) -> + _cleanUpDoc: (project, doc, path, userId, callback = (error) ->) -> project_id = project._id.toString() doc_id = doc._id.toString() unsetRootDocIfRequired = (callback) => @@ -486,7 +486,7 @@ module.exports = ProjectEntityHandler = changes = oldDocs: [ {doc, path} ] DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback - _cleanUpFile: (project, userId, file, path, callback = (error) ->) -> + _cleanUpFile: (project, file, path, userId, callback = (error) ->) -> project_id = project._id.toString() file_id = file._id.toString() FileStoreHandler.deleteFile project_id, file_id, (error) -> @@ -494,22 +494,22 @@ module.exports = ProjectEntityHandler = changes = oldFiles: [ {file, path} ] DocumentUpdaterHandler.updateProjectStructure project_id, userId, changes, callback - _cleanUpFolder: (project, userId, folder, folderPath, callback = (error) ->) -> + _cleanUpFolder: (project, folder, folderPath, userId, callback = (error) ->) -> jobs = [] for doc in folder.docs do (doc) -> docPath = path.join(folderPath, doc.name) - jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, userId, doc, docPath, callback + jobs.push (callback) -> ProjectEntityHandler._cleanUpDoc project, doc, docPath, userId, callback for file in folder.fileRefs do (file) -> filePath = path.join(folderPath, file.name) - jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, userId, file, filePath, callback + jobs.push (callback) -> ProjectEntityHandler._cleanUpFile project, file, filePath, userId, callback for childFolder in folder.folders do (childFolder) -> folderPath = path.join(folderPath, childFolder.name) - jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, userId, childFolder, folderPath, callback + jobs.push (callback) -> ProjectEntityHandler._cleanUpFolder project, childFolder, folderPath, userId, callback async.series jobs, callback diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee index 7a6b62d99f..7c4188052f 100644 --- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee +++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee @@ -182,7 +182,7 @@ describe 'ProjectEntityHandler', -> it "should clean up the entity from the rest of the system", -> @ProjectEntityHandler._cleanUpEntity - .calledWith(@project, userId, @entity, @type, @path.fileSystem) + .calledWith(@project, @entity, @type, @path.fileSystem, userId) .should.equal true describe "_cleanUpEntity", -> @@ -195,7 +195,7 @@ describe 'ProjectEntityHandler', -> beforeEach (done) -> @path = "/file/system/path.png" @entity = _id: @entity_id - @ProjectEntityHandler._cleanUpEntity @project, userId, @entity, 'file', @path, done + @ProjectEntityHandler._cleanUpEntity @project, @entity, 'file', @path, userId, done it "should delete the file from FileStoreHandler", -> @FileStoreHandler.deleteFile.calledWith(project_id, @entity_id).should.equal true @@ -214,11 +214,11 @@ describe 'ProjectEntityHandler', -> @path = "/file/system/path.tex" @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4) @entity = {_id: @entity_id} - @ProjectEntityHandler._cleanUpEntity @project, userId, @entity, 'doc', @path, done + @ProjectEntityHandler._cleanUpEntity @project, @entity, 'doc', @path, userId, done it "should clean up the doc", -> @ProjectEntityHandler._cleanUpDoc - .calledWith(@project, userId, @entity, @path) + .calledWith(@project, @entity, @path, userId) .should.equal true describe "a folder", -> @@ -236,22 +236,22 @@ describe 'ProjectEntityHandler', -> @ProjectEntityHandler._cleanUpDoc = sinon.stub().callsArg(4) @ProjectEntityHandler._cleanUpFile = sinon.stub().callsArg(4) path = "/folder" - @ProjectEntityHandler._cleanUpEntity @project, userId, @folder, "folder", path, done + @ProjectEntityHandler._cleanUpEntity @project, @folder, "folder", path, userId, done it "should clean up all sub files", -> @ProjectEntityHandler._cleanUpFile - .calledWith(@project, userId, @file1, "/folder/subfolder/file-name-1") + .calledWith(@project, @file1, "/folder/subfolder/file-name-1", userId) .should.equal true @ProjectEntityHandler._cleanUpFile - .calledWith(@project, userId, @file2, "/folder/file-name-2") + .calledWith(@project, @file2, "/folder/file-name-2", userId) .should.equal true it "should clean up all sub docs", -> @ProjectEntityHandler._cleanUpDoc - .calledWith(@project, userId, @doc1, "/folder/subfolder/doc-name-1") + .calledWith(@project, @doc1, "/folder/subfolder/doc-name-1", userId) .should.equal true @ProjectEntityHandler._cleanUpDoc - .calledWith(@project, userId, @doc2, "/folder/doc-name-2") + .calledWith(@project, @doc2, "/folder/doc-name-2", userId) .should.equal true describe 'moveEntity', -> @@ -1146,7 +1146,7 @@ describe 'ProjectEntityHandler', -> describe "when the doc is the root doc", -> beforeEach -> @project.rootDoc_id = @doc._id - @ProjectEntityHandler._cleanUpDoc @project, userId, @doc, @path, @callback + @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback it "should unset the root doc", -> @ProjectEntityHandler.unsetRootDoc @@ -1179,7 +1179,7 @@ describe 'ProjectEntityHandler', -> describe "when the doc is not the root doc", -> beforeEach -> @project.rootDoc_id = ObjectId() - @ProjectEntityHandler._cleanUpDoc @project, userId, @doc, @path, @callback + @ProjectEntityHandler._cleanUpDoc @project, @doc, @path, userId, @callback it "should not unset the root doc", -> @ProjectEntityHandler.unsetRootDoc.called.should.equal false From 4e02033c82a1f993ff602d8ae5b17c18ce48873a Mon Sep 17 00:00:00 2001 From: Paulo Reis Date: Wed, 13 Dec 2017 16:23:50 +0000 Subject: [PATCH 081/109] Use Lato as the sans-serif font for v2. --- services/web/public/stylesheets/app/editor/toolbar.less | 2 +- .../web/public/stylesheets/core/_common-variables.less | 8 ++------ services/web/public/stylesheets/core/ol-variables.less | 5 +++++ services/web/public/stylesheets/ol-style.less | 3 +++ services/web/public/stylesheets/style.less | 3 +++ 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less index 5c00f00567..e164f4fc02 100644 --- a/services/web/public/stylesheets/app/editor/toolbar.less +++ b/services/web/public/stylesheets/app/editor/toolbar.less @@ -143,7 +143,7 @@ .toolbar-label { display: none; margin: 0 4px; - font-size: 12px; + font-size: @toolbar-font-size; font-weight: 600; margin-bottom: 2px; vertical-align: middle; diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index 5ddac36738..177267779d 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -20,14 +20,9 @@ //== Typography // //## Font, line-height, and color for body text, headings, and more. - -@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); -//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,600,700); -//@import url(https://fonts.googleapis.com/css?family=PT+Serif:400,400i,700,700i); -@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i); - @font-family-sans-serif: "Open Sans", sans-serif; @font-family-serif: "Merriweather", serif; + //** Default monospace fonts for ``, ``, and `
`.
 @font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
 @font-family-base:        @font-family-sans-serif;
@@ -898,6 +893,7 @@
 @toolbar-btn-active-color       : white;
 @toolbar-btn-active-bg-color    : @link-color;
 @toolbar-btn-active-shadow      : inset 0 3px 5px rgba(0, 0, 0, 0.225);
+@toolbar-font-size              : 12px;
 @toolbar-alt-bg-color           : #fafafa;
 @toolbar-icon-btn-color         : @gray-light;
 @toolbar-icon-btn-hover-color   : @gray-dark;
diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less
index 370e47d8d6..290628c976 100644
--- a/services/web/public/stylesheets/core/ol-variables.less
+++ b/services/web/public/stylesheets/core/ol-variables.less
@@ -1,6 +1,9 @@
 @import "./_common-variables.less";
 
 @is-overleaf: true;
+
+@font-family-sans-serif: "Lato", sans-serif;
+
 @header-height: 68px;
 @footer-height: 50px;
 
@@ -176,6 +179,8 @@
 @toolbar-icon-btn-hover-shadow    : none;
 @toolbar-border-bottom            : 1px solid @toolbar-border-color;
 @toolbar-icon-btn-hover-boxshadow : none;
+@toolbar-font-size                : 13px;
+
 // Editor file-tree
 @file-tree-bg                     : @ol-blue-gray-4;
 @file-tree-line-height            : 2.05;
diff --git a/services/web/public/stylesheets/ol-style.less b/services/web/public/stylesheets/ol-style.less
index 2a9a140611..540d106ab3 100644
--- a/services/web/public/stylesheets/ol-style.less
+++ b/services/web/public/stylesheets/ol-style.less
@@ -1,3 +1,6 @@
+@import url(https://fonts.googleapis.com/css?family=Lato:300,400,700&subset=latin-ext);
+@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
+
 // Core variables and mixins
 @import "core/ol-variables.less";
 @import "app/ol-style-guide.less";
diff --git a/services/web/public/stylesheets/style.less b/services/web/public/stylesheets/style.less
index 760f378719..ae5820453f 100755
--- a/services/web/public/stylesheets/style.less
+++ b/services/web/public/stylesheets/style.less
@@ -1,3 +1,6 @@
+@import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+@import url(https://fonts.googleapis.com/css?family=Merriweather:400,400i,700,700i);
+
 // Core variables and mixins
 @import "core/variables.less";
 @import "_style_includes.less";
\ No newline at end of file

From dc2ddf7e0938a5891d79ca751e8f58c1855f301e Mon Sep 17 00:00:00 2001
From: Alasdair Smith 
Date: Mon, 18 Dec 2017 16:53:58 +0000
Subject: [PATCH 082/109] Check for OL free trial instead of host as it may be
 fragile indicator

---
 services/web/app/views/layout.pug | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug
index 14ebc3c576..2c6a836e8f 100644
--- a/services/web/app/views/layout.pug
+++ b/services/web/app/views/layout.pug
@@ -96,7 +96,7 @@ html(itemscope, itemtype='http://schema.org/Product')
 					indexName : '!{settings.templates.indexName}'
 				}
 
-		- if (settings.overleaf && settings.overleaf.host)
+		- if (settings.overleaf && settings.overleaf.useOLFreeTrial)
 			script.
 				window.redirectToOLFreeTrialUrl = '!{settings.overleaf.host}/users/trial'
 

From 8453a7a91037ea65b70a5588d8bcf6623fee435e Mon Sep 17 00:00:00 2001
From: Alasdair Smith 
Date: Mon, 18 Dec 2017 17:26:24 +0000
Subject: [PATCH 083/109] Increase width slightly

---
 services/web/public/stylesheets/app/list/v1-import-modal.less | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/web/public/stylesheets/app/list/v1-import-modal.less b/services/web/public/stylesheets/app/list/v1-import-modal.less
index 14a1813369..eac07d8772 100644
--- a/services/web/public/stylesheets/app/list/v1-import-modal.less
+++ b/services/web/public/stylesheets/app/list/v1-import-modal.less
@@ -28,7 +28,7 @@
 	margin-top: 20px;
 	margin-left: auto;
 	margin-right: auto;
-	width: 80%;
+	width: 90%;
 	text-align: center;
 }
 

From fdd861e82499dd95c6cc21e4a23b8edb7377d0f6 Mon Sep 17 00:00:00 2001
From: Alasdair Smith 
Date: Tue, 19 Dec 2017 10:02:15 +0000
Subject: [PATCH 084/109] Ignore content-addressible compiled require js

---
 services/web/.gitignore | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/web/.gitignore b/services/web/.gitignore
index 83d4d28b65..f4ee8dcbe0 100644
--- a/services/web/.gitignore
+++ b/services/web/.gitignore
@@ -69,7 +69,7 @@ Gemfile.lock
 
 public/stylesheets/ol-style.*.css
 public/stylesheets/style.*.css
-public/js/libs/require.js
+public/js/libs/require*.js
 
 
 *.swp

From 709a4bf4fb7464a3c13860d82e24d9ba077dd0e8 Mon Sep 17 00:00:00 2001
From: Paulo Reis 
Date: Wed, 13 Dec 2017 16:23:50 +0000
Subject: [PATCH 085/109] Adjust padding values.

---
 .../web/public/stylesheets/core/_common-variables.less    | 2 ++
 services/web/public/stylesheets/core/ol-variables.less    | 8 +++++---
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less
index 177267779d..5885235ddc 100644
--- a/services/web/public/stylesheets/core/_common-variables.less
+++ b/services/web/public/stylesheets/core/_common-variables.less
@@ -929,6 +929,8 @@
 @chat-message-name-color          : @gray-light;
 @chat-message-box-shadow          : -1px 2px 3px #ddd;
 @chat-message-border-radius       : 0;
+@chat-message-padding             : @line-height-computed / 2;
+@chat-message-weight              : normal;
 @chat-new-message-bg              : @gray-lightest;
 @chat-new-message-textarea-bg     : #FFF;
 @chat-new-message-textarea-color  : @gray-dark;
diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less
index 290628c976..def0e408f9 100644
--- a/services/web/public/stylesheets/core/ol-variables.less
+++ b/services/web/public/stylesheets/core/ol-variables.less
@@ -204,12 +204,14 @@
 @editor-border-color              : @ol-blue-gray-5;
 
 // Chat
-@chat-bg                          : @ol-blue-gray-6;
+@chat-bg                          : @ol-blue-gray-5;
 @chat-message-color               : #FFF;
 @chat-message-name-color          : #FFF;
-@chat-message-date-color          : @ol-blue-gray-3;
+@chat-message-date-color          : @ol-blue-gray-2;
 @chat-message-box-shadow          : none;
-@chat-message-border-radius       : @border-radius-base;
+@chat-message-padding             : 5px 10px;
+@chat-message-border-radius       : @border-radius-large;
+@chat-message-weight              : bold;
 @chat-new-message-bg              : @ol-blue-gray-4;
 @chat-new-message-textarea-bg     : @ol-blue-gray-1;
 @chat-new-message-textarea-color  : @ol-blue-gray-6;

From 91d479378b86e2bee8d353d295dcec007a85267e Mon Sep 17 00:00:00 2001
From: Paulo Reis 
Date: Tue, 19 Dec 2017 10:51:49 +0000
Subject: [PATCH 086/109] Apply padding and font-weight changes.

---
 services/web/public/stylesheets/app/editor/chat.less | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less
index 1f24f2e4b0..6a08cfc304 100644
--- a/services/web/public/stylesheets/app/editor/chat.less
+++ b/services/web/public/stylesheets/app/editor/chat.less
@@ -73,9 +73,10 @@
 					position: relative;
 					
 					.message-content {
-						padding: @line-height-computed / 2;
+						padding: @chat-message-padding;
 						overflow-x: auto;
 						color: @chat-message-color;
+						font-weight: @chat-message-weight;
 					}
 
 					.arrow {

From 88d669cbb5f44417afc5eedf45201f04d459224c Mon Sep 17 00:00:00 2001
From: Paulo Reis 
Date: Tue, 19 Dec 2017 10:52:17 +0000
Subject: [PATCH 087/109] Remove debugging statements.

---
 .../chat/controllers/ChatMessageController.coffee    | 12 +++---------
 1 file changed, 3 insertions(+), 9 deletions(-)

diff --git a/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee b/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee
index f69e04cad2..45e9821b96 100644
--- a/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee
+++ b/services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee
@@ -16,15 +16,9 @@ define [
 				return ColorManager.getHueForUserId(user.id)
 
 		$scope.getMessageStyle = (user) ->
-			style =
-				"border-color"     : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
-				"background-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.bgSaturation }, #{ hslColorConfigs.bgLightness })"
-			console.log style
-			return style
+			"border-color"     : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
+			"background-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.bgSaturation }, #{ hslColorConfigs.bgLightness })"
 
 		$scope.getArrowStyle = (user) ->
-			style =
-				"border-color"     : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
-			console.log style
-			return style
+			"border-color"     : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
 	]
\ No newline at end of file

From cb83e2c1b2335f5b7f698fecb43a41a3dc4e5a82 Mon Sep 17 00:00:00 2001
From: Paulo Reis 
Date: Tue, 19 Dec 2017 10:52:23 +0000
Subject: [PATCH 088/109] Tone down chat colors.

---
 .../web/app/coffee/infrastructure/ExpressLocals.coffee    | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index 6781aaffcd..54e04f51e5 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -302,8 +302,8 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
 			westResizerCursor          : if isOl then "ew-resize" else null
 			chatResizerSizeOpen        : if isOl then 2 else 12
 			chatResizerSizeClosed      : 0
-			chatMessageBorderSaturation: if isOl then "100%" else "70%"
-			chatMessageBorderLightness : if isOl then "45%" else "70%"
-			chatMessageBgSaturation    : if isOl then "100%" else "60%"
-			chatMessageBgLightness     : if isOl then "45%" else "97%"
+			chatMessageBorderSaturation: if isOl then "85%" else "70%"
+			chatMessageBorderLightness : if isOl then "40%" else "70%"
+			chatMessageBgSaturation    : if isOl then "85%" else "60%"
+			chatMessageBgLightness     : if isOl then "40%" else "97%"
 		next()

From 547c0cb79f2821ce33ab33f6180ec87d31d79cba Mon Sep 17 00:00:00 2001
From: Paulo Reis 
Date: Tue, 19 Dec 2017 13:10:12 +0000
Subject: [PATCH 089/109] Use new theme red shade as the danger red; tone down
 compile log alerts.

---
 .../public/stylesheets/app/editor/pdf.less    | 21 +++++++++++++------
 .../public/stylesheets/core/ol-variables.less |  2 +-
 2 files changed, 16 insertions(+), 7 deletions(-)

diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less
index f5ff31177f..d59ca93069 100644
--- a/services/web/public/stylesheets/app/editor/pdf.less
+++ b/services/web/public/stylesheets/app/editor/pdf.less
@@ -207,16 +207,25 @@
 			}
 		}
 
-		&.alert-danger:hover {
-			background-color: darken(@alert-danger-bg, 5%);
+		&.alert-danger {
+			background-color: tint(@alert-danger-bg, 15%);
+			&:hover {
+				background-color: @alert-danger-bg;
+			}
 		}
 
-		&.alert-warning:hover {
-			background-color: darken(@alert-warning-bg, 5%);
+		&.alert-warning {
+			background-color: tint(@alert-warning-bg, 15%);
+			&:hover {
+				background-color: @alert-warning-bg;
+			}
 		}
 
-		&.alert-info:hover {
-			background-color: darken(@alert-info-bg, 5%);
+		&.alert-info {
+			background-color: tint(@alert-info-bg, 15%);
+			&:hover {
+				background-color: @alert-info-bg;
+			}
 		}
 
 	}
diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less
index aee331f3d2..b89e476567 100644
--- a/services/web/public/stylesheets/core/ol-variables.less
+++ b/services/web/public/stylesheets/core/ol-variables.less
@@ -249,7 +249,7 @@
 @brand-success:         @green;
 @brand-info:            @ol-blue;
 @brand-warning:         @orange;
-@brand-danger:          #E03A06;
+@brand-danger:          @ol-red;
 
 @editor-loading-logo-padding-top: 115.44%;
 @editor-loading-logo-background-url: url(/img/ol-brand/overleaf-o-grey.svg);

From 70298ba65c6bce0a2325c426c9035c1de9158495 Mon Sep 17 00:00:00 2001
From: Henry Oswald 
Date: Tue, 19 Dec 2017 13:13:31 +0000
Subject: [PATCH 090/109] only hash the static content when minified is on

---
 .../infrastructure/ExpressLocals.coffee       | 35 ++++++++++---------
 1 file changed, 19 insertions(+), 16 deletions(-)

diff --git a/services/web/app/coffee/infrastructure/ExpressLocals.coffee b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
index 002a10ca89..a820a8500b 100644
--- a/services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ b/services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -36,7 +36,6 @@ getFileContent = (filePath)->
 		logger.log filePath:filePath, "file does not exist for hashing"
 		return ""
 
-logger.log "Generating file hashes..."
 pathList = [
 	"#{jsPath}libs/require.js"
 	"#{jsPath}ide.js"
@@ -46,23 +45,27 @@ pathList = [
 	"/stylesheets/ol-style.css"
 ]
 
-for path in pathList
-	content = getFileContent(path)
-	hash = crypto.createHash("md5").update(content).digest("hex")
-	
-	splitPath = path.split("/")
-	filenameSplit = splitPath.pop().split(".")
-	filenameSplit.splice(filenameSplit.length-1, 0, hash)
-	splitPath.push(filenameSplit.join("."))
+if !Settings.useMinifiedJs 
+	logger.log "not using minified JS, not hashing static files"
+else
+	logger.log "Generating file hashes..."
+	for path in pathList
+		content = getFileContent(path)
+		hash = crypto.createHash("md5").update(content).digest("hex")
+		
+		splitPath = path.split("/")
+		filenameSplit = splitPath.pop().split(".")
+		filenameSplit.splice(filenameSplit.length-1, 0, hash)
+		splitPath.push(filenameSplit.join("."))
 
-	hashPath = splitPath.join("/")
-	hashedFiles[path] = hashPath
+		hashPath = splitPath.join("/")
+		hashedFiles[path] = hashPath
 
-	fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}"
-	fs.writeFileSync(fsHashPath, content)
+		fsHashPath = Path.join __dirname, "../../../", "public#{hashPath}"
+		fs.writeFileSync(fsHashPath, content)
 
 
-logger.log "Finished hashing static content"
+		logger.log "Finished hashing static content"
 
 cdnAvailable = Settings.cdn?.web?.host?
 darkCdnAvailable = Settings.cdn?.web?.darkHost?
@@ -121,7 +124,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
 		res.locals.buildJsPath = (jsFile, opts = {})->
 			path = Path.join(jsPath, jsFile)
 
-			if opts.hashedPath
+			if opts.hashedPath && hashedFiles[path]?
 				path = hashedFiles[path]
 
 			if !opts.qs?
@@ -141,7 +144,7 @@ module.exports = (app, webRouter, privateApiRouter, publicApiRouter)->
 
 		res.locals.buildCssPath = (cssFile, opts)->
 			path = Path.join("/stylesheets/", cssFile)
-			if opts?.hashedPath
+			if opts?.hashedPath && hashedFiles[path]?
 				hashedPath = hashedFiles[path]
 				return Url.resolve(staticFilesBase, hashedPath)
 			return Url.resolve(staticFilesBase, path)

From a1615e6d846c7471dfc527bac79ebf1559b95f3f Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Tue, 28 Nov 2017 18:18:15 +0000
Subject: [PATCH 091/109] Prototype of requesting history information by doc
 path, while tracking renames

---
 .../web/app/views/project/editor/history.pug  |   6 +-
 .../coffee/ide/history/HistoryManager.coffee  | 129 ++++++++++++------
 2 files changed, 92 insertions(+), 43 deletions(-)

diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug
index 4806b0a9b8..f62dcebf15 100644
--- a/services/web/app/views/project/editor/history.pug
+++ b/services/web/app/views/project/editor/history.pug
@@ -134,8 +134,10 @@ div#history(ng-show="ui.view == 'history'")
 
 					div.description(ng-click="select()")
 						div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
-						div.docs(ng-repeat="(doc_id, doc) in update.docs")
-							span.doc {{ doc.entity.name }}
+						div.docs(ng-repeat="(doc_path, doc) in update.docs", ng-if="update.docs")
+							span.doc {{ doc_path }}
+						div.docs(ng-repeat="rename in update.renames", ng-if="update.renames")
+							span Renamed {{ rename.pathname }} to {{ rename.newPathname }}
 						div.users
 							div.user(ng-repeat="update_user in update.meta.users")
 								.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
diff --git a/services/web/public/coffee/ide/history/HistoryManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee
index 6b42714e79..a892077b90 100644
--- a/services/web/public/coffee/ide/history/HistoryManager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryManager.coffee
@@ -22,7 +22,8 @@ define [
 
 			@$scope.$on "entity:selected", (event, entity) =>
 				if (@$scope.ui.view == "history") and (entity.type == "doc")
-					@$scope.history.selection.doc = entity
+					# TODO: Set selection.doc_path to entity path name
+					# @$scope.history.selection.doc = entity
 					@reloadDiff()
 
 		show: () ->
@@ -83,30 +84,36 @@ define [
 
 		reloadDiff: () ->
 			diff = @$scope.history.diff
-			{updates, doc} = @$scope.history.selection
-			{fromV, toV, start_ts, end_ts}   = @_calculateRangeFromSelection()
+			{updates} = @$scope.history.selection
+			{fromV, toV, start_ts, end_ts, original_path} = @_calculateDiffDataFromSelection()
+			console.log "[reloadDiff] current diff", diff
+			console.log "[reloadDiff] new diff data", {fromV, toV, start_ts, end_ts, original_path}
 
-			return if !doc?
+			return if !original_path?
 
 			return if diff? and
-				diff.doc   == doc   and
-				diff.fromV == fromV and
-				diff.toV   == toV
+				diff.doc_path == original_path and
+				diff.fromV    == fromV and
+				diff.toV      == toV
 
 			@$scope.history.diff = diff = {
 				fromV:    fromV
 				toV:      toV
 				start_ts: start_ts
 				end_ts:   end_ts
-				doc:      doc
+				doc_path: original_path
 				error:    false
 			}
 
-			if !doc.deleted
+			# TODO: How do we track deleted files now? We can probably show the diffs easily
+			# with the new system!
+			if true # !doc.deleted
 				diff.loading = true
-				url = "/project/#{@$scope.project_id}/doc/#{diff.doc.id}/diff"
+				url = "/project/#{@$scope.project_id}/doc/by_path/diff"
+				query = ["path=#{encodeURIComponent(original_path)}"]
 				if diff.fromV? and diff.toV?
-					url += "?from=#{diff.fromV}&to=#{diff.toV}"
+					query.push "from=#{diff.fromV}", "to=#{diff.toV}"
+				url += "?" + query.join("&")
 
 				@ide.$http
 					.get(url)
@@ -190,8 +197,9 @@ define [
 			previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
 
 			for update in updates
-				for doc_id, doc of update.docs or {}
-					doc.entity = @ide.fileTreeManager.findEntityById(doc_id, includeDeleted: true)
+				for doc_path, doc of update.docs or {}
+					doc.path = doc_path
+					doc.entity = @ide.fileTreeManager.findEntityByPath(doc_path, includeDeleted: true)
 
 				for user in update.meta.users or []
 					if user?
@@ -210,50 +218,89 @@ define [
 
 			@$scope.history.updates =
 				@$scope.history.updates.concat(updates)
+			console.log "[_loadUpdates] updates", @$scope.history.updates
 
 			@autoSelectRecentUpdates() if firstLoad
 
-		_calculateRangeFromSelection: () ->
-			fromV = toV = start_ts = end_ts = null
+		_perDocSummaryOfUpdates: (updates) ->
+			current_paths = {}
+			docs_summary = {}
 
-			selected_doc_id = @$scope.history.selection.doc?.id
-
-			for update in @$scope.history.selection.updates or []
-				for doc_id, doc of update.docs
-					if doc_id == selected_doc_id
-						if fromV? and toV?
-							fromV = Math.min(fromV, doc.fromV)
-							toV = Math.max(toV, doc.toV)
-							start_ts = Math.min(start_ts, update.meta.start_ts)
-							end_ts = Math.max(end_ts, update.meta.end_ts)
+			for update in updates # Updates are reverse chronologically ordered
+				console.log "[_perDocSummaryOfUpdates] update", update
+				if update.docs?
+					for doc_path, doc of update.docs
+						# doc_path may not be the latest doc path that this doc has had
+						if !current_paths[doc_path]?
+							current_paths[doc_path] = doc_path
+						current_path = current_paths[doc_path]
+						console.log "[_perDocSummaryOfUpdates] doc", doc, current_path
+						if !docs_summary[current_path]?
+							# todo start_ts and end_ts
+							docs_summary[current_path] = {
+								fromV: doc.fromV, toV: doc.toV,
+								original_path: doc_path
+							}
 						else
-							fromV = doc.fromV
-							toV = doc.toV
-							start_ts = update.meta.start_ts
-							end_ts = update.meta.end_ts
-						break
+							docs_summary[current_path] = {
+								fromV: Math.min(docs_summary[current_path].fromV, doc.fromV),
+								toV: Math.max(docs_summary[current_path].toV, doc.toV),
+								original_path: doc_path
+							}
+				else if update.renames?
+					for rename in update.renames
+						console.log "[_perDocSummaryOfUpdates] rename", rename
+						if !current_paths[rename.newPathname]?
+							current_paths[rename.newPathname] = rename.newPathname
+						current_paths[rename.pathname] = current_paths[rename.newPathname]
+						delete current_paths[rename.newPathname]
 
-			return {fromV, toV, start_ts, end_ts}
+				console.log "[_perDocSummaryOfUpdates] docs_summary", docs_summary
+				console.log "[_perDocSummaryOfUpdates] current_paths", current_paths
+
+			return docs_summary
+
+		_calculateDiffDataFromSelection: () ->
+			fromV = toV = start_ts = end_ts = original_path = null
+
+			selected_doc_path = @$scope.history.selection.doc_path
+			console.log "[_calculateDiffDataFromSelection] selected_doc_path", selected_doc_path
+
+			for doc_path, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
+				if doc_path == selected_doc_path
+					fromV = doc.fromV
+					toV = doc.toV
+					start_ts = doc.start_ts
+					end_ts = doc.end_ts
+					original_path = doc.original_path
+					break
+
+			return {fromV, toV, start_ts, end_ts, original_path}
 
 		# Set the track changes selected doc to one of the docs in the range
 		# of currently selected updates. If we already have a selected doc
 		# then prefer this one if present.
 		_selectDocFromUpdates: () ->
-			affected_docs = {}
-			for update in @$scope.history.selection.updates
-				for doc_id, doc of update.docs
-					affected_docs[doc_id] = doc.entity
+			affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
+			console.log "[_selectDocFromUpdates] affected_docs", affected_docs
 
-			selected_doc = @$scope.history.selection.doc
-			if selected_doc? and affected_docs[selected_doc.id]?
+			selected_doc_path = @$scope.history.selection.doc_path
+			console.log "[_selectDocFromUpdates] current selected_doc_path", selected_doc_path
+			if selected_doc_path? and affected_docs[selected_doc_path]
 				# Selected doc is already open
 			else
-				for doc_id, doc of affected_docs
-					selected_doc = doc
+				# Set to first possible candidate
+				for doc_path, doc of affected_docs
+					selected_doc_path = doc_path
 					break
 
-			@$scope.history.selection.doc = selected_doc
-			@ide.fileTreeManager.selectEntity(selected_doc)
+			console.log "[_selectDocFromUpdates] new selected_doc_path", selected_doc_path
+
+			@$scope.history.selection.doc_path = selected_doc_path
+			if selected_doc_path?
+				entity = @ide.fileTreeManager.findEntityByPath(selected_doc_path)
+				if entity?
+					@ide.fileTreeManager.selectEntity(entity)
 
 		_updateContainsUserId: (update, user_id) ->
 			for user in update.meta.users

From 4691a6e85c8b43c6ac1068d75f6aeda3df4fb51a Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Thu, 7 Dec 2017 10:13:46 +0000
Subject: [PATCH 092/109] Get diffs showing in client

---
 services/web/app/coffee/router.coffee         |   1 +
 .../web/app/views/project/editor/history.pug  |   9 +-
 .../coffee/ide/history/HistoryManager.coffee  | 121 +++++++++---------
 3 files changed, 64 insertions(+), 67 deletions(-)

diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index c89d9cdf5e..2199bdd08e 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -197,6 +197,7 @@ module.exports = class Router
 
 		webRouter.get  "/project/:Project_id/updates", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
 		webRouter.get  "/project/:Project_id/doc/:doc_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
+		webRouter.get  "/project/:Project_id/diff", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
 		webRouter.post "/project/:Project_id/doc/:doc_id/version/:version_id/restore", AuthorizationMiddlewear.ensureUserCanReadProject, HistoryController.selectHistoryApi, HistoryController.proxyToHistoryApi
 
 		webRouter.get  '/Project/:Project_id/download/zip', AuthorizationMiddlewear.ensureUserCanReadProject, ProjectDownloadsController.downloadProject
diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug
index f62dcebf15..102d3e05f8 100644
--- a/services/web/app/views/project/editor/history.pug
+++ b/services/web/app/views/project/editor/history.pug
@@ -134,10 +134,11 @@ div#history(ng-show="ui.view == 'history'")
 
 					div.description(ng-click="select()")
 						div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
-						div.docs(ng-repeat="(doc_path, doc) in update.docs", ng-if="update.docs")
-							span.doc {{ doc_path }}
-						div.docs(ng-repeat="rename in update.renames", ng-if="update.renames")
-							span Renamed {{ rename.pathname }} to {{ rename.newPathname }}
+						div.docs(ng-repeat="pathname in update.docs", ng-if="update.docs")
+							span.doc {{ pathname }}
+						div.docs(ng-repeat="project_op in update.project_ops", ng-if="update.project_ops")
+							span(ng-if="project_op.rename")
+								| Renamed {{ project_op.rename.pathname }} to {{ project_op.rename.newPathname }}
 						div.users
 							div.user(ng-repeat="update_user in update.meta.users")
 								.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
diff --git a/services/web/public/coffee/ide/history/HistoryManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee
index a892077b90..80a39a3614 100644
--- a/services/web/public/coffee/ide/history/HistoryManager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryManager.coffee
@@ -22,7 +22,7 @@ define [
 
 			@$scope.$on "entity:selected", (event, entity) =>
 				if (@$scope.ui.view == "history") and (entity.type == "doc")
-					# TODO: Set selection.doc_path to entity path name
+					# TODO: Set selection.pathname to entity path name
 					# @$scope.history.selection.doc = entity
 					@reloadDiff()
 
@@ -46,8 +46,6 @@ define [
 					range: {
 						fromV: null
 						toV: null
-						start_ts: null
-						end_ts: null
 					}
 				}
 				diff: null
@@ -76,6 +74,7 @@ define [
 				.get(url)
 				.then (response) =>
 					{ data } = response
+					console.log "fetchNextBatchOfUpdates", data.updates
 					@_loadUpdates(data.updates)
 					@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
 					if !data.nextBeforeTimestamp?
@@ -85,23 +84,21 @@ define [
 		reloadDiff: () ->
 			diff = @$scope.history.diff
 			{updates} = @$scope.history.selection
-			{fromV, toV, start_ts, end_ts, original_path} = @_calculateDiffDataFromSelection()
+			{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
 			console.log "[reloadDiff] current diff", diff
-			console.log "[reloadDiff] new diff data", {fromV, toV, start_ts, end_ts, original_path}
+			console.log "[reloadDiff] new diff data", {fromV, toV, pathname}
 
-			return if !original_path?
+			return if !pathname?
 
 			return if diff? and
-				diff.doc_path == original_path and
+				diff.pathname == pathname and
 				diff.fromV    == fromV and
 				diff.toV      == toV
 
 			@$scope.history.diff = diff = {
 				fromV:    fromV
 				toV:      toV
-				start_ts: start_ts
-				end_ts:   end_ts
-				doc_path: original_path
+				pathname: pathname
 				error:    false
 			}
 
@@ -109,8 +106,8 @@ define [
 			# with the new system!
 			if true # !doc.deleted
 				diff.loading = true
-				url = "/project/#{@$scope.project_id}/doc/by_path/diff"
-				query = ["path=#{encodeURIComponent(original_path)}"]
+				url = "/project/#{@$scope.project_id}/diff"
+				query = ["pathname=#{encodeURIComponent(pathname)}"]
 				if diff.fromV? and diff.toV?
 					query.push "from=#{diff.fromV}", "to=#{diff.toV}"
 				url += "?" + query.join("&")
@@ -194,12 +191,12 @@ define [
 			return {text, highlights}
 
 		_loadUpdates: (updates = []) ->
+			console.log "FOO"
 			previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
+			console.log "BAR", updates
 
-			for update in updates
-				for doc_path, doc of update.docs or {}
-					doc.path = doc_path
-					doc.entity = @ide.fileTreeManager.findEntityByPath(doc_path, includeDeleted: true)
+			for update in updates or []
+				console.log "_loadUpdates, loading", update
 
 				for user in update.meta.users or []
 					if user?
@@ -214,6 +211,7 @@ define [
 
 				previousUpdate = update
 
+			console.log("BAZ")
 			firstLoad = @$scope.history.updates.length == 0
 
 			@$scope.history.updates =
@@ -223,59 +221,56 @@ define [
 			@autoSelectRecentUpdates() if firstLoad
 
 		_perDocSummaryOfUpdates: (updates) ->
-			current_paths = {}
+			current_pathnames = {}
 			docs_summary = {}
 
 			for update in updates # Updates are reverse chronologically ordered
 				console.log "[_perDocSummaryOfUpdates] update", update
-				if update.docs?
-					for doc_path, doc of update.docs
-						# doc_path may not be the latest doc path that this doc has had
-						if !current_paths[doc_path]?
-							current_paths[doc_path] = doc_path
-						current_path = current_paths[doc_path]
-						console.log "[_perDocSummaryOfUpdates] doc", doc, current_path
-						if !docs_summary[current_path]?
-							# todo start_ts and end_ts
-							docs_summary[current_path] = {
-								fromV: doc.fromV, toV: doc.toV,
-								original_path: doc_path
-							}
-						else
-							docs_summary[current_path] = {
-								fromV: Math.min(docs_summary[current_path].fromV, doc.fromV),
-								toV: Math.max(docs_summary[current_path].toV, doc.toV),
-								original_path: doc_path
-							}
-				else if update.renames?
-					for rename in update.renames
+				for pathname in update.docs or []
+					# current_pathname may not be the latest doc path that this doc has had
+					if !current_pathnames[pathname]?
+						current_pathnames[pathname] = pathname
+					current_pathname = current_pathnames[pathname]
+					if !docs_summary[current_pathname]?
+						docs_summary[current_pathname] = {
+							fromV: update.fromV, toV: update.toV,
+							pathname: pathname
+						}
+						console.log "[_perDocSummaryOfUpdates] creating summary", current_pathname, docs_summary[current_pathname]
+					else
+						console.log "[_perDocSummaryOfUpdates] updating summary", docs_summary[current_pathname], update
+						docs_summary[current_pathname] = {
+							fromV: Math.min(docs_summary[current_pathname].fromV, update.fromV),
+							toV: Math.max(docs_summary[current_pathname].toV, update.toV),
+							pathname: pathname
+						}
+				for project_op in update.project_ops or []
+					if project_op.rename?
+						rename = project_op.rename
 						console.log "[_perDocSummaryOfUpdates] rename", rename
-						if !current_paths[rename.newPathname]?
-							current_paths[rename.newPathname] = rename.newPathname
-						current_paths[rename.pathname] = current_paths[rename.newPathname]
-						delete current_paths[rename.newPathname]
+						if !current_pathnames[rename.newPathname]?
+							current_pathnames[rename.newPathname] = rename.newPathname
+						current_pathnames[rename.current_pathname] = current_pathnames[rename.newPathname]
+						delete current_pathnames[rename.newPathname]
 
 				console.log "[_perDocSummaryOfUpdates] docs_summary", docs_summary
-				console.log "[_perDocSummaryOfUpdates] current_paths", current_paths
+				console.log "[_perDocSummaryOfUpdates] current_pathnames", current_pathnames
 
 			return docs_summary
 
 		_calculateDiffDataFromSelection: () ->
-			fromV = toV = start_ts = end_ts = original_path = null
+			fromV = toV = pathname = null
 
-			selected_doc_path = @$scope.history.selection.doc_path
-			console.log "[_calculateDiffDataFromSelection] selected_doc_path", selected_doc_path
+			selected_pathname = @$scope.history.selection.pathname
+			console.log "[_calculateDiffDataFromSelection] selected_pathname", selected_pathname
 
-			for doc_path, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
-				if doc_path == selected_doc_path
-					fromV = doc.fromV
-					toV = doc.toV
-					start_ts = doc.start_ts
-					end_ts = doc.end_ts
-					original_path = doc.original_path
+			for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
+				console.log "[_calculateDiffDataFromSelection] pathname, doc", pathname, doc
+				if pathname == selected_pathname
+					{fromV, toV, pathname} = doc
 					break
 
-			return {fromV, toV, start_ts, end_ts, original_path}
+			return {fromV, toV, pathname}
 
 		# Set the track changes selected doc to one of the docs in the range
 		# of currently selected updates. If we already have a selected doc
@@ -284,21 +279,21 @@ define [
 			affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
 			console.log "[_selectDocFromUpdates] affected_docs", affected_docs
 
-			selected_doc_path = @$scope.history.selection.doc_path
-			console.log "[_selectDocFromUpdates] current selected_doc_path", selected_doc_path
-			if selected_doc_path? and affected_docs[selected_doc_path]
+			selected_pathname = @$scope.history.selection.pathname
+			console.log "[_selectDocFromUpdates] current selected_pathname", selected_pathname
+			if selected_pathname? and affected_docs[selected_pathname]
 				# Selected doc is already open
 			else
 				# Set to first possible candidate
-				for doc_path, doc of affected_docs
-					selected_doc_path = doc_path
+				for pathname, doc of affected_docs
+					selected_pathname = pathname
 					break
 
-			console.log "[_selectDocFromUpdates] new selected_doc_path", selected_doc_path
+			console.log "[_selectDocFromUpdates] new selected_pathname", selected_pathname
 
-			@$scope.history.selection.doc_path = selected_doc_path
-			if selected_doc_path?
-				entity = @ide.fileTreeManager.findEntityByPath(selected_doc_path)
+			@$scope.history.selection.pathname = selected_pathname
+			if selected_pathname?
+				entity = @ide.fileTreeManager.findEntityByPath(selected_pathname)
 				if entity?
 					@ide.fileTreeManager.selectEntity(entity)
 

From 8ea779af58ebbbc3c6a16ac98e7f158e46fd2b6d Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Thu, 7 Dec 2017 11:21:49 +0000
Subject: [PATCH 093/109] Add some front end tests for HistoryManager

---
 services/web/.gitignore                       |  1 +
 .../web/bin/{compile_app => compile_backend}  |  0
 services/web/bin/compile_frontend             |  5 ++
 services/web/bin/compile_frontend_unit_tests  |  5 ++
 services/web/bin/frontend_unit_test           |  5 ++
 services/web/package.json                     | 10 ++-
 .../coffee/ide/history/HistoryManager.coffee  | 55 ++++--------
 .../coffee/HistoryManagerTests.coffee         | 89 +++++++++++++++++++
 8 files changed, 130 insertions(+), 40 deletions(-)
 rename services/web/bin/{compile_app => compile_backend} (100%)
 create mode 100755 services/web/bin/compile_frontend
 create mode 100755 services/web/bin/compile_frontend_unit_tests
 create mode 100755 services/web/bin/frontend_unit_test
 create mode 100644 services/web/test/unit_frontend/coffee/HistoryManagerTests.coffee

diff --git a/services/web/.gitignore b/services/web/.gitignore
index 83d4d28b65..d725b0b837 100644
--- a/services/web/.gitignore
+++ b/services/web/.gitignore
@@ -39,6 +39,7 @@ data/*
 app.js
 app/js/*
 test/unit/js/*
+test/unit_frontend/js/*
 test/smoke/js/*
 test/acceptance/js/*
 cookies.txt
diff --git a/services/web/bin/compile_app b/services/web/bin/compile_backend
similarity index 100%
rename from services/web/bin/compile_app
rename to services/web/bin/compile_backend
diff --git a/services/web/bin/compile_frontend b/services/web/bin/compile_frontend
new file mode 100755
index 0000000000..bb1dde7dbb
--- /dev/null
+++ b/services/web/bin/compile_frontend
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e;
+COFFEE=node_modules/.bin/coffee
+echo Compiling public/coffee;
+$COFFEE -o public/js -c public/coffee;
diff --git a/services/web/bin/compile_frontend_unit_tests b/services/web/bin/compile_frontend_unit_tests
new file mode 100755
index 0000000000..0351ad70cd
--- /dev/null
+++ b/services/web/bin/compile_frontend_unit_tests
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e;
+COFFEE=node_modules/.bin/coffee
+echo Compiling test/unit_frontend/coffee;
+$COFFEE -o test/unit_frontend/js -c test/unit_frontend/coffee;
diff --git a/services/web/bin/frontend_unit_test b/services/web/bin/frontend_unit_test
new file mode 100755
index 0000000000..599055803a
--- /dev/null
+++ b/services/web/bin/frontend_unit_test
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e;
+MOCHA="node_modules/.bin/mocha --recursive --reporter spec"
+$MOCHA "$@" test/unit_frontend/js
+
diff --git a/services/web/package.json b/services/web/package.json
index a1cf0fd6e0..b5e843a371 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -14,11 +14,15 @@
     "test:acceptance:run": "bin/acceptance_test $@",
     "test:acceptance:dir": "npm -q run compile:acceptance_tests && npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@",
     "test:acceptance": "npm -q run test:acceptance:dir -- $@ test/acceptance/js",
-    "test:unit": "npm -q run compile:app && npm -q run compile:unit_tests && bin/unit_test $@",
+    "test:unit": "npm -q run compile:backend && npm -q run compile:unit_tests && bin/unit_test $@",
+    "test:frontend_unit": "npm -q run compile:frontend && npm -q run compile:frontend_unit_tests && bin/frontend_unit_test $@",
     "compile:unit_tests": "bin/compile_unit_tests",
+    "compile:frontend_unit_tests": "bin/compile_frontend_unit_tests",
     "compile:acceptance_tests": "bin/compile_acceptance_tests",
-    "compile:app": "bin/compile_app",
-    "start": "npm -q run compile:app && node app.js"
+    "compile:frontend": "bin/compile_frontend",
+    "compile:backend": "bin/compile_backend",
+    "compile": "npm -q run compile:backend && npm -q run compile:frontend",
+    "start": "npm -q run compile && node app.js"
   },
   "dependencies": {
     "archiver": "0.9.0",
diff --git a/services/web/public/coffee/ide/history/HistoryManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee
index 80a39a3614..5528c1153d 100644
--- a/services/web/public/coffee/ide/history/HistoryManager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryManager.coffee
@@ -191,13 +191,9 @@ define [
 			return {text, highlights}
 
 		_loadUpdates: (updates = []) ->
-			console.log "FOO"
 			previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
-			console.log "BAR", updates
 
 			for update in updates or []
-				console.log "_loadUpdates, loading", update
-
 				for user in update.meta.users or []
 					if user?
 						user.hue = ColorManager.getHueForUserId(user.id)
@@ -211,50 +207,41 @@ define [
 
 				previousUpdate = update
 
-			console.log("BAZ")
 			firstLoad = @$scope.history.updates.length == 0
 
 			@$scope.history.updates =
 				@$scope.history.updates.concat(updates)
-			console.log "[_loadUpdates] updates", @$scope.history.updates
 
 			@autoSelectRecentUpdates() if firstLoad
 
 		_perDocSummaryOfUpdates: (updates) ->
-			current_pathnames = {}
+			# Track current_pathname -> original_pathname
+			original_pathnames = {}
 			docs_summary = {}
 
-			for update in updates # Updates are reverse chronologically ordered
-				console.log "[_perDocSummaryOfUpdates] update", update
+			# Put updates in ascending chronological order
+			updates = updates.slice().reverse()
+			for update in updates
 				for pathname in update.docs or []
-					# current_pathname may not be the latest doc path that this doc has had
-					if !current_pathnames[pathname]?
-						current_pathnames[pathname] = pathname
-					current_pathname = current_pathnames[pathname]
-					if !docs_summary[current_pathname]?
-						docs_summary[current_pathname] = {
+					if !original_pathnames[pathname]?
+						original_pathnames[pathname] = pathname
+					original_pathname = original_pathnames[pathname]
+					if !docs_summary[original_pathname]?
+						docs_summary[original_pathname] = {
 							fromV: update.fromV, toV: update.toV,
-							pathname: pathname
 						}
-						console.log "[_perDocSummaryOfUpdates] creating summary", current_pathname, docs_summary[current_pathname]
 					else
-						console.log "[_perDocSummaryOfUpdates] updating summary", docs_summary[current_pathname], update
-						docs_summary[current_pathname] = {
-							fromV: Math.min(docs_summary[current_pathname].fromV, update.fromV),
-							toV: Math.max(docs_summary[current_pathname].toV, update.toV),
-							pathname: pathname
+						docs_summary[original_pathname] = {
+							fromV: Math.min(docs_summary[original_pathname].fromV, update.fromV),
+							toV: Math.max(docs_summary[original_pathname].toV, update.toV),
 						}
 				for project_op in update.project_ops or []
 					if project_op.rename?
 						rename = project_op.rename
-						console.log "[_perDocSummaryOfUpdates] rename", rename
-						if !current_pathnames[rename.newPathname]?
-							current_pathnames[rename.newPathname] = rename.newPathname
-						current_pathnames[rename.current_pathname] = current_pathnames[rename.newPathname]
-						delete current_pathnames[rename.newPathname]
-
-				console.log "[_perDocSummaryOfUpdates] docs_summary", docs_summary
-				console.log "[_perDocSummaryOfUpdates] current_pathnames", current_pathnames
+						if !original_pathnames[rename.pathname]?
+							original_pathnames[rename.pathname] = rename.pathname
+						original_pathnames[rename.newPathname] = original_pathnames[rename.pathname]
+						delete original_pathnames[rename.pathname]
 
 			return docs_summary
 
@@ -262,12 +249,10 @@ define [
 			fromV = toV = pathname = null
 
 			selected_pathname = @$scope.history.selection.pathname
-			console.log "[_calculateDiffDataFromSelection] selected_pathname", selected_pathname
 
 			for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
-				console.log "[_calculateDiffDataFromSelection] pathname, doc", pathname, doc
 				if pathname == selected_pathname
-					{fromV, toV, pathname} = doc
+					{fromV, toV} = doc
 					break
 
 			return {fromV, toV, pathname}
@@ -277,10 +262,8 @@ define [
 		# then prefer this one if present.
 		_selectDocFromUpdates: () ->
 			affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
-			console.log "[_selectDocFromUpdates] affected_docs", affected_docs
 
 			selected_pathname = @$scope.history.selection.pathname
-			console.log "[_selectDocFromUpdates] current selected_pathname", selected_pathname
 			if selected_pathname? and affected_docs[selected_pathname]
 				# Selected doc is already open
 			else
@@ -289,8 +272,6 @@ define [
 					selected_pathname = pathname
 					break
 
-			console.log "[_selectDocFromUpdates] new selected_pathname", selected_pathname
-
 			@$scope.history.selection.pathname = selected_pathname
 			if selected_pathname?
 				entity = @ide.fileTreeManager.findEntityByPath(selected_pathname)
diff --git a/services/web/test/unit_frontend/coffee/HistoryManagerTests.coffee b/services/web/test/unit_frontend/coffee/HistoryManagerTests.coffee
new file mode 100644
index 0000000000..8806cad66c
--- /dev/null
+++ b/services/web/test/unit_frontend/coffee/HistoryManagerTests.coffee
@@ -0,0 +1,89 @@
+Path = require 'path'
+SandboxedModule = require "sandboxed-module"
+modulePath = Path.join __dirname, '../../../public/js/ide/history/HistoryManager'
+sinon = require("sinon")
+expect = require("chai").expect
+
+describe "HistoryManager", ->
+	beforeEach ->
+		@moment = {}
+		@ColorManager = {}
+		SandboxedModule.require modulePath, globals:
+			"define": (dependencies, builder) =>
+				@HistoryManager = builder(@moment, @ColorManager)
+
+		@scope =
+			$watch: sinon.stub()
+			$on: sinon.stub()
+		@ide = {}
+
+		@historyManager = new @HistoryManager(@ide, @scope)
+
+	it "should setup the history scope on intialization", ->
+		expect(@scope.history).to.deep.equal({
+			updates: []
+			nextBeforeTimestamp: null
+			atEnd: false
+			selection: {
+				updates: []
+				doc: null
+				range: {
+					fromV: null
+					toV: null
+				}
+			}
+			diff: null
+		})
+
+	describe "_perDocSummaryOfUpdates", ->
+		it "should return the range of updates for the docs", ->
+			result = @historyManager._perDocSummaryOfUpdates([{
+				docs: ["main.tex"]
+				fromV: 7, toV: 9
+			},{
+				docs: ["main.tex", "foo.tex"]
+				fromV: 4, toV: 6
+			},{
+				docs: ["main.tex"]
+				fromV: 3, toV: 3
+			},{
+				docs: ["foo.tex"]
+				fromV: 0, toV: 2
+			}])
+
+			expect(result).to.deep.equal({
+				"main.tex": { fromV: 3, toV: 9 },
+				"foo.tex": { fromV: 0, toV: 6 }
+			})
+
+		it "should track renames", ->
+			result = @historyManager._perDocSummaryOfUpdates([{
+				docs: ["main2.tex"]
+				fromV: 5, toV: 9
+			},{
+				project_ops: [{
+					rename: {
+						pathname: "main1.tex",
+						newPathname: "main2.tex"
+					}
+				}],
+				fromV: 4, toV: 4
+			},{
+				docs: ["main1.tex"]
+				fromV: 3, toV: 3
+			},{
+				project_ops: [{
+					rename: {
+						pathname: "main0.tex",
+						newPathname: "main1.tex"
+					}
+				}],
+				fromV: 2, toV: 2
+			},{
+				docs: ["main0.tex"]
+				fromV: 0, toV: 1
+			}])
+
+			expect(result).to.deep.equal({
+				"main0.tex": { fromV: 0, toV: 9 }
+			})

From 50b12e88a25f856fa281a47a9176dff167530c69 Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Wed, 13 Dec 2017 16:11:12 +0000
Subject: [PATCH 094/109] Add HistoryV2Manager alongside existing
 HistoryManager

---
 .../Features/Project/ProjectController.coffee |   3 +-
 services/web/app/views/project/editor.pug     |   2 +-
 .../web/app/views/project/editor/history.pug  |   6 +-
 services/web/public/coffee/ide.coffee         |   7 +-
 .../coffee/ide/history/HistoryManager.coffee  | 119 +++----
 .../ide/history/HistoryV2Manager.coffee       | 298 ++++++++++++++++++
 ...ts.coffee => HistoryManagerV2Tests.coffee} |  66 +++-
 7 files changed, 415 insertions(+), 86 deletions(-)
 create mode 100644 services/web/public/coffee/ide/history/HistoryV2Manager.coffee
 rename services/web/test/unit_frontend/coffee/{HistoryManagerTests.coffee => HistoryManagerV2Tests.coffee} (55%)

diff --git a/services/web/app/coffee/Features/Project/ProjectController.coffee b/services/web/app/coffee/Features/Project/ProjectController.coffee
index da22373870..fb27611bca 100644
--- a/services/web/app/coffee/Features/Project/ProjectController.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectController.coffee
@@ -216,7 +216,7 @@ module.exports = ProjectController =
 			project: (cb)->
 				ProjectGetter.getProject(
 					project_id,
-					{ name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1 },
+					{ name: 1, lastUpdated: 1, track_changes: 1, owner_ref: 1, 'overleaf.history.display': 1 },
 					cb
 				)
 			user: (cb)->
@@ -351,6 +351,7 @@ module.exports = ProjectController =
 					themes: THEME_LIST
 					maxDocLength: Settings.max_doc_length
 					showLinkSharingOnboarding: !!results.couldShowLinkSharingOnboarding
+					useV2History: !!project.overleaf?.history?.display
 				timer.done()
 
 	_buildProjectList: (allProjects, v1Projects = [])->
diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug
index d1d0d94cf3..4a35b930ff 100644
--- a/services/web/app/views/project/editor.pug
+++ b/services/web/app/views/project/editor.pug
@@ -105,7 +105,7 @@ block requirejs
 	//- We need to do .replace(/\//g, '\\/') do that '' -> '<\/script>'
 	//- and doesn't prematurely end the script tag.
 	script#data(type="application/json").
-		!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState}).replace(/\//g, '\\/')}
+		!{JSON.stringify({userSettings: userSettings, user: user, trackChangesState: trackChangesState, useV2History: useV2History}).replace(/\//g, '\\/')}
 
 	script(type="text/javascript").
 		window.data = JSON.parse($("#data").text());
diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug
index 102d3e05f8..bdc834352f 100644
--- a/services/web/app/views/project/editor/history.pug
+++ b/services/web/app/views/project/editor/history.pug
@@ -134,11 +134,13 @@ div#history(ng-show="ui.view == 'history'")
 
 					div.description(ng-click="select()")
 						div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
-						div.docs(ng-repeat="pathname in update.docs", ng-if="update.docs")
+						div.docs(ng-repeat="pathname in update.pathnames")
 							span.doc {{ pathname }}
-						div.docs(ng-repeat="project_op in update.project_ops", ng-if="update.project_ops")
+						div.docs(ng-repeat="project_op in update.project_ops")
 							span(ng-if="project_op.rename")
 								| Renamed {{ project_op.rename.pathname }} to {{ project_op.rename.newPathname }}
+							span(ng-if="project_op.add")
+								| Added {{ project_op.add.pathname }}
 						div.users
 							div.user(ng-repeat="update_user in update.meta.users")
 								.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index dbf6a2724e..3d514d093c 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -5,6 +5,7 @@ define [
 	"ide/editor/EditorManager"
 	"ide/online-users/OnlineUsersManager"
 	"ide/history/HistoryManager"
+	"ide/history/HistoryV2Manager"
 	"ide/permissions/PermissionsManager"
 	"ide/pdf/PdfManager"
 	"ide/binary-files/BinaryFilesManager"
@@ -44,6 +45,7 @@ define [
 	EditorManager
 	OnlineUsersManager
 	HistoryManager
+	HistoryV2Manager
 	PermissionsManager
 	PdfManager
 	BinaryFilesManager
@@ -137,7 +139,10 @@ define [
 		ide.fileTreeManager = new FileTreeManager(ide, $scope)
 		ide.editorManager = new EditorManager(ide, $scope)
 		ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
-		ide.historyManager = new HistoryManager(ide, $scope)
+		if window.data.useV2History
+			ide.historyManager = new HistoryV2Manager(ide, $scope)
+		else
+			ide.historyManager = new HistoryManager(ide, $scope)
 		ide.pdfManager = new PdfManager(ide, $scope)
 		ide.permissionsManager = new PermissionsManager(ide, $scope)
 		ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
diff --git a/services/web/public/coffee/ide/history/HistoryManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee
index 5528c1153d..b2ecc8c8ef 100644
--- a/services/web/public/coffee/ide/history/HistoryManager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryManager.coffee
@@ -22,8 +22,7 @@ define [
 
 			@$scope.$on "entity:selected", (event, entity) =>
 				if (@$scope.ui.view == "history") and (entity.type == "doc")
-					# TODO: Set selection.pathname to entity path name
-					# @$scope.history.selection.doc = entity
+					@$scope.history.selection.doc = entity
 					@reloadDiff()
 
 		show: () ->
@@ -46,6 +45,8 @@ define [
 					range: {
 						fromV: null
 						toV: null
+						start_ts: null
+						end_ts: null
 					}
 				}
 				diff: null
@@ -74,7 +75,6 @@ define [
 				.get(url)
 				.then (response) =>
 					{ data } = response
-					console.log "fetchNextBatchOfUpdates", data.updates
 					@_loadUpdates(data.updates)
 					@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
 					if !data.nextBeforeTimestamp?
@@ -83,34 +83,30 @@ define [
 
 		reloadDiff: () ->
 			diff = @$scope.history.diff
-			{updates} = @$scope.history.selection
-			{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
-			console.log "[reloadDiff] current diff", diff
-			console.log "[reloadDiff] new diff data", {fromV, toV, pathname}
+			{updates, doc} = @$scope.history.selection
+			{fromV, toV, start_ts, end_ts}   = @_calculateRangeFromSelection()
 
-			return if !pathname?
+			return if !doc?
 
 			return if diff? and
-				diff.pathname == pathname and
-				diff.fromV    == fromV and
-				diff.toV      == toV
+				diff.doc   == doc   and
+				diff.fromV == fromV and
+				diff.toV   == toV
 
 			@$scope.history.diff = diff = {
 				fromV:    fromV
 				toV:      toV
-				pathname: pathname
+				start_ts: start_ts
+				end_ts:   end_ts
+				doc:      doc
 				error:    false
 			}
 
-			# TODO: How do we track deleted files now? We can probably show the diffs easily
-			# with the new system!
-			if true # !doc.deleted
+			if !doc.deleted
 				diff.loading = true
-				url = "/project/#{@$scope.project_id}/diff"
-				query = ["pathname=#{encodeURIComponent(pathname)}"]
+				url = "/project/#{@$scope.project_id}/doc/#{diff.doc.id}/diff"
 				if diff.fromV? and diff.toV?
-					query.push "from=#{diff.fromV}", "to=#{diff.toV}"
-				url += "?" + query.join("&")
+					url += "?from=#{diff.fromV}&to=#{diff.toV}"
 
 				@ide.$http
 					.get(url)
@@ -193,7 +189,12 @@ define [
 		_loadUpdates: (updates = []) ->
 			previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
 
-			for update in updates or []
+			for update in updates
+				update.pathnames = [] # Used for display
+				for doc_id, doc of update.docs or {}
+					doc.entity = @ide.fileTreeManager.findEntityById(doc_id, includeDeleted: true)
+					update.pathnames.push doc.entity.name
+
 				for user in update.meta.users or []
 					if user?
 						user.hue = ColorManager.getHueForUserId(user.id)
@@ -214,69 +215,47 @@ define [
 
 			@autoSelectRecentUpdates() if firstLoad
 
-		_perDocSummaryOfUpdates: (updates) ->
-			# Track current_pathname -> original_pathname
-			original_pathnames = {}
-			docs_summary = {}
+		_calculateRangeFromSelection: () ->
+			fromV = toV = start_ts = end_ts = null
 
-			# Put updates in ascending chronological order
-			updates = updates.slice().reverse()
-			for update in updates
-				for pathname in update.docs or []
-					if !original_pathnames[pathname]?
-						original_pathnames[pathname] = pathname
-					original_pathname = original_pathnames[pathname]
-					if !docs_summary[original_pathname]?
-						docs_summary[original_pathname] = {
-							fromV: update.fromV, toV: update.toV,
-						}
-					else
-						docs_summary[original_pathname] = {
-							fromV: Math.min(docs_summary[original_pathname].fromV, update.fromV),
-							toV: Math.max(docs_summary[original_pathname].toV, update.toV),
-						}
-				for project_op in update.project_ops or []
-					if project_op.rename?
-						rename = project_op.rename
-						if !original_pathnames[rename.pathname]?
-							original_pathnames[rename.pathname] = rename.pathname
-						original_pathnames[rename.newPathname] = original_pathnames[rename.pathname]
-						delete original_pathnames[rename.pathname]
+			selected_doc_id = @$scope.history.selection.doc?.id
 
-			return docs_summary
+			for update in @$scope.history.selection.updates or []
+				for doc_id, doc of update.docs
+					if doc_id == selected_doc_id
+						if fromV? and toV?
+							fromV = Math.min(fromV, doc.fromV)
+							toV = Math.max(toV, doc.toV)
+							start_ts = Math.min(start_ts, update.meta.start_ts)
+							end_ts = Math.max(end_ts, update.meta.end_ts)
+						else
+							fromV = doc.fromV
+							toV = doc.toV
+							start_ts = update.meta.start_ts
+							end_ts = update.meta.end_ts
+						break
 
-		_calculateDiffDataFromSelection: () ->
-			fromV = toV = pathname = null
-
-			selected_pathname = @$scope.history.selection.pathname
-
-			for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
-				if pathname == selected_pathname
-					{fromV, toV} = doc
-					break
-
-			return {fromV, toV, pathname}
+			return {fromV, toV, start_ts, end_ts}
 
 		# Set the track changes selected doc to one of the docs in the range
 		# of currently selected updates. If we already have a selected doc
 		# then prefer this one if present.
 		_selectDocFromUpdates: () ->
-			affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
+			affected_docs = {}
+			for update in @$scope.history.selection.updates
+				for doc_id, doc of update.docs
+					affected_docs[doc_id] = doc.entity
 
-			selected_pathname = @$scope.history.selection.pathname
-			if selected_pathname? and affected_docs[selected_pathname]
+			selected_doc = @$scope.history.selection.doc
+			if selected_doc? and affected_docs[selected_doc.id]?
 				# Selected doc is already open
 			else
-				# Set to first possible candidate
-				for pathname, doc of affected_docs
-					selected_pathname = pathname
+				for doc_id, doc of affected_docs
+					selected_doc = doc
 					break
 
-			@$scope.history.selection.pathname = selected_pathname
-			if selected_pathname?
-				entity = @ide.fileTreeManager.findEntityByPath(selected_pathname)
-				if entity?
-					@ide.fileTreeManager.selectEntity(entity)
+			@$scope.history.selection.doc = selected_doc
+			@ide.fileTreeManager.selectEntity(selected_doc)
 
 		_updateContainsUserId: (update, user_id) ->
 			for user in update.meta.users
diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
new file mode 100644
index 0000000000..d7e80d5153
--- /dev/null
+++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
@@ -0,0 +1,298 @@
+define [
+	"moment"
+	"ide/colors/ColorManager"
+	"ide/history/controllers/HistoryListController"
+	"ide/history/controllers/HistoryDiffController"
+	"ide/history/directives/infiniteScroll"
+], (moment, ColorManager) ->
+	class HistoryManager
+		constructor: (@ide, @$scope) ->
+			@reset()
+
+			@$scope.toggleHistory = () =>
+				if @$scope.ui.view == "history"
+					@hide()
+				else
+					@show()
+
+			@$scope.$watch "history.selection.updates", (updates) =>
+				if updates? and updates.length > 0
+					@_selectDocFromUpdates()
+					@reloadDiff()
+
+			@$scope.$on "entity:selected", (event, entity) =>
+				if (@$scope.ui.view == "history") and (entity.type == "doc")
+					# TODO: Set selection.pathname to entity path name
+					# @$scope.history.selection.doc = entity
+					@reloadDiff()
+
+		show: () ->
+			@$scope.ui.view = "history"
+			@reset()
+
+		hide: () ->
+			@$scope.ui.view = "editor"
+			# Make sure we run the 'open' logic for whatever is currently selected
+			@$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity()
+
+		reset: () ->
+			@$scope.history = {
+				updates: []
+				nextBeforeTimestamp: null
+				atEnd: false
+				selection: {
+					updates: []
+					doc: null
+					range: {
+						fromV: null
+						toV: null
+					}
+				}
+				diff: null
+			}
+
+		autoSelectRecentUpdates: () ->
+			return if @$scope.history.updates.length == 0
+
+			@$scope.history.updates[0].selectedTo = true
+
+			indexOfLastUpdateNotByMe = 0
+			for update, i in @$scope.history.updates
+				if @_updateContainsUserId(update, @$scope.user.id)
+					break
+				indexOfLastUpdateNotByMe = i
+
+			@$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true
+
+		BATCH_SIZE: 10
+		fetchNextBatchOfUpdates: () ->
+			url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
+			if @$scope.history.nextBeforeTimestamp?
+				url += "&before=#{@$scope.history.nextBeforeTimestamp}"
+			@$scope.history.loading = true
+			@ide.$http
+				.get(url)
+				.then (response) =>
+					{ data } = response
+					console.log "fetchNextBatchOfUpdates", data.updates
+					@_loadUpdates(data.updates)
+					@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
+					if !data.nextBeforeTimestamp?
+						@$scope.history.atEnd = true
+					@$scope.history.loading = false
+
+		reloadDiff: () ->
+			diff = @$scope.history.diff
+			{updates} = @$scope.history.selection
+			{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
+			console.log "[reloadDiff] current diff", diff
+			console.log "[reloadDiff] new diff data", {fromV, toV, pathname}
+
+			return if !pathname?
+
+			return if diff? and
+				diff.pathname == pathname and
+				diff.fromV    == fromV and
+				diff.toV      == toV
+
+			@$scope.history.diff = diff = {
+				fromV:    fromV
+				toV:      toV
+				pathname: pathname
+				error:    false
+			}
+
+			# TODO: How do we track deleted files now? We can probably show the diffs easily
+			# with the new system!
+			if true # !doc.deleted
+				diff.loading = true
+				url = "/project/#{@$scope.project_id}/diff"
+				query = ["pathname=#{encodeURIComponent(pathname)}"]
+				if diff.fromV? and diff.toV?
+					query.push "from=#{diff.fromV}", "to=#{diff.toV}"
+				url += "?" + query.join("&")
+
+				@ide.$http
+					.get(url)
+					.then (response) =>
+						{ data } = response
+						diff.loading = false
+						{text, highlights} = @_parseDiff(data)
+						diff.text = text
+						diff.highlights = highlights
+					.catch () ->
+						diff.loading = false
+						diff.error = true
+			else
+				diff.deleted = true
+				diff.restoreInProgress = false
+				diff.restoreDeletedSuccess = false
+				diff.restoredDocNewId = null
+
+		restoreDeletedDoc: (doc) ->
+			url = "/project/#{@$scope.project_id}/doc/#{doc.id}/restore"
+			@ide.$http.post(url, name: doc.name, _csrf: window.csrfToken)
+
+		restoreDiff: (diff) ->
+			url = "/project/#{@$scope.project_id}/doc/#{diff.doc.id}/version/#{diff.fromV}/restore"
+			@ide.$http.post(url, _csrf: window.csrfToken)
+
+		_parseDiff: (diff) ->
+			row    = 0
+			column = 0
+			highlights = []
+			text   = ""
+			for entry, i in diff.diff or []
+				content = entry.u or entry.i or entry.d
+				content ||= ""
+				text += content
+				lines   = content.split("\n")
+				startRow    = row
+				startColumn = column
+				if lines.length > 1
+					endRow    = startRow + lines.length - 1
+					endColumn = lines[lines.length - 1].length
+				else
+					endRow    = startRow
+					endColumn = startColumn + lines[0].length
+				row    = endRow
+				column = endColumn
+
+				range = {
+					start:
+						row: startRow
+						column: startColumn
+					end:
+						row: endRow
+						column: endColumn
+				}
+
+				if entry.i? or entry.d?
+					if entry.meta.user?
+						name = "#{entry.meta.user.first_name} #{entry.meta.user.last_name}"
+					else
+						name = "Anonymous"
+					if entry.meta.user?.id == @$scope.user.id
+						name = "you"
+					date = moment(entry.meta.end_ts).format("Do MMM YYYY, h:mm a")
+					if entry.i?
+						highlights.push {
+							label: "Added by #{name} on #{date}"
+							highlight: range
+							hue: ColorManager.getHueForUserId(entry.meta.user?.id)
+						}
+					else if entry.d?
+						highlights.push {
+							label: "Deleted by #{name} on #{date}"
+							strikeThrough: range
+							hue: ColorManager.getHueForUserId(entry.meta.user?.id)
+						}
+
+			return {text, highlights}
+
+		_loadUpdates: (updates = []) ->
+			previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1]
+
+			for update in updates or []
+				for user in update.meta.users or []
+					if user?
+						user.hue = ColorManager.getHueForUserId(user.id)
+
+				if !previousUpdate? or !moment(previousUpdate.meta.end_ts).isSame(update.meta.end_ts, "day")
+					update.meta.first_in_day = true
+
+				update.selectedFrom = false
+				update.selectedTo = false
+				update.inSelection = false
+
+				previousUpdate = update
+
+			firstLoad = @$scope.history.updates.length == 0
+
+			@$scope.history.updates =
+				@$scope.history.updates.concat(updates)
+
+			@autoSelectRecentUpdates() if firstLoad
+
+		_perDocSummaryOfUpdates: (updates) ->
+			# Track current_pathname -> original_pathname
+			original_pathnames = {}
+
+			# Map of original pathname -> doc summary
+			docs_summary = {}
+
+			updatePathnameWithUpdateVersions = (pathname, update) ->
+				# docs_summary is indexed by the original pathname the doc
+				# had at the start, so we have to look this up from the current
+				# pathname via original_pathname first
+				if !original_pathnames[pathname]?
+						original_pathnames[pathname] = pathname
+				original_pathname = original_pathnames[pathname]
+				doc_summary = docs_summary[original_pathname] ?= {
+					fromV: update.fromV, toV: update.toV,
+				}
+				doc_summary.fromV = Math.min(
+					doc_summary.fromV,
+					update.fromV
+				)
+				doc_summary.toV = Math.max(
+					doc_summary.toV,
+					update.toV
+				)
+
+			# Put updates in ascending chronological order
+			updates = updates.slice().reverse()
+			for update in updates
+				for pathname in update.pathnames or []
+					updatePathnameWithUpdateVersions(pathname, update)
+				for project_op in update.project_ops or []
+					if project_op.rename?
+						rename = project_op.rename
+						updatePathnameWithUpdateVersions(rename.pathname, update)
+						original_pathnames[rename.newPathname] = original_pathnames[rename.pathname]
+						delete original_pathnames[rename.pathname]
+					if project_op.add?
+						add = project_op.add
+						updatePathnameWithUpdateVersions(add.pathname, update)
+
+			return docs_summary
+
+		_calculateDiffDataFromSelection: () ->
+			fromV = toV = pathname = null
+
+			selected_pathname = @$scope.history.selection.pathname
+
+			console.log "[_calculateDiffDataFromSelection]", @$scope.history.selection.updates
+			for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
+				console.log "[_calculateDiffDataFromSelection] doc:", pathname, doc
+				if pathname == selected_pathname
+					{fromV, toV} = doc
+					break
+
+			return {fromV, toV, pathname}
+
+		# Set the track changes selected doc to one of the docs in the range
+		# of currently selected updates. If we already have a selected doc
+		# then prefer this one if present.
+		_selectDocFromUpdates: () ->
+			affected_docs = @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
+
+			selected_pathname = @$scope.history.selection.pathname
+			if selected_pathname? and affected_docs[selected_pathname]
+				# Selected doc is already open
+			else
+				# Set to first possible candidate
+				for pathname, doc of affected_docs
+					selected_pathname = pathname
+					break
+
+			@$scope.history.selection.pathname = selected_pathname
+			if selected_pathname?
+				entity = @ide.fileTreeManager.findEntityByPath(selected_pathname)
+				if entity?
+					@ide.fileTreeManager.selectEntity(entity)
+
+		_updateContainsUserId: (update, user_id) ->
+			for user in update.meta.users
+				return true if user?.id == user_id
+			return false
diff --git a/services/web/test/unit_frontend/coffee/HistoryManagerTests.coffee b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
similarity index 55%
rename from services/web/test/unit_frontend/coffee/HistoryManagerTests.coffee
rename to services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
index 8806cad66c..4517fc16dd 100644
--- a/services/web/test/unit_frontend/coffee/HistoryManagerTests.coffee
+++ b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
@@ -1,23 +1,23 @@
 Path = require 'path'
 SandboxedModule = require "sandboxed-module"
-modulePath = Path.join __dirname, '../../../public/js/ide/history/HistoryManager'
+modulePath = Path.join __dirname, '../../../public/js/ide/history/HistoryV2Manager'
 sinon = require("sinon")
 expect = require("chai").expect
 
-describe "HistoryManager", ->
+describe "HistoryV2Manager", ->
 	beforeEach ->
 		@moment = {}
 		@ColorManager = {}
 		SandboxedModule.require modulePath, globals:
 			"define": (dependencies, builder) =>
-				@HistoryManager = builder(@moment, @ColorManager)
+				@HistoryV2Manager = builder(@moment, @ColorManager)
 
 		@scope =
 			$watch: sinon.stub()
 			$on: sinon.stub()
 		@ide = {}
 
-		@historyManager = new @HistoryManager(@ide, @scope)
+		@historyManager = new @HistoryV2Manager(@ide, @scope)
 
 	it "should setup the history scope on intialization", ->
 		expect(@scope.history).to.deep.equal({
@@ -38,16 +38,16 @@ describe "HistoryManager", ->
 	describe "_perDocSummaryOfUpdates", ->
 		it "should return the range of updates for the docs", ->
 			result = @historyManager._perDocSummaryOfUpdates([{
-				docs: ["main.tex"]
+				pathnames: ["main.tex"]
 				fromV: 7, toV: 9
 			},{
-				docs: ["main.tex", "foo.tex"]
+				pathnames: ["main.tex", "foo.tex"]
 				fromV: 4, toV: 6
 			},{
-				docs: ["main.tex"]
+				pathnames: ["main.tex"]
 				fromV: 3, toV: 3
 			},{
-				docs: ["foo.tex"]
+				pathnames: ["foo.tex"]
 				fromV: 0, toV: 2
 			}])
 
@@ -58,7 +58,7 @@ describe "HistoryManager", ->
 
 		it "should track renames", ->
 			result = @historyManager._perDocSummaryOfUpdates([{
-				docs: ["main2.tex"]
+				pathnames: ["main2.tex"]
 				fromV: 5, toV: 9
 			},{
 				project_ops: [{
@@ -69,7 +69,7 @@ describe "HistoryManager", ->
 				}],
 				fromV: 4, toV: 4
 			},{
-				docs: ["main1.tex"]
+				pathnames: ["main1.tex"]
 				fromV: 3, toV: 3
 			},{
 				project_ops: [{
@@ -80,10 +80,54 @@ describe "HistoryManager", ->
 				}],
 				fromV: 2, toV: 2
 			},{
-				docs: ["main0.tex"]
+				pathnames: ["main0.tex"]
 				fromV: 0, toV: 1
 			}])
 
 			expect(result).to.deep.equal({
 				"main0.tex": { fromV: 0, toV: 9 }
 			})
+
+		it "should track single renames", ->
+			result = @historyManager._perDocSummaryOfUpdates([{
+				project_ops: [{
+					rename: {
+						pathname: "main1.tex",
+						newPathname: "main2.tex"
+					}
+				}],
+				fromV: 4, toV: 5
+			}])
+
+			expect(result).to.deep.equal({
+				"main1.tex": { fromV: 4, toV: 5 }
+			})
+
+		it "should track additions", ->
+			result = @historyManager._perDocSummaryOfUpdates([{
+				project_ops: [{
+					add:
+						pathname: "main.tex"
+				}]
+				fromV: 0, toV: 1
+			}, {
+				pathnames: ["main.tex"]
+				fromV: 1, toV: 4
+			}])
+
+			expect(result).to.deep.equal({
+				"main.tex": { fromV: 0, toV: 4 }
+			})
+
+		it "should track single additions", ->
+			result = @historyManager._perDocSummaryOfUpdates([{
+				project_ops: [{
+					add:
+						pathname: "main.tex"
+				}]
+				fromV: 0, toV: 1
+			}])
+
+			expect(result).to.deep.equal({
+				"main.tex": { fromV: 0, toV: 1 }
+			})

From 8a3fadbfc1356cd6d2eca67c3fb2b59b5d3403ca Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Wed, 13 Dec 2017 16:48:48 +0000
Subject: [PATCH 095/109] Style the edit/add/rename options

---
 services/web/app/views/project/editor/history.pug  | 13 +++++++++----
 .../coffee/ide/history/HistoryManager.coffee       |  1 +
 .../coffee/ide/history/HistoryV2Manager.coffee     |  9 +--------
 .../web/public/stylesheets/app/editor/history.less | 14 ++++++++++++--
 .../coffee/HistoryManagerV2Tests.coffee            |  1 +
 5 files changed, 24 insertions(+), 14 deletions(-)

diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug
index bdc834352f..ba7740c199 100644
--- a/services/web/app/views/project/editor/history.pug
+++ b/services/web/app/views/project/editor/history.pug
@@ -135,12 +135,17 @@ div#history(ng-show="ui.view == 'history'")
 					div.description(ng-click="select()")
 						div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
 						div.docs(ng-repeat="pathname in update.pathnames")
+							i.fa.fa-pencil(ng-if="history.isV2")
 							span.doc {{ pathname }}
 						div.docs(ng-repeat="project_op in update.project_ops")
 							span(ng-if="project_op.rename")
-								| Renamed {{ project_op.rename.pathname }} to {{ project_op.rename.newPathname }}
+								span Renamed 
+								span.doc {{ project_op.rename.pathname }}
+								span  to 
+								span.doc {{ project_op.rename.newPathname }}
 							span(ng-if="project_op.add")
-								| Added {{ project_op.add.pathname }}
+								span Added 
+								span.doc {{ project_op.add.pathname }}
 						div.users
 							div.user(ng-repeat="update_user in update.meta.users")
 								.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
@@ -170,8 +175,8 @@ div#history(ng-show="ui.view == 'history'")
 							'other': 'changes'\
 						}"
 					)
-					|  in {{history.diff.doc.name}}
-				.toolbar-right
+					|  in {{history.diff.pathname}}
+				.toolbar-right(ng-if="!history.isV2")
 					a.btn.btn-danger.btn-sm(
 						href,
 						ng-click="openRestoreDiffModal()"
diff --git a/services/web/public/coffee/ide/history/HistoryManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee
index b2ecc8c8ef..f896fbb8b4 100644
--- a/services/web/public/coffee/ide/history/HistoryManager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryManager.coffee
@@ -100,6 +100,7 @@ define [
 				end_ts:   end_ts
 				doc:      doc
 				error:    false
+				pathname: doc.name
 			}
 
 			if !doc.deleted
diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
index d7e80d5153..01c9a6ad2d 100644
--- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
@@ -37,6 +37,7 @@ define [
 
 		reset: () ->
 			@$scope.history = {
+				isV2: true
 				updates: []
 				nextBeforeTimestamp: null
 				atEnd: false
@@ -129,14 +130,6 @@ define [
 				diff.restoreDeletedSuccess = false
 				diff.restoredDocNewId = null
 
-		restoreDeletedDoc: (doc) ->
-			url = "/project/#{@$scope.project_id}/doc/#{doc.id}/restore"
-			@ide.$http.post(url, name: doc.name, _csrf: window.csrfToken)
-
-		restoreDiff: (diff) ->
-			url = "/project/#{@$scope.project_id}/doc/#{diff.doc.id}/version/#{diff.fromV}/restore"
-			@ide.$http.post(url, _csrf: window.csrfToken)
-
 		_parseDiff: (diff) ->
 			row    = 0
 			column = 0
diff --git a/services/web/public/stylesheets/app/editor/history.less b/services/web/public/stylesheets/app/editor/history.less
index b6dca4b7cc..558f460e3c 100644
--- a/services/web/public/stylesheets/app/editor/history.less
+++ b/services/web/public/stylesheets/app/editor/history.less
@@ -169,10 +169,20 @@
 					font-size: 0.8rem;
 					line-height: @line-height-computed;
 				}
-				.docs {
-					font-weight: bold;
+				.action {
 					font-size: 0.9rem;
 				}
+				.docs {
+					font-size: 0.9rem;
+					.doc {
+						font-weight: bold;
+					}
+					i {
+						font-size: 0.8rem;
+						color: @gray;
+						margin-right: 4px;
+					}
+				}
 			}
 			li.loading-changes, li.empty-message {
 				padding: 6px;
diff --git a/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
index 4517fc16dd..19b5156b1b 100644
--- a/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
+++ b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
@@ -21,6 +21,7 @@ describe "HistoryV2Manager", ->
 
 	it "should setup the history scope on intialization", ->
 		expect(@scope.history).to.deep.equal({
+			isV2: true
 			updates: []
 			nextBeforeTimestamp: null
 			atEnd: false

From d84580f12dd47ebf69ca207e55205057695596bf Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Wed, 13 Dec 2017 17:09:55 +0000
Subject: [PATCH 096/109] Label actions with text rather than icons

---
 .../web/app/views/project/editor/history.pug  | 19 +++++++++---------
 .../stylesheets/app/editor/history.less       | 20 +++++++++----------
 2 files changed, 19 insertions(+), 20 deletions(-)

diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug
index ba7740c199..6621cdb2d2 100644
--- a/services/web/app/views/project/editor/history.pug
+++ b/services/web/app/views/project/editor/history.pug
@@ -134,18 +134,17 @@ div#history(ng-show="ui.view == 'history'")
 
 					div.description(ng-click="select()")
 						div.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
+						div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
+							| Edited
 						div.docs(ng-repeat="pathname in update.pathnames")
-							i.fa.fa-pencil(ng-if="history.isV2")
-							span.doc {{ pathname }}
+							.doc {{ pathname }}
 						div.docs(ng-repeat="project_op in update.project_ops")
-							span(ng-if="project_op.rename")
-								span Renamed 
-								span.doc {{ project_op.rename.pathname }}
-								span  to 
-								span.doc {{ project_op.rename.newPathname }}
-							span(ng-if="project_op.add")
-								span Added 
-								span.doc {{ project_op.add.pathname }}
+							div(ng-if="project_op.rename")
+								.action Renamed
+								.doc {{ project_op.rename.pathname }} → {{ project_op.rename.newPathname }}
+							div(ng-if="project_op.add")
+								.action Created
+								.doc {{ project_op.add.pathname }}
 						div.users
 							div.user(ng-repeat="update_user in update.meta.users")
 								.color-square(ng-if="update_user != null", ng-style="{'background-color': 'hsl({{ update_user.hue }}, 70%, 50%)'}")
diff --git a/services/web/public/stylesheets/app/editor/history.less b/services/web/public/stylesheets/app/editor/history.less
index 558f460e3c..2824fd2e32 100644
--- a/services/web/public/stylesheets/app/editor/history.less
+++ b/services/web/public/stylesheets/app/editor/history.less
@@ -169,18 +169,18 @@
 					font-size: 0.8rem;
 					line-height: @line-height-computed;
 				}
-				.action {
+				.doc {
 					font-size: 0.9rem;
+					font-weight: bold;
 				}
-				.docs {
-					font-size: 0.9rem;
-					.doc {
-						font-weight: bold;
-					}
-					i {
-						font-size: 0.8rem;
-						color: @gray;
-						margin-right: 4px;
+				.action {
+					color: @gray;
+					text-transform: uppercase;
+					font-size: 0.7em;
+					margin-bottom: -2px;
+					margin-top: 2px;
+					&-edited {
+						margin-top: 0;
 					}
 				}
 			}

From d7a26e27e5c7cee5c75127011597fba5e827cfe7 Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Thu, 14 Dec 2017 09:32:38 +0000
Subject: [PATCH 097/109] Run front end tests in CI

---
 services/web/Jenkinsfile                      | 15 ++++++---------
 services/web/Makefile                         | 19 ++++++++++++-------
 ...tend_unit_tests => compile_frontend_tests} |  0
 .../bin/{frontend_unit_test => frontend_test} |  0
 services/web/docker-shared.template.yml       |  1 +
 services/web/package.json                     |  4 ++--
 6 files changed, 21 insertions(+), 18 deletions(-)
 rename services/web/bin/{compile_frontend_unit_tests => compile_frontend_tests} (100%)
 rename services/web/bin/{frontend_unit_test => frontend_test} (100%)

diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile
index 6421296330..57df9c7091 100644
--- a/services/web/Jenkinsfile
+++ b/services/web/Jenkinsfile
@@ -71,16 +71,9 @@ pipeline {
       }
     }
     
-    stage('Unit Tests') {
+    stage('Test') {
       steps {
-        sh 'make clean install' // Removes js files, so do before compile
-        sh 'make test_unit MOCHA_ARGS="--reporter=tap"'
-      }
-    }
-    
-    stage('Acceptance Tests') {
-      steps {
-        sh 'make test_acceptance MOCHA_ARGS="--reporter=tap"'
+        sh 'make ci'
       }
     }
 
@@ -155,6 +148,10 @@ pipeline {
   }
   
   post {
+    always {
+      sh 'make ci_clean'
+    }
+
     failure {
       mail(from: "${EMAIL_ALERT_FROM}", 
            to: "${EMAIL_ALERT_TO}", 
diff --git a/services/web/Makefile b/services/web/Makefile
index 98695a8c1f..558b59e5e6 100644
--- a/services/web/Makefile
+++ b/services/web/Makefile
@@ -16,9 +16,10 @@ add_dev: docker-shared.yml
 	$(NPM) install --save-dev ${P}
 
 install: docker-shared.yml
+	bin/generate_volumes_file
 	$(NPM) install
 
-clean:
+clean: ci_clean
 	rm -f app.js
 	rm -rf app/js
 	rm -rf test/unit/js
@@ -30,9 +31,8 @@ clean:
 		rm -rf $$dir/test/unit/js; \
 		rm -rf $$dir/test/acceptance/js; \
 	done
-	# Regenerate docker-shared.yml - not stictly a 'clean',
-	# but lets `make clean install` work nicely
-	bin/generate_volumes_file
+
+ci_clean:
 	# Deletes node_modules volume
 	docker-compose down --volumes
 
@@ -40,11 +40,14 @@ clean:
 docker-shared.yml:
 	bin/generate_volumes_file
 
-test: test_unit test_acceptance
+test: test_unit test_frontend test_acceptance
 
 test_unit: docker-shared.yml
 	docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:unit -- ${MOCHA_ARGS}
 
+test_frontend: docker-shared.yml
+	docker-compose ${DOCKER_COMPOSE_FLAGS} run --rm test_unit npm -q run test:frontend -- ${MOCHA_ARGS}
+
 test_acceptance: test_acceptance_app test_acceptance_modules
 
 test_acceptance_app: test_acceptance_app_start_service test_acceptance_app_run test_acceptance_app_stop_service
@@ -71,7 +74,9 @@ test_acceptance_modules: docker-shared.yml
 test_acceptance_module: docker-shared.yml
 	cd $(MODULE) && make test_acceptance
 
+ci: install test
+
 .PHONY:
-	all add install update test test_unit test_acceptance \
+	all add install update test test_unit test_unit_frontend test_acceptance \
 	test_acceptance_start_service test_acceptance_stop_service \
-	test_acceptance_run
+	test_acceptance_run ci ci_clean
diff --git a/services/web/bin/compile_frontend_unit_tests b/services/web/bin/compile_frontend_tests
similarity index 100%
rename from services/web/bin/compile_frontend_unit_tests
rename to services/web/bin/compile_frontend_tests
diff --git a/services/web/bin/frontend_unit_test b/services/web/bin/frontend_test
similarity index 100%
rename from services/web/bin/frontend_unit_test
rename to services/web/bin/frontend_test
diff --git a/services/web/docker-shared.template.yml b/services/web/docker-shared.template.yml
index d697e59e96..5f45c48c82 100644
--- a/services/web/docker-shared.template.yml
+++ b/services/web/docker-shared.template.yml
@@ -24,6 +24,7 @@ services:
       - ./app/views:/app/app/views:ro
       - ./config:/app/config
       - ./test/unit/coffee:/app/test/unit/coffee:ro
+      - ./test/unit_frontend/coffee:/app/test/unit_frontend/coffee:ro
       - ./test/acceptance/coffee:/app/test/acceptance/coffee:ro
       - ./test/acceptance/files:/app/test/acceptance/files:ro
       - ./test/smoke/coffee:/app/test/smoke/coffee:ro
diff --git a/services/web/package.json b/services/web/package.json
index b5e843a371..398ce3dc78 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -15,9 +15,9 @@
     "test:acceptance:dir": "npm -q run compile:acceptance_tests && npm -q run test:acceptance:wait_for_app && npm -q run test:acceptance:run -- $@",
     "test:acceptance": "npm -q run test:acceptance:dir -- $@ test/acceptance/js",
     "test:unit": "npm -q run compile:backend && npm -q run compile:unit_tests && bin/unit_test $@",
-    "test:frontend_unit": "npm -q run compile:frontend && npm -q run compile:frontend_unit_tests && bin/frontend_unit_test $@",
+    "test:frontend": "npm -q run compile:frontend && npm -q run compile:frontend_tests && bin/frontend_test $@",
     "compile:unit_tests": "bin/compile_unit_tests",
-    "compile:frontend_unit_tests": "bin/compile_frontend_unit_tests",
+    "compile:frontend_tests": "bin/compile_frontend_tests",
     "compile:acceptance_tests": "bin/compile_acceptance_tests",
     "compile:frontend": "bin/compile_frontend",
     "compile:backend": "bin/compile_backend",

From b0812864ac5214f96dc18bef0e14fcea08a68c01 Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Thu, 14 Dec 2017 09:43:59 +0000
Subject: [PATCH 098/109] Clean up CI output

---
 services/web/Jenkinsfile | 2 +-
 services/web/Makefile    | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile
index 57df9c7091..152d980787 100644
--- a/services/web/Jenkinsfile
+++ b/services/web/Jenkinsfile
@@ -60,7 +60,7 @@ pipeline {
         sh 'git config --global core.logallrefupdates false'
         sh 'mv app/views/external/robots.txt public/robots.txt'
         sh 'mv app/views/external/googlebdb0f8f7f4a17241.html public/googlebdb0f8f7f4a17241.html'
-        sh 'npm install'
+        sh 'npm --quiet install'
         sh 'npm rebuild'
         // It's too easy to end up shrinkwrapping to an outdated version of translations.
         // Ensure translations are always latest, regardless of shrinkwrap
diff --git a/services/web/Makefile b/services/web/Makefile
index 558b59e5e6..046a0c1a81 100644
--- a/services/web/Makefile
+++ b/services/web/Makefile
@@ -74,7 +74,9 @@ test_acceptance_modules: docker-shared.yml
 test_acceptance_module: docker-shared.yml
 	cd $(MODULE) && make test_acceptance
 
-ci: install test
+ci:
+	MOCHA_ARGS="--reporter tap" \
+	$(MAKE) install test
 
 .PHONY:
 	all add install update test test_unit test_unit_frontend test_acceptance \

From b4a5e5a041c4d111e43e359c2da4368c839ab01f Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Thu, 14 Dec 2017 10:07:17 +0000
Subject: [PATCH 099/109] Tidy up HistoryV2Manager

---
 .../ide/history/HistoryV2Manager.coffee       | 63 ++++++++-----------
 1 file changed, 26 insertions(+), 37 deletions(-)

diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
index 01c9a6ad2d..8198738e16 100644
--- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
+++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee
@@ -22,8 +22,7 @@ define [
 
 			@$scope.$on "entity:selected", (event, entity) =>
 				if (@$scope.ui.view == "history") and (entity.type == "doc")
-					# TODO: Set selection.pathname to entity path name
-					# @$scope.history.selection.doc = entity
+					@$scope.history.selection.pathname = _ide.fileTreeManager.getEntityPath(entity)
 					@reloadDiff()
 
 		show: () ->
@@ -43,7 +42,7 @@ define [
 				atEnd: false
 				selection: {
 					updates: []
-					doc: null
+					pathname: null
 					range: {
 						fromV: null
 						toV: null
@@ -52,6 +51,7 @@ define [
 				diff: null
 			}
 
+		MAX_RECENT_UPDATES_TO_SELECT: 2
 		autoSelectRecentUpdates: () ->
 			return if @$scope.history.updates.length == 0
 
@@ -59,7 +59,7 @@ define [
 
 			indexOfLastUpdateNotByMe = 0
 			for update, i in @$scope.history.updates
-				if @_updateContainsUserId(update, @$scope.user.id)
+				if @_updateContainsUserId(update, @$scope.user.id) or i > @MAX_RECENT_UPDATES_TO_SELECT
 					break
 				indexOfLastUpdateNotByMe = i
 
@@ -75,7 +75,6 @@ define [
 				.get(url)
 				.then (response) =>
 					{ data } = response
-					console.log "fetchNextBatchOfUpdates", data.updates
 					@_loadUpdates(data.updates)
 					@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
 					if !data.nextBeforeTimestamp?
@@ -86,10 +85,10 @@ define [
 			diff = @$scope.history.diff
 			{updates} = @$scope.history.selection
 			{fromV, toV, pathname} = @_calculateDiffDataFromSelection()
-			console.log "[reloadDiff] current diff", diff
-			console.log "[reloadDiff] new diff data", {fromV, toV, pathname}
 
-			return if !pathname?
+			if !pathname?
+				@$scope.history.diff = null
+				return
 
 			return if diff? and
 				diff.pathname == pathname and
@@ -103,32 +102,24 @@ define [
 				error:    false
 			}
 
-			# TODO: How do we track deleted files now? We can probably show the diffs easily
-			# with the new system!
-			if true # !doc.deleted
-				diff.loading = true
-				url = "/project/#{@$scope.project_id}/diff"
-				query = ["pathname=#{encodeURIComponent(pathname)}"]
-				if diff.fromV? and diff.toV?
-					query.push "from=#{diff.fromV}", "to=#{diff.toV}"
-				url += "?" + query.join("&")
+			diff.loading = true
+			url = "/project/#{@$scope.project_id}/diff"
+			query = ["pathname=#{encodeURIComponent(pathname)}"]
+			if diff.fromV? and diff.toV?
+				query.push "from=#{diff.fromV}", "to=#{diff.toV}"
+			url += "?" + query.join("&")
 
-				@ide.$http
-					.get(url)
-					.then (response) =>
-						{ data } = response
-						diff.loading = false
-						{text, highlights} = @_parseDiff(data)
-						diff.text = text
-						diff.highlights = highlights
-					.catch () ->
-						diff.loading = false
-						diff.error = true
-			else
-				diff.deleted = true
-				diff.restoreInProgress = false
-				diff.restoreDeletedSuccess = false
-				diff.restoredDocNewId = null
+			@ide.$http
+				.get(url)
+				.then (response) =>
+					{ data } = response
+					diff.loading = false
+					{text, highlights} = @_parseDiff(data)
+					diff.text = text
+					diff.highlights = highlights
+				.catch () ->
+					diff.loading = false
+					diff.error = true
 
 		_parseDiff: (diff) ->
 			row    = 0
@@ -255,14 +246,12 @@ define [
 
 			selected_pathname = @$scope.history.selection.pathname
 
-			console.log "[_calculateDiffDataFromSelection]", @$scope.history.selection.updates
 			for pathname, doc of @_perDocSummaryOfUpdates(@$scope.history.selection.updates)
-				console.log "[_calculateDiffDataFromSelection] doc:", pathname, doc
 				if pathname == selected_pathname
 					{fromV, toV} = doc
-					break
+					return {fromV, toV, pathname}
 
-			return {fromV, toV, pathname}
+			return {}
 
 		# Set the track changes selected doc to one of the docs in the range
 		# of currently selected updates. If we already have a selected doc

From 810b5e0e9a9629ba347e1ea88b0ec6a41d06edea Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Thu, 14 Dec 2017 10:18:17 +0000
Subject: [PATCH 100/109] Fix front end tests

---
 services/web/docker-shared.template.yml                    | 7 ++-----
 .../test/unit_frontend/coffee/HistoryManagerV2Tests.coffee | 2 +-
 2 files changed, 3 insertions(+), 6 deletions(-)

diff --git a/services/web/docker-shared.template.yml b/services/web/docker-shared.template.yml
index 5f45c48c82..8acc63ef81 100644
--- a/services/web/docker-shared.template.yml
+++ b/services/web/docker-shared.template.yml
@@ -13,11 +13,8 @@ services:
       - ./npm-shrinkwrap.json:/app/npm-shrinkwrap.json
       - node_modules:/app/node_modules
       - ./bin:/app/bin
-      # Copying the whole public dir is fine for now, and needed for
-      # some unit tests to pass, but we will want to isolate the coffee
-      # and vendor js files, so that the compiled js files are not written
-      # back to the local filesystem.
-      - ./public:/app/public
+      - ./public/coffee:/app/public/coffee:ro
+      - ./public/js/ace-1.2.5:/app/public/js/ace-1.2.5
       - ./app.coffee:/app/app.coffee:ro
       - ./app/coffee:/app/app/coffee:ro
       - ./app/templates:/app/app/templates:ro
diff --git a/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
index 19b5156b1b..9ad7ba9a2e 100644
--- a/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
+++ b/services/web/test/unit_frontend/coffee/HistoryManagerV2Tests.coffee
@@ -27,7 +27,7 @@ describe "HistoryV2Manager", ->
 			atEnd: false
 			selection: {
 				updates: []
-				doc: null
+				pathname: null
 				range: {
 					fromV: null
 					toV: null

From dfe6e26946ddd7f0e4ba9be0a178a032d8e37715 Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Thu, 14 Dec 2017 14:57:34 +0000
Subject: [PATCH 101/109] test_unit_frontend -> test_frontend in Makefile

---
 services/web/Makefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/web/Makefile b/services/web/Makefile
index 046a0c1a81..5db188e2b7 100644
--- a/services/web/Makefile
+++ b/services/web/Makefile
@@ -79,6 +79,6 @@ ci:
 	$(MAKE) install test
 
 .PHONY:
-	all add install update test test_unit test_unit_frontend test_acceptance \
+	all add install update test test_unit test_frontend test_acceptance \
 	test_acceptance_start_service test_acceptance_stop_service \
 	test_acceptance_run ci ci_clean

From 8311101ec0e2a86fcaf0bd88ecb57e15f085aba0 Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Thu, 14 Dec 2017 15:38:20 +0000
Subject: [PATCH 102/109] Split project_history.enable in
 initializeHistoryForNewProjects and sendProjectStructureOps

---
 .../Features/DocumentUpdater/DocumentUpdaterHandler.coffee | 2 +-
 .../app/coffee/Features/History/HistoryController.coffee   | 7 ++++---
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
index f7dd9f3e17..8dbb52d10b 100644
--- a/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
+++ b/services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee
@@ -205,7 +205,7 @@ module.exports = DocumentUpdaterHandler =
 				callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
 
 	updateProjectStructure : (project_id, userId, changes, callback = (error) ->)->
-		return callback() if !settings.apis.project_history?.enabled
+		return callback() if !settings.apis.project_history?.sendProjectStructureOps
 
 		docUpdates = DocumentUpdaterHandler._getRenameUpdates('doc', changes.oldDocs, changes.newDocs)
 		fileUpdates = DocumentUpdaterHandler._getRenameUpdates('file', changes.oldFiles, changes.newFiles)
diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee
index 70e0828160..0935118a7c 100644
--- a/services/web/app/coffee/Features/History/HistoryController.coffee
+++ b/services/web/app/coffee/Features/History/HistoryController.coffee
@@ -6,7 +6,7 @@ ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
 
 module.exports = HistoryController =
 	initializeProject: (callback = (error, history_id) ->) ->
-		return callback() if !settings.apis.project_history?.enabled
+		return callback() if !settings.apis.project_history?.initializeHistoryForNewProjects
 		request.post {
 			url: "#{settings.apis.project_history.url}/project"
 		}, (error, res, body)->
@@ -33,7 +33,8 @@ module.exports = HistoryController =
 		# find out which type of history service this project uses
 		ProjectDetailsHandler.getDetails project_id, (err, project) ->
 			return next(err) if err?
-			if project?.overleaf?.history?.display
+			history = project.overleaf?.history
+			if history?.id? and history?.display
 				req.useProjectHistory = true
 			else
 				req.useProjectHistory = false
@@ -58,7 +59,7 @@ module.exports = HistoryController =
 	buildHistoryServiceUrl: (useProjectHistory) ->
 		# choose a history service, either document-level (trackchanges)
 		# or project-level (project_history)
-		if settings.apis.project_history?.enabled && useProjectHistory
+		if useProjectHistory
 			return settings.apis.project_history.url
 		else
 			return settings.apis.trackchanges.url

From ffa2e231fdec7e104bf58ca8eaf4a4e720eb7d2b Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Fri, 15 Dec 2017 16:11:16 +0000
Subject: [PATCH 103/109] Fix up tests

---
 services/web/config/settings.defaults.coffee  |   3 +-
 .../DocumentUpdaterHandlerTests.coffee        |   4 +-
 .../History/HistoryControllerTests.coffee     | 124 ++++++------------
 3 files changed, 47 insertions(+), 84 deletions(-)

diff --git a/services/web/config/settings.defaults.coffee b/services/web/config/settings.defaults.coffee
index 65a8bf1e91..844bffae3c 100644
--- a/services/web/config/settings.defaults.coffee
+++ b/services/web/config/settings.defaults.coffee
@@ -111,7 +111,8 @@ module.exports = settings =
 		trackchanges:
 			url : "http://localhost:3015"
 		project_history:
-			enabled: process.env.PROJECT_HISTORY_ENABLED == 'true' or false
+			sendProjectStructureOps: process.env.PROJECT_HISTORY_ENABLED == 'true' or false
+			initializeHistoryForNewProjects: process.env.PROJECT_HISTORY_ENABLED == 'true' or false
 			url : "http://localhost:3054"
 		docstore:
 			url : "http://#{process.env['DOCSTORE_HOST'] or 'localhost'}:3016"
diff --git a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
index 14ccaa3a33..5eb3c057ab 100644
--- a/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
+++ b/services/web/test/unit/coffee/DocumentUpdater/DocumentUpdaterHandlerTests.coffee
@@ -393,7 +393,7 @@ describe 'DocumentUpdaterHandler', ->
 
 		describe "with project history disabled", ->
 			beforeEach ->
-				@settings.apis.project_history.enabled = false
+				@settings.apis.project_history.sendProjectStructureOps = false
 				@request.post = sinon.stub()
 
 				@handler.updateProjectStructure @project_id, @user_id, {}, @callback
@@ -406,7 +406,7 @@ describe 'DocumentUpdaterHandler', ->
 
 		describe "with project history enabled", ->
 			beforeEach ->
-				@settings.apis.project_history.enabled = true
+				@settings.apis.project_history.sendProjectStructureOps = true
 				@url = "#{@settings.apis.documentupdater.url}/project/#{@project_id}"
 				@request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204}, "")
 
diff --git a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
index e03189526a..3e0464254e 100644
--- a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
+++ b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
@@ -31,7 +31,7 @@ describe "HistoryController", ->
 
 		describe "for a project with project history", ->
 			beforeEach ->
-				@ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {overleaf:{history:{display:true}}})
+				@ProjectDetailsHandler.getDetails = sinon.stub().callsArgWith(1, null, {overleaf:{history:{id: 42, display:true}}})
 				@HistoryController.selectHistoryApi @req, @res, @next
 
 			it "should set the flag for project history to true", ->
@@ -57,93 +57,55 @@ describe "HistoryController", ->
 				on: (event, handler) -> @events[event] = handler
 			@request.returns @proxy
 
-		describe "with project history enabled", ->
+		describe "for a project with the project history flag", ->
 			beforeEach ->
-				@settings.apis.project_history.enabled = true
+				@req.useProjectHistory = true
+				@HistoryController.proxyToHistoryApi @req, @res, @next
 
-			describe "for a project with the project history flag", ->
-				beforeEach ->
-					@req.useProjectHistory = true
-					@HistoryController.proxyToHistoryApi @req, @res, @next
+			it "should get the user id", ->
+				@AuthenticationController.getLoggedInUserId
+					.calledWith(@req)
+					.should.equal true
 
-				it "should get the user id", ->
-					@AuthenticationController.getLoggedInUserId
-						.calledWith(@req)
-						.should.equal true
+			it "should call the project history api", ->
+				@request
+					.calledWith({
+						url: "#{@settings.apis.project_history.url}#{@req.url}"
+						method: @req.method
+						headers:
+							"X-User-Id": @user_id
+					})
+					.should.equal true
 
-				it "should call the project history api", ->
-					@request
-						.calledWith({
-							url: "#{@settings.apis.project_history.url}#{@req.url}"
-							method: @req.method
-							headers:
-								"X-User-Id": @user_id
-						})
-						.should.equal true
+			it "should pipe the response to the client", ->
+				@proxy.pipe
+					.calledWith(@res)
+					.should.equal true
 
-				it "should pipe the response to the client", ->
-					@proxy.pipe
-						.calledWith(@res)
-						.should.equal true
-
-			describe "for a project without the project history flag", ->
-				beforeEach ->
-					@req.useProjectHistory = false
-					@HistoryController.proxyToHistoryApi @req, @res, @next
-
-				it "should get the user id", ->
-					@AuthenticationController.getLoggedInUserId
-						.calledWith(@req)
-						.should.equal true
-
-				it "should call the track changes api", ->
-					@request
-						.calledWith({
-							url: "#{@settings.apis.trackchanges.url}#{@req.url}"
-							method: @req.method
-							headers:
-								"X-User-Id": @user_id
-						})
-						.should.equal true
-
-				it "should pipe the response to the client", ->
-					@proxy.pipe
-						.calledWith(@res)
-						.should.equal true
-
-		describe "with project history disabled", ->
+		describe "for a project without the project history flag", ->
 			beforeEach ->
-				@settings.apis.project_history.enabled = false
+				@req.useProjectHistory = false
+				@HistoryController.proxyToHistoryApi @req, @res, @next
 
-			describe "for a project with the project history flag", ->
-				beforeEach ->
-					@req.useProjectHistory = true
-					@HistoryController.proxyToHistoryApi @req, @res, @next
+			it "should get the user id", ->
+				@AuthenticationController.getLoggedInUserId
+					.calledWith(@req)
+					.should.equal true
 
-				it "should call the track changes api", ->
-					@request
-						.calledWith({
-							url: "#{@settings.apis.trackchanges.url}#{@req.url}"
-							method: @req.method
-							headers:
-								"X-User-Id": @user_id
-						})
-						.should.equal true
+			it "should call the track changes api", ->
+				@request
+					.calledWith({
+						url: "#{@settings.apis.trackchanges.url}#{@req.url}"
+						method: @req.method
+						headers:
+							"X-User-Id": @user_id
+					})
+					.should.equal true
 
-			describe "for a project without the project history flag", ->
-				beforeEach ->
-					@req.useProjectHistory = false
-					@HistoryController.proxyToHistoryApi @req, @res, @next
-
-				it "should call the track changes api", ->
-					@request
-						.calledWith({
-							url: "#{@settings.apis.trackchanges.url}#{@req.url}"
-							method: @req.method
-							headers:
-								"X-User-Id": @user_id
-						})
-						.should.equal true
+			it "should pipe the response to the client", ->
+				@proxy.pipe
+					.calledWith(@res)
+					.should.equal true
 
 		describe "with an error", ->
 			beforeEach ->
@@ -156,7 +118,7 @@ describe "HistoryController", ->
 	describe "initializeProject", ->
 		describe "with project history enabled", ->
 			beforeEach ->
-				@settings.apis.project_history.enabled = true
+				@settings.apis.project_history.initializeHistoryForNewProjects = true
 
 			describe "project history returns a successful response", ->
 				beforeEach ->
@@ -212,7 +174,7 @@ describe "HistoryController", ->
 
 		describe "with project history disabled", ->
 			beforeEach ->
-				@settings.apis.project_history.enabled = false
+				@settings.apis.project_history.initializeHistoryForNewProjects = false
 				@HistoryController.initializeProject @callback
 
 			it "should return the callback", ->

From 5463b608adbac2af8b6cbf5e320dc04003533b17 Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Mon, 18 Dec 2017 16:39:27 +0000
Subject: [PATCH 104/109] Add add{File|Doc}WithoutUpdatingHistory methods to
 allow importing OL projects with existing history

---
 .../Features/History/HistoryController.coffee | 23 -----
 .../Features/History/HistoryManager.coffee    | 26 ++++++
 .../Project/ProjectCreationHandler.coffee     |  4 +-
 .../Features/Project/ProjectDuplicator.coffee |  2 +-
 .../Project/ProjectEntityHandler.coffee       | 88 +++++++++++--------
 services/web/app/coffee/models/Project.coffee |  1 +
 .../coffee/ProjectStructureTests.coffee       |  2 +-
 .../History/HistoryControllerTests.coffee     | 65 --------------
 .../coffee/History/HistoryManagerTests.coffee | 86 ++++++++++++++++++
 .../ProjectCreationHandlerTests.coffee        |  8 +-
 .../Project/ProjectDuplicatorTests.coffee     | 12 +--
 .../Project/ProjectEntityHandlerTests.coffee  | 51 +++++++++++
 12 files changed, 229 insertions(+), 139 deletions(-)
 create mode 100644 services/web/app/coffee/Features/History/HistoryManager.coffee
 create mode 100644 services/web/test/unit/coffee/History/HistoryManagerTests.coffee

diff --git a/services/web/app/coffee/Features/History/HistoryController.coffee b/services/web/app/coffee/Features/History/HistoryController.coffee
index 0935118a7c..76476c8248 100644
--- a/services/web/app/coffee/Features/History/HistoryController.coffee
+++ b/services/web/app/coffee/Features/History/HistoryController.coffee
@@ -5,29 +5,6 @@ AuthenticationController = require "../Authentication/AuthenticationController"
 ProjectDetailsHandler = require "../Project/ProjectDetailsHandler"
 
 module.exports = HistoryController =
-	initializeProject: (callback = (error, history_id) ->) ->
-		return callback() if !settings.apis.project_history?.initializeHistoryForNewProjects
-		request.post {
-			url: "#{settings.apis.project_history.url}/project"
-		}, (error, res, body)->
-			return callback(error) if error?
-
-			if res.statusCode >= 200 and res.statusCode < 300
-				try
-					project = JSON.parse(body)
-				catch error
-					return callback(error)
-
-				overleaf_id = project?.project?.id
-				if !overleaf_id
-					error = new Error("project-history did not provide an id", project)
-					return callback(error)
-
-				callback null, { overleaf_id }
-			else
-				error = new Error("project-history returned a non-success status code: #{res.statusCode}")
-				callback error
-
 	selectHistoryApi: (req, res, next = (error) ->) ->
 		project_id = req.params?.Project_id
 		# find out which type of history service this project uses
diff --git a/services/web/app/coffee/Features/History/HistoryManager.coffee b/services/web/app/coffee/Features/History/HistoryManager.coffee
new file mode 100644
index 0000000000..75f552c907
--- /dev/null
+++ b/services/web/app/coffee/Features/History/HistoryManager.coffee
@@ -0,0 +1,26 @@
+request = require "request"
+settings = require "settings-sharelatex"
+
+module.exports = HistoryManager =
+	initializeProject: (callback = (error, history_id) ->) ->
+		return callback() if !settings.apis.project_history?.initializeHistoryForNewProjects
+		request.post {
+			url: "#{settings.apis.project_history.url}/project"
+		}, (error, res, body)->
+			return callback(error) if error?
+
+			if res.statusCode >= 200 and res.statusCode < 300
+				try
+					project = JSON.parse(body)
+				catch error
+					return callback(error)
+
+				overleaf_id = project?.project?.id
+				if !overleaf_id
+					error = new Error("project-history did not provide an id", project)
+					return callback(error)
+
+				callback null, { overleaf_id }
+			else
+				error = new Error("project-history returned a non-success status code: #{res.statusCode}")
+				callback error
\ No newline at end of file
diff --git a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
index cd78cddc09..ceddc2ad61 100644
--- a/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectCreationHandler.coffee
@@ -7,7 +7,7 @@ Project = require('../../models/Project').Project
 Folder = require('../../models/Folder').Folder
 ProjectEntityHandler = require('./ProjectEntityHandler')
 ProjectDetailsHandler = require('./ProjectDetailsHandler')
-HistoryController = require('../History/HistoryController')
+HistoryManager = require('../History/HistoryManager')
 User = require('../../models/User').User
 fs = require('fs')
 Path = require "path"
@@ -27,7 +27,7 @@ module.exports = ProjectCreationHandler =
 			if projectHistoryId?
 				ProjectCreationHandler._createBlankProject owner_id, projectName, projectHistoryId, callback
 			else
-				HistoryController.initializeProject (error, history) ->
+				HistoryManager.initializeProject (error, history) ->
 					return callback(error) if error?
 					ProjectCreationHandler._createBlankProject owner_id, projectName, history?.overleaf_id, callback
 
diff --git a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee
index 815e31eb27..d4640db966 100644
--- a/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectDuplicator.coffee
@@ -21,7 +21,7 @@ module.exports = ProjectDuplicator =
 				if !doc?._id?
 					return callback()
 				content = docContents[doc._id.toString()]
-				projectEntityHandler.addDocWithProject newProject, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)->
+				projectEntityHandler.addDoc newProject, desFolder._id, doc.name, content.lines, owner_id, (err, newDoc)->
 					if err?
 						logger.err err:err, "error copying doc"
 						return callback(err)
diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
index 446786fa57..180c080b2c 100644
--- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
@@ -149,14 +149,35 @@ module.exports = ProjectEntityHandler =
 		else
 			DocstoreManager.getDoc project_id, doc_id, options, callback
 
-	addDoc: (project_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
-		ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
+	addDoc: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
+		ProjectEntityHandler.addDocWithoutUpdatingHistory project_or_id, folder_id, docName, docLines, userId, (error, doc, folder_id, result) ->
+			return callback(error) if error?
+			newDocs = [
+				doc: doc
+				path: result?.path?.fileSystem
+				docLines: docLines.join('\n')
+			]
+			project_id = project_or_id._id or project_or_id
+			DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) ->
+				return callback(error) if error?
+				callback null, doc, folder_id
+
+	addDocWithoutUpdatingHistory: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
+		# This method should never be called directly, except when importing a project
+		# from Overleaf. It skips sending updates to the project history, which will break
+		# the history unless you are making sure it is updated in some other way.
+		getProject = (cb) ->
+			if project_or_id._id? # project
+				return cb(null, project_or_id)
+			else # id
+				return ProjectGetter.getProjectWithOnlyFolders project_or_id, cb
+		getProject (error, project) ->
 			if err?
 				logger.err project_id:project_id, err:err, "error getting project for add doc"
 				return callback(err)
-			ProjectEntityHandler.addDocWithProject project, folder_id, docName, docLines, userId, callback
+			ProjectEntityHandler._addDocWithProject project, folder_id, docName, docLines, userId, callback
 
-	addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
+	_addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id, result) ->)=>
 		project_id = project._id
 		logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project"
 		confirmFolder project, folder_id, (folder_id)=>
@@ -176,14 +197,7 @@ module.exports = ProjectEntityHandler =
 						rev:          0
 					}, (err) ->
 						return callback(err) if err?
-						newDocs = [
-							doc: doc
-							path: result?.path?.fileSystem
-							docLines: docLines.join('\n')
-						]
-						DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newDocs}, (error) ->
-							return callback(error) if error?
-							callback null, doc, folder_id
+						callback(null, doc, folder_id, result)
 
 	restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
 		# getDoc will return the deleted doc's lines, but we don't actually remove
@@ -192,37 +206,37 @@ module.exports = ProjectEntityHandler =
 			return callback(error) if error?
 			ProjectEntityHandler.addDoc project_id, null, name, lines, callback
 
-	addFile: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)->
+	addFileWithoutUpdatingHistory: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id, result, fileStoreUrl) ->)->
 		ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
 			if err?
 				logger.err project_id:project_id, err:err, "error getting project for add file"
 				return callback(err)
-			ProjectEntityHandler.addFileWithProject project, folder_id, fileName, path, userId, callback
-
-	addFileWithProject: (project, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)->
-		project_id = project._id
-		logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
-		return callback(err) if err?
-		confirmFolder project, folder_id, (folder_id)->
-			fileRef = new File name : fileName
-			FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err, fileStoreUrl)->
-				if err?
-					logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
-					return callback(err)
-				ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
+			logger.log project_id: project._id, folder_id: folder_id, file_name: fileName, path:path, "adding file"
+			return callback(err) if err?
+			confirmFolder project, folder_id, (folder_id)->
+				fileRef = new File name : fileName
+				FileStoreHandler.uploadFileFromDisk project._id, fileRef._id, path, (err, fileStoreUrl)->
 					if err?
-						logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project"
+						logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error uploading image to s3"
 						return callback(err)
-					tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) ->
-						return callback(err) if err?
-						newFiles = [
-							file: fileRef
-							path: result?.path?.fileSystem
-							url: fileStoreUrl
-						]
-						DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
-							return callback(error) if error?
-							callback null, fileRef, folder_id
+					ProjectEntityHandler._putElement project, folder_id, fileRef, "file", (err, result)=>
+						if err?
+							logger.err err:err, project_id: project._id, folder_id: folder_id, file_name: fileName, fileRef:fileRef, "error adding file with project"
+							return callback(err)
+						tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) ->
+							return callback(err) if err?
+							callback(null, fileRef, folder_id, result, fileStoreUrl)
+
+	addFile:  (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)->
+		ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, path, userId, (error, fileRef, folder_id, result, fileStoreUrl) ->
+			newFiles = [
+				file: fileRef
+				path: result?.path?.fileSystem
+				url: fileStoreUrl
+			]
+			DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->
+				return callback(error) if error?
+				callback null, fileRef, folder_id
 
 	replaceFile: (project_id, file_id, fsPath, userId, callback)->
 		self = ProjectEntityHandler
diff --git a/services/web/app/coffee/models/Project.coffee b/services/web/app/coffee/models/Project.coffee
index e0013f8c5f..b434d35ab9 100644
--- a/services/web/app/coffee/models/Project.coffee
+++ b/services/web/app/coffee/models/Project.coffee
@@ -56,6 +56,7 @@ ProjectSchema = new Schema
 		read_token      : { type: String }
 		history         :
 			id            : { type: Number }
+			display       : { type: Boolean }
 
 ProjectSchema.statics.getProject = (project_or_id, fields, callback)->
 	if project_or_id._id?
diff --git a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
index e54d4fac9d..5dd4fed251 100644
--- a/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
+++ b/services/web/test/acceptance/coffee/ProjectStructureTests.coffee
@@ -59,7 +59,7 @@ describe "ProjectStructureChanges", ->
 				@dup_project_id = body.project_id
 				done()
 
-		it "should version the dosc created", ->
+		it "should version the docs created", ->
 			updates = MockDocUpdaterApi.getProjectStructureUpdates(@dup_project_id).docUpdates
 			expect(updates.length).to.equal(2)
 			_.each updates, (update) =>
diff --git a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
index 3e0464254e..f0669a5902 100644
--- a/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
+++ b/services/web/test/unit/coffee/History/HistoryControllerTests.coffee
@@ -114,68 +114,3 @@ describe "HistoryController", ->
 
 			it "should pass the error up the call chain", ->
 				@next.calledWith(@error).should.equal true
-
-	describe "initializeProject", ->
-		describe "with project history enabled", ->
-			beforeEach ->
-				@settings.apis.project_history.initializeHistoryForNewProjects = true
-
-			describe "project history returns a successful response", ->
-				beforeEach ->
-					@overleaf_id = 1234
-					@res = statusCode: 200
-					@body = JSON.stringify(project: id: @overleaf_id)
-					@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
-
-					@HistoryController.initializeProject @callback
-
-				it "should call the project history api", ->
-					@request.post.calledWith(
-						url: "#{@settings.apis.project_history.url}/project"
-					).should.equal true
-
-				it "should return the callback with the overleaf id", ->
-					@callback.calledWithExactly(null, { @overleaf_id }).should.equal true
-
-			describe "project history returns a response without the project id", ->
-				beforeEach ->
-					@res = statusCode: 200
-					@body = JSON.stringify(project: {})
-					@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
-
-					@HistoryController.initializeProject @callback
-
-				it "should return the callback with an error", ->
-					@callback
-						.calledWith(sinon.match.has("message", "project-history did not provide an id"))
-						.should.equal true
-
-			describe "project history returns a unsuccessful response", ->
-				beforeEach ->
-					@res = statusCode: 404
-					@request.post = sinon.stub().callsArgWith(1, null, @res)
-
-					@HistoryController.initializeProject @callback
-
-				it "should return the callback with an error", ->
-					@callback
-						.calledWith(sinon.match.has("message", "project-history returned a non-success status code: 404"))
-						.should.equal true
-
-			describe "project history errors", ->
-				beforeEach ->
-					@error = sinon.stub()
-					@request.post = sinon.stub().callsArgWith(1, @error)
-
-					@HistoryController.initializeProject @callback
-
-				it "should return the callback with the error", ->
-					@callback.calledWithExactly(@error).should.equal true
-
-		describe "with project history disabled", ->
-			beforeEach ->
-				@settings.apis.project_history.initializeHistoryForNewProjects = false
-				@HistoryController.initializeProject @callback
-
-			it "should return the callback", ->
-				@callback.calledWithExactly().should.equal true
diff --git a/services/web/test/unit/coffee/History/HistoryManagerTests.coffee b/services/web/test/unit/coffee/History/HistoryManagerTests.coffee
new file mode 100644
index 0000000000..9b5f80df04
--- /dev/null
+++ b/services/web/test/unit/coffee/History/HistoryManagerTests.coffee
@@ -0,0 +1,86 @@
+chai = require('chai')
+chai.should()
+sinon = require("sinon")
+modulePath = "../../../../app/js/Features/History/HistoryManager"
+SandboxedModule = require('sandboxed-module')
+
+describe "HistoryManager", ->
+	beforeEach ->
+		@callback = sinon.stub()
+		@user_id = "user-id-123"
+		@AuthenticationController =
+			getLoggedInUserId: sinon.stub().returns(@user_id)
+		@HistoryManager = SandboxedModule.require modulePath, requires:
+			"request" : @request = sinon.stub()
+			"settings-sharelatex": @settings = {}
+		@settings.apis =
+			trackchanges:
+				enabled: false
+				url: "http://trackchanges.example.com"
+			project_history:
+				url: "http://project_history.example.com"
+
+	describe "initializeProject", ->
+		describe "with project history enabled", ->
+			beforeEach ->
+				@settings.apis.project_history.initializeHistoryForNewProjects = true
+
+			describe "project history returns a successful response", ->
+				beforeEach ->
+					@overleaf_id = 1234
+					@res = statusCode: 200
+					@body = JSON.stringify(project: id: @overleaf_id)
+					@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
+
+					@HistoryManager.initializeProject @callback
+
+				it "should call the project history api", ->
+					@request.post.calledWith(
+						url: "#{@settings.apis.project_history.url}/project"
+					).should.equal true
+
+				it "should return the callback with the overleaf id", ->
+					@callback.calledWithExactly(null, { @overleaf_id }).should.equal true
+
+			describe "project history returns a response without the project id", ->
+				beforeEach ->
+					@res = statusCode: 200
+					@body = JSON.stringify(project: {})
+					@request.post = sinon.stub().callsArgWith(1, null, @res, @body)
+
+					@HistoryManager.initializeProject @callback
+
+				it "should return the callback with an error", ->
+					@callback
+						.calledWith(sinon.match.has("message", "project-history did not provide an id"))
+						.should.equal true
+
+			describe "project history returns a unsuccessful response", ->
+				beforeEach ->
+					@res = statusCode: 404
+					@request.post = sinon.stub().callsArgWith(1, null, @res)
+
+					@HistoryManager.initializeProject @callback
+
+				it "should return the callback with an error", ->
+					@callback
+						.calledWith(sinon.match.has("message", "project-history returned a non-success status code: 404"))
+						.should.equal true
+
+			describe "project history errors", ->
+				beforeEach ->
+					@error = sinon.stub()
+					@request.post = sinon.stub().callsArgWith(1, @error)
+
+					@HistoryManager.initializeProject @callback
+
+				it "should return the callback with the error", ->
+					@callback.calledWithExactly(@error).should.equal true
+
+		describe "with project history disabled", ->
+			beforeEach ->
+				@settings.apis.project_history.initializeHistoryForNewProjects = false
+				@HistoryManager.initializeProject @callback
+
+			it "should return the callback", ->
+				@callback.calledWithExactly().should.equal true
diff --git a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
index 470b538bd9..378e909984 100644
--- a/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectCreationHandlerTests.coffee
@@ -38,7 +38,7 @@ describe 'ProjectCreationHandler', ->
 			setRootDoc: sinon.stub().callsArg(2)
 		@ProjectDetailsHandler =
 			validateProjectName: sinon.stub().yields()
-		@HistoryController =
+		@HistoryManager =
 			initializeProject: sinon.stub().callsArg(0)
 
 		@user =
@@ -53,7 +53,7 @@ describe 'ProjectCreationHandler', ->
 			'../../models/User': User:@User
 			'../../models/Project':{Project:@ProjectModel}
 			'../../models/Folder':{Folder:@FolderModel}
-			'../History/HistoryController': @HistoryController
+			'../History/HistoryManager': @HistoryManager
 			'./ProjectEntityHandler':@ProjectEntityHandler
 			"./ProjectDetailsHandler":@ProjectDetailsHandler
 			"settings-sharelatex": @Settings = {}
@@ -66,7 +66,7 @@ describe 'ProjectCreationHandler', ->
 	describe 'Creating a Blank project', ->
 		beforeEach ->
 			@overleaf_id = 1234
-			@HistoryController.initializeProject = sinon.stub().callsArgWith(0, null, { @overleaf_id })
+			@HistoryManager.initializeProject = sinon.stub().callsArgWith(0, null, { @overleaf_id })
 			@ProjectModel::save = sinon.stub().callsArg(0)
 
 		describe "successfully", ->
@@ -83,7 +83,7 @@ describe 'ProjectCreationHandler', ->
 
 			it "should initialize the project overleaf if history id not provided", (done)->
 				@handler.createBlankProject ownerId, projectName, done
-				@HistoryController.initializeProject.calledWith().should.equal true
+				@HistoryManager.initializeProject.calledWith().should.equal true
 
 			it "should set the overleaf id if overleaf id not provided", (done)->
 				@handler.createBlankProject ownerId, projectName, (err, project)=>
diff --git a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
index b489014e7e..b7afb79f83 100644
--- a/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectDuplicatorTests.coffee
@@ -64,7 +64,7 @@ describe 'ProjectDuplicator', ->
 		@projectOptionsHandler =
 			setCompiler : sinon.stub()								
 		@entityHandler =
-			addDocWithProject: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
+			addDoc: sinon.stub().callsArgWith(5, null, {name:"somDoc"})
 			copyFileFromExistingProjectWithProject: sinon.stub().callsArgWith(5)
 			setRootDoc: sinon.stub()
 			addFolderWithProject: sinon.stub().callsArgWith(3, null, @newFolder)
@@ -112,13 +112,13 @@ describe 'ProjectDuplicator', ->
 			done()
 
 	it 'should use the same compiler', (done)->
-		@entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
+		@entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
 		@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
 			@projectOptionsHandler.setCompiler.calledWith(@stubbedNewProject._id, @project.compiler).should.equal true
 			done()
 	
 	it 'should use the same root doc', (done)->
-		@entityHandler.addDocWithProject.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
+		@entityHandler.addDoc.callsArgWith(5, null, @rootFolder.docs[0], @owner._id)
 		@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
 			@entityHandler.setRootDoc.calledWith(@stubbedNewProject._id, @rootFolder.docs[0]._id).should.equal true
 			done()
@@ -139,13 +139,13 @@ describe 'ProjectDuplicator', ->
 	it 'should copy all the docs', (done)->
 		@duplicator.duplicate @owner, @old_project_id, "", (err, newProject)=>
 			@DocstoreManager.getAllDocs.calledWith(@old_project_id).should.equal true
-			@entityHandler.addDocWithProject
+			@entityHandler.addDoc
 				.calledWith(@stubbedNewProject, @stubbedNewProject.rootFolder[0]._id, @doc0.name, @doc0_lines, @owner._id)
 				.should.equal true
-			@entityHandler.addDocWithProject
+			@entityHandler.addDoc
 				.calledWith(@stubbedNewProject, @newFolder._id, @doc1.name, @doc1_lines, @owner._id)
 				.should.equal true
-			@entityHandler.addDocWithProject
+			@entityHandler.addDoc
 				.calledWith(@stubbedNewProject, @newFolder._id, @doc2.name, @doc2_lines, @owner._id)
 				.should.equal true
 			done()
diff --git a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
index f62690e226..75d19f459c 100644
--- a/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
+++ b/services/web/test/unit/coffee/Project/ProjectEntityHandlerTests.coffee
@@ -496,6 +496,51 @@ describe 'ProjectEntityHandler', ->
 				.calledWith(project_id, userId, {newDocs})
 				.should.equal true
 
+	describe 'addDocWithoutUpdatingHistory', ->
+		beforeEach ->
+			@name = "some new doc"
+			@lines = ['1234','abc']
+			@path = "/path/to/doc"
+
+			@ProjectEntityHandler._putElement = sinon.stub().callsArgWith(4, null, {path:{fileSystem:@path}})
+			@callback = sinon.stub()
+			@tpdsUpdateSender.addDoc = sinon.stub().callsArg(1)
+			@DocstoreManager.updateDoc = sinon.stub().yields(null, true, 0)
+
+			@ProjectEntityHandler.addDocWithoutUpdatingHistory project_id, folder_id, @name, @lines, userId, @callback
+
+			# Created doc
+			@doc = @ProjectEntityHandler._putElement.args[0][2]
+			@doc.name.should.equal @name
+			expect(@doc.lines).to.be.undefined
+
+		it 'should call put element', ->
+			@ProjectEntityHandler._putElement.calledWith(@project, folder_id, @doc).should.equal true
+
+		it 'should return doc and parent folder', ->
+			@callback.calledWith(null, @doc, folder_id).should.equal true
+
+		it 'should call third party data store', ->
+			@tpdsUpdateSender.addDoc
+				.calledWith({
+					project_id: project_id
+					doc_id: doc_id
+					path: @path
+					project_name: @project.name
+					rev: 0
+				})
+				.should.equal true
+
+		it "should send the doc lines to the doc store", ->
+			@DocstoreManager.updateDoc
+				.calledWith(project_id, @doc._id.toString(), @lines)
+				.should.equal true
+
+		it "should not should send the change in project structure to the doc updater", () ->
+			@documentUpdaterHandler.updateProjectStructure
+				.called
+				.should.equal false
+
 	describe "restoreDoc", ->
 		beforeEach ->
 			@name = "doc-name"
@@ -584,6 +629,12 @@ describe 'ProjectEntityHandler', ->
 
 			@ProjectEntityHandler.addFile project_id, folder_id, fileName, {}, userId, () ->
 
+		it "should not send the change in project structure to the doc updater when called as addFileWithoutUpdatingHistory", (done) ->
+			@documentUpdaterHandler.updateProjectStructure = sinon.stub().yields()
+			@ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, {}, userId, () =>
+				@documentUpdaterHandler.updateProjectStructure.called.should.equal false
+				done()
+
 	describe 'replaceFile', ->
 		beforeEach ->
 			@projectLocator

From 731cd57250b30911f775bd548a1dfd2a2ddefc6f Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Tue, 19 Dec 2017 11:52:15 +0000
Subject: [PATCH 105/109] Make intermediate argument signature clearer

---
 .../Project/ProjectEntityHandler.coffee        | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
index 180c080b2c..941bc5e988 100644
--- a/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
+++ b/services/web/app/coffee/Features/Project/ProjectEntityHandler.coffee
@@ -150,11 +150,11 @@ module.exports = ProjectEntityHandler =
 			DocstoreManager.getDoc project_id, doc_id, options, callback
 
 	addDoc: (project_or_id, folder_id, docName, docLines, userId, callback = (error, doc, folder_id) ->)=>
-		ProjectEntityHandler.addDocWithoutUpdatingHistory project_or_id, folder_id, docName, docLines, userId, (error, doc, folder_id, result) ->
+		ProjectEntityHandler.addDocWithoutUpdatingHistory project_or_id, folder_id, docName, docLines, userId, (error, doc, folder_id, path) ->
 			return callback(error) if error?
 			newDocs = [
 				doc: doc
-				path: result?.path?.fileSystem
+				path: path
 				docLines: docLines.join('\n')
 			]
 			project_id = project_or_id._id or project_or_id
@@ -177,7 +177,7 @@ module.exports = ProjectEntityHandler =
 				return callback(err)
 			ProjectEntityHandler._addDocWithProject project, folder_id, docName, docLines, userId, callback
 
-	_addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id, result) ->)=>
+	_addDocWithProject: (project, folder_id, docName, docLines, userId, callback = (error, doc, folder_id, path) ->)=>
 		project_id = project._id
 		logger.log project_id: project_id, folder_id: folder_id, doc_name: docName, "adding doc to project with project"
 		confirmFolder project, folder_id, (folder_id)=>
@@ -197,7 +197,7 @@ module.exports = ProjectEntityHandler =
 						rev:          0
 					}, (err) ->
 						return callback(err) if err?
-						callback(null, doc, folder_id, result)
+						callback(null, doc, folder_id, result?.path?.fileSystem)
 
 	restoreDoc: (project_id, doc_id, name, callback = (error, doc, folder_id) ->) ->
 		# getDoc will return the deleted doc's lines, but we don't actually remove
@@ -206,7 +206,7 @@ module.exports = ProjectEntityHandler =
 			return callback(error) if error?
 			ProjectEntityHandler.addDoc project_id, null, name, lines, callback
 
-	addFileWithoutUpdatingHistory: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id, result, fileStoreUrl) ->)->
+	addFileWithoutUpdatingHistory: (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id, path, fileStoreUrl) ->)->
 		ProjectGetter.getProjectWithOnlyFolders project_id, (err, project) ->
 			if err?
 				logger.err project_id:project_id, err:err, "error getting project for add file"
@@ -225,13 +225,13 @@ module.exports = ProjectEntityHandler =
 							return callback(err)
 						tpdsUpdateSender.addFile {project_id:project._id, file_id:fileRef._id, path:result?.path?.fileSystem, project_name:project.name, rev:fileRef.rev}, (err) ->
 							return callback(err) if err?
-							callback(null, fileRef, folder_id, result, fileStoreUrl)
+							callback(null, fileRef, folder_id, result?.path?.fileSystem, fileStoreUrl)
 
-	addFile:  (project_id, folder_id, fileName, path, userId, callback = (error, fileRef, folder_id) ->)->
-		ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, path, userId, (error, fileRef, folder_id, result, fileStoreUrl) ->
+	addFile:  (project_id, folder_id, fileName, fsPath, userId, callback = (error, fileRef, folder_id) ->)->
+		ProjectEntityHandler.addFileWithoutUpdatingHistory project_id, folder_id, fileName, fsPath, userId, (error, fileRef, folder_id, path, fileStoreUrl) ->
 			newFiles = [
 				file: fileRef
-				path: result?.path?.fileSystem
+				path: path
 				url: fileStoreUrl
 			]
 			DocumentUpdaterHandler.updateProjectStructure project_id, userId, {newFiles}, (error) ->

From 2d8b47295f7a245575bceca62daf822a218f5a8f Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Tue, 19 Dec 2017 11:53:17 +0000
Subject: [PATCH 106/109] Build from module branch

---
 services/web/Jenkinsfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile
index 152d980787..84a143c5d7 100644
--- a/services/web/Jenkinsfile
+++ b/services/web/Jenkinsfile
@@ -43,7 +43,7 @@ pipeline {
         checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/learn-wiki'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/learn-wiki-web-module.git']]])
         checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/templates'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/templates-webmodule.git']]])
         checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]])
-        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-integration'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-integration-web-module.git']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/ja-dont-send-history-updates-on-import']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-integration'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-integration-web-module.git']]])
         checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-account-merge'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-account-merge.git']]])
       }
     }

From 1653de9301b25c1e8b6577b62eee3530df4813b3 Mon Sep 17 00:00:00 2001
From: James Allen 
Date: Tue, 19 Dec 2017 15:01:17 +0000
Subject: [PATCH 107/109] Revert "Build from module branch"

This reverts commit 34d4b72ddd8b16bd34013e7bea9459940fbf0032.
---
 services/web/Jenkinsfile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/services/web/Jenkinsfile b/services/web/Jenkinsfile
index 84a143c5d7..152d980787 100644
--- a/services/web/Jenkinsfile
+++ b/services/web/Jenkinsfile
@@ -43,7 +43,7 @@ pipeline {
         checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/learn-wiki'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@bitbucket.org:sharelatex/learn-wiki-web-module.git']]])
         checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/templates'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/templates-webmodule.git']]])
         checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/track-changes'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/track-changes-web-module.git']]])
-        checkout([$class: 'GitSCM', branches: [[name: '*/ja-dont-send-history-updates-on-import']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-integration'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-integration-web-module.git']]])
+        checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-integration'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-integration-web-module.git']]])
         checkout([$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'modules/overleaf-account-merge'], [$class: 'CloneOption', shallow: true]], userRemoteConfigs: [[credentialsId: 'GIT_DEPLOY_KEY', url: 'git@github.com:sharelatex/overleaf-account-merge.git']]])
       }
     }

From 8d1736af596990a194cd5dbed2272f1042fc97fc Mon Sep 17 00:00:00 2001
From: Paulo Reis 
Date: Wed, 20 Dec 2017 09:51:32 +0000
Subject: [PATCH 108/109] Fix forgotten conflict.

---
 services/web/public/stylesheets/core/ol-variables.less | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less
index 8433c374b8..7a19278500 100644
--- a/services/web/public/stylesheets/core/ol-variables.less
+++ b/services/web/public/stylesheets/core/ol-variables.less
@@ -201,10 +201,7 @@
 @toolbar-icon-btn-hover-shadow    : none;
 @toolbar-border-bottom            : 1px solid @toolbar-border-color;
 @toolbar-icon-btn-hover-boxshadow : none;
-<<<<<<< HEAD
 @toolbar-font-size                : 13px;
-=======
->>>>>>> master
 
 // Editor file-tree
 @file-tree-bg                   : @ol-blue-gray-4;

From 1be9e29f356e67dbed1ec3814267a997280db83b Mon Sep 17 00:00:00 2001
From: Alasdair Smith 
Date: Thu, 21 Dec 2017 11:10:23 +0000
Subject: [PATCH 109/109] Switch from button to link to fit better

---
 services/web/app/views/project/list.pug       |  4 ++--
 .../stylesheets/app/sidebar-v2-dash-pane.less | 19 ++-----------------
 2 files changed, 4 insertions(+), 19 deletions(-)

diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug
index 69e791411c..0cd67d473c 100644
--- a/services/web/app/views/project/list.pug
+++ b/services/web/app/views/project/list.pug
@@ -65,9 +65,9 @@ block content
 
 					if isShowingV1Projects && settings.overleaf && settings.overleaf.host
 						.project-list-sidebar-v2-pane.col-md-2.col-xs-3
-							span Welcome to the Overleaf v2 alpha! #[a.project-list-sidebar-faq(href="https://www.overleaf.com/help/v2") Find out more].
+							span Welcome to the Overleaf v2 alpha! #[a(href="https://www.overleaf.com/help/v2") Find out more].
 							span To tag or rename your v1 projects, please go back to Overleaf v1.
-							a.project-list-sidebar-v1-link.btn.btn-default(
+							a.project-list-sidebar-v1-link(
 								href=settings.overleaf.host + "/dash?prefer-v1-dash=1"
 							) Go back to v1
 
diff --git a/services/web/public/stylesheets/app/sidebar-v2-dash-pane.less b/services/web/public/stylesheets/app/sidebar-v2-dash-pane.less
index 6adb952bed..d1586b4a5c 100644
--- a/services/web/public/stylesheets/app/sidebar-v2-dash-pane.less
+++ b/services/web/public/stylesheets/app/sidebar-v2-dash-pane.less
@@ -17,26 +17,11 @@
 	font-size: 13px;
 }
 
-	.project-list-sidebar-faq {
+	.project-list-sidebar-v2-pane a {
 		color: @v2-dash-pane-link-color;
 		text-decoration: underline;
 	}
 
-	.project-list-sidebar-faq:hover {
-		color: @v2-dash-pane-link-color;
+	.project-list-sidebar-v2-pane a:hover {
 		text-decoration: none;
 	}
-
-	.project-list-sidebar-v1-link {
-		display: block;
-		margin-left: auto;
-		margin-right: auto;
-		padding-top: 0;
-		padding-bottom: 0;
-		background-color: @v2-dash-pane-btn-bg;
-		font-size: 13px;
-	}
-
-	.project-list-sidebar-v1-link:hover {
-		background-color: @v2-dash-pane-btn-hover-bg;
-	}
\ No newline at end of file