From d1349c42722a05cf6a8da5e4ebe377d897ea0b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 23 Oct 2015 16:42:17 +0200 Subject: [PATCH] Create vote and watch buttons in story, task and issue detail pages --- app/coffee/modules/issues/detail.coffee | 46 +++++ app/coffee/modules/resources.coffee | 23 ++- app/coffee/modules/resources/issues.coffee | 16 ++ app/coffee/modules/resources/tasks.coffee | 16 ++ .../modules/resources/userstories.coffee | 16 ++ app/coffee/modules/tasks/detail.coffee | 67 +++++-- app/coffee/modules/userstories/detail.coffee | 47 +++++ .../vote-button/vote-button.controller.coffee | 36 ++++ .../vote-button.controller.spec.coffee | 82 +++++++++ .../vote-button/vote-button.directive.coffee | 14 ++ .../components/vote-button/vote-button.jade | 15 ++ .../watch-button.controller.coffee | 36 ++++ .../watch-button.controller.spec.coffee | 83 +++++++++ .../watch-button.directive.coffee | 14 ++ .../components/watch-button/watch-button.jade | 44 +++++ app/partials/issue/issues-detail.jade | 133 +++++++++----- app/partials/task/task-detail.jade | 146 ++++++++++----- app/partials/us/us-detail.jade | 168 +++++++++++++----- 18 files changed, 843 insertions(+), 159 deletions(-) create mode 100644 app/modules/components/vote-button/vote-button.controller.coffee create mode 100644 app/modules/components/vote-button/vote-button.controller.spec.coffee create mode 100644 app/modules/components/vote-button/vote-button.directive.coffee create mode 100644 app/modules/components/vote-button/vote-button.jade create mode 100644 app/modules/components/watch-button/watch-button.controller.coffee create mode 100644 app/modules/components/watch-button/watch-button.controller.spec.coffee create mode 100644 app/modules/components/watch-button/watch-button.directive.coffee create mode 100644 app/modules/components/watch-button/watch-button.jade diff --git a/app/coffee/modules/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee index ec70b2c5..9dc08296 100644 --- a/app/coffee/modules/issues/detail.coffee +++ b/app/coffee/modules/issues/detail.coffee @@ -26,6 +26,7 @@ toString = @.taiga.toString joinStr = @.taiga.joinStr groupBy = @.taiga.groupBy bindOnce = @.taiga.bindOnce +bindMethods = @.taiga.bindMethods module = angular.module("taigaIssues") @@ -52,6 +53,8 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @log, @appMetaService, @analytics, @navUrls, @translate) -> + bindMethods(@) + @scope.issueRef = @params.issueref @scope.sectionName = @translate.instant("ISSUES.SECTION_NAME") @.initializeEventHandlers() @@ -144,6 +147,49 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @.fillUsersAndRoles(project.members, project.roles) @.loadIssue() + ### + # Note: This methods (onUpvote() and onDownvote()) are related to tg-vote-button. + # See app/modules/components/vote-button for more info + ### + onUpvote: -> + onSuccess = => + @.loadIssue() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.issues.upvote(@scope.issueId).then(onSuccess, onError) + + onDownvote: -> + onSuccess = => + @.loadIssue() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.issues.downvote(@scope.issueId).then(onSuccess, onError) + + ### + # Note: This methods (onWatch() and onUnwatch()) are related to tg-watch-button. + # See app/modules/components/watch-button for more info + ### + onWatch: -> + onSuccess = => + @.loadIssue() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.issues.watch(@scope.issueId).then(onSuccess, onError) + + onUnwatch: -> + onSuccess = => + @.loadIssue() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.issues.unwatch(@scope.issueId).then(onSuccess, onError) module.controller("IssueDetailController", IssueDetailController) diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index f17841e1..73c75922 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -37,8 +37,11 @@ urls = { "users-change-password": "/users/change_password" "users-change-email": "/users/change_email" "users-cancel-account": "/users/cancel" - "contacts": "/users/%s/contacts" - "stats": "/users/%s/stats" + "user-stats": "/users/%s/stats" + "user-liked": "/users/%s/liked" + "user-voted": "/users/%s/voted" + "user-watched": "/users/%s/watched" + "user-contacts": "/users/%s/contacts" # User - Notification "permissions": "/permissions" @@ -63,6 +66,10 @@ urls = { "project-templates": "/project-templates" "project-modules": "/projects/%s/modules" "bulk-update-projects-order": "/projects/bulk_update_order" + "project-like": "/projects/%s/like" + "project-unlike": "/projects/%s/unlike" + "project-watch": "/projects/%s/watch" + "project-unwatch": "/projects/%s/unwatch" # Project Values - Choises "userstory-statuses": "/userstory-statuses" @@ -83,16 +90,28 @@ urls = { "bulk-update-us-sprint-order": "/userstories/bulk_update_sprint_order" "bulk-update-us-kanban-order": "/userstories/bulk_update_kanban_order" "userstories-filters": "/userstories/filters_data" + "userstory-upvote": "/userstories/%s/upvote" + "userstory-downvote": "/userstories/%s/downvote" + "userstory-watch": "/userstories/%s/watch" + "userstory-unwatch": "/userstories/%s/unwatch" # Tasks "tasks": "/tasks" "bulk-create-tasks": "/tasks/bulk_create" "bulk-update-task-taskboard-order": "/tasks/bulk_update_taskboard_order" + "task-upvote": "/tasks/%s/upvote" + "task-downvote": "/tasks/%s/downvote" + "task-watch": "/tasks/%s/watch" + "task-unwatch": "/tasks/%s/unwatch" # Issues "issues": "/issues" "bulk-create-issues": "/issues/bulk_create" "issues-filters": "/issues/filters_data" + "issue-upvote": "/issues/%s/upvote" + "issue-downvote": "/issues/%s/downvote" + "issue-watch": "/issues/%s/watch" + "issue-unwatch": "/issues/%s/unwatch" # Wiki pages "wiki": "/wiki" diff --git a/app/coffee/modules/resources/issues.coffee b/app/coffee/modules/resources/issues.coffee index 22cb0f05..7ca1ef15 100644 --- a/app/coffee/modules/resources/issues.coffee +++ b/app/coffee/modules/resources/issues.coffee @@ -55,6 +55,22 @@ resourceProvider = ($repo, $http, $urls, $storage, $q) -> params = {project_id: projectId, bulk_issues: data} return $http.post(url, params) + service.upvote = (issueId) -> + url = $urls.resolve("issue-upvote", issueId) + return $http.post(url) + + service.downvote = (issueId) -> + url = $urls.resolve("issue-downvote", issueId) + return $http.post(url) + + service.watch = (issueId) -> + url = $urls.resolve("issue-watch", issueId) + return $http.post(url) + + service.unwatch = (issueId) -> + url = $urls.resolve("issue-unwatch", issueId) + return $http.post(url) + service.stats = (projectId) -> return $repo.queryOneRaw("projects", "#{projectId}/issues_stats") diff --git a/app/coffee/modules/resources/tasks.coffee b/app/coffee/modules/resources/tasks.coffee index 7069012c..caab72f9 100644 --- a/app/coffee/modules/resources/tasks.coffee +++ b/app/coffee/modules/resources/tasks.coffee @@ -57,6 +57,22 @@ resourceProvider = ($repo, $http, $urls, $storage) -> return $http.post(url, params).then (result) -> return result.data + service.upvote = (taskId) -> + url = $urls.resolve("task-upvote", taskId) + return $http.post(url) + + service.downvote = (taskId) -> + url = $urls.resolve("task-downvote", taskId) + return $http.post(url) + + service.watch = (taskId) -> + url = $urls.resolve("task-watch", taskId) + return $http.post(url) + + service.unwatch = (taskId) -> + url = $urls.resolve("task-unwatch", taskId) + return $http.post(url) + service.bulkUpdateTaskTaskboardOrder = (projectId, data) -> url = $urls.resolve("bulk-update-task-taskboard-order") params = {project_id: projectId, bulk_tasks: data} diff --git a/app/coffee/modules/resources/userstories.coffee b/app/coffee/modules/resources/userstories.coffee index 0d0fbd3d..ce7c139c 100644 --- a/app/coffee/modules/resources/userstories.coffee +++ b/app/coffee/modules/resources/userstories.coffee @@ -67,6 +67,22 @@ resourceProvider = ($repo, $http, $urls, $storage) -> return $http.post(url, data) + service.upvote = (userStoryId) -> + url = $urls.resolve("userstory-upvote", userStoryId) + return $http.post(url) + + service.downvote = (userStoryId) -> + url = $urls.resolve("userstory-downvote", userStoryId) + return $http.post(url) + + service.watch = (userStoryId) -> + url = $urls.resolve("userstory-watch", userStoryId) + return $http.post(url) + + service.unwatch = (userStoryId) -> + url = $urls.resolve("userstory-unwatch", userStoryId) + return $http.post(url) + service.bulkUpdateBacklogOrder = (projectId, data) -> url = $urls.resolve("bulk-update-us-backlog-order") params = {project_id: projectId, bulk_stories: data} diff --git a/app/coffee/modules/tasks/detail.coffee b/app/coffee/modules/tasks/detail.coffee index 35b31a59..653e9f6b 100644 --- a/app/coffee/modules/tasks/detail.coffee +++ b/app/coffee/modules/tasks/detail.coffee @@ -23,6 +23,7 @@ taiga = @.taiga mixOf = @.taiga.mixOf groupBy = @.taiga.groupBy +bindMethods = @.taiga.bindMethods module = angular.module("taigaTasks") @@ -50,6 +51,8 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @log, @appMetaService, @navUrls, @analytics, @translate) -> + bindMethods(@) + @scope.taskRef = @params.taskref @scope.sectionName = @translate.instant("TASK.SECTION_NAME") @.initializeEventHandlers() @@ -145,6 +148,50 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @.fillUsersAndRoles(project.members, project.roles) @.loadTask().then(=> @q.all([@.loadSprint(), @.loadUserStory()])) + ### + # Note: This methods (onUpvote() and onDownvote()) are related to tg-vote-button. + # See app/modules/components/vote-button for more info + ### + onUpvote: -> + onSuccess = => + @.loadTask() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.tasks.upvote(@scope.taskId).then(onSuccess, onError) + + onDownvote: -> + onSuccess = => + @.loadTask() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.tasks.downvote(@scope.taskId).then(onSuccess, onError) + + ### + # Note: This methods (onWatch() and onUnwatch()) are related to tg-watch-button. + # See app/modules/components/watch-button for more info + ### + onWatch: -> + onSuccess = => + @.loadTask() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.tasks.watch(@scope.taskId).then(onSuccess, onError) + + onUnwatch: -> + onSuccess = => + @.loadTask() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.tasks.unwatch(@scope.taskId).then(onSuccess, onError) + module.controller("TaskDetailController", TaskDetailController) @@ -195,7 +242,7 @@ module.directive("tgTaskStatusDisplay", ["$tgTemplate", "$compile", TaskStatusDi ## Task status button directive ############################################################################# -TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $compile, $translate) -> +TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $compile, $translate, $template) -> # Display the status of Task and you can edit it. # # Example: @@ -206,21 +253,7 @@ TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $co # - scope.statusById object # - $scope.project.my_permissions - template = _.template(""" -
- - <%- status.name %> - <% if(editable){ %><% }%> - - - -
- """) + template = $template.get("us/us-status-button.html", true) link = ($scope, $el, $attrs, $model) -> isEditable = -> @@ -288,7 +321,7 @@ TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $co } module.directive("tgTaskStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQqueue", - "$compile", "$translate", TaskStatusButtonDirective]) + "$compile", "$translate", "$tgTemplate", TaskStatusButtonDirective]) TaskIsIocaineButtonDirective = ($rootscope, $tgrepo, $confirm, $loading, $qqueue, $compile) -> diff --git a/app/coffee/modules/userstories/detail.coffee b/app/coffee/modules/userstories/detail.coffee index d819a947..1ee45eca 100644 --- a/app/coffee/modules/userstories/detail.coffee +++ b/app/coffee/modules/userstories/detail.coffee @@ -24,6 +24,7 @@ taiga = @.taiga mixOf = @.taiga.mixOf groupBy = @.taiga.groupBy bindOnce = @.taiga.bindOnce +bindMethods = @.taiga.bindMethods module = angular.module("taigaUserStories") @@ -50,6 +51,8 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @log, @appMetaService, @navUrls, @analytics, @translate) -> + bindMethods(@) + @scope.usRef = @params.usref @scope.sectionName = @translate.instant("US.SECTION_NAME") @.initializeEventHandlers() @@ -182,6 +185,50 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @.fillUsersAndRoles(project.members, project.roles) @.loadUs().then(=> @q.all([@.loadSprint(), @.loadTasks()])) + ### + # Note: This methods (onUpvote() and onDownvote()) are related to tg-vote-button. + # See app/modules/components/vote-button for more info + ### + onUpvote: -> + onSuccess = => + @.loadUs() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.userstories.upvote(@scope.usId).then(onSuccess, onError) + + onDownvote: -> + onSuccess = => + @.loadUs() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.userstories.downvote(@scope.usId).then(onSuccess, onError) + + ### + # Note: This methods (onWatch() and onUnwatch()) are related to tg-watch-button. + # See app/modules/components/watch-button for more info + ### + onWatch: -> + onSuccess = => + @.loadUs() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.userstories.watch(@scope.usId).then(onSuccess, onError) + + onUnwatch: -> + onSuccess = => + @.loadUs() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.userstories.unwatch(@scope.usId).then(onSuccess, onError) + module.controller("UserStoryDetailController", UserStoryDetailController) diff --git a/app/modules/components/vote-button/vote-button.controller.coffee b/app/modules/components/vote-button/vote-button.controller.coffee new file mode 100644 index 00000000..cc7ce7f9 --- /dev/null +++ b/app/modules/components/vote-button/vote-button.controller.coffee @@ -0,0 +1,36 @@ +class VoteButtonController + @.$inject = [ + "tgCurrentUserService", + ] + + constructor: (@currentUserService) -> + @.user = @currentUserService.getUser() + @.isMouseOver = false + @.loading = false + + showTextWhenMouseIsOver: -> + @.isMouseOver = true + + showTextWhenMouseIsLeave: -> + @.isMouseOver = false + + toggleVote: -> + @.loading = true + + if not @.item.is_voter + promise = @._upvote() + else + promise = @._downvote() + + promise.finally () => @.loading = false + + return promise + + _upvote: -> + @.onUpvote().then => + @.showTextWhenMouseIsLeave() + + _downvote: -> + @.onDownvote() + +angular.module("taigaComponents").controller("VoteButton", VoteButtonController) diff --git a/app/modules/components/vote-button/vote-button.controller.spec.coffee b/app/modules/components/vote-button/vote-button.controller.spec.coffee new file mode 100644 index 00000000..1d3a41e1 --- /dev/null +++ b/app/modules/components/vote-button/vote-button.controller.spec.coffee @@ -0,0 +1,82 @@ +describe "VoteButton", -> + provide = null + $controller = null + $rootScope = null + mocks = {} + + _mockCurrentUser = () -> + mocks.currentUser = { + getUser: sinon.stub() + } + + provide.value "tgCurrentUserService", mocks.currentUser + + _mocks = -> + mocks = { + onUpvote: sinon.stub(), + onDownvote: sinon.stub() + } + + module ($provide) -> + provide = $provide + _mockCurrentUser() + return null + + _inject = (callback) -> + inject (_$controller_, _$rootScope_) -> + $rootScope = _$rootScope_ + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaComponents" + _setup() + + it "upvote", (done) -> + $scope = $rootScope.$new() + + mocks.onUpvote = sinon.stub().promise() + + ctrl = $controller("VoteButton", $scope, { + item: {is_voter: false} + onUpvote: mocks.onUpvote + onDownvote: mocks.onDownvote + }) + + promise = ctrl.toggleVote() + + expect(ctrl.loading).to.be.true; + + mocks.onUpvote.resolve() + + promise.finally () -> + expect(mocks.onUpvote).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() + + it "downvote", (done) -> + $scope = $rootScope.$new() + + mocks.onDownvote = sinon.stub().promise() + + ctrl = $controller("VoteButton", $scope, { + item: {is_voter: true} + onUpvote: mocks.onUpvote + onDownvote: mocks.onDownvote + }) + + promise = ctrl.toggleVote() + + expect(ctrl.loading).to.be.true; + + mocks.onDownvote.resolve() + + promise.finally () -> + expect(mocks.onDownvote).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() diff --git a/app/modules/components/vote-button/vote-button.directive.coffee b/app/modules/components/vote-button/vote-button.directive.coffee new file mode 100644 index 00000000..19d7ebce --- /dev/null +++ b/app/modules/components/vote-button/vote-button.directive.coffee @@ -0,0 +1,14 @@ +VoteButtonDirective = -> + return { + scope: {} + controller: "VoteButton", + bindToController: { + item: "=", + onUpvote: "=", + onDownvote: "=" + } + controllerAs: "vm", + templateUrl: "components/vote-button/vote-button.html", + } + +angular.module("taigaComponents").directive("tgVoteButton", VoteButtonDirective) diff --git a/app/modules/components/vote-button/vote-button.jade b/app/modules/components/vote-button/vote-button.jade new file mode 100644 index 00000000..30b89e74 --- /dev/null +++ b/app/modules/components/vote-button/vote-button.jade @@ -0,0 +1,15 @@ +a( + href="" + title="{{ 'COMMON.VOTE_BUTTON.BUTTON_TITLE' | translate }}" + ng-if="::vm.user" + ng-click="vm.toggleVote()" + ng-class="{'active': vm.item.is_voter, 'is-hover': vm.item.is_voter && vm.isMouseOver, 'disable': !vm.user}" + ng-mouseover="vm.showTextWhenMouseIsOver()" + ng-mouseleave="vm.showTextWhenMouseIsLeave()" +) + span.track-icon + include ../../../svg/upvote.svg + span.track-button-counter( + title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.total_voters||0}:'messageformat' }}", + tg-loading="vm.loading" + ) {{ vm.item.total_voters }} diff --git a/app/modules/components/watch-button/watch-button.controller.coffee b/app/modules/components/watch-button/watch-button.controller.coffee new file mode 100644 index 00000000..c9f94b7a --- /dev/null +++ b/app/modules/components/watch-button/watch-button.controller.coffee @@ -0,0 +1,36 @@ +class WatchButtonController + @.$inject = [ + "tgCurrentUserService", + ] + + constructor: (@currentUserService) -> + @.user = @currentUserService.getUser() + @.isMouseOver = false + @.loading = false + + showTextWhenMouseIsOver: -> + @.isMouseOver = true + + showTextWhenMouseIsLeave: -> + @.isMouseOver = false + + toggleWatch: -> + @.loading = true + + if not @.item.is_watcher + promise = @._watch() + else + promise = @._unwatch() + + promise.finally () => @.loading = false + + return promise + + _watch: -> + @.onWatch().then => + @.showTextWhenMouseIsLeave() + + _unwatch: -> + @.onUnwatch() + +angular.module("taigaComponents").controller("WatchButton", WatchButtonController) diff --git a/app/modules/components/watch-button/watch-button.controller.spec.coffee b/app/modules/components/watch-button/watch-button.controller.spec.coffee new file mode 100644 index 00000000..6d8bf0ff --- /dev/null +++ b/app/modules/components/watch-button/watch-button.controller.spec.coffee @@ -0,0 +1,83 @@ +describe "WatchButton", -> + provide = null + $controller = null + $rootScope = null + mocks = {} + + _mockCurrentUser = () -> + mocks.currentUser = { + getUser: sinon.stub() + } + + provide.value "tgCurrentUserService", mocks.currentUser + + _mocks = -> + mocks = { + onWatch: sinon.stub(), + onUnwatch: sinon.stub() + } + + module ($provide) -> + provide = $provide + _mockCurrentUser() + return null + + _inject = (callback) -> + inject (_$controller_, _$rootScope_) -> + $rootScope = _$rootScope_ + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaComponents" + _setup() + + it "watch", (done) -> + $scope = $rootScope.$new() + + mocks.onWatch = sinon.stub().promise() + + ctrl = $controller("WatchButton", $scope, { + item: {is_watcher: false} + onWatch: mocks.onWatch + onUnwatch: mocks.onUnwatch + }) + + + promise = ctrl.toggleWatch() + + expect(ctrl.loading).to.be.true; + + mocks.onWatch.resolve() + + promise.finally () -> + expect(mocks.onWatch).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() + + it "unwatch", (done) -> + $scope = $rootScope.$new() + + mocks.onUnwatch = sinon.stub().promise() + + ctrl = $controller("WatchButton", $scope, { + item: {is_watcher: true} + onWatch: mocks.onWatch + onUnwatch: mocks.onUnwatch + }) + + promise = ctrl.toggleWatch() + + expect(ctrl.loading).to.be.true; + + mocks.onUnwatch.resolve() + + promise.finally () -> + expect(mocks.onUnwatch).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() diff --git a/app/modules/components/watch-button/watch-button.directive.coffee b/app/modules/components/watch-button/watch-button.directive.coffee new file mode 100644 index 00000000..ade7526a --- /dev/null +++ b/app/modules/components/watch-button/watch-button.directive.coffee @@ -0,0 +1,14 @@ +WatchButtonDirective = -> + return { + scope: {} + controller: "WatchButton", + bindToController: { + item: "=", + onWatch: "=", + onUnwatch: "=" + } + controllerAs: "vm", + templateUrl: "components/watch-button/watch-button.html", + } + +angular.module("taigaComponents").directive("tgWatchButton", WatchButtonDirective) diff --git a/app/modules/components/watch-button/watch-button.jade b/app/modules/components/watch-button/watch-button.jade new file mode 100644 index 00000000..2d25bbde --- /dev/null +++ b/app/modules/components/watch-button/watch-button.jade @@ -0,0 +1,44 @@ +mixin counter + span.track-button-counter( + title="{{ 'COMMON.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.watchers.length||0}:'messageformat' }}", + tg-loading="vm.loading" + ) + | {{ vm.item.watchers.length }} + + +//- Registered user button +a.track-button.watch-button.watch-container( + href="" + title="{{ 'COMMON.WATCH_BUTTON.BUTTON_TITLE' | translate }}" + ng-if="::vm.user" + ng-click="vm.toggleWatch()" + ng-class="{'active': vm.item.is_watcher, 'is-hover': vm.item.is_watcher && vm.isMouseOver}" + ng-mouseover="vm.showTextWhenMouseIsOver()" + ng-mouseleave="vm.showTextWhenMouseIsLeave()" +) + span.track-inner + span.track-icon + include ../../../svg/watch.svg + span( + ng-if="!vm.item.is_watcher", + translate="COMMON.WATCH_BUTTON.WATCH" + ) + span( + ng-if="vm.item.is_watcher && !vm.isMouseOver", + translate="COMMON.WATCH_BUTTON.WATCHING" + ) + span( + ng-if="vm.item.is_watcher && vm.isMouseOver", + translate="COMMON.WATCH_BUTTON.UNWATCH" + ) + +counter + +//- Anonymous user button +span.track-button.watch-button.watch-container( + ng-if="::!vm.user" +) + span.track-inner + span.track-icon + include ../../../svg/watch.svg + span(translate="COMMON.WATCH_BUTTON.WATCHERS") + +counter diff --git a/app/partials/issue/issues-detail.jade b/app/partials/issue/issues-detail.jade index c4f1d6d5..397bb6fc 100644 --- a/app/partials/issue/issues-detail.jade +++ b/app/partials/issue/issues-detail.jade @@ -1,74 +1,115 @@ doctype html -div.wrapper(ng-controller="IssueDetailController as ctrl", - ng-init="section='issues'") +div.wrapper( + ng-controller="IssueDetailController as ctrl", + ng-init="section='issues'" +) tg-project-menu + div.main.us-detail div.us-detail-header.header-with-actions include ../includes/components/mainTitle section.us-story-main-data - div.us-title(ng-class="{blocked: issue.is_blocked}") - h2.us-title-text - span.us-number(tg-bo-ref="issue.ref") - span.us-name(tg-editable-subject, ng-model="issue", required-perm="modify_issue") + header + tg-vote-button.upvote-btn( + item="issue" + on-upvote="ctrl.onUpvote" + on-downvote="ctrl.onDownvote" + ) + .us-title(ng-class="{blocked: issue.is_blocked}") + h2.us-title-text + span.us-number(tg-bo-ref="issue.ref") + span.us-name(tg-editable-subject, ng-model="issue", required-perm="modify_issue") - p.us-related-task(ng-if="issue.generated_user_stories.length") - | {{ 'ISSUES.PROMOTED'|translate }} - a(ng-repeat="us in issue.generated_user_stories", - tg-check-permission="view_us", href="", - tg-bo-title="'#' + us.ref + ' ' + us.subject", - tg-nav="project-userstories-detail:project=project.slug,ref=us.ref") - span(tg-bo-ref="us.ref") + p.us-related-task(ng-if="issue.generated_user_stories.length") + | {{ 'ISSUES.PROMOTED'|translate }} + a( + href="" + ng-repeat="us in issue.generated_user_stories" + tg-check-permission="view_us" + tg-bo-title="'#' + us.ref + ' ' + us.subject" + tg-nav="project-userstories-detail:project=project.slug,ref=us.ref" + ) + span(tg-bo-ref="us.ref") - p.external-reference(ng-if="issue.external_reference") - | {{ 'ISSUES.EXTERNAL_REFERENCE'|translate }} - a(target="_blank", tg-bo-href="issue.external_reference[1]", - title="{{'ISSUES.GO_TO_EXTERNAL_REFERENCE' | translate}}") - span {{ issue.external_reference[1] }} + p.external-reference(ng-if="issue.external_reference") + | {{ 'ISSUES.EXTERNAL_REFERENCE'|translate }} + a( + target="_blank" + tg-bo-href="issue.external_reference[1]" + title="{{'ISSUES.GO_TO_EXTERNAL_REFERENCE' | translate}}" + ) + span {{ issue.external_reference[1] }} - p.block-desc-container(ng-show="issue.is_blocked") - span.block-description-title(translate="COMMON.BLOCKED") - span.block-description(ng-bind="issue.blocked_note || ('ISSUES.BLOCKED' | translate)") + p.block-desc-container(ng-show="issue.is_blocked") + span.block-description-title(translate="COMMON.BLOCKED") + span.block-description(ng-bind="issue.blocked_note || ('ISSUES.BLOCKED' | translate)") - div.issue-nav - a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl", - title="{{'ISSUES.TITLE_PREVIOUS_ISSUE' | translate}}") - a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl", - title="{{'ISSUES.TITLE_NEXT_ISSUE' | translate}}") + div.issue-nav + a.icon.icon-arrow-left( + ng-show="previousUrl" + tg-bo-href="previousUrl" + title="{{'ISSUES.TITLE_PREVIOUS_ISSUE' | translate}}" + ) + a.icon.icon-arrow-right( + ng-show="nextUrl" + tg-bo-href="nextUrl" + title="{{'ISSUES.TITLE_NEXT_ISSUE' | translate}}" + ) div.tags-block(tg-tag-line, ng-model="issue", required-perm="modify_issue") section.duty-content(tg-editable-description, ng-model="issue", required-perm="modify_issue") // Custom Fields - tg-custom-attributes-values(ng-model="issue", type="issue", project="project", required-edition-perm="modify_issue") + tg-custom-attributes-values( + ng-model="issue" + type="issue" + project="project" + required-edition-perm="modify_issue" + ) tg-attachments(ng-model="issue", type="issue") tg-history(ng-model="issue", type="issue") - sidebar.menu-secondary.sidebar - section.us-status - h1(tg-issue-status-display, ng-model="issue") - tg-created-by-display.us-created-by(ng-model="issue") - div.duty-data-container - div.duty-data(tg-issue-type-button, ng-model="issue") - div.duty-data(tg-issue-severity-button, ng-model="issue") - div.duty-data(tg-issue-priority-button, ng-model="issue") - div.duty-data(tg-issue-status-button, ng-model="issue") + sidebar.menu-secondary.sidebar.ticket-data + section.status + .ticket-title(tg-issue-status-display, ng-model="issue") + tg-created-by-display.ticket-created-by(ng-model="issue") + div.ticket-data-container + div.ticket-status(tg-issue-type-button, ng-model="issue") + div.ticket-status(tg-issue-severity-button, ng-model="issue") + div.ticket-status(tg-issue-priority-button, ng-model="issue") + div.ticket-status(tg-issue-status-button, ng-model="issue") - section.duty-assigned-to(tg-assigned-to, ng-model="issue", required-perm="modify_issue") + section.ticket-assigned-to(tg-assigned-to, ng-model="issue", required-perm="modify_issue") - section.watchers(tg-watchers, ng-model="issue", required-perm="modify_issue") + section.track-buttons-container.ticket-track-buttons - section.us-detail-settings - tg-promote-issue-to-us-button(tg-check-permission="add_us", ng-model="issue") - tg-block-button(tg-check-permission="modify_issue", ng-model="issue") - tg-delete-button(tg-check-permission="delete_issue", - on-delete-title="{{'ISSUES.ACTION_DELETE' | translate}}", - on-delete-go-to-url="onDeleteGoToUrl", - ng-model="issue") + div.watch-button + tg-watch-button( + item="issue" + on-watch="ctrl.onWatch" + on-unwatch="ctrl.onUnwatch" + ) - div.lightbox.lightbox-block(tg-lb-block, title="ISSUES.LIGHTBOX_TITLE_BLOKING_ISSUE", ng-model="issue") + div.ticket-watchers( + tg-watchers + ng-model="issue" + required-perm="modify_issue" + ) + + section.ticket-detail-settings + tg-promote-issue-to-us-button(tg-check-permission="add_us", ng-model="issue") + tg-block-button(tg-check-permission="modify_issue", ng-model="issue") + tg-delete-button( + tg-check-permission="delete_issue", + on-delete-title="{{'ISSUES.ACTION_DELETE' | translate}}", + on-delete-go-to-url="onDeleteGoToUrl", + ng-model="issue" + ) + + div.lightbox.lightbox-block(tg-lb-block, ng-model="issue", title="ISSUES.LIGHTBOX_TITLE_BLOKING_ISSUE") div.lightbox.lightbox-select-user(tg-lb-assignedto) div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/task/task-detail.jade b/app/partials/task/task-detail.jade index e24dd306..1f732574 100644 --- a/app/partials/task/task-detail.jade +++ b/app/partials/task/task-detail.jade @@ -1,76 +1,130 @@ doctype html -div.wrapper(ng-controller="TaskDetailController as ctrl", - ng-init="section='backlog-kanban'") +div.wrapper( + ng-controller="TaskDetailController as ctrl" + ng-init="section='backlog-kanban'" +) tg-project-menu + div.main.us-detail div.us-detail-header.header-with-actions include ../includes/components/mainTitle .action-buttons a.button-gray( - tg-check-permission="view_milestones", - href="", title="{{'TASK.TITLE_LINK_TASKBOARD' | translate}}", - tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", - ng-if="sprint && project.is_backlog_activated", translate="TASK.LINK_TASKBOARD") + href="" + title="{{'TASK.TITLE_LINK_TASKBOARD' | translate}}" + tg-check-permission="view_milestones" + tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug" + ng-if="sprint && project.is_backlog_activated" + translate="TASK.LINK_TASKBOARD" + ) section.us-story-main-data - div.us-title(ng-class="{blocked: task.is_blocked}") - h2.us-title-text - span.us-number(tg-bo-ref="task.ref") - span.us-name(tg-editable-subject, ng-model="task", required-perm="modify_task") + header + tg-vote-button.upvote-btn( + item="task", + on-upvote="ctrl.onUpvote", + on-downvote="ctrl.onDownvote" + ) + div.us-title(ng-class="{blocked: task.is_blocked}") + h2.us-title-text + span.us-number(tg-bo-ref="task.ref") + span.us-name( + tg-editable-subject + ng-model="task" + required-perm="modify_task" + ) - h3.us-related-task(ng-if="us") - | {{ 'TASK.OWNER_US'|translate }} - a(tg-check-permission="view_us", href="", title="{{'TASK.TITLE_LINK_GO_OWNER' | translate}}", - tg-nav="project-userstories-detail:project=project.slug,ref=us.ref") - span(tg-bo-ref="us.ref") - span(tg-bo-bind="us.subject") + h3.us-related-task(ng-if="us") + | {{ 'TASK.OWNER_US'|translate }} + a( + href="" + tg-check-permission="view_us" + tg-nav="project-userstories-detail:project=project.slug,ref=us.ref" + title="{{'TASK.TITLE_LINK_GO_OWNER' | translate}}" + ) + span(tg-bo-ref="us.ref") + span(tg-bo-bind="us.subject") - p.external-reference(ng-if="task.external_reference") - a(target="_blank", tg-bo-href="task.external_reference[1]", - title="{{'TASK.TITLE_LINK_GO_ORIGIN' | translate}}") - | {{ "TASK.ORIGIN_US"| translate }} - span {{ task.external_reference[1] }} + p.external-reference(ng-if="task.external_reference") + a( + tg-bo-href="task.external_reference[1]", + target="_blank" + title="{{'TASK.TITLE_LINK_GO_ORIGIN' | translate}}" + ) + | {{ "TASK.ORIGIN_US"| translate }} + span {{ task.external_reference[1] }} - p.block-desc-container(ng-show="task.is_blocked") - span.block-description-title(translate="COMMON.BLOCKED") - span.block-description(ng-bind="task.blocked_note || ('TASK.BLOCKED_DESCRIPTION' | translate)") + p.block-desc-container(ng-show="task.is_blocked") + span.block-description-title(translate="COMMON.BLOCKED") + span.block-description( + ng-bind="task.blocked_note || ('TASK.BLOCKED_DESCRIPTION' | translate)" + ) - div.issue-nav - a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl", - title="{{'TASK.PREVIOUS' | translate}}") - a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl", - title="{{'TASK.NEXT' | translate}}") + div.issue-nav + a.icon.icon-arrow-left( + ng-show="previousUrl" + tg-bo-href="previousUrl" + title="{{'TASK.PREVIOUS' | translate}}" + ) + a.icon.icon-arrow-right( + ng-show="nextUrl" + tg-bo-href="nextUrl" + title="{{'TASK.NEXT' | translate}}" + ) div.tags-block(tg-tag-line, ng-model="task", required-perm="modify_task") section.duty-content(tg-editable-description, ng-model="task", required-perm="modify_task") // Custom Fields - tg-custom-attributes-values(ng-model="task", type="task", project="project", required-edition-perm="modify_task") + tg-custom-attributes-values( + ng-model="task" + type="task" + project="project" + required-edition-perm="modify_task" + ) tg-attachments(ng-model="task", type="task") tg-history(ng-model="task", type="task") - sidebar.menu-secondary.sidebar - section.us-status - h1(tg-task-status-display, ng-model="task") - div.us-created-by(tg-created-by-display, ng-model="task") - div.duty-data-container - div.duty-data(tg-task-status-button, ng-model="task") + sidebar.menu-secondary.sidebar.ticket-data - section.duty-assigned-to(tg-assigned-to, ng-model="task", required-perm="modify_task") + section.status - section.watchers(tg-watchers, ng-model="task", required-perm="modify_task") + .ticket-title(tg-task-status-display, ng-model="task") - section.us-detail-settings - tg-task-is-iocaine-button(ng-model="task") - tg-block-button(tg-check-permission="modify_task", ng-model="task") - tg-delete-button(tg-check-permission="delete_task", - on-delete-title="{{'TASK.TITLE_DELETE_ACTION' | translate}}", - on-delete-go-to-url="onDeleteGoToUrl", - ng-model="task") + .ticket-created-by(tg-created-by-display, ng-model="task") - div.lightbox.lightbox-block(tg-lb-block, title="TASK.LIGHTBOX_TITLE_BLOKING_TASK", ng-model="task") + .ticket-data-container + .ticket-status(tg-task-status-button, ng-model="task") + + section.ticket-assigned-to(tg-assigned-to, ng-model="task", required-perm="modify_task") + + section.track-buttons-container.ticket-track-buttons + div.watch-button + tg-watch-button( + item="task" + on-watch="ctrl.onWatch" + on-unwatch="ctrl.onUnwatch" + ) + + div.ticket-watchers( + tg-watchers, + ng-model="task", + required-perm="modify_task" + ) + + section.ticket-detail-settings + tg-task-is-iocaine-button(ng-model="task") + tg-block-button(tg-check-permission="modify_task", ng-model="task") + tg-delete-button( + tg-check-permission="delete_task" + on-delete-title="{{'TASK.TITLE_DELETE_ACTION' | translate}}" + on-delete-go-to-url="onDeleteGoToUrl" + ng-model="task" + ) + + div.lightbox.lightbox-block(tg-lb-block, ng-model="task", title="TASK.LIGHTBOX_TITLE_BLOKING_TASK") div.lightbox.lightbox-select-user(tg-lb-assignedto) div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/us/us-detail.jade b/app/partials/us/us-detail.jade index 95ca50f1..f3606df8 100644 --- a/app/partials/us/us-detail.jade +++ b/app/partials/us/us-detail.jade @@ -1,80 +1,152 @@ doctype html -div.wrapper(ng-controller="UserStoryDetailController as ctrl", - ng-init="section='backlog-kanban'") +div.wrapper( + ng-controller="UserStoryDetailController as ctrl", + ng-init="section='backlog-kanban'" +) tg-project-menu + div.main.us-detail div.us-detail-header.header-with-actions include ../includes/components/mainTitle .action-buttons a.button-gray( - tg-check-permission="view_milestones", - href="", title="{{'US.TITLE_LINK_TASKBOARD' | translate}}", - tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", - ng-if="sprint && project.is_backlog_activated", translate="US.LINK_TASKBOARD") + href="" + tg-check-permission="view_milestones" + tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug" + ng-if="sprint && project.is_backlog_activated" + title="{{'US.TITLE_LINK_TASKBOARD' | translate}}" + translate="US.LINK_TASKBOARD" + ) section.us-story-main-data - div.us-title(ng-class="{blocked: us.is_blocked}") - h2.us-title-text - span.us-number(tg-bo-ref="us.ref") - span.us-name(tg-editable-subject, ng-model="us", required-perm="modify_us") + header + tg-vote-button.upvote-btn( + item="us" + on-upvote="ctrl.onUpvote" + on-downvote="ctrl.onDownvote" + ) + div.us-title(ng-class="{blocked: us.is_blocked}") + h2.us-title-text + span.us-number(tg-bo-ref="us.ref") + span.us-name(tg-editable-subject, ng-model="us", required-perm="modify_us") - p.us-related-task(ng-if="us.origin_issue") - | {{ 'US.PROMOTED'|translate }} - a(tg-check-permission="view_us", href="", title="{{'US.TITLE_LINK_GO_TO_ISSUE' | translate}}", - tg-nav="project-issues-detail:project=project.slug,ref=us.origin_issue.ref" - tg-bo-title="'#' + us.origin_issue.ref + ' ' + us.origin_issue.subject") - span(tg-bo-ref="us.origin_issue.ref") + p.us-related-task(ng-if="us.origin_issue") + | {{ 'US.PROMOTED'|translate }} + a( + href="" + tg-check-permission="view_us" + tg-nav="project-issues-detail:project=project.slug,ref=us.origin_issue.ref" + tg-bo-title="'#' + us.origin_issue.ref + ' ' + us.origin_issue.subject" + title="{{'US.TITLE_LINK_GO_TO_ISSUE' | translate}}" + ) + span(tg-bo-ref="us.origin_issue.ref") - p.external-reference(ng-if="us.external_reference") - | {{ 'US.EXTERNAL_REFERENCE'|translate }} - a(target="_blank", tg-bo-href="us.external_reference[1]", - title="{{'US.GO_TO_EXTERNAL_REFERENCE' | translate}}") - span {{ us.external_reference[1] }} + p.external-reference(ng-if="us.external_reference") + | {{ 'US.EXTERNAL_REFERENCE'|translate }} + a( + tg-bo-href="us.external_reference[1]", + title="{{'US.GO_TO_EXTERNAL_REFERENCE' | translate}}" + target="_blank" + ) + span {{ us.external_reference[1] }} - p.block-desc-container(ng-show="us.is_blocked") - span.block-description-title(translate="COMMON.BLOCKED") - span.block-description(ng-bind="us.blocked_note || ('US.BLOCKED' | translate)") - div.issue-nav - a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl", - title="{{'US.PREVIOUS' | translate}}") - a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl", - title="{{'US.NEXT' | translate}}") + p.block-desc-container(ng-show="us.is_blocked") + span.block-description-title(translate="COMMON.BLOCKED") + span.block-description(ng-bind="us.blocked_note || ('US.BLOCKED' | translate)") + div.issue-nav + a.icon.icon-arrow-left( + ng-show="previousUrl" + tg-bo-href="previousUrl" + title="{{'US.PREVIOUS' | translate}}" + ) + a.icon.icon-arrow-right( + ng-show="nextUrl" + tg-bo-href="nextUrl" + title="{{'US.NEXT' | translate}}" + ) div.tags-block(tg-tag-line, ng-model="us", required-perm="modify_us") section.duty-content(tg-editable-description, ng-model="us", required-perm="modify_us") // Custom Fields - tg-custom-attributes-values(ng-model="us", type="userstory", project="project", required-edition-perm="modify_us") + tg-custom-attributes-values( + ng-model="us" + type="userstory" + project="project" + required-edition-perm="modify_us" + ) include ../includes/modules/related-tasks - tg-attachments(ng-model="us", type="us") - tg-history(ng-model="us", type="us") + tg-attachments( + ng-model="us" + type="us" + ) + tg-history( + ng-model="us" + type="us" + ) - sidebar.menu-secondary.sidebar - section.us-status - h1(tg-us-status-display, ng-model="us") - div.us-detail-progress-bar(tg-us-tasks-progress-display, ng-model="tasks") - tg-created-by-display.us-created-by(ng-model="us") + sidebar.menu-secondary.sidebar.ticket-data + section + div.ticket-title( + tg-us-status-display + ng-model="us" + ) + + tg-created-by-display.ticket-created-by(ng-model="us") + + //div.ticket-detail-progress-bar(tg-us-tasks-progress-display, ng-model="tasks") + + div.ticket-data-container + div.ticket-status( + tg-us-status-button + ng-model="us" + ) + + section.ticket-estimation tg-us-estimation(ng-model="us") - div.duty-data-container - div.duty-data(tg-us-status-button, ng-model="us") - section.duty-assigned-to(tg-assigned-to, ng-model="us", required-perm="modify_us") + section.ticket-assigned-to( + tg-assigned-to + ng-model="us" + required-perm="modify_us" + ) - section.watchers(tg-watchers, ng-model="us", required-perm="modify_us") + section.track-buttons-container.ticket-track-buttons + div.watch-button + tg-watch-button( + item="us" + on-watch="ctrl.onWatch" + on-unwatch="ctrl.onUnwatch" + ) - section.us-detail-settings + div.ticket-watchers( + tg-watchers + ng-model="us" + required-perm="modify_us" + ) + + section.ticket-detail-settings tg-us-team-requirement-button(ng-model="us") tg-us-client-requirement-button(ng-model="us") - tg-block-button(tg-check-permission="modify_us", ng-model="us") - tg-delete-button(tg-check-permission="delete_us", - on-delete-title="{{'Delete User Story' | translate}}", - on-delete-go-to-url="onDeleteGoToUrl", - ng-model="us") + tg-block-button( + tg-check-permission="modify_us" + ng-model="us" + ) + tg-delete-button( + tg-check-permission="delete_us" + on-delete-title="{{'Delete User Story' | translate}}" + on-delete-go-to-url="onDeleteGoToUrl" + ng-model="us" + ) - div.lightbox.lightbox-block(tg-lb-block, title="{{ 'US.LIGHTBOX_TITLE_BLOKING_US' | translate }}", ng-model="us") + div.lightbox.lightbox-block( + tg-lb-block + title="{{ 'US.LIGHTBOX_TITLE_BLOKING_US' | translate }}" + ng-model="us" + ) div.lightbox.lightbox-select-user(tg-lb-assignedto) div.lightbox.lightbox-select-user(tg-lb-watchers)