diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index 6d7d18687d..08124c334c 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -65,7 +65,7 @@ block content ng-class="{ 'ide-history-open' : (ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME) }", layout="main", ng-hide="state.loading", - resize-on="layout:chat:resize", + resize-on="layout:chat:resize,history:toggle", minimum-restore-size-west="130" custom-toggler-pane=hasFeature('custom-togglers') ? "'west'" : "false" custom-toggler-msg-when-open=hasFeature('custom-togglers') ? "'" + translate("tooltip_hide_filetree") + "'" : "false" diff --git a/services/web/app/views/project/editor/history.pug b/services/web/app/views/project/editor/history.pug index fec7327d6f..35de68957f 100644 --- a/services/web/app/views/project/editor/history.pug +++ b/services/web/app/views/project/editor/history.pug @@ -70,17 +70,18 @@ script(type="text/ng-template", id="historyRestoreDiffModalTemplate") script(type="text/ng-template", id="historyLabelTpl") - .history-entry-label( - ng-class="{ 'history-entry-label-own' : $ctrl.isOwnedByCurrentUser }" + .history-label( + ng-class="{ 'history-label-own' : $ctrl.isOwnedByCurrentUser }" ) - span.history-entry-label-comment( + span.history-label-comment( tooltip-append-to-body="true" tooltip-template="'historyLabelTooltipTpl'" tooltip-placement="left" + tooltip-enable="$ctrl.showTooltip" ) i.fa.fa-tag |  {{ $ctrl.labelText }} - button.history-entry-label-delete-btn( + button.history-label-delete-btn( ng-if="$ctrl.isOwnedByCurrentUser" stop-propagation="click" ng-click="$ctrl.onLabelDelete()" diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index ca0c48b722..5e4c4b66c6 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -3,6 +3,7 @@ aside.change-list( ng-controller="HistoryV2ListController" ) history-entries-list( + ng-if="!history.showOnlyLabels && !history.error" entries="history.updates" current-user="user" users="projectUsers" @@ -13,6 +14,16 @@ aside.change-list( on-entry-select="handleEntrySelect(selectedEntry)" on-label-delete="handleLabelDelete(label)" ) + history-labels-list( + ng-if="history.showOnlyLabels && !history.error" + labels="history.labels" + current-user="user" + users="projectUsers" + is-loading="history.loading" + selected-label="history.selection.label" + on-label-select="handleLabelSelect(label)" + on-label-delete="handleLabelDelete(label)" + ) aside.change-list( ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE" @@ -141,7 +152,7 @@ script(type="text/ng-template", id="historyEntryTpl") .history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })") history-label( - ng-repeat="label in $ctrl.entry.labels" + ng-repeat="label in $ctrl.entry.labels | orderBy : '-created_at'" label-text="label.comment" label-owner-name="$ctrl.displayNameById(label.user_id)" label-creation-date-time="label.created_at" @@ -192,4 +203,37 @@ script(type="text/ng-template", id="historyEntryTpl") li.history-entry-metadata-user(ng-if="::$ctrl.entry.meta.users.length == 0") span.name( ng-style="$ctrl.getUserCSSStyle();" - ) #{translate("anonymous")} \ No newline at end of file + ) #{translate("anonymous")} + +script(type="text/ng-template", id="historyLabelsListTpl") + .history-labels-list + .history-entry-label( + ng-repeat="label in $ctrl.labels track by label.id" + ng-click="$ctrl.onLabelSelect({ label: label })" + ng-class="{ 'history-entry-label-selected': label.id === $ctrl.selectedLabel.id }" + ) + history-label( + show-tooltip="false" + label-text="label.comment" + is-owned-by-current-user="label.user_id === $ctrl.currentUser.id" + on-label-delete="$ctrl.onLabelDelete({ label: label })" + ) + .history-entry-label-metadata + .history-entry-label-metadata-user(ng-init="user = $ctrl.getUserById(label.user_id)") + | Saved by + span.name( + ng-if="user && user._id !== $ctrl.currentUser.id" + ng-style="$ctrl.getUserCSSStyle(user, label);" + ) {{ ::$ctrl.displayName(user) }} + span.name( + ng-if="user && user._id == $ctrl.currentUser.id" + ng-style="$ctrl.getUserCSSStyle(user, label);" + ) You + span.name( + ng-if="user == null" + ng-style="$ctrl.getUserCSSStyle(user, label);" + ) #{translate("anonymous")} + time.history-entry-label-metadata-time {{ ::label.created_at | formatDate }} + .loading(ng-show="$ctrl.isLoading") + i.fa.fa-spin.fa-refresh + |    #{translate("loading")}... \ No newline at end of file diff --git a/services/web/app/views/project/editor/history/previewPanelV2.pug b/services/web/app/views/project/editor/history/previewPanelV2.pug index 3d7a1ac3df..e75af8db90 100644 --- a/services/web/app/views/project/editor/history/previewPanelV2.pug +++ b/services/web/app/views/project/editor/history/previewPanelV2.pug @@ -45,7 +45,7 @@ text="history.diff.text", highlights="history.diff.highlights", read-only="true", - resize-on="layout:main:resize", + resize-on="layout:main:resize,history:toggle", navigate-highlights="true" ) .alert.alert-info(ng-if="history.diff.binary") @@ -70,12 +70,24 @@ font-size="settings.fontSize", text="history.selectedFile.text", read-only="true", - resize-on="layout:main:resize", + resize-on="layout:main:resize,history:toggle", ) .alert.alert-info(ng-if="history.selectedFile.binary") | We're still working on showing image and binary changes, sorry. Stay tuned! .loading-panel(ng-show="history.selectedFile.loading") i.fa.fa-spin.fa-refresh |   #{translate("loading")}... + .error-panel(ng-show="history.error") + .alert.alert-danger + p + | #{translate("generic_history_error")} + a( + ng-href="mailto:#{settings.adminEmail}?Subject=Error%20loading%20history%20for%project%20{{ project_id }}" + ) #{settings.adminEmail} + p.clearfix + a.alert-link-as-btn.pull-right( + href + ng-click="toggleHistory()" + ) #{translate("back_to_editor")} .error-panel(ng-show="history.selectedFile.error") .alert.alert-danger #{translate("generic_something_went_wrong")} diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index 2a4c222bad..a683facba4 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -2,23 +2,43 @@ ng-controller="HistoryV2ToolbarController" ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME" ) - span(ng-show="history.loadingFileTree") + span.history-toolbar-selected-version(ng-show="history.loadingFileTree") i.fa.fa-spin.fa-refresh |    #{translate("loading")}... - span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")}  + span.history-toolbar-selected-version( + ng-show="!history.loadingFileTree && !history.showOnlyLabels && !history.error" + ) #{translate("browsing_project_as_of")}  time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }} - button.history-toolbar-btn( - ng-click="showAddLabelDialog();" - ng-disabled="history.loadingFileTree" - ) - i.fa.fa-tag - |  #{translate("history_label_this_version")} - button.history-toolbar-btn( - ng-click="toggleHistoryViewMode();" - ng-disabled="history.loadingFileTree" - ) - i.fa.fa-exchange - |  #{translate("compare_to_another_version")} + span.history-toolbar-selected-version( + ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label && !history.error" + ) #{translate("browsing_project_labelled")}  + span.history-toolbar-selected-label "{{ history.selection.label.comment }}" + div.history-toolbar-actions( + ng-if="!history.error" + ) + button.history-toolbar-btn( + ng-click="showAddLabelDialog();" + ng-if="!history.showOnlyLabels" + ng-disabled="history.loadingFileTree" + ) + i.fa.fa-tag + |  #{translate("history_label_this_version")} + button.history-toolbar-btn( + ng-click="toggleHistoryViewMode();" + ng-disabled="history.loadingFileTree" + ) + i.fa.fa-exchange + |  #{translate("compare_to_another_version")} + + .history-toolbar-entries-list( + ng-if="!history.error" + ) + toggle-switch( + ng-model="history.showOnlyLabels" + label-true=translate("history_view_labels") + label-false=translate("history_view_all") + description=translate("history_view_a11y_description") + ) script(type="text/ng-template", id="historyV2AddLabelModalTemplate") form( diff --git a/services/web/public/coffee/ide/directives/layout.coffee b/services/web/public/coffee/ide/directives/layout.coffee index b4e2e7f83d..8c30943f39 100644 --- a/services/web/public/coffee/ide/directives/layout.coffee +++ b/services/web/public/coffee/ide/directives/layout.coffee @@ -112,7 +112,8 @@ define [ element.layout().resizeAll() if attrs.resizeOn? - scope.$on attrs.resizeOn, () -> onExternalResize() + for event in attrs.resizeOn.split "," + scope.$on event, () -> onExternalResize() if hasCustomToggler state = element.layout().readState() diff --git a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee index 7ff41985f4..3f231c046b 100644 --- a/services/web/public/coffee/ide/history/HistoryV2Manager.coffee +++ b/services/web/public/coffee/ide/history/HistoryV2Manager.coffee @@ -12,6 +12,7 @@ define [ "ide/history/directives/infiniteScroll" "ide/history/components/historyEntriesList" "ide/history/components/historyEntry" + "ide/history/components/historyLabelsList" "ide/history/components/historyLabel" "ide/history/components/historyFileTree" "ide/history/components/historyFileEntity" @@ -26,6 +27,9 @@ define [ @hide() else @show() + @ide.$timeout () => + @$scope.$broadcast "history:toggle" + , 0 @$scope.toggleHistoryViewMode = () => if @$scope.history.viewMode == HistoryViewModes.COMPARE @@ -34,6 +38,9 @@ define [ else @reset() @$scope.history.viewMode = HistoryViewModes.COMPARE + @ide.$timeout () => + @$scope.$broadcast "history:toggle" + , 0 @$scope.$watch "history.selection.updates", (updates) => if @$scope.history.viewMode == HistoryViewModes.COMPARE @@ -48,6 +55,18 @@ define [ else @reloadDiff() + @$scope.$watch "history.showOnlyLabels", (showOnlyLabels, prevVal) => + if showOnlyLabels? and showOnlyLabels != prevVal + if showOnlyLabels + @selectedLabelFromUpdatesSelection() + else + @$scope.history.selection.label = null + if @$scope.history.selection.updates.length == 0 + @autoSelectLastUpdate() + + @$scope.$watch "history.updates.length", () => + @recalculateSelectedUpdates() + show: () -> @$scope.ui.view = "history" @reset() @@ -64,6 +83,7 @@ define [ nextBeforeTimestamp: null atEnd: false selection: { + label: null updates: [] docs: {} pathname: null @@ -72,6 +92,8 @@ define [ toV: null } } + error: null + showOnlyLabels: false labels: null files: [] diff: null # When history.viewMode == HistoryViewModes.COMPARE @@ -86,10 +108,9 @@ define [ _csrf: window.csrfToken }) - loadFileTreeForUpdate: (update) -> - {fromV, toV} = update + loadFileTreeForVersion: (version) -> url = "/project/#{@$scope.project_id}/filetree/diff" - query = [ "from=#{toV}", "to=#{toV}" ] + query = [ "from=#{version}", "to=#{version}" ] url += "?" + query.join("&") @$scope.history.loadingFileTree = true @$scope.history.selectedFile = null @@ -118,6 +139,10 @@ define [ return if @$scope.history.updates.length == 0 @selectUpdate @$scope.history.updates[0] + autoSelectLastLabel: () -> + return if @$scope.history.labels.length == 0 + @selectLabel @$scope.history.labels[0] + selectUpdate: (update) -> selectedUpdateIndex = @$scope.history.updates.indexOf update if selectedUpdateIndex == -1 @@ -127,29 +152,107 @@ define [ update.selectedFrom = false @$scope.history.updates[selectedUpdateIndex].selectedTo = true @$scope.history.updates[selectedUpdateIndex].selectedFrom = true - @loadFileTreeForUpdate @$scope.history.updates[selectedUpdateIndex] + @recalculateSelectedUpdates() + @loadFileTreeForVersion @$scope.history.updates[selectedUpdateIndex].toV + + selectedLabelFromUpdatesSelection: () -> + # Get the number of labels associated with the currently selected update + nSelectedLabels = @$scope.history.selection.updates?[0]?.labels?.length + # If the currently selected update has no labels, select the last one (version-wise) + if nSelectedLabels == 0 + @autoSelectLastLabel() + # If the update has one label, select it + else if nSelectedLabels == 1 + @selectLabel @$scope.history.selection.updates[0].labels[0] + # If there are multiple labels for the update, select the latest + else if nSelectedLabels > 1 + sortedLabels = @ide.$filter("orderBy")(@$scope.history.selection.updates[0].labels, '-created_at') + lastLabelFromUpdate = sortedLabels[0] + @selectLabel lastLabelFromUpdate + + selectLabel: (labelToSelect) -> + updateToSelect = null + + if @_isLabelSelected labelToSelect + # Label already selected + return + + for update in @$scope.history.updates + if update.toV == labelToSelect.version + updateToSelect = update + break + + @$scope.history.selection.label = labelToSelect + if updateToSelect? + @selectUpdate updateToSelect + else + @$scope.history.selection.updates = [] + @loadFileTreeForVersion labelToSelect.version + + recalculateSelectedUpdates: () -> + beforeSelection = true + afterSelection = false + @$scope.history.selection.updates = [] + for update in @$scope.history.updates + if update.selectedTo + inSelection = true + beforeSelection = false + + update.beforeSelection = beforeSelection + update.inSelection = inSelection + update.afterSelection = afterSelection + + if inSelection + @$scope.history.selection.updates.push update + + if update.selectedFrom + inSelection = false + afterSelection = true BATCH_SIZE: 10 fetchNextBatchOfUpdates: () -> updatesURL = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}" if @$scope.history.nextBeforeTimestamp? updatesURL += "&before=#{@$scope.history.nextBeforeTimestamp}" - + labelsURL = "/project/#{@ide.project_id}/labels" + @$scope.history.loading = true @$scope.history.loadingFileTree = true - @ide.$http.get updatesURL + requests = + updates: @ide.$http.get updatesURL + + if !@$scope.history.labels? + requests.labels = @ide.$http.get labelsURL + + @ide.$q.all requests .then (response) => - updatesData = response.data + updatesData = response.updates.data + if response.labels? + @$scope.history.labels = @_sortLabelsByVersionAndDate response.labels.data @_loadUpdates(updatesData.updates) @$scope.history.nextBeforeTimestamp = updatesData.nextBeforeTimestamp if !updatesData.nextBeforeTimestamp? @$scope.history.atEnd = true @$scope.history.loading = false + .catch (error) => + { status, statusText } = error + @$scope.history.error = { status, statusText } + @$scope.history.loading = false + @$scope.history.loadingFileTree = false + + _sortLabelsByVersionAndDate: (labels) -> + @ide.$filter("orderBy")(labels, [ '-version', '-created_at' ]) loadFileAtPointInTime: () -> pathname = @$scope.history.selection.pathname - toV = @$scope.history.selection.updates[0].toV + if @$scope.history.selection.updates?[0]? + toV = @$scope.history.selection.updates[0].toV + else if @$scope.history.selection.label? + toV = @$scope.history.selection.label.version + + if !toV? + return url = "/project/#{@$scope.project_id}/diff" query = ["pathname=#{encodeURIComponent(pathname)}", "from=#{toV}", "to=#{toV}"] url += "?" + query.join("&") @@ -208,8 +311,8 @@ define [ labelCurrentVersion: (labelComment) => @_labelVersion labelComment, @$scope.history.selection.updates[0].toV - deleteLabel: (labelId) => - url = "/project/#{@$scope.project_id}/labels/#{labelId}" + deleteLabel: (label) => + url = "/project/#{@$scope.project_id}/labels/#{label.id}" @ide.$http({ url, @@ -217,14 +320,19 @@ define [ headers: "X-CSRF-Token": window.csrfToken }).then (response) => - @_deleteLabelFromLocalCollection @$scope.history.updates, labelId - @_deleteLabelFromLocalCollection @$scope.history.selection, labelId + @_deleteLabelLocally label + _isLabelSelected: (label) -> + label.id == @$scope.history.selection.label?.id - _deleteLabelFromLocalCollection: (collection, labelId) -> - for update in collection - update.labels = _.filter update.labels, (label) -> - label.id != labelId + _deleteLabelLocally: (labelToDelete) -> + for update, i in @$scope.history.updates + if update.toV == labelToDelete.version + update.labels = _.filter update.labels, (label) -> + label.id != labelToDelete.id + break + @$scope.history.labels = _.filter @$scope.history.labels, (label) -> + label.id != labelToDelete.id _parseDiff: (diff) -> if diff.binary @@ -303,7 +411,10 @@ define [ if @$scope.history.viewMode == HistoryViewModes.COMPARE @autoSelectRecentUpdates() else - @autoSelectLastUpdate() + if @$scope.history.showOnlyLabels + @autoSelectLastLabel() + else + @autoSelectLastUpdate() _labelVersion: (comment, version) -> url = "/project/#{@$scope.project_id}/labels" @@ -316,10 +427,11 @@ define [ .then (response) => @_addLabelToLocalUpdate response.data - _addLabelToLocalUpdate: (label) -> + _addLabelToLocalUpdate: (label) => localUpdate = _.find @$scope.history.updates, (update) -> update.toV == label.version if localUpdate? - localUpdate.labels.push label + localUpdate.labels = @_sortLabelsByVersionAndDate localUpdate.labels.concat label + @$scope.history.labels = @_sortLabelsByVersionAndDate @$scope.history.labels.concat label _perDocSummaryOfUpdates: (updates) -> # Track current_pathname -> original_pathname diff --git a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee index 934ca304fe..de4e4f1b92 100644 --- a/services/web/public/coffee/ide/history/components/historyEntriesList.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntriesList.coffee @@ -3,6 +3,22 @@ define [ ], (App) -> historyEntriesListController = ($scope, $element, $attrs) -> ctrl = @ + ctrl.$entryListViewportEl = null + _isEntryElVisible = ($entryEl) -> + entryElTop = $entryEl.offset().top + entryElBottom = entryElTop + $entryEl.outerHeight() + entryListViewportElTop = ctrl.$entryListViewportEl.offset().top + entryListViewportElBottom = entryListViewportElTop + ctrl.$entryListViewportEl.height() + return entryElTop >= entryListViewportElTop and entryElBottom <= entryListViewportElBottom; + _getScrollTopPosForEntry = ($entryEl) -> + halfViewportElHeight = ctrl.$entryListViewportEl.height() / 2 + return $entryEl.offset().top - halfViewportElHeight + ctrl.onEntryLinked = (entry, $entryEl) -> + if entry.selectedTo and entry.selectedFrom and !_isEntryElVisible $entryEl + $scope.$applyAsync () -> + ctrl.$entryListViewportEl.scrollTop _getScrollTopPosForEntry $entryEl + ctrl.$onInit = () -> + ctrl.$entryListViewportEl = $element.find "> .history-entries" return App.component "historyEntriesList", { diff --git a/services/web/public/coffee/ide/history/components/historyEntry.coffee b/services/web/public/coffee/ide/history/components/historyEntry.coffee index 0942613ef2..6d5eed53d5 100644 --- a/services/web/public/coffee/ide/history/components/historyEntry.coffee +++ b/services/web/public/coffee/ide/history/components/historyEntry.coffee @@ -1,7 +1,8 @@ define [ "base" + "ide/colors/ColorManager" "ide/history/util/displayNameForUser" -], (App, displayNameForUser) -> +], (App, ColorManager, displayNameForUser) -> historyEntryController = ($scope, $element, $attrs, _) -> ctrl = @ # This method (and maybe the one below) will be removed soon. User details data will be @@ -19,11 +20,14 @@ define [ else if projectOp.add? then "#{ projectOp.add.pathname}" else if projectOp.remove? then "#{ projectOp.remove.pathname}" ctrl.getUserCSSStyle = (user) -> - hue = user?.hue or 100 + curUserId = user?._id or user?.id + hue = ColorManager.getHueForUserId(curUserId) or 100 if ctrl.entry.inSelection color : "#FFF" else color: "hsl(#{ hue }, 70%, 50%)" + ctrl.$onInit = () -> + ctrl.historyEntriesList.onEntryLinked ctrl.entry, $element.find "> .history-entry" return App.component "historyEntry", { @@ -33,6 +37,8 @@ define [ users: "<" onSelect: "&" onLabelDelete: "&" + require: + historyEntriesList: '^historyEntriesList' controller: historyEntryController templateUrl: "historyEntryTpl" } \ No newline at end of file diff --git a/services/web/public/coffee/ide/history/components/historyLabel.coffee b/services/web/public/coffee/ide/history/components/historyLabel.coffee index 2394b76f8b..680d610d99 100644 --- a/services/web/public/coffee/ide/history/components/historyLabel.coffee +++ b/services/web/public/coffee/ide/history/components/historyLabel.coffee @@ -3,15 +3,18 @@ define [ ], (App) -> historyLabelController = ($scope, $element, $attrs, $filter, _) -> ctrl = @ + ctrl.$onInit = () -> + ctrl.showTooltip ?= true return App.component "historyLabel", { bindings: labelText: "<" - labelOwnerName: "<" - labelCreationDateTime: "<" + labelOwnerName: " + historyLabelsListController = ($scope, $element, $attrs) -> + ctrl = @ + # This method (and maybe the one below) will be removed soon. User details data will be + # injected into the history API responses, so we won't need to fetch user data from other + # local data structures. + ctrl.getUserById = (id) -> + _.find ctrl.users, (user) -> + curUserId = user?._id or user?.id + curUserId == id + ctrl.displayName = displayNameForUser + ctrl.getUserCSSStyle = (user, label) -> + curUserId = user?._id or user?.id + hue = ColorManager.getHueForUserId(curUserId) or 100 + if label.id == ctrl.selectedLabel?.id + color : "#FFF" + else + color: "hsl(#{ hue }, 70%, 50%)" + return + + App.component "historyLabelsList", { + bindings: + labels: "<" + users: "<" + currentUser: "<" + isLoading: "<" + selectedLabel: "<" + onLabelSelect: "&" + onLabelDelete: "&" + controller: historyLabelsListController + templateUrl: "historyLabelsListTpl" + } diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee index d03786c2c6..6001c33762 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee @@ -9,7 +9,7 @@ define [ $scope.deleteLabel = () -> $scope.state.inflight = true - ide.historyManager.deleteLabel labelDetails.id + ide.historyManager.deleteLabel labelDetails .then (response) -> $scope.state.inflight = false $modalInstance.close() diff --git a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee index 1635ec9f0a..adff4e1cb0 100644 --- a/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee +++ b/services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee @@ -19,8 +19,10 @@ define [ $scope.handleEntrySelect = (entry) -> ide.historyManager.selectUpdate(entry) - $scope.recalculateSelectedUpdates() + $scope.handleLabelSelect = (label) -> + ide.historyManager.selectLabel(label) + $scope.handleLabelDelete = (labelDetails) -> $modal.open( templateUrl: "historyV2DeleteLabelModalTemplate" @@ -28,64 +30,4 @@ define [ resolve: labelDetails: () -> labelDetails ) - - $scope.recalculateSelectedUpdates = () -> - beforeSelection = true - afterSelection = false - $scope.history.selection.updates = [] - for update in $scope.history.updates - if update.selectedTo - inSelection = true - beforeSelection = false - - update.beforeSelection = beforeSelection - update.inSelection = inSelection - update.afterSelection = afterSelection - - if inSelection - $scope.history.selection.updates.push update - - if update.selectedFrom - inSelection = false - afterSelection = true - - $scope.recalculateHoveredUpdates = () -> - hoverSelectedFrom = false - hoverSelectedTo = false - for update in $scope.history.updates - # Figure out whether the to or from selector is hovered over - if update.hoverSelectedFrom - hoverSelectedFrom = true - if update.hoverSelectedTo - hoverSelectedTo = true - - if hoverSelectedFrom - # We want to 'hover select' everything between hoverSelectedFrom and selectedTo - inHoverSelection = false - for update in $scope.history.updates - if update.selectedTo - update.hoverSelectedTo = true - inHoverSelection = true - update.inHoverSelection = inHoverSelection - if update.hoverSelectedFrom - inHoverSelection = false - if hoverSelectedTo - # We want to 'hover select' everything between hoverSelectedTo and selectedFrom - inHoverSelection = false - for update in $scope.history.updates - if update.hoverSelectedTo - inHoverSelection = true - update.inHoverSelection = inHoverSelection - if update.selectedFrom - update.hoverSelectedFrom = true - inHoverSelection = false - - $scope.resetHoverState = () -> - for update in $scope.history.updates - delete update.hoverSelectedFrom - delete update.hoverSelectedTo - delete update.inHoverSelection - - $scope.$watch "history.updates.length", () -> - $scope.recalculateSelectedUpdates() ] \ No newline at end of file diff --git a/services/web/public/coffee/ide/services/ide.coffee b/services/web/public/coffee/ide/services/ide.coffee index 6462859df2..24805b6270 100644 --- a/services/web/public/coffee/ide/services/ide.coffee +++ b/services/web/public/coffee/ide/services/ide.coffee @@ -3,11 +3,13 @@ define [ ], (App) -> # We create and provide this as service so that we can access the global ide # from within other parts of the angular app. - App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", ($http, queuedHttp, $modal, $q) -> + App.factory "ide", ["$http", "queuedHttp", "$modal", "$q", "$filter", "$timeout", ($http, queuedHttp, $modal, $q, $filter, $timeout) -> ide = {} ide.$http = $http ide.queuedHttp = queuedHttp ide.$q = $q + ide.$filter = $filter + ide.$timeout = $timeout @recentEvents = [] ide.pushEvent = (type, meta = {}) => diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index 404609917a..cfefbb462c 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -15,16 +15,30 @@ .history-toolbar when (@is-overleaf = false) { border-bottom: @toolbar-border-bottom; } - .history-toolbar-time { - font-weight: bold; + .history-toolbar-selected-version { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } - .history-toolbar-btn { - .btn; - .btn-info; - .btn-xs; - padding-left: @padding-small-horizontal; - padding-right: @padding-small-horizontal; - margin-left: (@line-height-computed / 2); + .history-toolbar-time, + .history-toolbar-selected-label { + font-weight: bold; + } + .history-toolbar-actions { + flex-grow: 1; + } + .history-toolbar-btn { + .btn; + .btn-info; + .btn-xs; + padding-left: @padding-small-horizontal; + padding-right: @padding-small-horizontal; + margin-left: (@line-height-computed / 2); + } + .history-toolbar-entries-list { + flex: 0 0 @changesListWidth; + padding: 0 10px; + border-left: 1px solid @editor-border-color; } .history-entries { @@ -48,51 +62,54 @@ padding: 5px 10px; cursor: pointer; - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { background-color: @history-entry-selected-bg; color: #FFF; } } - - .history-entry-label { + .history-label { display: inline-block; color: @history-entry-label-color; font-size: @font-size-small; margin-bottom: 3px; margin-right: 10px; white-space: nowrap; - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { color: @history-entry-selected-label-color; } } - .history-entry-label-comment, - .history-entry-label-delete-btn { + .history-label-comment, + .history-label-delete-btn { padding: 0 @padding-xs-horizontal 1px @padding-xs-horizontal; border: 0; background-color: @history-entry-label-bg-color; - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { background-color: @history-entry-selected-label-bg-color; } } - .history-entry-label-comment { + .history-label-comment { display: block; float: left; border-radius: 9999px; max-width: 190px; overflow: hidden; text-overflow: ellipsis; - .history-entry-label-own & { + .history-label-own & { padding-right: (@padding-xs-horizontal / 2); border-radius: 9999px 0 0 9999px; } } - .history-entry-label-delete-btn { + .history-label-delete-btn { padding-left: (@padding-xs-horizontal / 2); padding-right: @padding-xs-horizontal; border-radius: 0 9999px 9999px 0; &:hover { background-color: darken(@history-entry-label-bg-color, 8%); - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { background-color: darken(@history-entry-selected-label-bg-color, 8%); } } @@ -130,7 +147,8 @@ color: @history-highlight-color; font-weight: bold; word-break: break-all; - .history-entry-selected & { + .history-entry-selected &, + .history-entry-label-selected & { color: #FFF; } } @@ -155,6 +173,19 @@ } } +.history-labels-list { + .history-entries; + overflow-y: auto; +} + .history-entry-label { + .history-entry-details; + padding: 7px 10px; + &.history-entry-label-selected { + background-color: @history-entry-selected-bg; + color: #FFF; + } + } + .history-file-tree-inner { .full-size; overflow-y: auto; @@ -231,330 +262,3 @@ color: @brand-primary; } } -// @changesListWidth: 250px; -// @changesListPadding: @line-height-computed / 2; - -// @selector-padding-vertical: 10px; -// @selector-padding-horizontal: @line-height-computed / 2; -// @day-header-height: 24px; - -// @range-bar-color: @link-color; -// @range-bar-selected-offset: 14px; - -// #history { -// .upgrade-prompt { -// position: absolute; -// top: 0; -// bottom: 0; -// left: 0; -// right: 0; -// z-index: 100; -// background-color: rgba(128,128,128,0.4); -// .message { -// margin: auto; -// margin-top: 100px; -// padding: (@line-height-computed / 2) @line-height-computed; -// width: 400px; -// background-color: white; -// border-radius: 8px; -// } -// .message-wider { -// width: 650px; -// margin-top: 60px; -// padding: 0; -// } - -// .message-header { -// .modal-header; -// } - -// .message-body { -// .modal-body; -// } -// } - -// .diff-panel { -// .full-size; -// margin-right: @changesListWidth; -// } - -// .diff { -// .full-size; -// .toolbar { -// padding: 3px; -// .name { -// float: left; -// padding: 3px @line-height-computed / 4; -// display: inline-block; -// } -// } -// .diff-editor { -// .full-size; -// top: 40px; -// } -// .hide-ace-cursor { -// .ace_active-line, .ace_cursor-layer, .ace_gutter-active-line { -// display: none; -// } -// } -// .diff-deleted { -// padding: @line-height-computed; -// } -// .deleted-warning { -// background-color: @brand-danger; -// color: white; -// padding: @line-height-computed / 2; -// margin-right: @line-height-computed / 4; -// } -// &-binary { -// .alert { -// margin: @line-height-computed / 2; -// } -// } -// } - -// aside.change-list { -// border-left: 1px solid @editor-border-color; -// height: 100%; -// width: @changesListWidth; -// position: absolute; -// right: 0; - -// .loading { -// text-align: center; -// font-family: @font-family-serif; -// } - -// ul { -// li.change { -// position: relative; -// user-select: none; -// -ms-user-select: none; -// -moz-user-select: none; -// -webkit-user-select: none; - -// .day { -// background-color: #fafafa; -// border-bottom: 1px solid @editor-border-color; -// padding: 4px; -// font-weight: bold; -// text-align: center; -// height: @day-header-height; -// font-size: 14px; -// line-height: 1; -// } -// .selectors { -// input { -// margin: 0; -// } -// position: absolute; -// left: @selector-padding-horizontal; -// top: 0; -// bottom: 0; -// width: 24px; -// .selector-from { -// position: absolute; -// bottom: @selector-padding-vertical; -// left: 0; -// opacity: 0.8; -// } -// .selector-to { -// position: absolute; -// top: @selector-padding-vertical; -// left: 0; -// opacity: 0.8; -// } -// .range { -// position: absolute; -// left: 5px; -// width: 4px; -// top: 0; -// bottom: 0; -// } -// } -// .description { -// padding: (@line-height-computed / 4); -// padding-left: 38px; -// min-height: 38px; -// border-bottom: 1px solid @editor-border-color; -// cursor: pointer; -// &:hover { -// background-color: @gray-lightest; -// } -// } -// .users { -// .user { -// font-size: 0.8rem; -// color: @gray; -// text-transform: capitalize; -// position: relative; -// padding-left: 16px; -// .color-square { -// height: 12px; -// width: 12px; -// border-radius: 3px; -// position: absolute; -// left: 0; -// bottom: 3px; -// } -// .name { -// width: 94%; -// white-space: nowrap; -// overflow: hidden; -// text-overflow: ellipsis; -// } -// } -// } -// .time { -// float: right; -// color: @gray; -// display: inline-block; -// padding-right: (@line-height-computed / 2); -// font-size: 0.8rem; -// line-height: @line-height-computed; -// } -// .doc { -// font-size: 0.9rem; -// font-weight: bold; -// } -// .action { -// color: @gray; -// text-transform: uppercase; -// font-size: 0.7em; -// margin-bottom: -2px; -// margin-top: 2px; -// &-edited { -// margin-top: 0; -// } -// } -// } -// li.loading-changes, li.empty-message { -// padding: 6px; -// cursor: default; -// &:hover { -// background-color: inherit; -// } -// } -// li.selected { -// border-left: 4px solid @range-bar-color; -// .day { -// padding-left: 0; -// } -// .description { -// padding-left: 34px; -// } -// .selectors { -// left: @selector-padding-horizontal - 4px; -// .range { -// background-color: @range-bar-color; -// } -// } -// } -// li.selected-to { -// .selectors { -// .range { -// top: @range-bar-selected-offset; -// } -// .selector-to { -// opacity: 1; -// } -// } -// } -// li.selected-from { -// .selectors { -// .range { -// bottom: @range-bar-selected-offset; -// } -// .selector-from { -// opacity: 1; -// } -// } -// } -// li.first-in-day { -// .selectors { -// .selector-to { -// top: @day-header-height + @selector-padding-vertical; -// } -// } -// } -// li.first-in-day.selected-to { -// .selectors { -// .range { -// top: @day-header-height + @range-bar-selected-offset; -// } -// } -// } -// } -// ul.hover-state { -// li { -// .selectors { -// .range { -// background-color: transparent; -// top: 0; -// bottom: 0; -// } -// } -// } -// li.hover-selected { -// .selectors { -// .range { -// top: 0; -// background-color: @gray-light; -// } -// } -// } -// li.hover-selected-to { -// .selectors { -// .range { -// top: @range-bar-selected-offset; -// } -// .selector-to { -// opacity: 1; -// } -// } -// } -// li.hover-selected-from { -// .selectors { -// .range { -// bottom: @range-bar-selected-offset; -// } -// .selector-from { -// opacity: 1; -// } -// } -// } -// li.first-in-day.hover-selected-to { -// .selectors { -// .range { -// top: @day-header-height + @range-bar-selected-offset; -// } -// } -// } -// } -// } -// } - -// .diff-deleted { -// padding-top: 15px; -// } - -// .editor-dark { -// #history { -// aside.change-list { -// border-color: @editor-dark-toolbar-border-color; - -// ul li.change { -// .day { -// background-color: darken(@editor-dark-background-color, 10%); -// border-bottom: 1px solid @editor-dark-toolbar-border-color; -// } -// .description { -// border-bottom: 1px solid @editor-dark-toolbar-border-color; -// &:hover { -// background-color: black; -// } -// } -// } -// } -// } -// } diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less index 4006b56a7b..c5499180e0 100644 --- a/services/web/public/stylesheets/app/editor/toolbar.less +++ b/services/web/public/stylesheets/app/editor/toolbar.less @@ -194,56 +194,57 @@ } .toggle-switch { - position: relative; + position: relative; height: 100%; width: 100%; - background-color: @toggle-switch-bg; - border-radius: @btn-border-radius-base; + background-color: @toggle-switch-bg; + border-radius: @btn-border-radius-base; } .toggle-switch-label { position: relative; display: block; font-weight: normal; - z-index: 2; - float: left; - width: 50%; - height: 100%; - line-height: 24px; + z-index: 2; + float: left; + width: 50%; + height: 100%; + line-height: 24px; text-align: center; margin-bottom: 0; - cursor: pointer; - user-select: none; - transition: color 0.12s ease-out; + cursor: pointer; + user-select: none; + color: @text-color; + transition: color 0.12s ease-out; } .toggle-switch-input { - position: absolute; - opacity: 0; + position: absolute; + opacity: 0; } .toggle-switch-input:checked + .toggle-switch-label { - color: #fff; - font-weight: bold; + color: #fff; + font-weight: bold; } .toggle-switch-selection { - display: block; - position: absolute; - z-index: 1; - top: 2px; - left: 2px; - right: 2px; - width: calc(~"50% - 2px"); - height: calc(~"100% - 4px"); - background: @toggle-switch-highlight-color; - border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base; - transition: transform 0.12s ease-out, border-radius 0.12s ease-out; + display: block; + position: absolute; + z-index: 1; + top: 2px; + left: 2px; + right: 2px; + width: calc(~"50% - 2px"); + height: calc(~"100% - 4px"); + background: @toggle-switch-highlight-color; + border-radius: @btn-border-radius-base 0 0 @btn-border-radius-base; + transition: transform 0.12s ease-out, border-radius 0.12s ease-out; } .toggle-switch-input:checked:nth-child(4) ~ .toggle-switch-selection { - transform: translate(100%); - border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0; + transform: translate(100%); + border-radius: 0 @btn-border-radius-base @btn-border-radius-base 0; } /************************************** diff --git a/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee b/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee index 725befb721..2fc5c5b3b6 100644 --- a/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee +++ b/services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee @@ -15,14 +15,17 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) -> nextBeforeTimestamp: null atEnd: false selection: { + label: null updates: [] - pathname: null docs: {} + pathname: null range: { fromV: null toV: null } } + error: null + showOnlyLabels: false labels: null diff: null files: []