Explorar el Código

Merge pull request #796 from sharelatex/pr-history-labels-ui

History labels UI
James Allen hace 8 años
padre
commit
69e8eddff0

+ 49 - 0
services/web/app/views/project/editor/history.pug

@@ -67,3 +67,52 @@ script(type="text/ng-template", id="historyRestoreDiffModalTemplate")
 		)
 		)
 			span(ng-show="!state.inflight") #{translate("restore")}
 			span(ng-show="!state.inflight") #{translate("restore")}
 			span(ng-show="state.inflight") #{translate("restoring")} ...
 			span(ng-show="state.inflight") #{translate("restoring")} ...
+
+
+script(type="text/ng-template", id="historyLabelTpl")
+	.history-entry-label(
+		ng-class="{ 'history-entry-label-own' : $ctrl.isOwnedByCurrentUser }"
+	)
+		span.history-entry-label-comment(
+			tooltip-append-to-body="true"
+			tooltip-template="'historyLabelTooltipTpl'"
+			tooltip-placement="left"
+		)
+			i.fa.fa-tag
+			|  {{ $ctrl.labelText }}
+		button.history-entry-label-delete-btn(
+			ng-if="$ctrl.isOwnedByCurrentUser"
+			stop-propagation="click"
+			ng-click="$ctrl.onLabelDelete()"
+		) ×
+
+script(type="text/ng-template", id="historyLabelTooltipTpl")
+	.history-label-tooltip
+		p.history-label-tooltip-title 
+			i.fa.fa-tag
+			|  {{ $ctrl.labelText }}
+		p.history-label-tooltip-owner #{translate("history_label_created_by")} {{ $ctrl.labelOwnerName }}
+		time.history-label-tooltip-datetime {{ labelCreationDateTime | formatDate }}
+
+
+script(type="text/ng-template", id="historyV2DeleteLabelModalTemplate")
+	.modal-header
+		h3 #{translate("history_delete_label")}
+	.modal-body
+		.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} 
+		.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
+		p.help-block(ng-if="labelDetails")
+			| #{translate("history_are_you_sure_delete_label")}
+			strong "{{ labelDetails.comment }}"
+			| ?
+	.modal-footer
+		button.btn.btn-default(
+			type="button"
+			ng-disabled="state.inflight"
+			ng-click="$dismiss()"
+		) #{translate("cancel")}
+		button.btn.btn-primary(
+			type="button"
+			ng-click="deleteLabel()"
+			ng-disabled="state.inflight"
+		) {{ state.inflight ? '#{translate("history_deleting_label")}' : '#{translate("history_delete_label")}' }}

+ 22 - 0
services/web/app/views/project/editor/history/entriesListV2.pug

@@ -5,11 +5,13 @@ aside.change-list(
 	history-entries-list(
 	history-entries-list(
 		entries="history.updates"
 		entries="history.updates"
 		current-user="user"
 		current-user="user"
+		users="projectUsers"
 		load-entries="loadMore()"
 		load-entries="loadMore()"
 		load-disabled="history.loading || history.atEnd"
 		load-disabled="history.loading || history.atEnd"
 		load-initialize="ui.view == 'history'"
 		load-initialize="ui.view == 'history'"
 		is-loading="history.loading"
 		is-loading="history.loading"
 		on-entry-select="handleEntrySelect(selectedEntry)"
 		on-entry-select="handleEntrySelect(selectedEntry)"
+		on-label-delete="handleLabelDelete(label)"
 	)
 	)
 
 
 aside.change-list(
 aside.change-list(
@@ -65,6 +67,14 @@ aside.change-list(
 						)
 						)
 
 
 				div.description(ng-click="select()")
 				div.description(ng-click="select()")
+					history-label(
+						ng-repeat="label in update.labels"
+						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.time {{ update.meta.end_ts | formatDate:'h:mm a' }}
 					div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
 					div.action.action-edited(ng-if="history.isV2 && update.pathnames.length > 0")
 						| #{translate("file_action_edited")}
 						| #{translate("file_action_edited")}
@@ -106,7 +116,9 @@ script(type="text/ng-template", id="historyEntriesListTpl")
 				ng-repeat="entry in $ctrl.entries"
 				ng-repeat="entry in $ctrl.entries"
 				entry="entry"
 				entry="entry"
 				current-user="$ctrl.currentUser"
 				current-user="$ctrl.currentUser"
+				users="$ctrl.users"
 				on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })"
 				on-select="$ctrl.onEntrySelect({ selectedEntry: selectedEntry })"
+				on-label-delete="$ctrl.onLabelDelete({ label: label })"
 				ng-show="!$ctrl.isLoading"
 				ng-show="!$ctrl.isLoading"
 			)
 			)
 			.loading(ng-show="$ctrl.isLoading")
 			.loading(ng-show="$ctrl.isLoading")
