Relate to Epic button & lightbox

stable
Daniel García 2018-03-06 11:29:08 +01:00 committed by Alex Hermida
parent 0134ba0d26
commit 7ffa2ee292
13 changed files with 489 additions and 53 deletions

View File

@ -1122,4 +1122,120 @@ module.directive("tgLbCreateEdit", [
"$tgTemplate", "$tgTemplate",
"$compile", "$compile",
CreateEditDirective CreateEditDirective
]) ])
#############################################################################
## RelateToEpic Lightbox Directive
#############################################################################
debounceLeading = @.taiga.debounceLeading
RelateToEpicLightboxDirective = ($rootScope, $confirm, lightboxService, tgCurrentUserService
tgResources, $tgResources, $epicsService, tgAnalytics) ->
link = ($scope, $el, $attrs) ->
us = null
$scope.projects = null
$scope.projectEpics = Immutable.List()
$scope.loading = false
newEpicForm = $el.find(".new-epic-form").checksley()
existingEpicForm = $el.find(".existing-epic-form").checksley()
loadProjects = ->
if $scope.projects == null
$tgResources.projects.list({
blocked_code: 'null',
is_epics_activated: true
}).then (data) ->
$scope.projects = data
filterEpics = (selectedProjectId, filterText) ->
tgResources.epics.listInAllProjects(
{
is_epics_activated: true,
project__blocked_code: 'null',
project: selectedProjectId,
q: filterText
}, true).then (data) ->
excludeIds = []
if (us.epics)
excludeIds = us.epics.map((epic) -> epic.id)
filteredData = data.filter((epic) -> excludeIds.indexOf(epic.get('id')) == -1)
$scope.projectEpics = filteredData
$el.on "click", ".close", (event) ->
event.preventDefault()
lightboxService.close($el)
$scope.$on "relate-to-epic:add", (ctx, item) ->
us = item
$scope.selectedEpic = null
$scope.searchEpic = ""
loadProjects()
filterEpics(item.projectId, $scope.searchEpic).then () ->
lightboxService.open($el).then ->
$el.find('input').focus
$scope.$on "$destroy", ->
$el.off()
$scope.onUpdateSearchEpic = debounceLeading 300, () ->
$scope.selectedEpic = null
filterEpics($scope.selectedProject, $scope.searchEpic)
$scope.saveRelatedEpic = (selectedEpicId, onSavedRelatedEpic) ->
return if not existingEpicForm.validate()
$scope.loading = true
onError = (data) ->
$scope.loading = false
$confirm.notify("error")
existingEpicForm.setErrors(data)
onSuccess = (data) ->
tgAnalytics.trackEvent(
"user story related epic", "create", "create related epic on user story", 1)
$scope.loading = false
$rootScope.$broadcast("related-epics:changed", us)
lightboxService.close($el)
usId = us.id
tgResources.epics.addRelatedUserstory(selectedEpicId, usId).then(
onSuccess, onError)
$scope.createEpic = (selectedProjectId, epicSubject) ->
return if not newEpicForm.validate()
@.loading = true
onError = (data)->
$scope.loading = false
$confirm.notify("error")
newEpicForm.setErrors(errors)
onSuccess = () ->
tgAnalytics.trackEvent(
"user story related epic", "create", "create related epic on user story", 1)
$scope.loading = false
$rootScope.$broadcast("related-epics:changed", us)
lightboxService.close($el)
onCreateEpic = (epic) ->
epicId = epic.get('id')
usId = us.id
tgResources.epics.addRelatedUserstory(epicId, usId).then(onSuccess, onError)
$epicsService.createEpic(
{subject: epicSubject}, null, selectedProjectId).then(onCreateEpic, onError)
return {
templateUrl: "common/lightbox/lightbox-relate-to-epic.html"
link:link
}
module.directive("tgLbRelatetoepic", [
"$rootScope", "$tgConfirm", "lightboxService", "tgCurrentUserService", "tgResources",
"$tgResources", "tgEpicsService", "$tgAnalytics", RelateToEpicLightboxDirective])

View File

@ -36,8 +36,10 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $translate) ->
service.getBySlug = (projectSlug) -> service.getBySlug = (projectSlug) ->
return $repo.queryOne("projects", "by_slug?slug=#{projectSlug}") return $repo.queryOne("projects", "by_slug?slug=#{projectSlug}")
service.list = -> service.list = (filters) ->
return $repo.queryMany("projects") params = {"order_by": "user_order"}
params = _.extend({}, params, filters or {})
return $repo.queryMany("projects", params)
service.listByMember = (memberId) -> service.listByMember = (memberId) ->
params = {"member": memberId, "order_by": "user_order"} params = {"member": memberId, "order_by": "user_order"}

