From 9c5c6922b96acb75bf08f7707095b9764041d5bf Mon Sep 17 00:00:00 2001 From: Paulo Jorge Reis Date: Tue, 9 Apr 2019 10:56:45 +0100 Subject: [PATCH] Merge pull request #1626 from sharelatex/pr-history-drag-and-drop-selection History drag and drop selection GitOrigin-RevId: d2bc0dfc2e2634553224b7bef2ac5d926d42bf01 --- services/web/app/views/project/editor.pug | 3 + .../project/editor/history/entriesListV2.pug | 344 +++++------------- .../project/editor/history/previewPanelV2.pug | 8 +- .../project/editor/history/toolbarV2.pug | 25 +- .../public/js/libs/jquery.ui.touch-punch.js | 180 +++++++++ .../web/public/src/ide/directives/layout.js | 2 +- .../public/src/ide/history/HistoryManager.js | 1 - .../src/ide/history/HistoryV2Manager.js | 165 +++++---- .../history/components/historyEntriesList.js | 98 ++++- .../ide/history/components/historyEntry.js | 66 +++- .../history/components/historyLabelsList.js | 136 ++++++- .../controllers/HistoryCompareController.js | 87 ----- .../controllers/HistoryV2ListController.js | 15 +- .../controllers/HistoryV2ToolbarController.js | 19 + .../directives/historyDraggableBoundary.js | 32 ++ .../directives/historyDroppableArea.js | 25 ++ .../stylesheets/app/editor/history-v2.less | 103 +++++- .../stylesheets/core/_common-variables.less | 2 + .../public/stylesheets/core/ol-variables.less | 2 + .../src/ide/history/HistoryV2ManagerTests.js | 2 - 20 files changed, 843 insertions(+), 472 deletions(-) create mode 100644 services/web/public/js/libs/jquery.ui.touch-punch.js delete mode 100644 services/web/public/src/ide/history/controllers/HistoryCompareController.js create mode 100644 services/web/public/src/ide/history/directives/historyDraggableBoundary.js create mode 100644 services/web/public/src/ide/history/directives/historyDroppableArea.js diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index c717d8155d..f8c37d65e5 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -162,6 +162,9 @@ block requirejs "ace/keybinding-vim": { "deps": ["ace/ace"] }, + "libs/jquery.ui.touch-punch": { + "deps": [ "libs/#{lib('jquery-layout')}" ] + } }, "config":{ "moment":{ diff --git a/services/web/app/views/project/editor/history/entriesListV2.pug b/services/web/app/views/project/editor/history/entriesListV2.pug index 69c5b3edaf..39b674eea5 100644 --- a/services/web/app/views/project/editor/history/entriesListV2.pug +++ b/services/web/app/views/project/editor/history/entriesListV2.pug @@ -1,11 +1,13 @@ aside.change-list( - ng-if="history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME" + ng-if="history.isV2" ng-controller="HistoryV2ListController" ) history-entries-list( ng-if="!history.showOnlyLabels && !history.error" entries="history.updates" + range-selection-enabled="history.viewMode === HistoryViewModes.COMPARE" selected-history-version="history.selection.range.toV" + selected-history-range="history.selection.range" current-user="user" current-user-is-owner="project.owner._id === user.id" users="projectUsers" @@ -14,232 +16,23 @@ aside.change-list( load-initialize="ui.view == 'history'" is-loading="history.loading" free-history-limit-hit="history.freeHistoryLimitHit" - on-entry-select="handleEntrySelect(selectedEntry)" + on-version-select="handleVersionSelect(version)" + on-range-select="handleRangeSelect(selectedToV, selectedFromV)" on-label-delete="handleLabelDelete(label)" ) history-labels-list( ng-if="history.showOnlyLabels && !history.error" labels="history.labels" + range-selection-enabled="history.viewMode === HistoryViewModes.COMPARE" + selected-history-version="history.selection.range.toV" + selected-history-range="history.selection.range" current-user="user" users="projectUsers" is-loading="history.loading" - selected-label="history.selection.label" - on-label-select="handleLabelSelect(label)" + on-version-select="handleVersionSelect(version)" + on-range-select="handleRangeSelect(selectedToV, selectedFromV)" on-label-delete="handleLabelDelete(label)" ) -aside.change-list.change-list-compare( - ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE" - ng-controller="HistoryCompareController" -) - div( - ng-if="!history.showOnlyLabels && !history.error" - ) - aside.change-list( - infinite-scroll="loadMore()" - infinite-scroll-disabled="history.loading || history.atEnd" - infinite-scroll-initialize="ui.view == 'history' && history.viewMode === HistoryViewModes.COMPARE" - ) - .infinite-scroll-inner - ul.list-unstyled( - ng-class="{\ - 'hover-state': history.hoveringOverListSelectors\ - }" - ) - li.change( - ng-repeat="update in history.updates track by update.fromV" - ng-class="{\ - 'first-in-day': update.meta.first_in_day,\ - 'selected': update.toV <= history.selection.range.toV && update.fromV >= history.selection.range.fromV,\ - 'selected-to': update.toV === history.selection.range.toV,\ - 'selected-from': update.fromV === history.selection.range.fromV,\ - 'hover-selected': update.toV <= history.selection.hoveredRange.toV && update.fromV >= history.selection.hoveredRange.fromV,\ - 'hover-selected-to': update.toV === history.selection.hoveredRange.toV,\ - 'hover-selected-from': update.fromV === history.selection.hoveredRange.fromV,\ - }" - ) - - div.day(ng-if="::update.meta.first_in_day") {{ ::update.meta.end_ts | relativeDate }} - - div.selectors - div.range - form - input.selector-from( - type="radio" - name="fromVersion" - ng-model="history.selection.range.fromV" - ng-value="update.fromV" - ng-mouseover="setHoverFrom(update.fromV)" - ng-mouseout="resetHover()" - ng-show="update.fromV < history.selection.range.fromV || update.toV <= history.selection.range.toV && update.fromV >= history.selection.range.fromV" - ) - form - input.selector-to( - type="radio" - name="toVersion" - ng-model="history.selection.range.toV" - ng-value="update.toV" - ng-mouseover="setHoverTo(update.toV)" - ng-mouseout="resetHover()" - ng-show="update.toV > history.selection.range.toV || update.toV <= history.selection.range.toV && update.fromV >= history.selection.range.fromV" - ) - - div.description(ng-click="select(update.toV, update.fromV)") - history-label( - ng-repeat="label in update.labels track by label.id" - label-text="label.comment" - label-owner-name="getDisplayNameById(label.user_id)" - label-creation-date-time="label.created_at" - is-owned-by-current-user="label.user_id === user.id" - on-label-delete="deleteLabel(label)" - ) - div.time {{ ::update.meta.end_ts | formatDate:'h:mm a' }} - div.action.action-edited(ng-if="::history.isV2 && update.pathnames.length > 0") - | #{translate("file_action_edited")} - div.docs(ng-repeat="pathname in ::update.pathnames") - .doc {{ ::pathname }} - div.docs(ng-repeat="project_op in ::update.project_ops") - div(ng-if="::project_op.rename") - .action #{translate("file_action_renamed")} - .doc {{ ::project_op.rename.pathname }} → {{ ::project_op.rename.newPathname }} - div(ng-if="::project_op.add") - .action #{translate("file_action_created")} - .doc {{ ::project_op.add.pathname }} - div(ng-if="::project_op.remove") - .action #{translate("file_action_deleted")} - .doc {{ ::project_op.remove.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%)'}") - .color-square(ng-if="::update_user == null", ng-style="::{'background-color': 'hsl(100, 70%, 50%)'}") - .name(ng-if="::update_user && update_user.id != user.id" ng-bind="getDisplayNameForUser(update_user)") - .name(ng-if="::update_user && update_user.id == user.id") You - .name(ng-if="::update_user == null") #{translate("anonymous")} - div.user(ng-if="::update.meta.users.length == 0") - .color-square(style="background-color: hsl(100, 100%, 50%)") - span #{translate("anonymous")} - - .loading(ng-show="history.loading") - i.fa.fa-spin.fa-refresh - |    #{translate("loading")}... - .history-entries-list-upgrade-prompt( - ng-if="history.freeHistoryLimitHit && project.owner._id === user.id" - ng-controller="FreeTrialModalController" - ) - p #{translate("currently_seeing_only_24_hrs_history")} - p: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})} - ul.list-unstyled - li - i.fa.fa-check   - | #{translate("unlimited_projects")} - - li - i.fa.fa-check   - | #{translate("collabs_per_proj", {collabcount:'Multiple'})} - - li - i.fa.fa-check   - | #{translate("full_doc_history")} - - li - i.fa.fa-check   - | #{translate("sync_to_dropbox")} - - li - i.fa.fa-check   - | #{translate("sync_to_github")} - - li - i.fa.fa-check   - |#{translate("compile_larger_projects")} - p.text-center - a.btn.btn-success( - href - ng-class="buttonClass" - ng-click="startFreeTrial('history')" - ) #{translate("start_free_trial")} - p.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")} - .history-entries-list-upgrade-prompt( - ng-if="history.freeHistoryLimitHit && !project.owner._id === user.id" - ) - p #{translate("currently_seeing_only_24_hrs_history")} - strong #{translate("ask_proj_owner_to_upgrade_for_full_history")} - .history-labels-list-compare( - ng-if="history.showOnlyLabels && !history.error" - ) - ul.list-unstyled( - ng-class="{\ - 'hover-state': history.hoveringOverListSelectors\ - }" - ) - li.change( - ng-repeat="versionWithLabel in versionsWithLabels | orderBy:'-version' track by versionWithLabel.version" - ng-class="{\ - 'selected': versionWithLabel.version <= history.selection.range.toV && versionWithLabel.version >= history.selection.range.fromV,\ - 'selected-to': versionWithLabel.version === history.selection.range.toV,\ - 'selected-from': versionWithLabel.version === history.selection.range.fromV,\ - 'hover-selected': versionWithLabel.version <= history.selection.hoveredRange.toV && versionWithLabel.version >= history.selection.hoveredRange.fromV,\ - 'hover-selected-to': versionWithLabel.version === history.selection.hoveredRange.toV,\ - 'hover-selected-from': versionWithLabel.version === history.selection.hoveredRange.fromV,\ - }" - ) - div.selectors - div.range - form - input.selector-from( - type="radio" - name="fromVersionForLabel" - ng-model="history.selection.range.fromV" - ng-value="versionWithLabel.version" - ng-mouseover="setHoverFrom(versionWithLabel.version)" - ng-mouseout="resetHover()" - ng-show="versionWithLabel.version < history.selection.range.fromV || versionWithLabel.version <= history.selection.range.toV && versionWithLabel.version >= history.selection.range.fromV" - ) - form - input.selector-to( - type="radio" - name="toVersionForLabel" - ng-model="history.selection.range.toV" - ng-value="versionWithLabel.version" - ng-mouseover="setHoverTo(versionWithLabel.version)" - ng-mouseout="resetHover()" - ng-show="versionWithLabel.version > history.selection.range.toV || versionWithLabel.version <= history.selection.range.toV && versionWithLabel.version >= history.selection.range.fromV" - ) - - - .description(ng-click="addLabelVersionToSelection(versionWithLabel.version)") - div( - ng-repeat="label in versionWithLabel.labels track by label.id" - ) - history-label( - show-tooltip="false" - label-text="label.comment" - is-owned-by-current-user="label.user_id === user.id" - is-pseudo-current-state-label="label.isPseudoCurrentStateLabel" - on-label-delete="deleteLabel(label)" - ) - .history-entry-label-metadata - .history-entry-label-metadata-user( - ng-if="!label.isPseudoCurrentStateLabel" - ng-init="labelOwner = getUserById(label.user_id)" - ) - | Saved by - span.name( - ng-if="::labelOwner && labelOwner._id !== user.id" - ng-style="::{'color': 'hsl({{ hueForUser(label.user_id) }}, 70%, 50%)'}" - ) {{ ::getDisplayNameById(label.user_id) }} - span.name( - ng-if="labelOwner && labelOwner._id == user.id" - ng-style="::{'color': 'hsl({{ hueForUser(label.user_id) }}, 70%, 50%)'}" - ) You - span.name( - ng-if="::labelOwner == null" - ng-style="::{'color': 'hsl(100, 70%, 50%)'}" - ) #{translate("anonymous")} - time.history-entry-label-metadata-time {{ ::label.created_at | formatDate }} - - .loading(ng-show="history.loading") - i.fa.fa-spin.fa-refresh - |    #{translate("loading")}... script(type="text/ng-template", id="historyEntriesListTpl") .history-entries( @@ -250,11 +43,15 @@ script(type="text/ng-template", id="historyEntriesListTpl") .infinite-scroll-inner history-entry( ng-repeat="entry in $ctrl.entries" + range-selection-enabled="$ctrl.rangeSelectionEnabled" + is-dragging="$ctrl.isDragging" selected-history-version="$ctrl.selectedHistoryVersion" + selected-history-range="$ctrl.selectedHistoryRange" + hovered-history-range="$ctrl.hoveredHistoryRange" entry="entry" current-user="$ctrl.currentUser" users="$ctrl.users" - on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })" + on-select="$ctrl.handleEntrySelect(selectedEntry)" on-label-delete="$ctrl.onLabelDelete({ label: label })" ) .loading(ng-show="$ctrl.isLoading") @@ -307,13 +104,30 @@ script(type="text/ng-template", id="historyEntryTpl") .history-entry( ng-class="{\ 'history-entry-first-in-day': $ctrl.entry.meta.first_in_day,\ - 'history-entry-selected': $ctrl.entry.toV === $ctrl.selectedHistoryVersion,\ + 'history-entry-selected': !$ctrl.isDragging && $ctrl.isEntrySelected(),\ + 'history-entry-selected-to': $ctrl.rangeSelectionEnabled && !$ctrl.isDragging && $ctrl.selectedHistoryRange.toV === $ctrl.entry.toV,\ + 'history-entry-selected-from': $ctrl.rangeSelectionEnabled && !$ctrl.isDragging && $ctrl.selectedHistoryRange.fromV === $ctrl.entry.fromV,\ + 'history-entry-hover-selected': $ctrl.rangeSelectionEnabled && $ctrl.isDragging && $ctrl.isEntryHoverSelected(),\ + 'history-entry-hover-selected-to': $ctrl.rangeSelectionEnabled && $ctrl.isDragging && $ctrl.hoveredHistoryRange.toV === $ctrl.entry.toV,\ + 'history-entry-hover-selected-from': $ctrl.rangeSelectionEnabled && $ctrl.isDragging && $ctrl.hoveredHistoryRange.fromV === $ctrl.entry.fromV,\ }" + history-droppable-area + history-droppable-area-on-drop="$ctrl.onDrop(boundary)" + history-droppable-area-on-over="$ctrl.onOver(boundary)" ) time.history-entry-day(ng-if="::$ctrl.entry.meta.first_in_day") {{ ::$ctrl.entry.meta.end_ts | relativeDate }} - .history-entry-details(ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })") + .history-entry-details( + ng-click="$ctrl.onSelect({ selectedEntry: $ctrl.entry })" + ) + .history-entry-toV-handle( + ng-show="$ctrl.rangeSelectionEnabled && $ctrl.selectedHistoryRange && ((!$ctrl.isDragging && $ctrl.selectedHistoryRange.toV === $ctrl.entry.toV) || ($ctrl.isDragging && $ctrl.hoveredHistoryRange.toV === $ctrl.entry.toV))" + history-draggable-boundary="toV" + history-draggable-boundary-on-drag-start="$ctrl.onDraggingStart()" + history-draggable-boundary-on-drag-stop="$ctrl.onDraggingStop(isValidDrop, boundary)" + ) + history-label( ng-repeat="label in $ctrl.entry.labels | orderBy : '-created_at'" label-text="label.comment" @@ -342,7 +156,6 @@ script(type="text/ng-template", id="historyEntryTpl") ng-if="::project_op.remove" ) #{translate("file_action_deleted")} span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }} - .history-entry-metadata time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }} span @@ -368,39 +181,72 @@ script(type="text/ng-template", id="historyEntryTpl") ng-style="$ctrl.getUserCSSStyle();" ) #{translate("anonymous")} + .history-entry-fromV-handle( + ng-show="$ctrl.rangeSelectionEnabled && $ctrl.selectedHistoryRange && ((!$ctrl.isDragging && $ctrl.selectedHistoryRange.fromV === $ctrl.entry.fromV) || ($ctrl.isDragging && $ctrl.hoveredHistoryRange.fromV === $ctrl.entry.fromV))" + history-draggable-boundary="fromV" + history-draggable-boundary-on-drag-start="$ctrl.onDraggingStart()" + history-draggable-boundary-on-drag-stop="$ctrl.onDraggingStop(isValidDrop, boundary)" + ) + 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-version-with-label( + ng-repeat="versionWithLabel in $ctrl.versionsWithLabels | orderBy:'-version' track by versionWithLabel.version" + ng-class="{\ + 'history-version-with-label-selected': !$ctrl.isDragging && $ctrl.isVersionSelected(versionWithLabel.version),\ + 'history-version-with-label-selected-to': !$ctrl.isDragging && $ctrl.selectedHistoryRange.toV === versionWithLabel.version,\ + 'history-version-with-label-selected-from': !$ctrl.isDragging && $ctrl.selectedHistoryRange.fromV === versionWithLabel.version,\ + 'history-version-with-label-hover-selected': $ctrl.isDragging && $ctrl.isVersionHoverSelected(versionWithLabel.version),\ + 'history-version-with-label-hover-selected-to': $ctrl.isDragging && $ctrl.hoveredHistoryRange.toV === versionWithLabel.version,\ + 'history-version-with-label-hover-selected-from': $ctrl.isDragging && $ctrl.hoveredHistoryRange.fromV === versionWithLabel.version,\ + }" + ng-click="$ctrl.handleVersionSelect(versionWithLabel)" + history-droppable-area + history-droppable-area-on-drop="$ctrl.onDrop(boundary, versionWithLabel)" + history-droppable-area-on-over="$ctrl.onOver(boundary, versionWithLabel)" ) - 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 })" - is-pseudo-current-state-label="label.isPseudoCurrentStateLabel" + .history-entry-toV-handle( + ng-show="$ctrl.rangeSelectionEnabled && $ctrl.selectedHistoryRange && ((!$ctrl.isDragging && $ctrl.selectedHistoryRange.toV === versionWithLabel.version) || ($ctrl.isDragging && $ctrl.hoveredHistoryRange.toV === versionWithLabel.version))" + history-draggable-boundary="toV" + history-draggable-boundary-on-drag-start="$ctrl.onDraggingStart()" + history-draggable-boundary-on-drag-stop="$ctrl.onDraggingStop(isValidDrop, boundary)" ) - .history-entry-label-metadata - .history-entry-label-metadata-user( - ng-if="!label.isPseudoCurrentStateLabel" - ng-init="user = $ctrl.getUserById(label.user_id)" + div( + ng-repeat="label in versionWithLabel.labels track by label.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 })" + is-pseudo-current-state-label="label.isPseudoCurrentStateLabel" ) - | 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 }} + .history-entry-label-metadata + .history-entry-label-metadata-user( + ng-if="!label.isPseudoCurrentStateLabel" + 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, versionWithLabel);" + ) {{ ::$ctrl.displayName(user) }} + span.name( + ng-if="user && user._id == $ctrl.currentUser.id" + ng-style="$ctrl.getUserCSSStyle(user, versionWithLabel);" + ) You + span.name( + ng-if="user == null" + ng-style="$ctrl.getUserCSSStyle(user, versionWithLabel);" + ) #{translate("anonymous")} + time.history-entry-label-metadata-time {{ ::label.created_at | formatDate }} + .history-entry-fromV-handle( + ng-show="$ctrl.rangeSelectionEnabled && $ctrl.selectedHistoryRange && ((!$ctrl.isDragging && $ctrl.selectedHistoryRange.fromV === versionWithLabel.version) || ($ctrl.isDragging && $ctrl.hoveredHistoryRange.fromV === versionWithLabel.version))" + history-draggable-boundary="fromV" + history-draggable-boundary-on-drag-start="$ctrl.onDraggingStart()" + history-draggable-boundary-on-drag-stop="$ctrl.onDraggingStop(isValidDrop, boundary)" + ) + .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 77e1774165..ea742b4ef2 100644 --- a/services/web/app/views/project/editor/history/previewPanelV2.pug +++ b/services/web/app/views/project/editor/history/previewPanelV2.pug @@ -2,7 +2,7 @@ ng-if="history.isV2 && history.viewMode === HistoryViewModes.COMPARE" ) .diff( - ng-if="!!history.selection.diff && !history.selection.diff.loading && !history.selection.diff.error", + ng-if="!!history.selection.diff && !isHistoryLoading() && !history.selection.diff.error", ng-class="{ 'diff-binary': history.selection.diff.binary }" ) .diff-editor-v2.hide-ace-cursor( @@ -19,10 +19,10 @@ .alert.alert-info(ng-if="history.selection.diff.binary") | We're still working on showing image and binary changes, sorry. Stay tuned! - .loading-panel(ng-show="history.selection.diff.loading") + .loading-panel(ng-show="isHistoryLoading()") i.fa.fa-spin.fa-refresh |   #{translate("loading")}... - .error-panel(ng-show="history.selection.diff.error") + .error-panel(ng-show="history.selection.diff.error && !isHistoryLoading()") .alert.alert-danger #{translate("generic_something_went_wrong")} .point-in-time-panel.full-size( @@ -42,7 +42,7 @@ ) .alert.alert-info(ng-if="history.selection.file.binary") | We're still working on showing image and binary changes, sorry. Stay tuned! - .loading-panel(ng-show="history.selection.file.loading") + .loading-panel(ng-show="isHistoryLoading()") i.fa.fa-spin.fa-refresh |   #{translate("loading")}... .error-panel(ng-show="history.error") diff --git a/services/web/app/views/project/editor/history/toolbarV2.pug b/services/web/app/views/project/editor/history/toolbarV2.pug index 09c81f9722..b61716b37c 100644 --- a/services/web/app/views/project/editor/history/toolbarV2.pug +++ b/services/web/app/views/project/editor/history/toolbarV2.pug @@ -2,28 +2,32 @@ ng-controller="HistoryV2ToolbarController" ng-if="ui.view == 'history' && history.isV2" ) - span.history-toolbar-selected-version(ng-show="history.loadingFileTree || history.selection.diff.loading") + span.history-toolbar-selected-version(ng-show="history.loadingFileTree") i.fa.fa-spin.fa-refresh |    #{translate("loading")}... //- point-in-time mode info span.history-toolbar-selected-version( - ng-show="!history.loadingFileTree && !history.showOnlyLabels && history.selection.update && !history.error" + ng-show="!history.loadingFileTree && history.viewMode === HistoryViewModes.POINT_IN_TIME && !history.showOnlyLabels && currentUpdate && !history.error" ) #{translate("browsing_project_as_of")}  - time.history-toolbar-time {{ history.selection.update.meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }} + time.history-toolbar-time {{ currentUpdate.meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }} span.history-toolbar-selected-version( - ng-show="!history.loadingFileTree && history.showOnlyLabels && history.selection.label && !history.error" + ng-show="!history.loadingFileTree && history.viewMode === HistoryViewModes.POINT_IN_TIME && history.showOnlyLabels && currentUpdate && !history.error" ) - span(ng-if="history.selection.label.comment && !history.selection.label.isPseudoCurrentStateLabel") + span(ng-if="currentUpdate.labels.length > 0") | #{translate("browsing_project_labelled")}  - span.history-toolbar-selected-label "{{ history.selection.label.comment }}" - span.history-toolbar-selected-label(ng-if="history.selection.label.isPseudoCurrentStateLabel") + span.history-toolbar-selected-label( + ng-repeat="label in currentUpdate.labels" + ) + | {{ label.comment }} + span(ng-if="!$last") , + span.history-toolbar-selected-label(ng-if="currentUpdate.labels.length === 0 && history.labels[0].isPseudoCurrentStateLabel && currentUpdate.toV === history.labels[0].version") | #{translate("browsing_project_latest_for_pseudo_label")} //- compare mode info - span.history-toolbar-selected-version(ng-show="history.viewMode === HistoryViewModes.COMPARE && history.selection.diff && !history.selection.diff.binary && !history.selection.diff.loading && !history.selection.diff.error && !history.loadingFileTree") + span.history-toolbar-selected-version(ng-if="history.viewMode === HistoryViewModes.COMPARE && history.selection.diff && !history.selection.diff.binary && !history.selection.diff.loading && !history.selection.diff.error && !history.loadingFileTree") | {{history.selection.diff.highlights.length}} ng-pluralize( count="history.selection.diff.highlights.length", @@ -41,13 +45,13 @@ button.history-toolbar-btn( ng-click="showAddLabelDialog();" ng-if="!history.showOnlyLabels" - ng-disabled="history.loadingFileTree || history.selection.range.toV == null || history.selection.range.fromV == null" + ng-disabled="isHistoryLoading() || history.selection.range.toV == null || history.selection.range.fromV == null" ) i.fa.fa-tag |  #{translate("history_label_this_version")} button.history-toolbar-btn( ng-click="toggleHistoryViewMode();" - ng-disabled="history.loadingFileTree" + ng-disabled="isHistoryLoading()" ) i.fa.fa-exchange |  #{translate("compare_to_another_version")} @@ -68,6 +72,7 @@ ) button.history-toolbar-btn( ng-click="toggleHistoryViewMode();" + ng-disabled="isHistoryLoading()" ) i.fa | #{translate("view_single_version")} diff --git a/services/web/public/js/libs/jquery.ui.touch-punch.js b/services/web/public/js/libs/jquery.ui.touch-punch.js new file mode 100644 index 0000000000..f66237934a --- /dev/null +++ b/services/web/public/js/libs/jquery.ui.touch-punch.js @@ -0,0 +1,180 @@ +/*! + * jQuery UI Touch Punch 0.2.3 + * + * Copyright 2011–2014, Dave Furfero + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Depends: + * jquery.ui.widget.js + * jquery.ui.mouse.js + */ +(function ($) { + + // Detect touch support + $.support.touch = 'ontouchend' in document; + + // Ignore browsers without touch support + if (!$.support.touch) { + return; + } + + var mouseProto = $.ui.mouse.prototype, + _mouseInit = mouseProto._mouseInit, + _mouseDestroy = mouseProto._mouseDestroy, + touchHandled; + + /** + * Simulate a mouse event based on a corresponding touch event + * @param {Object} event A touch event + * @param {String} simulatedType The corresponding mouse event + */ + function simulateMouseEvent (event, simulatedType) { + + // Ignore multi-touch events + if (event.originalEvent.touches.length > 1) { + return; + } + + event.preventDefault(); + + var touch = event.originalEvent.changedTouches[0], + simulatedEvent = document.createEvent('MouseEvents'); + + // Initialize the simulated mouse event using the touch event's coordinates + simulatedEvent.initMouseEvent( + simulatedType, // type + true, // bubbles + true, // cancelable + window, // view + 1, // detail + touch.screenX, // screenX + touch.screenY, // screenY + touch.clientX, // clientX + touch.clientY, // clientY + false, // ctrlKey + false, // altKey + false, // shiftKey + false, // metaKey + 0, // button + null // relatedTarget + ); + + // Dispatch the simulated event to the target element + event.target.dispatchEvent(simulatedEvent); + } + + /** + * Handle the jQuery UI widget's touchstart events + * @param {Object} event The widget element's touchstart event + */ + mouseProto._touchStart = function (event) { + + var self = this; + + // Ignore the event if another widget is already being handled + if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) { + return; + } + + // Set the flag to prevent other widgets from inheriting the touch event + touchHandled = true; + + // Track movement to determine if interaction was a click + self._touchMoved = false; + + // Simulate the mouseover event + simulateMouseEvent(event, 'mouseover'); + + // Simulate the mousemove event + simulateMouseEvent(event, 'mousemove'); + + // Simulate the mousedown event + simulateMouseEvent(event, 'mousedown'); + }; + + /** + * Handle the jQuery UI widget's touchmove events + * @param {Object} event The document's touchmove event + */ + mouseProto._touchMove = function (event) { + + // Ignore event if not handled + if (!touchHandled) { + return; + } + + // Interaction was not a click + this._touchMoved = true; + + // Simulate the mousemove event + simulateMouseEvent(event, 'mousemove'); + }; + + /** + * Handle the jQuery UI widget's touchend events + * @param {Object} event The document's touchend event + */ + mouseProto._touchEnd = function (event) { + + // Ignore event if not handled + if (!touchHandled) { + return; + } + + // Simulate the mouseup event + simulateMouseEvent(event, 'mouseup'); + + // Simulate the mouseout event + simulateMouseEvent(event, 'mouseout'); + + // If the touch interaction did not move, it should trigger a click + if (!this._touchMoved) { + + // Simulate the click event + simulateMouseEvent(event, 'click'); + } + + // Unset the flag to allow other widgets to inherit the touch event + touchHandled = false; + }; + + /** + * A duck punch of the $.ui.mouse _mouseInit method to support touch events. + * This method extends the widget with bound touch event handlers that + * translate touch events to mouse events and pass them to the widget's + * original mouse event handling methods. + */ + mouseProto._mouseInit = function () { + + var self = this; + + // Delegate the touch handlers to the widget's element + self.element.bind({ + touchstart: $.proxy(self, '_touchStart'), + touchmove: $.proxy(self, '_touchMove'), + touchend: $.proxy(self, '_touchEnd') + }); + + // Call the original $.ui.mouse init method + _mouseInit.call(self); + }; + + /** + * Remove the touch event handlers + */ + mouseProto._mouseDestroy = function () { + + var self = this; + + // Delegate the touch handlers to the widget's element + self.element.unbind({ + touchstart: $.proxy(self, '_touchStart'), + touchmove: $.proxy(self, '_touchMove'), + touchend: $.proxy(self, '_touchEnd') + }); + + // Call the original $.ui.mouse destroy method + _mouseDestroy.call(self); + }; + +})(jQuery); diff --git a/services/web/public/src/ide/directives/layout.js b/services/web/public/src/ide/directives/layout.js index 009ac436e1..6fa56478ef 100644 --- a/services/web/public/src/ide/directives/layout.js +++ b/services/web/public/src/ide/directives/layout.js @@ -13,7 +13,7 @@ * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ -define(['base', 'libs/jquery-layout'], App => +define(['base', 'libs/jquery-layout', 'libs/jquery.ui.touch-punch'], App => App.directive('layout', [ '$parse', '$compile', diff --git a/services/web/public/src/ide/history/HistoryManager.js b/services/web/public/src/ide/history/HistoryManager.js index fd703037b6..03f84216c7 100644 --- a/services/web/public/src/ide/history/HistoryManager.js +++ b/services/web/public/src/ide/history/HistoryManager.js @@ -19,7 +19,6 @@ define([ 'moment', 'ide/colors/ColorManager', 'ide/history/util/displayNameForUser', - 'ide/history/controllers/HistoryCompareController', 'ide/history/controllers/HistoryListController', 'ide/history/controllers/HistoryDiffController', 'ide/history/directives/infiniteScroll' diff --git a/services/web/public/src/ide/history/HistoryV2Manager.js b/services/web/public/src/ide/history/HistoryV2Manager.js index 4bb8d1b4a1..f5fb882cc3 100644 --- a/services/web/public/src/ide/history/HistoryV2Manager.js +++ b/services/web/public/src/ide/history/HistoryV2Manager.js @@ -28,6 +28,8 @@ define([ 'ide/history/controllers/HistoryV2AddLabelModalController', 'ide/history/controllers/HistoryV2DeleteLabelModalController', 'ide/history/directives/infiniteScroll', + 'ide/history/directives/historyDraggableBoundary', + 'ide/history/directives/historyDroppableArea', 'ide/history/components/historyEntriesList', 'ide/history/components/historyEntry', 'ide/history/components/historyLabelsList', @@ -57,6 +59,7 @@ define([ $scope.project_id }` this._previouslySelectedPathname = null + this._loadFileTreeRequestCanceller = null this.hardReset() this.$scope.toggleHistory = () => { @@ -71,23 +74,18 @@ define([ }, 0) } - this.$scope.$watchGroup( - ['history.selection.range.toV', 'history.selection.range.fromV'], - (newRange, prevRange) => { - if (this.$scope.history.viewMode === HistoryViewModes.COMPARE) { - let [newTo, newFrom] = newRange - let [prevTo, prevFrom] = prevRange - if ( - newTo != null && - newFrom != null && - newTo !== prevTo && - newFrom !== prevFrom - ) { - this.loadFileTreeDiff(newTo, newFrom) - } - } - } - ) + this.$scope.isHistoryLoading = () => { + let selection = this.$scope.history.selection + return ( + this.$scope.history.loadingFileTree || + (this.$scope.history.viewMode === HistoryViewModes.POINT_IN_TIME && + selection.file && + selection.file.loading) || + (this.$scope.history.viewMode === HistoryViewModes.COMPARE && + selection.diff && + selection.diff.loading) + ) + } } show() { @@ -151,8 +149,6 @@ define([ }, diff: null, files: [], - update: null, - label: null, file: null }, error: null, @@ -186,8 +182,6 @@ define([ }, diff: null, // When history.viewMode == HistoryViewModes.COMPARE files: [], // When history.viewMode == HistoryViewModes.COMPARE - update: null, // When history.viewMode == HistoryViewModes.POINT_IN_TIME - label: null, // When history.viewMode == HistoryViewModes.POINT_IN_TIME file: null } this.$scope.history.error = null @@ -221,9 +215,9 @@ define([ } else { // Point-in-time mode if (this.$scope.history.showOnlyLabels) { - this.selectLabelFromUpdatesSelection() + this.autoSelectLabelForPointInTime() } else { - this.autoSelectLastVersionForPointInTime() + this.autoSelectVersionForPointInTime() } } } @@ -288,14 +282,23 @@ define([ let selection = this.$scope.history.selection const query = [`from=${fromV}`, `to=${toV}`] url += `?${query.join('&')}` - this.$scope.history.loadingFileTree = true + + this.$scope.$applyAsync( + () => (this.$scope.history.loadingFileTree = true) + ) + selection.file = null selection.pathname = null - if (selection.diff) { - selection.diff.loading = true + + // If `this._loadFileTreeRequestCanceller` is not null, then we have a request inflight + if (this._loadFileTreeRequestCanceller != null) { + // Resolving it will cancel the inflight request (or, rather, ignore its result) + this._loadFileTreeRequestCanceller.resolve() } + this._loadFileTreeRequestCanceller = this.ide.$q.defer() + return this.ide.$http - .get(url) + .get(url, { timeout: this._loadFileTreeRequestCanceller.promise }) .then(response => { this.$scope.history.selection.files = response.data.diff for (let file of this.$scope.history.selection.files) { @@ -305,15 +308,15 @@ define([ delete file.newPathname } } + this._loadFileTreeRequestCanceller = null + this.$scope.history.loadingFileTree = false this.autoSelectFile() }) .catch(err => { - console.error(err) - }) - .finally(() => { - this.$scope.history.loadingFileTree = false - if (selection.diff) { - selection.diff.loading = true + if (err.status !== -1) { + this._loadFileTreeRequestCanceller = null + } else { + this.$scope.history.loadingFileTree = false } }) } @@ -380,7 +383,8 @@ define([ return } - this.$scope.history.selection.range.toV = this.$scope.history.updates[0].toV + let toV = this.$scope.history.updates[0].toV + let fromV = null let indexOfLastUpdateNotByMe = 0 for (let i = 0; i < this.$scope.history.updates.length; i++) { @@ -394,19 +398,24 @@ define([ indexOfLastUpdateNotByMe = i } - this.$scope.history.selection.range.fromV = this.$scope.history.updates[ - indexOfLastUpdateNotByMe - ].fromV + fromV = this.$scope.history.updates[indexOfLastUpdateNotByMe].fromV + this.selectVersionsForCompare(toV, fromV) } - autoSelectLastVersionForPointInTime() { - this.$scope.history.selection.label = null + autoSelectVersionForPointInTime() { if (this.$scope.history.updates.length === 0) { return } - return this.selectVersionForPointInTime( - this.$scope.history.updates[0].toV - ) + let versionToSelect = this.$scope.history.updates[0].toV + let range = this.$scope.history.selection.range + if ( + range.toV != null && + range.fromV != null && + range.toV === range.fromV + ) { + versionToSelect = range.toV + } + this.selectVersionForPointInTime(versionToSelect) } autoSelectLastLabel() { @@ -429,14 +438,27 @@ define([ selectVersionForPointInTime(version) { let selection = this.$scope.history.selection - selection.range.toV = version - selection.range.fromV = version - selection.update = this._getUpdateForVersion(version) - this.loadFileTreeForVersion(version) + if ( + selection.range.toV !== version && + selection.range.fromV !== version + ) { + selection.range.toV = version + selection.range.fromV = version + this.loadFileTreeForVersion(version) + } } - selectLabelFromUpdatesSelection() { - const selectedUpdate = this._getUpdateForVersion( + selectVersionsForCompare(toV, fromV) { + let range = this.$scope.history.selection.range + if (range.toV !== toV || range.fromV !== fromV) { + range.toV = toV + range.fromV = fromV + this.loadFileTreeDiff(toV, fromV) + } + } + + autoSelectLabelForPointInTime() { + const selectedUpdate = this.getUpdateForVersion( this.$scope.history.selection.range.toV ) let nSelectedLabels = 0 @@ -450,9 +472,7 @@ define([ this.autoSelectLastLabel() // If the update has one label, select it } else if (nSelectedLabels === 1) { - this.selectLabelForPointInTime( - this.$scope.history.selection.update.labels[0] - ) + this.selectLabelForPointInTime(selectedUpdate.labels[0]) // If there are multiple labels for the update, select the latest } else if (nSelectedLabels > 1) { const sortedLabels = this.ide.$filter('orderBy')( @@ -479,19 +499,17 @@ define([ } } - this.$scope.history.selection.label = labelToSelect if (updateToSelect != null) { this.selectVersionForPointInTime(updateToSelect.toV) } else { let selection = this.$scope.history.selection selection.range.toV = labelToSelect.version selection.range.fromV = labelToSelect.version - selection.update = null this.loadFileTreeForVersion(labelToSelect.version) } } - _getUpdateForVersion(version) { + getUpdateForVersion(version) { for (let update of this.$scope.history.updates) { if (update.toV === version) { return update @@ -501,17 +519,16 @@ define([ autoSelectLabelsForComparison() { let labels = this.$scope.history.labels - let selection = this.$scope.history.selection let nLabels = 0 if (Array.isArray(labels)) { nLabels = labels.length } - if (nLabels === 1) { - selection.range.toV = labels[0].version - selection.range.fromV = labels[0].version + if (nLabels === 0) { + // TODO better handling + } else if (nLabels === 1) { + this.selectVersionsForCompare(labels[0].version, labels[0].version) } else if (nLabels > 1) { - selection.range.toV = labels[0].version - selection.range.fromV = labels[1].version + this.selectVersionsForCompare(labels[0].version, labels[1].version) } } @@ -573,26 +590,27 @@ define([ _loadLabels(labels, lastUpdateToV) { let sortedLabels = this._sortLabelsByVersionAndDate(labels) + let nLabels = sortedLabels.length let hasPseudoCurrentStateLabel = false let needsPseudoCurrentStateLabel = false - if (sortedLabels.length > 0 && lastUpdateToV) { - hasPseudoCurrentStateLabel = sortedLabels[0].isPseudoCurrentStateLabel + if (lastUpdateToV) { + hasPseudoCurrentStateLabel = + nLabels > 0 ? sortedLabels[0].isPseudoCurrentStateLabel : false if (hasPseudoCurrentStateLabel) { needsPseudoCurrentStateLabel = - sortedLabels.length > 1 - ? sortedLabels[1].version !== lastUpdateToV - : false + nLabels > 1 ? sortedLabels[1].version !== lastUpdateToV : false } else { needsPseudoCurrentStateLabel = - sortedLabels[0].version !== lastUpdateToV + nLabels > 0 ? sortedLabels[0].version !== lastUpdateToV : true } if (needsPseudoCurrentStateLabel && !hasPseudoCurrentStateLabel) { - sortedLabels.unshift({ + let pseudoCurrentStateLabel = { id: '1', isPseudoCurrentStateLabel: true, version: lastUpdateToV, created_at: new Date().toISOString() - }) + } + sortedLabels.unshift(pseudoCurrentStateLabel) } else if ( !needsPseudoCurrentStateLabel && hasPseudoCurrentStateLabel @@ -638,8 +656,6 @@ define([ reloadDiff() { let { diff } = this.$scope.history.selection - // const { updates } = this.$scope.history.selection - // const { fromV, toV, pathname } = this._calculateDiffDataFromSelection() const { range, pathname } = this.$scope.history.selection const { fromV, toV } = range @@ -654,7 +670,7 @@ define([ diff.fromV === fromV && diff.toV === toV ) { - return this.ide.$q.when(true) + return } this.$scope.history.selection.diff = diff = { @@ -732,8 +748,11 @@ define([ } _isLabelSelected(label) { - if (this.$scope.history.selection.label) { - return label.id === this.$scope.history.selection.label.id + if (label) { + return ( + label.version <= this.$scope.history.selection.range.toV && + label.version >= this.$scope.history.selection.range.fromV + ) } else { return false } diff --git a/services/web/public/src/ide/history/components/historyEntriesList.js b/services/web/public/src/ide/history/components/historyEntriesList.js index 6df211d326..fcd8ca451a 100644 --- a/services/web/public/src/ide/history/components/historyEntriesList.js +++ b/services/web/public/src/ide/history/components/historyEntriesList.js @@ -11,15 +11,18 @@ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ define(['base'], function(App) { - const historyEntriesListController = function($scope, $element, $attrs) { + const historyEntriesListController = function($scope, $element, $attrs, _) { const ctrl = this ctrl.$entryListViewportEl = null + ctrl.isDragging = false + const _isEntryElVisible = function($entryEl) { const entryElTop = $entryEl.offset().top const entryElBottom = entryElTop + $entryEl.outerHeight() const entryListViewportElTop = ctrl.$entryListViewportEl.offset().top const entryListViewportElBottom = entryListViewportElTop + ctrl.$entryListViewportEl.height() + return ( entryElTop >= entryListViewportElTop && entryElBottom <= entryListViewportElBottom @@ -31,18 +34,86 @@ define(['base'], function(App) { } ctrl.onEntryLinked = function(entry, $entryEl) { if ( - entry.toV === ctrl.selectedHistoryVersion && - !_isEntryElVisible($entryEl) + !ctrl.rangeSelectionEnabled && + entry.toV === ctrl.selectedHistoryVersion ) { - return $scope.$applyAsync(() => - ctrl.$entryListViewportEl.scrollTop( - _getScrollTopPosForEntry($entryEl) - ) - ) + $scope.$applyAsync(() => { + if (!_isEntryElVisible($entryEl)) { + ctrl.$entryListViewportEl.scrollTop( + _getScrollTopPosForEntry($entryEl) + ) + } + }) } } - ctrl.$onInit = () => - (ctrl.$entryListViewportEl = $element.find('> .history-entries')) + ctrl.handleEntrySelect = entry => { + if (ctrl.rangeSelectionEnabled) { + ctrl.onRangeSelect({ + selectedToV: entry.toV, + selectedFromV: entry.fromV + }) + } else { + ctrl.onVersionSelect({ version: entry.toV }) + } + } + ctrl.setRangeToV = toV => { + if (toV > ctrl.selectedHistoryRange.fromV) { + ctrl.onRangeSelect({ + selectedToV: toV, + selectedFromV: ctrl.selectedHistoryRange.fromV + }) + } + } + ctrl.setRangeFromV = fromV => { + if (fromV < ctrl.selectedHistoryRange.toV) { + ctrl.onRangeSelect({ + selectedToV: ctrl.selectedHistoryRange.toV, + selectedFromV: fromV + }) + } + } + ctrl.initHoveredRange = () => { + ctrl.hoveredHistoryRange = { + toV: ctrl.selectedHistoryRange.toV, + fromV: ctrl.selectedHistoryRange.fromV + } + } + ctrl.resetHoveredRange = () => { + ctrl.hoveredHistoryRange = { toV: null, fromV: null } + } + ctrl.setHoveredRangeToV = toV => { + if (toV > ctrl.hoveredHistoryRange.fromV) { + $scope.$applyAsync(() => (ctrl.hoveredHistoryRange.toV = toV)) + } + } + ctrl.setHoveredRangeFromV = fromV => { + if (fromV < ctrl.hoveredHistoryRange.toV) { + $scope.$applyAsync(() => (ctrl.hoveredHistoryRange.fromV = fromV)) + } + } + ctrl.onDraggingStart = () => { + $scope.$applyAsync(() => { + ctrl.isDragging = true + ctrl.initHoveredRange() + }) + } + ctrl.onDraggingStop = (isValidDrop, boundary) => { + $scope.$applyAsync(() => { + ctrl.isDragging = false + if (!isValidDrop) { + if (boundary === 'toV') { + ctrl.setRangeToV(ctrl.hoveredHistoryRange.toV) + } else if (boundary === 'fromV') { + ctrl.setRangeFromV(ctrl.hoveredHistoryRange.fromV) + } + } + ctrl.resetHoveredRange() + }) + } + ctrl.$onInit = () => { + ctrl.$entryListViewportEl = $element.find('> .history-entries') + ctrl.resetHoveredRange() + } } return App.component('historyEntriesList', { @@ -56,8 +127,11 @@ define(['base'], function(App) { currentUser: '<', freeHistoryLimitHit: '<', currentUserIsOwner: '<', - selectedHistoryVersion: '<', - onEntrySelect: '&', + rangeSelectionEnabled: '<', + selectedHistoryVersion: ' - ctrl.historyEntriesList.onEntryLinked( - ctrl.entry, - $element.find('> .history-entry') + ctrl.isEntrySelected = function() { + if (ctrl.rangeSelectionEnabled) { + return ( + ctrl.entry.toV <= ctrl.selectedHistoryRange.toV && + ctrl.entry.fromV >= ctrl.selectedHistoryRange.fromV + ) + } else { + return ctrl.entry.toV === ctrl.selectedHistoryVersion + } + } + + ctrl.isEntryHoverSelected = function() { + return ( + ctrl.rangeSelectionEnabled && + ctrl.entry.toV <= ctrl.hoveredHistoryRange.toV && + ctrl.entry.fromV >= ctrl.hoveredHistoryRange.fromV ) + } + + ctrl.onDraggingStart = () => { + ctrl.historyEntriesList.onDraggingStart() + } + ctrl.onDraggingStop = (isValidDrop, boundary) => + ctrl.historyEntriesList.onDraggingStop(isValidDrop, boundary) + + ctrl.onDrop = boundary => { + if (boundary === 'toV') { + $scope.$applyAsync(() => + ctrl.historyEntriesList.setRangeToV(ctrl.entry.toV) + ) + } else if (boundary === 'fromV') { + $scope.$applyAsync(() => + ctrl.historyEntriesList.setRangeFromV(ctrl.entry.fromV) + ) + } + } + ctrl.onOver = boundary => { + if (boundary === 'toV') { + $scope.$applyAsync(() => + ctrl.historyEntriesList.setHoveredRangeToV(ctrl.entry.toV) + ) + } else if (boundary === 'fromV') { + $scope.$applyAsync(() => + ctrl.historyEntriesList.setHoveredRangeFromV(ctrl.entry.fromV) + ) + } + } + + ctrl.$onInit = () => { + ctrl.$entryEl = $element.find('> .history-entry') + ctrl.$entryDetailsEl = $element.find('.history-entry-details') + ctrl.$toVHandleEl = $element.find('.history-entry-toV-handle') + ctrl.$fromVHandleEl = $element.find('.history-entry-fromV-handle') + ctrl.historyEntriesList.onEntryLinked(ctrl.entry, ctrl.$entryEl) + } } return App.component('historyEntry', { @@ -61,7 +111,11 @@ define([ entry: '<', currentUser: '<', users: '<', - selectedHistoryVersion: '<', + rangeSelectionEnabled: '<', + isDragging: '<', + selectedHistoryVersion: ' 0) { + const groupedLabelsHash = _.groupBy(labels, 'version') + ctrl.versionsWithLabels = _.map( + groupedLabelsHash, + (labels, version) => { + return { + version: parseInt(version, 10), + labels + } + } + ) + } + }) + ctrl.initHoveredRange = () => { + ctrl.hoveredHistoryRange = { + toV: ctrl.selectedHistoryRange.toV, + fromV: ctrl.selectedHistoryRange.fromV + } + } + ctrl.resetHoveredRange = () => { + ctrl.hoveredHistoryRange = { toV: null, fromV: null } + } + ctrl.setHoveredRangeToV = toV => { + if (toV >= ctrl.hoveredHistoryRange.fromV) { + ctrl.hoveredHistoryRange.toV = toV + } + } + ctrl.setHoveredRangeFromV = fromV => { + if (fromV <= ctrl.hoveredHistoryRange.toV) { + ctrl.hoveredHistoryRange.fromV = fromV + } + } + + ctrl.isVersionSelected = function(version) { + if (ctrl.rangeSelectionEnabled) { + return ( + version <= ctrl.selectedHistoryRange.toV && + version >= ctrl.selectedHistoryRange.fromV + ) + } else { + return version === ctrl.selectedHistoryVersion + } + } + ctrl.isVersionHoverSelected = function(version) { + return ( + ctrl.rangeSelectionEnabled && + version <= ctrl.hoveredHistoryRange.toV && + version >= ctrl.hoveredHistoryRange.fromV + ) + } + ctrl.onDraggingStart = () => { + $scope.$applyAsync(() => { + ctrl.isDragging = true + ctrl.initHoveredRange() + }) + } + ctrl.onDraggingStop = (isValidDrop, boundary) => { + $scope.$applyAsync(() => { + if (!isValidDrop) { + if (boundary === 'toV') { + ctrl.setRangeToV(ctrl.hoveredHistoryRange.toV) + } else if (boundary === 'fromV') { + ctrl.setRangeFromV(ctrl.hoveredHistoryRange.fromV) + } + } + ctrl.isDragging = false + ctrl.resetHoveredRange() + }) + } + ctrl.onDrop = (boundary, versionWithLabel) => { + if (boundary === 'toV') { + $scope.$applyAsync(() => ctrl.setRangeToV(versionWithLabel.version)) + } else if (boundary === 'fromV') { + $scope.$applyAsync(() => ctrl.setRangeFromV(versionWithLabel.version)) + } + } + ctrl.onOver = (boundary, versionWithLabel) => { + if (boundary === 'toV') { + $scope.$applyAsync(() => + ctrl.setHoveredRangeToV(versionWithLabel.version) + ) + } else if (boundary === 'fromV') { + $scope.$applyAsync(() => + ctrl.setHoveredRangeFromV(versionWithLabel.version) + ) + } + } + ctrl.handleVersionSelect = versionWithLabel => { + if (ctrl.rangeSelectionEnabled) { + // TODO + ctrl.onRangeSelect({ + selectedToV: versionWithLabel.version, + selectedFromV: versionWithLabel.version + }) + } else { + ctrl.onVersionSelect({ version: versionWithLabel.version }) + } + } + ctrl.setRangeToV = version => { + if (version >= ctrl.selectedHistoryRange.fromV) { + ctrl.onRangeSelect({ + selectedToV: version, + selectedFromV: ctrl.selectedHistoryRange.fromV + }) + } + } + ctrl.setRangeFromV = version => { + if (version <= ctrl.selectedHistoryRange.toV) { + ctrl.onRangeSelect({ + selectedToV: ctrl.selectedHistoryRange.toV, + selectedFromV: version + }) + } + } // 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. @@ -28,30 +145,37 @@ define([ return curUserId === id }) ctrl.displayName = displayNameForUser - ctrl.getUserCSSStyle = function(user, label) { + ctrl.getUserCSSStyle = function(user, versionWithLabel) { const curUserId = (user != null ? user._id : undefined) || (user != null ? user.id : undefined) const hue = ColorManager.getHueForUserId(curUserId) || 100 if ( - label.id === - (ctrl.selectedLabel != null ? ctrl.selectedLabel.id : undefined) + ctrl.isVersionSelected(versionWithLabel.version) || + ctrl.isVersionHoverSelected(versionWithLabel.version) ) { return { color: '#FFF' } } else { return { color: `hsl(${hue}, 70%, 50%)` } } } + + ctrl.$onInit = () => { + ctrl.resetHoveredRange() + } } return App.component('historyLabelsList', { bindings: { labels: '<', + rangeSelectionEnabled: '<', users: '<', currentUser: '<', isLoading: '<', - selectedLabel: '<', - onLabelSelect: '&', + selectedHistoryVersion: ' 0) { - const groupedLabelsHash = _.groupBy(labels, 'version') - $scope.versionsWithLabels = _.map( - groupedLabelsHash, - (labels, version) => { - return { - version: parseInt(version, 10), - labels - } - } - ) - } - }) - - $scope.loadMore = () => ide.historyManager.fetchNextBatchOfUpdates() - - $scope.setHoverFrom = fromV => ide.historyManager.setHoverFrom(fromV) - - $scope.setHoverTo = toV => ide.historyManager.setHoverTo(toV) - - $scope.resetHover = () => ide.historyManager.resetHover() - - $scope.select = (toV, fromV) => { - $scope.history.selection.range.toV = toV - $scope.history.selection.range.fromV = fromV - } - - $scope.addLabelVersionToSelection = version => { - ide.historyManager.expandSelectionToVersion(version) - } - - // 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. - $scope.getUserById = id => - _.find($scope.projectUsers, function(user) { - let curUserId - if (user) { - curUserId = user._id || user.id - } - return curUserId === id - }) - - $scope.getDisplayNameById = id => - displayNameForUser($scope.getUserById(id)) - - $scope.getDisplayNameForUser = user => displayNameForUser(user) - - $scope.deleteLabel = labelDetails => - $modal.open({ - templateUrl: 'historyV2DeleteLabelModalTemplate', - controller: 'HistoryV2DeleteLabelModalController', - resolve: { - labelDetails() { - return labelDetails - } - } - }) - } - ]) -}) diff --git a/services/web/public/src/ide/history/controllers/HistoryV2ListController.js b/services/web/public/src/ide/history/controllers/HistoryV2ListController.js index 23e7e43d85..80ff7ed248 100644 --- a/services/web/public/src/ide/history/controllers/HistoryV2ListController.js +++ b/services/web/public/src/ide/history/controllers/HistoryV2ListController.js @@ -35,11 +35,18 @@ define(['base', 'ide/history/util/displayNameForUser'], ( return ide.historyManager.fetchNextBatchOfUpdates() } - $scope.handleEntrySelect = entry => - ide.historyManager.selectVersionForPointInTime(entry.toV) + $scope.handleVersionSelect = version => + $scope.$applyAsync(() => + ide.historyManager.selectVersionForPointInTime(version) + ) - $scope.handleLabelSelect = label => - ide.historyManager.selectLabelForPointInTime(label) + $scope.handleRangeSelect = (selectedToV, selectedFromV) => + $scope.$applyAsync(() => + ide.historyManager.selectVersionsForCompare( + selectedToV, + selectedFromV + ) + ) return ($scope.handleLabelDelete = labelDetails => $modal.open({ diff --git a/services/web/public/src/ide/history/controllers/HistoryV2ToolbarController.js b/services/web/public/src/ide/history/controllers/HistoryV2ToolbarController.js index 9b2a8284e8..0b19c8f54f 100644 --- a/services/web/public/src/ide/history/controllers/HistoryV2ToolbarController.js +++ b/services/web/public/src/ide/history/controllers/HistoryV2ToolbarController.js @@ -21,6 +21,9 @@ define(['base'], App => ($scope, $modal, ide, event_tracking, waitFor) => { let openEntity + $scope.currentUpdate = null + $scope.currentLabel = null + $scope.restoreState = { inflight: false, error: false @@ -50,6 +53,22 @@ define(['base'], App => } }) + $scope.$watch('history.viewMode', (newVal, oldVal) => { + if (newVal != null && newVal !== oldVal) { + $scope.currentUpdate = ide.historyManager.getUpdateForVersion(newVal) + } + }) + + $scope.$watch('history.selection.range.toV', (newVal, oldVal) => { + if ( + newVal != null && + newVal !== oldVal && + $scope.history.viewMode === $scope.HistoryViewModes.POINT_IN_TIME + ) { + $scope.currentUpdate = ide.historyManager.getUpdateForVersion(newVal) + } + }) + $scope.toggleHistoryViewMode = () => { ide.historyManager.toggleHistoryViewMode() } diff --git a/services/web/public/src/ide/history/directives/historyDraggableBoundary.js b/services/web/public/src/ide/history/directives/historyDraggableBoundary.js new file mode 100644 index 0000000000..edc4c99fe0 --- /dev/null +++ b/services/web/public/src/ide/history/directives/historyDraggableBoundary.js @@ -0,0 +1,32 @@ +define(['base'], App => + App.directive('historyDraggableBoundary', () => ({ + scope: { + historyDraggableBoundary: '@', + historyDraggableBoundaryOnDragStart: '&', + historyDraggableBoundaryOnDragStop: '&' + }, + restrict: 'A', + link(scope, element, attrs) { + element.data('selectionBoundary', { + boundary: scope.historyDraggableBoundary + }) + element.draggable({ + axis: 'y', + opacity: false, + helper: 'clone', + revert: true, + scroll: true, + cursor: 'row-resize', + start(e, ui) { + ui.helper.data('wasProperlyDropped', false) + scope.historyDraggableBoundaryOnDragStart() + }, + stop(e, ui) { + scope.historyDraggableBoundaryOnDragStop({ + isValidDrop: ui.helper.data('wasProperlyDropped'), + boundary: scope.historyDraggableBoundary + }) + } + }) + } + }))) diff --git a/services/web/public/src/ide/history/directives/historyDroppableArea.js b/services/web/public/src/ide/history/directives/historyDroppableArea.js new file mode 100644 index 0000000000..40ce290bbd --- /dev/null +++ b/services/web/public/src/ide/history/directives/historyDroppableArea.js @@ -0,0 +1,25 @@ +define(['base'], App => + App.directive('historyDroppableArea', () => ({ + scope: { + historyDroppableAreaOnDrop: '&', + historyDroppableAreaOnOver: '&', + historyDroppableAreaOnOut: '&' + }, + restrict: 'A', + link(scope, element, attrs) { + element.droppable({ + accept: e => '.history-entry-toV-handle, .history-entry-fromV-handle', + drop: (e, ui) => { + const draggedBoundary = ui.draggable.data('selectionBoundary') + .boundary + ui.helper.data('wasProperlyDropped', true) + scope.historyDroppableAreaOnDrop({ boundary: draggedBoundary }) + }, + over: (e, ui) => { + const draggedBoundary = ui.draggable.data('selectionBoundary') + .boundary + scope.historyDroppableAreaOnOver({ boundary: draggedBoundary }) + } + }) + } + }))) diff --git a/services/web/public/stylesheets/app/editor/history-v2.less b/services/web/public/stylesheets/app/editor/history-v2.less index c2c504fc6e..866abbc432 100644 --- a/services/web/public/stylesheets/app/editor/history-v2.less +++ b/services/web/public/stylesheets/app/editor/history-v2.less @@ -69,6 +69,10 @@ color: @history-base-color; height: 100%; background-color: @history-base-bg; + position: relative; + &.history-entries-dragging { + cursor: row-resize; + } } .history-entry-day { @@ -79,18 +83,82 @@ line-height: 1; } +.history-entry-toV-handle, +.history-entry-fromV-handle { + position: absolute; + background-color: @history-entry-handle-bg; + height: @history-entry-handle-height; + top: 0; + left: 0; + right: 0; + z-index: 2; + cursor: row-resize; + + &.ui-draggable-dragging { + opacity: 0; + } + + &::after { + content: '\00b7\00b7\00b7\00b7'; + position: absolute; + text-align: center; + -webkit-font-smoothing: antialiased; + width: 100%; + font-size: 20px; + color: #FFF; + height: @history-entry-handle-height; + line-height: @history-entry-handle-height / 2; + } +} + +.history-entry-fromV-handle { + top: auto; + bottom: 0; +} + .history-entry-details { + position: relative; background-color: #FFF; - margin-bottom: 2px; + border-bottom: solid 2px @history-base-bg; padding: 5px 10px; cursor: pointer; +} - .history-entry-selected &, - .history-entry-label-selected & { +.history-version-with-label { + .history-entry-details; + padding: 7px 10px; +} + + .history-entry-selected .history-entry-details, + .history-version-with-label-selected & { background-color: @history-entry-selected-bg; color: #FFF; } -} + + .history-entry-hover-selected .history-entry-details, + .history-entry-hover-selected.history-entry-selected .history-entry-details, + .history-version-with-label-hover-selected &, + .history-version-with-label-hover-selected.history-entry-selected &, { + background-color: tint(@history-entry-selected-bg, 20%); + color: #FFF; + } + + .history-entry-selected-to .history-entry-details, + .history-entry-hover-selected-to .history-entry-details, + .history-version-with-label-selected-to &, + .history-version-with-label-hover-selected-to & { + padding-top: @history-entry-handle-height + 5px; + } + + .history-entry-selected-from .history-entry-details, + .history-entry-hover-selected-from .history-entry-details, + .history-version-with-label-selected-from &, + .history-version-with-label-hover-selected-from & { + padding-bottom: @history-entry-handle-height + 5px; + } + + + .history-label { display: inline-block; color: @history-entry-label-color; @@ -99,12 +167,16 @@ margin-right: 10px; white-space: nowrap; .history-entry-selected &, - .history-entry-label-selected & { + .history-entry-hover-selected &, + .history-version-with-label-selected &, + .history-version-with-label-hover-selected & { color: @history-entry-selected-label-color; } &.history-label-pseudo-current-state { .history-entry-selected &, - .history-entry-label-selected & { + .history-entry-hover-selected &, + .history-version-with-label-selected &, + .history-version-with-label-hover-selected & { color: @history-entry-selected-pseudo-label-color; } } @@ -119,7 +191,9 @@ } .history-entry-selected &, - .history-entry-label-selected & { + .history-entry-hover-selected &, + .history-version-with-label-selected &, + .history-version-with-label-hover-selected & { background-color: @history-entry-selected-label-bg-color; } } @@ -142,7 +216,9 @@ &:hover { background-color: darken(@history-entry-label-bg-color, 8%); .history-entry-selected &, - .history-entry-label-selected & { + .history-entry-hover-selected &, + .history-version-with-label-selected &, + .history-version-with-label-hover-selected & { background-color: darken(@history-entry-selected-label-bg-color, 8%); } } @@ -181,7 +257,8 @@ font-weight: bold; word-break: break-all; .history-entry-selected &, - .history-entry-label-selected & { + .history-entry-hover-selected &, + .history-version-with-label-selected & { color: #FFF; } } @@ -223,14 +300,6 @@ .history-labels-list-compare { background-color: transparent; } - .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; diff --git a/services/web/public/stylesheets/core/_common-variables.less b/services/web/public/stylesheets/core/_common-variables.less index a4ad6055b4..1e76cd9d82 100644 --- a/services/web/public/stylesheets/core/_common-variables.less +++ b/services/web/public/stylesheets/core/_common-variables.less @@ -1030,6 +1030,8 @@ @history-entry-selected-pseudo-label-color: @green; @history-entry-day-bg : @gray; @history-entry-selected-bg : @red; +@history-entry-handle-bg : darken(@history-entry-selected-bg, 10%); +@history-entry-handle-height : 8px; @history-base-color : @gray-light; @history-highlight-color : @gray; @history-toolbar-bg-color : @toolbar-alt-bg-color; diff --git a/services/web/public/stylesheets/core/ol-variables.less b/services/web/public/stylesheets/core/ol-variables.less index 0188b14b80..8971d42577 100644 --- a/services/web/public/stylesheets/core/ol-variables.less +++ b/services/web/public/stylesheets/core/ol-variables.less @@ -346,6 +346,8 @@ @history-entry-selected-pseudo-label-color: @ol-green; @history-entry-day-bg : @ol-blue-gray-2; @history-entry-selected-bg : @ol-green; +@history-entry-handle-bg : darken(@ol-green, 10%); +@history-entry-handle-height : 8px; @history-base-color : @ol-blue-gray-2; @history-highlight-color : @ol-type-color; @history-toolbar-bg-color : @editor-toolbar-bg; diff --git a/services/web/test/unit_frontend/src/ide/history/HistoryV2ManagerTests.js b/services/web/test/unit_frontend/src/ide/history/HistoryV2ManagerTests.js index 7a8799a8b0..1ba33255aa 100644 --- a/services/web/test/unit_frontend/src/ide/history/HistoryV2ManagerTests.js +++ b/services/web/test/unit_frontend/src/ide/history/HistoryV2ManagerTests.js @@ -34,8 +34,6 @@ define(['ide/history/HistoryV2Manager'], HistoryV2Manager => }, diff: null, files: [], - update: null, - label: null, file: null }, error: null,