Improve 'create project' (new, import, duplicate)

stable
Xavier Julián 2016-11-11 09:03:03 +01:00 committed by David Barragán Merino
parent 466ba6de1e
commit 0cfef30885
151 changed files with 7151 additions and 777 deletions

View File

@ -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.

View File

@ -15,6 +15,7 @@ window.taigaConfig = {
"privacyPolicyUrl": null,
"termsOfServiceUrl": null,
"maxUploadFileSize": null,
"importers": [],
"contribPlugins": []
}

View File

@ -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: "<tg-create-project-form type=\"scrum\"></tg-create-project-form>",
loader: true
}
)
# Project - kanban
$routeProvider.when("/project/new/kanban",
{
title: "PROJECT.CREATE.TITLE",
template: "<tg-create-project-form type=\"kanban\"></tg-create-project-form>",
loader: true
}
)
# Project - duplicate
$routeProvider.when("/project/new/duplicate",
{
title: "PROJECT.CREATE.TITLE",
template: "<tg-duplicate-project></tg-duplicate-project>",
loader: true
}
)
# Project - import
$routeProvider.when("/project/new/import/:platform?",
{
title: "PROJECT.CREATE.TITLE",
template: "<tg-import-project></tg-import-project>",
loader: true
}
)
# Project
$routeProvider.when("/project/:pslug/",
{

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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 = """
<a class="close" href="" title="{{'COMMON.CLOSE' | translate}}">
<a class="close" ng-click="onClose()" href="" title="{{'COMMON.CLOSE' | translate}}">
<tg-svg svg-icon="icon-close"></tg-svg>
</a>
"""
link = (scope, elm, attrs) ->
return {
scope: {
onClose: '&'
},
link: link,
template: template
}

View File

@ -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)

View File

@ -1,153 +0,0 @@
###
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
# Copyright (C) 2014-2016 Xavi Julian <xavier.julian@kaleidos.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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}

View File

@ -1,8 +0,0 @@
module = angular.module("taigaProject")
createProjectRestrictionDirective = () ->
return {
templateUrl: "project/wizard-restrictions.html"
}
module.directive('tgCreateProjectRestriction', [createProjectRestrictionDirective])

View File

@ -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
#############################################################################

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -203,7 +203,6 @@
"CONFIRM_DELETE": "Remeber that all values in this custom field will be deleted.\n Are you sure you want to continue?"
},
"FILTERS": {
"TITLE": "filters",
"INPUT_PLACEHOLDER": "Subject or reference",
"TITLE_ACTION_FILTER_BUTTON": "search",
"TITLE": "Filters",
@ -875,10 +874,8 @@
"SECTION_TITLE": "Your projects",
"PLACEHOLDER_SEARCH": "Search in...",
"ACTION_CREATE_PROJECT": "Create project",
"ACTION_IMPORT_PROJECT": "Import project",
"MANAGE_PROJECTS": "Manage projects",
"TITLE_CREATE_PROJECT": "Create project",
"TITLE_IMPORT_PROJECT": "Import project",
"TITLE_PRVIOUS_PROJECT": "Show previous projects",
"TITLE_NEXT_PROJECT": "Show next projects",
"HELP_TITLE": "Taiga Support Page",
@ -904,44 +901,6 @@
"DISCOVER": "Discover",
"ACTION_REORDER": "Drag & drop to reorder"
},
"IMPORT": {
"TITLE": "Importing Project",
"UPLOADING_FILE": "Uploading dump file",
"DESCRIPTION": "This process can take a while, please keep the window open.",
"ASYNC_IN_PROGRESS_TITLE": "Our Oompa Loompas are importing your project",
"ASYNC_IN_PROGRESS_MESSAGE": "This process could take a few minutes <br/> 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 <strong>{{user_external}}</strong> the same person as <strong>{{user_internal}}</strong>?",
"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 }}",

View File

@ -0,0 +1,39 @@
###
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
# Copyright (C) 2014-2016 Xavi Julian <xavier.julian@kaleidos.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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])

View File

@ -27,7 +27,6 @@ FileChangeDirective = ($parse) ->
scope.$on "$destroy", -> el.off()
return {
require: "ngModel",
restrict: "A",
link: link
}

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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")

View File

@ -0,0 +1,55 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,40 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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'")

View File

@ -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);
}
}

View File

@ -0,0 +1,73 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,172 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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()

View File

@ -0,0 +1,35 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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()"
)

View File

@ -0,0 +1,57 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,128 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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()

View File

@ -0,0 +1,63 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,139 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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

View File

@ -0,0 +1,31 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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'")

View File

@ -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])

View File

@ -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}"
)

View File

@ -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])

View File

@ -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 }}

View File

@ -0,0 +1,56 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,45 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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

View File

@ -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")

View File

@ -0,0 +1,3 @@
.create-project {
@include create-project;
}

View File

@ -0,0 +1,85 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,222 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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

View File

@ -0,0 +1,35 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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'")

View File

@ -0,0 +1,5 @@
.duplicate-project {
&-reference {
margin-bottom: 2rem;
}
}

View File

@ -0,0 +1,56 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,40 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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'")

View File

@ -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);
}
}

View File

@ -0,0 +1,74 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,172 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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()

View File

@ -0,0 +1,35 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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()"
)

View File

@ -0,0 +1,55 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,129 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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()

View File

@ -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"
)

View File

@ -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
)

View File

@ -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")

View File

@ -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
)

View File

@ -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")

View File

@ -0,0 +1,150 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,367 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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

View File

@ -0,0 +1,43 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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"
)

View File

@ -0,0 +1,3 @@
.import-project-members {
@include import-members;
}

View File

@ -0,0 +1,24 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# File: import-project-selector.controller.coffee
###
class ImportProjectSelectorController
selectProject: (project) ->
@.onSelectProject({project: Immutable.fromJS(project)})
angular.module('taigaProjects').controller('ImportProjectSelectorCtrl', ImportProjectSelectorController)

View File

@ -0,0 +1,36 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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"
)

View File

@ -0,0 +1,3 @@
.import-project-selector {
@include import-project-selector;
}

View File

@ -0,0 +1,43 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,29 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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")

View File

@ -0,0 +1,2 @@
section.import-project-from
h1.create-project-title(translate="PROJECT.IMPORT.TITLE")

View File

@ -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)

View File

@ -0,0 +1,113 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,183 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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")

View File

@ -0,0 +1,38 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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()"
)

View File

@ -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;
}
}

View File

@ -0,0 +1,121 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,294 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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
}
})

View File

@ -0,0 +1,26 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# File: invite-members.controller.coffee
###
class InviteMembersController
@.$inject = []
isDisabled: (id) ->
return @.invitedMembers.indexOf(id) == -1
angular.module("taigaProjects").controller("InviteMembersCtrl", InviteMembersController)

View File

@ -0,0 +1,38 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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')})"
)

View File

@ -0,0 +1,7 @@
.create-project-invite {
&-avatars {
display: flex;
flex-wrap: wrap;
}
}

View File

@ -0,0 +1,31 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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')}}"
)

View File

@ -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;
}
}

View File

@ -0,0 +1,58 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -0,0 +1,40 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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'")

View File

@ -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;
}
}

View File

@ -0,0 +1,78 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

Some files were not shown because too many files have changed in this diff Show More