Explorar el Código

Merge pull request #498 from sharelatex/pr-bulk-actions

Bulk actions
Paulo Jorge Reis hace 9 años
padre
commit
04585c59be

+ 11 - 8
services/web/app/coffee/Features/DocumentUpdater/DocumentUpdaterHandler.coffee

@@ -123,20 +123,23 @@ module.exports = DocumentUpdaterHandler =
 				logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
 				logger.error project_id:project_id, doc_id:doc_id, url: url, "doc updater returned a non-success status code: #{res.statusCode}"
 				callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
 				callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
 
 
-	acceptChange: (project_id, doc_id, change_id, callback = (error) ->) ->
-		timer = new metrics.Timer("accept-change")
-		url = "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept"
-		logger.log {project_id, doc_id, change_id}, "accepting change in document updater"
-		request.post url, (error, res, body)->
+	acceptChanges: (project_id, doc_id, change_ids = [], callback = (error) ->) ->
+		timer = new metrics.Timer("accept-changes")
+		reqSettings =
+			url: "#{settings.apis.documentupdater.url}/project/#{project_id}/doc/#{doc_id}/change/accept"
+			json:
+				change_ids: change_ids
+		logger.log {project_id, doc_id }, "accepting #{ change_ids.length } changes"
+		request.post reqSettings, (error, res, body)->
 			timer.done()
 			timer.done()
 			if error?
 			if error?
-				logger.error {err:error, project_id, doc_id, change_id}, "error accepting change in doc updater"
+				logger.error {err:error, project_id, doc_id }, "error accepting #{ change_ids.length } changes in doc updater"
 				return callback(error)
 				return callback(error)
 			if res.statusCode >= 200 and res.statusCode < 300
 			if res.statusCode >= 200 and res.statusCode < 300
-				logger.log {project_id, doc_id, change_id}, "accepted change in document updater"
+				logger.log {project_id, doc_id }, "accepted #{ change_ids.length } changes in document updater"
 				return callback(null)
 				return callback(null)
 			else
 			else
-				logger.error {project_id, doc_id, change_id}, "doc updater returned a non-success status code: #{res.statusCode}"
+				logger.error {project_id, doc_id }, "doc updater returned a non-success status code: #{res.statusCode}"
 				callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
 				callback new Error("doc updater returned a non-success status code: #{res.statusCode}")
 
 
 	deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) ->
 	deleteThread: (project_id, doc_id, thread_id, callback = (error) ->) ->

+ 63 - 0
services/web/app/views/project/editor/review-panel.pug

@@ -6,6 +6,22 @@
 			ng-click="toggleReviewPanel();"
 			ng-click="toggleReviewPanel();"
 			ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
 			ng-class="{ 'rp-track-changes-indicator-on-dark' : darkTheme }"
 		) !{translate("track_changes_is_on")}
 		) !{translate("track_changes_is_on")}
+		a.rp-bulk-actions-btn(
+			href
+			ng-if="reviewPanel.selectedEntryIds.length > 1"
+			ng-click="showBulkAcceptDialog();"
+		) 
+			i.fa.fa-check
+			| &nbsp;#{translate("accept_all")}
+			| ({{ reviewPanel.selectedEntryIds.length }})
+		a.rp-bulk-actions-btn(
+			href
+			ng-if="reviewPanel.selectedEntryIds.length > 1"
+			ng-click="showBulkRejectDialog();"
+		) 
+			i.fa.fa-times
+			| &nbsp;#{translate("reject_all")}
+			| ({{ reviewPanel.selectedEntryIds.length }})
 		a.rp-add-comment-btn(
 		a.rp-add-comment-btn(
 			href
 			href
 			ng-if="reviewPanel.entries[editor.open_doc_id]['add-comment'] != null"
 			ng-if="reviewPanel.entries[editor.open_doc_id]['add-comment'] != null"
@@ -13,6 +29,7 @@
 		) 
 		) 
 			i.fa.fa-comment
 			i.fa.fa-comment
 			| &nbsp;#{translate("add_comment")}
 			| &nbsp;#{translate("add_comment")}
+
 	.review-panel-toolbar
 	.review-panel-toolbar
 		resolved-comments-dropdown(
 		resolved-comments-dropdown(
 			class="rp-flex-block"
 			class="rp-flex-block"
@@ -82,6 +99,12 @@
 						on-submit="submitNewComment(content);"
 						on-submit="submitNewComment(content);"
 						on-cancel="cancelNewComment();"
 						on-cancel="cancelNewComment();"
 					)
 					)
