diff --git a/codebrag-dao/src/main/scala/com/softwaremill/codebrag/dao/finders/followup/FollowupFinder.scala b/codebrag-dao/src/main/scala/com/softwaremill/codebrag/dao/finders/followup/FollowupFinder.scala index 616c4916..fc0f40d4 100644 --- a/codebrag-dao/src/main/scala/com/softwaremill/codebrag/dao/finders/followup/FollowupFinder.scala +++ b/codebrag-dao/src/main/scala/com/softwaremill/codebrag/dao/finders/followup/FollowupFinder.scala @@ -9,9 +9,13 @@ trait FollowupFinder { def findAllFollowupsByCommitForUser(userId: ObjectId): FollowupsByCommitListView def findFollowupForUser(userId: ObjectId, followupId: ObjectId): Either[String, SingleFollowupView] + + def findFollowupforDashboard(followupId: ObjectId): Either[String, SingleFollowupView] def countFollowupsForUser(userId: ObjectId): Long def countFollowupsForUserSince(date: DateTime, userId: ObjectId): Long + + def findAllFollowupsByCommitForDashboard(): FollowupsByCommitListView } diff --git a/codebrag-dao/src/main/scala/com/softwaremill/codebrag/dao/finders/followup/SQLFollowupFinder.scala b/codebrag-dao/src/main/scala/com/softwaremill/codebrag/dao/finders/followup/SQLFollowupFinder.scala index 81bd26a2..e1973a6c 100644 --- a/codebrag-dao/src/main/scala/com/softwaremill/codebrag/dao/finders/followup/SQLFollowupFinder.scala +++ b/codebrag-dao/src/main/scala/com/softwaremill/codebrag/dao/finders/followup/SQLFollowupFinder.scala @@ -129,4 +129,39 @@ class SQLFollowupFinder(val database: SQLDatabase, userDAO: UserDAO) extends Fol case like: Like => FollowupLastLikeView(like.id.toString, author.name, like.postingTime, author.avatarUrl) } } + def findAllFollowupsByCommitForDashboard(): FollowupsByCommitListView = db.withTransaction { implicit session => + val followups = findAllFollowups() + val followupReactions = findFollowupReactions(followups) + val lastReactions = findLastReactionsForFollowups(followups) + val reactionAuthors = findReactionAuthors(lastReactions) + val commits = findCommitsForFollowups(followups) + + val followupsGroupedByCommit = followups.groupBy(_.threadCommitId) + + val sortFollowupsForCommitByDate = (f1: FollowupReactionsView, f2: FollowupReactionsView) => f1.lastReaction.date.isAfter(f2.lastReaction.date) + + val followupsForCommits = followupsGroupedByCommit.map { + case (commitId, commitFollowups) => + val followupsForCommitViews = commitFollowups + .map(f => followupToReactionsView(f, followupReactions.getOrElse(f.id, Nil), lastReactions, reactionAuthors)) + .sortWith(sortFollowupsForCommitByDate) + val commit = commits(commitId) + val commitView = FollowupCommitView(commit.id.toString, commit.sha, commit.repoName, commit.authorName, commit.message, commit.authorDate) + FollowupsByCommitView(commitView, followupsForCommitViews) + } + FollowupsByCommitListView(sortFollowupGroupsByNewest(followupsForCommits)) + } + private def findAllFollowups()(implicit session: Session): List[SQLFollowup] = { + followups.list() + } + def findFollowupforDashboard(followupId: ObjectId) = db.withTransaction { implicit session => + val r = for { + followup <- followups.filter(f => f.id === followupId).firstOption + reaction <- findLastReaction(followup.lastReactionId) + author <- userDAO.findPartialUserDetails(List(reaction.authorId)).headOption + commit <- commitInfos.filter(_.id === followup.threadCommitId).firstOption() + } yield recordsToFollowupView(commit, reaction, author, followup) + + r.fold[Either[String, SingleFollowupView]](Left("No such followup"))(Right(_)) + } } diff --git a/codebrag-rest/src/main/scala/ScalatraBootstrap.scala b/codebrag-rest/src/main/scala/ScalatraBootstrap.scala index 61e21022..57a35d64 100644 --- a/codebrag-rest/src/main/scala/ScalatraBootstrap.scala +++ b/codebrag-rest/src/main/scala/ScalatraBootstrap.scala @@ -75,6 +75,7 @@ class ScalatraBootstrap extends LifeCycle with Logging { context.mount(new CommitsServlet(authenticator, toReviewCommitsFinder, allCommitsFinder, reactionFinder, addCommentUseCase, reviewCommitUseCase, userReactionService, userDao, diffWithCommentsService, unlikeUseCaseFactory, likeUseCase), Prefix + CommitsServlet.MAPPING_PATH) context.mount(new FollowupsServlet(authenticator, followupFinder, followupDoneUseCase), Prefix + FollowupsServlet.MappingPath) +context.mount(new AllFollowupsServlet(authenticator, followupFinder, followupDoneUseCase), Prefix + AllFollowupsServlet.MappingPath) context.mount(new VersionServlet, Prefix + "version") context.mount(new ConfigServlet(config, authenticator), Prefix + "config") context.mount(new InvitationServlet(authenticator, generateInvitationCodeUseCase, sendInvitationEmailUseCase), Prefix + "invitation") diff --git a/codebrag-rest/src/main/scala/com/softwaremill/codebrag/rest/AllFollowupsServlet.scala b/codebrag-rest/src/main/scala/com/softwaremill/codebrag/rest/AllFollowupsServlet.scala new file mode 100644 index 00000000..73f5c477 --- /dev/null +++ b/codebrag-rest/src/main/scala/com/softwaremill/codebrag/rest/AllFollowupsServlet.scala @@ -0,0 +1,38 @@ +package com.softwaremill.codebrag.rest + +import com.softwaremill.codebrag.service.user.Authenticator +import org.scalatra.json.JacksonJsonSupport +import org.bson.types.ObjectId +import org.scalatra.NotFound +import com.softwaremill.codebrag.dao.finders.followup.FollowupFinder +import com.softwaremill.codebrag.dao.finders.views.SingleFollowupView +import com.softwaremill.codebrag.usecases.reactions.FollowupDoneUseCase + +class AllFollowupsServlet(val authenticator: Authenticator, + followupFinder: FollowupFinder, + followupDoneUseCase: FollowupDoneUseCase) + extends JsonServletWithAuthentication with JacksonJsonSupport { + + get("/") { + haltIfNotAuthenticated() + followupFinder.findAllFollowupsByCommitForDashboard() + } + + get("/:id") { + haltIfNotAuthenticated() + val followupId = params("id") + followupFinder.findFollowupforDashboard(new ObjectId(followupId)) match { + case Right(followup) => followup + case Left(msg) => NotFound(msg) + } + } + + delete("/:id") { + haltIfNotAuthenticated() + followupDoneUseCase.execute(user.id, new ObjectId(params("id"))) + } +} + +object AllFollowupsServlet { + val MappingPath = "allfollowups" +} diff --git a/codebrag-ui/app/index.html b/codebrag-ui/app/index.html index c95dc6b9..683e66e9 100644 --- a/codebrag-ui/app/index.html +++ b/codebrag-ui/app/index.html @@ -127,6 +127,10 @@ + + + + @@ -225,6 +229,14 @@ +
  • + + + Dashboard + + + +
  • @@ -248,4 +260,4 @@
    - \ No newline at end of file + diff --git a/codebrag-ui/app/scripts/app.js b/codebrag-ui/app/scripts/app.js index d10432c4..4c151ea4 100644 --- a/codebrag-ui/app/scripts/app.js +++ b/codebrag-ui/app/scripts/app.js @@ -23,6 +23,8 @@ angular.module('codebrag.commits', [ angular.module('codebrag.followups', ['ngResource', 'ui.compat', 'codebrag.auth', 'codebrag.events', 'codebrag.tour']); +angular.module('codebrag.dashboard', ['ngResource', 'ui.compat', 'codebrag.auth', 'codebrag.events', 'codebrag.tour','codebrag.followups']); + angular.module('codebrag.invitations', ['ui.validate', 'ui.keypress']); angular.module('codebrag.profile', ['codebrag.session']); @@ -47,6 +49,7 @@ angular.module('codebrag', [ 'codebrag.commits', 'codebrag.branches', 'codebrag.followups', + 'codebrag.dashboard', 'codebrag.repostatus', 'codebrag.favicon', 'codebrag.tour', @@ -159,3 +162,22 @@ angular.module('codebrag.common') angular.module('codebrag.userMgmt').run(function(userMgmtService) { userMgmtService.initialize(); }); + +angular.module('codebrag.dashboard') +.config(function ($stateProvider, authenticatedUser) { + $stateProvider + .state('dashboard', { + url: '/dashboard', + abstract: true, + templateUrl: 'views/secured/dashboard/dashboard.html', + resolve: authenticatedUser + }) + .state('dashboard.list', { + url: '', + templateUrl: 'views/secured/followups/emptyFollowups.html' + }) + .state('dashboard.details', { + url: '/{followupId}/comments/{commentId}', + templateUrl: 'views/secured/dashboard/dashboardDetails.html' + }); +}); diff --git a/codebrag-ui/app/scripts/dashboard/allFollowupsService.js b/codebrag-ui/app/scripts/dashboard/allFollowupsService.js new file mode 100644 index 00000000..573bf72f --- /dev/null +++ b/codebrag-ui/app/scripts/dashboard/allFollowupsService.js @@ -0,0 +1,118 @@ +angular.module('codebrag.dashboard') + + .factory('allFollowupsService', function($http, $rootScope, events) { + + var followupsListLocal = new codebrag.followups.LocalFollowupsList(); + var listFetched = false; + + function allFollowups() { + return _httpRequest('GET').then(function(response) { + followupsListLocal.addAll(response.data.followupsByCommit); + listFetched = true; + return followupsListLocal.collection; + }); + } + + function removeAndGetNext(followupId, commitId) { + return _httpRequest('DELETE', followupId, {unique: true, requestId: 'removeFollowup_' + followupId}).then(function() { + triggerCounterDecrease(); + var nextFollowup = followupsListLocal.removeOneAndGetNext(followupId, commitId); + return nextFollowup; + }); + + } + + function loadFollowupDetails(followupId) { + return _httpRequest('GET', followupId).then(function(response) { + return response.data; + }); + } + + function hasFollowups() { + return followupsListLocal.hasFollowups(); + } + + function mightHaveFollowups() { + return !listFetched || followupsListLocal.hasFollowups() + } + + function _httpRequest(method, id, config) { + var followupsUrl = 'rest/allfollowups/' + (id || ''); + var reqConfig = angular.extend(config || {}, {method: method, url: followupsUrl}); + return $http(reqConfig); + } + + function triggerCounterDecrease() { + $rootScope.$broadcast(events.followupDone); + } + + return { + allFollowups: allFollowups, + removeAndGetNext: removeAndGetNext, + loadFollowupDetails: loadFollowupDetails, + hasFollowups: hasFollowups, + mightHaveFollowups: mightHaveFollowups + }; + + }); + +var codebrag = codebrag || {}; +codebrag.followups = codebrag.followups || {}; + +codebrag.followups.LocalFollowupsList = function(collection) { + + var self = this; + + this.collection = collection || []; + + this.addAll = function(newCollection) { + this.collection.length = 0; + Array.prototype.push.apply(this.collection, newCollection); + }; + + function nextFollowup(commit, removeAtIndex) { + var followupToReturn = null; + var currentCommitIndex = self.collection.indexOf(commit); + + if (commit.followups[removeAtIndex]) { + followupToReturn = commit.followups[removeAtIndex]; + } else if (self.collection[currentCommitIndex + 1]) { + followupToReturn = self.collection[currentCommitIndex + 1].followups[0]; + } else if (removeAtIndex > 0) { + followupToReturn = commit.followups[removeAtIndex - 1]; + } else if (removeAtIndex === 0 && currentCommitIndex > 0) { + var previousCommitFollowupsLength = self.collection[currentCommitIndex - 1].followups.length; + followupToReturn = self.collection[currentCommitIndex - 1].followups[previousCommitFollowupsLength - 1]; + } + if (!commit.followups.length) { + self.collection.splice(currentCommitIndex, 1); + } + return followupToReturn; + } + + this.removeOneAndGetNext = function(followupId) { + var currentCommit = _.find(this.collection, function(group) { + return _.some(group.followups, function(followup) { + return followup.followupId === followupId; + }); + }); + var followupToRemove = _.find(currentCommit.followups, function(followup) { + return followup.followupId === followupId; + }); + var indexToRemove = currentCommit.followups.indexOf(followupToRemove); + currentCommit.followups.splice(indexToRemove, 1); + return nextFollowup(currentCommit, indexToRemove); + }; + + this.hasFollowups = function() { + return this.collection.length > 0; + }; + + this.followupsCount = function() { + return _.reduce(this.collection, function(sum, followupsGroup) { + return sum + followupsGroup.followups.length; + }, 0); + }; + +}; + diff --git a/codebrag-ui/app/scripts/dashboard/dashboardCtrl.js b/codebrag-ui/app/scripts/dashboard/dashboardCtrl.js new file mode 100644 index 00000000..a8146ca1 --- /dev/null +++ b/codebrag-ui/app/scripts/dashboard/dashboardCtrl.js @@ -0,0 +1,22 @@ +angular.module('codebrag.dashboard') + + .controller('DashboardCtrl', function ($scope, $http, allFollowupsService, pageTourService, events) { + + $scope.$on(events.allfollowupsTabOpened, initCtrl); + + + $scope.pageTourForFollowupsVisible = function() { + return pageTourService.stepActive('dashboard') || pageTourService.stepActive('invites'); + }; + + function initCtrl() { + allFollowupsService.allFollowups().then(function(followups) { + $scope.followupCommits = followups; + }); + $scope.hasFollowupsAvailable = allFollowupsService.hasFollowups; + $scope.mightHaveFollowups = allFollowupsService.mightHaveFollowups; + } + + initCtrl(); + + }); diff --git a/codebrag-ui/app/scripts/dashboard/dashboardDetailsCtrl.js b/codebrag-ui/app/scripts/dashboard/dashboardDetailsCtrl.js new file mode 100644 index 00000000..bc806598 --- /dev/null +++ b/codebrag-ui/app/scripts/dashboard/dashboardDetailsCtrl.js @@ -0,0 +1,32 @@ +angular.module('codebrag.dashboard') + + .controller('DashboardDetailsCtrl', function ($stateParams, $state, $scope, allFollowupsService, commitsService) { + + var followupId = $stateParams.followupId; + + $scope.scrollTo = $stateParams.commentId; + + allFollowupsService.loadFollowupDetails(followupId).then(function(followup) { + $scope.currentFollowup = followup; + commitsService.commitDetails(followup.commit.sha, followup.commit.repoName).then(function(commit) { + $scope.currentCommit = new codebrag.CurrentCommit(commit); + + }); + }); + + $scope.markCurrentFollowupAsDone = function() { + allFollowupsService.removeAndGetNext(followupId).then(function(nextFollowup) { + goTo(nextFollowup); + }); + }; + + function goTo(nextFollowup) { + if (_.isNull(nextFollowup)) { + $state.transitionTo('dashboard.list'); + } else { + $state.transitionTo('dashboard.details', {followupId: nextFollowup.followupId, commentId: nextFollowup.lastReaction.reactionId}); + } + } + + + }); diff --git a/codebrag-ui/app/scripts/dashboard/dashboardListItemCtrl.js b/codebrag-ui/app/scripts/dashboard/dashboardListItemCtrl.js new file mode 100644 index 00000000..ec05d473 --- /dev/null +++ b/codebrag-ui/app/scripts/dashboard/dashboardListItemCtrl.js @@ -0,0 +1,28 @@ +angular.module('codebrag.dashboard') + + .controller('DashboardListItemCtrl', function ($scope, $state, $stateParams, allFollowupsService, $rootScope, events) { + + $scope.openFollowupDetails = function (followup) { + if(_thisFollowupOpened(followup)) { + $rootScope.$broadcast(events.scrollOnly); + } else { + $state.transitionTo('dashboard.details', {followupId: followup.followupId, commentId: followup.lastReaction.reactionId}); + } + }; + + $scope.dismiss = function (followup) { + allfollowupsService.removeAndGetNext(followup.followupId).then(function(nextFollowup) { + if(nextFollowup) { + $state.transitionTo('dashboard.details', {followupId: nextFollowup.followupId, commentId: nextFollowup.lastReaction.reactionId}); + } else { + $state.transitionTo('dashboard.list'); + } + }); + }; + + function _thisFollowupOpened(followup) { + return $state.current.name === 'dashboard.details' && $state.params.followupId === followup.followupId; + } + + + }); diff --git a/codebrag-ui/app/scripts/notifications/notificationsCtrl.js b/codebrag-ui/app/scripts/notifications/notificationsCtrl.js index 06939590..17678c6b 100644 --- a/codebrag-ui/app/scripts/notifications/notificationsCtrl.js +++ b/codebrag-ui/app/scripts/notifications/notificationsCtrl.js @@ -43,5 +43,10 @@ angular.module('codebrag.notifications') $rootScope.$on('followupsNotificationRead', function() { $scope.followupsNotificationAvailable = false; }); + + $scope.openDashboard = function() { + $rootScope.$broadcast(events.allfollowupsTabOpened); + $state.transitionTo('dashboard.list'); + }; - }); \ No newline at end of file + }); diff --git a/codebrag-ui/app/views/secured/dashboard/dashboard.html b/codebrag-ui/app/views/secured/dashboard/dashboard.html new file mode 100644 index 00000000..dd078a56 --- /dev/null +++ b/codebrag-ui/app/views/secured/dashboard/dashboard.html @@ -0,0 +1,29 @@ +
    +
    +
    + +
    +
    +
    +
    {{commit.commit.authorName}}{{commit.commit.date | relativeDate}} in {{ commit.commit.repoName }}
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + Yay!
    No follow-ups! +
    + +
    +
    +
    +
    +
    diff --git a/codebrag-ui/app/views/secured/dashboard/dashboardDetails.html b/codebrag-ui/app/views/secured/dashboard/dashboardDetails.html new file mode 100644 index 00000000..b6f948ff --- /dev/null +++ b/codebrag-ui/app/views/secured/dashboard/dashboardDetails.html @@ -0,0 +1,23 @@ +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    {{currentCommit.info.authorName}}{{currentCommit.info.date | relativeDate}} +
    +
    +
    + +
    + OK THX +
    +
    +
    +