@@ -129,6 +141,15 @@ script(type="text/ng-template", id="historyEntryTpl")
 		time.history-entry-day(ng-if="::$ctrl.entry.meta.first_in_day") {{ ::$ctrl.entry.meta.end_ts | relativeDate }}
 		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-label(
+				ng-repeat="label in $ctrl.entry.labels"
+				label-text="label.comment"
+				label-owner-name="$ctrl.displayNameById(label.user_id)"
+				label-creation-date-time="label.created_at"
+				is-owned-by-current-user="label.user_id === $ctrl.currentUser.id"
+				on-label-delete="$ctrl.onLabelDelete({ label: label })"
+			)
+
 			ol.history-entry-changes
 			ol.history-entry-changes
 				li.history-entry-change(
 				li.history-entry-change(
 					ng-repeat="pathname in ::$ctrl.entry.pathnames"
 					ng-repeat="pathname in ::$ctrl.entry.pathnames"
@@ -148,6 +169,7 @@ script(type="text/ng-template", id="historyEntryTpl")
 						ng-if="::project_op.remove"
 						ng-if="::project_op.remove"
 					) #{translate("file_action_deleted")}
 					) #{translate("file_action_deleted")}
 					span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }}
 					span.history-entry-change-doc {{ ::$ctrl.getProjectOpDoc(project_op) }}
+
 			.history-entry-metadata
 			.history-entry-metadata
 				time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }}
 				time.history-entry-metadata-time {{ ::$ctrl.entry.meta.end_ts | formatDate:'h:mm a' }}
 				span
 				span

+ 45 - 3
services/web/app/views/project/editor/history/toolbarV2.pug

@@ -1,4 +1,5 @@
 .history-toolbar(
 .history-toolbar(
+	ng-controller="HistoryV2ToolbarController"
 	ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
 	ng-if="ui.view == 'history' && history.isV2 && history.viewMode === HistoryViewModes.POINT_IN_TIME"
 )
 )
 	span(ng-show="history.loadingFileTree")
 	span(ng-show="history.loadingFileTree")
@@ -6,8 +7,49 @@
 		|    #{translate("loading")}...
 		|    #{translate("loading")}...
 	span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")} 
 	span(ng-show="!history.loadingFileTree") #{translate("browsing_project_as_of")} 
 		time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }}
 		time.history-toolbar-time {{ history.selection.updates[0].meta.end_ts | formatDate:'Do MMM YYYY, h:mm a' }}
-	.history-toolbar-btn(
+	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-click="toggleHistoryViewMode();"
+		ng-disabled="history.loadingFileTree"
 	) 
 	) 
-		i.fa
-		| #{translate("compare_to_another_version")}
+		i.fa.fa-exchange
+		|  #{translate("compare_to_another_version")}
+
+script(type="text/ng-template", id="historyV2AddLabelModalTemplate")
+	form(
+		name="addLabelModalForm"
+		ng-submit="addLabelModalFormSubmit();"
+		novalidate
+	)
+		.modal-header
+			h3 #{translate("history_add_label")}
+		.modal-body
+			.alert.alert-danger(ng-show="state.error.message") {{ state.error.message}} 
+			.alert.alert-danger(ng-show="state.error && !state.error.message") #{translate("generic_something_went_wrong")}
+			.form-group
+				input.form-control(
+					type="text"
+					placeholder=translate("history_new_label_name")
+					ng-model="inputs.labelName"
+					focus-on="open"
+					required
+				)
+			p.help-block(ng-if="update")
+				| #{translate("history_new_label_added_at")} 
+				strong {{ update.meta.end_ts | formatDate:'ddd Do MMM YYYY, h:mm a' }}
+		.modal-footer
+			button.btn.btn-default(
+				type="button"
+				ng-disabled="state.inflight"
+				ng-click="$dismiss()"
+			) #{translate("cancel")}
+			input.btn.btn-primary(
+				ng-disabled="addLabelModalForm.$invalid || state.inflight"
+				ng-value="state.inflight ? '" + translate("history_adding_label") + "' : '" + translate("history_add_label") + "'"
+				type="submit"
+			)