+				div(ng-if="entry.type === 'bulk-actions'")
+					bulk-actions-entry(
+						on-bulk-accept="showBulkAcceptDialog();"
+						on-bulk-reject="showBulkRejectDialog();"
+						n-entries="reviewPanel.selectedEntryIds.length"
+					)
 
 
 	.rp-entry-list(
 	.rp-entry-list(
 		ng-if="reviewPanel.subView === SubViews.OVERVIEW"
 		ng-if="reviewPanel.subView === SubViews.OVERVIEW"
@@ -353,6 +376,25 @@ script(type='text/ng-template', id='addCommentEntryTemplate')
 						i.fa.fa-comment
 						i.fa.fa-comment
 						| &nbsp;#{translate("comment")}
 						| &nbsp;#{translate("comment")}
 
 
+script(type='text/ng-template', id='bulkActionsEntryTemplate')
+	div(ng-if="nEntries > 1")
+		.rp-entry-callout.rp-entry-callout-bulk-actions
+		.rp-entry.rp-entry-bulk-actions
+			a.rp-bulk-actions-btn(
+				href
+				ng-click="bulkReject();"
+			) 
+				i.fa.fa-times
+				| &nbsp;#{translate("reject_all")}
+				| ({{ nEntries }})
+			a.rp-bulk-actions-btn(
+				href
+				ng-click="bulkAccept();"
+			) 
+				i.fa.fa-check
+				| &nbsp;#{translate("accept_all")}
+				| ({{ nEntries }})
+			
 script(type='text/ng-template', id='resolvedCommentsDropdownTemplate')
 script(type='text/ng-template', id='resolvedCommentsDropdownTemplate')
 	.resolved-comments
 	.resolved-comments
 		.resolved-comments-backdrop(
 		.resolved-comments-backdrop(
@@ -433,3 +475,24 @@ script(type="text/ng-template", id="trackChangesUpgradeModalTemplate")
 			ng-click="cancel()"
 			ng-click="cancel()"
 		)
 		)
 			span #{translate("close")}
 			span #{translate("close")}
+
+script(type="text/ng-template", id="bulkActionsModalTemplate")
+	.modal-header
+		button.close(
+			type="button"
+			data-dismiss="modal"
+			ng-click="cancel()"
+		) &times;
+		h3 {{ isAccept ? '#{translate("accept_all")}' : '#{translate("reject_all")}' }}
+	.modal-body 
+		p(ng-if="isAccept") #{translate("bulk_accept_confirm", { nChanges: "{{ nChanges }}"})}
+		p(ng-if="!isAccept") #{translate("bulk_reject_confirm", { nChanges: "{{ nChanges }}"})}
+	.modal-footer()
+		button.btn.btn-default(
+			ng-click="cancel()"
+		)
+			span #{translate("cancel")}
+		button.btn.btn-primary(
+			ng-click="confirm()"
+		)
+			span #{translate("ok")}

+ 2 - 0
services/web/public/coffee/ide/review-panel/ReviewPanelManager.coffee

@@ -1,11 +1,13 @@
 define [
 define [
 	"ide/review-panel/controllers/ReviewPanelController"
 	"ide/review-panel/controllers/ReviewPanelController"
 	"ide/review-panel/controllers/TrackChangesUpgradeModalController"
 	"ide/review-panel/controllers/TrackChangesUpgradeModalController"
+	"ide/review-panel/controllers/BulkActionsModalController"
 	"ide/review-panel/directives/reviewPanelSorted"
 	"ide/review-panel/directives/reviewPanelSorted"
 	"ide/review-panel/directives/reviewPanelToggle"
 	"ide/review-panel/directives/reviewPanelToggle"
 	"ide/review-panel/directives/changeEntry"
 	"ide/review-panel/directives/changeEntry"
 	"ide/review-panel/directives/commentEntry"
 	"ide/review-panel/directives/commentEntry"
 	"ide/review-panel/directives/addCommentEntry"
 	"ide/review-panel/directives/addCommentEntry"
+	"ide/review-panel/directives/bulkActionsEntry"
 	"ide/review-panel/directives/resolvedCommentEntry"
 	"ide/review-panel/directives/resolvedCommentEntry"
 	"ide/review-panel/directives/resolvedCommentsDropdown"
 	"ide/review-panel/directives/resolvedCommentsDropdown"
 	"ide/review-panel/directives/reviewPanelCollapseHeight"
 	"ide/review-panel/directives/reviewPanelCollapseHeight"

+ 10 - 0
services/web/public/coffee/ide/review-panel/controllers/BulkActionsModalController.coffee

@@ -0,0 +1,10 @@
+define [
+	"base"
+], (App) ->
+	App.controller "BulkActionsModalController", ($scope, $modalInstance, isAccept, nChanges) ->
+		$scope.isAccept = isAccept
+		$scope.nChanges = nChanges
+		$scope.cancel = () ->
+			$modalInstance.dismiss()
+		$scope.confirm = () ->
+			$modalInstance.close(isAccept)

+ 52 - 2
services/web/public/coffee/ide/review-panel/controllers/ReviewPanelController.coffee

@@ -26,6 +26,7 @@ define [
 			resolvedThreadIds: {}
 			resolvedThreadIds: {}
 			rendererData: {}
 			rendererData: {}
 			loadingThreads: false
 			loadingThreads: false
+			selectedEntryIds: []
 
 
 		window.addEventListener "beforeunload", () ->
 		window.addEventListener "beforeunload", () ->
 			collapsedStates = {}
 			collapsedStates = {}
@@ -176,7 +177,7 @@ define [
 			entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}
 			entries = $scope.reviewPanel.entries[$scope.editor.open_doc_id] or {}
 			permEntries = {}
 			permEntries = {}
 			for entry, entryData of entries
 			for entry, entryData of entries
-				if entry != "add-comment"
+				if entry not in [ "add-comment", "bulk-actions" ]
 					permEntries[entry] = entryData 
 					permEntries[entry] = entryData 
 			Object.keys(permEntries).length
 			Object.keys(permEntries).length
 		), (nEntries) ->
 		), (nEntries) ->
@@ -294,23 +295,35 @@ define [
 		$scope.$on "editor:focus:changed", (e, selection_offset_start, selection_offset_end, selection) ->
 		$scope.$on "editor:focus:changed", (e, selection_offset_start, selection_offset_end, selection) ->
 			doc_id = $scope.editor.open_doc_id
 			doc_id = $scope.editor.open_doc_id
 			entries = getDocEntries(doc_id)
 			entries = getDocEntries(doc_id)
+			$scope.reviewPanel.selectedEntryIds = []
 
 
 			delete entries["add-comment"]
 			delete entries["add-comment"]
+			delete entries["bulk-actions"]
+
 			if selection
 			if selection
 				entries["add-comment"] = {
 				entries["add-comment"] = {
 					type: "add-comment"
 					type: "add-comment"
 					offset: selection_offset_start
 					offset: selection_offset_start
 					length: selection_offset_end - selection_offset_start
 					length: selection_offset_end - selection_offset_start
 				}
 				}
+				entries["bulk-actions"] = {
+					type: "bulk-actions"
+					offset: selection_offset_start
+					length: selection_offset_end - selection_offset_start
+				}
 			
 			
 			for id, entry of entries
 			for id, entry of entries
 				if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
 				if entry.type == "comment" and not $scope.reviewPanel.resolvedThreadIds[entry.thread_id]
 					entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
 					entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
 				else if entry.type == "insert"
 				else if entry.type == "insert"
+					isEntryWithinSelection = entry.offset >= selection_offset_start and entry.offset + entry.content.length <= selection_offset_end
 					entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
 					entry.focused = (entry.offset <= selection_offset_start <= entry.offset + entry.content.length)
+					$scope.reviewPanel.selectedEntryIds.push id if isEntryWithinSelection
 				else if entry.type == "delete"
 				else if entry.type == "delete"
+					isEntryWithinSelection = selection_offset_start <= entry.offset <= selection_offset_end
 					entry.focused = (entry.offset == selection_offset_start)
 					entry.focused = (entry.offset == selection_offset_start)
-				else if entry.type == "add-comment" and selection
+					$scope.reviewPanel.selectedEntryIds.push id if isEntryWithinSelection
+				else if entry.type in [ "add-comment", "bulk-actions" ] and selection
 					entry.focused = true
 					entry.focused = true
 			
 			
 			$scope.$broadcast "review-panel:recalculate-screen-positions"
 			$scope.$broadcast "review-panel:recalculate-screen-positions"
@@ -325,6 +338,43 @@ define [
 			$scope.$broadcast "change:reject", entry_id
 			$scope.$broadcast "change:reject", entry_id
 			event_tracking.sendMB "rp-change-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
 			event_tracking.sendMB "rp-change-rejected", { view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini' }
 		
 		
+		bulkAccept = () ->
+			entry_ids = $scope.reviewPanel.selectedEntryIds.slice()
+			$http.post "/project/#{$scope.project_id}/doc/#{$scope.editor.open_doc_id}/changes/accept", { change_ids: entry_ids, _csrf: window.csrfToken}
+			$scope.$broadcast "change:bulk-accept", entry_ids
+			$scope.reviewPanel.selectedEntryIds = []
+			event_tracking.sendMB "rp-bulk-accept", { 
+				view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini',  
+				nEntries: $scope.reviewPanel.selectedEntryIds.length
+			}
+
+		bulkReject = () ->
+			$scope.$broadcast "change:bulk-reject", $scope.reviewPanel.selectedEntryIds.slice()
+			$scope.reviewPanel.selectedEntryIds = []
+			event_tracking.sendMB "rp-bulk-reject", { 
+				view: if $scope.ui.reviewPanelOpen then $scope.reviewPanel.subView else 'mini',  
+				nEntries: $scope.reviewPanel.selectedEntryIds.length
+			}
+
+		$scope.showBulkAcceptDialog = () ->
+			showBulkActionsDialog true
+
+		$scope.showBulkRejectDialog = () -> showBulkActionsDialog false
+
+		showBulkActionsDialog = (isAccept) ->
+			$modal.open({
+				templateUrl: "bulkActionsModalTemplate"
+				controller: "BulkActionsModalController"
+				resolve:
+					isAccept: () -> isAccept
+					nChanges: () -> $scope.reviewPanel.selectedEntryIds.length
+				scope: $scope.$new()
+			}).result.then (isAccept) ->
+				if isAccept
+					bulkAccept()
+				else
+					bulkReject()
+
 		$scope.addNewComment = () ->
 		$scope.addNewComment = () ->
 			$scope.$broadcast "comment:start_adding"
 			$scope.$broadcast "comment:start_adding"
 			$scope.toggleReviewPanel()
 			$scope.toggleReviewPanel()

+ 15 - 0
services/web/public/coffee/ide/review-panel/directives/bulkActionsEntry.coffee

@@ -0,0 +1,15 @@
+define [
+	"base"
+], (App) ->
+	App.directive "bulkActionsEntry", () ->
+		restrict: "E"
+		templateUrl: "bulkActionsEntryTemplate"
+		scope: 
+			onBulkAccept: "&"
+			onBulkReject: "&"
+			nEntries: "="
+		link: (scope, element, attrs) ->
+			scope.bulkAccept = () ->
+				scope.onBulkAccept()
+			scope.bulkReject = () ->
+				scope.onBulkReject()

+ 32 - 6
services/web/public/stylesheets/app/editor/review-panel.less

@@ -277,6 +277,9 @@
 				right: 5px;
 				right: 5px;
 			}
 			}
 		}
 		}
+		&-bulk-actions {
+			right: auto;
+		}
 	}
 	}
 	.rp-state-overview & {
 	.rp-state-overview & {
 		border-radius: 0;
 		border-radius: 0;
@@ -335,6 +338,12 @@
 			border-left-color: @rp-yellow;
 			border-left-color: @rp-yellow;
 		}
 		}
 	}
 	}
