Refactor epics module (need tests)

stable
David Barragán Merino 2016-09-07 15:29:00 +02:00
parent da6cc67897
commit 99e04c369f
21 changed files with 286 additions and 195 deletions

View File

@ -812,6 +812,7 @@ modules = [
"taigaHistory",
"taigaWikiHistory",
"taigaEpics",
"taigaUtils"
# template cache
"templates",

View File

@ -405,6 +405,7 @@
},
"EPICS": {
"TITLE": "EPICS",
"SECTION_NAME": "Epics",
"EPIC": "EPIC",
"DASHBOARD": {
"ADD": "+ ADD EPIC",

View File

@ -24,13 +24,19 @@ getRandomDefaultColor = taiga.getRandomDefaultColor
class CreateEpicController
@.$inject = [
"tgResources"
"$tgConfirm"
"tgAttachmentsService"
"$q"
"tgProjectService",
"tgEpicsService"
]
constructor: (@rs, @confirm, @attachmentsService, @q) ->
constructor: (@confirm, @projectService, @epicsService) ->
# NOTE: To use Checksley setFormErrors() and validateForm()
# are defined in the directive.
# NOTE: We use project as no inmutable object to make
# the code compatible with the old code
@.project = @projectService.project.toJS()
@.newEpic = {
color: getRandomDefaultColor()
project: @.project.id
@ -39,25 +45,21 @@ class CreateEpicController
}
@.attachments = Immutable.List()
@.loading = false
createEpic: () ->
return if not @.validateForm()
@.loading = true
promise = @rs.epics.post(@.newEpic)
promise.then (response) =>
@._createAttachments(response.data)
promise.then (response) =>
@.onCreateEpic()
promise.then null, (response) =>
@.setFormErrors(response.data)
if response.data._error_message
confirm.notify("error", response.data._error_message)
promise.finally () =>
@.loading = false
return promise
@epicsService.createEpic(@.epic, @.attachments)
.then (response) => # On success
@.onCreateEpic()
.then null, (response) => # On error
@.setFormErrors(response.data)
if response.data._error_message
@confirm.notify("error", response.data._error_message)
@.loading = false
# Color selector
selectColor: (color) ->
@ -77,9 +79,4 @@ class CreateEpicController
addAttachment: (attachment) ->
@.attachments.push(attachment)
_createAttachments: (epic) ->
promises = _.map @.attachments.toJS(), (attachment) =>
return @attachmentsService.upload(attachment.file, epic.id, epic.project, 'epic')
return @q.all(promises)
angular.module("taigaEpics").controller("CreateEpicCtrl", CreateEpicController)

View File

@ -17,8 +17,6 @@
# File: create-epic.directive.coffee
###
module = angular.module('taigaEpics')
CreateEpicDirective = () ->
link = (scope, el, attrs, ctrl) ->
form = el.find("form").checksley()
@ -35,12 +33,9 @@ CreateEpicDirective = () ->
controller: "CreateEpicCtrl",
controllerAs: "vm",
bindToController: {
project: '=',
onCreateEpic: '&'
},
scope: {}
}
CreateEpicDirective.$inject = []
module.directive("tgCreateEpic", CreateEpicDirective)
angular.module('taigaEpics').directive("tgCreateEpic", CreateEpicDirective)

View File

@ -33,11 +33,11 @@ tg-lightbox-close
)
fieldset.tags-block
tg-tag-line-common(
project="vm.project"
tags="vm.newEpic.tags"
permissions="add_epic"
on-add-tag="vm.addTag(name, color)"
on-delete-tag="vm.deleteTag(tag)"
project="vm.project"
tags="vm.newEpic.tags"
permissions="add_epic"
on-add-tag="vm.addTag(name, color)"
on-delete-tag="vm.deleteTag(tag)"
)
fieldset
textarea.e2e-create-epic-description(

View File

@ -17,19 +17,23 @@
# File: epics-table.controller.coffee
###
module = angular.module("taigaEpics")
class EpicRowController
@.$inject = [
"tgResources",
"$tgConfirm"
"$tgConfirm",
"tgProjectService",
"tgEpicsService"
]
constructor: (@rs, @confirm) ->
constructor: (@confirm, @projectService, @epicsService) ->
@.displayUserStories = false
@.displayAssignedTo = false
@.displayStatusList = false
@.loadingStatus = false
# NOTE: We use project as no inmutable object to make
# the code compatible with the old code
@.project = @projectService.project.toJS()
_calculateProgressBar: () ->
if @.epic.getIn(['status_extra_info', 'is_closed']) == true
@.percentage = "100%"
@ -42,68 +46,32 @@ class EpicRowController
else
@.percentage = "#{@.closed * 100 / @.total}%"
updateEpicStatus: (status) ->
@.loadingStatus = true
@.displayStatusList = false
patch = {
'status': status,
'version': @.epic.get('version')
}
canEditEpics: () ->
return @projectService.hasPermission("modify_epic")
onSuccess = =>
@.loadingStatus = false
@.onUpdateEpic()
onError = (data) =>
@confirm.notify('error')
return @rs.epics.patch(@.epic.get('id'), patch).then(onSuccess, onError)
requestUserStories: (epic) ->
toggleUserStoryList: () ->
if !@.displayUserStories
onSuccess = (data) =>
@.epicStories = data
@.displayUserStories = true
onError = (data) =>
@confirm.notify('error')
return @rs.userstories.listInEpic(@.epic.get('id')).then(onSuccess, onError)
@epicsService.listRelatedUserStories(@.epic)
.then (userStories) =>
@.epicStories = userStories
@.displayUserStories = true
.catch =>
@confirm.notify('error')
else
@.displayUserStories = false
onRemoveAssigned: () ->
id = @.epic.get('id')
version = @.epic.get('version')
patch = {
'assigned_to': null,
'version': version
}
updateStatus: (statusId) ->
@.displayStatusList = false
@.loadingStatus = true
return @epicsService.updateEpicStatus(@.epic, statusId)
.catch () =>
@confirm.notify('error')
.finally () =>
@.loadingStatus = false
onSuccess = =>
@.onUpdateEpic()
updateAssignedTo: (member) ->
return @epicsService.updateEpicAssignedTo(@.epic, member?.id)
.catch () =>
@confirm.notify('error')
onError = (data) =>
@confirm.notify('error')
return @rs.epics.patch(id, patch).then(onSuccess, onError)
onAssignTo: (member) ->
id = @.epic.get('id')
version = @.epic.get('version')
patch = {
'assigned_to': member.id,
'version': version
}
onSuccess = =>
@.onUpdateEpic()
@confirm.notify('success')
onError = (data) =>
@confirm.notify('error')
return @rs.epics.patch(id, patch).then(onSuccess, onError)
module.controller("EpicRowCtrl", EpicRowController)
angular.module("taigaEpics").controller("EpicRowCtrl", EpicRowController)

View File

@ -17,28 +17,16 @@
# File: epics-table.directive.coffee
###
module = angular.module('taigaEpics')
EpicRowDirective = () ->
link = (scope, el, attrs, ctrl) ->
ctrl._calculateProgressBar()
return {
link: link,
templateUrl:"epics/dashboard/epic-row/epic-row.html",
controller: "EpicRowCtrl",
controllerAs: "vm",
bindToController: true,
scope: {
project: '=',
epic: '=',
column: '=',
permissions: '=',
onUpdateEpic: "&"
}
}
EpicRowDirective.$inject = []
module.directive("tgEpicRow", EpicRowDirective)
angular.module('taigaEpics').directive("tgEpicRow", EpicRowDirective)

View File

@ -1,10 +1,11 @@
.epic-row.e2e-epic-row(
ng-class="{'is-blocked': vm.epic.get('is_blocked'), 'is-closed': vm.epic.get('is_closed'), 'unfold': vm.displayUserStories}"
ng-click="vm.requestUserStories(vm.epic)"
ng-click="vm.toggleUserStoryList()"
)
tg-svg.icon-drag(
svg-icon="icon-drag"
)
.vote(
ng-if="vm.column.votes"
ng-class="{'is-voter': vm.epic.get('is_voter')}"
@ -28,23 +29,26 @@
)
.project(ng-if="vm.column.project")
.sprint(
ng-if="vm.column.sprint"
)
.assigned.e2e-assigned-to
.sprint(ng-if="vm.column.sprint")
.assigned.e2e-assigned-tio(ng-if="vm.column.assigned")
tg-assigned-to-component(
assigned-to="vm.epic.get('assigned_to_extra_info')"
project="vm.project"
on-remove-assigned="vm.onRemoveAssigned()"
on-assign-to="vm.onAssignTo(member)"
on-remove-assigned="vm.updateAssignedTo(null)"
on-assign-to="vm.updateAssignedTo(member)"
tg-isolate-click
)
.status(
ng-if="vm.column.status && !vm.permissions.canEdit"
ng-if="vm.column.status && !vm.canEditEpics()"
)
span {{vm.epic.getIn(['status_extra_info', 'name'])}}
.status(
ng-if="vm.column.status && vm.permissions.canEdit"
ng-if="vm.column.status && vm.canEditEpics()"
ng-mouseleave="vm.displayStatusList = false"
tg-isolate-click
)
button(
ng-click="vm.displayStatusList = true"
@ -59,20 +63,19 @@
ul.epic-statuses(ng-if="vm.displayStatusList")
li.e2e-edit-epic-status(
ng-repeat="status in vm.project.epic_statuses | orderBy:'order'"
ng-click="vm.updateEpicStatus(status.id)"
ng-click="vm.updateStatus(status.id)"
) {{status.name}}
.progress(ng-if="vm.column.progress")
.progress-bar
.progress-status(
ng-if="::vm.percentage"
ng-style="{'width':vm.percentage}"
)
.epic-stories-wrapper(ng-if="vm.displayUserStories && vm.epicStories")
.epic-stories-wrapper(ng-if="vm.displayUserStories && vm.epicStories")
.epic-story(tg-repeat="story in vm.epicStories track by story.get('id')")
tg-story-row.e2e-story(
epic="vm.epic"
story="story"
project="vm.project"
column="vm.column"
)

View File

@ -17,48 +17,50 @@
# File: epics.dashboard.controller.coffee
###
module = angular.module("taigaEpics")
taiga = @.taiga
class EpicsDashboardController
@.$inject = [
"$tgResources",
"tgResources",
"$routeParams",
"tgErrorHandlingService",
"tgLightboxFactory",
"lightboxService",
"$tgConfirm"
"$tgConfirm",
"tgProjectService",
"tgEpicsService"
]
constructor: (@rs, @resources, @params, @errorHandlingService, @lightboxFactory, @lightboxService, @confirm) ->
@.sectionName = "Epics"
@.createEpic = false
constructor: (@params, @errorHandlingService, @lightboxFactory, @lightboxService,
@confirm, @projectService, @epicsService) ->
loadProject: () ->
return @rs.projects.getBySlug(@params.pslug).then (project) =>
if not project.is_epics_activated
@errorHandlingService.permissionDenied()
@.project = project
@.loadEpics()
@.sectionName = "EPICS.SECTION_NAME"
loadEpics: () ->
projectId = @.project.id
return @resources.epics.list(projectId).then (epics) =>
@.epics = epics
taiga.defineImmutableProperty @, 'project', () => return @projectService.project
taiga.defineImmutableProperty @, 'epics', () => return @epicsService.epics
_onCreateEpic: () ->
@lightboxService.closeAll()
@confirm.notify("success")
@.loadEpics()
@._loadInitialData()
_loadInitialData: () ->
@epicsService.clear()
@projectService.setProjectBySlug(@params.pslug)
.then () =>
if not @.project.get("is_epics_activated") or not @projectService.hasPermission("view_epics")
@errorHandlingService.permissionDenied()
@epicsService.fetchEpics()
canCreateEpics: () ->
return @projectService.hasPermission("add_epic")
onCreateEpic: () ->
@lightboxFactory.create('tg-create-epic', {
"class": "lightbox lightbox-create-epic open"
"project": "project"
"on-create-epic": "onCreateEpic()"
}, {
"project": @.project
"onCreateEpic": @._onCreateEpic.bind(this)
"onCreateEpic": () =>
@lightboxService.closeAll()
@confirm.notify("success")
})
module.controller("EpicsDashboardCtrl", EpicsDashboardController)
angular.module("taigaEpics").controller("EpicsDashboardCtrl", EpicsDashboardController)

View File

@ -4,10 +4,10 @@
header.header-with-actions
h1(
tg-main-title
project-name="vm.project.name"
i18n-section-name="{{ vm.sectionName }}"
project-name="vm.project.get('name')"
i18n-section-name="{{vm.sectionName}}"
)
.action-buttons(ng-if="vm.epics.size")
.action-buttons(ng-if="vm.epics.size && vm.canCreateEpics()")
button.button-green.e2e-create-epic(
translate="EPICS.DASHBOARD.ADD"
title="{{ EPICS.DASHBOARD.ADD_TITLE | translate }}",
@ -15,10 +15,7 @@
)
tg-epics-table(
ng-if="vm.project && vm.epics.size"
project="vm.project"
epics="vm.epics"
on-update-epic="vm.loadEpics()"
ng-if="vm.epics.size"
)
section.empty-epics(ng-if="!vm.epics.size")
@ -30,11 +27,12 @@
p(translate="EPICS.EMPTY.EXPLANATION")
a(
translate="EPICS.EMPTY.HELP"
href="https://tree.taiga.io/support/frequently-asked-questions/who-is-taiga-for/"
href="#TODO: Link to Epics section in taiga-support"
target="_blank"
ng-title="EPICS.EMPTY.HELP | translate"
)
button.create-epic.button-green(
ng-if="vm.canCreateEpics()"
translate="EPICS.DASHBOARD.ADD"
title="{{ EPICS.DASHBOARD.ADD_TITLE | translate }}"
ng-click="vm.onCreateEpic()"

View File

@ -17,8 +17,10 @@
# File: epics-sortable.directive.coffee
###
EpicsSortableDirective = ($parse) ->
EpicsSortableDirective = ($parse, projectService) ->
link = (scope, el, attrs) ->
return if not projectService.hasPermission("modify_epic")
callback = $parse(attrs.tgEpicsSortable)
drake = dragula([el[0]], {
@ -55,7 +57,8 @@ EpicsSortableDirective = ($parse) ->
}
EpicsSortableDirective.$inject = [
"$parse"
"$parse",
"tgProjectService"
]
angular.module("taigaComponents").directive("tgEpicsSortable", EpicsSortableDirective)

View File

@ -17,12 +17,16 @@
# File: epics-table.controller.coffee
###
module = angular.module("taigaEpics")
taiga = @.taiga
class EpicsTableController
@.$inject = []
@.$inject = [
"$tgConfirm",
"tgEpicsService"
]
constructor: () ->
constructor: (@confirm, @epicsService) ->
@.displayOptions = false
@.displayVotes = true
@.column = {
@ -35,15 +39,14 @@ class EpicsTableController
progress: true
}
@.permissions = {
canEdit: _.includes(@.project.my_permissions, 'modify_epic')
}
taiga.defineImmutableProperty @, 'epics', () => return @epicsService.epics
toggleEpicTableOptions: () ->
@.displayOptions = !@.displayOptions
reorderEpic: (epic, newIndex) ->
console.log epic, newIndex
@epicsService.reorderEpic(epic, newIndex)
.then null, () => # on error
@confirm.notify("error")
module.controller("EpicsTableCtrl", EpicsTableController)
angular.module("taigaEpics").controller("EpicsTableCtrl", EpicsTableController)

View File

@ -17,21 +17,13 @@
# File: epics-table.directive.coffee
###
module = angular.module('taigaEpics')
EpicsTableDirective = () ->
return {
templateUrl:"epics/dashboard/epics-table/epics-table.html",
controller: "EpicsTableCtrl",
controllerAs: "vm",
bindToController: {
epics: "=",
project: "=",
onUpdateEpic: "&"
}
scope: {}
}
EpicsTableDirective.$inject = []
module.directive("tgEpicsTable", EpicsTableDirective)
angular.module('taigaEpics').directive("tgEpicsTable", EpicsTableDirective)

View File

@ -95,8 +95,5 @@ mixin epicSwitch(name, model)
)
tg-epic-row.e2e-epic(
epic="epic"
project="vm.project"
column="vm.column"
on-update-epic="vm.onUpdateEpic()"
permissions="vm.permissions"
)

View File

@ -29,13 +29,8 @@ class StoryRowController
if @.story.get('is_closed') == true
@.percentage = "100%"
else
tasks = @.story.get('tasks').toJS()
totalTasks = @.story.get('tasks').size
areTasksCompleted = _.map(tasks, 'is_closed')
totalTasksCompleted = _.pull(areTasksCompleted, false).length
totalTasksCompleted = @.story.get('tasks').filter((it) -> it.get("is_closed")).size
@.percentage = "#{totalTasksCompleted * 100 / totalTasks}%"
onSelectAssignedTo: () ->
console.log 'ng-click="vm.onSelectAssignedTo()"'
module.controller("StoryRowCtrl", StoryRowController)

View File

@ -20,20 +20,15 @@
module = angular.module('taigaEpics')
StoryRowDirective = () ->
return {
templateUrl:"epics/dashboard/story-row/story-row.html",
controller: "StoryRowCtrl",
controllerAs: "vm",
bindToController: true,
scope: {
epic: '=',
story: '=',
project: '=',
column: '='
}
}
StoryRowDirective.$inject = []
module.directive("tgStoryRow", StoryRowDirective)

View File

@ -1,5 +1,5 @@
.story-row(
ng-class="{'is-blocked': vm.story.is_blocked, 'is-closed': vm.story.is_closed}"
ng-class="{'is-blocked': vm.story.get('is_blocked'), 'is-closed': vm.story.get('is_closed')}"
)
.vote(
ng-if="vm.column.votes"
@ -11,12 +11,12 @@
.name(ng-if="vm.column.name")
- var hash = "#";
a(
tg-nav="project-userstories-detail:project=vm.project.slug,ref=vm.story.get('ref')"
tg-nav="project-userstories-detail:project=vm.story.getIn(['project_extra_info', 'slug']),ref=vm.story.get('ref')"
ng-attr-title="{{::vm.story.get('subject')}}"
) #{hash}{{::vm.story.get('ref')}} {{::vm.story.get('subject')}}
tg-belong-to-epics(
format="pill"
ng-if="vm.story.get('epics')"
format="pill"
epics="vm.story.get('epics')"
)
.project(

View File

@ -0,0 +1,99 @@
###
# 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: epics.service.coffee
###
taiga = @.taiga
class EpicsService
@.$inject = [
"tgProjectService",
"tgAttachmentsService"
"tgResources",
"tgXhrErrorService",
"$q"
]
constructor: (@projectService, @attachmentsService, @resources, @xhrError, @q) ->
@._epics = Immutable.List()
taiga.defineImmutableProperty @, "epics", () => return @._epics
clear: () ->
@._epics = Immutable.List()
fetchEpics: () ->
return @resources.epics.list(@projectService.project.get("id"))
.then (epics) =>
@._epics = epics
.catch (xhr) =>
@xhrError.response(xhr)
listRelatedUserStories: (epic) ->
return @resources.userstories.listInEpic(epic.get('id'))
createEpic: (epicData, attachments) ->
@.epicData.project = @projectsService.project.id
return @resources.epics.post(@.epicData)
.then (epic) =>
promises = _.map attachments.toJS(), (attachment) =>
@attachmentsService.upload(attachment.file, epic.get("id"), epic.get("project"), 'epic')
@q.all(promises).then () =>
@.fetchEpics()
reorderEpic: (epic, newIndex) ->
withoutMoved = @.epics.filter (it) => it.get("id") != epic.get("id")
beforeDestination = withoutMoved.slice(0, newIndex)
previous = beforeDestination.last()
newOrder = if !previous then 0 else epic.get("epics_order") + 1
previousWithTheSameOrder = beforeDestination.filter (it) =>
it.get("epics_order") == previous.get("epics_order")
setOrders = Immutable.OrderedMap previousWithTheSameOrder.map (it) =>
[it.get('id'), it.get("epics_order")]
data = {
order: newOrder,
version: epic.get("version")
}
return @resources.epics.reorder(epic.get("id"), data, setOrders)
.then () =>
@.fetchEpics()
updateEpicStatus: (epic, statusId) ->
data = {
status: statusId,
version: epic.get("version")
}
return @resources.epics.patch(epic.get("id"), data)
.then () =>
@.fetchEpics()
updateEpicAssignedTo: (epic, userId) ->
data = {
assigned_to: userId,
version: epic.get("version")
}
return @resources.epics.patch(epic.get("id"), data)
.then () =>
@.fetchEpics()
angular.module("taigaEpics").service("tgEpicsService", EpicsService)

View File

@ -51,6 +51,13 @@ Resource = (urlsService, http) ->
return http.post(url, params)
service.reorder = (id, data, setOrders) ->
url = urlsService.resolve("epics") + "/#{id}"
options = {"headers": {"set-orders": JSON.stringify(setOrders)}}
return http.patch(url, data, null, options)
service.addRelatedUserstory = (epicId, userstoryId) ->
url = urlsService.resolve("epic-related-userstories", epicId)

View File

@ -0,0 +1,27 @@
###
# 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: isolate-click.directive.coffee
###
IsolateClickDirective = () ->
link = (scope, el, attrs) ->
el.on 'click', (e) =>
e.stopPropagation()
return {link: link}
angular.module("taigaUtils").directive("tgIsolateClick", IsolateClickDirective)

View File

@ -0,0 +1,20 @@
###
# 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: utils.module.coffee
###
module = angular.module("taigaUtils", [])