+ 51 - 8
services/web/public/coffee/ide/history/HistoryV2Manager.coffee

@@ -6,9 +6,13 @@ define [
 	"ide/history/controllers/HistoryV2ListController"
 	"ide/history/controllers/HistoryV2ListController"
 	"ide/history/controllers/HistoryV2DiffController"
 	"ide/history/controllers/HistoryV2DiffController"
 	"ide/history/controllers/HistoryV2FileTreeController"
 	"ide/history/controllers/HistoryV2FileTreeController"
+	"ide/history/controllers/HistoryV2ToolbarController"
+	"ide/history/controllers/HistoryV2AddLabelModalController"
+	"ide/history/controllers/HistoryV2DeleteLabelModalController"
 	"ide/history/directives/infiniteScroll"
 	"ide/history/directives/infiniteScroll"
 	"ide/history/components/historyEntriesList"
 	"ide/history/components/historyEntriesList"
 	"ide/history/components/historyEntry"
 	"ide/history/components/historyEntry"
+	"ide/history/components/historyLabel"
 	"ide/history/components/historyFileTree"
 	"ide/history/components/historyFileTree"
 	"ide/history/components/historyFileEntity"
 	"ide/history/components/historyFileEntity"
 ], (moment, ColorManager, displayNameForUser, HistoryViewModes) ->
 ], (moment, ColorManager, displayNameForUser, HistoryViewModes) ->
@@ -68,6 +72,7 @@ define [
 						toV: null
 						toV: null
 					}
 					}
 				}
 				}
+				labels: null
 				files: []
 				files: []
 				diff: null # When history.viewMode == HistoryViewModes.COMPARE
 				diff: null # When history.viewMode == HistoryViewModes.COMPARE
 				selectedFile: null # When history.viewMode == HistoryViewModes.POINT_IN_TIME
 				selectedFile: null # When history.viewMode == HistoryViewModes.POINT_IN_TIME
@@ -126,18 +131,19 @@ define [
 
 
 		BATCH_SIZE: 10
 		BATCH_SIZE: 10
 		fetchNextBatchOfUpdates: () ->
 		fetchNextBatchOfUpdates: () ->
-			url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
+			updatesURL = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}"
 			if @$scope.history.nextBeforeTimestamp?
 			if @$scope.history.nextBeforeTimestamp?
-				url += "&before=#{@$scope.history.nextBeforeTimestamp}"
+				updatesURL += "&before=#{@$scope.history.nextBeforeTimestamp}"
+			
 			@$scope.history.loading = true
 			@$scope.history.loading = true
 			@$scope.history.loadingFileTree = true
 			@$scope.history.loadingFileTree = true
-			@ide.$http
-				.get(url)
+			
+			@ide.$http.get updatesURL
 				.then (response) =>
 				.then (response) =>
-					{ data } = response
-					@_loadUpdates(data.updates)
-					@$scope.history.nextBeforeTimestamp = data.nextBeforeTimestamp
-					if !data.nextBeforeTimestamp?
+					updatesData = response.data
+					@_loadUpdates(updatesData.updates)
+					@$scope.history.nextBeforeTimestamp = updatesData.nextBeforeTimestamp
+					if !updatesData.nextBeforeTimestamp?
 						@$scope.history.atEnd = true
 						@$scope.history.atEnd = true
 					@$scope.history.loading = false
 					@$scope.history.loading = false
 
 
