From 1c9abd35f8e6aa4d932655fad148c636246f03d5 Mon Sep 17 00:00:00 2001 From: James Allen Date: Wed, 5 Oct 2016 11:04:39 +0100 Subject: [PATCH 1/3] Rename track-changes -> history on the client Conflicts: app/views/project/editor/history.jade --- services/web/app/views/project/editor.jade | 2 +- .../app/views/project/editor/file-tree.jade | 4 +- .../web/app/views/project/editor/header.jade | 12 ++-- .../{track-changes.jade => history.jade} | 54 +++++++-------- services/web/public/coffee/ide.coffee | 6 +- .../HistoryManager.coffee} | 66 +++++++++---------- .../controllers/HistoryDiffController.coffee | 46 +++++++++++++ .../controllers/HistoryListController.coffee} | 44 ++++++------- .../directives/infiniteScroll.coffee | 0 .../TrackChangesDiffController.coffee | 46 ------------- .../web/public/stylesheets/app/editor.less | 4 +- .../{track-changes.less => history.less} | 4 +- 12 files changed, 144 insertions(+), 144 deletions(-) rename services/web/app/views/project/editor/{track-changes.jade => history.jade} (76%) rename services/web/public/coffee/ide/{track-changes/TrackChangesManager.coffee => history/HistoryManager.coffee} (76%) create mode 100644 services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee rename services/web/public/coffee/ide/{track-changes/controllers/TrackChangesListController.coffee => history/controllers/HistoryListController.coffee} (69%) rename services/web/public/coffee/ide/{track-changes => history}/directives/infiniteScroll.coffee (100%) delete mode 100644 services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee rename services/web/public/stylesheets/app/editor/{track-changes.less => history.less} (99%) diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index b5463c647d..d7cd72989e 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -66,7 +66,7 @@ block content .ui-layout-center include ./editor/editor include ./editor/binary-file - include ./editor/track-changes + include ./editor/history include ./editor/publish-template .ui-layout-east diff --git a/services/web/app/views/project/editor/file-tree.jade b/services/web/app/views/project/editor/file-tree.jade index b32bdbed23..92af8b627d 100644 --- a/services/web/app/views/project/editor/file-tree.jade +++ b/services/web/app/views/project/editor/file-tree.jade @@ -70,13 +70,13 @@ aside#file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected' ng-repeat="entity in rootFolder.children | orderBy:[orderByFoldersFirst, 'name']" ) - li(ng-show="deletedDocs.length > 0 && ui.view == 'track-changes'") + li(ng-show="deletedDocs.length > 0 && ui.view == 'history'") h3 #{translate("deleted_files")} li( ng-class="{ 'selected': entity.selected }", ng-repeat="entity in deletedDocs | orderBy:'name'", ng-controller="FileTreeEntityController", - ng-show="ui.view == 'track-changes'" + ng-show="ui.view == 'history'" ) .entity .entity-name( diff --git a/services/web/app/views/project/editor/header.jade b/services/web/app/views/project/editor/header.jade index db691265b6..02cec9d2ca 100644 --- a/services/web/app/views/project/editor/header.jade +++ b/services/web/app/views/project/editor/header.jade @@ -102,8 +102,8 @@ div(ng-if="!shouldABTestHeaderLabels") i.fa.fa-fw.fa-group a.btn.btn-full-height( href, - ng-click="toggleTrackChanges()", - ng-class="{ active: (ui.view == 'track-changes') }" + ng-click="toggleHistory()", + ng-class="{ active: (ui.view == 'history') }" tooltip="#{translate('recent_changes')}", tooltip-placement="bottom", ) @@ -232,8 +232,8 @@ div(ng-if="shouldABTestHeaderLabels") i.fa.fa-fw.fa-group a.btn.btn-full-height( href, - ng-click="toggleTrackChanges(); trackABTestConversion('history');", - ng-class="{ active: (ui.view == 'track-changes') }" + ng-click="toggleHistory(); trackABTestConversion('history');", + ng-class="{ active: (ui.view == 'history') }" tooltip="#{translate('recent_changes')}", tooltip-placement="bottom", sixpack-convert="editor-header" @@ -357,8 +357,8 @@ div(ng-if="shouldABTestHeaderLabels") p.toolbar-label #{translate("share")} a.btn.btn-full-height( href, - ng-click="toggleTrackChanges(); trackABTestConversion('history');", - ng-class="{ active: (ui.view == 'track-changes') }", + ng-click="toggleHistory(); trackABTestConversion('history');", + ng-class="{ active: (ui.view == 'history') }", sixpack-convert="editor-header" ) i.fa.fa-fw.fa-history diff --git a/services/web/app/views/project/editor/track-changes.jade b/services/web/app/views/project/editor/history.jade similarity index 76% rename from services/web/app/views/project/editor/track-changes.jade rename to services/web/app/views/project/editor/history.jade index de3c0e1730..9cf756c344 100644 --- a/services/web/app/views/project/editor/track-changes.jade +++ b/services/web/app/views/project/editor/history.jade @@ -1,5 +1,5 @@ -div#trackChanges(ng-show="ui.view == 'track-changes'") - span(ng-controller="TrackChangesPremiumPopup") +div#history(ng-show="ui.view == 'history'") + span(ng-controller="HistoryPremiumPopup") .upgrade-prompt(ng-show="!project.features.versioning") .message(ng-show="project.owner._id == user.id") p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})} @@ -33,29 +33,29 @@ div#trackChanges(ng-show="ui.view == 'track-changes'") a.btn.btn-success( href ng-class="buttonClass" - ng-click="startFreeTrial('track-changes')" + ng-click="startFreeTrial('history')" ) #{translate("start_free_trial")} .message(ng-show="project.owner._id != user.id") p #{translate("ask_proj_owner_to_upgrade_for_history")} p - a.small(href, ng-click="toggleTrackChanges()") #{translate("cancel")} + a.small(href, ng-click="toggleHistory()") #{translate("cancel")} aside.change-list( - ng-controller="TrackChangesListController" + ng-controller="HistoryListController" infinite-scroll="loadMore()" - infinite-scroll-disabled="trackChanges.loading || trackChanges.atEnd" - infinite-scroll-initialize="ui.view == 'track-changes'" + infinite-scroll-disabled="history.loading || history.atEnd" + infinite-scroll-initialize="ui.view == 'history'" ) .infinite-scroll-inner ul.list-unstyled( ng-class="{\ - 'hover-state': trackChanges.hoveringOverListSelectors\ + 'hover-state': history.hoveringOverListSelectors\ }" ) li.change( - ng-repeat="update in trackChanges.updates" + ng-repeat="update in history.updates" ng-class="{\ 'first-in-day': update.meta.first_in_day,\ 'selected': update.inSelection,\ @@ -65,7 +65,7 @@ div#trackChanges(ng-show="ui.view == 'track-changes'") 'hover-selected-to': update.hoverSelectedTo,\ 'hover-selected-from': update.hoverSelectedFrom,\ }" - ng-controller="TrackChangesListItemController" + ng-controller="HistoryListItemController" ) div.day(ng-show="update.meta.first_in_day") {{ update.meta.end_ts | relativeDate }} @@ -108,55 +108,55 @@ div#trackChanges(ng-show="ui.view == 'track-changes'") .color-square(style="background-color: hsl(100, 100%, 50%)") span #{translate("anonymous")} - .loading(ng-show="trackChanges.loading") + .loading(ng-show="history.loading") i.fa.fa-spin.fa-refresh |    #{translate("loading")}... - .diff-panel.full-size(ng-controller="TrackChangesDiffController") + .diff-panel.full-size(ng-controller="HistoryDiffController") .diff( - ng-show="!!trackChanges.diff && !trackChanges.diff.loading && !trackChanges.diff.deleted && !trackChanges.diff.error" + ng-show="!!history.diff && !history.diff.loading && !history.diff.deleted && !history.diff.error" ) .toolbar.toolbar-alt span.name - | {{trackChanges.diff.highlights.length}} + | {{history.diff.highlights.length}} ng-pluralize( - count="trackChanges.diff.highlights.length", + count="history.diff.highlights.length", when="{\ 'one': 'change',\ 'other': 'changes'\ }" ) - | in {{trackChanges.diff.doc.name}} + | in {{history.diff.doc.name}} .toolbar-right a.btn.btn-danger.btn-sm( href, ng-click="openRestoreDiffModal()" ) #{translate("restore_to_before_these_changes")} .diff-editor.hide-ace-cursor( - ace-editor="track-changes", + ace-editor="history", theme="settings.theme", font-size="settings.fontSize", - text="trackChanges.diff.text", - highlights="trackChanges.diff.highlights", + text="history.diff.text", + highlights="history.diff.highlights", read-only="true", resize-on="layout:main:resize", navigate-highlights="true" ) .diff-deleted.text-centered( - ng-show="trackChanges.diff.deleted && !trackChanges.diff.restoreDeletedSuccess" + ng-show="history.diff.deleted && !history.diff.restoreDeletedSuccess" ) - p.text-serif #{translate("file_has_been_deleted", {filename:"{{ trackChanges.diff.doc.name }} "})} + p.text-serif #{translate("file_has_been_deleted", {filename:"{{ history.diff.doc.name }} "})} p a.btn.btn-primary.btn-lg( href, ng-click="restoreDeletedDoc()", - ng-disabled="trackChanges.diff.restoreInProgress" + ng-disabled="history.diff.restoreInProgress" ) #{translate("restore")} .diff-deleted.text-centered( - ng-show="trackChanges.diff.deleted && trackChanges.diff.restoreDeletedSuccess" + ng-show="history.diff.deleted && history.diff.restoreDeletedSuccess" ) - p.text-serif #{translate("file_restored", {filename:"{{ trackChanges.diff.doc.name }} "})} + p.text-serif #{translate("file_restored", {filename:"{{ history.diff.doc.name }} "})} p.text-serif #{translate("file_restored_back_to_editor")} p a.btn.btn-default( @@ -164,13 +164,13 @@ div#trackChanges(ng-show="ui.view == 'track-changes'") ng-click="backToEditorAfterRestore()", ) #{translate("file_restored_back_to_editor_btn")} - .loading-panel(ng-show="trackChanges.diff.loading") + .loading-panel(ng-show="history.diff.loading") i.fa.fa-spin.fa-refresh |   #{translate("loading")}... - .error-panel(ng-show="trackChanges.diff.error") + .error-panel(ng-show="history.diff.error") .alert.alert-danger #{translate("generic_something_went_wrong")} -script(type="text/ng-template", id="trackChangesRestoreDiffModalTemplate") +script(type="text/ng-template", id="historyRestoreDiffModalTemplate") .modal-header button.close( type="button" diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee index 01f7363c80..607b8556aa 100644 --- a/services/web/public/coffee/ide.coffee +++ b/services/web/public/coffee/ide.coffee @@ -4,7 +4,7 @@ define [ "ide/connection/ConnectionManager" "ide/editor/EditorManager" "ide/online-users/OnlineUsersManager" - "ide/track-changes/TrackChangesManager" + "ide/history/HistoryManager" "ide/permissions/PermissionsManager" "ide/pdf/PdfManager" "ide/binary-files/BinaryFilesManager" @@ -36,7 +36,7 @@ define [ ConnectionManager EditorManager OnlineUsersManager - TrackChangesManager + HistoryManager PermissionsManager PdfManager BinaryFilesManager @@ -116,7 +116,7 @@ define [ ide.fileTreeManager = new FileTreeManager(ide, $scope) ide.editorManager = new EditorManager(ide, $scope) ide.onlineUsersManager = new OnlineUsersManager(ide, $scope) - ide.trackChangesManager = new TrackChangesManager(ide, $scope) + 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/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/history/HistoryManager.coffee similarity index 76% rename from services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee rename to services/web/public/coffee/ide/history/HistoryManager.coffee index ecd3b5a7f8..cb500246de 100644 --- a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/history/HistoryManager.coffee @@ -1,30 +1,30 @@ define [ - "ide/track-changes/controllers/TrackChangesListController" - "ide/track-changes/controllers/TrackChangesDiffController" - "ide/track-changes/directives/infiniteScroll" + "ide/history/controllers/HistoryListController" + "ide/history/controllers/HistoryDiffController" + "ide/history/directives/infiniteScroll" ], () -> - class TrackChangesManager + class HistoryManager constructor: (@ide, @$scope) -> @reset() - @$scope.toggleTrackChanges = () => - if @$scope.ui.view == "track-changes" + @$scope.toggleHistory = () => + if @$scope.ui.view == "history" @hide() else @show() - @$scope.$watch "trackChanges.selection.updates", (updates) => + @$scope.$watch "history.selection.updates", (updates) => if updates? and updates.length > 0 @_selectDocFromUpdates() @reloadDiff() @$scope.$on "entity:selected", (event, entity) => - if (@$scope.ui.view == "track-changes") and (entity.type == "doc") - @$scope.trackChanges.selection.doc = entity + if (@$scope.ui.view == "history") and (entity.type == "doc") + @$scope.history.selection.doc = entity @reloadDiff() show: () -> - @$scope.ui.view = "track-changes" + @$scope.ui.view = "history" @reset() hide: () -> @@ -33,7 +33,7 @@ define [ @$scope.$emit "entity:selected", @ide.fileTreeManager.findSelectedEntity() reset: () -> - @$scope.trackChanges = { + @$scope.history = { updates: [] nextBeforeTimestamp: null atEnd: false @@ -51,36 +51,36 @@ define [ } autoSelectRecentUpdates: () -> - return if @$scope.trackChanges.updates.length == 0 + return if @$scope.history.updates.length == 0 - @$scope.trackChanges.updates[0].selectedTo = true + @$scope.history.updates[0].selectedTo = true indexOfLastUpdateNotByMe = 0 - for update, i in @$scope.trackChanges.updates + for update, i in @$scope.history.updates if @_updateContainsUserId(update, @$scope.user.id) break indexOfLastUpdateNotByMe = i - @$scope.trackChanges.updates[indexOfLastUpdateNotByMe].selectedFrom = true + @$scope.history.updates[indexOfLastUpdateNotByMe].selectedFrom = true BATCH_SIZE: 10 fetchNextBatchOfUpdates: () -> url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}" - if @$scope.trackChanges.nextBeforeTimestamp? - url += "&before=#{@$scope.trackChanges.nextBeforeTimestamp}" - @$scope.trackChanges.loading = true + if @$scope.history.nextBeforeTimestamp? + url += "&before=#{@$scope.history.nextBeforeTimestamp}" + @$scope.history.loading = true @ide.$http .get(url) .success (data) => @_loadUpdates(data.updates) - @$scope.trackChanges.nextBeforeTimestamp = data.nextBeforeTimestamp + @$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp if !data.nextBeforeTimestamp? - @$scope.trackChanges.atEnd = true - @$scope.trackChanges.loading = false + @$scope.history.atEnd = true + @$scope.history.loading = false reloadDiff: () -> - diff = @$scope.trackChanges.diff - {updates, doc} = @$scope.trackChanges.selection + diff = @$scope.history.diff + {updates, doc} = @$scope.history.selection {fromV, toV, start_ts, end_ts} = @_calculateRangeFromSelection() return if !doc? @@ -90,7 +90,7 @@ define [ diff.fromV == fromV and diff.toV == toV - @$scope.trackChanges.diff = diff = { + @$scope.history.diff = diff = { fromV: fromV toV: toV start_ts: start_ts @@ -183,7 +183,7 @@ define [ return {text, highlights} _loadUpdates: (updates = []) -> - previousUpdate = @$scope.trackChanges.updates[@$scope.trackChanges.updates.length - 1] + previousUpdate = @$scope.history.updates[@$scope.history.updates.length - 1] for update in updates for doc_id, doc of update.docs or {} @@ -202,19 +202,19 @@ define [ previousUpdate = update - firstLoad = @$scope.trackChanges.updates.length == 0 + firstLoad = @$scope.history.updates.length == 0 - @$scope.trackChanges.updates = - @$scope.trackChanges.updates.concat(updates) + @$scope.history.updates = + @$scope.history.updates.concat(updates) @autoSelectRecentUpdates() if firstLoad _calculateRangeFromSelection: () -> fromV = toV = start_ts = end_ts = null - selected_doc_id = @$scope.trackChanges.selection.doc?.id + selected_doc_id = @$scope.history.selection.doc?.id - for update in @$scope.trackChanges.selection.updates or [] + 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? @@ -236,11 +236,11 @@ define [ # then prefer this one if present. _selectDocFromUpdates: () -> affected_docs = {} - for update in @$scope.trackChanges.selection.updates + for update in @$scope.history.selection.updates for doc_id, doc of update.docs affected_docs[doc_id] = doc.entity - selected_doc = @$scope.trackChanges.selection.doc + selected_doc = @$scope.history.selection.doc if selected_doc? and affected_docs[selected_doc.id]? # Selected doc is already open else @@ -248,7 +248,7 @@ define [ selected_doc = doc break - @$scope.trackChanges.selection.doc = selected_doc + @$scope.history.selection.doc = selected_doc @ide.fileTreeManager.selectEntity(selected_doc) _updateContainsUserId: (update, user_id) -> diff --git a/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee new file mode 100644 index 0000000000..d32155395e --- /dev/null +++ b/services/web/public/coffee/ide/history/controllers/HistoryDiffController.coffee @@ -0,0 +1,46 @@ +define [ + "base" +], (App) -> + App.controller "HistoryDiffController", ($scope, $modal, ide, event_tracking) -> + $scope.restoreDeletedDoc = () -> + event_tracking.sendMB "history-restore-deleted" + $scope.history.diff.restoreInProgress = true + ide.historyManager + .restoreDeletedDoc( + $scope.history.diff.doc + ) + .success (response) -> + $scope.history.diff.restoredDocNewId = response.doc_id + $scope.history.diff.restoreInProgress = false + $scope.history.diff.restoreDeletedSuccess = true + + $scope.openRestoreDiffModal = () -> + event_tracking.sendMB "history-restore-modal" + $modal.open { + templateUrl: "historyRestoreDiffModalTemplate" + controller: "HistoryRestoreDiffModalController" + resolve: + diff: () -> $scope.history.diff + } + + $scope.backToEditorAfterRestore = () -> + ide.editorManager.openDoc({ id: $scope.history.diff.restoredDocNewId }) + + App.controller "HistoryRestoreDiffModalController", ($scope, $modalInstance, diff, ide, event_tracking) -> + $scope.state = + inflight: false + + $scope.diff = diff + + $scope.restore = () -> + event_tracking.sendMB "history-restored" + $scope.state.inflight = true + ide.historyManager + .restoreDiff(diff) + .success () -> + $scope.state.inflight = false + $modalInstance.close() + ide.editorManager.openDoc(diff.doc) + + $scope.cancel = () -> + $modalInstance.dismiss() diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee similarity index 69% rename from services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee rename to services/web/public/coffee/ide/history/controllers/HistoryListController.coffee index 3511ddf6c1..d3ca0c50f2 100644 --- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryListController.coffee @@ -2,26 +2,26 @@ define [ "base" ], (App) -> - App.controller "TrackChangesPremiumPopup", ($scope, ide, sixpack)-> + App.controller "HistoryPremiumPopup", ($scope, ide, sixpack)-> $scope.$watch "ui.view", -> - if $scope.ui.view == "track-changes" + if $scope.ui.view == "history" if $scope.project?.features?.versioning $scope.versioningPopupType = "default" - else if $scope.ui.view == "track-changes" - sixpack.participate 'track-changes-discount', ['default', 'discount'], (chosenVariation, rawResponse)-> + else if $scope.ui.view == "history" + sixpack.participate 'history-discount', ['default', 'discount'], (chosenVariation, rawResponse)-> $scope.versioningPopupType = chosenVariation - App.controller "TrackChangesListController", ["$scope", "ide", ($scope, ide) -> + App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) -> $scope.hoveringOverListSelectors = false $scope.loadMore = () => - ide.trackChangesManager.fetchNextBatchOfUpdates() + ide.historyManager.fetchNextBatchOfUpdates() $scope.recalculateSelectedUpdates = () -> beforeSelection = true afterSelection = false - $scope.trackChanges.selection.updates = [] - for update in $scope.trackChanges.updates + $scope.history.selection.updates = [] + for update in $scope.history.updates if update.selectedTo inSelection = true beforeSelection = false @@ -31,7 +31,7 @@ define [ update.afterSelection = afterSelection if inSelection - $scope.trackChanges.selection.updates.push update + $scope.history.selection.updates.push update if update.selectedFrom inSelection = false @@ -40,7 +40,7 @@ define [ $scope.recalculateHoveredUpdates = () -> hoverSelectedFrom = false hoverSelectedTo = false - for update in $scope.trackChanges.updates + for update in $scope.history.updates # Figure out whether the to or from selector is hovered over if update.hoverSelectedFrom hoverSelectedFrom = true @@ -50,7 +50,7 @@ define [ if hoverSelectedFrom # We want to 'hover select' everything between hoverSelectedFrom and selectedTo inHoverSelection = false - for update in $scope.trackChanges.updates + for update in $scope.history.updates if update.selectedTo update.hoverSelectedTo = true inHoverSelection = true @@ -60,7 +60,7 @@ define [ if hoverSelectedTo # We want to 'hover select' everything between hoverSelectedTo and selectedFrom inHoverSelection = false - for update in $scope.trackChanges.updates + for update in $scope.history.updates if update.hoverSelectedTo inHoverSelection = true update.inHoverSelection = inHoverSelection @@ -69,49 +69,49 @@ define [ inHoverSelection = false $scope.resetHoverState = () -> - for update in $scope.trackChanges.updates + for update in $scope.history.updates delete update.hoverSelectedFrom delete update.hoverSelectedTo delete update.inHoverSelection - $scope.$watch "trackChanges.updates.length", () -> + $scope.$watch "history.updates.length", () -> $scope.recalculateSelectedUpdates() ] - App.controller "TrackChangesListItemController", ["$scope", "event_tracking", ($scope, event_tracking) -> + App.controller "HistoryListItemController", ["$scope", "event_tracking", ($scope, event_tracking) -> $scope.$watch "update.selectedFrom", (selectedFrom, oldSelectedFrom) -> if selectedFrom - for update in $scope.trackChanges.updates + for update in $scope.history.updates update.selectedFrom = false unless update == $scope.update $scope.recalculateSelectedUpdates() $scope.$watch "update.selectedTo", (selectedTo, oldSelectedTo) -> if selectedTo - for update in $scope.trackChanges.updates + for update in $scope.history.updates update.selectedTo = false unless update == $scope.update $scope.recalculateSelectedUpdates() $scope.select = () -> - event_tracking.sendMB "track-changes-view-change" + event_tracking.sendMB "history-view-change" $scope.update.selectedTo = true $scope.update.selectedFrom = true $scope.mouseOverSelectedFrom = () -> - $scope.trackChanges.hoveringOverListSelectors = true + $scope.history.hoveringOverListSelectors = true $scope.update.hoverSelectedFrom = true $scope.recalculateHoveredUpdates() $scope.mouseOutSelectedFrom = () -> - $scope.trackChanges.hoveringOverListSelectors = false + $scope.history.hoveringOverListSelectors = false $scope.resetHoverState() $scope.mouseOverSelectedTo = () -> - $scope.trackChanges.hoveringOverListSelectors = true + $scope.history.hoveringOverListSelectors = true $scope.update.hoverSelectedTo = true $scope.recalculateHoveredUpdates() $scope.mouseOutSelectedTo = () -> - $scope.trackChanges.hoveringOverListSelectors = false + $scope.history.hoveringOverListSelectors = false $scope.resetHoverState() $scope.displayName = (user) -> diff --git a/services/web/public/coffee/ide/track-changes/directives/infiniteScroll.coffee b/services/web/public/coffee/ide/history/directives/infiniteScroll.coffee similarity index 100% rename from services/web/public/coffee/ide/track-changes/directives/infiniteScroll.coffee rename to services/web/public/coffee/ide/history/directives/infiniteScroll.coffee diff --git a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee b/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee deleted file mode 100644 index a32acfb2b3..0000000000 --- a/services/web/public/coffee/ide/track-changes/controllers/TrackChangesDiffController.coffee +++ /dev/null @@ -1,46 +0,0 @@ -define [ - "base" -], (App) -> - App.controller "TrackChangesDiffController", ($scope, $modal, ide, event_tracking) -> - $scope.restoreDeletedDoc = () -> - event_tracking.sendMB "track-changes-restore-deleted" - $scope.trackChanges.diff.restoreInProgress = true - ide.trackChangesManager - .restoreDeletedDoc( - $scope.trackChanges.diff.doc - ) - .success (response) -> - $scope.trackChanges.diff.restoredDocNewId = response.doc_id - $scope.trackChanges.diff.restoreInProgress = false - $scope.trackChanges.diff.restoreDeletedSuccess = true - - $scope.openRestoreDiffModal = () -> - event_tracking.sendMB "track-changes-restore-modal" - $modal.open { - templateUrl: "trackChangesRestoreDiffModalTemplate" - controller: "TrackChangesRestoreDiffModalController" - resolve: - diff: () -> $scope.trackChanges.diff - } - - $scope.backToEditorAfterRestore = () -> - ide.editorManager.openDoc({ id: $scope.trackChanges.diff.restoredDocNewId }) - - App.controller "TrackChangesRestoreDiffModalController", ($scope, $modalInstance, diff, ide, event_tracking) -> - $scope.state = - inflight: false - - $scope.diff = diff - - $scope.restore = () -> - event_tracking.sendMB "track-changes-restored" - $scope.state.inflight = true - ide.trackChangesManager - .restoreDiff(diff) - .success () -> - $scope.state.inflight = false - $modalInstance.close() - ide.editorManager.openDoc(diff.doc) - - $scope.cancel = () -> - $modalInstance.dismiss() diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 2db26059c4..48b8b424ed 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -1,5 +1,5 @@ @import "./editor/file-tree.less"; -@import "./editor/track-changes.less"; +@import "./editor/history.less"; @import "./editor/toolbar.less"; @import "./editor/left-menu.less"; @import "./editor/pdf.less"; @@ -422,4 +422,4 @@ .dropbox-teaser-video { width: 100%; height: auto; -} \ No newline at end of file +} diff --git a/services/web/public/stylesheets/app/editor/track-changes.less b/services/web/public/stylesheets/app/editor/history.less similarity index 99% rename from services/web/public/stylesheets/app/editor/track-changes.less rename to services/web/public/stylesheets/app/editor/history.less index 6cf98e53eb..cb88be62c1 100644 --- a/services/web/public/stylesheets/app/editor/track-changes.less +++ b/services/web/public/stylesheets/app/editor/history.less @@ -8,7 +8,7 @@ @range-bar-color: @link-color; @range-bar-selected-offset: 14px; -#trackChanges { +#history { .upgrade-prompt { position: absolute; top: 0; @@ -272,7 +272,7 @@ } .editor-dark { - #trackChanges { + #history { aside.change-list { border-color: @editor-dark-toolbar-border-color; From 0995ba5ee6f6647c7e5faa9d6561e0922535d3bc Mon Sep 17 00:00:00 2001 From: James Allen Date: Mon, 10 Oct 2016 17:06:46 +0100 Subject: [PATCH 2/3] Add basic change tracking into editor behind a feature flag --- .../ide/editor/directives/aceEditor.coffee | 8 +- .../track-changes/TrackChangesManager.coffee | 410 ++++++++++++++++++ .../public/coffee/utils/EventEmitter.coffee | 4 +- .../web/public/stylesheets/app/editor.less | 11 + 4 files changed, 431 insertions(+), 2 deletions(-) create mode 100644 services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 0e8878e7b9..c054c22f8b 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -7,7 +7,8 @@ define [ "ide/editor/directives/aceEditor/spell-check/SpellCheckManager" "ide/editor/directives/aceEditor/highlights/HighlightsManager" "ide/editor/directives/aceEditor/cursor-position/CursorPositionManager" -], (App, Ace, SearchBox, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager) -> + "ide/editor/directives/aceEditor/track-changes/TrackChangesManager" +], (App, Ace, SearchBox, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager) -> EditSession = ace.require('ace/edit_session').EditSession # set the path for ace workers if using a CDN (from editor.jade) @@ -68,6 +69,9 @@ define [ undoManager = new UndoManager(scope, editor, element) highlightsManager = new HighlightsManager(scope, editor, element) cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage) + trackChangesManager = new TrackChangesManager(scope, editor, element) + if window.location.search.match /tcon/ # track changes on + trackChangesManager.enabled = true # Prevert Ctrl|Cmd-S from triggering save dialog editor.commands.addCommand @@ -217,7 +221,9 @@ define [ sharejs_doc.on "remoteop.recordForUndo", () => undoManager.nextUpdateIsRemote = true + editor.initing = true sharejs_doc.attachToAce(editor) + editor.initing = false # need to set annotations after attaching because attaching # deletes and then inserts document content session.setAnnotations scope.annotations diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee new file mode 100644 index 0000000000..1c092ad190 --- /dev/null +++ b/services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee @@ -0,0 +1,410 @@ +define [ + "ace/ace" + "utils/EventEmitter" +], (_, EventEmitter) -> + class TrackChangesManager + Range = ace.require("ace/range").Range + + constructor: (@$scope, @editor, @element) -> + @changesTracker = new ChangesTracker() + @changeIdToMarkerIdMap = {} + @enabled = false + + @changesTracker.on "insert:added", (change) => + @_onInsertAdded(change) + @changesTracker.on "insert:removed", (change) => + @_onInsertRemoved(change) + @changesTracker.on "delete:added", (change) => + @_onDeleteAdded(change) + @changesTracker.on "delete:removed", (change) => + @_onDeleteRemoved(change) + @changesTracker.on "changes:moved", (changes) => + @_onChangesMoved(changes) + + onChange = (e) => + if !@editor.initing and @enabled + @applyChange(e) + setTimeout () => + @checkMapping() + , 100 + + @editor.on "changeSession", (e) => + e.oldSession?.getDocument().off "change", onChange + e.session.getDocument().on "change", onChange + @editor.getSession().getDocument().on "change", onChange + + checkMapping: () -> + session = @editor.getSession() + + # Make a copy of session.getMarkers() so we can modify it + markers = {} + for marker_id, marker of session.getMarkers() + markers[marker_id] = marker + + for change in @changesTracker.changes + op = change.op + marker_id = @changeIdToMarkerIdMap[change.id] + + start = @_shareJsOffsetToAcePosition(op.p) + if op.i? + end = @_shareJsOffsetToAcePosition(op.p + op.i.length) + else if op.d? + end = start + + marker = markers[marker_id] + delete markers[marker_id] + if marker.range.start.row != start.row or + marker.range.start.column != start.column or + marker.range.end.row != end.row or + marker.range.end.column != end.column + console.error "Change doesn't match marker anymore", {change, marker, start, end} + + for marker_id, marker of markers + if marker.clazz.match("track-changes") + console.error "Orphaned ace marker", marker + + applyChange: (delta) -> + op = @_aceChangeToShareJs(delta) + console.log "Applying change", delta, op + @changesTracker.applyOp(op) + + _onInsertAdded: (change) -> + start = @_shareJsOffsetToAcePosition(change.op.p) + end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length) + session = @editor.getSession() + doc = session.getDocument() + ace_range = new Range(start.row, start.column, end.row, end.column) + marker_id = session.addMarker(ace_range, "track-changes-added-marker", "text") + @changeIdToMarkerIdMap[change.id] = marker_id + + _onDeleteAdded: (change) -> + position = @_shareJsOffsetToAcePosition(change.op.p) + session = @editor.getSession() + doc = session.getDocument() + ace_range = new Range(position.row, position.column, position.row, position.column) + + # Our delete marker is zero characters wide, but Ace doesn't draw ranges + # that are empty. So we monkey patch the range to tell Ace it's not empty. + # This is the code we need to trick: + # var range = marker.range.clipRows(config.firstRow, config.lastRow); + # if (range.isEmpty()) continue; + _clipRows = ace_range.clipRows + ace_range.clipRows = (args...) -> + range = _clipRows.apply(ace_range, args) + range.isEmpty = () -> + false + return range + + marker_id = session.addMarker(ace_range, "track-changes-deleted-marker", "text") + @changeIdToMarkerIdMap[change.id] = marker_id + + _onInsertRemoved: (change) -> + marker_id = @changeIdToMarkerIdMap[change.id] + session = @editor.getSession() + session.removeMarker marker_id + + _onDeleteRemoved: (change) -> + marker_id = @changeIdToMarkerIdMap[change.id] + session = @editor.getSession() + session.removeMarker marker_id + + _aceChangeToShareJs: (delta) -> + start = delta.start + lines = @editor.getSession().getDocument().getLines 0, start.row + offset = 0 + for line, i in lines + offset += if i < start.row + line.length + else + start.column + offset += start.row # Include newlines + + text = delta.lines.join('\n') + switch delta.action + when 'insert' + return { i: text, p: offset } + when 'remove' + return { d: text, p: offset } + else throw new Error "unknown action: #{delta.action}" + + _shareJsOffsetToAcePosition: (offset) -> + lines = @editor.getSession().getDocument().getAllLines() + row = 0 + for line, row in lines + break if offset <= line.length + offset -= lines[row].length + 1 # + 1 for newline char + return {row:row, column:offset} + + _onChangesMoved: (changes) -> + session = @editor.getSession() + markers = session.getMarkers() + for change in changes + start = @_shareJsOffsetToAcePosition(change.op.p) + if change.op.i? + end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length) + else + end = start + marker_id = @changeIdToMarkerIdMap[change.id] + marker = markers[marker_id] + console.log "moving marker", {marker, start, end, change} + marker.range.start = start + marker.range.end = end + + class ChangesTracker extends EventEmitter + # The purpose of this class is to track a set of inserts and deletes to a document, like + # track changes in Word. We store these as a set of ShareJs style ranges: + # {i: "foo", p: 42} # Insert 'foo' at offset 42 + # {d: "bar", p: 37} # Delete 'bar' at offset 37 + # We only track the inserts and deletes, not the whole document, but by being given all + # updates that are applied to a document, we can update these appropriately. + # + # Note that the set of inserts and deletes we store applies to the document as-is at the moment. + # So inserts correspond to text which is in the document, while deletes correspond to text which + # is no longer there, so their lengths do not affect the position of later offsets. + # E.g. + # this is the current text of the document + # |-----| | + # {i: "current ", p:12} -^ ^- {d: "old ", p: 31} + # + # Track changes rules (should be consistent with Word): + # * When text is inserted at a delete, the text goes to the left of the delete + # I.e. "foo|bar" -> "foobaz|bar", where | is the delete, and 'baz' is inserted + # * Deleting content flagged as 'inserted' does not create a new delete marker, it only + # removes the insert marker. E.g. + # * "abdefghijkl" -> "abfghijkl" when 'de' is deleted. No delete marker added + # |---| <- inserted |-| <- inserted + # * Deletes overlapping regular text and inserted text will insert a delete marker for the + # regular text: + # "abcdefghijkl" -> "abcdejkl" when 'fghi' is deleted + # |----| |--|| + # ^- inserted 'bcdefg' \ ^- deleted 'hi' + # \--inserted 'bcde' + # * Deletes overlapping other deletes are merged. E.g. + # "abcghijkl" -> "ahijkl" when 'bcg is deleted' + # | <- delete 'def' | <- delete 'bcdefg' + constructor: () -> + # Change objects have the following structure: + # { + # id: ... # Uniquely generated by us + # op: { # ShareJs style op tracking the offset (p) and content inserted (i) or deleted (d) + # i: "..." + # p: 42 + # } + # } + # + # Ids are used to uniquely identify a change, e.g. for updating it in the database, or keeping in + # sync with Ace ranges. + @changes = [] + @id = 0 + + applyOp: (op) -> + # Apply an op that has been applied to the document to our changes to keep them up to date + if op.i? + @applyInsert(op) + else if op.d? + @applyDelete(op) + + applyInsert: (op) -> + op_start = op.p + op_length = op.i.length + op_end = op.p + op_length + + already_merged = false + previous_change = null + moved_changes = [] + for change in @changes + change_start = change.op.p + + if change.op.d? + # Shift any deletes after this along by the length of this insert + if op_start <= change_start + change.op.p += op_length + moved_changes.push change + else if change.op.i? + change_end = change_start + change.op.i.length + is_change_overlapping = (op_start >= change_start and op_start <= change_end) + + # If there is a delete at the start of the insert, and we're inserting + # at the start, we SHOULDN'T merge since the delete acts as a partition. + # The previous op will be the delete, but it's already been shifted by this insert + # + # I.e. + # Originally: |-- existing insert --| + # | <- existing delete at same offset + # + # Now: |-- existing insert --| <- not shifted yet + # |-- this insert --|| <- existing delete shifted along to end of this op + # + # After: |-- existing insert --| + # |-- this insert --|| <- existing delete + # + # Without the delete, the inserts would be merged. + is_insert_blocked_by_delete = (previous_change? and previous_change.op.d? and previous_change.op.p == op_end) + + # If the insert is overlapping another insert, either at the beginning in the middle or touching the end, + # then we merge them into one. + if is_change_overlapping and + !is_insert_blocked_by_delete and + !already_merged # With the way we order our changes, there should only ever be one candidate to merge + # with since changes don't overlap. However, this flag just adds a little bit of protection + offset = op_start - change_start + change.op.i = change.op.i.slice(0, offset) + op.i + change.op.i.slice(offset) + already_merged = true + moved_changes.push change + else if op_start <= change_start + # If we're fully before the other insert we can just shift the other insert by our length. + # If they are touching, and should have been merged, they will have been above. + # If not merged above, then it must be blocked by a delete, and will be after this insert, so we shift it along as well + change.op.p += op_length + moved_changes.push change + previous_change = change + + if !already_merged + @_addOp op + + if moved_changes.length > 0 + @emit "changes:moved", moved_changes + + applyDelete: (op) -> + op_start = op.p + op_length = op.d.length + op_end = op.p + op_length + remove_changes = [] + moved_changes = [] + + # We might end up modifying our delete op if it merges with existing deletes, or cancels out + # with an existing insert. Since we might do multiple modifications, we record them and do + # all the modifications after looping through the existing changes, so as not to mess up the + # offset indexes as we go. + op_modifications = [] + for change in @changes + if change.op.i? + change_start = change.op.p + change_end = change_start + change.op.i.length + if op_end <= change_start + # Shift ops after us back by our length + change.op.p -= op_length + moved_changes.push change + else if op_start >= change_end + # Delete is after insert, nothing to do + else + # When the new delete overlaps an insert, we should remove the part of the insert that + # is now deleted, and also remove the part of the new delete that overlapped. I.e. + # the two cancel out where they overlap. + if op_start >= change_start + # |-- existing insert --| + # insert_remaining_before -> |.....||-- new delete --| + delete_remaining_before = "" + insert_remaining_before = change.op.i.slice(0, op_start - change_start) + else + # delete_remaining_before -> |.....||-- existing insert --| + # |-- new delete --| + delete_remaining_before = op.d.slice(0, change_start - op_start) + insert_remaining_before = "" + + if op_end <= change_end + # |-- existing insert --| + # |-- new delete --||.....| <- insert_remaining_after + delete_remaining_after = "" + insert_remaining_after = change.op.i.slice(op_end - change_start) + else + # |-- existing insert --||.....| <- delete_remaining_after + # |-- new delete --| + delete_remaining_after = op.d.slice(change_end - op_start) + insert_remaining_after = "" + + insert_remaining = insert_remaining_before + insert_remaining_after + if insert_remaining.length > 0 + change.op.i = insert_remaining + change.op.p = Math.min(change_start, op_start) + moved_changes.push change + else + remove_changes.push change + + # We know what we want to preserve of our delete op before (delete_remaining_before) and what we want to preserve + # afterwards (delete_remaining_before). Now we need to turn that into a modification which deletes the + # chunk in the middle not covered by these. + delete_removed_length = op.d.length - delete_remaining_before.length - delete_remaining_after.length + delete_removed_start = delete_remaining_before.length + modification = { + d: op.d.slice(delete_removed_start, delete_removed_start + delete_removed_length) + p: delete_removed_start + } + if modification.d.length > 0 + op_modifications.push modification + else if change.op.d? + change_start = change.op.p + if op_end < change_start + # Shift ops after us (but not touching) back by our length + change.op.p -= op_length + moved_changes.push change + else if op_start <= change_start <= op_end + # If we overlap a delete, add it in our content, and delete the existing change + offset = change_start - op_start + op_modifications.push { i: change.op.d, p: offset } + remove_changes.push change + + op.d = @_applyOpModifications(op.d, op_modifications) + if op.d.length > 0 + @_addOp op + + for change in remove_changes + @_removeChange change + + if moved_changes.length > 0 + @emit "changes:moved", moved_changes + + _newId: () -> + @id++ + + _addOp: (op) -> + change = { + id: @_newId() + op: op + } + @changes.push change + + # Keep ops in order of offset, with deletes before inserts + @changes.sort (c1, c2) -> + result = c1.op.p - c2.op.p + if result != 0 + return result + else if c1.op.i? and c2.op.d? + return 1 + else + return -1 + + if op.d? + @emit "delete:added", change + else if op.i? + @emit "insert:added", change + + _removeChange: (change) -> + @changes = @changes.filter (c) -> c.id != change.id + if change.op.d? + @emit "delete:removed", change + else if change.op.i? + @emit "insert:removed", change + + _applyOpModifications: (content, op_modifications) -> + # Put in descending position order, with deleting first if at the same offset + # (Inserting first would modify the content that the delete will delete) + op_modifications.sort (a, b) -> + result = b.p - a.p + if result != 0 + return result + else if a.i? and b.d? + return 1 + else + return -1 + + for modification in op_modifications + if modification.i? + content = content.slice(0, modification.p) + modification.i + content.slice(modification.p) + else if modification.d? + if content.slice(modification.p, modification.p + modification.d.length) != modification.d + throw new Error("deleted content does not match. content: #{JSON.stringify(content)}; modification: #{JSON.stringify(modification)}") + content = content.slice(0, modification.p) + content.slice(modification.p + modification.d.length) + return content + + return TrackChangesManager \ No newline at end of file diff --git a/services/web/public/coffee/utils/EventEmitter.coffee b/services/web/public/coffee/utils/EventEmitter.coffee index 12c34e73f0..b0fe5f1a36 100644 --- a/services/web/public/coffee/utils/EventEmitter.coffee +++ b/services/web/public/coffee/utils/EventEmitter.coffee @@ -30,4 +30,6 @@ define [], () -> trigger: (event, args...) -> @events ||= {} for callback in @events[event] or [] - callback.callback(args...) \ No newline at end of file + callback.callback(args...) + + emit: (args...) -> @trigger(args...) diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index 48b8b424ed..6d35e92293 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -146,6 +146,17 @@ background-repeat: repeat-x; background-position: bottom left; } + .track-changes-added-marker { + border-radius: 0; + position: absolute; + background-color: hsl(100, 70%, 70%); + } + .track-changes-deleted-marker { + border-radius: 0; + position: absolute; + border-left: 2px dotted red; + margin-left: -1px; + } .remote-cursor { position: absolute; border-left: 2px solid transparent; From 32d7bb16e5cc5f72fa39b75faa0fbfa4b012a617 Mon Sep 17 00:00:00 2001 From: James Allen Date: Tue, 11 Oct 2016 09:40:15 +0100 Subject: [PATCH 3/3] Be more explicit with query string to turn on track changes --- .../web/public/coffee/ide/editor/directives/aceEditor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee index 465a7185df..b212039d9f 100644 --- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee @@ -71,7 +71,7 @@ define [ highlightsManager = new HighlightsManager(scope, editor, element) cursorPositionManager = new CursorPositionManager(scope, editor, element, localStorage) trackChangesManager = new TrackChangesManager(scope, editor, element) - if window.location.search.match /tcon/ # track changes on + if window.location.search.match /tcon=true/ # track changes on trackChangesManager.enabled = true # Prevert Ctrl|Cmd-S from triggering save dialog