View File

@ -1127,6 +1127,10 @@
"DELETE_SPRINT": { "DELETE_SPRINT": {
"TITLE": "Delete sprint" "TITLE": "Delete sprint"
}, },
"REMOVE_RELATIONSHIP_WITH_EPIC": {
"TITLE": "Remove relationship with Epic",
"MESSAGE": "the relationship of this User Story with the Epic {{epicSubject}}"
},
"CREATE_MEMBER": { "CREATE_MEMBER": {
"PLACEHOLDER_INVITATION_TEXT": "(Optional) Add a personalized text to the invitation. Tell something lovely to your new members ;-)", "PLACEHOLDER_INVITATION_TEXT": "(Optional) Add a personalized text to the invitation. Tell something lovely to your new members ;-)",
"PLACEHOLDER_TYPE_EMAIL": "Type an Email", "PLACEHOLDER_TYPE_EMAIL": "Type an Email",
@ -1167,6 +1171,18 @@
"IN_THREE_MONTHS": "In three months" "IN_THREE_MONTHS": "In three months"
}, },
"TITLE_ACTION_DELETE_DUE_DATE": "Delete due date" "TITLE_ACTION_DELETE_DUE_DATE": "Delete due date"
},
"RELATE_TO_EPIC": {
"TITLE": "Relate to Epic",
"EXISTING_EPIC": "Existing epic",
"NEW_EPIC": "New epic",
"CHOOSE_PROJECT_FOR_CREATION": "What's the project?",
"CHOOSE_PROJECT_FROM": "What's the project?",
"SUBJECT": "Subject",
"SUBJECT_BULK_MODE": "Subject (bulk insert)",
"CHOOSE_EPIC": "What's the epic?",
"FILTER_EPICS": "Filter epics",
"NO_EPICS_FOUND": "It looks like nothing was found with your search criteria"
} }
}, },
"EPIC": { "EPIC": {
@ -1205,6 +1221,8 @@
"LIGHTBOX_TITLE_BLOKING_US": "Blocking us", "LIGHTBOX_TITLE_BLOKING_US": "Blocking us",
"NOT_ESTIMATED": "Not estimated", "NOT_ESTIMATED": "Not estimated",
"OWNER_US": "This User Story belongs to", "OWNER_US": "This User Story belongs to",
"RELATE_TO_EPIC": "Relate to Epic",
"REMOVE_RELATIONSHIP_WITH_EPIC": "Remove Epic relationship",
"TRIBE": { "TRIBE": {
"PUBLISH": "Publish as Gig in Taiga Tribe", "PUBLISH": "Publish as Gig in Taiga Tribe",
"PUBLISH_INFO": "More info", "PUBLISH_INFO": "More info",

View File

@ -1,11 +1,19 @@
- var hash = "#"; span(ng-if="epicsLength > 0", translate="US.OWNER_US")
span.belong-to-epic-text-wrapper(tg-repeat="epic in immutable_epics track by epic.get('id')") span(ng-if="epicsLength > 1") :
a.belong-to-epic-text( ul.belong-to-epics-list(ng-class="{'unique': epicsLength == 1 }")
href="" li.belong-to-epic-text-wrapper(tg-repeat="epic in immutable_epics track by epic.get('id')")
tg-nav="project-epics-detail:project=epic.getIn(['project', 'slug']),ref=epic.get('ref')" a.belong-to-epic-text(
ng-bind-html="'#'+epic.get('ref')+' '+epic.get('subject') | emojify" href=""
) tg-nav="project-epics-detail:project=epic.getIn(['project', 'slug']),ref=epic.get('ref')"
span.belong-to-epic-label( ng-bind-html="'#'+epic.get('ref')+' '+epic.get('subject') | emojify"
ng-style="::{'background-color': epic.get('color')}" )
translate="EPICS.EPIC" span.belong-to-epic-label(
) ng-style="::{'background-color': epic.get('color')}"
translate="EPICS.EPIC"
)
a.remove-epic-relationship(
title="{{'US.REMOVE_RELATIONSHIP_WITH_EPIC' | translate}}"
href=""
ng-click="removeEpicRelationship(epic)"
)
tg-svg(svg-icon="icon-close")

View File

@ -19,12 +19,43 @@
module = angular.module('taigaEpics') module = angular.module('taigaEpics')
BelongToEpicsDirective = () -> BelongToEpicsDirective = ($translate, $confirm, $rs, $rs2, lightboxService) ->
link = (scope, el, attrs) -> link = (scope, el, attrs) ->
scope.$watch 'epics', (epics) -> scope.$watch 'epics', (epics) ->
updateEpics(epics)
scope.$on "related-epics:changed", (ctx, userStory)->
$rs.userstories.getByRef(userStory.project, userStory.ref, {}).then (us) ->
scope.item.epics = us.epics
updateEpics(us.epics)
scope.removeEpicRelationship = (epic) ->
title = $translate.instant("LIGHTBOX.REMOVE_RELATIONSHIP_WITH_EPIC.TITLE")
message = $translate.instant(
"LIGHTBOX.REMOVE_RELATIONSHIP_WITH_EPIC.MESSAGE",
{ epicSubject: epic.get('subject') }
)
$confirm.askOnDelete(title, message).then (askResponse) ->
onSuccess = ->
askResponse.finish()
scope.$broadcast("related-epics:changed", scope.item)
onError = ->
askResponse.finish(false)
$confirm.notify("error")
epicId = epic.get('id')
usId = scope.item.id
$rs2.epics.deleteRelatedUserstory(epicId, usId).then(onSuccess, onError)
updateEpics = (epics) ->
scope.epicsLength = 0
scope.immutable_epics = []
if epics && !epics.isIterable if epics && !epics.isIterable
scope.immutable_epics = Immutable.fromJS(epics) scope.epicsLength = epics.length
scope.immutable_epics = Immutable.fromJS(epics)
templateUrl = (el, attrs) -> templateUrl = (el, attrs) ->
if attrs.format if attrs.format
@ -34,10 +65,13 @@ BelongToEpicsDirective = () ->
return { return {
link: link, link: link,
scope: { scope: {
epics: '=' epics: '=',
item: "="
}, },
templateUrl: templateUrl templateUrl: templateUrl
} }
module.directive("tgBelongToEpics", BelongToEpicsDirective) module.directive("tgBelongToEpics", [
"$translate", "$tgConfirm", "$tgResources", "tgResources", "lightboxService",
BelongToEpicsDirective])

View File

@ -1,13 +1,57 @@
.belong-to-epic-pill-wrapper { .belong-to-epic-pill-wrapper {
display: inline-block; display: inline-block;
position: relative; position: relative;
&:hover { &:hover .belong-to-epic-pill-data {
.belong-to-epic-pill-data { display: block;
display: block; }
}
.belong-to-epics-list {
margin-bottom: .5rem;
svg {
position: relative;
top: .1rem;
}
&.unique {
display: inline-block;
li {
display: inline-block;
svg {
top: .2rem;
}
} }
} }
} }
.belong-to-epic-text-wrapper {
display: flex;
flex-basis: 25%;
flex-wrap: wrap;
justify-content: flex-start;
padding-top: .25rem;
.remove-epic-relationship {
display: inline-block;
line-height: .5rem;
margin-left: .5rem;
svg {
visibility: hidden;
}
}
&:hover .remove-epic-relationship svg {
visibility: visible;
}
}
.belong-to-epic-label {
@include font-type(light);
@include font-size(xsmall);
background: $grayer;
border-radius: .25rem;
color: $white;
margin: 0 .5rem;
padding: .1rem .25rem;
}
.belong-to-epic-pill { .belong-to-epic-pill {
background-color: $mass-white; background-color: $mass-white;
border-radius: 50%; border-radius: 50%;
@ -17,20 +61,3 @@
position: relative; position: relative;
width: .7rem; width: .7rem;
} }
.belong-to-epic-text-wrapper {
margin-right: 1rem;
}
.belong-to-epic-text {
margin-left: .25rem;
}
.belong-to-epic-label {
@include font-type(light);
@include font-size(xsmall);
background: $grayer;
border-radius: .25rem;
color: $white;
margin: 0 .5rem;
padding: .1rem .25rem;
}

View File

@ -92,4 +92,7 @@ class StoryHeaderController
return item return item
return transform.then(onEditSubjectSuccess, onEditSubjectError) return transform.then(onEditSubjectSuccess, onEditSubjectError)
relateToEpic: (us) ->
@rootScope.$broadcast("relate-to-epic:add", us)
module.controller("StoryHeaderCtrl", StoryHeaderController) module.controller("StoryHeaderCtrl", StoryHeaderController)

View File

@ -43,14 +43,26 @@
) )
//- User Story belongs to epic //- User Story belongs to epic
.belong-to-epics-wrapper(ng-if="vm.item.epics") .belong-to-epics-wrapper(ng-if="vm.item._name == 'userstories' && vm.project.is_epics_activated")
span(translate="US.OWNER_US") .related-to-epics
tg-belong-to-epics( tg-belong-to-epics(
ng-if="::vm.item.epics" epics="::vm.item.epics"
epics="::vm.item.epics" item="::vm.item"
format="text" format="text"
)
//- User Story relate to Epic
a.relate-to-epic-button.ng-animate-disabled(
ng-if="vm.permissions.canEdit"
href=""
title="{{'US.RELATE_TO_EPIC' | translate}}"
ng-click="vm.relateToEpic(vm.item)"
) )
tg-svg(
svg-icon="icon-epics"
svg-title-translate="US.RELATE_TO_EPIC"
)
span.relate-to-epic-text(translate="US.RELATE_TO_EPIC")
//- Task belongs to US //- Task belongs to US
.task-belongs-to( .task-belongs-to(
ng-if="vm.item.user_story_extra_info" ng-if="vm.item.user_story_extra_info"

View File

@ -30,6 +30,22 @@
@include font-size(small); @include font-size(small);
margin-top: .5rem; margin-top: .5rem;
} }
.relate-to-epic-button {
color: $gray-light;
cursor: pointer;
display: inline-block;
&:hover {
color: $primary-light;
}
.icon-epics {
@include svg-size(.9rem);
fill: currentColor;
margin: .5rem .25rem 0 0;
}
.relate-to-epic-text {
@include font-size(small);
}
}
.item-generated-us, .item-generated-us,
.task-belongs-to, .task-belongs-to,
.item-origin-issue, .item-origin-issue,

View File

@ -64,15 +64,23 @@ class EpicsService
listRelatedUserStories: (epic) -> listRelatedUserStories: (epic) ->
return @resources.userstories.listInEpic(epic.get('id')) return @resources.userstories.listInEpic(epic.get('id'))
createEpic: (epicData, attachments) -> createEpic: (epicData, attachments, projectId) ->
epicData.project = @projectService.project.get('id') if projectId
epicData.project = projectId
else
epicData.project = @projectService.project.get('id')
return @resources.epics.post(epicData) return @resources.epics.post(epicData)
.then (epic) => .then (epic) =>
promises = _.map attachments.toJS(), (attachment) => if !attachments
@attachmentsService.upload(attachment.file, epic.get('id'), epic.get('project'), 'epic') return epic
else
promises = _.map attachments.toJS(), (attachment) =>
@attachmentsService.upload(
attachment.file, epic.get('id'), epic.get('project'), 'epic')
Promise.all(promises).then(@.fetchEpics.bind(this, true))
Promise.all(promises).then(@.fetchEpics.bind(this, true))
reorderEpic: (epic, newIndex) -> reorderEpic: (epic, newIndex) ->
orderList = {} orderList = {}

View File

@ -0,0 +1,106 @@
tg-lightbox-close
.lightbox-create-related-epic-wrapper
h2.title(translate="LIGHTBOX.RELATE_TO_EPIC.TITLE")
.related-with-selector(tg-check-permission="add_epic")
.related-with-selector-single
input(
type="radio"
name="related-with-selector"
id="existing-epic"
value="existing-epic"
ng-model="relatedWithSelector"
ng-init="relatedWithSelector='existing-epic'"
)
label.e2e-existing-epic-label(for="existing-epic")
span.name {{ 'LIGHTBOX.RELATE_TO_EPIC.EXISTING_EPIC' | translate}}
.related-with-selector-single
input(
type="radio"
name="related-with-selector"
id="new-epic"
value="new-epic"
ng-model="relatedWithSelector"
)
label.e2e-new-epic-label(for="new-epic")
span.name {{ 'LIGHTBOX.RELATE_TO_EPIC.NEW_EPIC' | translate}}
fieldset.project-selector
label(
ng-if="relatedWithSelector=='new-epic'"
translate="LIGHTBOX.RELATE_TO_EPIC.CHOOSE_PROJECT_FOR_CREATION"
for="project-selector-dropdown"
)
label(
ng-if="relatedWithSelector=='existing-epic'"
translate="LIGHTBOX.RELATE_TO_EPIC.CHOOSE_PROJECT_FROM"
for="project-selector-dropdown"
)
select(
ng-model="selectedProject"
ng-change="selectProject(selectedProject)"
data-required="true"
ng-options="p.id as p.name for p in projects | toMutable"
id="project-selector-dropdown"
)
fieldset(ng-show="relatedWithSelector=='new-epic'")
.new-epic-title
label(translate="LIGHTBOX.RELATE_TO_EPIC.SUBJECT")
form.new-epic-form
.single-creation
input.e2e-new-epic-input-text(
type="text"
ng-model="epicSubject"
data-required="true"
)
button.button-green.create-epic.e2e-create-epic-button.ng-animate-disabled(
href=""
ng-click="createEpic(selectedProject, epicSubject)"
tg-loading="loading"
translate="COMMON.SAVE"
)
fieldset.existing-epic(ng-show="relatedWithSelector=='existing-epic'")
label(
translate="LIGHTBOX.RELATE_TO_EPIC.CHOOSE_EPIC"
for="epic-filter"
)
input.epic-filter.e2e-filter-userstories-input(
id="epic-filter"
type="text"
placeholder="{{'LIGHTBOX.RELATE_TO_EPIC.FILTER_EPICS' | translate}}"
ng-model="searchEpic"
ng-change="onUpdateSearchEpic()"
)
form.existing-epic-form(ng-show="relatedWithSelector=='existing-epic' && projectEpics.size")
select.epic.e2e-userstories-select(
size="5"
ng-model="selectedEpic"
data-required="true"
)
- var hash = "#";
option.hidden(
value=""
)
option(
ng-repeat="epic in projectEpics | toMutable track by epic.id"
value="{{ ::epic.id }}"
) #{hash}{{::epic.ref}} {{::epic.subject}}
p.no-stories-found(
ng-show="relatedWithSelector=='existing-epic' && !projectEpics.size"
translate="LIGHTBOX.RELATE_TO_EPIC.NO_EPICS_FOUND"
)
button.button-green.e2e-select-related-epic-button(
href=""
ng-click="saveRelatedEpic(selectedEpic, closeLightbox)"
tg-loading="loading"
translate="COMMON.SAVE"
)

View File

@ -156,3 +156,4 @@ div.wrapper(
div.lightbox.lightbox-select-user(tg-lb-assignedto) div.lightbox.lightbox-select-user(tg-lb-assignedto)
div.lightbox.lightbox-select-user(tg-lb-assigned-users) div.lightbox.lightbox-select-user(tg-lb-assigned-users)
div.lightbox.lightbox-select-user(tg-lb-watchers) div.lightbox.lightbox-select-user(tg-lb-watchers)
div.lightbox.lightbox-relate-to-epic(tg-lb-relatetoepic)

View File

@ -854,7 +854,6 @@
} }
} }
.ticket-detail-settings .lightbox-assign-sprint-to-issue { .ticket-detail-settings .lightbox-assign-sprint-to-issue {
.lightbox-assign-related-sprint { .lightbox-assign-related-sprint {
width: 700px; width: 700px;
@ -883,6 +882,92 @@
button { button {
width: 100%; width: 100%;
} }
}
.lightbox-relate-to-epic {
.lightbox-create-related-epic-wrapper {
max-width: 600px;
width: 90%;
}
.related-with-selector {
display: flex;
margin-bottom: 1rem;
input {
display: none;
&:checked+label {
background: $primary-light;
color: $white;
transition: background .2s ease-in;
}
&:checked+label:hover {
background: $primary-light;
}
+label {
background: rgba($whitish, .7);
cursor: pointer;
display: block;
padding: 2rem 1rem;
text-align: center;
text-transform: uppercase;
transition: background .2s ease-in;
}
+label:hover {
background: rgba($primary-light, .3);
transition: background .2s ease-in;
}
}
.related-with-selector-single {
flex: 1;
&:first-child {
margin-right: .5rem;
}
}
}
fieldset {
label {
display: inline-block;
margin-bottom: .5rem;
}
}
.new-epic-title {
align-items: flex-end;
display: flex;
}
.existing-epic-form,
.new-epic-form {
margin-bottom: 1rem;
}
.no-epics-found {
padding: 1rem 0 0;
}
.new-epic-options {
display: flex;
margin-left: auto;
input {
display: none;
&:checked+label {
background: $primary-light;
color: $white;
fill: $white;
transition: background .2s ease-in;
}
+label {
background: $mass-white;
color: $grayer;
cursor: pointer;
display: block;
padding: .5rem;
transition: background .2s ease-in;
}
+label:hover {
background: $primary-light;
color: $white;
fill: $white;
}
}
}
button {
width: 100%;
}
} }