@@ -199,6 +205,27 @@ define [
 					diff.loading = false
 					diff.loading = false
 					diff.error = true
 					diff.error = true
 
 
+		labelCurrentVersion: (labelComment) => 
+			@_labelVersion labelComment, @$scope.history.selection.updates[0].toV
+
+		deleteLabel: (labelId) =>
+			url = "/project/#{@$scope.project_id}/labels/#{labelId}"
+
+			@ide.$http({
+				url,
+				method: "DELETE"
+				headers:
+					"X-CSRF-Token": window.csrfToken
+			}).then (response) =>
+				@_deleteLabelFromLocalCollection @$scope.history.updates, labelId
+				@_deleteLabelFromLocalCollection @$scope.history.selection, labelId
+
+
+		_deleteLabelFromLocalCollection: (collection, labelId) ->
+			for update in collection
+				update.labels = _.filter update.labels, (label) -> 
+					label.id != labelId
+
 		_parseDiff: (diff) ->
 		_parseDiff: (diff) ->
 			if diff.binary
 			if diff.binary
 				return { binary: true }
 				return { binary: true }
@@ -278,6 +305,22 @@ define [
 				else 
 				else 
 					@autoSelectLastUpdate()
 					@autoSelectLastUpdate()
 
 
+		_labelVersion: (comment, version) ->
+			url = "/project/#{@$scope.project_id}/labels"
+			@ide.$http
+				.post url, {
+					comment,
+					version,
+					_csrf: window.csrfToken
+				}
+				.then (response) =>
+					@_addLabelToLocalUpdate response.data
+
+		_addLabelToLocalUpdate: (label) ->
+			localUpdate = _.find @$scope.history.updates, (update) -> update.toV == label.version
+			if localUpdate?
+				localUpdate.labels.push label
+
 		_perDocSummaryOfUpdates: (updates) ->
 		_perDocSummaryOfUpdates: (updates) ->
 			# Track current_pathname -> original_pathname
 			# Track current_pathname -> original_pathname
 			# create bare object for use as Map
 			# create bare object for use as Map

+ 2 - 0
services/web/public/coffee/ide/history/components/historyEntriesList.coffee

@@ -8,12 +8,14 @@ define [
 	App.component "historyEntriesList", {
 	App.component "historyEntriesList", {
 		bindings:
 		bindings:
 			entries: "<"
 			entries: "<"
+			users: "<"
 			loadEntries: "&"
 			loadEntries: "&"
 			loadDisabled: "<"
 			loadDisabled: "<"
 			loadInitialize: "<"
 			loadInitialize: "<"
 			isLoading: "<"
 			isLoading: "<"
 			currentUser: "<"
 			currentUser: "<"
 			onEntrySelect: "&"
 			onEntrySelect: "&"
+			onLabelDelete: "&"
 		controller: historyEntriesListController
 		controller: historyEntriesListController
 		templateUrl: "historyEntriesListTpl"
 		templateUrl: "historyEntriesListTpl"
 	}
 	}

+ 12 - 1
services/web/public/coffee/ide/history/components/historyEntry.coffee

@@ -2,9 +2,18 @@ define [
 	"base"
 	"base"
 	"ide/history/util/displayNameForUser"
 	"ide/history/util/displayNameForUser"
 ], (App, displayNameForUser) ->
 ], (App, displayNameForUser) ->
-	historyEntryController = ($scope, $element, $attrs) ->
+	historyEntryController = ($scope, $element, $attrs, _) ->
 		ctrl = @
 		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.
+		_getUserById = (id) ->
+			_.find ctrl.users, (user) ->
+				curUserId = user?._id or user?.id
+				curUserId == id
 		ctrl.displayName = displayNameForUser
 		ctrl.displayName = displayNameForUser
+		ctrl.displayNameById = (id) ->
+			displayNameForUser(_getUserById(id))
 		ctrl.getProjectOpDoc = (projectOp) ->
 		ctrl.getProjectOpDoc = (projectOp) ->
 			if projectOp.rename? then "#{ projectOp.rename.pathname} → #{ projectOp.rename.newPathname }"
 			if projectOp.rename? then "#{ projectOp.rename.pathname} → #{ projectOp.rename.newPathname }"
 			else if projectOp.add? then "#{ projectOp.add.pathname}"
 			else if projectOp.add? then "#{ projectOp.add.pathname}"
@@ -21,7 +30,9 @@ define [
 		bindings:
 		bindings:
 			entry: "<"
 			entry: "<"
 			currentUser: "<"
 			currentUser: "<"
+			users: "<"
 			onSelect: "&"
 			onSelect: "&"
+			onLabelDelete: "&"
 		controller: historyEntryController
 		controller: historyEntryController
 		templateUrl: "historyEntryTpl"
 		templateUrl: "historyEntryTpl"
 	}
 	}

+ 17 - 0
services/web/public/coffee/ide/history/components/historyLabel.coffee

@@ -0,0 +1,17 @@
+define [
+	"base"
+], (App) ->
+	historyLabelController = ($scope, $element, $attrs, $filter, _) ->
+		ctrl = @
+		return
+
+	App.component "historyLabel", {
+		bindings:
+			labelText: "<"
+			labelOwnerName: "<"
+			labelCreationDateTime: "<"
+			isOwnedByCurrentUser: "<"
+			onLabelDelete: "&"
+		controller: historyLabelController
+		templateUrl: "historyLabelTpl"
+	}

+ 23 - 2
services/web/public/coffee/ide/history/controllers/HistoryListController.coffee

@@ -3,9 +3,30 @@ define [
 	"ide/history/util/displayNameForUser"
 	"ide/history/util/displayNameForUser"
 ], (App, displayNameForUser) ->
 ], (App, displayNameForUser) ->
 
 
