diff --git a/CHANGELOG.md b/CHANGELOG.md index 684fee24..c9327d06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,12 @@ ## 1.3.0 Dryas hookeriana (Unreleased) ### Features +- GitHub integration (Phase I): + + Add button to login/singin with a GitHub account. + + Create Admin Panel with the GitHub webhooks settings. - Differentiate blocked user stories on a milestone. -### Misc +### Misc - Lots of small and not so small bugfixes. @@ -27,12 +30,12 @@ ## 1.1.0 Alnus maximowiczii (2014-10-13) -### Features ### +### Features - Promote an issue to a user story. - Changed configuration format from coffeescript file to json. - Add builtin analytics support. -### Misc ### +### Misc - Fix bug related to stange behavior of browser autofill and angularjs on login page. - Fix bug on userstories ordering on sprints. - Fix bug of projects list visualization on project nav on first page loading. @@ -40,10 +43,10 @@ ## 1.0.0 (2014-10-07) -### Features ### +### Features - Redesign for taskboard and backlog summaries - Allow feedback for users from the platform - Real time changes for backlog, taskboard, kanban and issues -### Misc ### +### Misc - Lots of small and not so small bugfixes diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index f5c58e35..0b014ad3 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -94,18 +94,20 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven {templateUrl: "/partials/admin-memberships.html"}) $routeProvider.when("/project/:pslug/admin/roles", {templateUrl: "/partials/admin-roles.html"}) + $routeProvider.when("/project/:pslug/admin/third-parties/github", + {templateUrl: "/partials/admin-third-parties-github.html"}) # User settings $routeProvider.when("/project/:pslug/user-settings/user-profile", - {templateUrl: "/partials/user-profile.html"}) + {templateUrl: "/partials/user-profile.html"}) $routeProvider.when("/project/:pslug/user-settings/user-change-password", - {templateUrl: "/partials/user-change-password.html"}) + {templateUrl: "/partials/user-change-password.html"}) $routeProvider.when("/project/:pslug/user-settings/user-avatar", - {templateUrl: "/partials/user-avatar.html"}) + {templateUrl: "/partials/user-avatar.html"}) $routeProvider.when("/project/:pslug/user-settings/mail-notifications", - {templateUrl: "/partials/mail-notifications.html"}) + {templateUrl: "/partials/mail-notifications.html"}) $routeProvider.when("/change-email/:email_token", - {templateUrl: "/partials/change-email.html"}) + {templateUrl: "/partials/change-email.html"}) $routeProvider.when("/cancel-account/:cancel_token", {templateUrl: "/partials/cancel-account.html"}) @@ -216,6 +218,7 @@ modules = [ "taigaUserSettings", "taigaFeedback", "taigaPlugins", + "taigaIntegrations", # Vendor modules "ngRoute", diff --git a/app/coffee/modules/admin/third-parties.coffee b/app/coffee/modules/admin/third-parties.coffee new file mode 100644 index 00000000..44c4782c --- /dev/null +++ b/app/coffee/modules/admin/third-parties.coffee @@ -0,0 +1,123 @@ +### +# 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/third-parties.coffee +### + +taiga = @.taiga + +mixOf = @.taiga.mixOf + +module = angular.module("taigaAdmin") + + +############################################################################# +## Github Controller +############################################################################# + +class GithubController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.FiltersMixin) + @.$inject = [ + "$scope", + "$tgRepo", + "$tgResources", + "$routeParams", + "$appTitle" + ] + + constructor: (@scope, @repo, @rs, @params, @appTitle) -> + _.bindAll(@) + + @scope.sectionName = "Github" #i18n + @scope.project = {} + @scope.anyComputableRole = true + + promise = @.loadInitialData() + + promise.then () => + @appTitle.set("Github - " + @scope.project.name) + + promise.then null, @.onInitialDataError.bind(@) + + loadModules: -> + return @rs.modules.list(@scope.projectId, "github").then (github) => + @scope.github = github + + loadProject: -> + return @rs.projects.get(@scope.projectId).then (project) => + @scope.project = project + @scope.$emit('project:loaded', project) + @scope.anyComputableRole = _.some(_.map(project.roles, (point) -> point.computable)) + + return project + + loadInitialData: -> + promise = @repo.resolve({pslug: @params.pslug}).then (data) => + @scope.projectId = data.project + return data + + return promise.then(=> @.loadProject()) + .then(=> @.loadModules()) + + +module.controller("GithubController", GithubController) + +SelectInputText = -> + link = ($scope, $el, $attrs) -> + $el.on "click", ".select-input-content", () -> + $el.find("input").select() + $el.find(".help-copy").addClass("visible") + + return {link:link} + +module.directive("tgSelectInputText", SelectInputText) + +############################################################################# +## GithubWebhooks Directive +############################################################################# + +GithubWebhooksDirective = ($repo, $confirm, $loading) -> + link = ($scope, $el, $attrs) -> + form = $el.find("form").checksley({"onlyOneErrorElement": true}) + submit = (target) => + return if not form.validate() + + $loading.start(target) + + promise = $repo.saveAttribute($scope.github, "github") + promise.then -> + $loading.finish(target) + $confirm.notify("success") + + promise.then null, (data) -> + $loading.finish(target) + form.setErrors(data) + if data._error_message + $confirm.notify("error", data._error_message) + + $el.on "click", "a.button-green", (event) -> + event.preventDefault() + target = angular.element(event.currentTarget) + submit(target) + + $el.on "submit", "form", (event) -> + event.preventDefault() + submit() + + return {link:link} + +module.directive("tgGithubWebhooks", ["$tgRepo", "$tgConfirm", "$tgLoading", GithubWebhooksDirective]) diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index 8310d95c..3aeb6e99 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -89,6 +89,7 @@ urls = { "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-third-parties-github": "/project/:project/admin/third-parties/github" # User settings "user-settings-user-profile": "/project/:project/user-settings/user-profile" diff --git a/app/coffee/modules/base/repository.coffee b/app/coffee/modules/base/repository.coffee index 05ca4846..9241975a 100644 --- a/app/coffee/modules/base/repository.coffee +++ b/app/coffee/modules/base/repository.coffee @@ -31,6 +31,9 @@ class RepositoryService extends taiga.Service idAttrName = model.getIdAttrName() return "#{@urls.resolve(model.getName())}/#{model[idAttrName]}" + resolveUrlForAttributeModel: (model) -> + return @urls.resolve(model.getName(), model.parent) + create: (name, data, dataTypes={}, extraParams={}) -> defered = @q.defer() url = @urls.resolve(name) @@ -89,6 +92,37 @@ class RepositoryService extends taiga.Service return defered.promise + saveAttribute: (model, attribute, patch=true) -> + defered = @q.defer() + + if not model.isModified() and patch + defered.resolve(model) + return defered.promise + + url = @.resolveUrlForAttributeModel(model) + + data = {} + + data[attribute] = model.getAttrs() + + if patch + promise = @http.patch(url, data) + else + promise = @http.put(url, data) + + promise.success (data, status) => + model._isModified = false + model._attrs = _.extend(model.getAttrs(), data) + model._modifiedAttrs = {} + + model.applyCasts() + defered.resolve(model) + + promise.error (data, status) -> + defered.reject(data) + + return defered.promise + refresh: (model) -> defered = @q.defer() @@ -115,6 +149,19 @@ class RepositoryService extends taiga.Service return @http.get(url, params, httpOptions).then (data) => return _.map(data.data, (x) => @model.make_model(name, x)) + queryOneAttribute: (name, id, attribute, params, options={}) -> + url = @urls.resolve(name, id) + httpOptions = {headers: {}} + + if not options.enablePagination + httpOptions.headers["x-disable-pagination"] = "1" + + return @http.get(url, params, httpOptions).then (data) => + model = @model.make_model(name, data.data[attribute]) + model.parent = id + + return model + queryOne: (name, id, params, options={}) -> url = @urls.resolve(name) url = "#{url}/#{id}" if id diff --git a/app/coffee/modules/common.coffee b/app/coffee/modules/common.coffee index 11d3fa79..594469a8 100644 --- a/app/coffee/modules/common.coffee +++ b/app/coffee/modules/common.coffee @@ -29,10 +29,10 @@ module = angular.module("taigaCommon", []) CheckPermissionDirective = -> render = ($el, project, permission) -> - $el.show() if project.my_permissions.indexOf(permission) > -1 + $el.removeClass('hidden') if project.my_permissions.indexOf(permission) > -1 link = ($scope, $el, $attrs) -> - $el.hide() + $el.addClass('hidden') permission = $attrs.tgCheckPermission $scope.$watch "project", (project) -> diff --git a/app/coffee/modules/common/components.coffee b/app/coffee/modules/common/components.coffee index 4f0e4bf7..f6a57961 100644 --- a/app/coffee/modules/common/components.coffee +++ b/app/coffee/modules/common/components.coffee @@ -132,8 +132,12 @@ CreatedByDisplayDirective = -> link = ($scope, $el, $attrs) -> render = (model) -> + owner = $scope.usersById?[model.owner] or { + full_name_display: "external user" + photo: "/images/unnamed.png" + } html = template({ - owner: $scope.usersById?[model.owner] + owner: owner date: moment(model.created_date).format("DD MMM YYYY HH:mm") }) $el.html(html) diff --git a/app/coffee/modules/common/loader.coffee b/app/coffee/modules/common/loader.coffee index 6d04b025..ac1d745a 100644 --- a/app/coffee/modules/common/loader.coffee +++ b/app/coffee/modules/common/loader.coffee @@ -97,8 +97,6 @@ Loader = () -> startCurrentPageLoader: () -> if config.enabled start() - else - pageLoaded(true) onStart: (fn) -> $rootscope.$on("loader:start", fn) diff --git a/app/coffee/modules/integrations.coffee b/app/coffee/modules/integrations.coffee new file mode 100644 index 00000000..368e313c --- /dev/null +++ b/app/coffee/modules/integrations.coffee @@ -0,0 +1,22 @@ +### +# 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/integrations.coffee +### + +module = angular.module("taigaIntegrations", []) diff --git a/app/coffee/modules/integrations/github.coffee b/app/coffee/modules/integrations/github.coffee new file mode 100644 index 00000000..fa61a86f --- /dev/null +++ b/app/coffee/modules/integrations/github.coffee @@ -0,0 +1,111 @@ +### +# 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/integrations/github.coffee +### + +taiga = @.taiga + +module = angular.module("taigaIntegrations") + +AUTH_URL = "https://github.com/login/oauth/authorize" + + +############################################################################# +## User story team requirements button directive +############################################################################# + +GithubLoginButtonDirective = ($window, $params, $location, $config, $events, $confirm, $auth, $navUrls, $loader) -> + # Login or registar a user with his/her github account. + # + # Example: + # tg-github-login-button() + # + # Requirements: + # - ... + + template = """ + + + Login with Github + + """ #TODO: i18n + + link = ($scope, $el, $attrs) -> + clientId = $config.get("gitHubClientId", null) + return if not clientId + + renderGitHubButton = -> + $el.html(template) if clientId + + loginOnSuccess = (response) -> + if $params.next and $params.next != $navUrls.resolve("login") + nextUrl = $params.next + else + nextUrl = $navUrls.resolve("home") + + $events.setupConnection() + + $location.search("next", null) + $location.search("token", null) + $location.search("state", null) + $location.search("code", null) + $location.path(nextUrl) + + loginOnError = (response) -> + $location.search("state", null) + $location.search("code", null) + $loader.pageLoaded() + + if response.data.error_message + $confirm.notify("light-error", response.data.error_message ) + else + $confirm.notify("light-error", "Our Oompa Loompas have not been able to get you + credentials from GitHub.") #TODO: i18n + + loginWithGitHubAccount = -> + type = $params.state + code = $params.code + token = $params.token + + return if not (type == "github" and code) + $loader.start() + + data = {code: code, token: token} + $auth.login(data, type).then(loginOnSuccess, loginOnError) + + renderGitHubButton() + loginWithGitHubAccount() + + $el.on "click", ".button-github", (event) -> + redirectToUri = $location.absUrl() + url = "#{AUTH_URL}?client_id=#{clientId}&redirect_uri=#{redirectToUri}&state=github&scope=user:email" + $window.location.href = url + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + restrict: "EA" + template: "" + } + +module.directive("tgGithubLoginButton", ["$window", '$routeParams', "$tgLocation", "$tgConfig", "$tgEvents", + "$tgConfirm", "$tgAuth", "$tgNavUrls", "tgLoader", + GithubLoginButtonDirective]) diff --git a/app/coffee/modules/kanban/main.coffee b/app/coffee/modules/kanban/main.coffee index 3ab7a514..1038f5b3 100644 --- a/app/coffee/modules/kanban/main.coffee +++ b/app/coffee/modules/kanban/main.coffee @@ -276,25 +276,6 @@ KanbanDirective = ($repo, $rootscope) -> module.directive("tgKanban", ["$tgRepo", "$rootScope", KanbanDirective]) -############################################################################# -## Kanban Row Size Fixer Directive -############################################################################# - -KanbanRowWidthFixerDirective = -> - link = ($scope, $el, $attrs) -> - bindOnce $scope, "usStatusList", (statuses) -> - itemSize = 310 - size = (statuses.length * itemSize) - 10 - $el.css("width", "#{size}px") - - $scope.$on "$destroy", -> - $el.off() - - return {link: link} - -module.directive("tgKanbanRowWidthFixer", KanbanRowWidthFixerDirective) - - ############################################################################# ## Kanban Column Height Fixer Directive ############################################################################# @@ -355,6 +336,36 @@ KanbanUserstoryDirective = ($rootscope) -> module.directive("tgKanbanUserstory", ["$rootScope", KanbanUserstoryDirective]) +############################################################################# +## Kanban Squish Column Directive +############################################################################# + +KanbanSquishColumnDirective = (rs) -> + + link = ($scope, $el, $attrs) -> + $scope.$on "project:loaded", (event, project) -> + $scope.folds = rs.kanban.getStatusColumnModes(project.id) + updateTableWidth() + + $scope.foldStatus = (status) -> + $scope.folds[status.id] = !!!$scope.folds[status.id] + rs.kanban.storeStatusColumnModes($scope.projectId, $scope.folds) + updateTableWidth() + return + + updateTableWidth = -> + columnWidths = _.map $scope.usStatusList, (status) -> + if $scope.folds[status.id] + return 40 + else + return 310 + totalWidth = _.reduce columnWidths, (total, width) -> + return total + width + $el.find('.kanban-table-inner').css("width", totalWidth) + + return {link: link} + +module.directive("tgKanbanSquishColumn", ["$tgResources", KanbanSquishColumnDirective]) ############################################################################# ## Kaban WIP Limit Directive diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index 18675d29..2bce1f4b 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -83,6 +83,7 @@ urls = { "issue-types": "/issue-types" "priorities": "/priorities" "severities": "/severities" + "project-modules": "/projects/%s/modules" # History "history/us": "/history/userstory" @@ -138,5 +139,6 @@ module.run([ "$tgMdRenderResourcesProvider", "$tgHistoryResourcesProvider", "$tgKanbanResourcesProvider", + "$tgModulesResourcesProvider", initResources ]) diff --git a/app/coffee/modules/resources/kanban.coffee b/app/coffee/modules/resources/kanban.coffee index a5c1fd53..b1268dfe 100644 --- a/app/coffee/modules/resources/kanban.coffee +++ b/app/coffee/modules/resources/kanban.coffee @@ -27,6 +27,7 @@ generateHash = taiga.generateHash resourceProvider = ($storage) -> service = {} hashSuffixStatusViewModes = "kanban-statusviewmodels" + hashSuffixStatusColumnModes = "kanban-statuscolumnmodels" service.storeStatusViewModes = (projectId, params) -> ns = "#{projectId}:#{hashSuffixStatusViewModes}" @@ -38,6 +39,16 @@ resourceProvider = ($storage) -> hash = generateHash([projectId, ns]) return $storage.get(hash) or {} + service.storeStatusColumnModes = (projectId, params) -> + ns = "#{projectId}:#{hashSuffixStatusColumnModes}" + hash = generateHash([projectId, ns]) + $storage.set(hash, params) + + service.getStatusColumnModes = (projectId) -> + ns = "#{projectId}:#{hashSuffixStatusColumnModes}" + hash = generateHash([projectId, ns]) + return $storage.get(hash) or {} + return (instance) -> instance.kanban = service diff --git a/app/coffee/modules/resources/modules.coffee b/app/coffee/modules/resources/modules.coffee new file mode 100644 index 00000000..fcb6e6a7 --- /dev/null +++ b/app/coffee/modules/resources/modules.coffee @@ -0,0 +1,12 @@ +resourceProvider = ($repo) -> + service = {} + + service.list = (projectId, module) -> + return $repo.queryOneAttribute("project-modules", projectId, module) + + return (instance) -> + instance.modules = service + + +module = angular.module("taigaResources") +module.factory("$tgModulesResourcesProvider", ["$tgRepo", resourceProvider]) diff --git a/app/fonts/taiga.eot b/app/fonts/taiga.eot index 4d7ef8bb..3993e361 100644 Binary files a/app/fonts/taiga.eot and b/app/fonts/taiga.eot differ diff --git a/app/fonts/taiga.svg b/app/fonts/taiga.svg index ff41af91..f347cfdc 100644 --- a/app/fonts/taiga.svg +++ b/app/fonts/taiga.svg @@ -11,39 +11,41 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/taiga.ttf b/app/fonts/taiga.ttf index 8fb9b030..a41437dd 100644 Binary files a/app/fonts/taiga.ttf and b/app/fonts/taiga.ttf differ diff --git a/app/fonts/taiga.woff b/app/fonts/taiga.woff index 14d2f8f5..383aba43 100644 Binary files a/app/fonts/taiga.woff and b/app/fonts/taiga.woff differ diff --git a/app/images/github-help.png b/app/images/github-help.png new file mode 100644 index 00000000..43c64f77 Binary files /dev/null and b/app/images/github-help.png differ diff --git a/app/partials/admin-third-parties-github.jade b/app/partials/admin-third-parties-github.jade new file mode 100644 index 00000000..4f84553e --- /dev/null +++ b/app/partials/admin-third-parties-github.jade @@ -0,0 +1,95 @@ +block head + title Taiga Your agile, free, and open source project management tool + +block content + div.wrapper.roles(tg-github-webhooks, ng-controller="GithubController as ctrl", + ng-init="section='admin'") + sidebar.menu-secondary.sidebar(tg-admin-navigation="third-parties") + include views/modules/admin-menu + sidebar.menu-tertiary.sidebar(tg-admin-navigation="third-parties-github") + include views/modules/admin-submenu-third-parties + + section.main.admin-common.admin-third-parties + include views/components/mainTitle + + form + fieldset + label(for="secret-key") Secret key + input(type="text", name="secret-key", ng-model="github.secret", placeholder="Secret key", id="secret-key") + + fieldset + .select-input-text(tg-select-input-text) + div + label(for="payload-url") Payload URL + .field-with-option + input(type="text", ng-model="github.webhooks_url", name="payload-url", readonly="readonly", placeholder="Payload URL", id="payload-url") + .option-wrapper.select-input-content + .icon.icon-copy + .help-copy Copy to clipboard: Ctrl+C + + input(type="submit", class="hidden") + a.button.button-green(href="") Save + + + .help + h2 How to use it + + h3 Configure Taiga + ol + li Fill + span Secret key + | or use the auto generated one + + li Copy the + span Payload URL field. + + h3 Configure Github + ol + li Go to your github repository. + li Click on + span Settings + | > + span Webhooks & Services + | > + span Add webhook + + li On that screen set the payload url with the payload url of this screen. + li Secret must be filled with the same content as the secret field of this screen. + li Content type must be + span application/json. + li Taiga currently listen for three different kind of events: + ol + li Push events: changing element status via commit message + li Issues: issues created in github appear automatically in Taiga + li Issue comment: issue comments created in github appear automatically in Taiga + + p Just check "send me everything" or just the events you want Taiga to listen for. + + .img + .alt-image Github Webhooke page + img(src="/images/github-help.png", alt="webhook") + + h2 Changing elements status via commit message + + p + | The status of any issue, task or user story can be changed via commit message. + | Just add to your commit message something like: + + code + | TG-REF #STATUS + + ul.code-info + li + span REF: + | US/Issue/Task reference of the element you want to modify + li + span STATUS: + | New status slug to set, you can find all of them in: + a(href="", tg-nav="project-admin-project-values-us-status:project=project.slug") US STATUSES. + + h3 An example please! + + code + | TG-123 #closed + + p In this example, 123 is an issue reference and with this command, the issue will change its status to closed. diff --git a/app/partials/issues-detail.jade b/app/partials/issues-detail.jade index 91448c44..971daecf 100644 --- a/app/partials/issues-detail.jade +++ b/app/partials/issues-detail.jade @@ -23,6 +23,10 @@ block content 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") This issue has been created from + a(target="_blank", tg-bo-href="issue.external_reference[1]", title="Go to origin") + span {{ issue.external_reference[1] }} + p.block-desc-container(ng-show="issue.is_blocked") span.block-description-title Blocked span.block-description(ng-bind="issue.blocked_note || 'This issue is blocked'") diff --git a/app/partials/task-detail.jade b/app/partials/task-detail.jade index f1bb79c6..c9d4e80e 100644 --- a/app/partials/task-detail.jade +++ b/app/partials/task-detail.jade @@ -21,12 +21,18 @@ block content 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 This task belongs to a(tg-check-permission="view_us", href="", title="Go to user story", tg-nav="project-userstories-detail:project=project.slug, ref=us.ref", ng-if="us") span(tg-bo-ref="us.ref") span(tg-bo-bind="us.subject") + + p.external-reference(ng-if="task.external_reference") This task has been created from + a(target="_blank", tg-bo-href="task.external_reference[1]", title="Go to origin") + span {{ task.external_reference[1] }} + p.block-desc-container(ng-show="task.is_blocked") span.block-description-title Blocked span.block-description(ng-bind="task.blocked_note || 'This task is blocked'") diff --git a/app/partials/us-detail.jade b/app/partials/us-detail.jade index eb66d449..2bb65166 100644 --- a/app/partials/us-detail.jade +++ b/app/partials/us-detail.jade @@ -28,6 +28,10 @@ block content tg-bo-title="'#' + us.origin_issue.ref + ' ' + us.origin_issue.subject") span(tg-bo-ref="us.origin_issue.ref") + p.external-reference(ng-if="us.external_reference") This US has been created from + a(target="_blank", tg-bo-href="us.external_reference[1]", title="Go to origin") + span {{ us.external_reference[1] }} + p.block-desc-container(ng-show="us.is_blocked") span.block-description-title Blocked span.block-description(ng-bind="us.blocked_note || 'This user story is blocked'") diff --git a/app/partials/views/modules/admin-menu.jade b/app/partials/views/modules/admin-menu.jade index 14cfaef4..198667f3 100644 --- a/app/partials/views/modules/admin-menu.jade +++ b/app/partials/views/modules/admin-menu.jade @@ -20,3 +20,7 @@ section.admin-menu a(href="" tg-nav="project-admin-roles:project=project.slug") span.title Roles & Permissions span.icon.icon-arrow-right + li#adminmenu-third-parties + a(href="" tg-nav="project-admin-third-parties-github:project=project.slug") + span.title Third parties + span.icon.icon-arrow-right diff --git a/app/partials/views/modules/admin-submenu-third-parties.jade b/app/partials/views/modules/admin-submenu-third-parties.jade new file mode 100644 index 00000000..95746408 --- /dev/null +++ b/app/partials/views/modules/admin-submenu-third-parties.jade @@ -0,0 +1,10 @@ +section.admin-submenu + header + h1 Third parties + + nav + ul + li#adminmenu-third-parties-github + a(href="", tg-nav="project-admin-third-parties-github:project=project.slug") + span.title Github + span.icon.icon-arrow-right diff --git a/app/partials/views/modules/admin/project-points.jade b/app/partials/views/modules/admin/project-points.jade index 5117b528..02ad65f3 100644 --- a/app/partials/views/modules/admin/project-points.jade +++ b/app/partials/views/modules/admin/project-points.jade @@ -29,7 +29,7 @@ section.project-values-table data-required="true") div.project-values-value - input(name="name", type="text", placeholder="Value", ng-model="value.value", + input(name="value", type="text", placeholder="Value", ng-model="value.value", data-type="number") div.project-values-settings diff --git a/app/partials/views/modules/admin/project-status.jade b/app/partials/views/modules/admin/project-status.jade index aa303a39..d1c5a99a 100644 --- a/app/partials/views/modules/admin/project-status.jade +++ b/app/partials/views/modules/admin/project-status.jade @@ -3,6 +3,7 @@ section.colors-table div.row div.color-column Color div.status-name Name + div.status-slug Slug div.is-closed-column Is closed? div.options-column @@ -17,6 +18,9 @@ section.colors-table div.status-name span {{ value.name }} + div.status-slug + span {{ value.slug }} + div.is-closed-column div.icon.icon-check-square(ng-show="value.is_closed") diff --git a/app/partials/views/modules/admin/project-us-status.jade b/app/partials/views/modules/admin/project-us-status.jade index a20c8c68..50c22641 100644 --- a/app/partials/views/modules/admin/project-us-status.jade +++ b/app/partials/views/modules/admin/project-us-status.jade @@ -3,6 +3,7 @@ section.colors-table div.row div.color-column Color div.status-name Name + div.status-slug Slug div.is-closed-column Is closed? div.status-wip-limit WIP Limit div.options-column @@ -19,6 +20,9 @@ section.colors-table div.status-name span {{ value.name }} + div.status-slug + span {{ value.slug }} + div.is-closed-column div.icon.icon-check-square(ng-show="value.is_closed") diff --git a/app/partials/views/modules/invitation-login-form.jade b/app/partials/views/modules/invitation-login-form.jade index 234866bd..8a0b50b5 100644 --- a/app/partials/views/modules/invitation-login-form.jade +++ b/app/partials/views/modules/invitation-login-form.jade @@ -10,3 +10,5 @@ form.login-form fieldset a.button.button-login.button-gray(href="", title="Log in") Enter input(type="submit", style="display:none") + + fieldset(tg-github-login-button) diff --git a/app/partials/views/modules/invitation-register-form.jade b/app/partials/views/modules/invitation-register-form.jade index 5eec1e53..4005dc73 100644 --- a/app/partials/views/modules/invitation-register-form.jade +++ b/app/partials/views/modules/invitation-register-form.jade @@ -23,4 +23,4 @@ form.register-form a.button.button-register.button-gray(href="", title="Sign up") Sign up input(type="submit", style="display:none") - tg-terms-notice + tg-terms-notice diff --git a/app/partials/views/modules/kanban-table.jade b/app/partials/views/modules/kanban-table.jade index 6f29b442..1e80c37b 100644 --- a/app/partials/views/modules/kanban-table.jade +++ b/app/partials/views/modules/kanban-table.jade @@ -1,31 +1,36 @@ -div.kanban-table +div.kanban-table(tg-kanban-squish-column) div.kanban-table-header - div.kanban-table-inner(tg-kanban-row-width-fixer) - h2.task-colum-name(ng-repeat="s in usStatusList track by s.id", - ng-style="{'border-top-color':s.color}") + div.kanban-table-inner + h2.task-colum-name(ng-repeat="s in usStatusList track by s.id", ng-style="{'border-top-color':s.color}", tg-bo-title="s.name", ng-class='{vfold:folds[s.id]}') span(tg-bo-bind="s.name") + div.options - a.icon.icon-minimize(href="", title="Minimize", - ng-if="statusViewModes[s.id] == 'maximized'", - ng-click="ctrl.updateStatusViewMode(s.id, 'minimized')") - a.icon.icon-maximize(href="", title="Maximize", - ng-if="statusViewModes[s.id] == 'minimized'", - ng-click="ctrl.updateStatusViewMode(s.id, 'maximized')") - a.icon.icon-plus(href="", title="Add New task", - ng-click="ctrl.addNewUs('standard', s.id)", - tg-check-permission="add_us") + a.icon.icon-vfold.hfold(href="", ng-click='foldStatus(s)' title="Fold Column", ng-class='{hidden:folds[s.id]}') + a.icon.icon-vunfold.hunfold(href="", ng-click='foldStatus(s)', title="Unfold Column", ng-class='{hidden:!folds[s.id]}') - a.icon.icon-bulk(href="", title="Add New bulk", - ng-click="ctrl.addNewUs('bulk', s.id)", - tg-check-permission="add_us") + + a.icon.icon-vfold(href="", title="Fold Cards", + ng-class="{hidden:statusViewModes[s.id] == 'minimized'}", + ng-click="ctrl.updateStatusViewMode(s.id, 'minimized')") + a.icon.icon-vunfold(href="", title="Unfold Cards", + ng-class="{hidden:statusViewModes[s.id] == 'maximized'}", + ng-click="ctrl.updateStatusViewMode(s.id, 'maximized')") + + a.icon.icon-plus(href="", title="Add New task", + ng-click="ctrl.addNewUs('standard', s.id)", + tg-check-permission="add_us") + + a.icon.icon-bulk(href="", title="Add New bulk", + ng-click="ctrl.addNewUs('bulk', s.id)", + tg-check-permission="add_us") div.kanban-table-body div.kanban-table-inner(tg-kanban-row-width-fixer) - div.kanban-uses-box.task-column(ng-repeat="status in usStatusList track by status.id", + div.kanban-uses-box.task-column(ng-class='{vfold:folds[s.id]}', ng-repeat="s in usStatusList track by s.id", tg-kanban-sortable, tg-kanban-wip-limit, tg-kanban-column-height-fixer) - div.kanban-task(ng-repeat="us in usByStatus[status.id] track by us.id", + div.kanban-task(ng-repeat="us in usByStatus[s.id] track by us.id", tg-kanban-userstory, ng-model="us", - ng-class="ctrl.getCardClass(status.id)") + ng-class="ctrl.getCardClass(s.id)") diff --git a/app/partials/views/modules/login-form.jade b/app/partials/views/modules/login-form.jade index 0fe86cae..f5648eab 100644 --- a/app/partials/views/modules/login-form.jade +++ b/app/partials/views/modules/login-form.jade @@ -13,4 +13,6 @@ div.login-form-container(tg-login) a.button.button-login.button-gray(href="", title="Sign in") Sign in input(type="submit", style="display:none") + fieldset(tg-github-login-button) + tg-public-register-message diff --git a/app/partials/views/modules/register-form.jade b/app/partials/views/modules/register-form.jade index 514a476c..f6349c9e 100644 --- a/app/partials/views/modules/register-form.jade +++ b/app/partials/views/modules/register-form.jade @@ -17,13 +17,15 @@ div.register-form-container(tg-register) fieldset input(type="password", name="password", ng-model="data.password", - data-required="true", data-minlength="4", + data-required="true", data-minlength="4", placeholder="Set a password (case sensitive)") fieldset a.button.button-register.button-gray(href="", title="Sign up") Sign up input(type="submit", class="hidden") + fieldset(tg-github-login-button) + // Only displays terms notice when terms plugin is loaded. tg-terms-notice diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss index f5a060e1..28348313 100755 --- a/app/styles/components/buttons.scss +++ b/app/styles/components/buttons.scss @@ -34,7 +34,7 @@ a.button-green { a.button-gray { background: $button-gray; &:hover { - background: $button-gray-hover; + background: $fresh-taiga; color: $white; } span { @@ -102,3 +102,18 @@ a.button-bulk { background: $fresh-taiga; } } +.button-github { + @extend %button; + background: $grayer; + vertical-align: middle; + .icon { + @extend %large; + color: $white; + margin-right: .5rem; + vertical-align: text-bottom; + } + &:hover { + @include transition (background .3s linear); + background: $black; + } +} diff --git a/app/styles/layout/invitation.scss b/app/styles/layout/invitation.scss index 78a9c63e..8d8f781d 100644 --- a/app/styles/layout/invitation.scss +++ b/app/styles/layout/invitation.scss @@ -44,10 +44,12 @@ } .invitation-form { @include table-flex(); + fieldset { + margin-bottom: .5rem; + } input { background: $white; color: $gray; - margin-bottom: 1rem; position: relative; @include placeholder { color: $gray-light; @@ -80,16 +82,28 @@ background: $fresh-taiga; } } + .button-github { + &:hover { + background: $black; + } + } } .login-form, .register-form { @include table-flex-child(1, 200px, 0, 200px); padding: 1rem; - + text-align: center; .form-header { color: #999; } } + .register-form { + fieldset { + &:last-child { + margin-bottom: 1rem; + } + } + } .register-text { @extend %small; color: $white; diff --git a/app/styles/layout/login.scss b/app/styles/layout/login.scss index 8e62a629..3f53e123 100644 --- a/app/styles/layout/login.scss +++ b/app/styles/layout/login.scss @@ -1,5 +1,3 @@ - - .login-main { //@include table-flex(center, center, flex, row, wrap, center); @include display(flex); @@ -60,11 +58,7 @@ .button { color: $white; display: block; - margin-bottom: .5rem; text-align: center; - &:hover { - background: $fresh-taiga; - } } a { &:hover { diff --git a/app/styles/layout/typography.scss b/app/styles/layout/typography.scss index 6ff57c9e..df7f1103 100755 --- a/app/styles/layout/typography.scss +++ b/app/styles/layout/typography.scss @@ -138,108 +138,115 @@ a:visited { .icon-search:before { content: 'd'; } -.icon-video:before { +.icon-wiki:before { content: 'e'; } -.icon-wiki:before { +.icon-settings:before { content: 'f'; } -.icon-settings:before { +.icon-move:before { content: 'g'; } -.icon-move:before { +.icon-filter:before { content: 'h'; } -.icon-filter:before { +.icon-arrow-up:before { content: 'i'; } -.icon-arrow-up:before { +.icon-arrow-right:before { content: 'j'; } -.icon-arrow-right:before { +.icon-arrow-left:before { content: 'k'; } -.icon-arrow-left:before { +.icon-arrow-bottom:before { content: 'l'; } -.icon-arrow-bottom:before { +.icon-edit:before { content: 'm'; } -.icon-edit:before { +.icon-delete:before { content: 'n'; } -.icon-delete:before { +.icon-iocaine:before { content: 'o'; } -.icon-iocaine:before { +.icon-drag-h:before { content: 'p'; } -.icon-drag-h:before { +.icon-drag-v:before { content: 'q'; } -.icon-drag-v:before { +.icon-document:before { content: 'r'; } -.icon-document:before { +.icon-plus:before { content: 's'; } -.icon-plus:before { +.icon-reload:before { content: 't'; } -.icon-reload:before { +.icon-warning:before { content: 'u'; } -.icon-warning:before { +.icon-notification-error:before { content: 'v'; } -.icon-notification-error:before { +.icon-github:before { content: 'w'; } -.icon-github:before { +.icon-check-square:before { content: 'x'; } -.icon-check-square:before { +.icon-warning-alt:before { content: 'y'; } -.icon-warning-alt:before { +.icon-floppy:before { content: 'z'; } -.icon-floppy:before { +.icon-comment:before { content: 'A'; } -.icon-comment:before { +.icon-documents:before { content: 'B'; } -.icon-documents:before { +.icon-attachments:before { content: 'C'; } -.icon-attachments:before { +.icon-caret-up:before { content: 'D'; } -.icon-caret-up:before { +.icon-caret-down:before { content: 'E'; } -.icon-caret-down:before { +.icon-bulk:before { content: 'F'; } -.icon-bulk:before { +.icon-idea:before { content: 'G'; } -.icon-idea:before { +.icon-spinner:before { content: 'H'; } -.icon-spinner:before { +.icon-minimize:before { content: 'I'; } -.icon-minimize:before { +.icon-maximize:before { content: 'J'; } -.icon-maximize:before { +.icon-stats:before { content: 'K'; } -.icon-stats:before { +.icon-copy:before { content: 'L'; } -.icon-copy:before { +.icon-team:before { content: 'M'; } +.icon-vfold:before { + content: 'N'; +} +.icon-vunfold:before { + content: 'O'; +} + diff --git a/app/styles/modules/admin/third-parties.scss b/app/styles/modules/admin/third-parties.scss new file mode 100644 index 00000000..1baf83ea --- /dev/null +++ b/app/styles/modules/admin/third-parties.scss @@ -0,0 +1,91 @@ +.admin-third-parties { + form { + margin-top: 1rem; + max-width: 700px; + width: 100%; + } + input[type="text"], + textarea { + @extend %title; + } + fieldset { + margin-bottom: 1rem; + } + label { + @extend %title; + display: block; + margin-bottom: .2rem; + } + textarea { + height: 10rem; + } + .button-green { + color: $white; + display: block; + text-align: center; + } + .select-input-text { + .field-with-option { + @include display(flex); + } + .option-wrapper { + @include display(flex); + @include align-items(center); + border: 1px solid $gray-light; + border-left: 0; + border-radius: 0 5px 5px 0; + cursor: pointer; + padding: 0 1rem; + } + .help-copy { + @extend %small; + opacity: 0; + &.visible { + @include transition(opacity .2s linear); + opacity: 1; + } + } + } + .help { + margin-top: 2rem; + h3 { + font-family: opensans-semibold; + margin-bottom: 1rem; + } + ol { + padding: 0 0 0 2rem; + span { + font-family: opensans-semibold; + } + } + .img { + margin-bottom: 1rem; + } + .alt-image { + @extend %small; + font-style: italic; + } + code { + @extend %small; + background: $whitish; + direction: ltr; + display: block; + font-family: 'courier new', 'monospace'; + line-height: 1.4rem; + margin-bottom: 1rem; + padding: .5rem; + unicode-bidi: embed; + white-space: pre; + width: 100%; + } + .code-info { + padding-left: 1rem; + li { + margin-bottom: .5rem; + } + span { + font-family: opensans-semibold; + } + } + } +} diff --git a/app/styles/modules/auth/login-form.scss b/app/styles/modules/auth/login-form.scss index 432ad477..40d4a36c 100644 --- a/app/styles/modules/auth/login-form.scss +++ b/app/styles/modules/auth/login-form.scss @@ -1,5 +1,4 @@ .login-form-container { - //display: none; .login-password { position: relative; } diff --git a/app/styles/modules/common/colors-table.scss b/app/styles/modules/common/colors-table.scss index ec9b00f1..dbf4a42c 100644 --- a/app/styles/modules/common/colors-table.scss +++ b/app/styles/modules/common/colors-table.scss @@ -50,6 +50,10 @@ display: block; } } + .status-slug { + @include table-flex-child(6, 150px, 0); + padding: 0 10px; + } .options-column { max-width: 100px; opacity: 0; diff --git a/app/styles/modules/common/external-reference.scss b/app/styles/modules/common/external-reference.scss new file mode 100644 index 00000000..c69ed1b7 --- /dev/null +++ b/app/styles/modules/common/external-reference.scss @@ -0,0 +1,29 @@ +.blocked { + .external-reference { + color: $white; + a { + @include transition(color .3s linear); + color: $white; + &:hover { + color: $red-light; + } + } + } +} + +.external-reference { + @extend %small; + color: $gray-light; + margin-top: .5rem; + a { + @include transition(color .3s linear); + border-left: 1px solid $gray-light; + padding: 0 .2rem; + &:hover { + color: $green-taiga; + } + &:first-child { + border: 0; + } + } +} diff --git a/app/styles/modules/kanban/kanban-table.scss b/app/styles/modules/kanban/kanban-table.scss index 18792a43..fe12d441 100644 --- a/app/styles/modules/kanban/kanban-table.scss +++ b/app/styles/modules/kanban/kanban-table.scss @@ -1,13 +1,46 @@ //Table basic shared vars $column-width: 300px; -$column-flex: 1; +$column-folded-width: 30px; +$column-flex: 0; $column-shrink: 0; $column-margin: 0 10px 0 0; .kanban-table { overflow: hidden; width: 100%; + .vfold { + &.task-colum-name { + @include table-flex(); + @include align-items(center); + @include justify-content(center); + @include transition(opacity .3s linear); + cursor: pointer; + opacity: .8; + padding: .5rem 0; + .icon-plus, + .icon-bulk, + .icon-vfold, + .icon-vunfold, + span { + display: none; + } + .hunfold { + margin: 0; + } + } + &.task-colum-name, + &.task-column { + @include table-flex-child(1, 0, 0); + max-width: $column-folded-width; + min-height: 2.5rem; + min-width: $column-folded-width; + width: $column-folded-width; + } + .kanban-task { + display: none; + } + } } .kanban-table-header { @@ -20,34 +53,31 @@ $column-margin: 0 10px 0 0; position: absolute; } .task-colum-name { - @include table-flex-child(); + @include table-flex-child($column-flex, $column-width, $column-shrink, $column-width); + @include table-flex(); + @include justify-content(space-between); @extend %large; background: $whitish; border-top: 3px solid $gray-light; margin: $column-margin; - padding: .5rem 0; + padding: .5rem .5rem .5rem 1rem; position: relative; - text-align: center; text-transform: uppercase; &:last-child { margin-right: 0; } .icon { + @extend %medium; @include transition(color .2s linear); color: $gray-light; - position: absolute; - right: .5rem; - top: .5rem; + margin-right: .3rem; &:hover { color: $green-taiga; } - &.icon-plus { - right: 2rem; - } - &.icon-maximize, - &.icon-minimize { - left: .5rem; - right: inherit; + &.hfold, + &.hunfold { + @include transform(rotate(90deg)); + display: inline-block; } } } @@ -60,7 +90,7 @@ $column-margin: 0 10px 0 0; overflow-x: auto; width: 100%; .task-column { - @include table-flex-child(); + @include table-flex-child($column-flex, $column-width, $column-shrink, $column-width); margin: $column-margin; overflow-y: auto; &:last-child { diff --git a/conf/main.example.json b/conf/main.example.json index 5d382492..e40d5dcd 100644 --- a/conf/main.example.json +++ b/conf/main.example.json @@ -6,5 +6,6 @@ "feedbackEnabled": true, "privacyPolicyUrl": null, "termsOfServiceUrl": null, - "maxUploadFileSize": null + "maxUploadFileSize": null, + "gitHubClientId": null } diff --git a/gulpfile.coffee b/gulpfile.coffee index 85d7788b..fb125940 100644 --- a/gulpfile.coffee +++ b/gulpfile.coffee @@ -73,6 +73,7 @@ paths.coffee = [ paths.app + "coffee/modules/base/*.coffee", paths.app + "coffee/modules/resources/*.coffee", paths.app + "coffee/modules/user-settings/*.coffee" + paths.app + "coffee/modules/integrations/*.coffee" paths.app + "plugins/**/*.coffee" ] diff --git a/main-sass.js b/main-sass.js index b3ebd1da..af5a6cda 100644 --- a/main-sass.js +++ b/main-sass.js @@ -71,6 +71,7 @@ exports.files = function () { 'modules/common/related-tasks', 'modules/common/history', 'modules/common/wizard', + 'modules/common/external-reference', //Project modules 'modules/home-projects-list', @@ -120,6 +121,7 @@ exports.files = function () { 'modules/admin/admin-project-profile', 'modules/admin/default-values', 'modules/admin/project-values', + 'modules/admin/third-parties', //Modules user Settings 'modules/user-settings/user-profile',