From 0cfef30885c35d33b21833458a07e6593fe6703f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Juli=C3=A1n?= Date: Fri, 11 Nov 2016 09:03:03 +0100 Subject: [PATCH] Improve 'create project' (new, import, duplicate) --- CHANGELOG.md | 1 + app-loader/app-loader.coffee | 1 + app/coffee/app.coffee | 48 +++ app/coffee/modules/auth.coffee | 1 + app/coffee/modules/base.coffee | 7 +- app/coffee/modules/base/repository.coffee | 13 - app/coffee/modules/common.coffee | 11 +- app/coffee/modules/common/confirm.coffee | 5 +- app/coffee/modules/common/importer.coffee | 153 -------- app/coffee/modules/common/lightboxes.coffee | 17 +- ...reate-project-restriction.directive.coffee | 8 - app/coffee/modules/projects/lightboxes.coffee | 88 ----- app/coffee/modules/resources.coffee | 25 ++ app/images/import-logos/asana.png | Bin 0 -> 5087 bytes app/images/import-logos/github.png | Bin 0 -> 3617 bytes app/images/import-logos/jira.png | Bin 0 -> 6511 bytes app/images/import-logos/trello.png | Bin 0 -> 2612 bytes app/locales/taiga/locale-en.json | 202 +++++++--- .../click-input-file.directive.coffee | 39 ++ .../file-change/file-change.directive.coffee | 1 - .../home-project-list-directive.spec.coffee | 15 - .../home-project-list.directive.coffee | 8 +- .../home/projects/home-project-list.jade | 9 +- .../dropdown-project-list.jade | 10 +- ...sana-import-project-form.controller.coffee | 55 +++ ...asana-import-project-form.directive.coffee | 40 ++ .../asana-import-project-form.jade | 63 +++ .../asana-import-project-form.scss | 57 +++ .../asana-import.controller.coffee | 73 ++++ .../asana-import.controller.spec.coffee | 172 ++++++++ .../asana-import.directive.coffee | 35 ++ .../create/asana-import/asana-import.jade | 31 ++ .../asana-import/asana-import.service.coffee | 57 +++ .../asana-import.service.spec.coffee | 128 ++++++ .../create-project-form.controller.coffee | 63 +++ ...create-project-form.controller.spec.coffee | 139 +++++++ .../create-project-form.directive.coffee | 31 ++ .../create-project-form.jade | 32 ++ ...ject-members-restrictions.directive.coffee | 13 + .../create-project-members-restrictions.jade | 13 + ...eate-project-restrictions.directive.coffee | 13 + .../create-project-restrictions.jade | 11 + .../create/create-project.controller.coffee | 56 +++ .../create-project.controller.spec.coffee | 45 +++ .../projects/create/create-project.jade | 69 ++++ .../projects/create/create-project.scss | 3 + .../duplicate-project.controller.coffee | 85 ++++ .../duplicate-project.controller.spec.coffee | 222 +++++++++++ .../duplicate-project.directive.coffee | 35 ++ .../create/duplicate/duplicate-project.jade | 57 +++ .../create/duplicate/duplicate-project.scss | 5 + ...thub-import-project-form.controller.coffee | 56 +++ ...ithub-import-project-form.directive.coffee | 40 ++ .../github-import-project-form.jade | 71 ++++ .../github-import-project-form.scss | 61 +++ .../github-import.controller.coffee | 74 ++++ .../github-import.controller.spec.coffee | 172 ++++++++ .../github-import.directive.coffee | 35 ++ .../create/github-import/github-import.jade | 31 ++ .../github-import.service.coffee | 55 +++ .../github-import.service.spec.coffee | 129 ++++++ .../import-project-form-common/actions.jade | 13 + .../description.jade | 7 + .../import-project-form-common/links.jade | 20 + .../import-project-form-common/name.jade | 13 + .../project-privacy.jade | 30 ++ .../import-project-members.controller.coffee | 150 +++++++ ...ort-project-members.controller.spec.coffee | 367 ++++++++++++++++++ .../import-project-members.directive.coffee | 43 ++ .../import-project-members.jade | 85 ++++ .../import-project-members.scss | 3 + .../import-project-selector.controller.coffee | 24 ++ .../import-project-selector.directive.coffee | 36 ++ .../import-project-selector.jade | 34 ++ .../import-project-selector.scss | 3 + .../import-taiga.controller.coffee | 43 ++ .../import-taiga.directive.coffee | 29 ++ .../create/import-taiga/import-taiga.jade | 15 + .../projects/create/import/import-header.jade | 2 + .../import-project-error-lb.directive.coffee | 16 + .../import/import-project-error-lb.jade} | 0 .../import/import-project.controller.coffee | 113 ++++++ .../import-project.controller.spec.coffee | 183 +++++++++ .../import/import-project.directive.coffee | 38 ++ .../create/import/import-project.jade | 95 +++++ .../create/import/import-project.scss | 55 +++ .../import/import-project.service.coffee | 121 ++++++ .../import/import-project.service.spec.coffee | 294 ++++++++++++++ .../invite-members.controller.coffee | 26 ++ .../invite-members.directive.coffee | 38 ++ .../create/invite-members/invite-members.jade | 8 + .../create/invite-members/invite-members.scss | 7 + .../single-member.directive.coffee | 31 ++ .../single-member/single-member.jade | 6 + .../single-member/single-member.scss | 40 ++ ...jira-import-project-form.controller.coffee | 58 +++ .../jira-import-project-form.directive.coffee | 40 ++ .../jira-import-project-form.jade | 126 ++++++ .../jira-import-project-form.scss | 30 ++ .../jira-import/jira-import.controller.coffee | 78 ++++ .../jira-import.controller.spec.coffee | 171 ++++++++ .../jira-import/jira-import.directive.coffee | 35 ++ .../create/jira-import/jira-import.jade | 30 ++ .../jira-import/jira-import.service.coffee | 58 +++ .../jira-import.service.spec.coffee | 129 ++++++ ...ect-import-user-lightbox.controller.coffee | 35 ++ ...lect-import-user-lightbox.directive.coffee | 54 +++ .../select-import-user-lightbox.jade | 90 +++++ .../select-import-user-lightbox.scss | 54 +++ ...ello-import-project-form.controller.coffee | 54 +++ ...rello-import-project-form.directive.coffee | 40 ++ .../trello-import-project-form.jade | 25 ++ .../trello-import-project-form.scss | 3 + .../trello-import.controller.coffee | 71 ++++ .../trello-import.controller.spec.coffee | 176 +++++++++ .../trello-import.directive.coffee | 35 ++ .../create/trello-import/trello-import.jade | 27 ++ .../trello-import.service.coffee | 56 +++ .../trello-import.service.spec.coffee | 115 ++++++ ...ning-user-import-lightbox.directive.coffee | 41 ++ .../warning-user-import-lightbox.jade | 8 + .../warning-user-import-lightbox.scss | 9 + .../projects-listing.controller.coffee | 8 +- .../projects-listing.controller.spec.coffee | 13 - .../projects/listing/projects-listing.jade | 9 +- app/modules/projects/projects.service.coffee | 15 +- .../projects/projects.service.spec.coffee | 14 - .../importers-resource.service.coffee | 193 +++++++++ .../projects-resource.service.coffee | 22 ++ app/modules/resources/resources.coffee | 8 +- .../services/current-user.service.coffee | 53 ++- .../services/current-user.service.spec.coffee | 24 +- .../project/wizard-create-project.jade | 84 ---- app/partials/project/wizard-restrictions.jade | 7 - app/styles/dependencies/mixins/btn-group.scss | 45 +++ app/styles/dependencies/mixins/create.scss | 238 ++++++++++++ app/styles/dependencies/mixins/import.scss | 189 +++++++++ .../dependencies/mixins/radio-group.scss | 19 + app/styles/extras/dependencies.scss | 4 + app/styles/modules/common/wizard.scss | 143 ------- app/svg/sprite.svg | 14 +- conf.e2e.js | 1 + conf/conf.example.json | 1 + e2e/helpers/create-project-helper.js | 39 +- e2e/suites/admin/project/create-delete.e2e.js | 67 ---- e2e/suites/create-project/duplicate.e2e.js | 63 +++ e2e/suites/home.e2e.js | 12 - karma.conf.js | 4 - locales.js | 10 +- package.json | 2 +- prism-languages.json | 2 +- 151 files changed, 7151 insertions(+), 777 deletions(-) delete mode 100644 app/coffee/modules/common/importer.coffee delete mode 100644 app/coffee/modules/projects/create-project-restriction.directive.coffee create mode 100644 app/images/import-logos/asana.png create mode 100644 app/images/import-logos/github.png create mode 100644 app/images/import-logos/jira.png create mode 100644 app/images/import-logos/trello.png create mode 100644 app/modules/components/click-input-file.directive.coffee create mode 100644 app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.controller.coffee create mode 100644 app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.directive.coffee create mode 100644 app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.jade create mode 100644 app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.scss create mode 100644 app/modules/projects/create/asana-import/asana-import.controller.coffee create mode 100644 app/modules/projects/create/asana-import/asana-import.controller.spec.coffee create mode 100644 app/modules/projects/create/asana-import/asana-import.directive.coffee create mode 100644 app/modules/projects/create/asana-import/asana-import.jade create mode 100644 app/modules/projects/create/asana-import/asana-import.service.coffee create mode 100644 app/modules/projects/create/asana-import/asana-import.service.spec.coffee create mode 100644 app/modules/projects/create/create-project-form/create-project-form.controller.coffee create mode 100644 app/modules/projects/create/create-project-form/create-project-form.controller.spec.coffee create mode 100644 app/modules/projects/create/create-project-form/create-project-form.directive.coffee create mode 100644 app/modules/projects/create/create-project-form/create-project-form.jade create mode 100644 app/modules/projects/create/create-project-members-restrictions/create-project-members-restrictions.directive.coffee create mode 100644 app/modules/projects/create/create-project-members-restrictions/create-project-members-restrictions.jade create mode 100644 app/modules/projects/create/create-project-restrictions/create-project-restrictions.directive.coffee create mode 100644 app/modules/projects/create/create-project-restrictions/create-project-restrictions.jade create mode 100644 app/modules/projects/create/create-project.controller.coffee create mode 100644 app/modules/projects/create/create-project.controller.spec.coffee create mode 100644 app/modules/projects/create/create-project.jade create mode 100644 app/modules/projects/create/create-project.scss create mode 100644 app/modules/projects/create/duplicate/duplicate-project.controller.coffee create mode 100644 app/modules/projects/create/duplicate/duplicate-project.controller.spec.coffee create mode 100644 app/modules/projects/create/duplicate/duplicate-project.directive.coffee create mode 100644 app/modules/projects/create/duplicate/duplicate-project.jade create mode 100644 app/modules/projects/create/duplicate/duplicate-project.scss create mode 100644 app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.controller.coffee create mode 100644 app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.directive.coffee create mode 100644 app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.jade create mode 100644 app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.scss create mode 100644 app/modules/projects/create/github-import/github-import.controller.coffee create mode 100644 app/modules/projects/create/github-import/github-import.controller.spec.coffee create mode 100644 app/modules/projects/create/github-import/github-import.directive.coffee create mode 100644 app/modules/projects/create/github-import/github-import.jade create mode 100644 app/modules/projects/create/github-import/github-import.service.coffee create mode 100644 app/modules/projects/create/github-import/github-import.service.spec.coffee create mode 100644 app/modules/projects/create/import-project-form-common/actions.jade create mode 100644 app/modules/projects/create/import-project-form-common/description.jade create mode 100644 app/modules/projects/create/import-project-form-common/links.jade create mode 100644 app/modules/projects/create/import-project-form-common/name.jade create mode 100644 app/modules/projects/create/import-project-form-common/project-privacy.jade create mode 100644 app/modules/projects/create/import-project-members/import-project-members.controller.coffee create mode 100644 app/modules/projects/create/import-project-members/import-project-members.controller.spec.coffee create mode 100644 app/modules/projects/create/import-project-members/import-project-members.directive.coffee create mode 100644 app/modules/projects/create/import-project-members/import-project-members.jade create mode 100644 app/modules/projects/create/import-project-members/import-project-members.scss create mode 100644 app/modules/projects/create/import-project-selector/import-project-selector.controller.coffee create mode 100644 app/modules/projects/create/import-project-selector/import-project-selector.directive.coffee create mode 100644 app/modules/projects/create/import-project-selector/import-project-selector.jade create mode 100644 app/modules/projects/create/import-project-selector/import-project-selector.scss create mode 100644 app/modules/projects/create/import-taiga/import-taiga.controller.coffee create mode 100644 app/modules/projects/create/import-taiga/import-taiga.directive.coffee create mode 100644 app/modules/projects/create/import-taiga/import-taiga.jade create mode 100644 app/modules/projects/create/import/import-header.jade create mode 100644 app/modules/projects/create/import/import-project-error-lb.directive.coffee rename app/{partials/common/lightbox/lightbox-import-error.jade => modules/projects/create/import/import-project-error-lb.jade} (100%) create mode 100644 app/modules/projects/create/import/import-project.controller.coffee create mode 100644 app/modules/projects/create/import/import-project.controller.spec.coffee create mode 100644 app/modules/projects/create/import/import-project.directive.coffee create mode 100644 app/modules/projects/create/import/import-project.jade create mode 100644 app/modules/projects/create/import/import-project.scss create mode 100644 app/modules/projects/create/import/import-project.service.coffee create mode 100644 app/modules/projects/create/import/import-project.service.spec.coffee create mode 100644 app/modules/projects/create/invite-members/invite-members.controller.coffee create mode 100644 app/modules/projects/create/invite-members/invite-members.directive.coffee create mode 100644 app/modules/projects/create/invite-members/invite-members.jade create mode 100644 app/modules/projects/create/invite-members/invite-members.scss create mode 100644 app/modules/projects/create/invite-members/single-member/single-member.directive.coffee create mode 100644 app/modules/projects/create/invite-members/single-member/single-member.jade create mode 100644 app/modules/projects/create/invite-members/single-member/single-member.scss create mode 100644 app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.controller.coffee create mode 100644 app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.directive.coffee create mode 100644 app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.jade create mode 100644 app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.scss create mode 100644 app/modules/projects/create/jira-import/jira-import.controller.coffee create mode 100644 app/modules/projects/create/jira-import/jira-import.controller.spec.coffee create mode 100644 app/modules/projects/create/jira-import/jira-import.directive.coffee create mode 100644 app/modules/projects/create/jira-import/jira-import.jade create mode 100644 app/modules/projects/create/jira-import/jira-import.service.coffee create mode 100644 app/modules/projects/create/jira-import/jira-import.service.spec.coffee create mode 100644 app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.controller.coffee create mode 100644 app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.directive.coffee create mode 100644 app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.jade create mode 100644 app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.scss create mode 100644 app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.controller.coffee create mode 100644 app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.directive.coffee create mode 100644 app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.jade create mode 100644 app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.scss create mode 100644 app/modules/projects/create/trello-import/trello-import.controller.coffee create mode 100644 app/modules/projects/create/trello-import/trello-import.controller.spec.coffee create mode 100644 app/modules/projects/create/trello-import/trello-import.directive.coffee create mode 100644 app/modules/projects/create/trello-import/trello-import.jade create mode 100644 app/modules/projects/create/trello-import/trello-import.service.coffee create mode 100644 app/modules/projects/create/trello-import/trello-import.service.spec.coffee create mode 100644 app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.directive.coffee create mode 100644 app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.jade create mode 100644 app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.scss create mode 100644 app/modules/resources/importers-resource.service.coffee delete mode 100644 app/partials/project/wizard-create-project.jade delete mode 100644 app/partials/project/wizard-restrictions.jade create mode 100644 app/styles/dependencies/mixins/btn-group.scss create mode 100644 app/styles/dependencies/mixins/create.scss create mode 100644 app/styles/dependencies/mixins/import.scss create mode 100644 app/styles/dependencies/mixins/radio-group.scss delete mode 100644 app/styles/modules/common/wizard.scss delete mode 100644 e2e/suites/admin/project/create-delete.e2e.js create mode 100644 e2e/suites/create-project/duplicate.e2e.js diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb9a4ac..32421c32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Add thumbnails and preview for PSD files. - Add thumbnails and preview for SVG files. - Improve add-members form: Now users can select between their contacts or type an email. +- New project creation with importing - i18n: - Add japanese (ja) translation. - Add korean (ko) translation. diff --git a/app-loader/app-loader.coffee b/app-loader/app-loader.coffee index 2951fb09..b93e09e7 100644 --- a/app-loader/app-loader.coffee +++ b/app-loader/app-loader.coffee @@ -15,6 +15,7 @@ window.taigaConfig = { "privacyPolicyUrl": null, "termsOfServiceUrl": null, "maxUploadFileSize": null, + "importers": [], "contribPlugins": [] } diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index cca4eae4..1b5f35ca 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -126,6 +126,54 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven controllerAs: "vm" } ) + + # Project + $routeProvider.when("/project/new", + { + title: "PROJECT.CREATE.TITLE", + templateUrl: "projects/create/create-project.html", + loader: true, + controller: "CreateProjectCtrl", + controllerAs: "vm" + } + ) + + # Project - scrum + $routeProvider.when("/project/new/scrum", + { + title: "PROJECT.CREATE.TITLE", + template: "", + loader: true + } + ) + + # Project - kanban + $routeProvider.when("/project/new/kanban", + { + title: "PROJECT.CREATE.TITLE", + template: "", + loader: true + } + ) + + # Project - duplicate + $routeProvider.when("/project/new/duplicate", + { + title: "PROJECT.CREATE.TITLE", + template: "", + loader: true + } + ) + + # Project - import + $routeProvider.when("/project/new/import/:platform?", + { + title: "PROJECT.CREATE.TITLE", + template: "", + loader: true + } + ) + # Project $routeProvider.when("/project/:pslug/", { diff --git a/app/coffee/modules/auth.coffee b/app/coffee/modules/auth.coffee index d05023c2..ee581f05 100644 --- a/app/coffee/modules/auth.coffee +++ b/app/coffee/modules/auth.coffee @@ -105,6 +105,7 @@ class AuthService extends taiga.Service return @rootscope.user userData = @storage.get("userInfo") + if userData user = @model.make_model("users", userData) @rootscope.user = user diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index 5cfa7dd4..c1e4dbed 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -62,7 +62,12 @@ urls = { "cancel-account": "/cancel-account/:token" "register": "/register" "invitation": "/invitation/:token" - "create-project": "/create-project" + "create-project": "/project/new" + "create-project-scrum": "/project/new/scrum" + "create-project-kanban": "/project/new/kanban" + "create-project-duplicate": "/project/new/duplicate" + "create-project-import": "/project/new/import" + "create-project-import-platform": "/project/new/import/:platform" "profile": "/profile" "user-profile": "/profile/:username" diff --git a/app/coffee/modules/base/repository.coffee b/app/coffee/modules/base/repository.coffee index 290bed50..40c4a0be 100644 --- a/app/coffee/modules/base/repository.coffee +++ b/app/coffee/modules/base/repository.coffee @@ -37,19 +37,6 @@ class RepositoryService extends taiga.Service resolveUrlForAttributeModel: (model) -> return @urls.resolve(model.getName(), model.parent) - create: (name, data, dataTypes={}, extraParams={}) -> - defered = @q.defer() - url = @urls.resolve(name) - - promise = @http.post(url, JSON.stringify(data), extraParams) - promise.success (_data, _status) => - defered.resolve(@model.make_model(name, _data, null, dataTypes)) - - promise.error (data, status) => - defered.reject(data) - - return defered.promise - remove: (model, params={}) -> defered = @q.defer() url = @.resolveUrlForModel(model) diff --git a/app/coffee/modules/common.coffee b/app/coffee/modules/common.coffee index 64434411..ae4d6c2d 100644 --- a/app/coffee/modules/common.coffee +++ b/app/coffee/modules/common.coffee @@ -216,6 +216,9 @@ module.directive("tgToggleComment", ToggleCommentDirective) ProjectUrl = ($navurls) -> get = (project) -> + if project.toJS + project = project.toJS() + ctx = {project: project.slug} if project.is_backlog_activated and project.my_permissions.indexOf("view_us") > -1 @@ -353,12 +356,18 @@ module.directive("tgCapslock", [Capslock]) LightboxClose = () -> template = """ - + """ + link = (scope, elm, attrs) -> + return { + scope: { + onClose: '&' + }, + link: link, template: template } diff --git a/app/coffee/modules/common/confirm.coffee b/app/coffee/modules/common/confirm.coffee index 04e80a62..6c37e262 100644 --- a/app/coffee/modules/common/confirm.coffee +++ b/app/coffee/modules/common/confirm.coffee @@ -207,13 +207,16 @@ class ConfirmService extends taiga.Service return defered.promise - loader: (title, message) -> + loader: (title, message, spin=false) -> el = angular.element(".lightbox-generic-loading") # Render content el.find(".title").html(title) if title el.find(".message").html(message) if message + if spin + el.find(".spin").removeClass("hidden") + return { start: => @lightboxService.open(el) stop: => @lightboxService.close(el) diff --git a/app/coffee/modules/common/importer.coffee b/app/coffee/modules/common/importer.coffee deleted file mode 100644 index 4ff587c1..00000000 --- a/app/coffee/modules/common/importer.coffee +++ /dev/null @@ -1,153 +0,0 @@ -### -# Copyright (C) 2014-2016 Andrey Antukh -# Copyright (C) 2014-2016 Jesús Espino Garcia -# Copyright (C) 2014-2016 David Barragán Merino -# Copyright (C) 2014-2016 Alejandro Alonso -# Copyright (C) 2014-2016 Juan Francisco Alcántara -# Copyright (C) 2014-2016 Xavi Julian -# -# 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/common/importer.coffee -### - -module = angular.module("taigaCommon") - - -ImportProjectButtonDirective = ($rs, $confirm, $location, $navUrls, $translate, $lightboxFactory, currentUserService, $tgAuth) -> - link = ($scope, $el, $attrs) -> - getRestrictionError = (result) -> - if result.headers - errorKey = '' - - user = currentUserService.getUser() - maxMemberships = 0 - - if result.headers.isPrivate - privateError = !currentUserService.canCreatePrivateProjects().valid - maxMemberships = null - - if user.get('max_memberships_private_projects') != null && result.headers.memberships >= user.get('max_memberships_private_projects') - membersError = true - else - membersError = false - - if privateError && membersError - errorKey = 'private-space-members' - maxMemberships = user.get('max_memberships_private_projects') - else if privateError - errorKey = 'private-space' - else if membersError - errorKey = 'private-members' - maxMemberships = user.get('max_memberships_private_projects') - - else - publicError = !currentUserService.canCreatePublicProjects().valid - - if user.get('max_memberships_public_projects') != null && result.headers.memberships >= user.get('max_memberships_public_projects') - membersError = true - else - membersError = false - - if publicError && membersError - errorKey = 'public-space-members' - maxMemberships = user.get('max_memberships_public_projects') - else if publicError - errorKey = 'public-space' - else if membersError - errorKey = 'public-members' - maxMemberships = user.get('max_memberships_public_projects') - - return { - key: errorKey, - values: { - max_memberships: maxMemberships, - members: result.headers.memberships - } - } - else - return false - - $el.on "click", ".import-project-button", (event) -> - event.preventDefault() - $el.find("input.import-file").val("") - $el.find("input.import-file").trigger("click") - - $el.on "change", "input.import-file", (event) -> - event.preventDefault() - file = event.target.files[0] - return if not file - - loader = $confirm.loader($translate.instant("PROJECT.IMPORT.UPLOADING_FILE")) - - onSuccess = (result) -> - currentUserService.loadProjects().then () -> - loader.stop() - - if result.status == 202 # Async mode - title = $translate.instant("PROJECT.IMPORT.ASYNC_IN_PROGRESS_TITLE") - message = $translate.instant("PROJECT.IMPORT.ASYNC_IN_PROGRESS_MESSAGE") - $confirm.success(title, message) - - else # result.status == 201 # Sync mode - ctx = {project: result.data.slug} - $location.path($navUrls.resolve("project-admin-project-profile-details", ctx)) - msg = $translate.instant("PROJECT.IMPORT.SYNC_SUCCESS") - $confirm.notify("success", msg) - - onError = (result) -> - $tgAuth.refresh().then () -> - restrictionError = getRestrictionError(result) - - loader.stop() - - if restrictionError - $lightboxFactory.create('tg-lb-import-error', { - class: 'lightbox lightbox-import-error' - }, restrictionError) - - else - errorMsg = $translate.instant("PROJECT.IMPORT.ERROR") - - if result.status == 429 # TOO MANY REQUESTS - errorMsg = $translate.instant("PROJECT.IMPORT.ERROR_TOO_MANY_REQUEST") - else if result.data?._error_message - errorMsg = $translate.instant("PROJECT.IMPORT.ERROR_MESSAGE", {error_message: result.data._error_message}) - $confirm.notify("error", errorMsg) - - loader.start() - $rs.projects.import(file, loader.update).then(onSuccess, onError) - - return {link: link} - -module.directive("tgImportProjectButton", -["$tgResources", "$tgConfirm", "$location", "$tgNavUrls", "$translate", "tgLightboxFactory", "tgCurrentUserService", "$tgAuth", - ImportProjectButtonDirective]) - -LbImportErrorDirective = (lightboxService) -> - link = (scope, el, attrs) -> - lightboxService.open(el) - - scope.close = () -> - lightboxService.close(el) - return - - return { - templateUrl: "common/lightbox/lightbox-import-error.html", - link: link - } - -LbImportErrorDirective.$inject = ["lightboxService"] - -module.directive("tgLbImportError", LbImportErrorDirective) diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index dc10423f..0507e99d 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -38,7 +38,7 @@ trim = @.taiga.trim class LightboxService extends taiga.Service constructor: (@animationFrame, @q, @rootScope) -> - open: ($el, onClose) -> + open: ($el, onClose, onEsc) -> @.onClose = onClose if _.isString($el) @@ -68,7 +68,12 @@ class LightboxService extends taiga.Service docEl = angular.element(document) docEl.on "keydown.lightbox", (e) => code = if e.keyCode then e.keyCode else e.which - @.close($el) if code == 27 + if code == 27 + if onEsc + @rootScope.$applyAsync(onEsc) + else + @.close($el) + return defered.promise @@ -171,9 +176,11 @@ module.service("lightboxKeyboardNavigationService", LightboxKeyboardNavigationSe LightboxDirective = (lightboxService) -> link = ($scope, $el, $attrs) -> - $el.on "click", ".close", (event) -> - event.preventDefault() - lightboxService.close($el) + + if !$attrs.$attr.visible + $el.on "click", ".close", (event) -> + event.preventDefault() + lightboxService.close($el) return {restrict: "C", link: link} diff --git a/app/coffee/modules/projects/create-project-restriction.directive.coffee b/app/coffee/modules/projects/create-project-restriction.directive.coffee deleted file mode 100644 index 66564563..00000000 --- a/app/coffee/modules/projects/create-project-restriction.directive.coffee +++ /dev/null @@ -1,8 +0,0 @@ -module = angular.module("taigaProject") - -createProjectRestrictionDirective = () -> - return { - templateUrl: "project/wizard-restrictions.html" - } - -module.directive('tgCreateProjectRestriction', [createProjectRestrictionDirective]) diff --git a/app/coffee/modules/projects/lightboxes.coffee b/app/coffee/modules/projects/lightboxes.coffee index eca7121f..4d904933 100644 --- a/app/coffee/modules/projects/lightboxes.coffee +++ b/app/coffee/modules/projects/lightboxes.coffee @@ -29,94 +29,6 @@ debounce = @.taiga.debounce module = angular.module("taigaProject") -CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $projectUrl, $loading, lightboxService, $cacheFactory, $translate, currentUserService, $auth) -> - link = ($scope, $el, attrs) -> - $scope.data = {} - $scope.templates = [] - currentLoading = null - - form = $el.find("form").checksley({"onlyOneErrorElement": true}) - - onSuccessSubmit = (response) -> - # remove all $http cache - # This is necessary when a project is created with the same name - # than another deleted in the same session - $cacheFactory.get('$http').removeAll() - - currentLoading.finish() - $rootscope.$broadcast("projects:reload") - - $confirm.notify("success", $translate.instant("COMMON.SAVE")) - - $location.url($projectUrl.get(response)) - lightboxService.close($el) - currentUserService.loadProjects() - - onErrorSubmit = (response) -> - currentLoading.finish() - form.setErrors(response) - selectors = [] - for error_field in _.keys(response) - selectors.push("[name=#{error_field}]") - - submit = (event) => - event.preventDefault() - - if not form.validate() - return - - currentLoading = $loading() - .target(submitButton) - .start() - - promise = $repo.create("projects", $scope.data) - promise.then(onSuccessSubmit, onErrorSubmit) - - openLightbox = -> - $scope.data = { - is_private: false - } - - if !$scope.templates.length - $rs.projects.templates().then (result) => - $scope.templates = result - $scope.data.creation_template = _.head(_.filter($scope.templates, (x) -> x.slug == "scrum")).id - else - $scope.data.creation_template = _.head(_.filter($scope.templates, (x) -> x.slug == "scrum")).id - - $scope.canCreatePrivateProjects = currentUserService.canCreatePrivateProjects() - $scope.canCreatePublicProjects = currentUserService.canCreatePublicProjects() - - lightboxService.open($el) - - submitButton = $el.find(".submit-button") - - $el.on "submit", "form", submit - - $el.on "click", ".close", (event) -> - event.preventDefault() - lightboxService.close($el) - - $scope.$on "$destroy", -> - $el.off() - - $auth.refresh().then () -> - openLightbox() - - directive = { - link: link, - templateUrl: "project/wizard-create-project.html" - scope: {} - } - - return directive - - -module.directive("tgLbCreateProject", ["$rootScope", "$tgRepo", "$tgConfirm", - "$location", "$tgNavUrls", "$tgResources", "$projectUrl", "$tgLoading", - "lightboxService", "$cacheFactory", "$translate", "tgCurrentUserService", "$tgAuth", CreateProject]) - - ############################################################################# ## Delete Project Lightbox Directive ############################################################################# diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index adc6daed..c7742a1b 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -203,6 +203,31 @@ urls = { # Stats "stats-discover": "/stats/discover" + + # Importers + "importers-trello-auth-url": "/importers/trello/auth_url" + "importers-trello-authorize": "/importers/trello/authorize" + "importers-trello-list-projects": "/importers/trello/list_projects" + "importers-trello-list-users": "/importers/trello/list_users" + "importers-trello-import-project": "/importers/trello/import_project" + + "importers-jira-auth-url": "/importers/jira/auth_url" + "importers-jira-authorize": "/importers/jira/authorize" + "importers-jira-list-projects": "/importers/jira/list_projects" + "importers-jira-list-users": "/importers/jira/list_users" + "importers-jira-import-project": "/importers/jira/import_project" + + "importers-github-auth-url": "/importers/github/auth_url" + "importers-github-authorize": "/importers/github/authorize" + "importers-github-list-projects": "/importers/github/list_projects" + "importers-github-list-users": "/importers/github/list_users" + "importers-github-import-project": "/importers/github/import_project" + + "importers-asana-auth-url": "/importers/asana/auth_url" + "importers-asana-authorize": "/importers/asana/authorize" + "importers-asana-list-projects": "/importers/asana/list_projects" + "importers-asana-list-users": "/importers/asana/list_users" + "importers-asana-import-project": "/importers/asana/import_project" } # Initialize api urls service diff --git a/app/images/import-logos/asana.png b/app/images/import-logos/asana.png new file mode 100644 index 0000000000000000000000000000000000000000..1de82ae57b5f67e63c3a72a1eb4a66d3ae795125 GIT binary patch literal 5087 zcmY+IRaDcD!-v1yh>?zv76wSyMvoj_g2X_iJ4B>QflXQv7>&|kP$H?)h@gOklN6~D zA|Q;E6yEc@crX6vb3T{P^>fbi#MDHOj+%oS0024zeQk390NnmNlmY+%|K%JE003m3 z8pav`(2!1Z;Y^# z;bNqx4P5=V($3de002-18yM?QE>W`5Kox%6?%f9f#uWo?4NUm#wtd8h>-{-bMVmX1 zcx-1hF_`k30XKOhf8UaoBp@GDP`pQ0c$wM-%$agklz*T|6%2q%mO7i6$)r)F+7<@r zdX$d}YYTjUrzLqM(MX&9*x9ZN+W)e>V{1PjMT$yK{Fga*5Oox?SnhbI=s>wUsJgwH zqg2Az+sJnId+xjDmlD3GO{}7AkfQ5X-;IneMw7dBVHb4T?d{H1+(iC7#RsPby zh+H=KE32Wxf~iQ`SW$*L75bv!@>%%D-hVK%m7YL0?X&4e4{G2?5CVtrL#{ey+imI1 z#SR*wKth{r7B`)~|G}FVCFdnt3}km=-7%qgO&I(kicsS}FE){;e9_y!%BOKHhnq&< z{f~&Pi`JSv0xR^xQd3Qt4{6O9%fx;CpR(cF_}3s$2`QT|;u{$_%;%>+dAhJtI9@8a zx%~sM{<2EI1;j4w{%IE~o%v0TMD=g_8FSRmDid+*3rLR4AAh&9KlAw56aw4r7z$Xq z%y6_dZyuuBFr%&F3fnw}kp%&jHq+2j;~p48-`;S@sOX}?!MpMhzgL8H$sFWsQVKkr z5iLT&FNuD?2GehZ%Bmdi?svXdw`!*2@IGuc44FeYW`=CBYSc$iKi%<2vc{o--h08p zf9$#reubsH(;=eb7Hz5SIu=ow6?pzi+!^J)JDtQ4m#nU(eh^9cC0xv#!!-JX)$(eC zgGt&jj}5%~w62kzaV;nFxM+XQJDtTlU6Z|ctee)WOLT37>YCNeD(#a^ImfldK+iIh zO_m=Yuv_!(G2SE&Cgw(V@QMO{BwIXVmr?9lN&r1*zzKRFd2#Xe9LRUfHl`*g1C>An zDj*c zVFqqOT<0#@O&|hJ!TXudZ+VQ2@IkUa4bR{L{sg^hfQit<)}qaM)+%LTNYlzh?&Y?` ztc}xGaaew0RNUq|QeQVec}EP%1`wOdhs|$lSxr54mbs~vkYHhV?2&mN-TPGs7m3D% zb8{2nGFT|&Mf9C)%VbDjKpu`?%F+OylxprtA;1Qnc~XETRz=)QsyyMU)imv8gfL0R zLE)A&vM$azQ_=#>YE}qXxcc#9Je{37V1u#WL4aLSS37bNbzEi+{wPs9M@Jqm>S;=4MTD!#dQo~+{x(TU-!Tn1_ z?PV^3+54u>e}zNU$bg&;>XVON!rOd_0@=uALx@Y#j6o+woVF7ltT@bmvn41AzvWeeS;iM-ccYKTFqq@ZbJ0I~~ez1|KDT85%ovB(&P2#2gT3K=2SH2DV+vTZ$ zbk3TJuD?W?KSZkrZwz}fDZ6EWl3xG~#i->AGuNb~b_$a(YA<;fPFx7`l^fq<`??S3 zqIx*FAscEDRK<+EVzc*Pohr>)Z83diqB0qGN{93wTD(#%SUkn_)_Az1Y%_v?lxNh_^v0L)J7eLEUPZY3Td zWoeDar=tBZWjo~L3=H&%9=>B<>F^~yZ|uyM&^G&>9|XcJy&8Yy{O`(6lLvD3FHPQT!qeXe0ekcThN z#7-9!u)eY2-`K+eILx%>m`XI;h1??KRF5I1NTZffYe~N-ywZqo zmqlwX!vB2kWm<>xvsst(a!YSfEF*5FoTPRSF@U=4U zADu~57j1j{|2m3v`2NPgzI&JAQ1z-azSdTTacD6iTb=EJY0SQue~jhsVc_N+hd)nI zds=cAJ~~?qrr~St-KwojAxUNUhM26H`->Pilr7|oxp+%STuj+&c z>qd;`&?tz&FX@_+Si3{$o96(my^A%5 zNIeXut(LwqyOlSR@KotsDMJ>{2srUjXH5?H`t3@pwX9i+OGrVgiiKLS4VJ7_2VKqdHpp%AOIXa2cfpXpK~XH#{sPVTQN?=jdCd(61f+4fl_xgV zKws)LDcV_8*XoLRxf#b3{>E;B8TPM-34g49L6!eSw!26!C=X`nD4I?5Cu{{paYVmg zx_9?4=KZXm!}BNxMMZVGn0fVk#_6{>tqmKvC3xzd6CKq%MfQzBRKHXsz~HC{b5<6q z+tEK7o_r6{{1G2(a8Z&)PwM#5q%$@bh4n`WG(?L@PLVL!jM|j94c*ik(;v9Mq&V_) zo8d6Gm9S!Dmm_qk#UZk(+{crcxs%jF^8zQndTe4>5Q&$Q?(_WlVPf5A0-ou%!&*yG zRV=xK<|4~|2%xcBG4zC~Yt|k8!n~CMf9Wmdg`thDh|e@_R=Xy-caMPsjGezG&jQ0t z-J%$`+PipI-YF=+DlSVQB++SBWs+mb6UL1nnrYJ0*dm1*pV#8??%_V}`Yzlm&$ojB ziIu~`&bAI2tHsu7D<_m#CtcY>opsbbx=QF16Xq%Gogzd8$pixKRzm){yUv+9J@G1M zz-kVW-QcOESQkXRGfEPLS6Ypd*97`bmS_F2YRpf|Boi*PNNLuO@L6)45kXh42+_w) z>4p6O^qb(11Rjq5dgc}3*-<(Il1&>|SFaB9eAIhjvR{gn?@Es#c@y~ zOTd&G(SJR|Z;Gtb`?AAM563?N8^5{4p1QiQTG$v2I>l=f2f>;%pc(S!ah{Usa3@bt zhUM2T@;CCap`RY1LPG2NwY@R-?QwH{esVk^t&I=nGbAA3DN1>6gS&v6V5n<<1UG;A zWZd3!kqR~WV&?&z5*-wbf(Kkn5eVqK{+aPfiA-S8U|O7nO??g&9*H)>?fMu2#CaR7 zP6KXYA?!Lkns&v=Dqk8+kkqc3S4v6rYAL91I!I!f-^Ww_N$I zLX7gS7u?JixZjD~+rYs)d!t`gGehq2(ke0=Et!aX&MwQ4iu#tK=nIb5mWY!#>(E(t zhDH9aydSyOdl`u`QqyJq{*ygrcl}1+cBy^a3A(2oBX0XH!uU~qLc;5(u93#g>jlef z#)Pn{xD{iWfT%kiCo;U>H zaRB+b49dzc_QWqoG8E@&Q&sJ2}!>VNP| zE4e13>MO%faA2-p_B#$uIEoM-tY@t+q@XX%nx3{p8>(NL0n@F3)8K12mUzx!wU9cW zV;RSrx>TwZL#~x+R6B$Ua(iaENKSN!9k6|h6uaf$n_+okqq40S&>aHNm93Nk1s2g5 zjr)zSh}Lu1av0k4QzYJ$ z)fOT`*+eHDwY;o5*5J`X?nQc69u9excPg47req zb+Zpqiwo0_Xi(L>w`tv`p+A1aKX@)2@hFefdVcl%= zg<_imOCCsIm{`i^8uU(!+Z}vc{EBS$9ou|H&t-#C{w-D3@Y?x$dlgW{tvzd5tHxvi=MU)3>&H@VTNW$7wn&>? z3pvSQGT{cP=Yw+zIP;Cd%q*GZGet));bTy?QPz-*P-jckjpJWQ^(G@ueub8k>G`tNku_z* zmPS@>5*d*QyW~q!?y)0xlS|5j>{D|c)y{fskuS95o$4I7|4&i+FD~tS!m(sK91HX< zD0_DR00e?{tb^U0gWZ)}1Kpj2-2rI?Qb7WNlt3zCka9|Lib~Q5F$7Wxfw(#91N$Gq w*U!z{BjP_mUIL*YAuWSJB9#z|N^){y2x%n*LIM}t3jhFMpktz4r|A^)e-2+ixBvhE literal 0 HcmV?d00001 diff --git a/app/images/import-logos/github.png b/app/images/import-logos/github.png new file mode 100644 index 0000000000000000000000000000000000000000..77aaf337650bae95bcbe9dd194b7fd9403588b9a GIT binary patch literal 3617 zcmV++4&L#JP)~Kl653!8aD7zV}oNf_DspM=TH2s1ZD(7-EbXk1FGVAs!W@Ni-U( zykb1ccvMp6u%hA>BgP{d!BP|xO+ZQOtz}vuF;5B9RtMwu^nf@jt3c^vq$-o3)1h5s*)<;cg2bKX# zfEmD4W%Q!{Bqh_ABmhYw4hDV(93%v^_OrI=6oO^}*D0fC7Hju_CnTO}(y=yRJn(Da zYt%RXD-&i0&>r`p*2HA`NG%Nmz9Ajlmw|MYX-s_9tZ4B!|BHXaB5 zq>O&Fma43l5;7dP9ykivl*Yg(z>~mFmC;M01aiOPzC@JQfxU^|)~qo1m_Ag@}R5e4C?ztnV9T^edGZ`j&E~LVlhs zp40{u$G5V+uX6d^Y?-{NNh#Z?M?rY9cvC|*jC-&60ys+2r8~RKdGQ@xn1KtF--Z0i>;X`1wGWz2*YZNc7-2fh|kc_qR8R)3AA6v`VC66J> zH|hmO{CeP|3L5hc@K@|nsR2Hh@3RAhgyp~jU=A=7cm)`WJMlb7OOvYeO5i41a&jltb zqnGq#Pm0Oj1TqkI6{t`$QkK zI5(AGEHEVs!c0%by|owHu@`Vw6`foNtSm-$0Xdz@=;wg#q9D8!_$4;pstf3l&lf`Q zYruQJV&H#LeolTLyP7_*8SoWgTkIa&o+6DyfI;$U!NzZ0ql~^PZclRN_AqEG#I77# z;kcY7c6D|!DQWFNWeoz(myT76#meZ8-u{P#(||G>W%RXC5IzoECI)v__qPPDRYuP(;!*b2=8h-`7gsSZn*rxXL3oBtww`TBLQn1o{Huye zJAreR(Ra8C2&kqU*KLS`@Eox<)r2nXuZ(`Fux&l9LvF6J?d?Fm7qRbkN1)ihs~ON@ zHms+Arz|HYeXIsIms}VM`xAhD92m;(Y6h-a5^7vWw}Yna69wVnh4mMckgFV2)`EnC zR&!WWZ%Fi|4F~ZE#m3r8Rul!{0kUwb78}KWdOd~uJCv@Fo+Izea)V{e*Kyz#RCK1e zgf1{#t`#&Cq))CQ@(0P7uj9Z=R5Vg-hpSkvT`OG8);mJ!n+s&bmW*7dvJ!gn0PJ-D zSJ)TWrU41r2DkCaBV_D}f)GzUt5#VsZS1h3zX9_bkdXP(H&@sgI3NnbR%P@$l{FmL z+hHYhfJ>FppEjD8rHuX*xJ3Hs3VX+PDJEoVN8S~(2X(nJdU3s7sC40FNuQPgm&=ZX z3yc*G@WgA_!;xPN@Q^b42^t*I$A=sh*8=Pj1)=AO>ov|{ThC4I`!paC$pzwh4vQWK z42lWa&rzXwDx+64Dj7vo*9zcHN5$?J6EXxFo9Y6$HQMSt#j5=_M@5bThIkGaIrD*e zjpS9O@w$L{z`L#%(;hI!QDM&lYnwqj3Z!JM^xXx;R^#kP$Y)KCi2=aG(z``n{QKy-x|)ud{x#UUXpJ#0|33+xHBHsR)C zEB2;<3oQ4r2_RiyKVVQ3ZZ5XrE(&FdhrMs%0z-gZnowo!g1gir?|Ik+!>({jLX?|9 zU4cz;>k4mWTJ!~p&R~RfegBn^&62a+b;5HM|`yO_e zcDBPpe;!|IY(!ELx2Qa9z?>`020CLxUUFFQcEH6=psg;#U2q7(L8pg3TzbJ_!5(mS z6ojK2O-d5du^TWMcYA>d2koBt(hD4}*ug$8aak0En>1i3=_c4)!Y)%9t^^h&gRPf3 zwj%&1V6XoU_^WLRBi;sF0DRAJ6|Z8ia@)P9Tt1i2`u@(i&pmP|kk9)5E6V7uCHj|X5+Mm6 z`!3+L5(OQTyp$6K;ZWfF!12I=lo#;uIxr2JLwz|m+s?|u7u4dJTaA}gkYb|^d-S>m zHWAGs*i=!w_Mu=-RYsqhoFthdjUOSlq_^wQ?*SLYq(nh@1ok}SYHa$U8>uV`V<9$* zei8PZW;yVoGJ1w1={OpDb!(C7)u=w}=eawPUamU_VL%INdbW&^s4(!md#V>8Y@4GdF8e=a0V zkwvBp7xC(tjB1R3LS;RLef6imkpr$Q_KQppjc<{ee?!U&9stf(Mt_zlXB%-BD?M*s z3hV^rT^V-in=dN2Ki#{FvFVw*N@fF+#ZZ^1qt{bba3Xd?F(VzHfPGKo4&bv28~qz> z3O?6ZP4)S}s=*xK0cCVo>Li##%u?VM=|CD>i+wH?-~%DzNh&)tSdD$AuFIv{o0x3R z(La}qRs**r-P}}!B=q$?Vk6R^O{4A>B+ z*~H$`#3E~f$+Cb+gF~Vq++P{pEvwHRu{mxhVqf-rJ7qaD0ae3+S%tgsr*i6?tc+gU z+k&?@l@*hs%ADa>CdzmRdu@2NGWsoS`i@h8M`gfQ$>#%+#0!B@%IFJ~(W`5?t&*#; z1Qx?-%IM{#b|1V_I#k^5u2n`q)>{JQRD{T&bjwV=f^<302LabaK{zz=xA8{WENuGO z%_KDBOXO|bwkQZ&N~n=Ofh=G*s$mrxo7jJTxsFLC5)vEx-GN6)e>Y;TtZQCj zeG>f+?CsrFYy_Y$TN+9kJ*UQm+=AQmN-^FCj#oy{uH-zY?2DQScX*B*$=(TkJ;k4n zW&O$9hcb)uV(bTF55+?48O3eb#{dQzUIk9?>CKdy5K=(O8&MFRh<(#=d>KV-P4Dkn ztrZt>Af@Vf2Kb>edOKEVcAKd6jeQ_I~1=iW>a@m-^go{9~%xd|HxQEKyWFttx+ zQ;PI1Y3q%^Hp=L`lPcJyno6SPnHS=xMwQVWxqNPV*7xs~#o$nE=ulgcKb!6<7OST= zWqtoZY*1ez@?x;>l11LbxqPmpuW}J*`by>tMLY_^Er7$Z4>}&q`W@ui*ps!_Y8grz z1>v5+?^z!evj`jX{493I+0;@s^*bTO21R}zjNK252)N z6=YL!gHgr>Wn5>z85EEriwG2&pG7Dbxvzg;r|FJY*>nhZZzwJG35bvY~E0ZjEjyB zUpfA8Wt)q=8mGLm!-LU1tSA2e=<4DgvdVuSRM>dG%qZsoA%ICk50eMJD3Uz^MI@wd zAsT`BBuer!4C1>p2Rl~8K7Z*OH^yCSyV%R;A+Hvc;lW<%0DxJWN(UYCMgAO7`EXg~ zZ5e>tT`y1q@TPQibFam%ORHkZ#V~$j5%2WOA{{et`TU>bjx{QmxeHO_( zRE1$3QcwJU@htTL0j1};Dl%z>qnDVZ z=hhwZzjZ^7kx%NIp~k-3Rvf@JZ4DPJ2n)A+Ft+DOdU}0(8P$nYcep$}+!a)2`9kb+ za`qy)P9%rA+4Z*&0H_Mf`X|@Bo~;P0_k>k$s_$VAfOEj4rGA4*-;gNzZ+eRG!^A%J zk_#1#5B8Shk6ljc+WA}D>4$vb9GyUKt_Z7t4Jj;JVNrcipNNzM3=;ih(TJ=E2qAtl ze~Sx$tD&MNzAPS55n)sPGRsvM$JxLk0JH+(0wtS`8d{L5p{u9&w|^xHw7Rv=o;P=T zaPQS8X9ee+Jh{H)`tp!EB%<V;_c*JRrz4CDqydc-2muHxbOQ0|B)TSDFMjY;|BSVS5Y}3N*jgMw z?^6cY>jj?v2g^f$EDN%MDq}Vdq%(f*vN#7Mfxu=EUhQnv%^K6gS_}Z*paP3ttRKji zwz{v{=MFEeil|rM+<>1ZT9JmWI$=#liuk_F_$Tgwq8c><&&|@~SDn)8{`UDogeU%@ z**kvoaX1U5p+8XBsmtT1}qA~|R4{&(RUmSo*OCeFR zN2DarsG(n1hS@=bL@L{xHC1DJSbc<$CI?{Ep(+foWgXR~qBnPXQ;$^0snxP-GHb=0 zfT9krj|0�a z`eC~?Y4PN~_B{ab%-5xu+TVW0=gwKjJG;I3OLq%6=j4gC&W-_@|0O7^ll?L$G{FJR z0Z;&x5>SEwV<@Kt0YVVeP*RzyrR8B2`_pu^qAJ7>cCu<10WT@4RE`YDNueFh8g#N~ zPb>E)pNt>9VH7w6T;<$qOA$L4y2ElCjnK)jsOe#a+Zo5DA%z)L4jm;(Q#lL*fhnT$ z(+rw;gmFMC5@e=`Z*$<~IfEU$VvO+VcRu{~3Uj^BHmJ_on_ZaDCu*Th`>KRiN0^au zKHD2oM@Ljn8;g8M4Fi&>Zsg=7Bl2@KzLvfv1RymXOlnbZqj4q$LAFW!;>rx&qjz?- zd>{M#`OPj&?32-;_fEh@_vLJN2A(UamIv2Z9^!@SuTTOmP~tU+bakI>!-P@YEJvfB zya)GOWj*h~7?;#W{lmS!OEw?(-w{yQa7E?0A(d+zbttv~iBX$IAhJNoHoZW;yv(9q z`slT`{ijBQ<6($|Wd0U6{&roo^2Iy7mYpTR%k>hur6R1}7FKwN7)KM*I)I3wVbTa5 zy+oD*Sk~UG-8-&#`iZEfYZcAOca1Er zl1KT%YTt;;4FGiDka!CPf)Xek2uXxQOj;p7+a!JRM6T_}>Agdi-Ag9NcVqSDCvQ292YK=f()KJKb|0GdzlyU4$)6%0s zh5w)tNYSMx>DVK^ZFQpFO&wTmHj4jnuMf9(Ib9ikyw9iIbj&XrwSvAZAPa4DqCO{0 z>ayDvO35^eJ-tC@td6KUdy3dSC^IR-I3jB8;3$wHQRpQi{p5>mu;@kAq8EYzneFRp z(}hwt)Zpn?N2)QPo$=?T{JcR0w|#mT0Pt#oTX=u3 zkH)@RxZRVKI`+DQNpJTzMgF9fH8o2AwIE8i6P_3sz@+6x3dg9^{G=!CpfE!{b}ubnpw zeSNgA+_w5q_10>cuNAnv?{kL- zgjL>OX1o{U$dMUGnpP;&iKN4VVOdqp0?OiDo*UO6@qeN+UWa@IN;m<$8cK>xI$=d_ zmSOqtuQYG`IRSC%8V3NpyT^NFp)2$UUr7B`P+{2|Xu~+9I`2*h2w;8hHQ+C#pG+|{mQ_jI@NnVVdg+_&Xq$Go%0i;uta z(Ro80x)lei1{GB)BLfk2WL1P^oQ;z>2h2JFI+0jk8kQ9|C^V7A&lq6$NCHtCg1jfF zO75UCI{$cJ`M2&+Fs$;82fdM-Ip?GyfE@OP6EYH3960y#%%UAp1LaV8IJZNZX5+m4 zGXHjG@N;LCd~d}$0ExY*!njjmNGQ8BZcx@7t^y$>oT?F=4bf;Z|AG>rI>O)!sngL9tFIj1*WW;$mZ$Mmp*b1pfnl{|Mq z%?&85PTL#em8NLup^OwkO$3y<12sc(qD!fwfYk)JIv&o9sLbLGDI+-N+s~F{@$5hc z7HoA{_qoH%|N7Z63*!jhx$I~#nqn(}qwcVXs2tT1#z!qXrV|MCqCm>#Y<4~VM4yZ; z_0ipdG+0mk)!rR7Ek2V@D82Rdr*~^-R zzGCv|wYIfJi6UO3JgoAr0>O|mJ*{}Hpse2C2_(9D81KlV1VlK;7@V-i`9M(NnF++y zj!;T|N;nvr$iv%;0{{TCj(!raBdahDPf(rrO_AStrp%6*TyD|Mk>cbOi4rCf@_%D{ zrk^%T-r5ya?*G{BvA6=ty@~XRMj+o0Zf88)tf0h#beTqJzD6K%kK&-h9RDoz%>jUC zHEujwSE{1jN_PuKyxtG&DOXS6Z|YgGd%8Vl{9;+FlZvv+7`V>e!xEOkV3e z@m-Et3vWr1DCe?zS^Ik2&Vw0suB0_mA}i)!$JPx2*z7fNYcWBmj(0!oHoPfC2!s2Rpte zz{iOsNH7jmN7%S|yF51n(Y626LOa*ni|i@l6oHV)*#OZB1Z`8LJMZslbxv7dg4)jG z6zl6=NGYr>uA4xq*&JmzbngXTrrG(iPn@bPFE3#-^U?wLd@DfqD7v zl)dio0)_L-5@{<3&_*XL{O~iD5?X7bxawp7fDqjmKY&`H>kEB zv$E*DJ*EDXNqy~@_EkxpunRB~bA~u>GHZnuq=rVK+YE#~L)#l48`C4*_tb{E8_Whw zDgfu49;=Ex6;jy9L>38abe6}FD!%m2$V+)LcTOh_6abjRG*j+JuUztp249GksR`oClAuKypy2GLu-mn^k(o_xoxNC;~_TOD;{$Xyu3r}Bv z@|nk07ZLn(Af|p*GQBLIjF(wr%eDdmj2dC#q&^w@kYq_JDAW2!J7GwNv?Xng(#izi z8=M24fc(I}zVl4uoYRT5%;cEaIiB zg%^fq@ri4TA3W@fJjrxyP4lPd#ARI7i=nST(@z6tZoUhD>2AUDA1ia0 zAFNvIS6EVPRh>v87M=L`vja1KoW7x?&VdF$3;L=pFKu;WTyGm@ZYurrK6iMDnuH8O z9$gFfw>65n&kS^Y8*>c*^?e0i9-f7#HU9IABi^R}XA&y!F=3Y1LEKJ8Pl|tq7EuJhH0jaA`nkm#oTl0%ekb!l2AA zS2))uqs*Gbx;<@l!k}4$9p2gbE=*J5!!NU>0h8>_ zMMe!ho@J6oOz)pr*wllTHf3ec4|ZV59xtp0$$P0u)7PXCerjo>i$_B!rI|+Qp6UHF z3-OD8qg8_f0HeBDFmbIDPhD>>x*}aW${^9>Xd%KlkgB1$)TFuNnE{!r0N|_T!h4JwxcFufSwNhO%L`;Um0^w$M#Hb{WGPuOjuitX9qeqTlB(k ziBhFG+aggYS*hah9_*FAGPVre9DuZlnKE|eaV#8>jW>6A$K)6JUkJ!d-_!>o2n4}2 zo$&B$H)bzx2{cNZy8#PFWaH(nW#rx-mPMVd+R+Ay`kQKRJ+E;@bt1hl2FQ%BoBue> z7Iy%|jIT>a9Vid~T~@iSkpbZvfsiY#+A)**+W!s!53ef1%fquS&XiLC!L__=ng`NIycD)53PEp#ANjHV|e42m*BD0 z#gCQv<>&b++oR(ZokaKb%TBrR?yD@v&UE6^MNk?j&u%Jx{d;$KjKXAqndP5zeluC%3t5z8f>(${`}wn=(MN+dnLmiWTz9qW%)-vtX(Wx3?RWfLzT4uKKYl=UPeYRwe_aZ2te&@pzxfyt+psZ7Mgnbm08LGkz z?tt>i)b%A}fs+}LkMy=-$sR8VAyxNSma3r}9Y*Pvrw3$~ zUC=Y-#c!f?^yGTy{hnZ5KmZ`Da;>vkeqmf*(SX{P*z`dRhL)8-^jMtk?MF>zH zQgz#%fln9i@LWE%pB;Z&T?_!2J;Z@O{r4DV4|aT*ZV+Ed#3HNN)|W1;d=3!3O5=j0 zuPN(FI#h<$+s<r4J^3dBm+(F%weeEao%S$!7 zDbpzZK20kWC6cZjQ91UOg%$w73pM3lFra+8&u8!k)Vt5O|NJul^^%?5@q0>xLhRI| z*bS`{`(*etjp83Afh0s@F2CI8?cLsPk6mj!0r&+0idioX3M#D2`F5YeIV!?z#y|J^ zx}19I(ULu0^mG`uw$TX-6SR=c+lm9X*9N>*gAy}?JwauT!r~jr0hz%dk<*gIM|HDc zWM@-2BSm^aBa$QWwvlnrkiu_xd8^ye90Rt=L3#9A8{Yr6yn9(d`JZ^LB}xE+kc0Ia z5dAU^*!0pn1c>{dNe+I^kL8iBtsazDwncToHfP{3Dv!6bfHbXeup+|pmKN5(#cj@z z%$Zt|#2u3l${Yrf{A%TaN=>Tt;N9Pmc05tUyO@5rTu_^0)^e+mF+JDIh%SPf#eRwQ}xwmYP-Tp;mtAzLhcy}QSo zCNmz_p;*!LAccK|knpPo4PWxOv#U9%6UfGRUsP7PopY{l%3Ft;6ci4gArQpvBAd0s z8UT2(SHsgxFSF=Wnd4g_4yT9!2&)_mws|@=Wj{43DAf^`$~aQuRq`^!ig=ziX{cMG z#1rR80wughs|BSjs8CtuG|s-kq`tO5JkLfL$`sDy%TXnOH=w4sT2LG*64eMq!)slY z-H<9zk7FVgPnBapwOs>7h^|Qs6<7ofLmec9;S*WZKch&I~M;KaIwaFc>a2`$@4w(|WaB z;j6#xPdv1{ zC&B=jEIPdU&HG`~_0fr!r$Mxe)H3xbEt1yJ_VnTLo(ZE4rw$bvA1zo1HshlOP8}-t zl-AD`9e#XxPt}%%@ifGm>DCFa=xcaLas0q&&*v8Qj}@3I7O;ND-vL{1#dzqJ4_w+AvSrFj?t? z>QdWmfa8^#3W@DAqaszufC?~GDwx(xXtZhOuF&2UMPFPdk}X;IX>EQL32B!Bn(@;d z-X|xP;h9npiO!QGWA;a4UIW@Y_Pvmd^s*h3k+#V)h|U;5#+-WH_9`0Tr0O7a>-LD- z_H|9PN|e9MfQE>HPp|Rc@kEE)6LcAlB9(wN^(@RTD-73ndT3$e_$qa$-i;N^Zgx5RFc}RRBtL;+zJm|mw=e(#%a~>i_No0N^NtFrS#?z{NW>}h| zEMrTRgFHlhK}uFfPk_SbEDsTOIygaV2$`KE6_rE5h!syABcG|VGDz!*mTI-Zf&N3X zWntrX)Kz^*fLi!h4aLS&^)L^SfN~lM%Iee73<>9H3tyGrWMysL!^V$D#xq$HdL)NV zedqi1dLAN8MFYN^MEOiv+M%RKL?NKx?53SFNjy(|{C7MRe}P&i`O%n`70wJzXvOhV z@@TU9G7rf-QzU1!CbeQHIU1CCq19lUyyK1KBZ`B!H_BvS#W2tYheeqw=S)%j5|5|H zSKBKYSO2`4EV~>E--;yPij zhz_7Nil@@-)Wb8!9kFt#;k6rU?0NPre(;q~^Yzac**#oHl#+%2-s2~)e!xF&tiq6v zameA$-N|L1)h-QMadv*0v-8U+trDxQ5tqgylcPK0krQ2#9aPeo?3OUrv~5(oWbr%U72>KjOp9Bz@0C+m2GSA}lVaM|W^JW2 z{JK5$lZ=|A{Cjx=f#jho-ZY7`JwGJrBr+bqJBK{vBT1n?KOG;)nD_^}9jml`kI8Ts4rNRzrbC20fttE}-~Tf(!Y z8$B1uzdpRjyDRH(e)@GlL&ePfwlzrNg4fgFG79o9Cs>}a*Uq)Tlr z?%7`DVnOk}m;cT07gpHlvOjshyw1bFe2ayft9@~byW;e`gqk&jBf`J1Zho=XI%{NBZvmd14EG+C_5mFz{73F*6q%EsxGw6KmJ9HPzs#~ z3o@r+yR<@flm)hpx@SM*7fKrBg6Euzs9mj|c^3%XRT?($V{*@a#*bXkz-3^`JRLT5 zeC_IT-{M~aby@Sw;T7Qgz@MfL=Xqv!rD4sUy0i#tD*nhLT8~FcC=53{a%o56zrx&+ z>1w9_)GPHbk0Nphkl1R==AvtxlTYKyg`4l$ANU2hgW@Ovr%Jxy>*S?b;3BYVU_p8U zcYw#3J5s8Dp__z3^#?f!awYlo$-(8juAt-~b4N;?ywqalTH)kh&H#@C6N7{ We will send you an email when ready", - "UPLOAD_IN_PROGRESS_MESSAGE": "Uploaded {{uploadedSize}} of {{totalSize}}", - "ERROR": "Our Oompa Loompas have some problems importing your dump data. Please try again.", - "ERROR_TOO_MANY_REQUEST": "Sorry, our Oompa Loompas are very busy right now. Please try again in a few minutes.", - "ERROR_MESSAGE": "Our Oompa Loompas have some problems importing your dump data: {{error_message}}", - "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) is too heavy for our Oompa Loompas, try it with a smaller than ({{maxFileSize}})", - "SYNC_SUCCESS": "Your project has been imported successfuly", - "PROJECT_RESTRICTIONS": { - "PROJECT_MEMBERS_DESC": "The project you are trying to import has {{members}} members, unfortunately, your current plan allows for a maximum of {{max_memberships}} members per project. If you would like to increase that limit please contact the administrator.", - "PRIVATE_PROJECTS_SPACE": { - "TITLE": "Unfortunately, your current plan does not allow for additional private projects", - "DESC": "The project you are trying to import is private. Unfortunately, your current plan does not allow for additional private projects." - }, - "PUBLIC_PROJECTS_SPACE": { - "TITLE": "Unfortunately, your current plan does not allow for additional public projects", - "DESC": "The project you are trying to import is public. Unfortunately, your current plan does not allow additional public projects." - }, - "PRIVATE_PROJECTS_MEMBERS": { - "TITLE": "Your current plan allows for a maximum of {{max_memberships}} members per private project" - }, - "PUBLIC_PROJECTS_MEMBERS": { - "TITLE": "Your current plan allows for a maximum of {{max_memberships}} members per public project." - }, - "PRIVATE_PROJECTS_SPACE_MEMBERS": { - "TITLE": "Unfortunately your current plan doesn't allow additional private projects or an increase of more than {{max_memberships}} members per private project", - "DESC": "The project that you are trying to import is private and has {{members}} members." - }, - "PUBLIC_PROJECTS_SPACE_MEMBERS": { - "TITLE": "Unfortunately your current plan doesn't allow additional public projects or an increase of more than {{max_memberships}} members per public project", - "DESC": "The project that you are trying to import is public and has more than {{members}} members." - } - } - }, "LIKE_BUTTON": { "LIKE": "Like", "LIKED": "Liked", @@ -966,6 +925,152 @@ "CONTACT_BUTTON": { "CONTACT_TITLE": "Contact the project team", "CONTACT_BUTTON": "Contact the project" + }, + "CREATE": { + "TITLE": "Create Project", + "FRESH": "Fresh and clean. So exciting!", + "CHOOSE_TEMPLATE": "Which template fits your project better?", + "TEMPLATE_SCRUM": "Scrum", + "TEMPLATE_SCRUM_DESC": "Prioritize and solve your tasks in short time cycles.", + "TEMPLATE_SCRUM_LONGDESC": "Scrum is an iterative and incremental agile software development methodology for managing product development.\nThe product backlog is what will ultimately be delivered, ordered into the sequence in which it should be delivered. Product Backlogs are broken into manageable, executable chunks named sprints. Every certain amount of time the team initiates a new sprint and commits to deliver a certain number of user stories from the backlog, in accordance with their skills, abilities and resources. The project advances as the backlog becomes depleted.", + "TEMPLATE_KANBAN": "Kanban", + "TEMPLATE_KANBAN_DESC": "Keep a constant workflow on independent tasks", + "TEMPLATE_KANBAN_LONGDESC": "The Kanban methodology is used to divide project development (any sort of project) into stages.\nA kanban card is like an index card or post-it note that details every task (or user story) in a project that needs to be completed. The Kanban board is used to move each card from one state of completion to the next and in so doing, helps track progress.", + "DUPLICATE": "Duplicate project", + "DUPLICATE_DESC": "Start clean and keep your configuration", + "IMPORT": "Import project", + "IMPORT_DESC": "Import from Taiga, Trello, Jira, Github...", + "INVITE": "Invite to the project", + "SOLO_PROJECT": "You'll be alone in this project", + "INVITE_LATER": "(You'll be able to invite more members later)", + "BACK": "Back", + "MAX_PRIVATE_PROJECTS": "Unfortunately, You've reached the maximum number of private projects.\nIf you would like to increase the current limit please contact the administrator.", + "MAX_PUBLIC_PROJECTS": "Unfortunately, You've reached the maximum number of public projects.\nIf you would like to increase the current limit please contact the administrator.", + "PUBLIC_PROJECT": "Public Project", + "PRIVATE_PROJECT": "Private Project", + "MAX_MEMBERS": "Unfortunately, your current plan allows for a maximum of {{max_memberships}} members per project.\n Unselect some members or contact the administrator.", + "PRIVATE_PROJECT": "Private Project" + }, + "COMMON": { + "DETAILS": "New project details", + "PROJECT_TITLE": "Project Name", + "PROJECT_DESCRIPTION": "Project Description" + }, + "DUPLICATE": { + "TITLE": "Duplicate Project", + "DESCRIPTION": "Start clean and keep your configuration", + "SELECT_PLACEHOLDER": "Choose an existing project to duplicate", + "DETAILS": "New project details", + "CREATE_PROJECT_TEXT": "Fresh and clean. So exciting!", + "CHOOSE_TEMPLATE_TITLE": "More info about project templates", + "CHOOSE_TEMPLATE_INFO": "More info", + "PROJECT_DETAILS": "Project Details", + "PUBLIC_PROJECT": "Public Project", + "PRIVATE_PROJECT": "Private Project", + "CREATE_PROJECT": "Create project", + "CHANGE_PLANS": "change plans" + }, + "IMPORT": { + "TITLE": "Import Project", + "IMPORT": "Import", + "ARCHIVED": "Archived", + "ARCHIVED_DESCRIPTION": "You have archived projects, Do you want to import your archived projects?", + "WHO_IS": "Their tasks will be assigned to ...", + "WRITE_EMAIL": "Or if you want, write the email that their use in Taiga", + "SEARCH_CONTACT": "Or if you want, search in your contacts", + "WRITE_EMAIL_LABEL": "Write the email that their use in Taiga", + "EMAIL_NOT_FOUND": "We did not find any users with that email", + "ACCEEDE": "Acceede", + "PROJECT_MEMBERS": "Project Members", + "PROCESS_DESCRIPTION": "Tell us who from Taiga you want to assign the tasks of {{platform}}", + "MATCH": "Is {{user_external}} the same person as {{user_internal}}?", + "CHOOSE": "Select user", + "LINKS": "Links with {{platform}}", + "LINKS_DESCRIPTION": "Do you want to keep the link of each item with the original {{platform}} card?", + "WARNING_MAIL_USER": "Note that if the user does not have a Taiga account we will not be able to assign the tasks to him.", + "ASSIGN": "Assign", + "PROJECT_RESTRICTIONS": { + "PROJECT_MEMBERS_DESC_PRIVATE": "The project you are trying to import has {{members}} members including you, unfortunately, your current plan allows for a maximum of {{max_memberships}} members per private project. If you would like to increase that limit please contact the administrator.", + "PROJECT_MEMBERS_DESC_PUBLIC": "The project you are trying to import has {{members}} members including you, unfortunately, your current plan allows for a maximum of {{max_memberships}} members per public project. If you would like to increase that limit please contact the administrator.", + "ACCOUNT_ALLOW_MEMBERS": "Your account only allows {{members}} members", + "PRIVATE_PROJECTS_SPACE": { + "TITLE": "Unfortunately, your current plan does not allow for additional private projects", + "DESC": "The project you are trying to import is private. Unfortunately, your current plan does not allow for additional private projects." + }, + "PUBLIC_PROJECTS_SPACE": { + "TITLE": "Unfortunately, your current plan does not allow for additional public projects", + "DESC": "The project you are trying to import is public. Unfortunately, your current plan does not allow additional public projects." + }, + "PRIVATE_PROJECTS_MEMBERS": { + "TITLE": "Your current plan allows for a maximum of {{max_memberships}} members per private project" + }, + "PUBLIC_PROJECTS_MEMBERS": { + "TITLE": "Your current plan allows for a maximum of {{max_memberships}} members per public project." + }, + "PRIVATE_PROJECTS_SPACE_MEMBERS": { + "TITLE": "Unfortunately your current plan doesn't allow additional private projects or an increase of more than {{max_memberships}} members per private project", + "DESC": "The project that you are trying to import is private and has {{members}} members." + }, + "PUBLIC_PROJECTS_SPACE_MEMBERS": { + "TITLE": "Unfortunately your current plan doesn't allow additional public projects or an increase of more than {{max_memberships}} members per public project", + "DESC": "The project that you are trying to import is public and has more than {{members}} members." + } + }, + "IN_PROGRESS": { + "TITLE": "Importing Project", + "DESCRIPTION": "This process can take a while, please keep the window open." + }, + "WARNING": { + "TITLE": "Some taks will be unassigned", + "DESCRIPTION": "There are still unidentified people. The cards assigned to these people will remain unassigned. Check all the contacts to not lose that information.", + "CHECK": "Check contacts" + }, + "TAIGA": { + "SELECTOR": "Import your Taiga project" + }, + "TRELLO": { + "TITLE": "Trello", + "SELECTOR": "Import your Trello boards into Taiga", + "CHOOSE_BOARD": "Choose board that you want to import" + }, + "GITHUB": { + "TITLE": "Github", + "SELECTOR": "Import your Github project issues", + "CHOOSE_BOARD": "Find the project you want to import", + "PROJECT_MEMBERS": "Project Members", + "HOW_DO_YOU_WANT_TO_IMPORT": "How do you want to import your issues into Taiga?", + "KANBAN_PROJECT": "As user stories in a kanban project", + "KANBAN_PROJECT_DESCRIPTION": "After that you can enable scrum with backlog.", + "SCRUM_PROJECT": "As user stories in a scrum project", + "SCRUM_PROJECT_DESCRIPTION": "After that you can enable kanban mode.", + "ISSUES_PROJECT": "As issues", + "ISSUES_PROJECT_DESCRIPTION": "You will not able to use your issues in kanban or scrum mode. You will be able to enable kanban or scrum for new user stories" + }, + "ASANA": { + "TITLE": "Asana", + "SELECTOR": "Import your Asana project and choose how to manage it", + "CHOOSE_BOARD": "Choose project that you want to import", + "KANBAN_PROJECT": "Kanban", + "SCRUM_PROJECT": "Scrum", + "CREATE_AS_SCRUM_DESCRIPTION": "The tasks and sub-tasks of your project will be created as Taiga user stories and tasks.", + "CREATE_AS_KANBAN_DESCRIPTION": "The tasks and sub-tasks of your project will be created as Taiga user stories and tasks.", + "PROJECT_MEMBERS": "Project Members" + }, + "JIRA": { + "TITLE": "Jira", + "CHOOSE_PROJECT": "Choose project or board that you want to import", + "SELECTOR": "Import your Jira project and choose how to manage it", + "URL": "Your Jira URL", + "PROJECT_MEMBERS": "Project Members", + "KANBAN_PROJECT": "Kanban", + "SCRUM_PROJECT": "Scrum", + "ISSUES_PROJECT": "Issues", + "CREATE_AS_SCRUM_DESCRIPTION": "The issues and sub-issues of your project will be created as Taiga user stories and tasks.", + "CREATE_AS_KANBAN_DESCRIPTION": "The issues and sub-issues of your project will be created as Taiga user stories and tasks.", + "CREATE_AS_ISSUES_DESCRIPTION": "What do you want to do with sub-issues from the Jira project? (Taiga doesn't allow sub-issues)", + "CREATE_NEW_ISSUES": "Convert sub-issues to new Taiga issues", + "NOT_CREATE_NEW_ISSUES": "Do not import sub-issues" + } } }, "LIGHTBOX": { @@ -1518,20 +1623,7 @@ "THEME_DEFAULT": "-- use default theme --" } }, - "WIZARD": { - "SECTION_TITLE_CREATE_PROJECT": "Create Project", - "CREATE_PROJECT_TEXT": "Fresh and clean. So exciting!", - "CHOOSE_TEMPLATE": "Which template fits your project best?", - "CHOOSE_TEMPLATE_TITLE": "More info about project templates", - "CHOOSE_TEMPLATE_INFO": "More info", - "PROJECT_DETAILS": "Project Details", - "PUBLIC_PROJECT": "Public Project", - "PRIVATE_PROJECT": "Private Project", - "CREATE_PROJECT": "Create project", - "MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects", - "MAX_PUBLIC_PROJECTS": "Unfortunately, you've reached the maximum number of public projects", - "CHANGE_PLANS": "change plans" - }, + "WIKI": { "PAGE_TITLE": "{{wikiPageName}} - Wiki - {{projectName}}", "PAGE_DESCRIPTION": "Last edition on {{lastModifiedDate}} ({{totalEditions}} editions in total) Content: {{ wikiPageContent }}", diff --git a/app/modules/components/click-input-file.directive.coffee b/app/modules/components/click-input-file.directive.coffee new file mode 100644 index 00000000..bce23fbc --- /dev/null +++ b/app/modules/components/click-input-file.directive.coffee @@ -0,0 +1,39 @@ +### +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino Garcia +# Copyright (C) 2014-2016 David Barragán Merino +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Juan Francisco Alcántara +# Copyright (C) 2014-2016 Xavi Julian +# +# 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/components/click-input-file.directive.coffee +### + +ClickInputFile = () -> + return { + link: (scope, el) -> + el.on 'click', (e) -> + if !$(e.target).is('input') + e.preventDefault() + inputFile = el.find('input[type="file"]') + inputFile.val('') + inputFile.trigger('click') + + scope.$on "$destroy", -> el.off() + } + +angular.module("taigaComponents") + .directive("tgClickInputFile", [ClickInputFile]) diff --git a/app/modules/components/file-change/file-change.directive.coffee b/app/modules/components/file-change/file-change.directive.coffee index 3c94f94a..bbd90bfa 100644 --- a/app/modules/components/file-change/file-change.directive.coffee +++ b/app/modules/components/file-change/file-change.directive.coffee @@ -27,7 +27,6 @@ FileChangeDirective = ($parse) -> scope.$on "$destroy", -> el.off() return { - require: "ngModel", restrict: "A", link: link } diff --git a/app/modules/home/projects/home-project-list-directive.spec.coffee b/app/modules/home/projects/home-project-list-directive.spec.coffee index d19de5ee..877d575c 100644 --- a/app/modules/home/projects/home-project-list-directive.spec.coffee +++ b/app/modules/home/projects/home-project-list-directive.spec.coffee @@ -40,12 +40,6 @@ describe "homeProjectListDirective", () -> provide.value "tgCurrentUserService", mocks.currentUserService - _mockTgProjectsService = () -> - mocks.projectsService = { - newProject: sinon.stub() - } - provide.value "tgProjectsService", mocks.projectsService - _mockTranslateFilter = () -> mockTranslateFilter = (value) -> return value @@ -55,7 +49,6 @@ describe "homeProjectListDirective", () -> module ($provide) -> provide = $provide _mockTgCurrentUserService() - _mockTgProjectsService() _mockTranslateFilter() return null @@ -82,11 +75,3 @@ describe "homeProjectListDirective", () -> elm = createDirective() scope.$apply() expect(elm.isolateScope().vm.projects.size).to.be.equal(3) - - it "home project list directive newProject", () -> - elm = createDirective() - scope.$apply() - - expect(mocks.projectsService.newProject.callCount).to.be.equal(0) - elm.isolateScope().vm.newProject() - expect(mocks.projectsService.newProject.callCount).to.be.equal(1) diff --git a/app/modules/home/projects/home-project-list.directive.coffee b/app/modules/home/projects/home-project-list.directive.coffee index d59a28e3..f6506f68 100644 --- a/app/modules/home/projects/home-project-list.directive.coffee +++ b/app/modules/home/projects/home-project-list.directive.coffee @@ -17,15 +17,12 @@ # File: home-project-list.directive.coffee ### -HomeProjectListDirective = (currentUserService, projectsService) -> +HomeProjectListDirective = (currentUserService) -> link = (scope, el, attrs, ctrl) -> scope.vm = {} taiga.defineImmutableProperty(scope.vm, "projects", () -> currentUserService.projects.get("recents")) - scope.vm.newProject = -> - projectsService.newProject() - directive = { templateUrl: "home/projects/home-project-list.html" scope: {} @@ -35,8 +32,7 @@ HomeProjectListDirective = (currentUserService, projectsService) -> return directive HomeProjectListDirective.$inject = [ - "tgCurrentUserService", - "tgProjectsService" + "tgCurrentUserService" ] angular.module("taigaHome").directive("tgHomeProjectList", HomeProjectListDirective) diff --git a/app/modules/home/projects/home-project-list.jade b/app/modules/home/projects/home-project-list.jade index beecb13c..73cf4ca6 100644 --- a/app/modules/home/projects/home-project-list.jade +++ b/app/modules/home/projects/home-project-list.jade @@ -84,14 +84,7 @@ section.projects-empty(ng-if="vm.projects != undefined && vm.projects.size === 0 p(translate="HOME.EMPTY_PROJECT_LIST") a.create-project-button.button-green( href="#" - ng-click="vm.newProject()" + tg-nav="create-project" title="{{'PROJECT.NAVIGATION.TITLE_CREATE_PROJECT' | translate}}" translate="PROJECT.NAVIGATION.ACTION_CREATE_PROJECT" ) - span(tg-import-project-button) - a.import-project-button.button-blackish( - href="#" - title="{{'PROJECT.NAVIGATION.TITLE_IMPORT_PROJECT' | translate}}" - translate="PROJECT.NAVIGATION.ACTION_IMPORT_PROJECT" - ) - input.import-file.hidden(type="file") diff --git a/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.jade b/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.jade index 7940b928..b273ae62 100644 --- a/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.jade +++ b/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.jade @@ -29,14 +29,6 @@ div.navbar-dropdown.dropdown-project-list div.create-options a.create-project-btn.button-green( href="#", - ng-click="vm.newProject()", + tg-nav="create-project" title="{{'PROJECT.NAVIGATION.ACTION_CREATE_PROJECT' | translate}}", translate="PROJECT.NAVIGATION.ACTION_CREATE_PROJECT") - - span(tg-import-project-button) - a.button-blackish.import-project-button( - href="" - title="{{'PROJECT.NAVIGATION.TITLE_IMPORT_PROJECT' | translate}}" - ) - tg-svg(svg-icon="icon-upload") - input.import-file.hidden(type="file") diff --git a/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.controller.coffee b/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.controller.coffee new file mode 100644 index 00000000..d108f026 --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.controller.coffee @@ -0,0 +1,55 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: asana-import-project-form.controller.coffee +### + +class AsanaImportProjectFormController + @.$inject = [ + "tgCurrentUserService" + ] + + constructor: (@currentUserService) -> + @.canCreatePublicProjects = @currentUserService.canCreatePublicProjects() + @.canCreatePrivateProjects = @currentUserService.canCreatePrivateProjects() + + @.projectForm = @.project.toJS() + + @.platformName = "Asana" + @.projectForm.is_private = false + @.projectForm.keepExternalReference = false + @.projectForm.project_type = "scrum" + + if !@.canCreatePublicProjects.valid && @.canCreatePrivateProjects.valid + @.projectForm.is_private = true + + checkUsersLimit: () -> + @.limitMembersPrivateProject = @currentUserService.canAddMembersPrivateProject(@.members.size) + @.limitMembersPublicProject = @currentUserService.canAddMembersPublicProject(@.members.size) + + saveForm: () -> + @.onSaveProjectDetails({project: Immutable.fromJS(@.projectForm)}) + + canCreateProject: () -> + if @.projectForm.is_private + return @.canCreatePrivateProjects.valid + else + return @.canCreatePublicProjects.valid + + isDisabled: () -> + return !@.canCreateProject() + +angular.module('taigaProjects').controller('AsanaImportProjectFormCtrl', AsanaImportProjectFormController) diff --git a/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.directive.coffee b/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.directive.coffee new file mode 100644 index 00000000..5c3879ca --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.directive.coffee @@ -0,0 +1,40 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: asana-import-project-form.directive.coffee +### + +AsanaImportProjectFormDirective = () -> + return { + link: (scope, elm, attr, ctrl) -> + scope.$watch('vm.members', ctrl.checkUsersLimit.bind(ctrl)) + + templateUrl:"projects/create/asana-import/asana-import-project-form/asana-import-project-form.html", + controller: "AsanaImportProjectFormCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + members: '<', + project: '<', + onSaveProjectDetails: '&', + onCancelForm: '&', + fetchingUsers: '<' + } + } + +AsanaImportProjectFormDirective.$inject = [] + +angular.module("taigaProjects").directive("tgAsanaImportProjectForm", AsanaImportProjectFormDirective) diff --git a/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.jade b/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.jade new file mode 100644 index 00000000..681f9f1c --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.jade @@ -0,0 +1,63 @@ +.import-project-asana-form + div(ng-include="'projects/create/import/import-header.html'") + + .spin(tg-loading="vm.fetchingUsers") + + form( + ng-if="!vm.fetchingUsers", + name="projectForm", + ng-submit="vm.saveForm()" + ) + div(ng-include="'projects/create/import-project-form-common/name.html'") + div(ng-include="'projects/create/import-project-form-common/description.html'") + .create-project-import-type(role="group") + fieldset + input( + type="radio" + name="project_type" + id="template-scrum" + data-required="true" + aria-hidden="true" + ng-value="'scrum'" + ng-model="vm.projectForm.project_type" + required + ) + label(for="template-scrum") + tg-svg(svg-icon="icon-scrum") + span(translate="PROJECT.IMPORT.ASANA.SCRUM_PROJECT") + fieldset + input( + type="radio" + name="project_type" + id="template-kanban" + data-required="true" + aria-hidden="true" + ng-value="'kanban'" + ng-model="vm.projectForm.project_type" + required + ) + label(for="template-kanban") + tg-svg(svg-icon="icon-kanban") + span(translate="PROJECT.IMPORT.ASANA.KANBAN_PROJECT") + + p.create-project-import-type-info( + ng-if="vm.projectForm.project_type == 'scrum'" + translate='PROJECT.IMPORT.ASANA.CREATE_AS_SCRUM_DESCRIPTION' + ) + p.create-project-import-type-info( + ng-if="vm.projectForm.project_type == 'kanban'" + translate='PROJECT.IMPORT.ASANA.CREATE_AS_KANBAN_DESCRIPTION' + ) + div(ng-include="'projects/create/import-project-form-common/project-privacy.html'") + tg-create-project-restrictions( + is-private="vm.projectForm.is_private" + can-create-public-projects="vm.canCreatePublicProjects" + can-create-private-projects="vm.canCreatePrivateProjects" + ) + tg-create-project-members-restrictions( + is-private="vm.projectForm.is_private" + limit-members-private-project="vm.limitMembersPrivateProject" + limit-members-public-project="vm.limitMembersPublicProject" + ) + div(ng-include="'projects/create/import-project-form-common/links.html'") + div(ng-include="'projects/create/import-project-form-common/actions.html'") diff --git a/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.scss b/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.scss new file mode 100644 index 00000000..e31c798c --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import-project-form/asana-import-project-form.scss @@ -0,0 +1,57 @@ +.import-project-asana-form { + @include create-project; +} + +.create-project-asana-import-type { + margin-bottom: 1rem; + text-align: center; + + &-question { + align-content: stretch; + align-items: stretch; + display: flex; + } + + fieldset { + background: $white; + border-right: 1px solid $whitish; + transition: background .2s linear; + + &:last-child { + border: 0; + } + } + + input { + display: none; + + &:checked { + +label { + background: rgba($primary, .1); + } + } + } + + label { + background: $white; + height: 100%; + padding: 1rem; + transition: background .2s ease-in; + + &:hover { + background: rgba($primary, .1); + cursor: pointer; + } + } + + &-name { + @include font-type(normal); + display: inline-block; + margin-bottom: .5rem; + } + + &-description { + @include font-type(light); + @include font-size(small); + } +} diff --git a/app/modules/projects/create/asana-import/asana-import.controller.coffee b/app/modules/projects/create/asana-import/asana-import.controller.coffee new file mode 100644 index 00000000..51e61a5a --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import.controller.coffee @@ -0,0 +1,73 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: asana-import.controller.coffee +### + +class AsanaImportController + @.$inject = [ + 'tgAsanaImportService', + '$tgConfirm', + '$translate', + 'tgImportProjectService', + ] + + constructor: (@asanaImportService, @confirm, @translate, @importProjectService) -> + @.step = 'autorization-asana' + @.project = null + taiga.defineImmutableProperty @, 'projects', () => return @asanaImportService.projects + taiga.defineImmutableProperty @, 'members', () => return @asanaImportService.projectUsers + + startProjectSelector: () -> + @.step = 'project-select-asana' + @asanaImportService.fetchProjects() + + onSelectProject: (project) -> + @.step = 'project-form-asana' + @.project = project + @.fetchingUsers = true + + @asanaImportService.fetchUsers(@.project.get('id')).then () => @.fetchingUsers = false + + onSaveProjectDetails: (project) -> + @.project = project + @.step = 'project-members-asana' + + onCancelMemberSelection: () -> + @.step = 'project-form-asana' + + startImport: (users) -> + loader = @confirm.loader(@translate.instant('PROJECT.IMPORT.IN_PROGRESS.TITLE'), @translate.instant('PROJECT.IMPORT.IN_PROGRESS.DESCRIPTION'), true) + + loader.start() + + promise = @asanaImportService.importProject( + @.project.get('name'), + @.project.get('description'), + @.project.get('id'), + users, + @.project.get('keepExternalReference'), + @.project.get('is_private') + @.project.get('project_type') + ) + + @importProjectService.importPromise(promise).then () => loader.stop() + + submitUserSelection: (users) -> + @.startImport(users) + return null + +angular.module('taigaProjects').controller('AsanaImportCtrl', AsanaImportController) diff --git a/app/modules/projects/create/asana-import/asana-import.controller.spec.coffee b/app/modules/projects/create/asana-import/asana-import.controller.spec.coffee new file mode 100644 index 00000000..49720ae1 --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import.controller.spec.coffee @@ -0,0 +1,172 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: asana-import.controller.spec.coffee +### + +describe "AsanaImportCtrl", -> + $provide = null + $controller = null + mocks = {} + + _mockCurrentUserService = -> + mocks.currentUserService = { + canAddMembersPrivateProject: sinon.stub() + canAddMembersPublicProject: sinon.stub() + } + + $provide.value("tgCurrentUserService", mocks.currentUserService) + + _mockAsanaImportService = -> + mocks.asanaService = { + fetchProjects: sinon.stub(), + fetchUsers: sinon.stub(), + importProject: sinon.stub() + } + + $provide.value("tgAsanaImportService", mocks.asanaService) + + _mockImportProjectService = -> + mocks.importProjectService = { + importPromise: sinon.stub() + } + + $provide.value("tgImportProjectService", mocks.importProjectService) + + _mockConfirm = -> + mocks.confirm = { + loader: sinon.stub() + } + + $provide.value("$tgConfirm", mocks.confirm) + + _mockTranslate = -> + mocks.translate = { + instant: sinon.stub() + } + + $provide.value("$translate", mocks.translate) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockAsanaImportService() + _mockConfirm() + _mockTranslate() + _mockImportProjectService() + _mockCurrentUserService() + + return null + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "start project selector", () -> + ctrl = $controller("AsanaImportCtrl") + ctrl.startProjectSelector() + + expect(ctrl.step).to.be.equal('project-select-asana') + expect(mocks.asanaService.fetchProjects).have.been.called + + it "on select project reload projects", (done) -> + project = Immutable.fromJS({ + id: 1, + name: "project-name" + }) + + mocks.asanaService.fetchUsers.promise().resolve() + + ctrl = $controller("AsanaImportCtrl") + + promise = ctrl.onSelectProject(project) + + expect(ctrl.fetchingUsers).to.be.true + + promise.then () -> + expect(ctrl.fetchingUsers).to.be.false + expect(ctrl.step).to.be.equal('project-form-asana') + expect(ctrl.project).to.be.equal(project) + done() + + it "on save project details reload users", () -> + project = Immutable.fromJS({ + id: 1, + name: "project-name" + }) + + ctrl = $controller("AsanaImportCtrl") + ctrl.onSaveProjectDetails(project) + + expect(ctrl.step).to.be.equal('project-members-asana') + expect(ctrl.project).to.be.equal(project) + + it "on select user init import", (done) -> + users = Immutable.fromJS([ + { + id: 0 + }, + { + id: 1 + }, + { + id: 2 + } + ]) + + loaderObj = { + start: sinon.spy(), + update: sinon.stub(), + stop: sinon.spy() + } + + projectResult = { + id: 3, + name: "name" + } + + mocks.confirm.loader.returns(loaderObj) + + mocks.importProjectService.importPromise.promise().resolve() + + ctrl = $controller("AsanaImportCtrl") + ctrl.project = Immutable.fromJS({ + id: 1, + name: 'project-name', + description: 'project-description', + keepExternalReference: false, + is_private: true + }) + + + mocks.asanaService.importProject.promise().resolve(projectResult) + + ctrl.startImport(users).then () -> + expect(loaderObj.start).have.been.called + expect(loaderObj.stop).have.been.called + expect(mocks.asanaService.importProject).have.been.calledWith('project-name', 'project-description', 1, users, false, true) + + done() diff --git a/app/modules/projects/create/asana-import/asana-import.directive.coffee b/app/modules/projects/create/asana-import/asana-import.directive.coffee new file mode 100644 index 00000000..18a8308e --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import.directive.coffee @@ -0,0 +1,35 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: asana-import.directive.coffee +### + +AsanaImportDirective = () -> + return { + link: (scope, elm, attrs, ctrl) -> + ctrl.startProjectSelector() + templateUrl:"projects/create/asana-import/asana-import.html", + controller: "AsanaImportCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + onCancel: '&' + } + } + +AsanaImportDirective.$inject = [] + +angular.module("taigaProjects").directive("tgAsanaImport", AsanaImportDirective) diff --git a/app/modules/projects/create/asana-import/asana-import.jade b/app/modules/projects/create/asana-import/asana-import.jade new file mode 100644 index 00000000..ac684194 --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import.jade @@ -0,0 +1,31 @@ +.create-project.import-project(ng-if="vm.step == 'autorization-asana'") + p autorization... + + +tg-import-project-selector( + logo="/#{v}/images/import-logos/asana.png" + search="{{ 'PROJECT.IMPORT.ASANA.CHOOSE_BOARD' | translate }}" + projects="vm.projects" + on-cancel="vm.onCancel()" + on-select-project="vm.onSelectProject(project)" + ng-if="vm.step == 'project-select-asana'" +) + +tg-asana-import-project-form( + ng-if="vm.step == 'project-form-asana'" + project="vm.project" + members="vm.members" + fetching-users="vm.fetchingUsers" + on-save-project-details="vm.onSaveProjectDetails(project)" + on-cancel-form="vm.step = 'project-select-asana'" +) + +tg-import-project-members( + ng-if="vm.step == 'project-members-asana'" + platform="Asana" + logo="/#{v}/images/import-logos/asana.png" + project="vm.project" + members="vm.members" + on-submit="vm.submitUserSelection(users)" + on-cancel="vm.onCancelMemberSelection()" +) diff --git a/app/modules/projects/create/asana-import/asana-import.service.coffee b/app/modules/projects/create/asana-import/asana-import.service.coffee new file mode 100644 index 00000000..e3723fec --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import.service.coffee @@ -0,0 +1,57 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: asana-import.service.coffee +### + +class AsanaImportService extends taiga.Service + @.$inject = [ + 'tgResources', + '$location' + ] + + constructor: (@resources, @location) -> + @.projects = Immutable.List() + @.projectUsers = Immutable.List() + @.token = null + + setToken: (token) -> + @.token = token + + fetchProjects: () -> + @resources.asanaImporter.listProjects(@.token).then (projects) => @.projects = projects + + fetchUsers: (projectId) -> + @resources.asanaImporter.listUsers(@.token, projectId).then (users) => @.projectUsers = users + + importProject: (name, description, projectId, userBindings, keepExternalReference, isPrivate, projectType) -> + return @resources.asanaImporter.importProject(@.token, name, description, projectId, userBindings, keepExternalReference, isPrivate, projectType) + + getAuthUrl: () -> + return new Promise (resolve) => + @resources.asanaImporter.getAuthUrl().then (response) => + @.authUrl = response.data.url + resolve(@.authUrl) + + authorize: (code) -> + return new Promise (resolve, reject) => + @resources.asanaImporter.authorize(code).then ((response) => + @.token = response.data.token + resolve(@.token) + ), (error) -> + reject(new Error(error.status)) + +angular.module("taigaProjects").service("tgAsanaImportService", AsanaImportService) diff --git a/app/modules/projects/create/asana-import/asana-import.service.spec.coffee b/app/modules/projects/create/asana-import/asana-import.service.spec.coffee new file mode 100644 index 00000000..0574e582 --- /dev/null +++ b/app/modules/projects/create/asana-import/asana-import.service.spec.coffee @@ -0,0 +1,128 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: asana-import.controller.spec.coffee +### + +describe "tgAsanaImportService", -> + $provide = null + service = null + mocks = {} + + _mockResources = -> + mocks.resources = { + asanaImporter: { + listProjects: sinon.stub(), + listUsers: sinon.stub(), + importProject: sinon.stub(), + getAuthUrl: sinon.stub(), + authorize: sinon.stub() + } + } + + $provide.value("tgResources", mocks.resources) + + _mockLocation = -> + mocks.location = { + search: sinon.stub() + } + + mocks.location.search.returns({ + from: 'asana' + token: 123 + }) + + $provide.value("$location", mocks.location) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockResources() + _mockLocation() + + return null + + _inject = -> + inject (_tgAsanaImportService_) -> + service = _tgAsanaImportService_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "fetch projects", (done) -> + service.setToken(123) + mocks.resources.asanaImporter.listProjects.withArgs(123).promise().resolve('projects') + + service.fetchProjects().then () -> + service.projects = "projects" + done() + + it "fetch user", (done) -> + service.setToken(123) + projectId = 3 + mocks.resources.asanaImporter.listUsers.withArgs(123, projectId).promise().resolve('users') + + service.fetchUsers(projectId).then () -> + service.projectUsers = 'users' + done() + + it "import project", () -> + service.setToken(123) + projectId = 2 + + service.importProject(projectId, true, true ,true) + + expect(mocks.resources.asanaImporter.importProject).to.have.been.calledWith(123, projectId, true, true, true) + + it "get auth url", (done) -> + service.setToken(123) + projectId = 3 + + response = { + data: { + url: "url123" + } + } + + mocks.resources.asanaImporter.getAuthUrl.promise().resolve(response) + + service.getAuthUrl().then (url) -> + expect(url).to.be.equal("url123") + done() + + it "authorize", (done) -> + service.setToken(123) + projectId = 3 + verifyCode = 12345 + + response = { + data: { + token: "token123" + } + } + + mocks.resources.asanaImporter.authorize.withArgs(verifyCode).promise().resolve(response) + + service.authorize(verifyCode).then (token) -> + expect(token).to.be.equal("token123") + done() diff --git a/app/modules/projects/create/create-project-form/create-project-form.controller.coffee b/app/modules/projects/create/create-project-form/create-project-form.controller.coffee new file mode 100644 index 00000000..1a4c235d --- /dev/null +++ b/app/modules/projects/create/create-project-form/create-project-form.controller.coffee @@ -0,0 +1,63 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: create-project-form.controller.coffee +### + +class CreatetProjectFormController + @.$inject = [ + "tgCurrentUserService", + "tgProjectsService", + "$projectUrl", + "$location", + "$tgNavUrls" + ] + + constructor: (@currentUserService, @projectsService, @projectUrl, @location, @navUrls) -> + @.projectForm = { + is_private: false + } + + @.canCreatePublicProjects = @currentUserService.canCreatePublicProjects() + @.canCreatePrivateProjects = @currentUserService.canCreatePrivateProjects() + + if !@.canCreatePublicProjects.valid && @.canCreatePrivateProjects.valid + @.projectForm.is_private = true + + if @.type == 'scrum' + @.projectForm.creation_template = 1 + else + @.projectForm.creation_template = 2 + + submit: () -> + @.formSubmitLoading = true + + @projectsService.create(@.projectForm).then (project) => + @location.url(@projectUrl.get(project)) + + onCancelForm: () -> + @location.path(@navUrls.resolve("create-project")) + + canCreateProject: () -> + if @.projectForm.is_private + return @.canCreatePrivateProjects.valid + else + return @.canCreatePublicProjects.valid + + isDisabled: () -> + return @.formSubmitLoading || !@.canCreateProject() + +angular.module('taigaProjects').controller('CreateProjectFormCtrl', CreatetProjectFormController) diff --git a/app/modules/projects/create/create-project-form/create-project-form.controller.spec.coffee b/app/modules/projects/create/create-project-form/create-project-form.controller.spec.coffee new file mode 100644 index 00000000..bc68c788 --- /dev/null +++ b/app/modules/projects/create/create-project-form/create-project-form.controller.spec.coffee @@ -0,0 +1,139 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: create-project-form.controller.spec.coffee +### + +describe "CreateProjectFormCtrl", -> + $provide = null + $controller = null + mocks = {} + + _mockNavUrlsService = -> + mocks.navUrls = { + resolve: sinon.stub() + } + + $provide.value("$tgNavUrls", mocks.navUrls) + + _mockCurrentUserService = -> + mocks.currentUserService = { + canCreatePublicProjects: sinon.stub().returns({valid: true}), + canCreatePrivateProjects: sinon.stub().returns({valid: true}) + } + + $provide.value("tgCurrentUserService", mocks.currentUserService) + + _mockProjectsService = -> + mocks.projectsService = { + create: sinon.stub() + } + + $provide.value("tgProjectsService", mocks.projectsService) + + _mockProjectUrl = -> + mocks.projectUrl = { + get: sinon.stub() + } + + $provide.value("$projectUrl", mocks.projectUrl) + + _mockLocation = -> + mocks.location = { + url: sinon.stub() + } + + $provide.value("$location", mocks.location) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockCurrentUserService() + _mockProjectsService() + _mockProjectUrl() + _mockLocation() + _mockNavUrlsService() + + return null + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "submit project form", () -> + ctrl = $controller("CreateProjectFormCtrl") + + ctrl.projectForm = 'form' + + mocks.projectsService.create.withArgs('form').promise().resolve('project1') + mocks.projectUrl.get.returns('project-url') + + ctrl.submit().then () -> + expect(ctrl.formSubmitLoading).to.be.true + + expect(mocks.location.url).to.have.been.calledWith('project-url') + + it 'check if the user can create a private projects', () -> + mocks.currentUserService.canCreatePrivateProjects = sinon.stub().returns({valid: true}) + + ctrl = $controller("CreateProjectFormCtrl") + + ctrl.projectForm = { + is_private: true + } + + expect(ctrl.canCreateProject()).to.be.true + + mocks.currentUserService.canCreatePrivateProjects = sinon.stub().returns({valid: false}) + + ctrl = $controller("CreateProjectFormCtrl") + + ctrl.projectForm = { + is_private: true + } + + expect(ctrl.canCreateProject()).to.be.false + + it 'check if the user can create a public projects', () -> + mocks.currentUserService.canCreatePublicProjects = sinon.stub().returns({valid: true}) + + ctrl = $controller("CreateProjectFormCtrl") + + ctrl.projectForm = { + is_private: false + } + + expect(ctrl.canCreateProject()).to.be.true + + mocks.currentUserService.canCreatePublicProjects = sinon.stub().returns({valid: false}) + + ctrl = $controller("CreateProjectFormCtrl") + + ctrl.projectForm = { + is_private: false + } + + expect(ctrl.canCreateProject()).to.be.false diff --git a/app/modules/projects/create/create-project-form/create-project-form.directive.coffee b/app/modules/projects/create/create-project-form/create-project-form.directive.coffee new file mode 100644 index 00000000..c3853d83 --- /dev/null +++ b/app/modules/projects/create/create-project-form/create-project-form.directive.coffee @@ -0,0 +1,31 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: create-project-form.directive.coffee +### + +CreateProjectFormDirective = () -> + return { + templateUrl:"projects/create/create-project-form/create-project-form.html", + controller: "CreateProjectFormCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + type: '@' + } + } + +angular.module("taigaProjects").directive("tgCreateProjectForm", CreateProjectFormDirective) diff --git a/app/modules/projects/create/create-project-form/create-project-form.jade b/app/modules/projects/create/create-project-form/create-project-form.jade new file mode 100644 index 00000000..c5a29054 --- /dev/null +++ b/app/modules/projects/create/create-project-form/create-project-form.jade @@ -0,0 +1,32 @@ +.create-project + h1.create-project-title(ng-if="vm.type == 'scrum'") + tg-svg(svg-icon="icon-scrum") + span(translate="PROJECT.CREATE.TEMPLATE_SCRUM") + h3.create-project-description( + ng-if="vm.type == 'scrum'" + translate="PROJECT.CREATE.TEMPLATE_SCRUM_DESC" + ) + + h1.create-project-title(ng-if="vm.type == 'kanban'") + tg-svg(svg-icon="icon-kanban") + span(translate="PROJECT.CREATE.TEMPLATE_KANBAN") + h3.create-project-description( + ng-if="vm.type == 'kanban'" + translate="PROJECT.CREATE.TEMPLATE_KANBAN_DESC" + ) + + form( + name="projectForm", + ng-submit="vm.submit()" + ) + div(ng-include="'projects/create/import-project-form-common/name.html'") + div(ng-include="'projects/create/import-project-form-common/description.html'") + div(ng-include="'projects/create/import-project-form-common/project-privacy.html'") + + tg-create-project-restrictions( + is-private="vm.projectForm.is_private" + can-create-public-projects="vm.canCreatePublicProjects" + can-create-private-projects="vm.canCreatePrivateProjects" + ) + + div(ng-include="'projects/create/import-project-form-common/actions.html'") \ No newline at end of file diff --git a/app/modules/projects/create/create-project-members-restrictions/create-project-members-restrictions.directive.coffee b/app/modules/projects/create/create-project-members-restrictions/create-project-members-restrictions.directive.coffee new file mode 100644 index 00000000..da6d0c90 --- /dev/null +++ b/app/modules/projects/create/create-project-members-restrictions/create-project-members-restrictions.directive.coffee @@ -0,0 +1,13 @@ +module = angular.module("taigaProject") + +createProjectMembersRestrictionsDirective = () -> + return { + scope: { + isPrivate: '=', + limitMembersPrivateProject: '=', + limitMembersPublicProject: '=' + }, + templateUrl: "projects/create/create-project-members-restrictions/create-project-members-restrictions.html" + } + +module.directive('tgCreateProjectMembersRestrictions', [createProjectMembersRestrictionsDirective]) diff --git a/app/modules/projects/create/create-project-members-restrictions/create-project-members-restrictions.jade b/app/modules/projects/create/create-project-members-restrictions/create-project-members-restrictions.jade new file mode 100644 index 00000000..c65b9d6e --- /dev/null +++ b/app/modules/projects/create/create-project-members-restrictions/create-project-members-restrictions.jade @@ -0,0 +1,13 @@ +div.create-project-warning(ng-if="!limitMembersPublicProject.valid && !isPrivate") + tg-svg(svg-icon="icon-exclamation") + span( + translate="PROJECT.IMPORT.PROJECT_RESTRICTIONS.PROJECT_MEMBERS_DESC_PUBLIC", + translate-values="{'members': limitMembersPublicProject.current, 'max_memberships': limitMembersPublicProject.max}" + ) + +div.create-project-warning(ng-if="!limitMembersPrivateProject.valid && isPrivate") + tg-svg(svg-icon="icon-exclamation") + span( + translate="PROJECT.IMPORT.PROJECT_RESTRICTIONS.PROJECT_MEMBERS_DESC_PRIVATE", + translate-values="{'members': limitMembersPrivateProject.current, 'max_memberships': limitMembersPrivateProject.max}" + ) \ No newline at end of file diff --git a/app/modules/projects/create/create-project-restrictions/create-project-restrictions.directive.coffee b/app/modules/projects/create/create-project-restrictions/create-project-restrictions.directive.coffee new file mode 100644 index 00000000..e825a916 --- /dev/null +++ b/app/modules/projects/create/create-project-restrictions/create-project-restrictions.directive.coffee @@ -0,0 +1,13 @@ +module = angular.module("taigaProject") + +createProjectRestrictionsDirective = () -> + return { + scope: { + isPrivate: '=', + canCreatePrivateProjects: '=', + canCreatePublicProjects: '=' + }, + templateUrl: "projects/create/create-project-restrictions/create-project-restrictions.html" + } + +module.directive('tgCreateProjectRestrictions', [createProjectRestrictionsDirective]) diff --git a/app/modules/projects/create/create-project-restrictions/create-project-restrictions.jade b/app/modules/projects/create/create-project-restrictions/create-project-restrictions.jade new file mode 100644 index 00000000..1943527b --- /dev/null +++ b/app/modules/projects/create/create-project-restrictions/create-project-restrictions.jade @@ -0,0 +1,11 @@ +div.create-project-warning( + ng-if="isPrivate && !canCreatePrivateProjects.valid && canCreatePrivateProjects.reason == 'max_private_projects'" +) + tg-svg(svg-icon="icon-exclamation") + span {{ 'PROJECT.CREATE.MAX_PRIVATE_PROJECTS' | translate }} + +div.create-project-warning( + ng-if="!isPrivate && !canCreatePublicProjects.valid && canCreatePublicProjects.reason == 'max_public_projects'" +) + tg-svg(svg-icon="icon-exclamation") + span {{ 'PROJECT.CREATE.MAX_PUBLIC_PROJECTS' | translate }} \ No newline at end of file diff --git a/app/modules/projects/create/create-project.controller.coffee b/app/modules/projects/create/create-project.controller.coffee new file mode 100644 index 00000000..7678aa68 --- /dev/null +++ b/app/modules/projects/create/create-project.controller.coffee @@ -0,0 +1,56 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: project.controller.coffee +### + +class CreateProjectController + @.$inject = [ + "tgAppMetaService", + "$translate", + "tgProjectService", + "$location" + ] + + constructor: (@appMetaService, @translate, @projectService, @location) -> + taiga.defineImmutableProperty @, "project", () => return @projectService.project + + @appMetaService.setfn @._setMeta.bind(this) + + @.displayScrumDesc = false + @.displayKanbanDesc = false + + _setMeta: () -> + return null if !@.project + + ctx = {projectName: @.project.get("name")} + + return { + title: @translate.instant("PROJECT.PAGE_TITLE", ctx) + description: @.project.get("description") + } + + displayHelp: (type, $event) -> + $event.stopPropagation() + $event.preventDefault() + + if type == 'scrum' + @.displayScrumDesc = !@.displayScrumDesc + if type == 'kanban' + @.displayKanbanDesc = !@.displayKanbanDesc + + +angular.module("taigaProjects").controller("CreateProjectCtrl", CreateProjectController) diff --git a/app/modules/projects/create/create-project.controller.spec.coffee b/app/modules/projects/create/create-project.controller.spec.coffee new file mode 100644 index 00000000..385e5652 --- /dev/null +++ b/app/modules/projects/create/create-project.controller.spec.coffee @@ -0,0 +1,45 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: create-project.controller.spec.coffee +### + +describe "CreateProjectController", -> + provide = null + controller = null + mocks = {} + + _inject = (callback) -> + inject (_$controller_, _$q_, _$rootScope_) -> + controller = _$controller_ + + beforeEach -> + module "taigaProjects" + _inject() + + # it "get Home Step", () -> + # ctrl = controller "CreateProjectCtrl" + # ctrl.inDefaultStep = true + # ctrl.getStep('home') + # expect(ctrl.inDefaultStep).to.be.true + # expect(ctrl.inStepDuplicateProject).to.be.false + # + # it "get Duplicate Project Step", () -> + # ctrl = controller "CreateProjectCtrl" + # ctrl.inDefaultStep = true + # ctrl.getStep('duplicate') + # expect(ctrl.inDefaultStep).to.be.false + # expect(ctrl.inStepDuplicateProject).to.be.true diff --git a/app/modules/projects/create/create-project.jade b/app/modules/projects/create/create-project.jade new file mode 100644 index 00000000..2fdc47de --- /dev/null +++ b/app/modules/projects/create/create-project.jade @@ -0,0 +1,69 @@ +.create-project + .create-project-wrapper + h1.create-project-title(translate="PROJECT.CREATE.TITLE") + h3.create-project-description(translate="PROJECT.CREATE.CHOOSE_TEMPLATE") + ul.create-project-selector.e2e-create-project-selector + li + a.e2e-create-project-scrum( + title="{{'PROJECT.CREATE.TEMPLATE_SCRUM' | translate}}", + tg-nav="create-project-scrum" + href="" + ) + .create-project-selector-icon + tg-svg(svg-icon="icon-scrum") + .create-project-selector-template-wrapper + p.create-project-selector-template(translate="PROJECT.CREATE.TEMPLATE_SCRUM") + p.create-project-selector-description(translate="PROJECT.CREATE.TEMPLATE_SCRUM_DESC") + .create-project-selector-question + tg-svg( + svg-icon="icon-question" + ng-click="vm.displayHelp('scrum', $event)" + ) + .create-project-selector-long-description(ng-show="vm.displayScrumDesc") + p(translate="PROJECT.CREATE.TEMPLATE_SCRUM_LONGDESC") + li + a.e2e-create-project-kanban( + tg-nav="create-project-kanban" + title="{{'PROJECT.CREATE.TEMPLATE_KANBAN' | translate}}", + href="" + ) + .create-project-selector-icon + tg-svg(svg-icon="icon-kanban") + + .create-project-selector-template-wrapper + p.create-project-selector-template(translate="PROJECT.CREATE.TEMPLATE_KANBAN") + p.create-project-selector-description(translate="PROJECT.CREATE.TEMPLATE_KANBAN_DESC") + .create-project-selector-question + tg-svg( + svg-icon="icon-question" + ng-click="vm.displayHelp('kanban', $event)" + ) + .create-project-selector-long-description(ng-show="vm.displayKanbanDesc") + p(translate="PROJECT.CREATE.TEMPLATE_KANBAN_LONGDESC") + li + a.e2e-duplicate-project( + tg-nav="create-project-duplicate" + title="{{'PROJECT.CREATE.DUPLICATE' | translate}}", + href="" + ) + .create-project-selector-icon + tg-svg(svg-icon="icon-duplicate") + .create-project-selector-template-wrapper + p.create-project-selector-template(translate="PROJECT.CREATE.DUPLICATE") + p.create-project-selector-description(translate="PROJECT.CREATE.DUPLICATE_DESC") + + li + a( + tg-nav="create-project-import" + title="{{'PROJECT.CREATE.IMPORT' | translate}}", + href="" + ) + .create-project-selector-icon + tg-svg(svg-icon="icon-upload") + .create-project-selector-template-wrapper + p.create-project-selector-template( + href="#" + title="{{'PROJECT.CREATE.IMPORT' | translate}}", + translate="PROJECT.CREATE.IMPORT" + ) + p.create-project-selector-description(translate="PROJECT.CREATE.IMPORT_DESC") diff --git a/app/modules/projects/create/create-project.scss b/app/modules/projects/create/create-project.scss new file mode 100644 index 00000000..9d120830 --- /dev/null +++ b/app/modules/projects/create/create-project.scss @@ -0,0 +1,3 @@ +.create-project { + @include create-project; +} diff --git a/app/modules/projects/create/duplicate/duplicate-project.controller.coffee b/app/modules/projects/create/duplicate/duplicate-project.controller.coffee new file mode 100644 index 00000000..f84bc340 --- /dev/null +++ b/app/modules/projects/create/duplicate/duplicate-project.controller.coffee @@ -0,0 +1,85 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: project.controller.coffee +### + +class DuplicateProjectController + @.$inject = [ + "tgCurrentUserService", + "tgProjectsService", + "$tgLocation", + "$tgNavUrls" + ] + + constructor: (@currentUserService, @projectsService, @location, @navUrls) -> + @.user = @currentUserService.getUser() + @.members = Immutable.List() + + @.canCreatePublicProjects = @currentUserService.canCreatePublicProjects() + @.canCreatePrivateProjects = @currentUserService.canCreatePrivateProjects() + + taiga.defineImmutableProperty @, 'projects', () => @currentUserService.projects.get("all") + + @.projectForm = { + is_private: false + } + + if !@.canCreatePublicProjects.valid && @.canCreatePrivateProjects.valid + @.projectForm.is_private = true + + refreshReferenceProject: (slug) -> + @projectsService.getProjectBySlug(slug).then (project) => + @.referenceProject = project + @.members = project.get('members') + @.invitedMembers = @.members.map (it) -> return it.get('id') + @.checkUsersLimit() + + toggleInvitedMember: (member) -> + if @.invitedMembers.includes(member) + @.invitedMembers = @.invitedMembers.filter (it) -> it != member + else + @.invitedMembers = @.invitedMembers.push(member) + + @.checkUsersLimit() + + checkUsersLimit: () -> + @.limitMembersPrivateProject = @currentUserService.canAddMembersPrivateProject(@.invitedMembers.size + 1) + @.limitMembersPublicProject = @currentUserService.canAddMembersPublicProject(@.invitedMembers.size + 1) + + submit: () -> + projectId = @.referenceProject.get('id') + data = @.projectForm + + @.formSubmitLoading = true + @projectsService.duplicate(projectId, data).then (newProject) => + @.formSubmitLoading = false + @location.path(@navUrls.resolve("project", {project: newProject.data.slug})) + @currentUserService.loadProjects() + + canCreateProject: () -> + if @.projectForm.is_private + return @.canCreatePrivateProjects.valid && @.limitMembersPrivateProject.valid + else + return @.canCreatePublicProjects.valid && @.limitMembersPublicProject.valid + + isDisabled: () -> + return @.formSubmitLoading || !@.canCreateProject() + + onCancelForm: () -> + @location.path(@navUrls.resolve("create-project")) + +angular.module("taigaProjects").controller("DuplicateProjectCtrl", DuplicateProjectController) diff --git a/app/modules/projects/create/duplicate/duplicate-project.controller.spec.coffee b/app/modules/projects/create/duplicate/duplicate-project.controller.spec.coffee new file mode 100644 index 00000000..92c35ebe --- /dev/null +++ b/app/modules/projects/create/duplicate/duplicate-project.controller.spec.coffee @@ -0,0 +1,222 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: home.controller.spec.coffee +### + +describe "DuplicateProjectController", -> + ctrl = null + provide = null + controller = null + mocks = {} + + _mockCurrentUserService = () -> + mocks.currentUserService = {} + mocks.currentUserService.getUser = sinon.stub() + mocks.currentUserService.canCreatePublicProjects = sinon.stub().returns(true) + mocks.currentUserService.canCreatePrivateProjects = sinon.stub().returns(true) + + mocks.currentUserService.projects = {} + mocks.currentUserService.projects.get = sinon.stub().returns([]) + + mocks.currentUserService.loadProjects = sinon.stub() + + mocks.currentUserService.canAddMembersPrivateProject = sinon.stub() + mocks.currentUserService.canAddMembersPublicProject = sinon.stub() + + provide.value "tgCurrentUserService", mocks.currentUserService + + _mockProjectService = () -> + mocks.projectsService = {} + mocks.projectsService.getProjectBySlug = sinon.stub() + mocks.projectsService.duplicate = sinon.stub() + + provide.value "tgProjectsService", mocks.projectsService + + _mockLocation = () -> + mocks.location = { + path: sinon.stub() + } + provide.value "$tgLocation", mocks.location + + _mockTgNav = () -> + mocks.urlservice = { + resolve: sinon.stub() + } + provide.value "$tgNavUrls", mocks.urlservice + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockCurrentUserService() + _mockProjectService() + _mockLocation() + _mockTgNav() + + return null + + beforeEach -> + module "taigaProjects" + + _mocks() + + inject ($controller) -> + controller = $controller + + ctrl = controller "DuplicateProjectCtrl" + + ctrl.projects = Immutable.fromJS([ + { + id: 1 + }, + { + id: 2 + } + ]) + + + + ctrl.canCreatePublicProjects = mocks.currentUserService.canCreatePublicProjects() + ctrl.canCreatePrivateProjects = mocks.currentUserService.canCreatePublicProjects() + ctrl.projectForm = {} + + it "toggle invited Member", () -> + ctrl = controller "DuplicateProjectCtrl" + + ctrl.invitedMembers = Immutable.List([1, 2, 3]) + ctrl.checkUsersLimit = sinon.spy() + + ctrl.toggleInvitedMember(2) + + expect(ctrl.invitedMembers.toJS()).to.be.eql([1, 3]) + + ctrl.toggleInvitedMember(5) + + expect(ctrl.invitedMembers.toJS()).to.be.eql([1, 3, 5]) + + expect(ctrl.checkUsersLimit).to.have.been.called + + it "get project to duplicate", () -> + project = Immutable.fromJS({ + members: [ + {id: 1}, + {id: 2}, + {id: 3} + ] + }) + + slug = 'slug' + ctrl._getInvitedMembers = sinon.stub() + + promise = mocks.projectsService.getProjectBySlug.withArgs(slug).promise().resolve(project) + + ctrl.refreshReferenceProject(slug).then () -> + expect(ctrl.referenceProject).to.be.equal(project) + expect(ctrl.members).to.be.equal(project.get('members')) + expect(ctrl.invitedMembers.toJS()).to.be.eql([1, 2, 3]) + + it 'check users limits', () -> + mocks.currentUserService.canAddMembersPrivateProject.withArgs(4).returns(1) + mocks.currentUserService.canAddMembersPublicProject.withArgs(4).returns(2) + + members = Immutable.fromJS([ + {id: 1}, + {id: 2}, + {id: 3} + ]) + size = members.size #3 + + ctrl.user = Immutable.fromJS({ + max_memberships_public_projects: 1 + max_memberships_private_projects: 1 + }) + + ctrl.projectForm = {} + ctrl.projectForm.is_private = false + ctrl.invitedMembers = members + + ctrl.checkUsersLimit() + expect(ctrl.limitMembersPrivateProject).to.be.equal(1) + expect(ctrl.limitMembersPublicProject).to.be.equal(2) + + it 'duplicate project', (done) -> + ctrl.referenceProject = Immutable.fromJS({ + id: 1 + }) + ctrl.projectForm = Immutable.fromJS({ + id: 1 + }) + projectId = ctrl.referenceProject.get('id') + data = ctrl.projectForm + + newProject = {} + newProject.data = { + slug: 'slug' + } + + mocks.urlservice.resolve.withArgs("project", {project: newProject.data.slug}).returns("/project/slug/") + + promise = mocks.projectsService.duplicate.withArgs(projectId, data).promise().resolve(newProject) + + ctrl.submit().then () -> + expect(ctrl.formSubmitLoading).to.be.false + expect(mocks.location.path).to.be.calledWith("/project/slug/") + expect(mocks.currentUserService.loadProjects).to.have.been.called + done() + + it 'check if the user can create a private projects', () -> + mocks.currentUserService.canCreatePrivateProjects = sinon.stub().returns({valid: true}) + + ctrl = controller "DuplicateProjectCtrl" + ctrl.limitMembersPrivateProject = {valid: true} + + ctrl.projectForm = { + is_private: true + } + + expect(ctrl.canCreateProject()).to.be.true + + mocks.currentUserService.canCreatePrivateProjects = sinon.stub().returns({valid: false}) + + ctrl = controller "DuplicateProjectCtrl" + + ctrl.projectForm = { + is_private: true + } + + expect(ctrl.canCreateProject()).to.be.false + + it 'check if the user can create a public projects', () -> + mocks.currentUserService.canCreatePublicProjects = sinon.stub().returns({valid: true}) + + ctrl = controller "DuplicateProjectCtrl" + ctrl.limitMembersPublicProject = {valid: true} + + ctrl.projectForm = { + is_private: false + } + + expect(ctrl.canCreateProject()).to.be.true + + mocks.currentUserService.canCreatePublicProjects = sinon.stub().returns({valid: false}) + + ctrl = controller "DuplicateProjectCtrl" + + ctrl.projectForm = { + is_private: false + } + + expect(ctrl.canCreateProject()).to.be.false diff --git a/app/modules/projects/create/duplicate/duplicate-project.directive.coffee b/app/modules/projects/create/duplicate/duplicate-project.directive.coffee new file mode 100644 index 00000000..c19d2f59 --- /dev/null +++ b/app/modules/projects/create/duplicate/duplicate-project.directive.coffee @@ -0,0 +1,35 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: duplicate-project.directive.coffee +### + +DuplicateProjectDirective = () -> + + link = (scope, el, attr, ctrl) -> + + return { + link: link, + templateUrl:"projects/create/duplicate/duplicate-project.html", + controller: "DuplicateProjectCtrl", + controllerAs: "vm", + bindToController: true, + scope: {} + } + +DuplicateProjectDirective.$inject = [] + +angular.module("taigaProjects").directive("tgDuplicateProject", DuplicateProjectDirective) diff --git a/app/modules/projects/create/duplicate/duplicate-project.jade b/app/modules/projects/create/duplicate/duplicate-project.jade new file mode 100644 index 00000000..d90643fb --- /dev/null +++ b/app/modules/projects/create/duplicate/duplicate-project.jade @@ -0,0 +1,57 @@ +.create-project + h1.create-project-title(translate="PROJECT.DUPLICATE.TITLE") + h3.create-project-description(translate="PROJECT.DUPLICATE.DESCRIPTION") + form.duplicate-project.e2e-duplicate-project( + name="projectForm" + ng-submit="vm.submit()" + ) + fieldset.duplicate-project-reference.e2e-duplicate-project-reference + select( + ng-model="vm.projectForm.project" + ng-change="vm.refreshReferenceProject(vm.projectForm.project)" + data-required="true" + ng-options="p.slug as p.name for p in vm.projects | toMutable| filter:{blocked_code: '!'}" + id="project-selector-dropdown" + autofocus + ) + option( + value="" + disabled + selected + translate="PROJECT.DUPLICATE.SELECT_PLACEHOLDER" + ) + + div(ng-include="'projects/create/import-project-form-common/name.html'") + div(ng-include="'projects/create/import-project-form-common/description.html'") + + div(ng-include="'projects/create/import-project-form-common/project-privacy.html'") + tg-create-project-restrictions( + is-private="vm.projectForm.is_private" + can-create-public-projects="vm.canCreatePublicProjects" + can-create-private-projects="vm.canCreatePrivateProjects" + ) + + + label(ng-if="vm.invitedMembers") + span(translate="PROJECT.CREATE.INVITE") + span.mumble( + ng-if="vm.displayUserWarning" + translate="PROJECT.CREATE.SOLO_PROJECT" + ) + span.mumble(translate="PROJECT.CREATE.INVITE_LATER") + + tg-invite-members( + ng-if="vm.members.size" + members="vm.members" + invited-members="vm.invitedMembers" + on-toggle-invited-member="vm.toggleInvitedMember(member)" + ) + + tg-create-project-members-restrictions( + ng-if="vm.referenceProject" + is-private="vm.projectForm.is_private" + limit-members-private-project="vm.limitMembersPrivateProject" + limit-members-public-project="vm.limitMembersPublicProject" + ) + + div(ng-include="'projects/create/import-project-form-common/actions.html'") diff --git a/app/modules/projects/create/duplicate/duplicate-project.scss b/app/modules/projects/create/duplicate/duplicate-project.scss new file mode 100644 index 00000000..63384b32 --- /dev/null +++ b/app/modules/projects/create/duplicate/duplicate-project.scss @@ -0,0 +1,5 @@ +.duplicate-project { + &-reference { + margin-bottom: 2rem; + } +} diff --git a/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.controller.coffee b/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.controller.coffee new file mode 100644 index 00000000..30256e31 --- /dev/null +++ b/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.controller.coffee @@ -0,0 +1,56 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: github-import-project-form.controller.coffee +### + +class GithubImportProjectFormController + @.$inject = [ + "tgCurrentUserService" + ] + + constructor: (@currentUserService) -> + @.canCreatePublicProjects = @currentUserService.canCreatePublicProjects() + @.canCreatePrivateProjects = @currentUserService.canCreatePrivateProjects() + + @.projectForm = @.project.toJS() + + @.platformName = "Github" + @.projectForm.is_private = false + @.projectForm.keepExternalReference = false + @.projectForm.project_type = "kanban" + + if !@.canCreatePublicProjects.valid && @.canCreatePrivateProjects.valid + @.projectForm.is_private = true + + checkUsersLimit: () -> + @.limitMembersPrivateProject = @currentUserService.canAddMembersPrivateProject(@.members.size) + @.limitMembersPublicProject = @currentUserService.canAddMembersPublicProject(@.members.size) + + saveForm: () -> + @.onSaveProjectDetails({project: Immutable.fromJS(@.projectForm)}) + + canCreateProject: () -> + if @.projectForm.is_private + return @.canCreatePrivateProjects.valid + else + return @.canCreatePublicProjects.valid + + isDisabled: () -> + return !@.canCreateProject() + + +angular.module('taigaProjects').controller('GithubImportProjectFormCtrl', GithubImportProjectFormController) diff --git a/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.directive.coffee b/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.directive.coffee new file mode 100644 index 00000000..2761e649 --- /dev/null +++ b/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.directive.coffee @@ -0,0 +1,40 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: github-import-project-form.directive.coffee +### + +GithubImportProjectFormDirective = () -> + return { + link: (scope, elm, attr, ctrl) -> + scope.$watch('vm.members', ctrl.checkUsersLimit.bind(ctrl)) + + templateUrl:"projects/create/github-import/github-import-project-form/github-import-project-form.html", + controller: "GithubImportProjectFormCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + members: '<', + project: '<', + onSaveProjectDetails: '&', + onCancelForm: '&', + fetchingUsers: '<' + } + } + +GithubImportProjectFormDirective.$inject = [] + +angular.module("taigaProjects").directive("tgGithubImportProjectForm", GithubImportProjectFormDirective) diff --git a/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.jade b/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.jade new file mode 100644 index 00000000..936cb685 --- /dev/null +++ b/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.jade @@ -0,0 +1,71 @@ +.import-project-github-form + div(ng-include="'projects/create/import/import-header.html'") + + .spin(tg-loading="vm.fetchingUsers") + + form( + ng-if="!vm.fetchingUsers", + name="projectForm", + ng-submit="vm.saveForm()" + ) + div(ng-include="'projects/create/import-project-form-common/name.html'") + div(ng-include="'projects/create/import-project-form-common/description.html'") + + .create-project-github-import-type(role="group") + p.question(translate="PROJECT.IMPORT.GITHUB.HOW_DO_YOU_WANT_TO_IMPORT") + .create-project-github-import-type-question + fieldset + input( + type="radio" + name="project_type" + id="template-issues" + data-required="true" + aria-hidden="true" + ng-value="'issues'" + ng-model="vm.projectForm.project_type" + required + ) + label(for="template-issues") + span.create-project-github-import-type-name(translate="PROJECT.IMPORT.GITHUB.ISSUES_PROJECT") + p.create-project-github-import-type-description(translate="PROJECT.IMPORT.GITHUB.ISSUES_PROJECT_DESCRIPTION") + fieldset + input( + type="radio" + name="project_type" + id="template-scrum" + data-required="true" + aria-hidden="true" + ng-value="'scrum'" + ng-model="vm.projectForm.project_type" + required + ) + label(for="template-scrum") + span.create-project-github-import-type-name(translate="PROJECT.IMPORT.GITHUB.SCRUM_PROJECT") + p.create-project-github-import-type-description(translate="PROJECT.IMPORT.GITHUB.SCRUM_PROJECT_DESCRIPTION") + fieldset + input( + type="radio" + name="project_type" + id="template-kanban" + data-required="true" + aria-hidden="true" + ng-value="'kanban'" + ng-model="vm.projectForm.project_type" + required + ) + label(for="template-kanban") + span.create-project-github-import-type-name(translate="PROJECT.IMPORT.GITHUB.KANBAN_PROJECT") + p.create-project-github-import-type-description(translate="PROJECT.IMPORT.GITHUB.KANBAN_PROJECT_DESCRIPTION") + div(ng-include="'projects/create/import-project-form-common/project-privacy.html'") + tg-create-project-restrictions( + is-private="vm.projectForm.is_private" + can-create-public-projects="vm.canCreatePublicProjects" + can-create-private-projects="vm.canCreatePrivateProjects" + ) + tg-create-project-members-restrictions( + is-private="vm.projectForm.is_private" + limit-members-private-project="vm.limitMembersPrivateProject" + limit-members-public-project="vm.limitMembersPublicProject" + ) + div(ng-include="'projects/create/import-project-form-common/links.html'") + div(ng-include="'projects/create/import-project-form-common/actions.html'") diff --git a/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.scss b/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.scss new file mode 100644 index 00000000..fe6578ec --- /dev/null +++ b/app/modules/projects/create/github-import/github-import-project-form/github-import-project-form.scss @@ -0,0 +1,61 @@ +.import-project-github-form { + @include create-project; +} + +.create-project-github-import-type { + margin-bottom: 1rem; + text-align: center; + + p { + margin-bottom: .5rem; + } + + &-question { + align-content: stretch; + align-items: stretch; + display: flex; + } + + fieldset { + background: $white; + border-right: 1px solid $whitish; + transition: background .2s linear; + + &:last-child { + border: 0; + } + } + + input { + display: none; + + &:checked { + +label { + background: rgba($primary, .1); + } + } + } + + label { + background: $white; + height: 100%; + padding: 1rem; + transition: background .2s ease-in; + + &:hover { + background: rgba($primary, .1); + cursor: pointer; + } + } + + &-name { + @include font-type(normal); + display: inline-block; + margin-bottom: .5rem; + } + + &-description { + @include font-type(light); + @include font-size(small); + } +} diff --git a/app/modules/projects/create/github-import/github-import.controller.coffee b/app/modules/projects/create/github-import/github-import.controller.coffee new file mode 100644 index 00000000..d4053093 --- /dev/null +++ b/app/modules/projects/create/github-import/github-import.controller.coffee @@ -0,0 +1,74 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: github-import.controller.coffee +### + +class GithubImportController + @.$inject = [ + 'tgGithubImportService', + '$tgConfirm', + '$translate', + 'tgImportProjectService', + ] + + constructor: (@githubImportService, @confirm, @translate, @importProjectService) -> + @.step = 'autorization-github' + @.project = null + + taiga.defineImmutableProperty @, 'projects', () => return @githubImportService.projects + taiga.defineImmutableProperty @, 'members', () => return @githubImportService.projectUsers + + startProjectSelector: () -> + @.step = 'project-select-github' + @githubImportService.fetchProjects() + + onSelectProject: (project) -> + @.step = 'project-form-github' + @.project = project + @.fetchingUsers = true + + @githubImportService.fetchUsers(@.project.get('id')).then () => @.fetchingUsers = false + + onSaveProjectDetails: (project) -> + @.project = project + @.step = 'project-members-github' + + onCancelMemberSelection: () -> + @.step = 'project-form-github' + + startImport: (users) -> + loader = @confirm.loader(@translate.instant('PROJECT.IMPORT.IN_PROGRESS.TITLE'), @translate.instant('PROJECT.IMPORT.IN_PROGRESS.DESCRIPTION'), true) + + loader.start() + + promise = @githubImportService.importProject( + @.project.get('name'), + @.project.get('description'), + @.project.get('id'), + users, + @.project.get('keepExternalReference'), + @.project.get('is_private') + @.project.get('project_type') + ) + + @importProjectService.importPromise(promise).then () => loader.stop() + + submitUserSelection: (users) -> + @.startImport(users) + return null + +angular.module('taigaProjects').controller('GithubImportCtrl', GithubImportController) diff --git a/app/modules/projects/create/github-import/github-import.controller.spec.coffee b/app/modules/projects/create/github-import/github-import.controller.spec.coffee new file mode 100644 index 00000000..5a975f7f --- /dev/null +++ b/app/modules/projects/create/github-import/github-import.controller.spec.coffee @@ -0,0 +1,172 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: github-import.controller.spec.coffee +### + +describe "GithubImportCtrl", -> + $provide = null + $controller = null + mocks = {} + + _mockCurrentUserService = -> + mocks.currentUserService = { + canAddMembersPrivateProject: sinon.stub() + canAddMembersPublicProject: sinon.stub() + } + + $provide.value("tgCurrentUserService", mocks.currentUserService) + + _mockImportProjectService = -> + mocks.importProjectService = { + importPromise: sinon.stub() + } + + $provide.value("tgImportProjectService", mocks.importProjectService) + + _mockGithubImportService = -> + mocks.githubService = { + fetchProjects: sinon.stub(), + fetchUsers: sinon.stub(), + importProject: sinon.stub() + } + + $provide.value("tgGithubImportService", mocks.githubService) + + _mockConfirm = -> + mocks.confirm = { + loader: sinon.stub() + } + + $provide.value("$tgConfirm", mocks.confirm) + + _mockTranslate = -> + mocks.translate = { + instant: sinon.stub() + } + + $provide.value("$translate", mocks.translate) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockGithubImportService() + _mockConfirm() + _mockTranslate() + _mockImportProjectService() + _mockCurrentUserService() + + return null + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "start project selector", () -> + ctrl = $controller("GithubImportCtrl") + ctrl.startProjectSelector() + + expect(ctrl.step).to.be.equal('project-select-github') + expect(mocks.githubService.fetchProjects).have.been.called + + it "on select project reload projects", (done) -> + project = Immutable.fromJS({ + id: 1, + name: "project-name" + }) + + mocks.githubService.fetchUsers.promise().resolve() + + ctrl = $controller("GithubImportCtrl") + + promise = ctrl.onSelectProject(project) + + expect(ctrl.fetchingUsers).to.be.true + + promise.then () -> + expect(ctrl.fetchingUsers).to.be.false + expect(ctrl.step).to.be.equal('project-form-github') + expect(ctrl.project).to.be.equal(project) + done() + + it "on save project details reload users", () -> + project = Immutable.fromJS({ + id: 1, + name: "project-name" + }) + + ctrl = $controller("GithubImportCtrl") + ctrl.onSaveProjectDetails(project) + + expect(ctrl.step).to.be.equal('project-members-github') + expect(ctrl.project).to.be.equal(project) + + it "on select user init import", (done) -> + users = Immutable.fromJS([ + { + id: 0 + }, + { + id: 1 + }, + { + id: 2 + } + ]) + + loaderObj = { + start: sinon.spy(), + update: sinon.stub(), + stop: sinon.spy() + } + + projectResult = { + id: 3, + name: "name" + } + + mocks.confirm.loader.returns(loaderObj) + + mocks.importProjectService.importPromise.promise().resolve() + + ctrl = $controller("GithubImportCtrl") + ctrl.project = Immutable.fromJS({ + id: 1, + name: 'project-name', + description: 'project-description', + keepExternalReference: false, + is_private: true + }) + + + mocks.githubService.importProject.promise().resolve(projectResult) + + ctrl.startImport(users).then () -> + expect(loaderObj.start).have.been.called + expect(loaderObj.stop).have.been.called + expect(mocks.githubService.importProject).have.been.calledWith('project-name', 'project-description', 1, users, false, true) + + done() diff --git a/app/modules/projects/create/github-import/github-import.directive.coffee b/app/modules/projects/create/github-import/github-import.directive.coffee new file mode 100644 index 00000000..bb02064f --- /dev/null +++ b/app/modules/projects/create/github-import/github-import.directive.coffee @@ -0,0 +1,35 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: github-import.directive.coffee +### + +GithubImportDirective = () -> + return { + link: (scope, elm, attrs, ctrl) -> + ctrl.startProjectSelector() + templateUrl:"projects/create/github-import/github-import.html", + controller: "GithubImportCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + onCancel: '&' + } + } + +GithubImportDirective.$inject = [] + +angular.module("taigaProjects").directive("tgGithubImport", GithubImportDirective) diff --git a/app/modules/projects/create/github-import/github-import.jade b/app/modules/projects/create/github-import/github-import.jade new file mode 100644 index 00000000..21bfee8b --- /dev/null +++ b/app/modules/projects/create/github-import/github-import.jade @@ -0,0 +1,31 @@ +.create-project.import-project(ng-if="vm.step == 'autorization-github'") + p autorization... + + +tg-import-project-selector( + logo="/#{v}/images/import-logos/github.png" + search="{{ 'PROJECT.IMPORT.GITHUB.CHOOSE_BOARD' | translate }}" + projects="vm.projects" + on-cancel="vm.onCancel()" + on-select-project="vm.onSelectProject(project)" + ng-if="vm.step == 'project-select-github'" +) + +tg-github-import-project-form( + ng-if="vm.step == 'project-form-github'" + project="vm.project" + members="vm.members" + fetching-users="vm.fetchingUsers" + on-save-project-details="vm.onSaveProjectDetails(project)" + on-cancel-form="vm.step = 'project-select-github'" +) + +tg-import-project-members( + ng-if="vm.step == 'project-members-github'" + platform="Github" + logo="/#{v}/images/import-logos/github.png" + project="vm.project" + members="vm.members" + on-submit="vm.submitUserSelection(users)" + on-cancel="vm.onCancelMemberSelection()" +) diff --git a/app/modules/projects/create/github-import/github-import.service.coffee b/app/modules/projects/create/github-import/github-import.service.coffee new file mode 100644 index 00000000..992c3a62 --- /dev/null +++ b/app/modules/projects/create/github-import/github-import.service.coffee @@ -0,0 +1,55 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: github-import.service.coffee +### + +class GithubImportService extends taiga.Service + @.$inject = [ + 'tgResources' + ] + + constructor: (@resources, @location) -> + @.projects = Immutable.List() + @.projectUsers = Immutable.List() + + setToken: (token) -> + @.token = token + + fetchProjects: () -> + @resources.githubImporter.listProjects(@.token).then (projects) => @.projects = projects + + fetchUsers: (projectId) -> + @resources.githubImporter.listUsers(@.token, projectId).then (users) => @.projectUsers = users + + importProject: (name, description, projectId, userBindings, keepExternalReference, isPrivate, projectType) -> + return @resources.githubImporter.importProject(@.token, name, description, projectId, userBindings, keepExternalReference, isPrivate, projectType) + + getAuthUrl: (callbackUri) -> + return new Promise (resolve) => + @resources.githubImporter.getAuthUrl(callbackUri).then (response) => + @.authUrl = response.data.url + resolve(@.authUrl) + + authorize: (code) -> + return new Promise (resolve, reject) => + @resources.githubImporter.authorize(code).then ((response) => + @.token = response.data.token + resolve(@.token) + ), (error) -> + reject(new Error(error.status)) + +angular.module("taigaProjects").service("tgGithubImportService", GithubImportService) diff --git a/app/modules/projects/create/github-import/github-import.service.spec.coffee b/app/modules/projects/create/github-import/github-import.service.spec.coffee new file mode 100644 index 00000000..be9732f5 --- /dev/null +++ b/app/modules/projects/create/github-import/github-import.service.spec.coffee @@ -0,0 +1,129 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: github-import.controller.spec.coffee +### + +describe "tgGithubImportService", -> + $provide = null + service = null + mocks = {} + + _mockResources = -> + mocks.resources = { + githubImporter: { + listProjects: sinon.stub(), + listUsers: sinon.stub(), + importProject: sinon.stub(), + getAuthUrl: sinon.stub(), + authorize: sinon.stub() + } + } + + $provide.value("tgResources", mocks.resources) + + _mockLocation = -> + mocks.location = { + search: sinon.stub() + } + + mocks.location.search.returns({ + token: 123 + }) + + $provide.value("$location", mocks.location) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockResources() + _mockLocation() + + return null + + _inject = -> + inject (_tgGithubImportService_) -> + service = _tgGithubImportService_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "fetch projects", (done) -> + service.setToken(123) + + mocks.resources.githubImporter.listProjects.withArgs(123).promise().resolve('projects') + + service.fetchProjects().then () -> + service.projects = "projects" + done() + + it "fetch user", (done) -> + service.setToken(123) + + projectId = 3 + mocks.resources.githubImporter.listUsers.withArgs(123, projectId).promise().resolve('users') + + service.fetchUsers(projectId).then () -> + service.projectUsers = 'users' + done() + + it "import project", () -> + service.setToken(123) + projectId = 2 + + service.importProject(projectId, true, true ,true) + + expect(mocks.resources.githubImporter.importProject).to.have.been.calledWith(123, projectId, true, true, true) + + it "get auth url", (done) -> + service.setToken(123) + projectId = 3 + + response = { + data: { + url: "url123" + } + } + + mocks.resources.githubImporter.getAuthUrl.promise().resolve(response) + + service.getAuthUrl().then (url) -> + expect(url).to.be.equal("url123") + done() + + it "authorize", (done) -> + service.setToken(123) + projectId = 3 + verifyCode = 12345 + + response = { + data: { + token: "token123" + } + } + + mocks.resources.githubImporter.authorize.withArgs(verifyCode).promise().resolve(response) + + service.authorize(verifyCode).then (token) -> + expect(token).to.be.equal("token123") + done() diff --git a/app/modules/projects/create/import-project-form-common/actions.jade b/app/modules/projects/create/import-project-form-common/actions.jade new file mode 100644 index 00000000..0ad2f57a --- /dev/null +++ b/app/modules/projects/create/import-project-form-common/actions.jade @@ -0,0 +1,13 @@ +.create-project-action + button.trans-button.create-project-action-cancel( + type="button" + ng-click="vm.onCancelForm()" + title="{{'PROJECT.CREATE.BACK' | translate}}" + translate="PROJECT.CREATE.BACK" + ) + button.button-green.create-project-action-submit.e2e-create-project-action-submit( + type="submit" + ng-disabled="projectForm.$invalid || vm.isDisabled()" + tg-loading="vm.formSubmitLoading" + translate="PROJECT.CREATE.TITLE" + ) diff --git a/app/modules/projects/create/import-project-form-common/description.jade b/app/modules/projects/create/import-project-form-common/description.jade new file mode 100644 index 00000000..72105418 --- /dev/null +++ b/app/modules/projects/create/import-project-form-common/description.jade @@ -0,0 +1,7 @@ +fieldset + textarea.e2e-create-project-description( + ng-model="vm.projectForm.description" + placeholder="{{'PROJECT.COMMON.PROJECT_DESCRIPTION' | translate}}" + data-required="true" + required + ) diff --git a/app/modules/projects/create/import-project-form-common/links.jade b/app/modules/projects/create/import-project-form-common/links.jade new file mode 100644 index 00000000..05e3f94c --- /dev/null +++ b/app/modules/projects/create/import-project-form-common/links.jade @@ -0,0 +1,20 @@ +fieldset.create-project-check + label(for="links") + span.title( + translate="PROJECT.IMPORT.LINKS" + translate-values="{platform: vm.platformName}" + ) + span.description( + translate="PROJECT.IMPORT.LINKS_DESCRIPTION" + translate-values="{platform: vm.platformName}" + ) + + .check + input.activate-input( + ng-model="vm.projectForm.keepExternalReference" + name="links" + type="checkbox" + ) + div + span.check-text.check-yes(translate="COMMON.YES") + span.check-text.check-no(translate="COMMON.NO") \ No newline at end of file diff --git a/app/modules/projects/create/import-project-form-common/name.jade b/app/modules/projects/create/import-project-form-common/name.jade new file mode 100644 index 00000000..8d329172 --- /dev/null +++ b/app/modules/projects/create/import-project-form-common/name.jade @@ -0,0 +1,13 @@ +fieldset + label( + translate="PROJECT.COMMON.DETAILS" + for="project-name" + ) + input.e2e-create-project-title( + type="text" + ng-model="vm.projectForm.name" + placeholder="{{'PROJECT.COMMON.PROJECT_TITLE' | translate}}" + name="project-name" + data-required="true" + required + ) \ No newline at end of file diff --git a/app/modules/projects/create/import-project-form-common/project-privacy.jade b/app/modules/projects/create/import-project-form-common/project-privacy.jade new file mode 100644 index 00000000..aebbd5f4 --- /dev/null +++ b/app/modules/projects/create/import-project-form-common/project-privacy.jade @@ -0,0 +1,30 @@ +.create-project-privacity(role="group") + fieldset + input( + type="radio" + name="is_private" + id="template-public" + data-required="true" + aria-hidden="true" + ng-value="false" + ng-model="vm.projectForm.is_private" + required + ng-checked="vm.canCreatePublicProjects" + ) + label(for="template-public") + tg-svg(svg-icon="icon-discover") + span(translate="PROJECT.CREATE.PUBLIC_PROJECT") + fieldset + input( + type="radio" + name="is_private" + id="template-private" + data-required="true" + ng-value="true" + ng-model="vm.projectForm.is_private" + aria-hidden="true" + required + ) + label(for="template-private") + tg-svg(svg-icon="icon-lock") + span(translate="PROJECT.CREATE.PRIVATE_PROJECT") diff --git a/app/modules/projects/create/import-project-members/import-project-members.controller.coffee b/app/modules/projects/create/import-project-members/import-project-members.controller.coffee new file mode 100644 index 00000000..77922f41 --- /dev/null +++ b/app/modules/projects/create/import-project-members/import-project-members.controller.coffee @@ -0,0 +1,150 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: import-project-members.controller.coffee +### + +class ImportProjectMembersController + @.$inject = [ + 'tgCurrentUserService', + 'tgUserService' + ] + + constructor: (@currentUserService, @userService) -> + @.selectImportUserLightbox = false + @.warningImportUsers = false + @.cancelledUsers = Immutable.List() + @.selectedUsers = Immutable.List() + @.selectableUsers = Immutable.List() + @.userContacts = Immutable.List() + + fetchUser: () -> + @.currentUser = @currentUserService.getUser() + + @userService.getContacts(@.currentUser.get('id')).then (userContacts) => + @.userContacts = userContacts + @.refreshSelectableUsers() + + searchUser: (user) -> + @.selectImportUserLightbox = true + @.searchingUser = user + + beforeSubmitUsers: () -> + if @.selectedUsers.size != @.members.size + @.warningImportUsers = true + else + @.submit() + + confirmUser: (externalUser, taigaUser) -> + @.selectImportUserLightbox = false + + user = Immutable.Map() + user = user.set('user', externalUser) + user = user.set('taigaUser', taigaUser) + + @.selectedUsers = @.selectedUsers.push(user) + @.discardSuggestedUser(externalUser) + + @.refreshSelectableUsers() + + unselectUser: (user) -> + index = @.selectedUsers.findIndex (it) -> it.getIn(['user', 'id']) == user.get('id') + + @.selectedUsers = @.selectedUsers.delete(index) + @.refreshSelectableUsers() + + discardSuggestedUser: (member) -> + @.cancelledUsers = @.cancelledUsers.push(member.get('id')) + + getSelectedMember: (member) -> + return @.selectedUsers.find (it) -> + return it.getIn(['user', 'id']) == member.get('id') + + isMemberSelected: (member) -> + return !!@.getSelectedMember(member) + + getUser: (user) -> + userSelected = @.getSelectedMember(user) + + if userSelected + return userSelected.get('taigaUser') + else + return null + + submit: () -> + @.warningImportUsers = false + + users = Immutable.Map() + + @.selectedUsers.map (it) -> + id = '' + + if _.isString(it.get('taigaUser')) + id = it.get('taigaUser') + else + id = it.getIn(['taigaUser', 'id']) + + users = users.set(it.getIn(['user', 'id']), id) + + @.onSubmit({users: users}) + + checkUsersLimit: () -> + @.limitMembersPrivateProject = @currentUserService.canAddMembersPrivateProject(@.members.size + 1) + @.limitMembersPublicProject = @currentUserService.canAddMembersPublicProject(@.members.size + 1) + + showSuggestedMatch: (member) -> + return member.get('user') && @.cancelledUsers.indexOf(member.get('id')) == -1 && !@.isMemberSelected(member) + + getDistinctSelectedTaigaUsers: () -> + ids = [] + + users = @.selectedUsers.filter (it) -> + id = it.getIn(['taigaUser', 'id']) + + if ids.indexOf(id) == -1 + ids.push(id) + return true + + return false + + return users.filter (it) => + return it.getIn(['taigaUser', 'id']) != @.currentUser.get('id') + + refreshSelectableUsers: () -> + @.importMoreUsersDisabled = @.isImportMoreUsersDisabled() + + if @.importMoreUsersDisabled + users = @.getDistinctSelectedTaigaUsers() + + @.selectableUsers = users.map (it) -> return it.get('taigaUser') + else + @.selectableUsers = @.userContacts + + @.selectableUsers = @.selectableUsers.push(@.currentUser) + + isImportMoreUsersDisabled: () -> + users = @.getDistinctSelectedTaigaUsers() + + # currentUser + newUser = +2 + total = users.size + 2 + + + if @.project.get('is_private') + return !@currentUserService.canAddMembersPrivateProject(total).valid + else + return !@currentUserService.canAddMembersPublicProject(total).valid + +angular.module('taigaProjects').controller('ImportProjectMembersCtrl', ImportProjectMembersController) diff --git a/app/modules/projects/create/import-project-members/import-project-members.controller.spec.coffee b/app/modules/projects/create/import-project-members/import-project-members.controller.spec.coffee new file mode 100644 index 00000000..88fe51b3 --- /dev/null +++ b/app/modules/projects/create/import-project-members/import-project-members.controller.spec.coffee @@ -0,0 +1,367 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: import.controller.spec.coffee +### + +describe "ImportProjectMembersCtrl", -> + $provide = null + $controller = null + mocks = {} + + _mockCurrentUserService = -> + mocks.currentUserService = { + getUser: sinon.stub().returns(Immutable.fromJS({ + id: 1 + })), + canAddMembersPrivateProject: sinon.stub(), + canAddMembersPublicProject: sinon.stub() + } + + $provide.value("tgCurrentUserService", mocks.currentUserService) + + _mockUserService = -> + mocks.userService = { + getContacts: sinon.stub() + } + + $provide.value("tgUserService", mocks.userService) + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockCurrentUserService() + _mockUserService() + + return null + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "fetch user info", (done) -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.refreshSelectableUsers = sinon.spy() + + mocks.userService.getContacts.withArgs(1).promise().resolve('contacts') + + ctrl.fetchUser().then () -> + expect(ctrl.userContacts).to.be.equal('contacts') + expect(ctrl.refreshSelectableUsers).have.been.called + done() + + it "search user", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + user = { + id: 1, + name: "username" + } + + ctrl.searchUser(user) + + expect(ctrl.selectImportUserLightbox).to.be.true + expect(ctrl.searchingUser).to.be.equal(user) + + it "prepare submit users, warning if needed", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + user = { + id: 1, + name: "username" + } + + ctrl.selectedUsers = Immutable.fromJS([ + {id: 1}, + {id: 2} + ]) + + ctrl.members = Immutable.fromJS([ + {id: 1} + ]) + + ctrl.beforeSubmitUsers() + + expect(ctrl.warningImportUsers).to.be.true + + it "prepare submit users, submit", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + user = { + id: 1, + name: "username" + } + + ctrl.selectedUsers = Immutable.fromJS([ + {id: 1} + ]) + + ctrl.members = Immutable.fromJS([ + {id: 1} + ]) + + + ctrl.submit = sinon.spy() + ctrl.beforeSubmitUsers() + + expect(ctrl.warningImportUsers).to.be.false + expect(ctrl.submit).have.been.called + + it "confirm user", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.discardSuggestedUser = sinon.spy() + ctrl.refreshSelectableUsers = sinon.spy() + + ctrl.confirmUser('user', 'taiga-user') + + expect(ctrl.selectedUsers.size).to.be.equal(1) + + expect(ctrl.selectedUsers.get(0).get('user')).to.be.equal('user') + expect(ctrl.selectedUsers.get(0).get('taigaUser')).to.be.equal('taiga-user') + expect(ctrl.discardSuggestedUser).have.been.called + expect(ctrl.refreshSelectableUsers).have.been.called + + it "discard suggested user", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.discardSuggestedUser(Immutable.fromJS({ + id: 3 + })) + + expect(ctrl.cancelledUsers.get(0)).to.be.equal(3) + + it "clean member selection", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.refreshSelectableUsers = sinon.spy() + + ctrl.selectedUsers = Immutable.fromJS([ + { + user: { + id: 1 + } + }, + { + user: { + id: 2 + } + } + ]) + + ctrl.unselectUser(Immutable.fromJS({ + id: 2 + })) + + expect(ctrl.selectedUsers.size).to.be.equal(1) + expect(ctrl.refreshSelectableUsers).have.been.called + + + it "get a selected member", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + member = Immutable.fromJS({ + id: 3 + }) + + ctrl.selectedUsers = ctrl.selectedUsers.push(Immutable.fromJS({ + user: { + id: 3 + } + })) + + user = ctrl.getSelectedMember(member) + + expect(user.getIn(['user', 'id'])).to.be.equal(3) + + it "submit", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.selectedUsers = ctrl.selectedUsers.push(Immutable.fromJS({ + user: { + id: 3 + }, + taigaUser: { + id: 2 + } + })) + + ctrl.selectedUsers = ctrl.selectedUsers.push(Immutable.fromJS({ + user: { + id: 3 + }, + taigaUser: "xx@yy.com" + })) + + + ctrl.onSubmit = sinon.stub() + + ctrl.submit() + + user = Immutable.Map() + user = user.set(3, 2) + + expect(ctrl.onSubmit).have.been.called + expect(ctrl.warningImportUsers).to.be.false + + it "show suggested match", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.isMemberSelected = sinon.stub().returns(false) + ctrl.cancelledUsers = [ + 3 + ] + + member = Immutable.fromJS({ + id: 1, + user: { + id: 10 + } + }) + + expect(ctrl.showSuggestedMatch(member)).to.be.true + + it "doesn't show suggested match", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.isMemberSelected = sinon.stub().returns(false) + ctrl.cancelledUsers = [ + 3 + ] + + member = Immutable.fromJS({ + id: 3, + user: { + id: 10 + } + }) + + expect(ctrl.showSuggestedMatch(member)).to.be.false + + it "check users limit", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.members = Immutable.fromJS([ + 1, 2, 3 + ]) + + mocks.currentUserService.canAddMembersPrivateProject.withArgs(4).returns('xx') + mocks.currentUserService.canAddMembersPublicProject.withArgs(4).returns('yy') + + ctrl.checkUsersLimit() + + expect(ctrl.limitMembersPrivateProject).to.be.equal('xx') + expect(ctrl.limitMembersPublicProject).to.be.equal('yy') + + + it "get distict select taiga users excluding the current user", () -> + ctrl = $controller("ImportProjectMembersCtrl") + ctrl.selectedUsers = Immutable.fromJS([ + { + taigaUser: { + id: 1 + } + }, + { + taigaUser: { + id: 1 + } + }, + { + taigaUser: { + id: 3 + } + }, + { + taigaUser: { + id: 5 + } + } + ]) + + ctrl.currentUser = Immutable.fromJS({ + id: 5 + }) + + users = ctrl.getDistinctSelectedTaigaUsers() + + expect(users.size).to.be.equal(2) + + it "refresh selectable users array with all users available", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.isImportMoreUsersDisabled = sinon.stub().returns(false) + + ctrl.userContacts = Immutable.fromJS([1]) + ctrl.currentUser = 2 + + ctrl.refreshSelectableUsers() + + expect(ctrl.selectableUsers.toJS()).to.be.eql([1, 2]) + + + it "refresh selectable users array with the selected ones", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.getDistinctSelectedTaigaUsers = sinon.stub().returns(Immutable.fromJS([ + {taigaUser: 1} + ])) + + ctrl.isImportMoreUsersDisabled = sinon.stub().returns(true) + + ctrl.userContacts = Immutable.fromJS([1]) + ctrl.currentUser = 2 + + ctrl.refreshSelectableUsers() + + expect(ctrl.selectableUsers.toJS()).to.be.eql([1, 2]) + + it "import more user disable in private project", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.project = Immutable.fromJS({ + is_private: true + }) + + ctrl.getDistinctSelectedTaigaUsers = sinon.stub().returns(Immutable.fromJS([1,2,3])) + + mocks.currentUserService.canAddMembersPrivateProject.withArgs(5).returns({valid: true}) + + expect(ctrl.isImportMoreUsersDisabled()).to.be.false + + it "import more user disable in public project", () -> + ctrl = $controller("ImportProjectMembersCtrl") + + ctrl.project = Immutable.fromJS({ + is_private: false + }) + + ctrl.getDistinctSelectedTaigaUsers = sinon.stub().returns(Immutable.fromJS([1,2,3])) + + mocks.currentUserService.canAddMembersPublicProject.withArgs(5).returns({valid: true}) + + expect(ctrl.isImportMoreUsersDisabled()).to.be.false diff --git a/app/modules/projects/create/import-project-members/import-project-members.directive.coffee b/app/modules/projects/create/import-project-members/import-project-members.directive.coffee new file mode 100644 index 00000000..122b4fb6 --- /dev/null +++ b/app/modules/projects/create/import-project-members/import-project-members.directive.coffee @@ -0,0 +1,43 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: import-project-form.directive.coffee +### + +ImportProjectMembersDirective = () -> + return { + link: (scope, elm, attr, ctrl) -> + ctrl.fetchUser() + + scope.$watch('vm.members', ctrl.checkUsersLimit.bind(ctrl)) + + templateUrl:"projects/create/import-project-members/import-project-members.html", + controller: "ImportProjectMembersCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + members: '<', + project: '<', + onSubmit: '&', + platform: '@', + logo: '@', + onCancel: '&' + } + } + +ImportProjectMembersDirective.$inject = [] + +angular.module("taigaProjects").directive("tgImportProjectMembers", ImportProjectMembersDirective) diff --git a/app/modules/projects/create/import-project-members/import-project-members.jade b/app/modules/projects/create/import-project-members/import-project-members.jade new file mode 100644 index 00000000..ccde76ac --- /dev/null +++ b/app/modules/projects/create/import-project-members/import-project-members.jade @@ -0,0 +1,85 @@ +.import-project-members + div(ng-include="'projects/create/import/import-header.html'") + + h2.import-project-members-title(translate="PROJECT.IMPORT.PROJECT_MEMBERS") + p( + translate="PROJECT.IMPORT.PROCESS_DESCRIPTION", + translate-values="{'platform': vm.platform}" + ) + + tg-create-project-members-restrictions( + is-private="vm.project.get('is_private')" + limit-members-private-project="vm.limitMembersPrivateProject" + limit-members-public-project="vm.limitMembersPublicProject" + ) + + .import-project-members-system(ng-if="vm.members.size") + .import-project-members-logo + img(ng-src="{{vm.logo}}") + .import-project-members-logo + img( + src="/#{v}/images/logo-color.png" + alt="Taiga Logo" + ) + + ul(ng-if="vm.members.size") + li.import-project-members-row(tg-repeat="member in vm.members track by member.get('id')") + .import-project-members-single + .avatar.empty(ng-if="!member.get('avatar')") {{member.get('full_name')[0].toUpperCase()}} + .avatar(ng-if="member.get('avatar')") + img(ng-src="{{member.get('avatar')}}") + span.import-project-members-username {{member.get('full_name') || member.get('username') }} + + .import-project-members-actions + .import-project-members-match(ng-if="vm.showSuggestedMatch(member)") + span( + translate="PROJECT.IMPORT.MATCH" + translate-values="{user_external:member.get('full_name'), user_internal: member.getIn(['user', 'full_name'])}" + ) + button.import-project-members-match-true(ng-click="vm.confirmUser(member, member.get('user'))") + tg-svg(svg-icon="icon-check-empty") + button.import-project-members-match-false(ng-click="vm.discardSuggestedUser(member)") + tg-svg(svg-icon="icon-close") + + .import-project-members-selected(ng-if="vm.getUser(member) && !vm.showSuggestedMatch(member)") + button.import-project-members-delete(ng-click="vm.unselectUser(member)") + tg-svg(svg-icon="icon-close") + span {{vm.getUser(member).get('full_name') || vm.getUser(member)}} + span.import-project-members-selected-img + img(tg-avatar="vm.getUser(member)") + + button.button.button-trans.import-project-members-choose.ng-animate-disabled( + ng-if="!vm.getUser(member) && !vm.showSuggestedMatch(member)" + ng-click="vm.searchUser(member)" + translate="PROJECT.IMPORT.CHOOSE" + ) + + .create-project-action + button.trans-button.create-project-action-cancel( + type="button" + ng-click="vm.onCancel()" + title="{{'PROJECT.CREATE.BACK' | translate}}" + translate="PROJECT.CREATE.BACK" + ) + button.button.button-green.create-project-action-submit( + ng-if="vm.members.size > 0" + ng-click="vm.beforeSubmitUsers()" + translate="PROJECT.IMPORT.IMPORT" + ) + + tg-select-import-user-lightbox.lightbox( + is-private="vm.project.get('is_private')" + limit-members-private-project="vm.limitMembersPrivateProject" + limit-members-public-project="vm.limitMembersPublicProject" + visible="vm.selectImportUserLightbox" + user="vm.searchingUser" + selectable-users="vm.selectableUsers" + on-close="vm.selectImportUserLightbox = false" + on-select-user="vm.confirmUser(user, taigaUser)" + ) + + tg-warning-user-import-lightbox.lightbox( + visible="vm.warningImportUsers" + on-confirm="vm.submit()" + on-close="vm.warningImportUsers = false" + ) diff --git a/app/modules/projects/create/import-project-members/import-project-members.scss b/app/modules/projects/create/import-project-members/import-project-members.scss new file mode 100644 index 00000000..a343b3ea --- /dev/null +++ b/app/modules/projects/create/import-project-members/import-project-members.scss @@ -0,0 +1,3 @@ +.import-project-members { + @include import-members; +} diff --git a/app/modules/projects/create/import-project-selector/import-project-selector.controller.coffee b/app/modules/projects/create/import-project-selector/import-project-selector.controller.coffee new file mode 100644 index 00000000..28628852 --- /dev/null +++ b/app/modules/projects/create/import-project-selector/import-project-selector.controller.coffee @@ -0,0 +1,24 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: import-project-selector.controller.coffee +### + +class ImportProjectSelectorController + selectProject: (project) -> + @.onSelectProject({project: Immutable.fromJS(project)}) + +angular.module('taigaProjects').controller('ImportProjectSelectorCtrl', ImportProjectSelectorController) diff --git a/app/modules/projects/create/import-project-selector/import-project-selector.directive.coffee b/app/modules/projects/create/import-project-selector/import-project-selector.directive.coffee new file mode 100644 index 00000000..c13dadb5 --- /dev/null +++ b/app/modules/projects/create/import-project-selector/import-project-selector.directive.coffee @@ -0,0 +1,36 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: import-project-selector.directive.coffee +### + +ImportProjectSelectorDirective = () -> + return { + templateUrl:"projects/create/import-project-selector/import-project-selector.html", + controller: "ImportProjectSelectorCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + projects: '<', + onCancel: '&', + onSelectProject: '&', + platfrom: '@', + logo: '@', + search: '@' + } + } + +angular.module("taigaProjects").directive("tgImportProjectSelector", ImportProjectSelectorDirective) diff --git a/app/modules/projects/create/import-project-selector/import-project-selector.jade b/app/modules/projects/create/import-project-selector/import-project-selector.jade new file mode 100644 index 00000000..5a6d0baf --- /dev/null +++ b/app/modules/projects/create/import-project-selector/import-project-selector.jade @@ -0,0 +1,34 @@ +.import-project-selector + div(ng-include="'projects/create/import/import-header.html'") + + .import-project-selector + + .import-project-selector-service + img( + ng-src="{{vm.logo}}" + ) + + .import-project-selector-boards + form.import-project-selector-filter(ng-if="vm.projects.size > 5") + input( + ng-model="vm.searchText" + placeholder="{{ vm.search }}" + ) + tg-svg( + svg-icon="icon-search" + title="{{'SEARCH.TITLE_ACTION_SEARCH' | translate}}" + ) + + ul.import-project-board-list + li.import-project-selector-title( + ng-repeat="project in vm.projects | toMutable | orderBy:'name' | filter:vm.searchText track by project.id" + ng-click="vm.selectProject(project)" + ) {{project.name}} + + .create-project-action + button.trans-button.create-project-action-cancel( + type="button" + ng-click="vm.onCancel()" + title="{{'COMMON.CANCEL' | translate}}" + translate="COMMON.CANCEL" + ) diff --git a/app/modules/projects/create/import-project-selector/import-project-selector.scss b/app/modules/projects/create/import-project-selector/import-project-selector.scss new file mode 100644 index 00000000..ac0fbf17 --- /dev/null +++ b/app/modules/projects/create/import-project-selector/import-project-selector.scss @@ -0,0 +1,3 @@ +.import-project-selector { + @include import-project-selector; +} diff --git a/app/modules/projects/create/import-taiga/import-taiga.controller.coffee b/app/modules/projects/create/import-taiga/import-taiga.controller.coffee new file mode 100644 index 00000000..47b0a84a --- /dev/null +++ b/app/modules/projects/create/import-taiga/import-taiga.controller.coffee @@ -0,0 +1,43 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: import-project.controller.coffee +### + +class ImportTaigaController + @.$inject = [ + '$tgConfirm', + '$tgResources', + 'tgImportProjectService', + '$translate' + ] + + constructor: (@confirm, @rs, @importProjectService, @translate) -> + + importTaiga: (files) -> + file = files[0] + + loader = @confirm.loader(@translate.instant('PROJECT.IMPORT.IN_PROGRESS.TITLE'), @translate.instant('PROJECT.IMPORT.IN_PROGRESS.DESCRIPTION'), true) + + loader.start() + + promise = @rs.projects.import(file, loader.update) + + @importProjectService.importPromise(promise).finally () => loader.stop() + + return + +angular.module("taigaProjects").controller("ImportTaigaCtrl", ImportTaigaController) diff --git a/app/modules/projects/create/import-taiga/import-taiga.directive.coffee b/app/modules/projects/create/import-taiga/import-taiga.directive.coffee new file mode 100644 index 00000000..fb6684d9 --- /dev/null +++ b/app/modules/projects/create/import-taiga/import-taiga.directive.coffee @@ -0,0 +1,29 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: import-taiga.directive.coffee +### + +ImportTaigaDirective = () -> + return { + templateUrl:"projects/create/import-taiga/import-taiga.html", + controller: "ImportTaigaCtrl", + controllerAs: "vm", + bindToController: true, + scope: {} + } + +angular.module("taigaProjects").directive("tgImportTaiga", ImportTaigaDirective) diff --git a/app/modules/projects/create/import-taiga/import-taiga.jade b/app/modules/projects/create/import-taiga/import-taiga.jade new file mode 100644 index 00000000..08c0fd20 --- /dev/null +++ b/app/modules/projects/create/import-taiga/import-taiga.jade @@ -0,0 +1,15 @@ +input.hidden( + tg-file-change="vm.importTaiga(files)" + type="file" +) +.import-project-logo + img( + src="/#{v}/images/logo-color.png" + alt="Taiga Logo" + ) +.import-project-name-wrapper + p.import-project-name( + href="#" + title="Taiga" + ) Taiga + p.import-project-description(translate="PROJECT.IMPORT.TAIGA.SELECTOR") diff --git a/app/modules/projects/create/import/import-header.jade b/app/modules/projects/create/import/import-header.jade new file mode 100644 index 00000000..0daaa50e --- /dev/null +++ b/app/modules/projects/create/import/import-header.jade @@ -0,0 +1,2 @@ +section.import-project-from + h1.create-project-title(translate="PROJECT.IMPORT.TITLE") diff --git a/app/modules/projects/create/import/import-project-error-lb.directive.coffee b/app/modules/projects/create/import/import-project-error-lb.directive.coffee new file mode 100644 index 00000000..9c03cc19 --- /dev/null +++ b/app/modules/projects/create/import/import-project-error-lb.directive.coffee @@ -0,0 +1,16 @@ +LbImportErrorDirective = (lightboxService) -> + link = (scope, el, attrs) -> + lightboxService.open(el) + + scope.close = () -> + lightboxService.close(el) + return + + return { + templateUrl: "projects/create/import/import-project-error-lb.html", + link: link + } + +LbImportErrorDirective.$inject = ["lightboxService"] + +angular.module("taigaProjects").directive("tgLbImportError", LbImportErrorDirective) diff --git a/app/partials/common/lightbox/lightbox-import-error.jade b/app/modules/projects/create/import/import-project-error-lb.jade similarity index 100% rename from app/partials/common/lightbox/lightbox-import-error.jade rename to app/modules/projects/create/import/import-project-error-lb.jade diff --git a/app/modules/projects/create/import/import-project.controller.coffee b/app/modules/projects/create/import/import-project.controller.coffee new file mode 100644 index 00000000..88bda138 --- /dev/null +++ b/app/modules/projects/create/import/import-project.controller.coffee @@ -0,0 +1,113 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: import-project.controller.coffee +### + +class ImportProjectController + @.$inject = [ + 'tgTrelloImportService', + 'tgJiraImportService', + 'tgGithubImportService', + 'tgAsanaImportService', + '$location', + '$window', + '$routeParams', + '$tgNavUrls', + '$tgConfig', + ] + + constructor: (@trelloService, @jiraService, @githubService, @asanaService, @location, @window, @routeParams, @tgNavUrls, @config) -> + + start: -> + @.token = null + @.from = @routeParams.platform + + locationSearch = @location.search() + + if @.from == "asana" + asanaOauthToken = locationSearch.code + if locationSearch.code + asanaOauthToken = locationSearch.code + + return @asanaService.authorize(asanaOauthToken).then ((token) => + @location.search({token: encodeURIComponent(JSON.stringify(token))}) + ), @.cancelCurrentImport.bind(this) + else + @.token = JSON.parse(decodeURIComponent(locationSearch.token)) + @asanaService.setToken(@.token) + + if @.from == 'trello' + if locationSearch.oauth_verifier + trelloOauthToken = locationSearch.oauth_verifier + return @trelloService.authorize(trelloOauthToken).then ((token) => + @location.search({token: token}) + ), @.cancelCurrentImport.bind(this) + else if locationSearch.token + @.token = locationSearch.token + @trelloService.setToken(locationSearch.token) + + if @.from == "github" + if locationSearch.code + githubOauthToken = locationSearch.code + + return @githubService.authorize(githubOauthToken).then ((token) => + @location.search({token: token}) + ), @.cancelCurrentImport.bind(this) + else if locationSearch.token + @.token = locationSearch.token + @githubService.setToken(locationSearch.token) + + if @.from == "jira" + jiraOauthToken = locationSearch.oauth_token + + if jiraOauthToken + return @jiraService.authorize().then ((data) => + @location.search({token: data.token, url: data.url}) + ), @.cancelCurrentImport.bind(this) + else + @.token = locationSearch.token + @jiraService.setToken(locationSearch.token, locationSearch.url) + + select: (from) -> + if from == "trello" + @trelloService.getAuthUrl().then (url) => + @window.open(url, "_self") + else if from == "jira" + @jiraService.getAuthUrl(@.jiraUrl).then (url) => + @window.open(url, "_self") + else if from == "github" + callbackUri = @location.absUrl() + "/github" + @githubService.getAuthUrl(callbackUri).then (url) => + @window.open(url, "_self") + else if from == "asana" + @asanaService.getAuthUrl().then (url) => + @window.open(url, "_self") + else + @.from = from + + unfoldOptions: (options) -> + @.unfoldedOptions = options + + isActiveImporter: (importer) -> + if @config.get('importers').indexOf(importer) == -1 + return false + return true + + cancelCurrentImport: () -> + @location.url(@tgNavUrls.resolve('create-project-import')) + +angular.module("taigaProjects").controller("ImportProjectCtrl", ImportProjectController) diff --git a/app/modules/projects/create/import/import-project.controller.spec.coffee b/app/modules/projects/create/import/import-project.controller.spec.coffee new file mode 100644 index 00000000..670b6484 --- /dev/null +++ b/app/modules/projects/create/import/import-project.controller.spec.coffee @@ -0,0 +1,183 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: import-project.controller.spec.coffee +### + +describe "ImportProjectCtrl", -> + $provide = null + $controller = null + mocks = {} + + _mockConfig = -> + mocks.config = Immutable.fromJS({ + importers: ['trello', 'github', 'jira', 'asana'] + }) + + $provide.value("$tgConfig", mocks.config) + + _mockTrelloImportService = -> + mocks.trelloService = { + authorize: sinon.stub(), + getAuthUrl: sinon.stub() + } + + $provide.value("tgTrelloImportService", mocks.trelloService) + + _mockJiraImportService = -> + mocks.jiraService = { + authorize: sinon.stub(), + getAuthUrl: sinon.stub() + } + + $provide.value("tgJiraImportService", mocks.jiraService) + + _mockGithubImportService = -> + mocks.githubService = { + authorize: sinon.stub(), + getAuthUrl: sinon.stub() + } + + $provide.value("tgGithubImportService", mocks.githubService) + + _mockAsanaImportService = -> + mocks.asanaService = { + authorize: sinon.stub(), + getAuthUrl: sinon.stub() + } + + $provide.value("tgAsanaImportService", mocks.asanaService) + + _mockWindow = -> + mocks.window = { + open: sinon.stub() + } + + $provide.value("$window", mocks.window) + + _mockLocation = -> + mocks.location = { + search: sinon.stub() + } + + $provide.value("$location", mocks.location) + + _mockRouteParams = -> + mocks.routeParams = { + platform: null + } + + $provide.value("$routeParams", mocks.routeParams) + + _mockTgNavUrls = -> + mocks.tgNavUrls = { + resolve: sinon.stub() + } + + $provide.value("$tgNavUrls", mocks.tgNavUrls) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockGithubImportService() + _mockTrelloImportService() + _mockJiraImportService() + _mockAsanaImportService() + _mockWindow() + _mockLocation() + _mockTgNavUrls() + _mockRouteParams() + _mockConfig() + + return null + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "initialize form with trello", (done) -> + searchResult = { + oauth_verifier: 123, + token: "token" + } + + mocks.location.search.returns(searchResult) + mocks.trelloService.authorize.withArgs(123).promise().resolve("token2") + + ctrl = $controller("ImportProjectCtrl") + + mocks.routeParams.platform = 'trello' + + ctrl.start().then () -> + expect(mocks.location.search).have.been.calledWith({token: "token2"}) + + done() + + it "initialize form with github", (done) -> + searchResult = { + code: 123, + token: "token", + from: "github" + } + + mocks.location.search.returns(searchResult) + mocks.githubService.authorize.withArgs(123).promise().resolve("token2") + + ctrl = $controller("ImportProjectCtrl") + + mocks.routeParams.platform = 'github' + + ctrl.start().then () -> + expect(mocks.location.search).have.been.calledWith({token: "token2"}) + + done() + + it "initialize form with asana", (done) -> + searchResult = { + code: 123, + token: encodeURIComponent("{\"token\": 222}") + from: "asana" + } + + mocks.location.search.returns(searchResult) + mocks.asanaService.authorize.withArgs(123).promise().resolve("token2") + + ctrl = $controller("ImportProjectCtrl") + + mocks.routeParams.platform = 'asana' + + ctrl.start().then () -> + expect(mocks.location.search).have.been.calledWith({token: encodeURIComponent(JSON.stringify("token2"))}) + + done() + + it "select trello import", () -> + ctrl = $controller("ImportProjectCtrl") + + mocks.trelloService.getAuthUrl.promise().resolve("url") + + ctrl.select("trello").then () -> + expect(mocks.window.open).have.been.calledWith("url", "_self") diff --git a/app/modules/projects/create/import/import-project.directive.coffee b/app/modules/projects/create/import/import-project.directive.coffee new file mode 100644 index 00000000..d68a463d --- /dev/null +++ b/app/modules/projects/create/import/import-project.directive.coffee @@ -0,0 +1,38 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: import-project.directive.coffee +### + +ImportProjectDirective = () -> + + link = (scope, el, attr, ctrl) -> + ctrl.start() + + return { + link: link, + templateUrl:"projects/create/import/import-project.html", + controller: "ImportProjectCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + onCancelImport: '&' + } + } + +ImportProjectDirective.$inject = [] + +angular.module("taigaProjects").directive("tgImportProject", ImportProjectDirective) diff --git a/app/modules/projects/create/import/import-project.jade b/app/modules/projects/create/import/import-project.jade new file mode 100644 index 00000000..a2014d45 --- /dev/null +++ b/app/modules/projects/create/import/import-project.jade @@ -0,0 +1,95 @@ +.create-project.import-project(ng-if="!vm.from") + div(ng-include="'projects/create/import/import-header.html'") + + ul.import-project-from + li.import-project-from-site( + tg-click-input-file, + tg-import-taiga + ) + li.import-project-from-site(ng-click="vm.unfoldOptions('jira')", ng-if="vm.isActiveImporter('jira')") + .import-project-logo + img( + src="/#{v}/images/import-logos/jira.png" + alt="Jira Logo" + ) + .import-project-name-wrapper + a.import-project-name( + href="#" + title="Jira" + ) Jira + p.import-project-description(translate="PROJECT.IMPORT.JIRA.SELECTOR") + fieldset.import-project-url(ng-if="vm.unfoldedOptions == 'jira'") + label( + for="jira-host" + translate="PROJECT.IMPORT.JIRA.URL" + ) + input.import-project-input( + ng-keyup="$event.keyCode == 13 && vm.select('jira')" + id="jira-host" + ng-model="vm.jiraUrl" + ) + button.button-green.import-project-button( + ng-click="vm.select('jira')" + title="{{'PROJECT.IMPORT.ACCEEDE' | translate}}" + translate="PROJECT.IMPORT.ACCEEDE" + ) + li.import-project-from-site(ng-click="vm.select('github')", ng-if="vm.isActiveImporter('github')") + .import-project-logo + img( + src="/#{v}/images/import-logos/github.png" + alt="Github Logo" + ) + .import-project-name-wrapper + a.import-project-name( + href="#" + title="Github" + ) Github + p.import-project-description(translate="PROJECT.IMPORT.GITHUB.SELECTOR") + li.import-project-from-site(ng-click="vm.select('trello')", ng-if="vm.isActiveImporter('trello')") + .import-project-logo + img( + src="/#{v}/images/import-logos/trello.png" + alt="Trello Logo" + ) + .import-project-name-wrapper + a.import-project-name( + href="#" + title="Trello" + ) Trello + p.import-project-description(translate="PROJECT.IMPORT.TRELLO.SELECTOR") + li.import-project-from-site(ng-click="vm.select('asana')", ng-if="vm.isActiveImporter('asana')") + .import-project-logo + img( + src="/#{v}/images/import-logos/asana.png" + alt="Asana Logo" + ) + .import-project-name-wrapper + a.import-project-name( + href="#" + title="Asana" + ) Asana + p.import-project-description(translate="PROJECT.IMPORT.ASANA.SELECTOR") + + .create-project-action + a.trans-button.create-project-action-back( + tg-nav="create-project", + title="{{'PROJECT.CREATE.BACK' | translate}}" + translate="PROJECT.CREATE.BACK" + ) + +tg-trello-import( + ng-if="vm.from == 'trello' && vm.token" + on-cancel="vm.cancelCurrentImport()" +) +tg-jira-import( + ng-if="vm.from == 'jira'" + on-cancel="vm.cancelCurrentImport()" +) +tg-github-import( + ng-if="vm.from == 'github' && vm.token" + on-cancel="vm.cancelCurrentImport()" +) +tg-asana-import( + ng-if="vm.from == 'asana' && vm.token" + on-cancel="vm.cancelCurrentImport()" +) diff --git a/app/modules/projects/create/import/import-project.scss b/app/modules/projects/create/import/import-project.scss new file mode 100644 index 00000000..2c84f33c --- /dev/null +++ b/app/modules/projects/create/import/import-project.scss @@ -0,0 +1,55 @@ +.import-project { + &-from-site { + align-items: center; + border-bottom: 1px solid $whitish; + color: $grayer; + cursor: pointer; + display: flex; + padding: 1rem; + position: relative; + + &:hover { + background: rgba($primary, .1); + transition: background .3s ease-in; + } + &:first-child { + border-top: 1px solid $whitish; + .import-project-name { + margin: 0; + } + .import-project-logo img { + padding: 0 .9rem 0 1rem; + width: 5.1rem; + } + } + } + &-logo { + align-self: flex-start; + margin-right: .5rem; + img { + padding: 0 1rem; + width: 5rem; + } + } + &-name-wrapper { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + } + &-description { + @include font-type(light); + margin-bottom: 0; + } + &-url { + margin-top: .5rem; + } + &-input { + vertical-align: middle; + } + &-button { + background: $primary; + color: $white; + padding: .25rem 1rem; + } +} diff --git a/app/modules/projects/create/import/import-project.service.coffee b/app/modules/projects/create/import/import-project.service.coffee new file mode 100644 index 00000000..4759fd57 --- /dev/null +++ b/app/modules/projects/create/import/import-project.service.coffee @@ -0,0 +1,121 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: import-project.service.coffee +### + +class ImportProjectService extends taiga.Service + @.$inject = [ + 'tgCurrentUserService', + '$tgAuth', + 'tgLightboxFactory', + '$translate', + '$tgConfirm', + '$location', + '$tgNavUrls' + ] + + constructor: (@currentUserService, @tgAuth, @lightboxFactory, @translate, @confirm, @location, @tgNavUrls) -> + + importPromise: (promise) -> + return promise.then(@.importSuccess.bind(this), @.importError.bind(this)) + + importSuccess: (result) -> + return @currentUserService.loadProjects().then () => + if result.status == 202 # Async mode + title = @translate.instant('PROJECT.IMPORT.ASYNC_IN_PROGRESS_TITLE') + message = @translate.instant('PROJECT.IMPORT.ASYNC_IN_PROGRESS_MESSAGE') + @confirm.success(title, message) + else # result.status == 201 # Sync mode + ctx = {project: result.data.slug} + @location.path(@tgNavUrls.resolve('project-admin-project-profile-details', ctx)) + msg = @translate.instant('PROJECT.IMPORT.SYNC_SUCCESS') + @confirm.notify('success', msg) + + importError: (result) -> + return @tgAuth.refresh().then () => + restrictionError = @.getRestrictionError(result) + + if restrictionError + @lightboxFactory.create('tg-lb-import-error', { + class: 'lightbox lightbox-import-error' + }, restrictionError) + + else + errorMsg = @translate.instant("PROJECT.IMPORT.ERROR") + + if result.status == 429 # TOO MANY REQUESTS + errorMsg = @translate.instant("PROJECT.IMPORT.ERROR_TOO_MANY_REQUEST") + else if result.data?._error_message + errorMsg = @translate.instant("PROJECT.IMPORT.ERROR_MESSAGE", {error_message: result.data._error_message}) + + @confirm.notify("error", errorMsg) + + getRestrictionError: (result) -> + if result.headers + errorKey = '' + + user = @currentUserService.getUser() + maxMemberships = null + + if result.headers.isPrivate + privateError = !@currentUserService.canCreatePrivateProjects().valid + + if user.get('max_memberships_private_projects') != null && result.headers.memberships >= user.get('max_memberships_private_projects') + membersError = true + else + membersError = false + + if privateError && membersError + errorKey = 'private-space-members' + maxMemberships = user.get('max_memberships_private_projects') + else if privateError + errorKey = 'private-space' + else if membersError + errorKey = 'private-members' + maxMemberships = user.get('max_memberships_private_projects') + + else + publicError = !@currentUserService.canCreatePublicProjects().valid + + if user.get('max_memberships_public_projects') != null && result.headers.memberships >= user.get('max_memberships_public_projects') + membersError = true + else + membersError = false + + if publicError && membersError + errorKey = 'public-space-members' + maxMemberships = user.get('max_memberships_public_projects') + else if publicError + errorKey = 'public-space' + else if membersError + errorKey = 'public-members' + maxMemberships = user.get('max_memberships_public_projects') + + if !errorKey + return false + + return { + key: errorKey, + values: { + max_memberships: maxMemberships, + members: result.headers.memberships + } + } + else + return false + +angular.module("taigaProjects").service("tgImportProjectService", ImportProjectService) diff --git a/app/modules/projects/create/import/import-project.service.spec.coffee b/app/modules/projects/create/import/import-project.service.spec.coffee new file mode 100644 index 00000000..78417d7d --- /dev/null +++ b/app/modules/projects/create/import/import-project.service.spec.coffee @@ -0,0 +1,294 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: import-project.service.spec.coffee +### + +describe "tgImportProjectService", -> + $provide = null + importProjectService = null + mocks = {} + + _mockCurrentUserService = -> + mocks.currentUserService = { + loadProjects: sinon.stub(), + getUser: sinon.stub(), + canCreatePrivateProjects: sinon.stub(), + canCreatePublicProjects: sinon.stub() + } + + $provide.value("tgCurrentUserService", mocks.currentUserService) + + _mockAuth = -> + mocks.auth = { + refresh: sinon.stub() + } + + $provide.value("$tgAuth", mocks.auth) + + _mockLightboxFactory = -> + mocks.lightboxFactory = { + create: sinon.stub() + } + + $provide.value("tgLightboxFactory", mocks.lightboxFactory) + + _mockTranslate = -> + mocks.translate = { + instant: sinon.stub() + } + + $provide.value("$translate", mocks.translate) + + _mockConfirm = -> + mocks.confirm = { + success: sinon.stub(), + notify: sinon.stub() + } + + $provide.value("$tgConfirm", mocks.confirm) + + _mockLocation = -> + mocks.location = { + path: sinon.stub() + } + + $provide.value("$location", mocks.location) + + _mockNavUrls = -> + mocks.navUrls = { + resolve: sinon.stub() + } + + $provide.value("$tgNavUrls", mocks.navUrls) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockCurrentUserService() + _mockAuth() + _mockLightboxFactory() + _mockTranslate() + _mockConfirm() + _mockLocation() + _mockNavUrls() + + return null + + _inject = -> + inject (_tgImportProjectService_) -> + importProjectService = _tgImportProjectService_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "import success async mode", (done) -> + result = { + status: 202, + data: { + slug: 'project-slug' + } + } + + mocks.translate.instant.returns('xxx') + + mocks.currentUserService.loadProjects.promise().resolve() + + importProjectService.importSuccess(result).then () -> + expect(mocks.confirm.success).have.been.calledOnce + done() + + it "import success sync mode", (done) -> + result = { + status: 201, + data: { + slug: 'project-slug' + } + } + + mocks.translate.instant.returns('msg') + + mocks.navUrls.resolve.withArgs('project-admin-project-profile-details', {project: 'project-slug'}).returns('url') + + mocks.currentUserService.loadProjects.promise().resolve() + + importProjectService.importSuccess(result).then () -> + expect(mocks.location.path).have.been.calledWith('url') + expect(mocks.confirm.notify).have.been.calledWith('success', 'msg') + done() + + it "private get restriction errors, private & member error", () -> + result = { + headers: { + isPrivate: true, + memberships: 10 + } + } + + mocks.currentUserService.getUser.returns(Immutable.fromJS({ + max_memberships_private_projects: 1 + })) + + mocks.currentUserService.canCreatePrivateProjects.returns({ + valid: false + }) + + error = importProjectService.getRestrictionError(result) + + expect(error).to.be.eql({ + key: 'private-space-members', + values: { + max_memberships: 1, + members: 10 + } + }) + + it "private get restriction errors, private limit error", () -> + result = { + headers: { + isPrivate: true, + memberships: 10 + } + } + + mocks.currentUserService.getUser.returns(Immutable.fromJS({ + max_memberships_private_projects: 20 + })) + + mocks.currentUserService.canCreatePrivateProjects.returns({ + valid: false + }) + + error = importProjectService.getRestrictionError(result) + + expect(error).to.be.eql({ + key: 'private-space', + values: { + max_memberships: null, + members: 10 + } + }) + + it "private get restriction errors, members error", () -> + result = { + headers: { + isPrivate: true, + memberships: 10 + } + } + + mocks.currentUserService.getUser.returns(Immutable.fromJS({ + max_memberships_private_projects: 1 + })) + + mocks.currentUserService.canCreatePrivateProjects.returns({ + valid: true + }) + + error = importProjectService.getRestrictionError(result) + + expect(error).to.be.eql({ + key: 'private-members', + values: { + max_memberships: 1, + members: 10 + } + }) + + it "public get restriction errors, public & member error", () -> + result = { + headers: { + isPrivate: false, + memberships: 10 + } + } + + mocks.currentUserService.getUser.returns(Immutable.fromJS({ + max_memberships_public_projects: 1 + })) + + mocks.currentUserService.canCreatePublicProjects.returns({ + valid: false + }) + + error = importProjectService.getRestrictionError(result) + + expect(error).to.be.eql({ + key: 'public-space-members', + values: { + max_memberships: 1, + members: 10 + } + }) + + it "public get restriction errors, public limit error", () -> + result = { + headers: { + isPrivate: false, + memberships: 10 + } + } + + mocks.currentUserService.getUser.returns(Immutable.fromJS({ + max_memberships_public_projects: 20 + })) + + mocks.currentUserService.canCreatePublicProjects.returns({ + valid: false + }) + + error = importProjectService.getRestrictionError(result) + + expect(error).to.be.eql({ + key: 'public-space', + values: { + max_memberships: null, + members: 10 + } + }) + + it "public get restriction errors, members error", () -> + result = { + headers: { + isPrivate: false, + memberships: 10 + } + } + + mocks.currentUserService.getUser.returns(Immutable.fromJS({ + max_memberships_public_projects: 1 + })) + + mocks.currentUserService.canCreatePublicProjects.returns({ + valid: true + }) + + error = importProjectService.getRestrictionError(result) + + expect(error).to.be.eql({ + key: 'public-members', + values: { + max_memberships: 1, + members: 10 + } + }) diff --git a/app/modules/projects/create/invite-members/invite-members.controller.coffee b/app/modules/projects/create/invite-members/invite-members.controller.coffee new file mode 100644 index 00000000..a1f61385 --- /dev/null +++ b/app/modules/projects/create/invite-members/invite-members.controller.coffee @@ -0,0 +1,26 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: invite-members.controller.coffee +### + +class InviteMembersController + @.$inject = [] + + isDisabled: (id) -> + return @.invitedMembers.indexOf(id) == -1 + +angular.module("taigaProjects").controller("InviteMembersCtrl", InviteMembersController) diff --git a/app/modules/projects/create/invite-members/invite-members.directive.coffee b/app/modules/projects/create/invite-members/invite-members.directive.coffee new file mode 100644 index 00000000..0867cab3 --- /dev/null +++ b/app/modules/projects/create/invite-members/invite-members.directive.coffee @@ -0,0 +1,38 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: invite-members.directive.coffee +### + +InviteMembersDirective = () -> + link = (scope, el, attr, ctrl) -> + + return { + link: link, + templateUrl:"projects/create/invite-members/invite-members.html", + controller: "InviteMembersCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + invitedMembers: '<', + members: '<', + onToggleInvitedMember: '&' + } + } + +InviteMembersDirective.$inject = [] + +angular.module("taigaProjects").directive("tgInviteMembers", InviteMembersDirective) diff --git a/app/modules/projects/create/invite-members/invite-members.jade b/app/modules/projects/create/invite-members/invite-members.jade new file mode 100644 index 00000000..eb5ec521 --- /dev/null +++ b/app/modules/projects/create/invite-members/invite-members.jade @@ -0,0 +1,8 @@ +fieldset.create-project-invite + .create-project-invite-avatars + tg-single-member( + tg-repeat="member in vm.members track by member.get('id')" + disabled="vm.isDisabled(member.get('id'))" + avatar="member" + ng-click="vm.onToggleInvitedMember({member: member.get('id')})" + ) diff --git a/app/modules/projects/create/invite-members/invite-members.scss b/app/modules/projects/create/invite-members/invite-members.scss new file mode 100644 index 00000000..567b51af --- /dev/null +++ b/app/modules/projects/create/invite-members/invite-members.scss @@ -0,0 +1,7 @@ +.create-project-invite { + &-avatars { + display: flex; + flex-wrap: wrap; + } +} + diff --git a/app/modules/projects/create/invite-members/single-member/single-member.directive.coffee b/app/modules/projects/create/invite-members/single-member/single-member.directive.coffee new file mode 100644 index 00000000..ce63d178 --- /dev/null +++ b/app/modules/projects/create/invite-members/single-member/single-member.directive.coffee @@ -0,0 +1,31 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: single-member.directive.coffee +### + +SingleMemberDirective = () -> + return { + templateUrl:"projects/create/invite-members/single-member/single-member.html", + scope: { + disabled: "<", + avatar: "=" + } + } + +SingleMemberDirective.$inject = [] + +angular.module("taigaProjects").directive("tgSingleMember", SingleMemberDirective) diff --git a/app/modules/projects/create/invite-members/single-member/single-member.jade b/app/modules/projects/create/invite-members/single-member/single-member.jade new file mode 100644 index 00000000..1974e4c1 --- /dev/null +++ b/app/modules/projects/create/invite-members/single-member/single-member.jade @@ -0,0 +1,6 @@ +label.create-project-invite-avatar(ng-class="{'disabled': disabled}") + img( + tg-avatar="avatar" + alt="{{avatar.get('full_name_display')}}" + title="{{avatar.get('full_name_display')}}" + ) diff --git a/app/modules/projects/create/invite-members/single-member/single-member.scss b/app/modules/projects/create/invite-members/single-member/single-member.scss new file mode 100644 index 00000000..d0d0c7ec --- /dev/null +++ b/app/modules/projects/create/invite-members/single-member/single-member.scss @@ -0,0 +1,40 @@ +.create-project-invite-avatar { + cursor: pointer; + display: block; + margin-right: .25rem; + &:hover { + @include empty-color(47); + border: 0; + opacity: .9; + transition: all .2s; + transition-delay: .2s; + } + &.disabled { + opacity: .3; + transition: opacity .2s; + &:hover { + @include empty-color(23); + border: 0; + opacity: .6; + transition: all .2s ease-in; + &::after { + background: $grayer; + left: 24px; + top: 8px; + transform: rotate(0); + transform-origin: center; + } + &::before { + background: $grayer; + right: 22px; + top: 8px; + transform: rotate(90deg); + transform-origin: center; + } + } + } + img { + cursor: pointer; + width: 3rem; + } +} diff --git a/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.controller.coffee b/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.controller.coffee new file mode 100644 index 00000000..2f99c010 --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.controller.coffee @@ -0,0 +1,58 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: jira-import-project-form.controller.coffee +### + +class JiraImportProjectFormController + @.$inject = [ + "tgCurrentUserService" + ] + + constructor: (@currentUserService) -> + @.canCreatePublicProjects = @currentUserService.canCreatePublicProjects() + @.canCreatePrivateProjects = @currentUserService.canCreatePrivateProjects() + + @.projectForm = @.project.toJS() + + @.projectForm.is_private = false + @.projectForm.keepExternalReference = false + if @.projectForm.importer_type == "agile" + @.projectForm.project_type = null + else + @.projectForm.project_type = "scrum" + @.projectForm.create_subissues = true + + if !@.canCreatePublicProjects.valid && @.canCreatePrivateProjects.valid + @.projectForm.is_private = true + + checkUsersLimit: () -> + @.limitMembersPrivateProject = @currentUserService.canAddMembersPrivateProject(@.members.size) + @.limitMembersPublicProject = @currentUserService.canAddMembersPublicProject(@.members.size) + + saveForm: () -> + @.onSaveProjectDetails({project: Immutable.fromJS(@.projectForm)}) + + canCreateProject: () -> + if @.projectForm.is_private + return @.canCreatePrivateProjects.valid + else + return @.canCreatePublicProjects.valid + + isDisabled: () -> + return !@.canCreateProject() + +angular.module('taigaProjects').controller('JiraImportProjectFormCtrl', JiraImportProjectFormController) diff --git a/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.directive.coffee b/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.directive.coffee new file mode 100644 index 00000000..72806f9e --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.directive.coffee @@ -0,0 +1,40 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: jira-import-project-form.directive.coffee +### + +JiraImportProjectFormDirective = () -> + return { + link: (scope, elm, attr, ctrl) -> + scope.$watch('vm.members', ctrl.checkUsersLimit.bind(ctrl)) + + templateUrl:"projects/create/jira-import/jira-import-project-form/jira-import-project-form.html", + controller: "JiraImportProjectFormCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + members: '<', + project: '<', + onSaveProjectDetails: '&', + onCancelForm: '&', + fetchingUsers: '<' + } + } + +JiraImportProjectFormDirective.$inject = [] + +angular.module("taigaProjects").directive("tgJiraImportProjectForm", JiraImportProjectFormDirective) diff --git a/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.jade b/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.jade new file mode 100644 index 00000000..864ba248 --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.jade @@ -0,0 +1,126 @@ +.import-project-jira-form + div(ng-include="'projects/create/import/import-header.html'") + + .spin(tg-loading="vm.fetchingUsers") + + form( + ng-if="!vm.fetchingUsers", + name="projectForm", + ng-submit="vm.saveForm()" + ) + div(ng-include="'projects/create/import-project-form-common/name.html'") + div(ng-include="'projects/create/import-project-form-common/description.html'") + + .create-project-import-type(role="group", ng-if="vm.projectForm.importer_type !== 'agile'") + fieldset + input( + type="radio" + name="project_type" + id="template-scrum" + data-required="true" + aria-hidden="true" + ng-value="'scrum'" + ng-model="vm.projectForm.project_type" + required + ) + label(for="template-scrum") + tg-svg(svg-icon="icon-scrum") + span(translate="PROJECT.IMPORT.JIRA.SCRUM_PROJECT") + fieldset + input( + type="radio" + name="project_type" + id="template-kanban" + data-required="true" + aria-hidden="true" + ng-value="'kanban'" + ng-model="vm.projectForm.project_type" + required + ) + label(for="template-kanban") + tg-svg(svg-icon="icon-kanban") + span(translate="PROJECT.IMPORT.JIRA.KANBAN_PROJECT") + fieldset + input( + type="radio" + name="project_type" + id="template-issues" + data-required="true" + aria-hidden="true" + ng-value="'issues'" + ng-model="vm.projectForm.project_type" + required + ) + label(for="template-issues") + tg-svg(svg-icon="icon-issues") + span(translate="PROJECT.IMPORT.JIRA.ISSUES_PROJECT") + + p.create-project-import-type-info( + ng-if="vm.projectForm.project_type == 'scrum'" + translate='PROJECT.IMPORT.JIRA.CREATE_AS_SCRUM_DESCRIPTION' + ) + p.create-project-import-type-info( + ng-if="vm.projectForm.project_type == 'kanban'" + translate='PROJECT.IMPORT.JIRA.CREATE_AS_KANBAN_DESCRIPTION' + ) + .create-project-type-issues-subform(ng-if="vm.projectForm.project_type == 'issues'") + p.create-project-type-issues-subform-title( + translate='PROJECT.IMPORT.JIRA.CREATE_AS_ISSUES_DESCRIPTION' + ) + fieldset.create-project-type-issues-subform-radiogr + label + input( + type="radio" + name="create_subissues" + id="template-issues-create" + data-required="true" + aria-hidden="true" + ng-value="true" + ng-model="vm.projectForm.create_subissues" + required + ) + svg( + viewBox="0 0 16 16" + xmlns="http://www.w3.org/2000/svg" + ) + circle( + cx="7" + cy="7" + r="6" + ) + span.control-indicator(translate="PROJECT.IMPORT.JIRA.CREATE_NEW_ISSUES") + label + input( + type="radio" + name="create_subissues" + id="template-issues-ignore" + data-required="true" + aria-hidden="true" + ng-value="false" + ng-model="vm.projectForm.create_subissues" + required + ) + svg( + viewBox="0 0 16 16" + xmlns="http://www.w3.org/2000/svg" + ) + circle( + cx="7" + cy="7" + r="6" + ) + span.control-indicator(translate="PROJECT.IMPORT.JIRA.NOT_CREATE_NEW_ISSUES") + + div(ng-include="'projects/create/import-project-form-common/project-privacy.html'") + tg-create-project-restrictions( + is-private="vm.projectForm.is_private" + can-create-public-projects="vm.canCreatePublicProjects" + can-create-private-projects="vm.canCreatePrivateProjects" + ) + tg-create-project-members-restrictions( + is-private="vm.projectForm.is_private" + limit-members-private-project="vm.limitMembersPrivateProject" + limit-members-public-project="vm.limitMembersPublicProject" + ) + div(ng-include="'projects/create/import-project-form-common/links.html'") + div(ng-include="'projects/create/import-project-form-common/actions.html'") diff --git a/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.scss b/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.scss new file mode 100644 index 00000000..639b5deb --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import-project-form/jira-import-project-form.scss @@ -0,0 +1,30 @@ +.import-project-jira-form { + @include create-project; +} + +.create-project-import-type-info { + @include font-size(small); + @include font-type(light); + margin-bottom: 1rem; +} + +.create-project-type-issues-subform { + margin: 1rem 0 2rem; + + &-title { + @include font-size(small); + @include font-type(bold); + } + + &-radiogr { + @include radio-group; + } +} + +.create-project-import-type { + margin-bottom: .25rem; + + fieldset { + margin: 0; + } +} diff --git a/app/modules/projects/create/jira-import/jira-import.controller.coffee b/app/modules/projects/create/jira-import/jira-import.controller.coffee new file mode 100644 index 00000000..ec9c0b20 --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import.controller.coffee @@ -0,0 +1,78 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: jira-import.controller.coffee +### + +class JiraImportController + @.$inject = [ + 'tgJiraImportService', + '$tgConfirm', + '$translate', + 'tgImportProjectService', + ] + + constructor: (@jiraImportService, @confirm, @translate, @importProjectService) -> + @.step = 'autorization-jira' + @.project = null + taiga.defineImmutableProperty @, 'projects', () => return @jiraImportService.projects + taiga.defineImmutableProperty @, 'members', () => return @jiraImportService.projectUsers + + startProjectSelector: () -> + @.step = 'project-select-jira' + @jiraImportService.fetchProjects() + + onSelectProject: (project) -> + @.step = 'project-form-jira' + @.project = project + @.fetchingUsers = true + + @jiraImportService.fetchUsers(@.project.get('id')).then () => @.fetchingUsers = false + + onSaveProjectDetails: (project) -> + @.project = project + @.step = 'project-members-jira' + + onCancelMemberSelection: () -> + @.step = 'project-form-jira' + + startImport: (users) -> + loader = @confirm.loader(@translate.instant('PROJECT.IMPORT.IN_PROGRESS.TITLE'), @translate.instant('PROJECT.IMPORT.IN_PROGRESS.DESCRIPTION'), true) + + loader.start() + + projectType = @.project.get('project_type') + if projectType == "issues" and @.project.get('create_subissues') + projectType = "issues-with-subissues" + + promise = @jiraImportService.importProject( + @.project.get('name'), + @.project.get('description'), + @.project.get('id'), + users, + @.project.get('keepExternalReference'), + @.project.get('is_private'), + projectType, + @.project.get('importer_type'), + ) + + @importProjectService.importPromise(promise).then () => loader.stop() + + submitUserSelection: (users) -> + @.startImport(users) + return null + +angular.module('taigaProjects').controller('JiraImportCtrl', JiraImportController) diff --git a/app/modules/projects/create/jira-import/jira-import.controller.spec.coffee b/app/modules/projects/create/jira-import/jira-import.controller.spec.coffee new file mode 100644 index 00000000..f4542dd8 --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import.controller.spec.coffee @@ -0,0 +1,171 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: jira-import.controller.spec.coffee +### + +describe "JiraImportCtrl", -> + $provide = null + $controller = null + mocks = {} + + _mockCurrentUserService = -> + mocks.currentUserService = { + canAddMembersPrivateProject: sinon.stub() + canAddMembersPublicProject: sinon.stub() + } + + $provide.value("tgCurrentUserService", mocks.currentUserService) + + _mockJiraImportService = -> + mocks.jiraService = { + fetchProjects: sinon.stub(), + fetchUsers: sinon.stub(), + importProject: sinon.stub() + } + + $provide.value("tgJiraImportService", mocks.jiraService) + + _mockImportProjectService = -> + mocks.importProjectService = { + importPromise: sinon.stub() + } + + $provide.value("tgImportProjectService", mocks.importProjectService) + + _mockConfirm = -> + mocks.confirm = { + loader: sinon.stub() + } + + $provide.value("$tgConfirm", mocks.confirm) + + _mockTranslate = -> + mocks.translate = { + instant: sinon.stub() + } + + $provide.value("$translate", mocks.translate) + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockJiraImportService() + _mockConfirm() + _mockTranslate() + _mockImportProjectService() + _mockCurrentUserService() + + return null + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "start project selector", () -> + ctrl = $controller("JiraImportCtrl") + ctrl.startProjectSelector() + + expect(ctrl.step).to.be.equal('project-select-jira') + expect(mocks.jiraService.fetchProjects).have.been.called + + it "on select project reload projects", (done) -> + project = Immutable.fromJS({ + id: 1, + name: "project-name" + }) + + mocks.jiraService.fetchUsers.promise().resolve() + + ctrl = $controller("JiraImportCtrl") + + promise = ctrl.onSelectProject(project) + + expect(ctrl.fetchingUsers).to.be.true + + promise.then () -> + expect(ctrl.fetchingUsers).to.be.false + expect(ctrl.step).to.be.equal('project-form-jira') + expect(ctrl.project).to.be.equal(project) + done() + + it "on save project details reload users", () -> + project = Immutable.fromJS({ + id: 1, + name: "project-name" + }) + + ctrl = $controller("JiraImportCtrl") + ctrl.onSaveProjectDetails(project) + + expect(ctrl.step).to.be.equal('project-members-jira') + expect(ctrl.project).to.be.equal(project) + + it "on select user init import", (done) -> + users = Immutable.fromJS([ + { + id: 0 + }, + { + id: 1 + }, + { + id: 2 + } + ]) + + loaderObj = { + start: sinon.spy(), + update: sinon.stub(), + stop: sinon.spy() + } + + projectResult = { + id: 3, + name: "name" + } + + mocks.confirm.loader.returns(loaderObj) + + mocks.importProjectService.importPromise.promise().resolve() + + ctrl = $controller("JiraImportCtrl") + ctrl.project = Immutable.fromJS({ + id: 1, + name: 'project-name', + description: 'project-description', + keepExternalReference: false, + is_private: true + }) + + + mocks.jiraService.importProject.promise().resolve(projectResult) + + ctrl.startImport(users).then () -> + expect(loaderObj.start).have.been.called + expect(loaderObj.stop).have.been.called + expect(mocks.jiraService.importProject).have.been.calledWith('project-name', 'project-description', 1, users, false, true) + + done() diff --git a/app/modules/projects/create/jira-import/jira-import.directive.coffee b/app/modules/projects/create/jira-import/jira-import.directive.coffee new file mode 100644 index 00000000..70860d9e --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import.directive.coffee @@ -0,0 +1,35 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: jira-import.directive.coffee +### + +JiraImportDirective = () -> + return { + link: (scope, elm, attrs, ctrl) -> + ctrl.startProjectSelector() + templateUrl:"projects/create/jira-import/jira-import.html", + controller: "JiraImportCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + onCancel: '&' + } + } + +JiraImportDirective.$inject = [] + +angular.module("taigaProjects").directive("tgJiraImport", JiraImportDirective) diff --git a/app/modules/projects/create/jira-import/jira-import.jade b/app/modules/projects/create/jira-import/jira-import.jade new file mode 100644 index 00000000..836171c6 --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import.jade @@ -0,0 +1,30 @@ +.create-project.import-project(ng-if="vm.step == 'autorization-jira'") + p autorization... + +tg-import-project-selector( + logo="/#{v}/images/import-logos/jira.png" + search="{{ 'PROJECT.IMPORT.JIRA.CHOOSE_PROJECT' | translate }}" + projects="vm.projects" + on-cancel="vm.onCancel()" + on-select-project="vm.onSelectProject(project)" + ng-if="vm.step == 'project-select-jira'" +) + +tg-jira-import-project-form( + ng-if="vm.step == 'project-form-jira'" + project="vm.project" + members="vm.members" + fetching-users="vm.fetchingUsers" + on-save-project-details="vm.onSaveProjectDetails(project)" + on-cancel-form="vm.step = 'project-select-jira'" +) + +tg-import-project-members( + ng-if="vm.step == 'project-members-jira'" + platform="Jira" + logo="/#{v}/images/import-logos/jira.png" + project="vm.project" + members="vm.members" + on-submit="vm.submitUserSelection(users)" + on-cancel="vm.onCancelMemberSelection()" +) diff --git a/app/modules/projects/create/jira-import/jira-import.service.coffee b/app/modules/projects/create/jira-import/jira-import.service.coffee new file mode 100644 index 00000000..57ed346b --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import.service.coffee @@ -0,0 +1,58 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: jira-import.service.coffee +### + +class JiraImportService extends taiga.Service + @.$inject = [ + 'tgResources', + '$location' + ] + + constructor: (@resources, @location) -> + @.projects = Immutable.List() + @.projectUsers = Immutable.List() + + setToken: (token, url) -> + @.token = token + @.url = url + + fetchProjects: () -> + @resources.jiraImporter.listProjects(@.url, @.token).then (projects) => @.projects = projects + + fetchUsers: (projectId) -> + @resources.jiraImporter.listUsers(@.url, @.token, projectId).then (users) => @.projectUsers = users + + importProject: (name, description, projectId, userBindings, keepExternalReference, isPrivate, projectType, importerType) -> + @resources.jiraImporter.importProject(@.url, @.token, name, description, projectId, userBindings, keepExternalReference, isPrivate, projectType, importerType) + + getAuthUrl: (url) -> + return new Promise (resolve) => + @resources.jiraImporter.getAuthUrl(url).then (response) => + @.authUrl = response.data.url + resolve(@.authUrl) + + authorize: () -> + return new Promise (resolve, reject) => + @resources.jiraImporter.authorize().then ((response) => + @.token = response.data.token + @.url = response.data.url + resolve(response.data) + ), (error) -> + reject(new Error(error.status)) + +angular.module("taigaProjects").service("tgJiraImportService", JiraImportService) diff --git a/app/modules/projects/create/jira-import/jira-import.service.spec.coffee b/app/modules/projects/create/jira-import/jira-import.service.spec.coffee new file mode 100644 index 00000000..3b5650d0 --- /dev/null +++ b/app/modules/projects/create/jira-import/jira-import.service.spec.coffee @@ -0,0 +1,129 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: jira-import.controller.spec.coffee +### + +describe "tgJiraImportService", -> + $provide = null + service = null + mocks = {} + + _mockResources = -> + mocks.resources = { + jiraImporter: { + listProjects: sinon.stub(), + listUsers: sinon.stub(), + importProject: sinon.stub(), + getAuthUrl: sinon.stub(), + authorize: sinon.stub() + } + } + + $provide.value("tgResources", mocks.resources) + + _mockLocation = -> + mocks.location = { + search: sinon.stub() + } + + mocks.location.search.returns({ + url: "http://test", + token: 123 + }) + + $provide.value("$location", mocks.location) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockResources() + _mockLocation() + + return null + + _inject = -> + inject (_tgJiraImportService_) -> + service = _tgJiraImportService_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "fetch projects", (done) -> + service.setToken(123, 'http://test') + mocks.resources.jiraImporter.listProjects.withArgs("http://test", 123).promise().resolve('projects') + + service.fetchProjects().then () -> + service.projects = "projects" + done() + + it "fetch user", (done) -> + service.setToken(123, 'http://test') + projectId = 3 + mocks.resources.jiraImporter.listUsers.withArgs("http://test", 123, projectId).promise().resolve('users') + + service.fetchUsers(projectId).then () -> + service.projectUsers = 'users' + done() + + it "import project", () -> + service.setToken(123, 'http://test') + service.url = 'url' + projectId = 2 + + service.importProject(projectId, true, true ,true) + + expect(mocks.resources.jiraImporter.importProject).to.have.been.calledWith('url', 123, projectId, true, true, true) + + it "get auth url", (done) -> + service.setToken(123, 'http://test') + projectId = 3 + + response = { + data: { + url: "url123" + } + } + + mocks.resources.jiraImporter.getAuthUrl.promise("http://test").resolve(response) + + service.getAuthUrl().then (url) -> + expect(url).to.be.equal("url123") + done() + + it "authorize", (done) -> + service.setToken(123, 'http://test') + projectId = 3 + + response = { + data: { + url: "http://test", + token: "token123" + } + } + + mocks.resources.jiraImporter.authorize.withArgs().promise().resolve(response) + + service.authorize().then (token) -> + expect(token).to.be.deep.equal({url: "http://test", token: "token123"}) + done() diff --git a/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.controller.coffee b/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.controller.coffee new file mode 100644 index 00000000..f151b175 --- /dev/null +++ b/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.controller.coffee @@ -0,0 +1,35 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: trello-import-project-members.controller.coffee +### + +class SelectImportUserLightboxCtrl + @.$inject = [] + + constructor: () -> + + start: () -> + @.mode = 'search' + @.invalid = false + + assignUser: () -> + @.onSelectUser({user: @.user, taigaUser: @.userEmail}) + + selectUser: (taigaUser) -> + @.onSelectUser({user: @.user, taigaUser: Immutable.fromJS(taigaUser)}) + +angular.module('taigaProjects').controller('SelectImportUserLightboxCtrl', SelectImportUserLightboxCtrl) diff --git a/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.directive.coffee b/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.directive.coffee new file mode 100644 index 00000000..342eceb4 --- /dev/null +++ b/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.directive.coffee @@ -0,0 +1,54 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: select-import-user-lightbox.directive.coffee +### + +SelectImportUserLightboxDirective = (lightboxService, lightboxKeyboardNavigationService) -> + link = (scope, el, attrs, ctrl) -> + pepe = () -> + scope.$watch 'vm.visible', (visible) -> + if visible && !el.hasClass('open') + ctrl.start() + lightboxService.open(el, null, scope.vm.onClose).then -> + el.find('input').focus() + lightboxKeyboardNavigationService.init(el) + else if !visible && el.hasClass('open') + lightboxService.close(el).then () -> + ctrl.userEmail = '' + ctrl.usersSearch = '' + + return { + controller: "SelectImportUserLightboxCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + user: '<', + visible: '<', + onClose: '&', + onSelectUser: '&', + selectableUsers: '<', + isPrivate: '=', + limitMembersPrivateProject: '=', + limitMembersPublicProject: '=' + }, + templateUrl: 'projects/create/select-import-user-lightbox/select-import-user-lightbox.html' + link: link + } + +SelectImportUserLightboxDirective.$inject = ['lightboxService', 'lightboxKeyboardNavigationService'] + +angular.module("taigaProjects").directive("tgSelectImportUserLightbox", SelectImportUserLightboxDirective) diff --git a/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.jade b/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.jade new file mode 100644 index 00000000..1030b1c9 --- /dev/null +++ b/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.jade @@ -0,0 +1,90 @@ +tg-lightbox-close(on-close="vm.onClose()") + +.form(ng-if="vm.visible") + .candidate-user + .avatar.empty(ng-if="!vm.user.get('avatar')") {{vm.user.get('full_name')[0].toUpperCase() || vm.user.get('username')[0].toUpperCase()}} + .avatar(ng-if="vm.user.get('avatar')") + img(ng-src="{{vm.user.get('avatar')}}") + p {{vm.user.get('full_name') || vm.user.get('username')}} + + h2.title(translate="PROJECT.IMPORT.WHO_IS") + + div.create-project-warning(ng-if="!vm.limitMembersPublicProject.valid && !vm.isPrivate") + tg-svg(svg-icon="icon-exclamation") + span( + translate="PROJECT.IMPORT.PROJECT_RESTRICTIONS.ACCOUNT_ALLOW_MEMBERS", + translate-values="{'members': vm.limitMembersPublicProject.max}" + ) + + div.create-project-warning(ng-if="!vm.limitMembersPrivateProject.valid && vm.isPrivate") + tg-svg(svg-icon="icon-exclamation") + span( + translate="PROJECT.IMPORT.PROJECT_RESTRICTIONS.ACCOUNT_ALLOW_MEMBERS", + translate-values="{'members': vm.limitMembersPrivateProject.max}" + ) + + form(ng-if="vm.mode == 'mail'", ng-submit="vm.assignUser()") + div.create-project-warning + tg-svg(svg-icon="icon-exclamation") + span(translate="PROJECT.IMPORT.WARNING_MAIL_USER") + + fieldset + label( + translate="PROJECT.IMPORT.WRITE_EMAIL_LABEL" + for="user-name" + ) + + .group + input( + name="user-name" + type="text", + data-maxlength="500", + ng-model="vm.userEmail" + ) + button.button-green.submit-button( + type="submit", + title="{{'PROJECT.IMPORT.ASSIGN' | translate}}", + translate="PROJECT.IMPORT.ASSIGN" + ) + + .search-user-mode + p + a( + href="" + ng-click="vm.mode = 'search'" + ) {{'PROJECT.IMPORT.SEARCH_CONTACT' | translate}} + + div(ng-if="vm.mode == 'search'") + fieldset + input( + type="text", + data-maxlength="500", + placeholder="{{'LIGHTBOX.ASSIGNED_TO.SEARCH' | translate}}", + ng-model="vm.usersSearch" + ) + + .assigned-to-list + .user-list-single( + ng-repeat="user in vm.selectableUsers | toMutable | filter: vm.usersSearch | orderBy:'full_name_display' | limitTo: 5 as filteredCollection", + ng-click="vm.selectUser(user)" + ) + .user-list-avatar + a( + href="#" + title="{{'COMMON.ASSIGNED_TO.TITLE' | translate}}" + ) + img(tg-avatar="user") + a.user-list-name( + href="" + title="{{user.full_name_display || user.full_name}}" + ) {{user.full_name_display || user.full_name}} + + .more-users(ng-if="filteredCollection.length >= 5") + span(translate="COMMON.ASSIGNED_TO.TOO_MANY") + + .search-user-mode + p + a( + href="" + ng-click="vm.mode = 'mail'" + ) {{'PROJECT.IMPORT.WRITE_EMAIL' | translate}} diff --git a/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.scss b/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.scss new file mode 100644 index 00000000..09c29ed2 --- /dev/null +++ b/app/modules/projects/create/select-import-user-lightbox/select-import-user-lightbox.scss @@ -0,0 +1,54 @@ +tg-select-import-user-lightbox { + .form { + flex-basis: 600px; + flex-grow: 0; + width: 600px; + } + .candidate-user { + align-items: center; + display: flex; + justify-content: center; + padding-bottom: 1.5rem; + p { + margin-bottom: 0; + } + .empty { + margin-right: .5rem; + } + .user-list-avatar { + background-color: $red; + height: 32px; + margin-right: .5rem; + width: 32px; + } + } + .error { + color: $red-light; + text-align: center; + } + .more-users { + padding: .5rem; + text-align: center; + } + .group { + display: flex; + input { + flex-grow: 2; + margin-right: .5rem; + } + .submit-button { + flex-grow: 0; + width: auto; + } + } + .search-user-mode { + border-top: 1px solid $gray-light; + margin-top: 1rem; + padding-top: 1rem; + text-align: center; + } + label { + display: block; + padding-bottom: .5rem; + } +} diff --git a/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.controller.coffee b/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.controller.coffee new file mode 100644 index 00000000..aba397cd --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.controller.coffee @@ -0,0 +1,54 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: trello-import-project-form.controller.coffee +### + +class TrelloImportProjectFormController + @.$inject = [ + "tgCurrentUserService" + ] + + constructor: (@currentUserService) -> + @.canCreatePublicProjects = @currentUserService.canCreatePublicProjects() + @.canCreatePrivateProjects = @currentUserService.canCreatePrivateProjects() + + @.projectForm = @.project.toJS() + + @.platformName = "Trello" + @.projectForm.is_private = false + @.projectForm.keepExternalReference = false + + if !@.canCreatePublicProjects.valid && @.canCreatePrivateProjects.valid + @.projectForm.is_private = true + + checkUsersLimit: () -> + @.limitMembersPrivateProject = @currentUserService.canAddMembersPrivateProject(@.members.size) + @.limitMembersPublicProject = @currentUserService.canAddMembersPublicProject(@.members.size) + + saveForm: () -> + @.onSaveProjectDetails({project: Immutable.fromJS(@.projectForm)}) + + canCreateProject: () -> + if @.projectForm.is_private + return @.canCreatePrivateProjects.valid + else + return @.canCreatePublicProjects.valid + + isDisabled: () -> + return !@.canCreateProject() + +angular.module('taigaProjects').controller('TrelloImportProjectFormCtrl', TrelloImportProjectFormController) diff --git a/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.directive.coffee b/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.directive.coffee new file mode 100644 index 00000000..f4b29e4a --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.directive.coffee @@ -0,0 +1,40 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: trello-import-project-form.directive.coffee +### + +TrelloImportProjectFormDirective = () -> + return { + link: (scope, elm, attr, ctrl) -> + scope.$watch('vm.members', ctrl.checkUsersLimit.bind(ctrl)) + + templateUrl:"projects/create/trello-import/trello-import-project-form/trello-import-project-form.html", + controller: "TrelloImportProjectFormCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + members: '<', + project: '<', + onSaveProjectDetails: '&', + onCancelForm: '&', + fetchingUsers: '<' + } + } + +TrelloImportProjectFormDirective.$inject = [] + +angular.module("taigaProjects").directive("tgTrelloImportProjectForm", TrelloImportProjectFormDirective) diff --git a/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.jade b/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.jade new file mode 100644 index 00000000..576cf489 --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.jade @@ -0,0 +1,25 @@ +.import-project-trello-form.create-project + div(ng-include="'projects/create/import/import-header.html'") + + .spin(tg-loading="vm.fetchingUsers") + + form( + ng-if="!vm.fetchingUsers", + name="projectForm", + ng-submit="vm.saveForm()" + ) + div(ng-include="'projects/create/import-project-form-common/name.html'") + div(ng-include="'projects/create/import-project-form-common/description.html'") + div(ng-include="'projects/create/import-project-form-common/project-privacy.html'") + tg-create-project-restrictions( + is-private="vm.projectForm.is_private" + can-create-public-projects="vm.canCreatePublicProjects" + can-create-private-projects="vm.canCreatePrivateProjects" + ) + tg-create-project-members-restrictions( + is-private="vm.projectForm.is_private" + limit-members-private-project="vm.limitMembersPrivateProject" + limit-members-public-project="vm.limitMembersPublicProject" + ) + div(ng-include="'projects/create/import-project-form-common/links.html'") + div(ng-include="'projects/create/import-project-form-common/actions.html'") diff --git a/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.scss b/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.scss new file mode 100644 index 00000000..02491eb7 --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import-project-form/trello-import-project-form.scss @@ -0,0 +1,3 @@ +.import-project-trello-form { + @include create-project; +} diff --git a/app/modules/projects/create/trello-import/trello-import.controller.coffee b/app/modules/projects/create/trello-import/trello-import.controller.coffee new file mode 100644 index 00000000..0d7782b6 --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import.controller.coffee @@ -0,0 +1,71 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: trello-import.controller.coffee +### + +class TrelloImportController + @.$inject = [ + 'tgTrelloImportService', + '$tgConfirm', + '$translate', + 'tgImportProjectService', + ] + + constructor: (@trelloImportService, @confirm, @translate, @importProjectService) -> + @.project = null + taiga.defineImmutableProperty @, 'projects', () => return @trelloImportService.projects + taiga.defineImmutableProperty @, 'members', () => return @trelloImportService.projectUsers + + startProjectSelector: () -> + @trelloImportService.fetchProjects().then () => @.step = 'project-select-trello' + + onSelectProject: (project) -> + @.step = 'project-form-trello' + @.project = project + @.fetchingUsers = true + + @trelloImportService.fetchUsers(@.project.get('id')).then () => @.fetchingUsers = false + + onSaveProjectDetails: (project) -> + @.project = project + @.step = 'project-members-trello' + + onCancelMemberSelection: () -> + @.step = 'project-form-trello' + + startImport: (users) -> + loader = @confirm.loader(@translate.instant('PROJECT.IMPORT.IN_PROGRESS.TITLE'), @translate.instant('PROJECT.IMPORT.IN_PROGRESS.DESCRIPTION'), true) + + loader.start() + + promise = @trelloImportService.importProject( + @.project.get('name'), + @.project.get('description'), + @.project.get('id'), + users, + @.project.get('keepExternalReference'), + @.project.get('is_private') + ) + + @importProjectService.importPromise(promise).then () => loader.stop() + + submitUserSelection: (users) -> + @.startImport(users) + + return null + +angular.module('taigaProjects').controller('TrelloImportCtrl', TrelloImportController) diff --git a/app/modules/projects/create/trello-import/trello-import.controller.spec.coffee b/app/modules/projects/create/trello-import/trello-import.controller.spec.coffee new file mode 100644 index 00000000..c93a3274 --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import.controller.spec.coffee @@ -0,0 +1,176 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: trello-import.controller.spec.coffee +### + +describe "TrelloImportCtrl", -> + $provide = null + $controller = null + mocks = {} + + _mockCurrentUserService = -> + mocks.currentUserService = { + canAddMembersPrivateProject: sinon.stub() + canAddMembersPublicProject: sinon.stub() + } + + $provide.value("tgCurrentUserService", mocks.currentUserService) + + + _mockImportProjectService = -> + mocks.importProjectService = { + importPromise: sinon.stub() + } + + $provide.value("tgImportProjectService", mocks.importProjectService) + + _mockTrelloImportService = -> + mocks.trelloService = { + fetchProjects: sinon.stub(), + fetchUsers: sinon.stub(), + importProject: sinon.stub() + } + + $provide.value("tgTrelloImportService", mocks.trelloService) + + _mockConfirm = -> + mocks.confirm = { + loader: sinon.stub() + } + + $provide.value("$tgConfirm", mocks.confirm) + + _mockTranslate = -> + mocks.translate = { + instant: sinon.stub() + } + + $provide.value("$translate", mocks.translate) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockTrelloImportService() + _mockConfirm() + _mockTranslate() + _mockImportProjectService() + _mockCurrentUserService() + + return null + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "start project selector", () -> + ctrl = $controller("TrelloImportCtrl") + + mocks.trelloService.fetchProjects.promise().resolve() + + ctrl.startProjectSelector().then () -> + expect(ctrl.step).to.be.equal('project-select-trello') + expect(mocks.trelloService.fetchProjects).have.been.called + + it "on select project reload projects", (done) -> + project = Immutable.fromJS({ + id: 1, + name: "project-name" + }) + + mocks.trelloService.fetchUsers.promise().resolve() + + ctrl = $controller("TrelloImportCtrl") + + promise = ctrl.onSelectProject(project) + + expect(ctrl.fetchingUsers).to.be.true + + promise.then () -> + expect(ctrl.fetchingUsers).to.be.false + expect(ctrl.step).to.be.equal('project-form-trello') + expect(ctrl.project).to.be.equal(project) + done() + + it "on save project details reload users", () -> + project = Immutable.fromJS({ + id: 1, + name: "project-name" + }) + + ctrl = $controller("TrelloImportCtrl") + ctrl.onSaveProjectDetails(project) + + expect(ctrl.step).to.be.equal('project-members-trello') + expect(ctrl.project).to.be.equal(project) + + + it "on select user init import", (done) -> + users = Immutable.fromJS([ + { + id: 0 + }, + { + id: 1 + }, + { + id: 2 + } + ]) + + loaderObj = { + start: sinon.spy(), + update: sinon.stub(), + stop: sinon.spy() + } + + projectResult = { + id: 3, + name: "name" + } + + mocks.confirm.loader.returns(loaderObj) + + mocks.importProjectService.importPromise.promise().resolve() + + ctrl = $controller("TrelloImportCtrl") + ctrl.project = Immutable.fromJS({ + id: 1, + name: 'project-name', + description: 'project-description', + keepExternalReference: false, + is_private: true + }) + + + mocks.trelloService.importProject.promise().resolve(projectResult) + + ctrl.startImport(users).then () -> + expect(loaderObj.start).have.been.called + expect(loaderObj.stop).have.been.called + expect(mocks.trelloService.importProject).have.been.calledWith('project-name', 'project-description', 1, users, false, true) + + done() diff --git a/app/modules/projects/create/trello-import/trello-import.directive.coffee b/app/modules/projects/create/trello-import/trello-import.directive.coffee new file mode 100644 index 00000000..77fca833 --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import.directive.coffee @@ -0,0 +1,35 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: trello-import.directive.coffee +### + +TrelloImportDirective = () -> + return { + link: (scope, elm, attrs, ctrl) -> + ctrl.startProjectSelector() + templateUrl:"projects/create/trello-import/trello-import.html", + controller: "TrelloImportCtrl", + controllerAs: "vm", + bindToController: true, + scope: { + onCancel: '&' + } + } + +TrelloImportDirective.$inject = [] + +angular.module("taigaProjects").directive("tgTrelloImport", TrelloImportDirective) diff --git a/app/modules/projects/create/trello-import/trello-import.jade b/app/modules/projects/create/trello-import/trello-import.jade new file mode 100644 index 00000000..9702988d --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import.jade @@ -0,0 +1,27 @@ +tg-import-project-selector( + logo="/#{v}/images/import-logos/trello.png" + search="{{ 'PROJECT.IMPORT.TRELLO.CHOOSE_BOARD' | translate }}" + projects="vm.projects" + on-cancel="vm.onCancel()" + on-select-project="vm.onSelectProject(project)" + ng-if="vm.step == 'project-select-trello'" +) + +tg-trello-import-project-form( + ng-if="vm.step == 'project-form-trello'" + project="vm.project" + members="vm.members" + fetching-users="vm.fetchingUsers" + on-save-project-details="vm.onSaveProjectDetails(project)" + on-cancel-form="vm.step = 'project-select-trello'" +) + +tg-import-project-members( + ng-if="vm.step == 'project-members-trello'" + platform="Trello" + logo="/#{v}/images/import-logos/trello.png" + project="vm.project" + members="vm.members" + on-submit="vm.submitUserSelection(users)" + on-cancel="vm.onCancelMemberSelection()" +) diff --git a/app/modules/projects/create/trello-import/trello-import.service.coffee b/app/modules/projects/create/trello-import/trello-import.service.coffee new file mode 100644 index 00000000..aa8a2ab3 --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import.service.coffee @@ -0,0 +1,56 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: trello-import.service.coffee +### + +class TrelloImportService extends taiga.Service + @.$inject = [ + 'tgResources' + ] + + constructor: (@resources) -> + @.projects = Immutable.List() + @.projectUsers = Immutable.List() + @.token = null + + setToken: (token) -> + @.token = token + + fetchProjects: () -> + @resources.trelloImporter.listProjects(@.token).then (projects) => @.projects = projects + + fetchUsers: (projectId) -> + @resources.trelloImporter.listUsers(@.token, projectId).then (users) => @.projectUsers = users + + importProject: (name, description, projectId, userBindings, keepExternalReference, isPrivate) -> + return @resources.trelloImporter.importProject(@.token, name, description, projectId, userBindings, keepExternalReference, isPrivate) + + getAuthUrl: () -> + return new Promise (resolve) => + @resources.trelloImporter.getAuthUrl().then (response) => + @.authUrl = response.data.url + resolve(@.authUrl) + + authorize: (verifyCode) -> + return new Promise (resolve, reject) => + @resources.trelloImporter.authorize(verifyCode).then ((response) => + @.token = response.data.token + resolve(@.token) + ), (error) -> + reject(new Error(error.status)) + +angular.module("taigaProjects").service("tgTrelloImportService", TrelloImportService) diff --git a/app/modules/projects/create/trello-import/trello-import.service.spec.coffee b/app/modules/projects/create/trello-import/trello-import.service.spec.coffee new file mode 100644 index 00000000..91bd4c78 --- /dev/null +++ b/app/modules/projects/create/trello-import/trello-import.service.spec.coffee @@ -0,0 +1,115 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# 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: trello-import.controller.spec.coffee +### + +describe "tgTrelloImportService", -> + $provide = null + service = null + mocks = {} + + _mockResources = -> + mocks.resources = { + trelloImporter: { + listProjects: sinon.stub(), + listUsers: sinon.stub(), + importProject: sinon.stub(), + getAuthUrl: sinon.stub(), + authorize: sinon.stub() + } + } + + $provide.value("tgResources", mocks.resources) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockResources() + + return null + + _inject = -> + inject (_tgTrelloImportService_) -> + service = _tgTrelloImportService_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "fetch projects", (done) -> + service.setToken(123) + mocks.resources.trelloImporter.listProjects.withArgs(123).promise().resolve('projects') + + service.fetchProjects().then () -> + service.projects = "projects" + done() + + it "fetch user", (done) -> + service.setToken(123) + projectId = 3 + mocks.resources.trelloImporter.listUsers.withArgs(123, projectId).promise().resolve('users') + + service.fetchUsers(projectId).then () -> + service.projectUsers = 'users' + done() + + it "import project", () -> + service.setToken(123) + projectId = 2 + + service.importProject(projectId, true, true ,true) + + expect(mocks.resources.trelloImporter.importProject).to.have.been.calledWith(123, projectId, true, true, true) + + it "get auth url", (done) -> + service.setToken(123) + projectId = 3 + + response = { + data: { + url: "url123" + } + } + + mocks.resources.trelloImporter.getAuthUrl.promise().resolve(response) + + service.getAuthUrl().then (url) -> + expect(url).to.be.equal("url123") + done() + + it "authorize", (done) -> + service.setToken(123) + projectId = 3 + verifyCode = 12345 + + response = { + data: { + token: "token123" + } + } + + mocks.resources.trelloImporter.authorize.withArgs(verifyCode).promise().resolve(response) + + service.authorize(verifyCode).then (token) -> + expect(token).to.be.equal("token123") + done() diff --git a/app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.directive.coffee b/app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.directive.coffee new file mode 100644 index 00000000..7c34379c --- /dev/null +++ b/app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.directive.coffee @@ -0,0 +1,41 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# 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: warning-user-import.directive.coffee +### + +WarningUserImportDirective = (lightboxService, lightboxKeyboardNavigationService) -> + return { + link: (scope, el, attr) -> + scope.$watch 'visible', (visible) -> + if visible && !el.hasClass('open') + lightboxService.open(el, scope.onClose).then -> + el.find('input').focus() + lightboxKeyboardNavigationService.init(el) + else if !visible && el.hasClass('open') + lightboxService.close(el) + + templateUrl:"projects/create/warning-user-import-lightbox/warning-user-import-lightbox.html", + scope: { + visible: '<', + onClose: '&', + onConfirm: '&' + } + } + +WarningUserImportDirective.$inject = ['lightboxService', 'lightboxKeyboardNavigationService'] + +angular.module("taigaProjects").directive("tgWarningUserImportLightbox", WarningUserImportDirective) diff --git a/app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.jade b/app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.jade new file mode 100644 index 00000000..1a280aee --- /dev/null +++ b/app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.jade @@ -0,0 +1,8 @@ +tg-lightbox-close(on-close="onClose()") + +.create-project.import-project + h1(translate="PROJECT.IMPORT.WARNING.TITLE") + p(translate="PROJECT.IMPORT.WARNING.DESCRIPTION") + .actions + button.button.button-gray(translate="PROJECT.IMPORT.WARNING.CHECK", ng-click="onClose()") + button.button.button-green(type="submit", translate="PROJECT.IMPORT.IMPORT", ng-click="onConfirm()") diff --git a/app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.scss b/app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.scss new file mode 100644 index 00000000..7d2a25d5 --- /dev/null +++ b/app/modules/projects/create/warning-user-import-lightbox/warning-user-import-lightbox.scss @@ -0,0 +1,9 @@ +tg-warning-user-import-lightbox { + .actions { + display: flex; + justify-content: center; + button:first-child { + margin-right: .5rem; + } + } +} diff --git a/app/modules/projects/listing/projects-listing.controller.coffee b/app/modules/projects/listing/projects-listing.controller.coffee index d7a60304..a78b765c 100644 --- a/app/modules/projects/listing/projects-listing.controller.coffee +++ b/app/modules/projects/listing/projects-listing.controller.coffee @@ -19,14 +19,10 @@ class ProjectsListingController @.$inject = [ - "tgCurrentUserService", - "tgProjectsService", + "tgCurrentUserService" ] - constructor: (@currentUserService, @projectsService) -> + constructor: (@currentUserService) -> taiga.defineImmutableProperty(@, "projects", () => @currentUserService.projects.get("all")) - newProject: -> - @projectsService.newProject() - angular.module("taigaProjects").controller("ProjectsListing", ProjectsListingController) diff --git a/app/modules/projects/listing/projects-listing.controller.spec.coffee b/app/modules/projects/listing/projects-listing.controller.spec.coffee index d7a3f00d..7331624a 100644 --- a/app/modules/projects/listing/projects-listing.controller.spec.coffee +++ b/app/modules/projects/listing/projects-listing.controller.spec.coffee @@ -43,16 +43,11 @@ describe "ProjectsListingController", -> _mockProjectsService = () -> stub = sinon.stub() - mocks.projectsService = { - newProject: sinon.stub() - } - provide.value "tgProjectsService", mocks.projectsService _mocks = () -> module ($provide) -> provide = $provide - _mockProjectsService() _mockCurrentUserService() return null @@ -70,11 +65,3 @@ describe "ProjectsListingController", -> $scope: {} expect(pageCtrl.projects).to.be.equal(projects.get('all')) - - it "new project", () -> - pageCtrl = controller "ProjectsListing", - $scope: {} - - pageCtrl.newProject() - - expect(mocks.projectsService.newProject).to.be.calledOnce diff --git a/app/modules/projects/listing/projects-listing.jade b/app/modules/projects/listing/projects-listing.jade index 096af8b6..9c72475b 100644 --- a/app/modules/projects/listing/projects-listing.jade +++ b/app/modules/projects/listing/projects-listing.jade @@ -4,17 +4,10 @@ .create-options a.create-project-btn.button-green( href="#" - ng-click="vm.newProject()" + tg-nav="create-project" title="{{'PROJECT.NAVIGATION.ACTION_CREATE_PROJECT' | translate}}" translate="PROJECT.NAVIGATION.ACTION_CREATE_PROJECT" ) - span(tg-import-project-button) - a.button-blackish.import-project-button( - href="" - title="{{'PROJECT.NAVIGATION.TITLE_IMPORT_PROJECT' | translate}}" - ) - tg-svg(svg-icon="icon-upload") - input.import-file.hidden(type="file") section.project-list-section .project-list diff --git a/app/modules/projects/projects.service.coffee b/app/modules/projects/projects.service.coffee index 3c1415d5..c3ce4dae 100644 --- a/app/modules/projects/projects.service.coffee +++ b/app/modules/projects/projects.service.coffee @@ -22,9 +22,15 @@ groupBy = @.taiga.groupBy class ProjectsService extends taiga.Service - @.$inject = ["tgResources", "$projectUrl", "tgLightboxFactory"] + @.$inject = ["tgResources", "$projectUrl"] - constructor: (@rs, @projectUrl, @lightboxFactory) -> + constructor: (@rs, @projectUrl) -> + + create: (data) -> + return @rs.projects.create(data) + + duplicate: (projectId, data) -> + return @rs.projects.duplicate(projectId, data) getProjectBySlug: (projectSlug) -> return @rs.projects.getProjectBySlug(projectSlug) @@ -46,11 +52,6 @@ class ProjectsService extends taiga.Service return project - newProject: -> - @lightboxFactory.create("tg-lb-create-project", { - "class": "wizard-create-project lightbox" - }) - bulkUpdateProjectsOrder: (sortData) -> return @rs.projects.bulkUpdateOrder(sortData) diff --git a/app/modules/projects/projects.service.spec.coffee b/app/modules/projects/projects.service.spec.coffee index 80a98389..11b15c05 100644 --- a/app/modules/projects/projects.service.spec.coffee +++ b/app/modules/projects/projects.service.spec.coffee @@ -46,12 +46,6 @@ describe "tgProjectsService", -> provide.value "$projectUrl", mocks.projectUrl - _mockLightboxFactory = () -> - mocks.lightboxFactory = { - create: sinon.stub() - } - - provide.value "tgLightboxFactory", mocks.lightboxFactory _inject = (callback) -> inject (_$q_, _$rootScope_, _tgProjectsService_) -> @@ -65,7 +59,6 @@ describe "tgProjectsService", -> provide = $provide _mockResources() _mockProjectUrl() - _mockLightboxFactory() _mockAuthService() return null @@ -75,13 +68,6 @@ describe "tgProjectsService", -> _mocks() _inject() - it "newProject, create the wizard lightbox", () -> - projectsService.newProject() - - expect(mocks.lightboxFactory.create).to.have.been.calledWith("tg-lb-create-project", { - "class": "wizard-create-project lightbox" - }) - it "bulkUpdateProjectsOrder and then fetch projects again", () -> projects_order = [ {"id": 8}, diff --git a/app/modules/resources/importers-resource.service.coffee b/app/modules/resources/importers-resource.service.coffee new file mode 100644 index 00000000..ed425544 --- /dev/null +++ b/app/modules/resources/importers-resource.service.coffee @@ -0,0 +1,193 @@ +### +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino Garcia +# Copyright (C) 2014-2016 David Barragán Merino +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Juan Francisco Alcántara +# Copyright (C) 2014-2016 Xavi Julian +# +# 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/importers.coffee +### + + +taiga = @.taiga + +TrelloResource = (urlsService, http) -> + service = {} + + service.getAuthUrl = (url) -> + url = urlsService.resolve("importers-trello-auth-url") + return http.get(url) + + service.authorize = (verifyCode) -> + url = urlsService.resolve("importers-trello-authorize") + return http.post(url, {code: verifyCode}) + + service.listProjects = (token) -> + url = urlsService.resolve("importers-trello-list-projects") + return http.post(url, {token: token}).then (response) -> Immutable.fromJS(response.data) + + service.listUsers = (token, projectId) -> + url = urlsService.resolve("importers-trello-list-users") + return http.post(url, {token: token, project: projectId}).then (response) -> Immutable.fromJS(response.data) + + service.importProject = (token, name, description, projectId, userBindings, keepExternalReference, isPrivate) -> + url = urlsService.resolve("importers-trello-import-project") + data = { + token: token, + name: name, + description: description, + project: projectId, + users_bindings: userBindings.toJS(), + keep_external_reference: keepExternalReference, + is_private: isPrivate, + template: "kanban", + } + return http.post(url, data) + + return () -> + return {"trelloImporter": service} + +TrelloResource.$inject = ["$tgUrls", "$tgHttp"] + +JiraResource = (urlsService, http) -> + service = {} + + service.getAuthUrl = (jira_url) -> + url = urlsService.resolve("importers-jira-auth-url") + "?url=" + jira_url + return http.get(url) + + service.authorize = () -> + url = urlsService.resolve("importers-jira-authorize") + return http.post(url) + + service.listProjects = (jira_url, token) -> + url = urlsService.resolve("importers-jira-list-projects") + return http.post(url, {url: jira_url, token: token}).then (response) -> Immutable.fromJS(response.data) + + service.listUsers = (jira_url, token, projectId) -> + url = urlsService.resolve("importers-jira-list-users") + return http.post(url, {url: jira_url, token: token, project: projectId}).then (response) -> Immutable.fromJS(response.data) + + service.importProject = (jira_url, token, name, description, projectId, userBindings, keepExternalReference, isPrivate, projectType, importerType) -> + url = urlsService.resolve("importers-jira-import-project") + projectTemplate = "kanban" + if projectType != "kanban" + projectTemplate = "scrum" + + data = { + url: jira_url, + token: token, + name: name, + description: description, + project: projectId, + users_bindings: userBindings.toJS(), + keep_external_reference: keepExternalReference, + is_private: isPrivate, + project_type: projectType, + importer_type: importerType, + template: projectTemplate, + } + return http.post(url, data) + + return () -> + return {"jiraImporter": service} + +JiraResource.$inject = ["$tgUrls", "$tgHttp"] + +GithubResource = (urlsService, http) -> + service = {} + + service.getAuthUrl = (callbackUri) -> + url = urlsService.resolve("importers-github-auth-url") + "?uri=" + callbackUri + return http.get(url) + + service.authorize = (code) -> + url = urlsService.resolve("importers-github-authorize") + return http.post(url, {code: code}) + + service.listProjects = (token) -> + url = urlsService.resolve("importers-github-list-projects") + return http.post(url, {token: token}).then (response) -> Immutable.fromJS(response.data) + + service.listUsers = (token, projectId) -> + url = urlsService.resolve("importers-github-list-users") + return http.post(url, {token: token, project: projectId}).then (response) -> Immutable.fromJS(response.data) + + service.importProject = (token, name, description, projectId, userBindings, keepExternalReference, isPrivate, projectType) -> + url = urlsService.resolve("importers-github-import-project") + + data = { + token: token, + name: name, + description: description, + project: projectId, + users_bindings: userBindings.toJS(), + keep_external_reference: keepExternalReference, + is_private: isPrivate, + template: projectType, + } + return http.post(url, data) + + return () -> + return {"githubImporter": service} + +GithubResource.$inject = ["$tgUrls", "$tgHttp"] + +AsanaResource = (urlsService, http) -> + service = {} + + service.getAuthUrl = () -> + url = urlsService.resolve("importers-asana-auth-url") + return http.get(url) + + service.authorize = (code) -> + url = urlsService.resolve("importers-asana-authorize") + return http.post(url, {code: code}) + + service.listProjects = (token) -> + url = urlsService.resolve("importers-asana-list-projects") + return http.post(url, {token: token}).then (response) -> Immutable.fromJS(response.data) + + service.listUsers = (token, projectId) -> + url = urlsService.resolve("importers-asana-list-users") + return http.post(url, {token: token, project: projectId}).then (response) -> Immutable.fromJS(response.data) + + service.importProject = (token, name, description, projectId, userBindings, keepExternalReference, isPrivate, projectType) -> + url = urlsService.resolve("importers-asana-import-project") + + data = { + token: token, + name: name, + description: description, + project: projectId, + users_bindings: userBindings.toJS(), + keep_external_reference: keepExternalReference, + is_private: isPrivate, + template: projectType, + } + return http.post(url, data) + + return () -> + return {"asanaImporter": service} + +AsanaResource.$inject = ["$tgUrls", "$tgHttp"] + +module = angular.module("taigaResources2") +module.factory("tgTrelloImportResource", TrelloResource) +module.factory("tgJiraImportResource", JiraResource) +module.factory("tgGithubImportResource", GithubResource) +module.factory("tgAsanaImportResource", AsanaResource) diff --git a/app/modules/resources/projects-resource.service.coffee b/app/modules/resources/projects-resource.service.coffee index 82942b37..7b306c1b 100644 --- a/app/modules/resources/projects-resource.service.coffee +++ b/app/modules/resources/projects-resource.service.coffee @@ -22,6 +22,28 @@ pagination = () -> Resource = (urlsService, http, paginateResponseService) -> service = {} + service.create = (data) -> + url = urlsService.resolve('projects') + + return http.post(url, JSON.stringify(data)) + .then (result) => return Immutable.fromJS(result.data) + + service.duplicate = (projectId, data) -> + + url = urlsService.resolve("projects") + url = "#{url}/#{projectId}/duplicate" + + members = data.users.map (member) => {"id": member} + + params = { + "name": data.name, + "description": data.description, + "is_private": data.is_private, + "users": members + } + + return http.post(url, params) + service.getProjects = (params = {}, pagination = true) -> url = urlsService.resolve("projects") diff --git a/app/modules/resources/resources.coffee b/app/modules/resources/resources.coffee index f55dd7cc..6c1dabb6 100644 --- a/app/modules/resources/resources.coffee +++ b/app/modules/resources/resources.coffee @@ -28,7 +28,11 @@ services = [ "tgAttachmentsResource", "tgStatsResource", "tgWikiHistory", - "tgEpicsResource" + "tgEpicsResource", + "tgTrelloImportResource", + "tgJiraImportResource", + "tgGithubImportResource", + "tgAsanaImportResource" ] Resources = ($injector) -> @@ -39,7 +43,7 @@ Resources = ($injector) -> for serviceProperty in Object.keys(service) if @[serviceProperty] - console.warm("repeated resource " + serviceProperty) + console.warn("repeated resource " + serviceProperty) @[serviceProperty] = service[serviceProperty] diff --git a/app/modules/services/current-user.service.coffee b/app/modules/services/current-user.service.coffee index 60985801..39d7f3e1 100644 --- a/app/modules/services/current-user.service.coffee +++ b/app/modules/services/current-user.service.coffee @@ -121,7 +121,13 @@ class CurrentUserService canCreatePrivateProjects: () -> user = @.getUser() if user.get('max_private_projects') != null && user.get('total_private_projects') >= user.get('max_private_projects') - return {valid: false, reason: 'max_private_projects', type: 'private_project'} + return { + valid: false, + reason: 'max_private_projects', + type: 'private_project', + current: user.get('total_private_projects'), + max: user.get('max_private_projects') + } return {valid: true} @@ -129,7 +135,41 @@ class CurrentUserService user = @.getUser() if user.get('max_public_projects') != null && user.get('total_public_projects') >= user.get('max_public_projects') - return {valid: false, reason: 'max_public_projects', type: 'public_project'} + return { + valid: false, + reason: 'max_public_projects', + type: 'public_project', + current: user.get('total_public_projects'), + max: user.get('max_public_projects') + } + + return {valid: true} + + canAddMembersPublicProject: (totalMembers) -> + user = @.getUser() + + if user.get('max_memberships_public_projects') != null && totalMembers > user.get('max_memberships_public_projects') + return { + valid: false, + reason: 'max_members_public_projects', + type: 'public_project', + current: totalMembers, + max: user.get('max_memberships_public_projects') + } + + return {valid: true} + + canAddMembersPrivateProject: (totalMembers) -> + user = @.getUser() + + if user.get('max_memberships_private_projects') != null && totalMembers > user.get('max_memberships_private_projects') + return { + valid: false, + reason: 'max_members_private_projects', + type: 'private_project', + current: totalMembers, + max: user.get('max_memberships_private_projects') + } return {valid: true} @@ -139,15 +179,14 @@ class CurrentUserService result = @.canCreatePrivateProjects() return result if !result.valid - if user.get('max_memberships_private_projects') != null && project.get('total_memberships') > user.get('max_memberships_private_projects') - return {valid: false, reason: 'max_members_private_projects', type: 'private_project'} - + membersResult = @.canAddMembersPrivateProject(project.get('total_memberships')) + return membersResult if !membersResult.valid else result = @.canCreatePublicProjects() return result if !result.valid - if user.get('max_memberships_public_projects') != null && project.get('total_memberships') > user.get('max_memberships_public_projects') - return {valid: false, reason: 'max_members_public_projects', type: 'public_project'} + membersResult = @.canAddMembersPublicProject(project.get('total_memberships')) + return membersResult if !membersResult.valid return {valid: true} diff --git a/app/modules/services/current-user.service.spec.coffee b/app/modules/services/current-user.service.spec.coffee index 96f4fff2..b088c255 100644 --- a/app/modules/services/current-user.service.spec.coffee +++ b/app/modules/services/current-user.service.spec.coffee @@ -219,7 +219,9 @@ describe "tgCurrentUserService", -> expect(result).to.be.eql({ valid: false, reason: 'max_private_projects', - type: 'private_project' + type: 'private_project', + current: 1, + max: 1 }) it "the user can create private projects", () -> @@ -254,7 +256,9 @@ describe "tgCurrentUserService", -> expect(result).to.be.eql({ valid: false, reason: 'max_public_projects', - type: 'public_project' + type: 'public_project', + current: 1, + max: 1 }) it "the user can create public projects", () -> @@ -321,7 +325,9 @@ describe "tgCurrentUserService", -> expect(result).to.be.eql({ valid: false reason: 'max_public_projects' - type: 'public_project' + type: 'public_project', + current: 1, + max: 1 }) @@ -348,7 +354,9 @@ describe "tgCurrentUserService", -> expect(result).to.be.eql({ valid: false reason: 'max_members_public_projects' - type: 'public_project' + type: 'public_project', + current: 5, + max: 4 }) it "the user can own private project", () -> @@ -398,7 +406,9 @@ describe "tgCurrentUserService", -> expect(result).to.be.eql({ valid: false reason: 'max_private_projects' - type: 'private_project' + type: 'private_project', + current: 1, + max: 1 }) @@ -425,5 +435,7 @@ describe "tgCurrentUserService", -> expect(result).to.be.eql({ valid: false reason: 'max_members_private_projects' - type: 'private_project' + type: 'private_project', + current: 5, + max: 4 }) diff --git a/app/partials/project/wizard-create-project.jade b/app/partials/project/wizard-create-project.jade deleted file mode 100644 index 9015795a..00000000 --- a/app/partials/project/wizard-create-project.jade +++ /dev/null @@ -1,84 +0,0 @@ -tg-lightbox-close -form - header - h1.title(translate="WIZARD.SECTION_TITLE_CREATE_PROJECT") - .subtitle( - translate="WIZARD.CREATE_PROJECT_TEXT" - role="presentation" - ) - section.template-option - .template-selector-title - legend(translate="WIZARD.CHOOSE_TEMPLATE") - .template-selector - fieldset(ng-repeat="template in templates") - input( - type="radio" - name="template" - id="template-{{ template.id }}" - ng-value='template.id' - ng-model="data.creation_template" - data-required="true" - ) - label.template-label(for="template-{{ template.id }}") - tg-svg(svg-icon="{{'icon-'+template.slug}}") - span.template-name {{ template.name }} - .template-data - legend(translate="WIZARD.PROJECT_DETAILS") - fieldset - input( - type="text" - name="name" - ng-model="data.name" - data-required="true" - placeholder="{{'COMMON.FIELDS.NAME' | translate}}" - maxlength="45" - aria-hidden="true" - ) - fieldset - textarea( - name="description" - ng-model="data.description" - data-required="true" - ng-attr-placeholder="{{'COMMON.FIELDS.DESCRIPTION' | translate}}" - ) - .template-privacity - fieldset - input( - type="radio" - name="is_private" - id="template-public" - data-required="true" - aria-hidden="true" - ng-value="false" - ng-model="data.is_private" - required - ng-disabled="!canCreatePublicProjects.valid" - ng-checked="canCreatePublicProjects.valid" - ) - label.template-privacity(for="template-public") - tg-svg(svg-icon="icon-discover") - span(translate="WIZARD.PUBLIC_PROJECT") - fieldset - input( - type="radio" - name="is_private" - id="template-private" - data-required="true" - ng-value="true" - ng-model="data.is_private" - aria-hidden="true" - required - ng-disabled="!canCreatePrivateProjects.valid" - ng-checked="!canCreatePublicProjects.valid" - ) - label.template-privacity(for="template-private") - tg-svg(svg-icon="icon-lock") - span(translate="WIZARD.PRIVATE_PROJECT") - - tg-create-project-restriction - - button.button-green.submit-button( - translate="WIZARD.CREATE_PROJECT" - title="{{'WIZARD.CREATE_PROJECT' | translate}}" - ng-click="" - ) diff --git a/app/partials/project/wizard-restrictions.jade b/app/partials/project/wizard-restrictions.jade deleted file mode 100644 index fb80f608..00000000 --- a/app/partials/project/wizard-restrictions.jade +++ /dev/null @@ -1,7 +0,0 @@ -div.create-warning(ng-if="!canCreatePrivateProjects.valid && canCreatePrivateProjects.reason == 'max_private_projects'") - tg-svg(svg-icon="icon-exclamation") - span {{ 'WIZARD.MAX_PRIVATE_PROJECTS' | translate }} - -div.create-warning(ng-if="!canCreatePublicProjects.valid && canCreatePublicProjects.reason == 'max_public_projects'") - tg-svg(svg-icon="icon-exclamation") - span {{ 'WIZARD.MAX_PUBLIC_PROJECTS' | translate }} diff --git a/app/styles/dependencies/mixins/btn-group.scss b/app/styles/dependencies/mixins/btn-group.scss new file mode 100644 index 00000000..af18c9e2 --- /dev/null +++ b/app/styles/dependencies/mixins/btn-group.scss @@ -0,0 +1,45 @@ +@mixin btn-group { + display: flex; + fieldset { + &:first-child { + label { + border-radius: .25rem 0 0 .25rem; + } + } + &:last-child { + label { + border-radius: 0 .25rem .25rem 0; + } + } + } + label { + align-items: center; + background: $mass-white; + cursor: pointer; + display: flex; + justify-content: center; + text-align: center; + padding: .75rem; + text-transform: uppercase; + &:hover { + background: darken($mass-white, 5%); + transition: background .2s linear; + } + .icon { + margin-right: .25rem; + } + } + input { + &:checked+label { + background: darken($mass-white, 10%); + @include font-type(bold); + } + &:disabled+label { + cursor: not-allowed; + color: lighten($gray-light, 15%); + .icon { + color: lighten($gray-light, 15%); + } + } + } +} diff --git a/app/styles/dependencies/mixins/create.scss b/app/styles/dependencies/mixins/create.scss new file mode 100644 index 00000000..cdf763c5 --- /dev/null +++ b/app/styles/dependencies/mixins/create.scss @@ -0,0 +1,238 @@ +@mixin create-project { + @include centered; + max-width: 800px; + + fieldset { + margin-bottom: 1rem; + } + + label { + @include font-size(small); + display: block; + margin-bottom: 0.25rem; + + .mumble { + @include font-type(light); + margin-left: 0.25rem; + } + } + + &-check { + @include font-type(bold); + position: relative; + + span { + display: block; + } + + .description { + @include font-type(normal); + } + + .check { + position: absolute; + right: 0; + top: 0; + } + } + + &-description, + &-title { + text-align: center; + } + + &-title { + align-items: center; + display: flex; + justify-content: center; + margin-bottom: 0; + + tg-svg { + display: flex; + } + + .icon { + @include svg-size(1.5rem); + margin-right: 0.5rem; + vertical-align: middle; + } + } + + &-description { + color: $gray-light; + margin-bottom: 2rem; + } + + &-limit { + @include font-type(light); + @include font-size(small); + color: $gray; + } + + &-import-type { + @include btn-group; + + input { + display: none; + } + + label { + background: $mass-white; + } + } + + &-privacity { + @include btn-group; + + input { + display: none; + } + + label { + background: $mass-white; + } + } + + &-type { + @include font-type(bold); + align-items: center; + display: flex; + justify-content: center; + margin-bottom: 2rem; + text-transform: uppercase; + + span { + margin-left: 0.5rem; + } + } + + &-selector { + a { + align-items: center; + border-bottom: 1px solid $whitish; + color: $grayer; + cursor: pointer; + display: flex; + justify-content: center; + position: relative; + } + li { + &:hover { + background: rgba($primary, .1); + transition: background 0.3s ease-in; + } + + &:first-child { + border-top: 1px solid $whitish; + } + } + + &-icon { + align-self: flex-start; + padding: 1.5rem 1rem; + + .icon { + @include svg-size(2.25rem); + } + } + + &-template-wrapper { + flex: 1; + padding: 1.25rem; + } + + &-template { + @include font-type(bold); + text-transform: uppercase; + } + + &-description { + @include font-type(light); + } + + &-long-description { + margin-top: 1rem; + max-height: 120px; + overflow: hidden; + transition: all 0.3s 0.2s cubic-bezier(0, 0, .53, 1.32); + + &.ng-hide { + line-height: 0; + max-height: 0; + } + } + + &-question { + position: absolute; + right: 1.5rem; + top: 1.5rem; + + &:hover { + svg { + fill: $primary; + transition: fill 0.2s linear; + } + } + + svg { + @include svg-size(1.2rem); + fill: $grayer; + } + } + + p { + margin-bottom: 0; + } + } + + &-action { + display: flex; + margin: 3rem 0 0; + + button { + @include font-size(large); + padding: 0.75rem; + } + + &-submit { + flex: 4; + margin-left: 1rem; + } + + &-back, + &-cancel { + color: currentColor; + + &:hover { + color: $primary-light; + } + } + + &-cancel { + flex: 1; + } + + &-back { + width: 10%; + } + } + &-warning { + @include font-size(small); + padding: 1rem; + text-align: center; + .icon-exclamation { + fill: $red-light; + margin-right: .5rem; + vertical-align: middle; + } + a { + color: $primary; + display: inline-block; + margin-left: .25rem; + } + } + .spin { + text-align: center; + width: 100%; + } +} diff --git a/app/styles/dependencies/mixins/import.scss b/app/styles/dependencies/mixins/import.scss new file mode 100644 index 00000000..ffa4a605 --- /dev/null +++ b/app/styles/dependencies/mixins/import.scss @@ -0,0 +1,189 @@ +@mixin import-project-selector { + @include centered; + max-width: 800px; + .import-project-selector { + + &-service { + img { + margin: 1rem auto; + width: 4rem; + display: block; + } + } + + &-filter { + align-items: center; + background: $whitish; + display: flex; + padding: .5rem; + + input { + background: $mass-white; + border: 0; + flex: 1; + padding: .5rem; + } + + svg { + @include svg-size(1rem); + fill: $gray; + margin: 0 1rem; + } + } + + &-title { + border-bottom: 1px solid $whitish; + padding: 1rem; + + &:hover { + background: rgba($primary, .1); + cursor: pointer; + } + } + } +} +@mixin import-members { + @include centered; + max-width: 800px; + .avatar { + width: 48px; + } + &-title { + @include font-size(normal); + @include font-type(bold); + margin-bottom: 0; + } + + &-system { + display: flex; + justify-content: space-between; + margin: 1rem 0 0; + padding: .5rem 0; + + img { + width: 100%; + } + } + + &-logo { + max-height: 3rem; + max-width: 3rem; + } + + &-row { + align-items: center; + border-bottom: 1px solid $whitish; + display: flex; + padding: .5rem 0; + justify-content: space-between; + + &:first-child { + border-top: 1px solid $whitish; + } + + &:last-child { + border: 0; + } + + &:hover { + .import-project-members-delete { + opacity: 1; + transition: all .2s ease-in; + } + } + } + + &-single { + align-items: center; + display: flex; + } + + &-username { + margin-left: 1rem; + } + + .avatar { + &.empty { + background-color: $whitish; + line-height: 3rem; + text-align: center; + width: 3rem; + } + } + + &-actions { + align-items: center; + display: flex; + } + &-delete { + background: transparent; + opacity: 0; + padding: .25rem .5rem; + svg { + @include svg-size(.75rem); + fill: $red; + } + } + &-match { + color: $gray-light; + button { + border-radius: 50%; + padding: .25rem .5rem; + background: $white; + svg { + @include svg-size(.75rem); + } + } + &-true { + border: 1px solid $primary; + margin: 0 .1rem 0 .25rem; + transition: background .2s; + &:hover { + background: rgba($primary-light, .3); + } + svg { + fill: $primary; + } + } + &-false { + border: 1px solid $red; + margin: 0 .25rem 0 .1rem; + transition: background .2s; + &:hover { + background: rgba($red, .3); + } + svg { + fill: $red; + } + } + } + + &-choose { + color: $primary; + text-transform: lowercase; + padding-right: 0; + &:hover { + color: $primary-light; + } + } + + &-selected { + align-items: center; + display: flex; + + &-img { + margin-left: .5rem; + max-width: 3rem; + } + + img { + width: 100%; + } + } + + &-submit { + display: block; + margin: 2rem auto 0; + padding: .75rem 4rem; + } +} diff --git a/app/styles/dependencies/mixins/radio-group.scss b/app/styles/dependencies/mixins/radio-group.scss new file mode 100644 index 00000000..f04577da --- /dev/null +++ b/app/styles/dependencies/mixins/radio-group.scss @@ -0,0 +1,19 @@ +@mixin radio-group { + input { + opacity: 0; + &:checked+svg { + fill: rgba($primary, .6); + stroke: rgba($primary, .1); + } + } + svg { + fill: $whitish; + stroke: darken($whitish, 10%); + stroke-width: 1px; + vertical-align: middle; + } + .control-indicator { + padding-left: .25rem; + @include font-type(normal); + } +} diff --git a/app/styles/extras/dependencies.scss b/app/styles/extras/dependencies.scss index 7203c465..c6b29c92 100644 --- a/app/styles/extras/dependencies.scss +++ b/app/styles/extras/dependencies.scss @@ -14,6 +14,10 @@ @import '../dependencies/mixins/box-arrow'; @import '../dependencies/mixins/box-shadow'; @import '../dependencies/mixins/centered'; +@import '../dependencies/mixins/btn-group'; +@import '../dependencies/mixins/import'; +@import '../dependencies/mixins/create'; +@import '../dependencies/mixins/radio-group'; @import '../dependencies/mixins/lightbox'; @import '../dependencies/mixins/loading-spinner'; @import '../dependencies/mixins/popover'; diff --git a/app/styles/modules/common/wizard.scss b/app/styles/modules/common/wizard.scss deleted file mode 100644 index 1ac0c065..00000000 --- a/app/styles/modules/common/wizard.scss +++ /dev/null @@ -1,143 +0,0 @@ -.wizard-create-project { - @include lightbox; - .close { - @include svg-size(2rem); - } - form { - width: 700px; - } - header { - margin-bottom: 3rem; - .title { - margin-bottom: 0; - } - .subtitle { - @include font-size(small); - color: $gray-light; - text-align: center; - } - } - .template-selector-title { - display: flex; - justify-content: space-between; - margin-bottom: 1rem; - } - .template-selector { - display: flex; - margin-bottom: 1rem; - input { - display: none; - } - fieldset { - &:first-child { - margin-right: .5rem; - } - } - } - input { - &:checked+label { - background: $primary-light; - color: $white; - transition: background .2s ease-in; - &:hover { - background: $primary-light; - } - } - +label { - background: rgba($whitish, .7); - cursor: pointer; - display: block; - padding: 2rem 1rem; - text-align: center; - transition: background .2s ease-in; - &:hover { - background: rgba($primary-light, .3); - transition: background .2s ease-in; - } - .icon { - @include svg-size(1.5rem); - fill: currentColor; - margin-right: .3rem; - vertical-align: text-top; - } - .template-name { - @include font-size(large); - text-transform: uppercase; - } - } - } - input[disabled]+label { - background: lighten($whitish, 5%); - box-shadow: none; - color: lighten($gray-light, 20%); - cursor: not-allowed; - opacity: .65; - &:hover { - background: lighten($whitish, 5%); - color: lighten($gray-light, 20%); - } - } - .template-data { - legend { - display: block; - margin-bottom: .5rem; - } - input, - textarea { - background: none; - border: 1px solid $whitish; - color: $gray-light; - @include placeholder { - color: darken($whitish, 20%); - } - } - textarea { - height: 7rem; - min-height: 7rem; - } - } - .template-privacity { - display: flex; - fieldset { - margin-bottom: 0; - &:first-child { - margin-right: .5rem; - } - } - input { - display: none; - } - label { - display: block; - text-align: center; - text-transform: uppercase; - - } - input+label { - padding: 1rem; - } - svg { - @include svg-size(.7rem); - } - } - .create-warning { - @include font-size(small); - padding: 1rem; - text-align: center; - .icon-exclamation { - fill: $red-light; - margin-right: .5rem; - vertical-align: middle; - } - a { - color: $primary; - display: inline-block; - margin-left: .25rem; - } - } - .button-green { - display: block; - margin: 1rem 5rem; - width: calc(100% - 10rem); - } -} diff --git a/app/svg/sprite.svg b/app/svg/sprite.svg index bfbdeb26..bdca9a97 100644 --- a/app/svg/sprite.svg +++ b/app/svg/sprite.svg @@ -440,7 +440,7 @@ Merge - + Fill @@ -448,14 +448,20 @@ Epics - + Broken Link - + - + + + + Duplicate + diff --git a/conf.e2e.js b/conf.e2e.js index a250f24c..28dab222 100644 --- a/conf.e2e.js +++ b/conf.e2e.js @@ -47,6 +47,7 @@ var config = { search: "e2e/suites/search.e2e.js", team: "e2e/suites/team.e2e.js", discover: "e2e/suites/discover/*.e2e.js", + createProject: "e2e/suites/create-project/*.e2e.js", transferProject: "e2e/suites/transfer-project.e2e.js", compileModules: "app/modules/compile-modules/**/*.e2e.js" }, diff --git a/conf/conf.example.json b/conf/conf.example.json index 4f0d8cb6..2412774b 100644 --- a/conf/conf.example.json +++ b/conf/conf.example.json @@ -16,5 +16,6 @@ "maxUploadFileSize": null, "contribPlugins": [], "tribeHost": null, + "importers": [], "gravatar": true } diff --git a/e2e/helpers/create-project-helper.js b/e2e/helpers/create-project-helper.js index 8323b73d..7f99f2bb 100644 --- a/e2e/helpers/create-project-helper.js +++ b/e2e/helpers/create-project-helper.js @@ -2,42 +2,35 @@ var utils = require('../utils'); var helper = module.exports; -helper.openWizard = function() { +helper.openCreateProjectPage = function() { $$('.create-project-btn').get(1).click(); }; -helper.createProjectLightbox = function() { +helper.newProjectScreen = function() { let obj = { - el: function() { - return $('div[tg-lb-create-project]'); + selectDuplicateOption: function() { + return utils.common.link($('.e2e-duplicate-project')); }, - waitOpen: function() { - return utils.lightbox.open(obj.el()); + selectScrumOption: function() { + return utils.common.link($('.e2e-create-project-scrum')); }, - waitClose: function() { - return utils.lightbox.close(obj.el()); + selectKanbanOption: function() { + return utils.common.link($('.e2e-create-project-kanban')); }, - next: async function() { - $('.wizard-step.active .button-green').click(); - - await browser.sleep(1000); + selectProjectToDuplicate: function() { + return $$('.e2e-duplicate-project-reference option').get(1).click(); }, - submit: function() { - return $('div[tg-lb-create-project] .button-green').click(); + fillNameAndDescription: async function(name, title){ + await $('.e2e-create-project-title').sendKeys(name); + await $('.e2e-create-project-description').sendKeys(title); }, - name: function() { - return $$('div[tg-lb-create-project] input[type="text"]').get(0); - }, - description: function() { - return $('div[tg-lb-create-project] textarea'); - }, - errors: function() { - return $$('.checksley-error-list li'); + createProject: function() { + return $('.e2e-create-project-action-submit').click(); } }; return obj; -}; +} helper.delete = async function() { $('.delete-project').click(); diff --git a/e2e/suites/admin/project/create-delete.e2e.js b/e2e/suites/admin/project/create-delete.e2e.js deleted file mode 100644 index 9e3041e2..00000000 --- a/e2e/suites/admin/project/create-delete.e2e.js +++ /dev/null @@ -1,67 +0,0 @@ -var utils = require('../../../utils'); -var createProject = require('../../../helpers').createProject; - -var chai = require('chai'); -var chaiAsPromised = require('chai-as-promised'); - -chai.use(chaiAsPromised); -var expect = chai.expect; - -describe('create-delete project', function() { - before(async function(){ - browser.get(browser.params.glob.host + 'projects/'); - - await utils.common.waitLoader(); - }); - - let lb; - - before(async function() { - lb = createProject.createProjectLightbox(); - - createProject.openWizard(); - - await lb.waitOpen(); - - utils.common.takeScreenshot('project-wizard', 'create-project'); - }); - - it('create project error', async function() { - utils.common.takeScreenshot('project-wizard', 'create-project-errors'); - - await lb.submit(); - - let errors = await lb.errors().count(); - - expect(errors).to.be.equal(2); - }); - - it('create project', async function() { - let originalUrl = await browser.getCurrentUrl(); - - lb.name().sendKeys('aaa'); - lb.description().sendKeys('bbb'); - - await lb.submit(); - - let projectUrl = await browser.getCurrentUrl(); - - expect(projectUrl).not.to.be.equal(originalUrl); - }); - - it('delete', async function() { - let linkAdmin = $('#nav-admin a'); - utils.common.link(linkAdmin); - - browser.wait(function() { - return $('.project-details').isPresent(); - }); - - await createProject.delete(); - await browser.waitForAngular(); - - let url = await browser.getCurrentUrl(); - - expect(url).to.be.equal(browser.params.glob.host); - }); -}); diff --git a/e2e/suites/create-project/duplicate.e2e.js b/e2e/suites/create-project/duplicate.e2e.js new file mode 100644 index 00000000..767e7de3 --- /dev/null +++ b/e2e/suites/create-project/duplicate.e2e.js @@ -0,0 +1,63 @@ +var utils = require('../../utils'); +var createProjectHelper = require('../../helpers/create-project-helper'); +var newProjectScreen = createProjectHelper.newProjectScreen(); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('create-duplicate-delete project', function() { + + it('duplicate project', async function() { + browser.get(browser.params.glob.host + 'project/new'); + await utils.common.waitLoader(); + utils.common.takeScreenshot('new-project', 'new-project'); + await newProjectScreen.selectDuplicateOption(); + utils.common.takeScreenshot('new-project', 'duplicate-project'); + await newProjectScreen.selectProjectToDuplicate(); + let projectName = 'duplicated-project-' + Date.now(); + newProjectScreen.fillNameAndDescription(projectName, 'Lorem Ipsum') + await newProjectScreen.createProject(); + let url = await browser.getCurrentUrl(); + expect(url).to.be.equal(browser.params.glob.host + 'project/admin-' + projectName + '/'); + }); + + it('create scrum project', async function() { + browser.get(browser.params.glob.host + 'project/new'); + await utils.common.waitLoader(); + await newProjectScreen.selectScrumOption(); + utils.common.takeScreenshot('new-project', 'create-scrum-project'); + let projectName = 'scrum-project-' + Date.now(); + await newProjectScreen.fillNameAndDescription(projectName, 'Lorem Ipsum'); + await newProjectScreen.createProject(); + let url = await browser.getCurrentUrl(); + expect(url).to.be.equal(browser.params.glob.host + 'project/admin-' + projectName + '/backlog'); + }); + + it('create kanban project', async function() { + browser.get(browser.params.glob.host + 'project/new'); + await utils.common.waitLoader(); + await newProjectScreen.selectKanbanOption(); + utils.common.takeScreenshot('new-project', 'create-kanban-project'); + let projectName = 'kanban-project-' + Date.now(); + await newProjectScreen.fillNameAndDescription(projectName, 'Lorem Ipsum'); + await newProjectScreen.createProject(); + let url = await browser.getCurrentUrl(); + expect(url).to.be.equal(browser.params.glob.host + 'project/admin-' + projectName + '/kanban'); + }); + + it('delete', async function() { + let linkAdmin = $('#nav-admin a'); + utils.common.link(linkAdmin); + browser.wait(function() { + return $('.project-details').isPresent(); + }); + await createProjectHelper.delete(); + await browser.waitForAngular(); + let url = await browser.getCurrentUrl(); + expect(url).to.be.equal(browser.params.glob.host); + }); + +}); diff --git a/e2e/suites/home.e2e.js b/e2e/suites/home.e2e.js index 2f7a19ea..71d26bd0 100644 --- a/e2e/suites/home.e2e.js +++ b/e2e/suites/home.e2e.js @@ -33,18 +33,6 @@ describe('home', function() { await utils.common.waitLoader(); utils.common.takeScreenshot("home", "projects"); }); - - it('open create project lightbox', async function() { - $('.master .create-project-btn').click(); - - return expect(await utils.lightbox.open('div[tg-lb-create-project]')).to.be.equal(true); - }); - - it('close create project lightbox', async function() { - $('div[tg-lb-create-project] .close').click(); - - return expect(await utils.lightbox.close('div[tg-lb-create-project]')).to.be.equal(true); - }); }); describe("project drag and drop", function() { diff --git a/karma.conf.js b/karma.conf.js index 4e24c1ec..edde0e8f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -19,11 +19,7 @@ module.exports = function(config) { 'karma.app.conf.js', 'dist/**/js/libs.js', 'node_modules/angular-mocks/angular-mocks.js', - 'vendor/bluebird/js/browser/bluebird.js', 'node_modules/chai-jquery/chai-jquery.js', - 'node_modules/chai-jquery/chai-jquery.js', - 'vendor/lodash/dist/lodash.js', - 'vendor/underscore.string/lib/underscore.string.js', 'test-utils.js', 'dist/**/js/app.js', 'dist/**/js/templates.js', diff --git a/locales.js b/locales.js index 76cec254..4357803d 100644 --- a/locales.js +++ b/locales.js @@ -1,4 +1,4 @@ -var glob = require('glob') +var glob = require('glob'); var inquirer = require('inquirer'); var fs = require('fs'); var _ = require('lodash'); @@ -28,13 +28,14 @@ inquirer.prompt([question], function( answer ) { if (answer.command === 'find-duplicates') findDuplicates(); }); +findDuplicates(); + function replaceKeys() { question() .then(searchKey) .then(printFiles) .then(confirm) - .then(replace) - + .then(replace); function question() { return new Promise(function (resolve, reject) { @@ -120,7 +121,8 @@ function replaceKeys() { function findDuplicates() { - glob(app + 'locales/*.json', {}, function (er, files) { + glob(app + 'locales/taiga/*.json', {}, function (er, files) { + console.log(files); files.forEach(duplicates); }); diff --git a/package.json b/package.json index 4d158b25..80fa4db2 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "gulp-util": "^3.0.7", "gulp-wrap": "^0.12.0", "image-size": "^0.5.0", - "inquirer": "^1.0.2", + "inquirer": "^3.0.1", "jade": "^1.11.0", "karma": "^0.13.10", "karma-chai-plugins": "^0.7.0", diff --git a/prism-languages.json b/prism-languages.json index 6d79b676..be5b8e15 100644 --- a/prism-languages.json +++ b/prism-languages.json @@ -1 +1 @@ -[{"file":"prism-abap.min.js","name":"abap"},{"file":"prism-actionscript.min.js","name":"actionscript"},{"file":"prism-ada.min.js","name":"ada"},{"file":"prism-apacheconf.min.js","name":"apacheconf"},{"file":"prism-apl.min.js","name":"apl"},{"file":"prism-applescript.min.js","name":"applescript"},{"file":"prism-asciidoc.min.js","name":"asciidoc"},{"file":"prism-aspnet.min.js","name":"aspnet"},{"file":"prism-autohotkey.min.js","name":"autohotkey"},{"file":"prism-autoit.min.js","name":"autoit"},{"file":"prism-bash.min.js","name":"bash"},{"file":"prism-basic.min.js","name":"basic"},{"file":"prism-batch.min.js","name":"batch"},{"file":"prism-bison.min.js","name":"bison"},{"file":"prism-brainfuck.min.js","name":"brainfuck"},{"file":"prism-bro.min.js","name":"bro"},{"file":"prism-c.min.js","name":"c"},{"file":"prism-clike.min.js","name":"clike"},{"file":"prism-coffeescript.min.js","name":"coffeescript"},{"file":"prism-core.min.js","name":"core"},{"file":"prism-cpp.min.js","name":"cpp"},{"file":"prism-crystal.min.js","name":"crystal"},{"file":"prism-csharp.min.js","name":"csharp"},{"file":"prism-css-extras.min.js","name":"css-extras"},{"file":"prism-css.min.js","name":"css"},{"file":"prism-d.min.js","name":"d"},{"file":"prism-dart.min.js","name":"dart"},{"file":"prism-diff.min.js","name":"diff"},{"file":"prism-docker.min.js","name":"docker"},{"file":"prism-eiffel.min.js","name":"eiffel"},{"file":"prism-elixir.min.js","name":"elixir"},{"file":"prism-erlang.min.js","name":"erlang"},{"file":"prism-fortran.min.js","name":"fortran"},{"file":"prism-fsharp.min.js","name":"fsharp"},{"file":"prism-gherkin.min.js","name":"gherkin"},{"file":"prism-git.min.js","name":"git"},{"file":"prism-glsl.min.js","name":"glsl"},{"file":"prism-go.min.js","name":"go"},{"file":"prism-graphql.min.js","name":"graphql"},{"file":"prism-groovy.min.js","name":"groovy"},{"file":"prism-haml.min.js","name":"haml"},{"file":"prism-handlebars.min.js","name":"handlebars"},{"file":"prism-haskell.min.js","name":"haskell"},{"file":"prism-haxe.min.js","name":"haxe"},{"file":"prism-http.min.js","name":"http"},{"file":"prism-icon.min.js","name":"icon"},{"file":"prism-inform7.min.js","name":"inform7"},{"file":"prism-ini.min.js","name":"ini"},{"file":"prism-j.min.js","name":"j"},{"file":"prism-jade.min.js","name":"jade"},{"file":"prism-java.min.js","name":"java"},{"file":"prism-javascript.min.js","name":"javascript"},{"file":"prism-jolie.min.js","name":"jolie"},{"file":"prism-json.min.js","name":"json"},{"file":"prism-jsx.min.js","name":"jsx"},{"file":"prism-julia.min.js","name":"julia"},{"file":"prism-keyman.min.js","name":"keyman"},{"file":"prism-kotlin.min.js","name":"kotlin"},{"file":"prism-latex.min.js","name":"latex"},{"file":"prism-less.min.js","name":"less"},{"file":"prism-livescript.min.js","name":"livescript"},{"file":"prism-lolcode.min.js","name":"lolcode"},{"file":"prism-lua.min.js","name":"lua"},{"file":"prism-makefile.min.js","name":"makefile"},{"file":"prism-markdown.min.js","name":"markdown"},{"file":"prism-markup.min.js","name":"markup"},{"file":"prism-matlab.min.js","name":"matlab"},{"file":"prism-mel.min.js","name":"mel"},{"file":"prism-mizar.min.js","name":"mizar"},{"file":"prism-monkey.min.js","name":"monkey"},{"file":"prism-nasm.min.js","name":"nasm"},{"file":"prism-nginx.min.js","name":"nginx"},{"file":"prism-nim.min.js","name":"nim"},{"file":"prism-nix.min.js","name":"nix"},{"file":"prism-nsis.min.js","name":"nsis"},{"file":"prism-objectivec.min.js","name":"objectivec"},{"file":"prism-ocaml.min.js","name":"ocaml"},{"file":"prism-oz.min.js","name":"oz"},{"file":"prism-parigp.min.js","name":"parigp"},{"file":"prism-parser.min.js","name":"parser"},{"file":"prism-pascal.min.js","name":"pascal"},{"file":"prism-perl.min.js","name":"perl"},{"file":"prism-php-extras.min.js","name":"php-extras"},{"file":"prism-php.min.js","name":"php"},{"file":"prism-powershell.min.js","name":"powershell"},{"file":"prism-processing.min.js","name":"processing"},{"file":"prism-prolog.min.js","name":"prolog"},{"file":"prism-properties.min.js","name":"properties"},{"file":"prism-protobuf.min.js","name":"protobuf"},{"file":"prism-puppet.min.js","name":"puppet"},{"file":"prism-pure.min.js","name":"pure"},{"file":"prism-python.min.js","name":"python"},{"file":"prism-q.min.js","name":"q"},{"file":"prism-qore.min.js","name":"qore"},{"file":"prism-r.min.js","name":"r"},{"file":"prism-reason.min.js","name":"reason"},{"file":"prism-rest.min.js","name":"rest"},{"file":"prism-rip.min.js","name":"rip"},{"file":"prism-roboconf.min.js","name":"roboconf"},{"file":"prism-ruby.min.js","name":"ruby"},{"file":"prism-rust.min.js","name":"rust"},{"file":"prism-sas.min.js","name":"sas"},{"file":"prism-sass.min.js","name":"sass"},{"file":"prism-scala.min.js","name":"scala"},{"file":"prism-scheme.min.js","name":"scheme"},{"file":"prism-scss.min.js","name":"scss"},{"file":"prism-smalltalk.min.js","name":"smalltalk"},{"file":"prism-smarty.min.js","name":"smarty"},{"file":"prism-sql.min.js","name":"sql"},{"file":"prism-stylus.min.js","name":"stylus"},{"file":"prism-swift.min.js","name":"swift"},{"file":"prism-tcl.min.js","name":"tcl"},{"file":"prism-textile.min.js","name":"textile"},{"file":"prism-twig.min.js","name":"twig"},{"file":"prism-typescript.min.js","name":"typescript"},{"file":"prism-verilog.min.js","name":"verilog"},{"file":"prism-vhdl.min.js","name":"vhdl"},{"file":"prism-vim.min.js","name":"vim"},{"file":"prism-wiki.min.js","name":"wiki"},{"file":"prism-xojo.min.js","name":"xojo"},{"file":"prism-yaml.min.js","name":"yaml"}] \ No newline at end of file +[{"file":"prism-abap.min.js","name":"abap"},{"file":"prism-actionscript.min.js","name":"actionscript"},{"file":"prism-apacheconf.min.js","name":"apacheconf"},{"file":"prism-apl.min.js","name":"apl"},{"file":"prism-applescript.min.js","name":"applescript"},{"file":"prism-asciidoc.min.js","name":"asciidoc"},{"file":"prism-aspnet.min.js","name":"aspnet"},{"file":"prism-autohotkey.min.js","name":"autohotkey"},{"file":"prism-autoit.min.js","name":"autoit"},{"file":"prism-bash.min.js","name":"bash"},{"file":"prism-basic.min.js","name":"basic"},{"file":"prism-batch.min.js","name":"batch"},{"file":"prism-bison.min.js","name":"bison"},{"file":"prism-brainfuck.min.js","name":"brainfuck"},{"file":"prism-bro.min.js","name":"bro"},{"file":"prism-c.min.js","name":"c"},{"file":"prism-clike.min.js","name":"clike"},{"file":"prism-coffeescript.min.js","name":"coffeescript"},{"file":"prism-core.min.js","name":"core"},{"file":"prism-cpp.min.js","name":"cpp"},{"file":"prism-crystal.min.js","name":"crystal"},{"file":"prism-csharp.min.js","name":"csharp"},{"file":"prism-css-extras.min.js","name":"css-extras"},{"file":"prism-css.min.js","name":"css"},{"file":"prism-d.min.js","name":"d"},{"file":"prism-dart.min.js","name":"dart"},{"file":"prism-diff.min.js","name":"diff"},{"file":"prism-docker.min.js","name":"docker"},{"file":"prism-eiffel.min.js","name":"eiffel"},{"file":"prism-elixir.min.js","name":"elixir"},{"file":"prism-erlang.min.js","name":"erlang"},{"file":"prism-fortran.min.js","name":"fortran"},{"file":"prism-fsharp.min.js","name":"fsharp"},{"file":"prism-gherkin.min.js","name":"gherkin"},{"file":"prism-git.min.js","name":"git"},{"file":"prism-glsl.min.js","name":"glsl"},{"file":"prism-go.min.js","name":"go"},{"file":"prism-groovy.min.js","name":"groovy"},{"file":"prism-haml.min.js","name":"haml"},{"file":"prism-handlebars.min.js","name":"handlebars"},{"file":"prism-haskell.min.js","name":"haskell"},{"file":"prism-haxe.min.js","name":"haxe"},{"file":"prism-http.min.js","name":"http"},{"file":"prism-icon.min.js","name":"icon"},{"file":"prism-inform7.min.js","name":"inform7"},{"file":"prism-ini.min.js","name":"ini"},{"file":"prism-j.min.js","name":"j"},{"file":"prism-jade.min.js","name":"jade"},{"file":"prism-java.min.js","name":"java"},{"file":"prism-javascript.min.js","name":"javascript"},{"file":"prism-json.min.js","name":"json"},{"file":"prism-jsx.min.js","name":"jsx"},{"file":"prism-julia.min.js","name":"julia"},{"file":"prism-keyman.min.js","name":"keyman"},{"file":"prism-kotlin.min.js","name":"kotlin"},{"file":"prism-latex.min.js","name":"latex"},{"file":"prism-less.min.js","name":"less"},{"file":"prism-lolcode.min.js","name":"lolcode"},{"file":"prism-lua.min.js","name":"lua"},{"file":"prism-makefile.min.js","name":"makefile"},{"file":"prism-markdown.min.js","name":"markdown"},{"file":"prism-markup.min.js","name":"markup"},{"file":"prism-matlab.min.js","name":"matlab"},{"file":"prism-mel.min.js","name":"mel"},{"file":"prism-mizar.min.js","name":"mizar"},{"file":"prism-monkey.min.js","name":"monkey"},{"file":"prism-nasm.min.js","name":"nasm"},{"file":"prism-nginx.min.js","name":"nginx"},{"file":"prism-nim.min.js","name":"nim"},{"file":"prism-nix.min.js","name":"nix"},{"file":"prism-nsis.min.js","name":"nsis"},{"file":"prism-objectivec.min.js","name":"objectivec"},{"file":"prism-ocaml.min.js","name":"ocaml"},{"file":"prism-oz.min.js","name":"oz"},{"file":"prism-parigp.min.js","name":"parigp"},{"file":"prism-parser.min.js","name":"parser"},{"file":"prism-pascal.min.js","name":"pascal"},{"file":"prism-perl.min.js","name":"perl"},{"file":"prism-php-extras.min.js","name":"php-extras"},{"file":"prism-php.min.js","name":"php"},{"file":"prism-powershell.min.js","name":"powershell"},{"file":"prism-processing.min.js","name":"processing"},{"file":"prism-prolog.min.js","name":"prolog"},{"file":"prism-protobuf.min.js","name":"protobuf"},{"file":"prism-puppet.min.js","name":"puppet"},{"file":"prism-pure.min.js","name":"pure"},{"file":"prism-python.min.js","name":"python"},{"file":"prism-q.min.js","name":"q"},{"file":"prism-qore.min.js","name":"qore"},{"file":"prism-r.min.js","name":"r"},{"file":"prism-rest.min.js","name":"rest"},{"file":"prism-rip.min.js","name":"rip"},{"file":"prism-roboconf.min.js","name":"roboconf"},{"file":"prism-ruby.min.js","name":"ruby"},{"file":"prism-rust.min.js","name":"rust"},{"file":"prism-sas.min.js","name":"sas"},{"file":"prism-sass.min.js","name":"sass"},{"file":"prism-scala.min.js","name":"scala"},{"file":"prism-scheme.min.js","name":"scheme"},{"file":"prism-scss.min.js","name":"scss"},{"file":"prism-smalltalk.min.js","name":"smalltalk"},{"file":"prism-smarty.min.js","name":"smarty"},{"file":"prism-sql.min.js","name":"sql"},{"file":"prism-stylus.min.js","name":"stylus"},{"file":"prism-swift.min.js","name":"swift"},{"file":"prism-tcl.min.js","name":"tcl"},{"file":"prism-textile.min.js","name":"textile"},{"file":"prism-twig.min.js","name":"twig"},{"file":"prism-typescript.min.js","name":"typescript"},{"file":"prism-verilog.min.js","name":"verilog"},{"file":"prism-vhdl.min.js","name":"vhdl"},{"file":"prism-vim.min.js","name":"vim"},{"file":"prism-wiki.min.js","name":"wiki"},{"file":"prism-yaml.min.js","name":"yaml"}] \ No newline at end of file