-	App.controller "HistoryListController", ["$scope", "ide", ($scope, ide) ->
+	App.controller "HistoryListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
 		$scope.hoveringOverListSelectors = false
 		$scope.hoveringOverListSelectors = false
-		
+
+		projectUsers = $scope.project.members.concat $scope.project.owner
+
+		# 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.
+		_getUserById = (id) ->
+			_.find projectUsers, (user) ->
+				curUserId = user?._id or user?.id
+				curUserId == id
+
+		$scope.getDisplayNameById = (id) ->
+			displayNameForUser(_getUserById(id))
+
+		$scope.deleteLabel = (labelDetails) ->
+			$modal.open(
+				templateUrl: "historyV2DeleteLabelModalTemplate"
+				controller: "HistoryV2DeleteLabelModalController"
+				resolve:
+					labelDetails: () -> labelDetails
+			)
+
 		$scope.loadMore = () =>
 		$scope.loadMore = () =>
 			ide.historyManager.fetchNextBatchOfUpdates()
 			ide.historyManager.fetchNextBatchOfUpdates()
 
 

+ 29 - 0
services/web/public/coffee/ide/history/controllers/HistoryV2AddLabelModalController.coffee

@@ -0,0 +1,29 @@
+define [
+	"base",
+], (App) ->
+	App.controller "HistoryV2AddLabelModalController", ["$scope", "$modalInstance", "ide", "update", ($scope, $modalInstance, ide, update) ->
+		$scope.update = update
+		$scope.inputs = 
+			labelName: null
+		$scope.state =
+			inflight: false
+			error: false
+			
+		$modalInstance.opened.then () ->
+			$scope.$applyAsync () ->
+				$scope.$broadcast "open"
+
+		$scope.addLabelModalFormSubmit = () ->
+			$scope.state.inflight = true
+			ide.historyManager.labelCurrentVersion $scope.inputs.labelName
+				.then (response) ->
+					$scope.state.inflight = false
+					$modalInstance.close()
+				.catch (response) ->
+					{ data, status } = response
+					$scope.state.inflight = false
+					if status == 400
+						$scope.state.error = { message: data }
+					else
+						$scope.state.error = true
+	]

+ 23 - 0
services/web/public/coffee/ide/history/controllers/HistoryV2DeleteLabelModalController.coffee

@@ -0,0 +1,23 @@
+define [
+	"base",
+], (App) ->
+	App.controller "HistoryV2DeleteLabelModalController", ["$scope", "$modalInstance", "ide", "labelDetails", ($scope, $modalInstance, ide, labelDetails) ->
+		$scope.labelDetails = labelDetails
+		$scope.state =
+			inflight: false
+			error: false
+
+		$scope.deleteLabel = () ->
+			$scope.state.inflight = true
+			ide.historyManager.deleteLabel labelDetails.id
+				.then (response) ->
+					$scope.state.inflight = false
+					$modalInstance.close()
+				.catch (response) ->
+					{ data, status } = response
+					$scope.state.inflight = false
+					if status == 400
+						$scope.state.error = { message: data }
+					else
+						$scope.state.error = true
+	]

+ 12 - 2
services/web/public/coffee/ide/history/controllers/HistoryV2ListController.coffee

@@ -3,17 +3,27 @@ define [
 	"ide/history/util/displayNameForUser"
 	"ide/history/util/displayNameForUser"
 ], (App, displayNameForUser) ->
 ], (App, displayNameForUser) ->
 
 