+
+	&-bulk-actions {
+		background-color: transparent;
+		right: auto;
+		border-left-width: 0;
+	}
 }
 }
 	.rp-entry-body {
 	.rp-entry-body {
 		display: flex;
 		display: flex;
@@ -451,17 +460,35 @@
 		padding: 0 5px;
 		padding: 0 5px;
 	}
 	}
 
 
-	.rp-add-comment-btn {
+	.rp-add-comment-btn,
+	.rp-bulk-actions-btn {
 		.rp-button();
 		.rp-button();
-		display: block;
+		display: inline-block;
 		padding: 5px 10px;
 		padding: 5px 10px;
 		border-radius: 3px;
 		border-radius: 3px;
 
 
 		.rp-in-editor-widgets & {
 		.rp-in-editor-widgets & {
+			display: block;
 			white-space: nowrap;
 			white-space: nowrap;
 			border-radius: 0;
 			border-radius: 0;
+
+			&:last-child {
+				border-bottom-left-radius: 3px;
+			}
+		}
+	}
+
+	.rp-bulk-actions-btn {
+		border-radius: 0;
+		&:first-child {
+			border-top-left-radius: 3px;
 			border-bottom-left-radius: 3px;
 			border-bottom-left-radius: 3px;
 		}
 		}
+		&:last-child {
+			border-top-right-radius: 3px;
+			border-bottom-right-radius: 3px;
+			margin-left: 1px;
+		}
 	}
 	}
 
 
 	.rp-new-comment {
 	.rp-new-comment {
@@ -903,6 +930,9 @@
 	.rp-size-mini & {
 	.rp-size-mini & {
 		right: @review-off-width;
 		right: @review-off-width;
 	}
 	}
+	.rp-size-expanded & {
+		display: none;
+	}
 }
 }
 	.rp-track-changes-indicator {
 	.rp-track-changes-indicator {
 		display: block;
 		display: block;
@@ -931,10 +961,6 @@
 			background-color: rgba(240, 240, 240, 1);
 			background-color: rgba(240, 240, 240, 1);
 			color: @rp-type-blue;
 			color: @rp-type-blue;
 		}
 		}
-
-		.rp-size-expanded & {
-			display: none;
-		}
 	}
 	}
 
 
 // Helper class for elements which aren't treated as flex-items by IE10, e.g:
 // Helper class for elements which aren't treated as flex-items by IE10, e.g: