From 62f31aeab30958ab9d07cf136c55b35ef728cefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 29 Jul 2014 11:14:05 +0200 Subject: [PATCH] Adding admin roles page --- app/coffee/app.coffee | 3 + app/coffee/modules/admin/roles.coffee | 329 ++++++++++++++++++ app/coffee/modules/base.coffee | 1 + app/coffee/modules/resources.coffee | 1 + app/coffee/modules/resources/roles.coffee | 39 +++ app/partials/admin-roles.jade | 48 +-- app/partials/views/modules/admin-menu.jade | 2 +- .../views/modules/admin-submenu-roles.jade | 14 + .../views/modules/category-config.jade | 4 + app/styles/main.scss | 1 + app/styles/modules/admin/admin-roles.scss | 5 + .../modules/admin/admin-submenu-roles.scss | 40 +++ .../modules/common/category-config.scss | 5 +- 13 files changed, 457 insertions(+), 35 deletions(-) create mode 100644 app/coffee/modules/admin/roles.coffee create mode 100644 app/coffee/modules/resources/roles.coffee create mode 100644 app/partials/views/modules/admin-submenu-roles.jade create mode 100644 app/styles/modules/admin/admin-submenu-roles.scss diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index b9e8b900..7826859e 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -97,6 +97,9 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide) -> $routeProvider.when("/project/:pslug/admin/memberships", {templateUrl: "/partials/admin-memberships.html"}) + $routeProvider.when("/project/:pslug/admin/roles", + {templateUrl: "/partials/admin-roles.html"}) + # User settings $routeProvider.when("/project/:pslug/user-settings/user-profile", {templateUrl: "/partials/user-profile.html"}) diff --git a/app/coffee/modules/admin/roles.coffee b/app/coffee/modules/admin/roles.coffee new file mode 100644 index 00000000..6281e1c4 --- /dev/null +++ b/app/coffee/modules/admin/roles.coffee @@ -0,0 +1,329 @@ +### +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino Garcia +# Copyright (C) 2014 David Barragán Merino +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules/admin/memberships.coffee +### + +taiga = @.taiga + +mixOf = @.taiga.mixOf +bindOnce = @.taiga.bindOnce + +module = angular.module("taigaAdmin") + + +############################################################################# +## Project Roles Controller +############################################################################# + +class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin) + @.$inject = [ + "$scope", + "$rootScope", + "$tgRepo", + "$tgConfirm", + "$tgResources", + "$routeParams", + "$q", + "$tgLocation" + ] + + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location) -> + _.bindAll(@) + + @scope.sectionName = "Roles" #i18n + @scope.project = {} + + promise = @.loadInitialData() + promise.then null, -> + console.log "FAIL" #TODO + + loadProject: -> + return @rs.projects.get(@scope.projectId).then (project) => + @scope.project = project + return project + + loadRoles: -> + return @rs.roles.list(@scope.projectId).then (data) => + @scope.roles = data + @scope.role = @scope.roles[0] + return data + + loadInitialData: -> + promise = @repo.resolve({pslug: @params.pslug}).then (data) => + @scope.projectId = data.project + return data + + return promise.then(=> @.loadProject()) + .then(=> @.loadUsersAndRoles()) + .then(=> @.loadRoles()) + + setRole: (role) -> + @scope.role = role + @scope.$broadcast("role:changed", @scope.role) + + delete: -> + # TODO: i18n + title = "Delete Role" + subtitle = @scope.role.name + + @confirm.ask(title, subtitle).then => + promise = @repo.remove(@scope.role) + promise.then => + @confirm.notify('success') + @.loadRoles() + promise.then null, => + @confirm.notify('error') + + setComputable: -> + onSuccess = (role) => + @confirm.notify('success') + + onError = => + @confirm.notify("error") + @scope.role.computable = !@scope.role.computable + + @repo.save(@scope.role).then onSuccess, onError + + +module.controller("RolesController", RolesController) + + +RolesDirective = -> + link = ($scope, $el, $attrs) -> + $ctrl = $el.controller() + + $scope.$on "$destroy", -> + $el.off() + + return {link:link} + +module.directive("tgRoles", RolesDirective) + +NewRoleDirective = ($tgrepo) -> + DEFAULT_PERMISSIONS = ["view_project", "view_milestones", "view_us", "view_tasks", "view_issues"] + + link = ($scope, $el, $attrs) -> + $ctrl = $el.controller() + + $scope.$on "$destroy", -> + $el.off() + + $el.on "click", "a.add-button", (event) -> + event.preventDefault() + $el.find(".new").removeClass("hidden") + $el.find(".new").focus() + $el.find(".add-button").hide() + + $el.on "keyup", ".new", (event) -> + event.preventDefault() + if event.keyCode == 13 # Enter key + target = angular.element(event.currentTarget) + newRole = { + project: $scope.projectId + name: target.val() + permissions: DEFAULT_PERMISSIONS + order: _.max($scope.roles, (r) -> r.order).order + 1 + computable: false + } + + $el.find(".new").addClass("hidden") + $el.find(".new").val('') + + $tgrepo.create("roles", newRole).then (role) -> + $scope.roles.push(role) + $ctrl.setRole(role) + $el.find(".add-button").show() + + else if event.keyCode == 27 # ESC key + target = angular.element(event.currentTarget) + $el.find(".new").addClass("hidden") + $el.find(".new").val('') + $el.find(".add-button").show() + + return {link:link} + +module.directive("tgNewRole", ["$tgRepo", NewRoleDirective]) + + +# Use category-config.scss styles +RolePermissionsDirective = ($repo, $confirm) -> + resumeTemplate = _.template(""" +
<%- category.name %>
+
<%- category.activePermissions %>/<%- category.permissions.length %>
+
+ <% _.each(category.permissions, function(permission) { %> +
+ <% }) %> +
+
+ """) + + categoryTemplate = _.template(""" +
+
+
+
+
+ <% _.each(category.permissions, function(permission) { %> +
<%- permission.description %> +
+ checked="checked"<% } %>/> +
+
+
+ <% }) %> +
+
+
+ """) + + baseTemplate = _.template(""" +
+
+ """) + + link = ($scope, $el, $attrs) -> + $ctrl = $el.controller() + + generateCategoriesFromRole = (role) -> + setActivePermissions = (permissions) -> + return _.map(permissions, (x) -> _.extend({}, x, {active: x["key"] in role.permissions})) + + setActivePermissionsPerCategory = (category) -> + return _.map(category, (x) -> + _.extend({}, x, { + activePermissions: _.filter(x["permissions"], "active").length + }) + ) + + categories = [] + + projectPermissions = [ + { key: "view_project", description: "View project" } + ] + categories.push({ name: "Project", permissions: setActivePermissions(projectPermissions) }) + + milestonePermissions = [ + { key: "view_milestones", description: "View milestones" } + { key: "add_milestone", description: "Add milestone" } + { key: "modify_milestone", description: "Modify milestone" } + { key: "delete_last_milestone", description: "Delete last milestone" } + { key: "delete_milestone", description: "Delete milestone" } + { key: "add_us_to_milestone", description: "Add use to milestone" } + { key: "remove_us_from_milestone", description: "Remove us from milestone" } + { key: "reorder_us_on_milestone", description: "Reorder us on milestone" } + ] + categories.push({ name: "Milestones", permissions: setActivePermissions(milestonePermissions) }) + + userStoryPermissions = [ + { key: "view_us", description: "View user story" } + { key: "add_us", description: "Add user story" } + { key: "modify_us", description: "Modify user story" } + { key: "delete_us", description: "Delete user story" } + ] + categories.push({ name: "User Stories", permissions: setActivePermissions(userStoryPermissions) }) + + taskPermissions = [ + { key: "view_tasks", description: "View tasks" } + { key: "add_task", description: "Add task" } + { key: "modify_task", description: "Modify task" } + { key: "delete_task", description: "Delete task" } + ] + categories.push({ name: "Tasks", permissions: setActivePermissions(taskPermissions) }) + + issuePermissions = [ + { key: "view_issues", description: "View issues" } + { key: "add_issue", description: "Add issue" } + { key: "modify_issue", description: "Modify issue" } + { key: "delete_issue", description: "Delete issue" } + { key: "vote_issues", description: "Vote issues" } + ] + categories.push({ name: "Issues", permissions: setActivePermissions(issuePermissions) }) + + wikiPermissions = [ + { key: "view_wiki_pages", description: "View wiki pages" } + { key: "add_wiki_page", description: "Add wiki page" } + { key: "modify_wiki_page", description: "Modify wiki page" } + { key: "delete_wiki_page", description: "Delete wiki page" } + { key: "view_wiki_links", description: "View wiki links" } + { key: "add_wiki_link", description: "Add wiki link" } + { key: "modify_wiki_link", description: "Modify wiki link" } + { key: "delete_wiki_link", description: "Delete wiki link" } + ] + categories.push({ name: "Wiki", permissions: setActivePermissions(wikiPermissions) }) + + return setActivePermissionsPerCategory(categories) + + renderResume = (element, category) -> + element.find(".resume").html(resumeTemplate({category: category})) + + renderCategory = (category, index) -> + html = categoryTemplate({category: category, index: index}) + html = angular.element(html) + renderResume(html, category) + return html + + renderPermissions = () -> + $el.off() + html = baseTemplate() + _.each generateCategoriesFromRole($scope.role), (category, index) -> + html = angular.element(html).append(renderCategory(category, index)) + + $el.html(html) + $el.on "click", ".resume", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + target.next().toggleClass("open") + + $el.on "change", ".category-item input", (event) -> + getActivePermissions = -> + activePermissions = _.filter($el.find(".category-item input"), (t) -> angular.element(t).is(":checked")) + activePermissions = _.sortBy(_.map(activePermissions, (t) -> + permission = angular.element(t).parents(".category-item").data("id") + )) + return activePermissions + + target = angular.element(event.currentTarget) + $scope.role.permissions = getActivePermissions() + + onSuccess = (role) -> + $confirm.notify('success') + categories = generateCategoriesFromRole(role) + categoryId = target.parents(".category-config").data("id") + renderResume(target.parents(".category-config"), categories[categoryId]) + + onError = -> + $confirm.notify("error") + target.prop "checked", !target.prop("checked") + $scope.role.permissions = getActivePermissions() + + $repo.save($scope.role).then onSuccess, onError + + + $scope.$on "$destroy", -> + $el.off() + + $scope.$on "role:changed", -> + renderPermissions() + + bindOnce($scope, $attrs.ngModel, renderPermissions) + + return {link:link} + +module.directive("tgRolePermissions", ['$tgRepo', '$tgConfirm', RolePermissionsDirective]) diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index 2a070d9f..55033cf4 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -85,6 +85,7 @@ urls = { "project-admin-project-values-issue-priorities": "/project/:project/admin/project-values/issue-priorities" "project-admin-project-values-issue-severities": "/project/:project/admin/project-values/issue-severities" "project-admin-memberships": "/project/:project/admin/memberships" + "project-admin-roles": "/project/:project/admin/roles" "project-admin-project-profile-features": "/project/:project/admin/project-profile/features" "project-admin-project-values-us-status": "/project/:project/admin/project-values/us-status" diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index 795be0c8..f38665a4 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -116,6 +116,7 @@ module.run([ "$tgProjectsResourcesProvider", "$tgMembershipsResourcesProvider", "$tgInvitationsResourcesProvider", + "$tgRolesResourcesProvider", "$tgSprintsResourcesProvider", "$tgUserstoriesResourcesProvider", "$tgTasksResourcesProvider", diff --git a/app/coffee/modules/resources/roles.coffee b/app/coffee/modules/resources/roles.coffee new file mode 100644 index 00000000..9bcbf950 --- /dev/null +++ b/app/coffee/modules/resources/roles.coffee @@ -0,0 +1,39 @@ +### +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino Garcia +# Copyright (C) 2014 David Barragán Merino +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules/resources/memberships.coffee +### + + +taiga = @.taiga + +resourceProvider = ($repo, $http, $urls) -> + service = {} + + service.get = (id) -> + return $repo.queryOne("roles", id) + + service.list = (projectId) -> + return $repo.queryMany("roles", {project: projectId}) + + return (instance) -> + instance.roles = service + + +module = angular.module("taigaResources") +module.factory("$tgRolesResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", resourceProvider]) diff --git a/app/partials/admin-roles.jade b/app/partials/admin-roles.jade index ebda81f4..95893ef1 100644 --- a/app/partials/admin-roles.jade +++ b/app/partials/admin-roles.jade @@ -1,48 +1,30 @@ -extends layout +extends dummy-layout block head title Taiga Project management web application with scrum in mind! block content - div.wrapper - sidebar.menu-secondary.sidebar + div.wrapper.roles(ng-controller="RolesController as ctrl", + ng-init="section='admin'", tg-roles) + sidebar.menu-secondary.sidebar(tg-admin-navigation="roles") include views/modules/admin-menu sidebar.menu-tertiary.sidebar - include views/modules/admin-submenu + include views/modules/admin-submenu-roles section.main.admin-roles header include views/components/mainTitle + a.button.button-red.delete-role(href="", title="Delete", ng-click="ctrl.delete()") Delete + p.total - | UX - span (6 members with this role) + | {{ role.name }} + span ({{ role.members_count }} members with this role) - include views/modules/category-config + div.general-category + | Can do estimations? + div.check + input(type="checkbox", ng-model="role.computable", ng-change="ctrl.setComputable()") + div - script(type='text/javascript'). - function randomIntFromInterval(min,max) { - return Math.floor(Math.random()*(max-min+1)+min); - } - (function() { - if(randomIntFromInterval(0, 4) !== 4) return true; - - var inputs = document.querySelectorAll('input'); - - function change(input) { - var num = randomIntFromInterval(100, 600); - - setTimeout(function() { - if(input.hasAttribute('checked')) { - input.removeAttribute('checked'); - } else { - input.setAttribute('checked', 'checked'); - } - }, num); - } - setInterval(function() { - for(var i = 0; i < inputs.length; i++) { - change(inputs[i]); - } - }, 500); - })() + div(tg-role-permissions, ng-model="role") diff --git a/app/partials/views/modules/admin-menu.jade b/app/partials/views/modules/admin-menu.jade index eb6353e8..1fcfe3ae 100644 --- a/app/partials/views/modules/admin-menu.jade +++ b/app/partials/views/modules/admin-menu.jade @@ -17,6 +17,6 @@ section.admin-menu span.title Memberships span.icon.icon-arrow-right li#adminmenu-roles - a(href="") + a(href="" tg-nav="project-admin-roles:project=project.slug") span.title Roles span.icon.icon-arrow-right diff --git a/app/partials/views/modules/admin-submenu-roles.jade b/app/partials/views/modules/admin-submenu-roles.jade new file mode 100644 index 00000000..e4c2455d --- /dev/null +++ b/app/partials/views/modules/admin-submenu-roles.jade @@ -0,0 +1,14 @@ +section.admin-submenu-roles + header + h1 Roles + + nav + ul + li(ng-repeat="item in roles") + a(href="" ng-click="ctrl.setRole(item)", ng-class="{active: role.id == item.id}") {{ item.name }} + span.icon.icon-arrow-right + + div(tg-new-role) + a.button.button-gray.add-button(href="", title="Add New Role") + span.text + New role + input(type="text", class="hidden new") diff --git a/app/partials/views/modules/category-config.jade b/app/partials/views/modules/category-config.jade index 854682f6..24a8fe44 100644 --- a/app/partials/views/modules/category-config.jade +++ b/app/partials/views/modules/category-config.jade @@ -1,3 +1,7 @@ +///////////////////////////////////////////////////////////////////////////// +// Included in the admin-roles.jade and in the RolePermissionsDirective // +// in the modules/admin/roles.coffee file. // +///////////////////////////////////////////////////////////////////////////// div.general-category | Can do estimations? div.check diff --git a/app/styles/main.scss b/app/styles/main.scss index 4d879775..9066641e 100755 --- a/app/styles/main.scss +++ b/app/styles/main.scss @@ -96,6 +96,7 @@ $prefix-for-spec: true; //modules admin @import 'modules/admin/admin-menu'; @import 'modules/admin/admin-submenu'; +@import 'modules/admin/admin-submenu-roles'; @import 'modules/admin/admin-roles'; @import 'modules/admin/admin-functionalities'; @import 'modules/admin/admin-membership-table'; diff --git a/app/styles/modules/admin/admin-roles.scss b/app/styles/modules/admin/admin-roles.scss index 6a6fe5c8..c7210f3b 100644 --- a/app/styles/modules/admin/admin-roles.scss +++ b/app/styles/modules/admin/admin-roles.scss @@ -50,4 +50,9 @@ } } } + .delete-role { + position: absolute; + right: 2rem; + top: 2rem; + } } diff --git a/app/styles/modules/admin/admin-submenu-roles.scss b/app/styles/modules/admin/admin-submenu-roles.scss new file mode 100644 index 00000000..f2cd8e61 --- /dev/null +++ b/app/styles/modules/admin/admin-submenu-roles.scss @@ -0,0 +1,40 @@ +.admin-submenu-roles { + h1 { + @extend %xlarge; + color: $white; + } + li { + @extend %larger; + @extend %title; + border-bottom: 1px solid #a6b2a7; + text-transform: uppercase; + &:last-child { + border-bottom: 0; + } + } + a { + color: $white; + display: block; + padding: 1rem 0 1rem 1rem; + &.active, + &:hover { + color: $blackish; + .icon { + @include transition (opacity .3s linear); + opacity: 1; + } + } + } + .icon { + color: $white; + float: right; + opacity: 0; + } + .button-gray { + padding: .5rem 0; + text-align: center; + &:hover { + background-color: darken($button-gray-hover, 15%); + } + } +} diff --git a/app/styles/modules/common/category-config.scss b/app/styles/modules/common/category-config.scss index 28f03e25..c65a9542 100644 --- a/app/styles/modules/common/category-config.scss +++ b/app/styles/modules/common/category-config.scss @@ -38,9 +38,12 @@ text-align: right; } .category-items { + @include slide(400px, overflow-y); background-color: $whitish; - padding: 2rem 1rem; width: 100%; + .items-container { + padding: 2rem 1rem; + } } .category-item { border-bottom: 1px dotted $gray;