-	App.controller "HistoryV2ListController", ["$scope", "ide", ($scope, ide) ->
+	App.controller "HistoryV2ListController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
 		$scope.hoveringOverListSelectors = false
 		$scope.hoveringOverListSelectors = false
+		$scope.listConfig =
+			showOnlyLabelled: false
+		$scope.projectUsers = $scope.project.members.concat $scope.project.owner
 		
 		
 		$scope.loadMore = () =>
 		$scope.loadMore = () =>
 			ide.historyManager.fetchNextBatchOfUpdates()
 			ide.historyManager.fetchNextBatchOfUpdates()
 
 
 		$scope.handleEntrySelect = (entry) ->
 		$scope.handleEntrySelect = (entry) ->
-			# $scope.$applyAsync () ->
 			ide.historyManager.selectUpdate(entry)
 			ide.historyManager.selectUpdate(entry)
 			$scope.recalculateSelectedUpdates()
 			$scope.recalculateSelectedUpdates()
 
 
+		$scope.handleLabelDelete = (labelDetails) ->
+			$modal.open(
+				templateUrl: "historyV2DeleteLabelModalTemplate"
+				controller: "HistoryV2DeleteLabelModalController"
+				resolve:
+					labelDetails: () -> labelDetails
+			)
+
 		$scope.recalculateSelectedUpdates = () ->
 		$scope.recalculateSelectedUpdates = () ->
 			beforeSelection = true
 			beforeSelection = true
 			afterSelection = false
 			afterSelection = false

+ 12 - 0
services/web/public/coffee/ide/history/controllers/HistoryV2ToolbarController.coffee

@@ -0,0 +1,12 @@
+define [
+	"base",
+], (App) ->
+	App.controller "HistoryV2ToolbarController", ["$scope", "$modal", "ide", ($scope, $modal, ide) ->
+		$scope.showAddLabelDialog = () ->
+			$modal.open(
+				templateUrl: "historyV2AddLabelModalTemplate"
+				controller: "HistoryV2AddLabelModalController"
+				resolve:
+					update: () -> $scope.history.selection.updates[0]
+			)
+	]

+ 62 - 0
services/web/public/stylesheets/app/editor/history-v2.less

@@ -53,6 +53,68 @@
 		color: #FFF;
 		color: #FFF;
 	}
 	}
 }
 }
+
+	.history-entry-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 & {
+			color: @history-entry-selected-label-color;
+		}
+	}
+		.history-entry-label-comment,
+		.history-entry-label-delete-btn {
+			padding: 0 @padding-xs-horizontal 1px @padding-xs-horizontal;
+			border: 0;
+			background-color: @history-entry-label-bg-color;
+			.history-entry-selected & {
+				background-color: @history-entry-selected-label-bg-color;
+			}
+		}
+		.history-entry-label-comment {
+			display: block;
+			float: left;
+			border-radius: 9999px;
+			max-width: 190px;
+			overflow: hidden;
+			text-overflow: ellipsis;
+			.history-entry-label-own & {
+				padding-right: (@padding-xs-horizontal / 2);
+				border-radius: 9999px 0 0 9999px;
+			}
+		}
+		.history-entry-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 & {
+					background-color: darken(@history-entry-selected-label-bg-color, 8%);
+				}
+			}
+		}
+
+	.history-label-tooltip {
+		white-space: normal;
+		padding: (@line-height-computed / 4);
+		text-align: left;
+	}
+		.history-label-tooltip-title,
+		.history-label-tooltip-owner,
+		.history-label-tooltip-datetime {
+			margin: 0 0 (@line-height-computed / 4) 0;
+		}  
+		.history-label-tooltip-title {
+			font-weight: bold;
+		}
+		.history-label-tooltip-datetime {
+			margin-bottom: 0;
+		}
+
 	.history-entry-changes {
 	.history-entry-changes {
 		.list-unstyled;
 		.list-unstyled;
 		margin-bottom: 3px;
 		margin-bottom: 3px;

+ 12 - 8
services/web/public/stylesheets/core/_common-variables.less

@@ -986,14 +986,18 @@
 @sys-msg-border     : 1px solid @common-border-color;
 @sys-msg-border     : 1px solid @common-border-color;
 
 
 // v2 History
 // v2 History
-@history-base-font-size           : @font-size-small;
-@history-base-bg                  : @gray-lightest;
-@history-entry-day-bg             : @gray;
-@history-entry-selected-bg        : @red;
-@history-base-color               : @gray-light;
-@history-highlight-color          : @gray;
-@history-toolbar-bg-color         : @toolbar-alt-bg-color;
-@history-toolbar-color            : @text-color;
+@history-base-font-size                : @font-size-small;
+@history-base-bg                       : @gray-lightest;
+@history-entry-label-bg-color          : @red;
+@history-entry-label-color             : #FFF;
+@history-entry-selected-label-bg-color : #FFF;
+@history-entry-selected-label-color    : @red;
+@history-entry-day-bg                  : @gray;
+@history-entry-selected-bg             : @red;
+@history-base-color                    : @gray-light;
+@history-highlight-color               : @gray;
+@history-toolbar-bg-color              : @toolbar-alt-bg-color;
+@history-toolbar-color                 : @text-color;
 
 
 // Input suggestions
 // Input suggestions
 @input-suggestion-v-offset        : 6px;
 @input-suggestion-v-offset        : 6px;

+ 12 - 8
services/web/public/stylesheets/core/ol-variables.less

@@ -275,14 +275,18 @@
 
 
 
 
 // v2 History
 // v2 History
-@history-base-font-size          : @font-size-small;
-@history-base-bg                 : @ol-blue-gray-1;
-@history-entry-day-bg            : @ol-blue-gray-2;
-@history-entry-selected-bg       : @ol-green;
-@history-base-color              : @ol-blue-gray-2;
-@history-highlight-color         : @ol-type-color;
-@history-toolbar-bg-color        : @editor-toolbar-bg;
-@history-toolbar-color           : #FFF;
+@history-base-font-size               : @font-size-small;
+@history-base-bg                      : @ol-blue-gray-1;
+@history-entry-label-bg-color         : @ol-blue;
+@history-entry-label-color            : #FFF;
+@history-entry-selected-label-bg-color: #FFF;
+@history-entry-selected-label-color   : @ol-blue;
+@history-entry-day-bg                 : @ol-blue-gray-2;
+@history-entry-selected-bg            : @ol-green;
+@history-base-color                   : @ol-blue-gray-2;
+@history-highlight-color              : @ol-type-color;
+@history-toolbar-bg-color             : @editor-toolbar-bg;
+@history-toolbar-color                : #FFF;
 
 
 // System messages
 // System messages
 @sys-msg-background             : @ol-blue;
 @sys-msg-background             : @ol-blue;

+ 1 - 0
services/web/test/unit_frontend/coffee/ide/history/HistoryV2ManagerTests.coffee

@@ -23,6 +23,7 @@ define ['ide/history/HistoryV2Manager'], (HistoryV2Manager) ->
 						toV: null
 						toV: null
 					}
 					}
 				}
 				}
+				labels: null
 				diff: null
 				diff: null
 				files: []
 				files: []
 				selectedFile: null
 				selectedFile: null