Merge pull request #102 from taigaio/us/1286/us-task-issue-details-refactor

US #1286:  US/Task/Issue Visualization and edition refactor
stable
Alejandro 2014-10-30 17:17:43 +01:00
commit 6061da9c80
47 changed files with 2385 additions and 1171 deletions

View File

@ -3,6 +3,7 @@
## 1.2.0 (Unreleased) ## 1.2.0 (Unreleased)
### Features ### Features
- US/Task/Issue visualization and edition refactor. Now only one view for both.
- Multiple User stories Drag & Drop in the backlog. - Multiple User stories Drag & Drop in the backlog.
- Add visual difference to closed USs in backlog panel. - Add visual difference to closed USs in backlog panel.
- Show crerated date of attachments in the hover of the filename. - Show crerated date of attachments in the hover of the filename.

View File

@ -52,30 +52,22 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
# User stories # User stories
$routeProvider.when("/project/:pslug/us/:usref", $routeProvider.when("/project/:pslug/us/:usref",
{templateUrl: "/partials/us-detail.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "/partials/us-detail.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/us/:usref/edit",
{templateUrl: "/partials/us-detail-edit.html"})
# Tasks # Tasks
$routeProvider.when("/project/:pslug/task/:taskref", $routeProvider.when("/project/:pslug/task/:taskref",
{templateUrl: "/partials/task-detail.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "/partials/task-detail.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/task/:taskref/edit",
{templateUrl: "/partials/task-detail-edit.html"})
# Wiki # Wiki
$routeProvider.when("/project/:pslug/wiki", $routeProvider.when("/project/:pslug/wiki",
{redirectTo: (params) -> "/project/#{params.pslug}/wiki/home"}, ) {redirectTo: (params) -> "/project/#{params.pslug}/wiki/home"}, )
$routeProvider.when("/project/:pslug/wiki/:slug", $routeProvider.when("/project/:pslug/wiki/:slug",
{templateUrl: "/partials/wiki.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "/partials/wiki.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/wiki/:slug/edit",
{templateUrl: "/partials/wiki-edit.html"})
# Issues # Issues
$routeProvider.when("/project/:pslug/issues", $routeProvider.when("/project/:pslug/issues",
{templateUrl: "/partials/issues.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "/partials/issues.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/issue/:issueref", $routeProvider.when("/project/:pslug/issue/:issueref",
{templateUrl: "/partials/issues-detail.html"}) {templateUrl: "/partials/issues-detail.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/issue/:issueref/edit",
{templateUrl: "/partials/issues-detail-edit.html"})
# Admin # Admin
$routeProvider.when("/project/:pslug/admin/project-profile/details", $routeProvider.when("/project/:pslug/admin/project-profile/details",

View File

@ -67,17 +67,13 @@ urls = {
"project-search": "/project/:project/search" "project-search": "/project/:project/search"
"project-userstories-detail": "/project/:project/us/:ref" "project-userstories-detail": "/project/:project/us/:ref"
"project-userstories-detail-edit": "/project/:project/us/:ref/edit"
"project-tasks-detail": "/project/:project/task/:ref" "project-tasks-detail": "/project/:project/task/:ref"
"project-tasks-detail-edit": "/project/:project/task/:ref/edit"
"project-issues-detail": "/project/:project/issue/:ref" "project-issues-detail": "/project/:project/issue/:ref"
"project-issues-detail-edit": "/project/:project/issue/:ref/edit"
"project-wiki": "/project/:project/wiki", "project-wiki": "/project/:project/wiki",
"project-wiki-page": "/project/:project/wiki/:slug", "project-wiki-page": "/project/:project/wiki/:slug",
"project-wiki-page-edit": "/project/:project/wiki/:slug/edit",
# Admin # Admin
"project-admin-home": "/project/:project/admin/project-profile/details" "project-admin-home": "/project/:project/admin/project-profile/details"

View File

@ -24,6 +24,7 @@ bindOnce = @.taiga.bindOnce
module = angular.module("taigaCommon") module = angular.module("taigaCommon")
############################################################################# #############################################################################
## Date Range Directive (used mainly for sprint date range) ## Date Range Directive (used mainly for sprint date range)
############################################################################# #############################################################################
@ -46,6 +47,33 @@ DateRangeDirective = ->
module.directive("tgDateRange", DateRangeDirective) module.directive("tgDateRange", DateRangeDirective)
#############################################################################
## Date Selector Directive (using pikaday)
#############################################################################
DateSelectorDirective =->
link = ($scope, $el, $attrs, $model) ->
selectedDate = null
$el.picker = new Pikaday({
field: $el[0]
format: "DD MMM YYYY"
onSelect: (date) =>
selectedDate = date
onOpen: =>
$el.picker.setDate(selectedDate) if selectedDate?
})
$scope.$watch $attrs.ngModel, (val) ->
$el.picker.setDate(val) if val?
return {
link: link
require: "ngModel"
}
module.directive("tgDateSelector", DateSelectorDirective)
############################################################################# #############################################################################
## Sprint Progress Bar Directive ## Sprint Progress Bar Directive
############################################################################# #############################################################################
@ -76,92 +104,126 @@ module.directive("tgSprintProgressbar", SprintProgressBarDirective)
############################################################################# #############################################################################
## Date Selector Directive (using pikaday) ## Created-by display directive
############################################################################# #############################################################################
DateSelectorDirective =-> CreatedByDisplayDirective = ->
link = ($scope, $el, $attrs, $model) -> # Display the owner information (full name and photo) and the date of
selectedDate = null # creation of an object (like USs, tasks and issues).
$el.picker = new Pikaday({ #
field: $el[0] # Example:
format: "DD MMM YYYY" # div.us-created-by(tg-created-by-display, ng-model="us")
onSelect: (date) => #
selectedDate = date # Requirements:
onOpen: => # - model object must have the attributes 'created_date' and
$el.picker.setDate(selectedDate) if selectedDate? # 'owner'(ng-model)
}) # - scope.usersById object is required.
$scope.$watch $attrs.ngModel, (val) -> template = _.template("""
$el.picker.setDate(val) if val? <div class="user-avatar">
<img src="<%= owner.photo %>" alt="<%- owner.full_name_display %>" />
</div>
<div class="created-by">
<span class="created-title">Created by <%- owner.full_name_display %></span>
<span class="created-date"><%- date %></span>
</div>
""") # TODO: i18n
link = ($scope, $el, $attrs) ->
render = (model) ->
html = template({
owner: $scope.usersById?[model.owner]
date: moment(model.created_date).format("DD MMM YYYY HH:mm")
})
$el.html(html)
bindOnce $scope, $attrs.ngModel, (model) ->
render(model) if model?
$scope.$on "$destroy", ->
$el.off()
return { return {
link: link link: link
restrict: "EA"
require: "ngModel" require: "ngModel"
} }
module.directive("tgDateSelector", DateSelectorDirective) module.directive("tgCreatedByDisplay", CreatedByDisplayDirective)
############################################################################# #############################################################################
## Watchers directive ## Watchers directive
############################################################################# #############################################################################
WatchersDirective = ($rootscope, $confirm) -> WatchersDirective = ($rootscope, $confirm, $repo) ->
# You have to include a div with the tg-lb-watchers directive in the page
# where use this directive
#
# TODO: i18n # TODO: i18n
template = _.template(""" template = _.template("""
<% if(isEditable){ %>
<div class="watchers-header"> <div class="watchers-header">
<span class="title">watchers</span> <span class="title">watchers</span>
<% if (editable) { %>
<a href="" title="Add watcher" class="icon icon-plus add-watcher"></a> <a href="" title="Add watcher" class="icon icon-plus add-watcher"></a>
<% } %>
</div> </div>
<% } else if(watchers.length > 0){ %>
<div class="watchers-header">
<span class="title">watchers</span>
</div>
<% }; %>
<% _.each(watchers, function(watcher) { %> <% _.each(watchers, function(watcher) { %>
<div class="watcher-single"> <div class="watcher-single">
<div class="watcher-avatar"> <div class="watcher-avatar">
<a class="avatar" href="" title="Assigned to"> <span class="avatar" href="" title="<%- watcher.full_name_display %>">
<img src="<%= watcher.photo %>" alt="<%- watcher.full_name_display %>"> <img src="<%= watcher.photo %>" alt="<%- watcher.full_name_display %>">
</a> </span>
</div> </div>
<div class="watcher-name"> <div class="watcher-name">
<span> <span><%- watcher.full_name_display %></span>
<%- watcher.full_name_display %>
</span>
<% if (editable) { %> <% if(isEditable){ %>
<a class="icon icon-delete" <a class="icon icon-delete"
data-watcher-id="<%= watcher.id %>" href="" title="delete-watcher"> data-watcher-id="<%= watcher.id %>" href="" title="delete-watcher">
</a> </a>
<% } %> <% }; %>
</div> </div>
</div> </div>
<% }); %> <% }); %>
""") """)
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
editable = $attrs.editable? isEditable = ->
return $scope.project?.my_permissions?.indexOf($attrs.requiredPerm) != -1
save = (model) ->
promise = $repo.save($model.$modelValue)
promise.then ->
$confirm.notify("success")
watchers = _.map(model.watchers, (watcherId) -> $scope.usersById[watcherId])
renderWatchers(watchers)
$rootscope.$broadcast("history:reload")
promise.then null, ->
model.revert()
$confirm.notify("error")
renderWatchers = (watchers) -> renderWatchers = (watchers) ->
html = template({watchers: watchers, editable:editable}) ctx = {
watchers: watchers
isEditable: isEditable()
}
html = template(ctx)
$el.html(html) $el.html(html)
if watchers.length == 0 if isEditable() and watchers.length == 0
if editable
$el.find(".title").text("Add watchers") $el.find(".title").text("Add watchers")
$el.find(".watchers-header").addClass("no-watchers") $el.find(".watchers-header").addClass("no-watchers")
else
$el.find(".watchers-header").hide()
$scope.$watch $attrs.ngModel, (item) ->
return if not item?
watchers = _.map(item.watchers, (watcherId) -> $scope.usersById[watcherId])
renderWatchers(watchers)
if not editable
$el.find(".add-watcher").remove()
$el.on "click", ".icon-delete", (event) -> $el.on "click", ".icon-delete", (event) ->
event.preventDefault() event.preventDefault()
return if not isEditable()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
watcherId = target.data("watcher-id") watcherId = target.data("watcher-id")
@ -176,9 +238,11 @@ WatchersDirective = ($rootscope, $confirm) ->
item = $model.$modelValue.clone() item = $model.$modelValue.clone()
item.watchers = watcherIds item.watchers = watcherIds
$model.$setViewValue(item) $model.$setViewValue(item)
save(item)
$el.on "click", ".add-watcher", (event) -> $el.on "click", ".add-watcher", (event) ->
event.preventDefault() event.preventDefault()
return if not isEditable()
$scope.$apply -> $scope.$apply ->
$rootscope.$broadcast("watcher:add", $model.$modelValue) $rootscope.$broadcast("watcher:add", $model.$modelValue)
@ -189,19 +253,30 @@ WatchersDirective = ($rootscope, $confirm) ->
item = $model.$modelValue.clone() item = $model.$modelValue.clone()
item.watchers = watchers item.watchers = watchers
$model.$setViewValue(item) $model.$setViewValue(item)
save(item)
$scope.$watch $attrs.ngModel, (item) ->
return if not item?
watchers = _.map(item.watchers, (watcherId) -> $scope.usersById[watcherId])
renderWatchers(watchers)
$scope.$on "$destroy", ->
$el.off()
return {link:link, require:"ngModel"} return {link:link, require:"ngModel"}
module.directive("tgWatchers", ["$rootScope", "$tgConfirm", WatchersDirective]) module.directive("tgWatchers", ["$rootScope", "$tgConfirm", "$tgRepo", WatchersDirective])
############################################################################# #############################################################################
## Assigned to directive ## Assigned to directive
############################################################################# #############################################################################
AssignedToDirective = ($rootscope, $confirm) -> AssignedToDirective = ($rootscope, $confirm, $repo, $loading) ->
# You have to include a div with the tg-lb-assignedto directive in the page
# where use this directive
#
# TODO: i18n # TODO: i18n
template = _.template(""" template = _.template("""
<% if (assignedTo) { %> <% if (assignedTo) { %>
@ -213,62 +288,360 @@ AssignedToDirective = ($rootscope, $confirm) ->
<div class="assigned-to"> <div class="assigned-to">
<span class="assigned-title">Assigned to</span> <span class="assigned-title">Assigned to</span>
<a href="" title="edit assignment" class="user-assigned <% if (editable) { %> editable <% } %>"> <a href="" title="edit assignment" class="user-assigned <% if(isEditable){ %>editable<% }; %>">
<span class="assigned-name">
<% if (assignedTo) { %> <% if (assignedTo) { %>
<%- assignedTo.full_name_display %> <%- assignedTo.full_name_display %>
<% } else { %> <% } else { %>
Not assigned Not assigned
<% } %> <% } %>
<% if (editable) { %> </span>
<span class="icon icon-arrow-bottom"></span> <% if(isEditable){ %><span class="icon icon-arrow-bottom"></span><% }; %>
<% } %>
</a> </a>
<% if (editable && assignedTo!==null) { %> <% if (assignedTo!==null && isEditable) { %>
<a href="" title="delete assignment" class="icon icon-delete"></a> <a href="" title="delete assignment" class="icon icon-delete"></a>
<% } %> <% } %>
</div> </div>
""") """) # TODO: i18n
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
editable = $attrs.editable? isEditable = ->
return $scope.project?.my_permissions?.indexOf($attrs.requiredPerm) != -1
save = (model) ->
$loading.start($el)
promise = $repo.save($model.$modelValue)
promise.then ->
$loading.finish($el)
$confirm.notify("success")
renderAssignedTo(model)
$rootscope.$broadcast("history:reload")
promise.then null, ->
model.revert()
$confirm.notify("error")
$loading.finish($el)
renderAssignedTo = (issue) -> renderAssignedTo = (issue) ->
assignedToId = issue?.assigned_to assignedToId = issue?.assigned_to
assignedTo = null assignedTo = if assignedToId? then $scope.usersById[assignedToId] else null
assignedTo = $scope.usersById[assignedToId] if assignedToId?
html = template({assignedTo: assignedTo, editable:editable}) ctx = {
assignedTo: assignedTo
isEditable: isEditable()
}
html = template(ctx)
$el.html(html) $el.html(html)
$scope.$watch $attrs.ngModel, (instance) ->
renderAssignedTo(instance)
if editable
$el.on "click", ".user-assigned", (event) -> $el.on "click", ".user-assigned", (event) ->
event.preventDefault() event.preventDefault()
return if not isEditable()
$scope.$apply -> $scope.$apply ->
$rootscope.$broadcast("assigned-to:add", $model.$modelValue) $rootscope.$broadcast("assigned-to:add", $model.$modelValue)
$el.on "click", ".icon-delete", (event) -> $el.on "click", ".icon-delete", (event) ->
event.preventDefault() event.preventDefault()
title = "Delete assignetion" return if not isEditable()
message = "" title = "Are you sure you want to leave it unassigned?" # TODO: i18n
$confirm.askOnDelete(title, message).then (finish) => $confirm.ask(title).then (finish) =>
finish() finish()
$model.$modelValue.assigned_to = null $model.$modelValue.assigned_to = null
renderAssignedTo($model.$modelValue) save($model.$modelValue)
$scope.$on "assigned-to:added", (ctx, userId) -> $scope.$on "assigned-to:added", (ctx, userId) ->
$model.$modelValue.assigned_to = userId $model.$modelValue.assigned_to = userId
renderAssignedTo($model.$modelValue) save($model.$modelValue)
$scope.$watch $attrs.ngModel, (instance) ->
renderAssignedTo(instance)
$scope.$on "$destroy", ->
$el.off()
return { return {
link:link, link:link,
require:"ngModel" require:"ngModel"
} }
module.directive("tgAssignedTo", ["$rootScope", "$tgConfirm", "$tgRepo", "$tgLoading", AssignedToDirective])
module.directive("tgAssignedTo", ["$rootScope", "$tgConfirm", AssignedToDirective])
#############################################################################
## Block Button directive
#############################################################################
BlockButtonDirective = ($rootscope, $loading) ->
template = """
<a href="#" class="button button-gray item-block">Block</a>
<a href="#" class="button button-red item-unblock">Unblock</a>
"""
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_us") != -1
$scope.$watch $attrs.ngModel, (item) ->
return if not item
if isEditable()
$el.find('.item-block').addClass('editable')
if item.is_blocked
$el.find('.item-block').hide()
$el.find('.item-unblock').show()
else
$el.find('.item-block').show()
$el.find('.item-unblock').hide()
$el.on "click", ".item-block", (event) ->
event.preventDefault()
$rootscope.$broadcast("block", $model.$modelValue)
$el.on "click", ".item-unblock", (event) ->
event.preventDefault()
$loading.start($el.find(".item-unblock"))
finish = ->
$loading.finish($el.find(".item-unblock"))
$rootscope.$broadcast("unblock", $model.$modelValue, finish)
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
template: template
}
module.directive("tgBlockButton", ["$rootScope", "$tgLoading", BlockButtonDirective])
#############################################################################
## Delete Button directive
#############################################################################
DeleteButtonDirective = ($log, $repo, $confirm, $location) ->
template = """
<a href="" class="button button-red">Delete</a>
""" #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
if not $attrs.onDeleteGoToUrl
return $log.error "DeleteButtonDirective requires on-delete-go-to-url set in scope."
if not $attrs.onDeleteTitle
return $log.error "DeleteButtonDirective requires on-delete-title set in scope."
$el.on "click", ".button", (event) ->
title = $scope.$eval($attrs.onDeleteTitle)
subtitle = $model.$modelValue.subject
$confirm.askOnDelete(title, subtitle).then (finish) =>
promise = $repo.remove($model.$modelValue)
promise.then =>
finish()
url = $scope.$eval($attrs.onDeleteGoToUrl)
$location.path(url)
promise.then null, =>
finish(false)
$confirm.notify("error")
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
template: template
}
module.directive("tgDeleteButton", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", DeleteButtonDirective])
#############################################################################
## Editable subject directive
#############################################################################
EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading) ->
template = """
<div class="view-subject">
{{ item.subject }}
<a class="edit icon icon-edit" href="" title="Edit" />
</div>
<div class="edit-subject">
<input type="text" ng-model="item.subject" data-required="true" data-maxlength="500"/>
<span class="save-container">
<a class="save icon icon-floppy" href="" title="Save" />
</span>
</div>
"""
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
save = ->
$model.$modelValue.subject = $scope.item.subject
$loading.start($el.find('.save-container'))
promise = $repo.save($model.$modelValue)
promise.then ->
$confirm.notify("success")
$rootscope.$broadcast("history:reload")
$el.find('.edit-subject').hide()
$el.find('.view-subject').show()
promise.then null, ->
$confirm.notify("error")
promise.finally ->
$loading.finish($el.find('.save-container'))
$el.click ->
return if not isEditable()
$el.find('.edit-subject').show()
$el.find('.view-subject').hide()
$el.find('input').focus()
$el.on "click", ".save", ->
save()
$el.on "keyup", "input", ->
if event.keyCode == 13
save()
else if event.keyCode == 27
$model.$modelValue.revert()
$el.find('div.edit-subject').hide()
$el.find('div.view-subject').show()
$el.find('div.edit-subject').hide()
$el.find('div.view-subject span.edit').hide()
$scope.$watch $attrs.ngModel, (value) ->
return if not value
$scope.item = value
if not isEditable()
$el.find('.view-subject .edit').remove()
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
template: template
}
module.directive("tgEditableSubject", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading",
EditableSubjectDirective])
#############################################################################
## Editable subject directive
#############################################################################
EditableDescriptionDirective = ($window, $document, $rootscope, $repo, $confirm, $compile, $loading) ->
template = """
<div class="view-description">
<section class="us-content wysiwyg"
tg-bind-html="item.description_html || noDescriptionMsg"></section>
<span class="edit icon icon-edit" href="" title="Edit" />
</div>
<div class="edit-description">
<textarea placeholder="Write a description of your user story"
ng-model="item.description"
tg-markitup="tg-markitup"></textarea>
<span class="save-container">
<a class="save icon icon-floppy" href="" title="Save" />
</span>
</div>
""" # TODO: i18n
noDescriptionMegEditMode = """
<p class="no-description editable">
No description yet, why don't you add a good one clicking here?
</p>
""" # TODO: i18n
noDescriptionMegReadMode = """
<p class="no-description">
No description
</p>
""" # TODO: i18n
link = ($scope, $el, $attrs, $model) ->
$el.find('.edit-description').hide()
$el.find('.view-description .edit').hide()
isEditable = ->
return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
getSelectedText = ->
if $window.getSelection
return $window.getSelection().toString()
else if $document.selection
return $document.selection.createRange().text
return null
$el.on "mouseup", ".view-description", (event) ->
# We want to dettect the a inside the div so we use the target and
# not the currentTarget
target = angular.element(event.target)
return if not isEditable()
return if target.is('a')
return if getSelectedText()
$el.find('.edit-description').show()
$el.find('.view-description').hide()
$el.find('textarea').focus()
$el.on "click", ".save", ->
$model.$modelValue.description = $scope.item.description
$loading.start($el.find('.save-container'))
promise = $repo.save($model.$modelValue)
promise.then ->
$confirm.notify("success")
$rootscope.$broadcast("history:reload")
$el.find('.edit-description').hide()
$el.find('.view-description').show()
promise.then null, ->
$confirm.notify("error")
promise.finally ->
$loading.finish($el.find('.save-container'))
$el.on "keyup", "textarea", ->
if event.keyCode == 27
$scope.item.revert()
$el.find('.edit-description').hide()
$el.find('.view-description').show()
$scope.$watch $attrs.ngModel, (value) ->
return if not value
$scope.item = value
if isEditable()
$el.find('.view-description .edit').show()
$el.find('.view-description .us-content').addClass('editable')
$scope.noDescriptionMsg = noDescriptionMegEditMode
else
$scope.noDescriptionMsg = noDescriptionMegReadMode
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
template: template
}
module.directive("tgEditableDescription", ["$window", "$document", "$rootScope", "$tgRepo", "$tgConfirm",
"$compile", "$tgLoading", EditableDescriptionDirective])
############################################################################# #############################################################################
@ -377,6 +750,7 @@ ListItemSeverityDirective = ->
template: template template: template
} }
ListItemTypeDirective = -> ListItemTypeDirective = ->
template = """ template = """
<div class="level"></div> <div class="level"></div>

View File

@ -127,16 +127,33 @@ module.directive("lightbox", ["lightboxService", LightboxDirective])
# Issue/Userstory blocking message lightbox directive. # Issue/Userstory blocking message lightbox directive.
BlockLightboxDirective = (lightboxService) -> BlockLightboxDirective = ($rootscope, $tgrepo, $confirm, lightboxService, $loading) ->
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
$el.find("h2.title").text($attrs.title) $el.find("h2.title").text($attrs.title)
$scope.$on "block", -> $scope.$on "block", ->
$el.find(".reason").val($model.$modelValue.blocked_note)
lightboxService.open($el) lightboxService.open($el)
$scope.$on "unblock", -> $scope.$on "unblock", (event, model, finishCallback) ->
$model.$modelValue.is_blocked = false item = $model.$modelValue.clone()
$model.$modelValue.blocked_note_html = "" item.is_blocked = false
item.blocked_note = ""
promise = $tgrepo.save(item)
promise.then ->
$confirm.notify("success")
$rootscope.$broadcast("history:reload")
$model.$setViewValue(item)
finishCallback()
promise.then null, ->
$confirm.notify("error")
item.revert()
$model.$setViewValue(item)
promise.finally ->
finishCallback()
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -144,19 +161,34 @@ BlockLightboxDirective = (lightboxService) ->
$el.on "click", ".button-green", (event) -> $el.on "click", ".button-green", (event) ->
event.preventDefault() event.preventDefault()
$scope.$apply -> item = $model.$modelValue.clone()
$model.$modelValue.is_blocked = true item.is_blocked = true
$model.$modelValue.blocked_note = $el.find(".reason").val() item.blocked_note = $el.find(".reason").val()
$model.$setViewValue(item)
$loading.start($el.find(".button-green"))
promise = $tgrepo.save($model.$modelValue)
promise.then ->
$confirm.notify("success")
$rootscope.$broadcast("history:reload")
promise.then null, ->
$confirm.notify("error")
item.revert()
$model.$setViewValue(item)
promise.finally ->
$loading.finish($el.find(".button-green"))
lightboxService.close($el) lightboxService.close($el)
return { return {
templateUrl: "/partials/views/modules/lightbox-block.html" templateUrl: "/partials/views/modules/lightbox-block.html"
link:link, link: link
require:"ngModel" require: "ngModel"
} }
module.directive("tgLbBlock", ["lightboxService", BlockLightboxDirective]) module.directive("tgLbBlock", ["$rootScope", "$tgRepo", "$tgConfirm", "lightboxService", "$tgLoading", BlockLightboxDirective])
############################################################################# #############################################################################
@ -202,10 +234,10 @@ module.directive("tgBlockingMessageInput", ["$log", BlockingMessageInputDirectiv
CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $loading) -> CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $loading) ->
link = ($scope, $el, attrs) -> link = ($scope, $el, attrs) ->
isNew = true $scope.isNew = true
$scope.$on "usform:new", (ctx, projectId, status, statusList) -> $scope.$on "usform:new", (ctx, projectId, status, statusList) ->
isNew = true $scope.isNew = true
$scope.usStatusList = statusList $scope.usStatusList = statusList
$scope.us = { $scope.us = {
@ -229,7 +261,7 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
$scope.$on "usform:edit", (ctx, us) -> $scope.$on "usform:edit", (ctx, us) ->
$scope.us = us $scope.us = us
isNew = false $scope.isNew = false
# Update texts for edition # Update texts for edition
$el.find(".button-green span").html("Save") #TODO: i18n $el.find(".button-green span").html("Save") #TODO: i18n
@ -264,7 +296,7 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
$loading.start(target) $loading.start(target)
if isNew if $scope.isNew
promise = $repo.create("userstories", $scope.us) promise = $repo.create("userstories", $scope.us)
broadcastEvent = "usform:new:success" broadcastEvent = "usform:new:success"
else else

View File

@ -43,6 +43,9 @@ TagsDirective = ->
$ctrl.$formatters.push(formatter) $ctrl.$formatters.push(formatter)
$ctrl.$parsers.push(parser) $ctrl.$parsers.push(parser)
$scope.$on "$destroy", ->
$el.off()
return { return {
require: "ngModel" require: "ngModel"
link: link link: link
@ -83,86 +86,118 @@ ColorizeTagsDirective = ->
$scope.$watch $attrs.tgColorizeTags, (tags) -> $scope.$watch $attrs.tgColorizeTags, (tags) ->
render(tags) if tags? render(tags) if tags?
$scope.$on "$destroy", ->
$el.off()
return {link: link} return {link: link}
module.directive("tgColorizeTags", ColorizeTagsDirective) module.directive("tgColorizeTags", ColorizeTagsDirective)
############################################################################# #############################################################################
## TagLine (possible should be moved as generic directive) ## TagLine Directive (for Lightboxes)
############################################################################# #############################################################################
TagLineDirective = ($log, $rs) -> LbTagLineDirective = ($rs) ->
# Main directive template (rendered by angular) ENTER_KEY = 13
template = """ template = """
<div class="tags-container"></div> <div class="tags-container"></div>
<input type="text" placeholder="Write tag..." class="tag-input" /> <input type="text" placeholder="Write tag..." class="tag-input" />
<a href="" title="Save" class="save icon icon-floppy"></a> <a href="" title="Save" class="save icon icon-floppy hidden"></a>
""" """ # TODO: i18n
# Tags template (rendered manually using lodash) # Tags template (rendered manually using lodash)
templateTags = _.template(""" templateTags = _.template("""
<% _.each(tags, function(tag) { %> <% _.each(tags, function(tag) { %>
<div class="tag" style="border-left: 5px solid <%- tag.color %>;"> <span class="tag" style="border-left: 5px solid <%- tag.color %>;">
<span class="tag-name"><%- tag.name %></span> <span class="tag-name"><%- tag.name %></span>
<% if (editable) { %>
<a href="" title="delete tag" class="icon icon-delete"></a> <a href="" title="delete tag" class="icon icon-delete"></a>
<% } %> </span>
</div> <% }); %>
<% }); %>""") """) # TODO: i18n
renderTags = ($el, tags, editable, tagsColors) -> link = ($scope, $el, $attrs, $model) ->
## Render
renderTags = (tags, tagsColors) ->
ctx = { ctx = {
tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]}) tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]})
editable: editable
} }
html = templateTags(ctx) html = templateTags(ctx)
$el.find("div.tags-container").html(html) $el.find("div.tags-container").html(html)
normalizeTags = (tags) -> showSaveButton = -> $el.find(".save").removeClass("hidden")
tags = _.map(tags, trim) hideSaveButton = -> $el.find(".save").addClass("hidden")
tags = _.map(tags, (x) -> x.toLowerCase())
return _.uniq(tags)
link = ($scope, $el, $attrs, $model) -> resetInput = ->
editable = if $attrs.editable == "true" then true else false $el.find("input").val("")
$el.addClass("tags-block") $el.find("input").autocomplete("close")
## Aux methods
addValue = (value) -> addValue = (value) ->
value = trim(value) value = trim(value.toLowerCase())
return if value.length <= 0 return if value.length == 0
tags = _.clone($model.$modelValue, false) tags = _.clone($model.$modelValue, false)
tags = [] if not tags? tags = [] if not tags?
tags.push(value) tags.push(value) if value not in tags
$scope.$apply -> $scope.$apply ->
$model.$setViewValue(normalizeTags(tags)) $model.$setViewValue(tags)
deleteValue = (value) ->
value = trim(value.toLowerCase())
return if value.length == 0
tags = _.clone($model.$modelValue, false)
tags = _.pull(tags, value)
$scope.$apply ->
$model.$setViewValue(tags)
saveInputTag = () -> saveInputTag = () ->
input = $el.find('input') value = $el.find("input").val()
addValue(input.val()) addValue(value)
input.val("") resetInput()
input.autocomplete("close") hideSaveButton()
$el.find('.save').hide()
$scope.$watch $attrs.ngModel, (val) -> ## Events
tags_colors = if $scope.project?.tags_colors? then $scope.project.tags_colors else [] $el.on "keypress", "input", (event) ->
renderTags($el, val, editable, tags_colors) return if event.keyCode != ENTER_KEY
event.preventDefault()
bindOnce $scope, "projectId", (projectId) -> $el.on "keyup", "input", (event) ->
# If not editable, no tags preloading is needed. target = angular.element(event.currentTarget)
return if not editable
if event.keyCode == ENTER_KEY
saveInputTag()
else
if target.val().length
showSaveButton()
else
hideSaveButton()
$el.on "click", ".save", (event) ->
event.preventDefault()
saveInputTag()
$el.on "click", ".icon-delete", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
value = target.siblings(".tag-name").text()
deleteValue(value)
bindOnce $scope, "project", (project) ->
positioningFunction = (position, elements) -> positioningFunction = (position, elements) ->
menu = elements.element.element menu = elements.element.element
menu.css("width", elements.target.width) menu.css("width", elements.target.width)
menu.css("top", position.top) menu.css("top", position.top)
menu.css("left", position.left) menu.css("left", position.left)
$rs.projects.tags(projectId).then (data) ->
$el.find("input").autocomplete({ $el.find("input").autocomplete({
source: data source: _.keys(project.tags_colors)
position: { position: {
my: "left top", my: "left top",
using: positioningFunction using: positioningFunction
@ -172,38 +207,12 @@ TagLineDirective = ($log, $rs) ->
ui.item.value = "" ui.item.value = ""
}) })
if not editable $scope.$watch $attrs.ngModel, (tags) ->
$el.find("input").remove() tagsColors = $scope.project?.tags_colors or []
renderTags(tags, tagsColors)
$el.on "keypress", "input", (event) -> $scope.$on "$destroy", ->
return if event.keyCode != 13 $el.off()
event.preventDefault()
$el.on "keyup", "input", (event) ->
target = angular.element(event.currentTarget)
if event.keyCode == 13
saveInputTag()
else if target.val().length
$el.find('.save').show()
else
$el.find('.save').hide()
$el.on "click", ".save", saveInputTag
$el.on "click", ".icon-delete", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
value = trim(target.siblings(".tag-name").text())
if value.length <= 0
return
tags = _.clone($model.$modelValue, false)
tags = _.pull(tags, value)
$scope.$apply ->
$model.$setViewValue(normalizeTags(tags))
return { return {
link:link, link:link,
@ -211,4 +220,198 @@ TagLineDirective = ($log, $rs) ->
template: template template: template
} }
module.directive("tgTagLine", ["$log", "$tgResources", TagLineDirective]) module.directive("tgLbTagLine", ["$tgResources", LbTagLineDirective])
#############################################################################
## TagLine Directive (for detail pages)
#############################################################################
TagLineDirective = ($rootScope, $repo, $rs, $confirm) ->
ENTER_KEY = 13
ESC_KEY = 27
template = """
<div class="tags-container"></div>
<a href="#" class="add-tag hidden" title="Add tag">
<span class="icon icon-plus"></span>
<span class="add-tag-text">Add tag</span>
</a>
<input type="text" placeholder="Write tag..." class="tag-input hidden" />
<a href="" title="Save" class="save icon icon-floppy hidden"></a>
""" # TODO: i18n
# Tags template (rendered manually using lodash)
templateTags = _.template("""
<% _.each(tags, function(tag) { %>
<span class="tag" style="border-left: 5px solid <%- tag.color %>;">
<span class="tag-name"><%- tag.name %></span>
<% if (isEditable) { %>
<a href="" title="delete tag" class="icon icon-delete"></a>
<% } %>
</span>
<% }); %>
""") # TODO: i18n
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
## Render
renderTags = (tags, tagsColors) ->
ctx = {
tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]})
isEditable: isEditable()
}
html = templateTags(ctx)
$el.find("div.tags-container").html(html)
renderInReadModeOnly = ->
$el.find(".add-tag").remove()
$el.find("input").remove()
$el.find(".save").remove()
showAddTagButton = -> $el.find(".add-tag").removeClass("hidden")
hideAddTagButton = -> $el.find(".add-tag").addClass("hidden")
showAddTagButtonText = -> $el.find(".add-tag-text").removeClass("hidden")
hideAddTagButtonText = -> $el.find(".add-tag-text").addClass("hidden")
showSaveButton = -> $el.find(".save").removeClass("hidden")
hideSaveButton = -> $el.find(".save").addClass("hidden")
showInput = -> $el.find("input").removeClass("hidden")
hideInput = -> $el.find("input").addClass("hidden")
resetInput = ->
$el.find("input").val("")
$el.find("input").autocomplete("close")
## Aux methods
addValue = (value) ->
value = trim(value.toLowerCase())
return if value.length == 0
tags = _.clone($model.$modelValue.tags, false)
tags = [] if not tags?
tags.push(value) if value not in tags
model = $model.$modelValue.clone()
model.tags = tags
$model.$setViewValue(model)
onSuccess = ->
$rootScope.$broadcast("history:reload")
onError = ->
$confirm.notify("error")
model.revert()
$model.$setViewValue(model)
$repo.save(model).then(onSuccess, onError)
deleteValue = (value) ->
value = trim(value.toLowerCase())
return if value.length == 0
tags = _.clone($model.$modelValue.tags, false)
tags = _.pull(tags, value)
model = $model.$modelValue.clone()
model.tags = tags
$model.$setViewValue(model)
onSuccess = ->
$rootScope.$broadcast("history:reload")
onError = ->
$confirm.notify("error")
model.revert()
$model.$setViewValue(model)
$repo.save(model).then(onSuccess, onError)
saveInputTag = () ->
value = $el.find("input").val()
addValue(value)
resetInput()
hideSaveButton()
## Events
$el.on "keypress", "input", (event) ->
return if event.keyCode not in [ENTER_KEY, ESC_KEY]
event.preventDefault()
$el.on "keyup", "input", (event) ->
target = angular.element(event.currentTarget)
if event.keyCode == ENTER_KEY
saveInputTag()
else if event.keyCode == ESC_KEY
resetInput()
hideInput()
hideSaveButton()
showAddTagButton()
else
if target.val().length
showSaveButton()
else
hideSaveButton()
$el.on "click", ".save", (event) ->
event.preventDefault()
saveInputTag()
$el.on "click", ".add-tag", (event) ->
event.preventDefault()
hideAddTagButton()
showInput()
$el.on "click", ".icon-delete", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
value = target.siblings(".tag-name").text()
deleteValue(value)
bindOnce $scope, "project", (project) ->
if not isEditable()
renderInReadModeOnly()
return
showAddTagButton()
positioningFunction = (position, elements) ->
menu = elements.element.element
menu.css("width", elements.target.width)
menu.css("top", position.top)
menu.css("left", position.left)
$el.find("input").autocomplete({
source: _.keys(project.tags_colors)
position: {
my: "left top",
using: positioningFunction
}
select: (event, ui) ->
addValue(ui.item.value)
ui.item.value = ""
})
$scope.$watch $attrs.ngModel, (model) ->
return if not model
if model.tags.length
hideAddTagButtonText()
else
showAddTagButtonText()
tagsColors = $scope.project?.tags_colors or []
renderTags(model.tags, tagsColors)
$scope.$on "$destroy", ->
$el.off()
return {
link:link,
require:"ngModel"
template: template
}
module.directive("tgTagLine", ["$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", TagLineDirective])

View File

@ -32,7 +32,7 @@ tgMarkitupDirective = ($rootscope, $rs, $tr) ->
previewTemplate = _.template(""" previewTemplate = _.template("""
<div class="preview"> <div class="preview">
<div class="actions"> <div class="actions">
<a href="#" title="Edit">Edit</a> <a href="#" title="Edit" class="icon icon-edit edit"></a>
</div> </div>
<div class="content wysiwyg"> <div class="content wysiwyg">
<%= data %> <%= data %>

View File

@ -46,11 +46,12 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"$log", "$log",
"$appTitle", "$appTitle",
"$tgAnalytics", "$tgAnalytics",
"$tgNavUrls" "$tgNavUrls",
"tgLoader"
] ]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
@log, @appTitle, @analytics, @navUrls) -> @log, @appTitle, @analytics, @navUrls, tgLoader) ->
@scope.issueRef = @params.issueref @scope.issueRef = @params.issueref
@scope.sectionName = "Issue Details" @scope.sectionName = "Issue Details"
@.initializeEventHandlers() @.initializeEventHandlers()
@ -60,11 +61,12 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
# On Success # On Success
promise.then => promise.then =>
@appTitle.set(@scope.issue.subject + " - " + @scope.project.name) @appTitle.set(@scope.issue.subject + " - " + @scope.project.name)
@.initializeOnDeleteGoToUrl()
tgLoader.pageLoaded()
# On Error # On Error
promise.then null, @.onInitialDataError.bind(@) promise.then null, @.onInitialDataError.bind(@)
initializeEventHandlers: -> initializeEventHandlers: ->
@scope.$on "attachment:create", => @scope.$on "attachment:create", =>
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
@ -81,6 +83,13 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
@.loadIssue() @.loadIssue()
initializeOnDeleteGoToUrl: ->
ctx = {project: @scope.project.slug}
if @scope.project.is_issues_activated
@scope.onDeleteGoToUrl = @navUrls.resolve("project-issues", ctx)
else
@scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project @scope.project = project
@ -130,78 +139,24 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
.then(=> @.loadUsersAndRoles()) .then(=> @.loadUsersAndRoles())
.then(=> @.loadIssue()) .then(=> @.loadIssue())
block: ->
@rootscope.$broadcast("block", @scope.issue)
unblock: ->
@rootscope.$broadcast("unblock", @scope.issue)
delete: ->
# TODO: i18n
title = "Delete Issue"
message = @scope.issue.subject
@confirm.askOnDelete(title, message).then (finish) =>
promise = @.repo.remove(@scope.issue)
promise.then =>
finish()
@location.path(@navUrls.resolve("project-issues", {project: @scope.project.slug}))
promise.then null, =>
finish(false)
@confirm.notify("error")
module.controller("IssueDetailController", IssueDetailController) module.controller("IssueDetailController", IssueDetailController)
############################################################################# #############################################################################
## Issue Main Directive ## Issue status display directive
############################################################################# #############################################################################
IssueDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) -> IssueStatusDisplayDirective = ->
linkSidebar = ($scope, $el, $attrs, $ctrl) -> # Display if a Issue is open or closed and its issueboard status.
#
# Example:
# tg-issue-status-display(ng-model="issue")
#
# Requirements:
# - Issue object (ng-model)
# - scope.statusById object
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
linkSidebar($scope, $el, $attrs, $ctrl)
if $el.is("form")
form = $el.checksley()
$el.on "click", ".save-issue", (event) ->
if not form.validate()
return
onSuccess = ->
$loading.finish(target)
$confirm.notify("success")
ctx = {
project: $scope.project.slug
ref: $scope.issue.ref
}
$location.path($navUrls.resolve("project-issues-detail", ctx))
onError = ->
$loading.finish(target)
$confirm.notify("error")
target = angular.element(event.currentTarget)
$loading.start(target)
$tgrepo.save($scope.issue).then(onSuccess, onError)
return {link:link}
module.directive("tgIssueDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", "$tgNavUrls",
"$tgLoading", IssueDirective])
#############################################################################
## Issue status directive
#############################################################################
IssueStatusDirective = () ->
# TODO: i18n
template = _.template(""" template = _.template("""
<h1>
<span> <span>
<% if (status.is_closed) { %> <% if (status.is_closed) { %>
Closed Closed
@ -209,171 +164,407 @@ IssueStatusDirective = () ->
Open Open
<% } %> <% } %>
</span> </span>
<span class="us-detail-status" style="color:<%= status.color %>"><%= status.name %></span> <span class="us-detail-status" style="color:<%= status.color %>">
</h1> <%= status.name %>
<div class="us-created-by"> </span>
<div class="user-avatar"> """) # TODO: i18n
<img src="<%= owner.photo %>" alt="<%- owner.full_name_display %>" />
</div>
<div class="created-by"> link = ($scope, $el, $attrs) ->
<span class="created-title">Created by <%- owner.full_name_display %></span> render = (issue) ->
<span class="created-date"><%- date %></span>
</div>
</div>
<div class="issue-data">
<div class="type-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= type.color %>"></span>
<span class="type-status"><%= type.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">type</span>
</div>
<div class="severity-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= severity.color %>"></span>
<span class="severity-status"><%= severity.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">severity</span>
</div>
<div class="priority-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= priority.color %>"></span>
<span class="priority-status"><%= priority.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">priority</span>
</div>
<div class="status-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= status.color %>"></span>
<span class="status-status"><%= status.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">status</span>
</div>
</div>
""")
selectionTypeTemplate = _.template("""
<ul class="popover pop-type">
<% _.each(types, function(type) { %>
<li><a href="" class="type" title="<%- type.name %>"
data-type-id="<%- type.id %>"><%- type.name %></a></li>
<% }); %>
</ul>
""")
selectionSeverityTemplate = _.template("""
<ul class="popover pop-severity">
<% _.each(severities, function(severity) { %>
<li><a href="" class="severity" title="<%- severity.name %>"
data-severity-id="<%- severity.id %>"><%- severity.name %></a></li>
<% }); %>
</ul>
""")
selectionPriorityTemplate = _.template("""
<ul class="popover pop-priority">
<% _.each(priorities, function(priority) { %>
<li><a href="" class="priority" title="<%- priority.name %>"
data-priority-id="<%- priority.id %>"><%- priority.name %></a></li>
<% }); %>
</ul>
""")
selectionStatusTemplate = _.template("""
<ul class="popover pop-status">
<% _.each(statuses, function(status) { %>
<li><a href="" class="status" title="<%- status.name %>"
data-status-id="<%- status.id %>"><%- status.name %></a></li>
<% }); %>
</ul>
""")
link = ($scope, $el, $attrs, $model) ->
editable = $attrs.editable?
renderIssuestatus = (issue) ->
owner = $scope.usersById?[issue.owner]
date = moment(issue.created_date).format("DD MMM YYYY HH:mm")
type = $scope.typeById[issue.type]
status = $scope.statusById[issue.status]
severity = $scope.severityById[issue.severity]
priority = $scope.priorityById[issue.priority]
html = template({ html = template({
owner: owner status: $scope.statusById[issue.status]
date: date
editable: editable
status: status
severity: severity
priority: priority
type: type
}) })
$el.html(html) $el.html(html)
$el.find(".type-data").append(selectionTypeTemplate({types:$scope.typeList}))
$el.find(".severity-data").append(selectionSeverityTemplate({severities:$scope.severityList}))
$el.find(".priority-data").append(selectionPriorityTemplate({priorities:$scope.priorityList}))
$el.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList}))
$scope.$watch $attrs.ngModel, (issue) -> $scope.$watch $attrs.ngModel, (issue) ->
if issue? render(issue) if issue?
renderIssuestatus(issue)
if editable $scope.$on "$destroy", ->
$el.on "click", ".type-data", (event) -> $el.off()
event.preventDefault()
event.stopPropagation()
$el.find(".pop-type").popover().open()
$el.on "click", ".type", (event) -> return {
event.preventDefault() link: link
event.stopPropagation() restrict: "EA"
target = angular.element(event.currentTarget) require: "ngModel"
$model.$modelValue.type = target.data("type-id") }
renderIssuestatus($model.$modelValue)
$.fn.popover().closeAll()
$el.on "click", ".severity-data", (event) -> module.directive("tgIssueStatusDisplay", IssueStatusDisplayDirective)
event.preventDefault()
event.stopPropagation()
$el.find(".pop-severity").popover().open()
$el.on "click", ".severity", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$model.$modelValue.severity = target.data("severity-id")
renderIssuestatus($model.$modelValue)
$.fn.popover().closeAll()
$el.on "click", ".priority-data", (event) -> #############################################################################
event.preventDefault() ## Issue status button directive
event.stopPropagation() #############################################################################
$el.find(".pop-priority").popover().open()
$el.on "click", ".priority", (event) -> IssueStatusButtonDirective = ($rootScope, $repo, $confirm, $loading) ->
event.preventDefault() # Display the status of Issue and you can edit it.
event.stopPropagation() #
target = angular.element(event.currentTarget) # Example:
$model.$modelValue.priority = target.data("priority-id") # tg-issue-status-button(ng-model="issue")
renderIssuestatus($model.$modelValue) #
$.fn.popover().closeAll() # Requirements:
# - Issue object (ng-model)
# - scope.statusById object
# - $scope.project.my_permissions
template = _.template("""
<div class="status-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%= status.color %>"></span>
<span class="status-status"><%= status.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">status</span>
<ul class="popover pop-status">
<% _.each(statuses, function(st) { %>
<li><a href="" class="status" title="<%- st.name %>"
data-status-id="<%- st.id %>"><%- st.name %></a></li>
<% }); %>
</ul>
</div>
""") #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_issue") != -1
render = (issue) =>
status = $scope.statusById[issue.status]
html = template({
status: status
statuses: $scope.statusList
editable: isEditable()
})
$el.html(html)
$el.on "click", ".status-data", (event) -> $el.on "click", ".status-data", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
$el.find(".pop-status").popover().open() $el.find(".pop-status").popover().open()
$el.on "click", ".status", (event) -> $el.on "click", ".status", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
$model.$modelValue.status = target.data("status-id")
renderIssuestatus($model.$modelValue)
$.fn.popover().closeAll() $.fn.popover().closeAll()
return {link:link, require:"ngModel"} issue = $model.$modelValue.clone()
issue.status = target.data("status-id")
$model.$setViewValue(issue)
module.directive("tgIssueStatus", IssueStatusDirective) $scope.$apply()
onSuccess = ->
$confirm.notify("success")
$rootScope.$broadcast("history:reload")
$loading.finish($el.find(".level-name"))
onError = ->
$confirm.notify("error")
issue.revert()
$model.$setViewValue(issue)
$loading.finish($el.find(".level-name"))
$loading.start($el.find(".level-name"))
$repo.save($model.$modelValue).then(onSuccess, onError)
$scope.$watch $attrs.ngModel, (issue) ->
render(issue) if issue
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgIssueStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", IssueStatusButtonDirective])
#############################################################################
## Issue type button directive
#############################################################################
IssueTypeButtonDirective = ($rootScope, $repo, $confirm, $loading) ->
# Display the type of Issue and you can edit it.
#
# Example:
# tg-issue-type-button(ng-model="issue")
#
# Requirements:
# - Issue object (ng-model)
# - scope.typeById object
# - $scope.project.my_permissions
template = _.template("""
<div class="type-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%= type.color %>"></span>
<span class="type-type"><%= type.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">type</span>
<ul class="popover pop-type">
<% _.each(typees, function(tp) { %>
<li><a href="" class="type" title="<%- tp.name %>"
data-type-id="<%- tp.id %>"><%- tp.name %></a></li>
<% }); %>
</ul>
</div>
""") #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_issue") != -1
render = (issue) =>
type = $scope.typeById[issue.type]
html = template({
type: type
typees: $scope.typeList
editable: isEditable()
})
$el.html(html)
$el.on "click", ".type-data", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
$el.find(".pop-type").popover().open()
$el.on "click", ".type", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget)
$.fn.popover().closeAll()
issue = $model.$modelValue.clone()
issue.type = target.data("type-id")
$model.$setViewValue(issue)
$scope.$apply()
onSuccess = ->
$confirm.notify("success")
$rootScope.$broadcast("history:reload")
$loading.finish($el.find(".level-name"))
onError = ->
$confirm.notify("error")
issue.revert()
$model.$setViewValue(issue)
$loading.finish($el.find(".level-name"))
$loading.start($el.find(".level-name"))
$repo.save($model.$modelValue).then(onSuccess, onError)
$scope.$watch $attrs.ngModel, (issue) ->
render(issue) if issue
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgIssueTypeButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", IssueTypeButtonDirective])
#############################################################################
## Issue severity button directive
#############################################################################
IssueSeverityButtonDirective = ($rootScope, $repo, $confirm, $loading) ->
# Display the severity of Issue and you can edit it.
#
# Example:
# tg-issue-severity-button(ng-model="issue")
#
# Requirements:
# - Issue object (ng-model)
# - scope.severityById object
# - $scope.project.my_permissions
template = _.template("""
<div class="severity-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%= severity.color %>"></span>
<span class="severity-severity"><%= severity.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">severity</span>
<ul class="popover pop-severity">
<% _.each(severityes, function(sv) { %>
<li><a href="" class="severity" title="<%- sv.name %>"
data-severity-id="<%- sv.id %>"><%- sv.name %></a></li>
<% }); %>
</ul>
</div>
""") #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_issue") != -1
render = (issue) =>
severity = $scope.severityById[issue.severity]
html = template({
severity: severity
severityes: $scope.severityList
editable: isEditable()
})
$el.html(html)
$el.on "click", ".severity-data", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
$el.find(".pop-severity").popover().open()
$el.on "click", ".severity", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget)
$.fn.popover().closeAll()
issue = $model.$modelValue.clone()
issue.severity = target.data("severity-id")
$model.$setViewValue(issue)
$scope.$apply()
onSuccess = ->
$confirm.notify("success")
$rootScope.$broadcast("history:reload")
$loading.finish($el.find(".level-name"))
onError = ->
$confirm.notify("error")
issue.revert()
$model.$setViewValue(issue)
$loading.finish($el.find(".level-name"))
$loading.start($el.find(".level-name"))
$repo.save($model.$modelValue).then(onSuccess, onError)
$scope.$watch $attrs.ngModel, (issue) ->
render(issue) if issue
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgIssueSeverityButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", IssueSeverityButtonDirective])
#############################################################################
## Issue priority button directive
#############################################################################
IssuePriorityButtonDirective = ($rootScope, $repo, $confirm, $loading) ->
# Display the priority of Issue and you can edit it.
#
# Example:
# tg-issue-priority-button(ng-model="issue")
#
# Requirements:
# - Issue object (ng-model)
# - scope.priorityById object
# - $scope.project.my_permissions
template = _.template("""
<div class="priority-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%= priority.color %>"></span>
<span class="priority-priority"><%= priority.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">priority</span>
<ul class="popover pop-priority">
<% _.each(priorityes, function(pr) { %>
<li><a href="" class="priority" title="<%- pr.name %>"
data-priority-id="<%- pr.id %>"><%- pr.name %></a></li>
<% }); %>
</ul>
</div>
""") #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_issue") != -1
render = (issue) =>
priority = $scope.priorityById[issue.priority]
html = template({
priority: priority
priorityes: $scope.priorityList
editable: isEditable()
})
$el.html(html)
$el.on "click", ".priority-data", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
$el.find(".pop-priority").popover().open()
$el.on "click", ".priority", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget)
$.fn.popover().closeAll()
issue = $model.$modelValue.clone()
issue.priority = target.data("priority-id")
$model.$setViewValue(issue)
$scope.$apply()
onSuccess = ->
$confirm.notify("success")
$rootScope.$broadcast("history:reload")
$loading.finish($el.find(".level-name"))
onError = ->
$confirm.notify("error")
issue.revert()
$model.$setViewValue(issue)
$loading.finish($el.find(".level-name"))
$loading.start($el.find(".level-name"))
$repo.save($model.$modelValue).then(onSuccess, onError)
$scope.$watch $attrs.ngModel, (issue) ->
render(issue) if issue
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgIssuePriorityButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", IssuePriorityButtonDirective])
############################################################################# #############################################################################

View File

@ -53,7 +53,7 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) ->
</a> </a>
</div> </div>
<div tg-related-task-assigned-to-inline-edition="task" class="assigned-to"> <div tg-related-task-assigned-to-inline-edition="task" class="assigned-to">
<div title="Assigned to" class="task-assignedto"> <div title="Assigned to" class="task-assignedto <% if(perms.modify_task) { %>editable<% } %>">
<figure class="avatar"></figure> <figure class="avatar"></figure>
<% if(perms.modify_task) { %> <% if(perms.modify_task) { %>
<span class="icon icon-arrow-bottom"></span> <span class="icon icon-arrow-bottom"></span>
@ -166,7 +166,7 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) ->
return {link:link, require:"ngModel"} return {link:link, require:"ngModel"}
module.directive("tgRelatedTaskRow", ["$tgRepo", "$compile", "$tgConfirm", "$rootScope", "$tgLoading", RelatedTaskRowDirective]) module.directive("tgRelatedTaskRow", ["$tgRepo", "$compile", "$tgConfirm", "$rootScope", "$tgLoading", "$tgAnalytics", RelatedTaskRowDirective])
RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel, $loading, $analytics) -> RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel, $loading, $analytics) ->
template = _.template(""" template = _.template("""

View File

@ -45,9 +45,6 @@ resourceProvider = ($repo) ->
service.stats = (projectId) -> service.stats = (projectId) ->
return $repo.queryOneRaw("projects", "#{projectId}/stats") return $repo.queryOneRaw("projects", "#{projectId}/stats")
service.tags = (projectId) ->
return $repo.queryOneRaw("projects", "#{projectId}/tags")
service.tagsColors = (id) -> service.tagsColors = (id) ->
return $repo.queryOne("projects", "#{id}/tags_colors") return $repo.queryOne("projects", "#{id}/tags_colors")

View File

@ -25,7 +25,7 @@ debounce = @.taiga.debounce
CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) -> CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) ->
link = ($scope, $el, attrs) -> link = ($scope, $el, attrs) ->
isNew = true $scope.isNew = true
$scope.$on "taskform:new", (ctx, sprintId, usId) -> $scope.$on "taskform:new", (ctx, sprintId, usId) ->
$scope.task = { $scope.task = {
@ -37,7 +37,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) ->
assigned_to: null assigned_to: null
tags: [] tags: []
} }
isNew = true $scope.isNew = true
# Update texts for creation # Update texts for creation
$el.find(".button-green span").html("Create") #TODO: i18n $el.find(".button-green span").html("Create") #TODO: i18n
@ -46,7 +46,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) ->
$scope.$on "taskform:edit", (ctx, task) -> $scope.$on "taskform:edit", (ctx, task) ->
$scope.task = task $scope.task = task
isNew = false $scope.isNew = false
# Update texts for edition # Update texts for edition
$el.find(".button-green span").html("Save") #TODO: i18n $el.find(".button-green span").html("Save") #TODO: i18n
@ -60,7 +60,7 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, lightboxService) ->
if not form.validate() if not form.validate()
return return
if isNew if $scope.isNew
promise = $repo.create("tasks", $scope.task) promise = $repo.create("tasks", $scope.task)
broadcastEvent = "taskform:new:success" broadcastEvent = "taskform:new:success"
else else

View File

@ -57,6 +57,7 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
promise.then () => promise.then () =>
@appTitle.set(@scope.task.subject + " - " + @scope.project.name) @appTitle.set(@scope.task.subject + " - " + @scope.project.name)
@.initializeOnDeleteGoToUrl()
tgLoader.pageLoaded() tgLoader.pageLoaded()
promise.then null, @.onInitialDataError.bind(@) promise.then null, @.onInitialDataError.bind(@)
@ -70,6 +71,21 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$on "attachment:delete", => @scope.$on "attachment:delete", =>
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
initializeOnDeleteGoToUrl: ->
ctx = {project: @scope.project.slug}
@scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx)
if @scope.project.is_backlog_activated
if @scope.task.milestone
ctx.sprint = @scope.sprint.slug
@scope.onDeleteGoToUrl = @navUrls.resolve("project-taskboard", ctx)
else if @scope.task.us
ctx.ref = @scope.us.ref
@scope.onDeleteGoToUrl = @navUrls.resolve("project-userstories-detail", ctx)
else if @scope.project.is_kanban_activated
if @scope.us
ctx.ref = @scope.us.ref
@scope.onDeleteGoToUrl = @navUrls.resolve("project-userstories-detail", ctx)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project @scope.project = project
@ -97,14 +113,19 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
ref: @scope.task.neighbors.next.ref ref: @scope.task.neighbors.next.ref
} }
@scope.nextUrl = @navUrls.resolve("project-tasks-detail", ctx) @scope.nextUrl = @navUrls.resolve("project-tasks-detail", ctx)
return task
if task.milestone loadSprint: ->
@rs.sprints.get(task.project, task.milestone).then (sprint) => if @scope.task.milestone
return @rs.sprints.get(@scope.task.project, @scope.task.milestone).then (sprint) =>
@scope.sprint = sprint @scope.sprint = sprint
return sprint
if task.user_story loadUserStory: ->
@rs.userstories.get(task.project, task.user_story).then (us) => if @scope.task.user_story
return @rs.userstories.get(@scope.task.project, @scope.task.user_story).then (us) =>
@scope.us = us @scope.us = us
return us
loadInitialData: -> loadInitialData: ->
params = { params = {
@ -119,157 +140,216 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles()) .then(=> @.loadUsersAndRoles())
.then(=> @.loadTask()) .then(=> @.loadTask().then(=> @q.all([@.loadUserStory(),
@.loadSprint()])))
block: ->
@rootscope.$broadcast("block", @scope.task)
unblock: ->
@rootscope.$broadcast("unblock", @scope.task)
delete: ->
#TODO: i18n
title = "Delete Task"
message = @scope.task.subject
@confirm.askOnDelete(title, message).then (finish) =>
promise = @.repo.remove(@scope.task)
promise.then =>
finish()
if @scope.task.milestone
@location.path(@navUrls.resolve("project-taskboard", {project: @scope.project.slug, sprint: @scope.sprint.slug}))
else if @scope.us
@location.path(@navUrls.resolve("project-userstories-detail", {project: @scope.project.slug, ref: @scope.us.ref}))
promise.then null, =>
finish(false)
@confirm.notify("error")
module.controller("TaskDetailController", TaskDetailController) module.controller("TaskDetailController", TaskDetailController)
############################################################################# #############################################################################
## Task Main Directive ## Task status display directive
############################################################################# #############################################################################
TaskDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) -> TaskStatusDisplayDirective = ->
linkSidebar = ($scope, $el, $attrs, $ctrl) -> # Display if a Task is open or closed and its taskboard status.
#
# Example:
# tg-task-status-display(ng-model="task")
#
# Requirements:
# - Task object (ng-model)
# - scope.statusById object
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
linkSidebar($scope, $el, $attrs, $ctrl)
if $el.is("form")
form = $el.checksley()
$el.on "click", ".save-task", (event) ->
if not form.validate()
return
onSuccess = ->
$loading.finish(target)
$confirm.notify("success")
ctx = {
project: $scope.project.slug
ref: $scope.task.ref
}
$location.path($navUrls.resolve("project-tasks-detail", ctx))
onError = ->
$loading.finish(target)
$confirm.notify("error")
target = angular.element(event.currentTarget)
$loading.start(target)
$tgrepo.save($scope.task).then(onSuccess, onError)
return {link:link}
module.directive("tgTaskDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", "$tgNavUrls",
"$tgLoading", TaskDirective])
#############################################################################
## Task status directive
#############################################################################
TaskStatusDirective = () ->
#TODO: i18n
template = _.template(""" template = _.template("""
<h1>
<span> <span>
<% if (status.is_closed) { %> <% if (status.is_closed) { %>
Closed Closed
<% } else { %> <% } else { %>
Open Open
<% } %> <% } %>
<span class="us-detail-status" style="color:<%= status.color %>"><%= status.name %></span> </span>
</h1> <span class="us-detail-status" style="color:<%= status.color %>">
<div class="us-created-by"> <%= status.name %>
<div class="user-avatar"> </span>
<img src="<%= owner.photo %>" alt="<%- owner.full_name_display %>" /> """) # TODO: i18n
</div>
<div class="created-by"> link = ($scope, $el, $attrs) ->
<span class="created-title">Created by <%- owner.full_name_display %></span> render = (task) ->
<span class="created-date"><%- date %></span>
</div>
</div>
<div class="issue-data">
<div class="status-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= status.color %>"></span>
<span class="status-status"><%= status.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">status</span>
</div>
</div>
""")
selectionStatusTemplate = _.template("""
<ul class="popover pop-status">
<% _.each(statuses, function(status) { %>
<li><a href="" class="status" title="<%- status.name %>"
data-status-id="<%- status.id %>"><%- status.name %></a></li>
<% }); %>
</ul>
""")
link = ($scope, $el, $attrs, $model) ->
editable = $attrs.editable?
renderTaskstatus = (task) ->
owner = $scope.usersById?[task.owner]
date = moment(task.created_date).format("DD MMM YYYY HH:mm")
status = $scope.statusById[task.status]
html = template({ html = template({
owner: owner status: $scope.statusById[task.status]
date: date
editable: editable
status: status
}) })
$el.html(html) $el.html(html)
$el.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList}))
$scope.$watch $attrs.ngModel, (task) -> $scope.$watch $attrs.ngModel, (task) ->
if task? render(task) if task?
renderTaskstatus(task)
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgTaskStatusDisplay", TaskStatusDisplayDirective)
#############################################################################
## Task status button directive
#############################################################################
TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading) ->
# Display the status of Task and you can edit it.
#
# Example:
# tg-task-status-button(ng-model="task")
#
# Requirements:
# - Task object (ng-model)
# - scope.statusById object
# - $scope.project.my_permissions
template = _.template("""
<div class="status-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%= status.color %>"></span>
<span class="status-status"><%= status.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">status</span>
<ul class="popover pop-status">
<% _.each(statuses, function(st) { %>
<li><a href="" class="status" title="<%- st.name %>"
data-status-id="<%- st.id %>"><%- st.name %></a></li>
<% }); %>
</ul>
</div>
""") #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_task") != -1
render = (task) =>
status = $scope.statusById[task.status]
html = template({
status: status
statuses: $scope.statusList
editable: isEditable()
})
$el.html(html)
if editable
$el.on "click", ".status-data", (event) -> $el.on "click", ".status-data", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
$el.find(".pop-status").popover().open() $el.find(".pop-status").popover().open()
$el.on "click", ".status", (event) -> $el.on "click", ".status", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
$model.$modelValue.status = target.data("status-id")
renderTaskstatus($model.$modelValue)
$el.find(".popover").popover().close()
return {link:link, require:"ngModel"} $.fn.popover().closeAll()
module.directive("tgTaskStatus", TaskStatusDirective) task = $model.$modelValue.clone()
task.status = target.data("status-id")
$model.$setViewValue(task)
$scope.$apply()
onSuccess = ->
$confirm.notify("success")
$rootScope.$broadcast("history:reload")
$loading.finish($el.find(".level-name"))
onError = ->
$confirm.notify("error")
task.revert()
$model.$setViewValue(task)
$loading.finish($el.find(".level-name"))
$loading.start($el.find(".level-name"))
$repo.save($model.$modelValue).then(onSuccess, onError)
$scope.$watch $attrs.ngModel, (task) ->
render(task) if task
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgTaskStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading",
TaskStatusButtonDirective])
TaskIsIocaineButtonDirective = ($rootscope, $tgrepo, $confirm, $loading) ->
template = _.template("""
<fieldset title="Feeling a bit overwhelmed by a task? Make sure others know about it by clicking on Iocaine when editing a task. It's possible to become immune to this (fictional) deadly poison by consuming small amounts over time just as it's possible to get better at what you do by occasionally taking on extra challenges!">
<label for="is-iocaine"
class="button button-gray is-iocaine <% if(isEditable){ %>editable<% }; %> <% if(isIocaine){ %>active<% }; %>">
Iocaine
</label>
<input type="checkbox" id="is-iocaine" name="is-iocaine"/>
</fieldset>
""")
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_task") != -1
render = (task) ->
if not isEditable() and not task.is_iocaine
$el.html("")
return
ctx = {
isIocaine: task.is_iocaine
isEditable: isEditable()
}
html = template(ctx)
$el.html(html)
$el.on "click", ".is-iocaine", (event) ->
return if not isEditable()
task = $model.$modelValue.clone()
task.is_iocaine = not task.is_iocaine
$model.$setViewValue(task)
$loading.start($el.find('label'))
promise = $tgrepo.save($model.$modelValue)
promise.then ->
$confirm.notify("success")
$rootscope.$broadcast("history:reload")
promise.then null, ->
task.revert()
$model.$setViewValue(task)
$confirm.notify("error")
promise.finally ->
$loading.finish($el.find('label'))
$scope.$watch $attrs.ngModel, (task) ->
render(task) if task
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgTaskIsIocaineButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", TaskIsIocaineButtonDirective])

View File

@ -59,12 +59,17 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
# On Success # On Success
promise.then => promise.then =>
@appTitle.set(@scope.us.subject + " - " + @scope.project.name) @appTitle.set(@scope.us.subject + " - " + @scope.project.name)
@.initializeOnDeleteGoToUrl()
tgLoader.pageLoaded() tgLoader.pageLoaded()
# On Error # On Error
promise.then null, @.onInitialDataError.bind(@) promise.then null, @.onInitialDataError.bind(@)
initializeEventHandlers: -> initializeEventHandlers: ->
@scope.$on "related-tasks:update", =>
@.loadUs()
@scope.tasks = _.clone(@scope.tasks, false)
@scope.$on "attachment:create", => @scope.$on "attachment:create", =>
@analytics.trackEvent("attachment", "create", "create attachment on userstory", 1) @analytics.trackEvent("attachment", "create", "create attachment on userstory", 1)
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
@ -75,6 +80,18 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$on "attachment:delete", => @scope.$on "attachment:delete", =>
@rootscope.$broadcast("history:reload") @rootscope.$broadcast("history:reload")
initializeOnDeleteGoToUrl: ->
ctx = {project: @scope.project.slug}
@scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx)
if @scope.project.is_backlog_activated
if @scope.us.milestone
ctx.sprint = @scope.sprint.slug
@scope.onDeleteGoToUrl = @navUrls.resolve("project-taskboard", ctx)
else
@scope.onDeleteGoToUrl = @navUrls.resolve("project-backlog", ctx)
else if @scope.project.is_kanban_activated
@scope.onDeleteGoToUrl = @navUrls.resolve("project-kanban", ctx)
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project @scope.project = project
@ -106,12 +123,14 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
} }
@scope.nextUrl = @navUrls.resolve("project-userstories-detail", ctx) @scope.nextUrl = @navUrls.resolve("project-userstories-detail", ctx)
if us.milestone
@rs.sprints.get(us.project, us.milestone).then (sprint) =>
@scope.sprint = sprint
return us return us
loadSprint: ->
if @scope.us.milestone
return @rs.sprints.get(@scope.us.project, @scope.us.milestone).then (sprint) =>
@scope.sprint = sprint
return sprint
loadTasks: -> loadTasks: ->
return @rs.tasks.list(@scope.projectId, null, @scope.usId).then (tasks) => return @rs.tasks.list(@scope.projectId, null, @scope.usId).then (tasks) =>
@scope.tasks = tasks @scope.tasks = tasks
@ -130,263 +149,128 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles()) .then(=> @.loadUsersAndRoles())
.then(=> @q.all([@.loadUs(), .then(=> @q.all([@.loadUs().then(=> @.loadSprint()),
@.loadTasks()])) @.loadTasks()]))
block: ->
@rootscope.$broadcast("block", @scope.us)
unblock: ->
@rootscope.$broadcast("unblock", @scope.us)
delete: ->
#TODO: i18n
title = "Delete User Story"
message = @scope.us.subject
@confirm.askOnDelete(title, message).then (finish) =>
promise = @.repo.remove(@scope.us)
promise.then =>
finish()
if @scope.us.milestone
@location.path(@navUrls.resolve("project-taskboard", {project: @scope.project.slug, sprint: @scope.sprint.slug}))
else if @scope.project.is_backlog_activated
@location.path(@navUrls.resolve("project-backlog", {project: @scope.project.slug}))
else
@location.path(@navUrls.resolve("project-kanban", {project: @scope.project.slug}))
promise.then null, =>
finish(false)
$confirm.notify("error")
module.controller("UserStoryDetailController", UserStoryDetailController) module.controller("UserStoryDetailController", UserStoryDetailController)
#############################################################################
## User story Main Directive
#############################################################################
UsDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) ->
linkSidebar = ($scope, $el, $attrs, $ctrl) ->
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
linkSidebar($scope, $el, $attrs, $ctrl)
if $el.is("form")
form = $el.checksley()
$el.on "click", ".save-us", (event) ->
if not form.validate()
return
onSuccess = ->
$loading.finish(target)
$confirm.notify("success")
ctx = {
project: $scope.project.slug
ref: $scope.us.ref
}
$location.path($navUrls.resolve("project-userstories-detail", ctx))
onError = ->
$loading.finish(target)
$confirm.notify("error")
target = angular.element(event.currentTarget)
$loading.start(target)
$tgrepo.save($scope.us).then(onSuccess, onError)
return {link:link}
module.directive("tgUsDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm",
"$tgNavUrls", "$tgLoading", UsDirective])
############################################################################# #############################################################################
## User story status directive ## User story status display directive
############################################################################# #############################################################################
UsStatusDetailDirective = () -> UsStatusDisplayDirective = ->
#TODO: i18n # Display if a US is open or closed and its kanban status.
#
# Example:
# tg-us-status-display(ng-model="us")
#
# Requirements:
# - US object (ng-model)
# - scope.statusById object
template = _.template(""" template = _.template("""
<h1>
<span> <span>
<% if (is_closed) { %> <% if (is_closed) { %>
Closed Closed
<% } else { %> <% } else { %>
Open Open
<% } %> <% } %>
<span class="us-detail-status" style="color:<%= status.color %>"><%= status.name %></span> </span>
</h1> <span class="us-detail-status" style="color:<%= status.color %>">
<%= status.name %>
</span>
""") # TODO: i18n
<div class="us-detail-progress-bar"> link = ($scope, $el, $attrs) ->
<div class="current-progress" style="width:<%- usProgress %>%"/> render = (us) ->
html = template({
is_closed: us.is_closed
status: $scope.statusById[us.status]
})
$el.html(html)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us?
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgUsStatusDisplay", UsStatusDisplayDirective)
#############################################################################
## User story related tasts progress splay Directive
#############################################################################
UsTasksProgressDisplayDirective = ->
# Display a progress bar with the stats of completed tasks.
#
# Example:
# tg-us-tasks-progress-display(ng-model="tasks")
#
# Requirements:
# - Task object list (ng-model)
# - scope.taskStatusById object
template = _.template("""
<div class="current-progress" style="width:<%- progress %>%" />
<span clasS="tasks-completed"> <span clasS="tasks-completed">
<%- totalClosedTasks %>/<%- totalTasks %> tasks completed <%- totalClosedTasks %>/<%- totalTasks %> tasks completed
</span> </span>
</div> """) # TODO: i18n
<div class="us-created-by"> link = ($scope, $el, $attrs) ->
<div class="user-avatar"> render = (tasks) ->
<img src="<%= owner.photo %>" alt="<%- owner.full_name_display %>" /> totalTasks = tasks.length
</div> totalClosedTasks = _.filter(tasks, (task) => $scope.taskStatusById[task.status].is_closed).length
<div class="created-by"> progress = if totalTasks > 0 then 100 * totalClosedTasks / totalTasks else 0
<span class="created-title">Created by <%- owner.full_name_display %></span>
<span class="created-date"><%- date %></span>
</div>
</div>
<ul class="points-per-role">
<li class="total">
<span class="points"><%- totalPoints %></span>
<span class="role">total</span>
</li>
<% _.each(rolePoints, function(rolePoint) { %>
<li class="total <% if (editable) { %>clickable<% } %>" data-role-id="<%- rolePoint.id %>">
<span class="points"><%- rolePoint.points %></span>
<span class="role"><%- rolePoint.name %></span></li>
<% }); %>
</ul>
<div class="issue-data">
<div class="status-data <% if (editable) { %>clickable<% } %>">
<span class="level" style="background-color:<%= status.color %>"></span>
<span class="status-status"><%= status.name %></span>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<span class="level-name">status</span>
</div>
</div>
""")
selectionStatusTemplate = _.template("""
<ul class="popover pop-status">
<% _.each(statuses, function(status) { %>
<li><a href="" class="status" title="<%- status.name %>"
data-status-id="<%- status.id %>"><%- status.name %></a></li>
<% }); %>
</ul>
""")
selectionPointsTemplate = _.template("""
<ul class="popover pop-points-open">
<% _.each(points, function(point) { %>
<li><a href="" class="point" title="<%- point.name %>"
data-point-id="<%- point.id %>"><%- point.name %></a>
</li>
<% }); %>
</ul>
""")
link = ($scope, $el, $attrs, $model) ->
editable = $attrs.editable?
updatingSelectedRoleId = null
$ctrl = $el.controller()
showSelectPoints = (target) ->
us = $model.$modelValue
$el.find(".pop-points-open").remove()
$el.find(target).append(selectionPointsTemplate({ "points": $scope.project.points }))
target.removeClass('active')
$el.find(".pop-points-open a[data-point-id='#{us.points[updatingSelectedRoleId]}']").addClass("active")
# If not showing role selection let's move to the left
$el.find(".pop-points-open").popover().open()
calculateTotalPoints = (us)->
values = _.map(us.points, (v, k) -> $scope.pointsById[v].value)
values = _.filter(values, (num) -> num?)
if values.length == 0
return "?"
return _.reduce(values, (acc, num) -> acc + num)
renderUsstatus = (us) ->
owner = $scope.usersById?[us.owner]
date = moment(us.created_date).format("DD MMM YYYY HH:mm")
status = $scope.statusById[us.status]
rolePoints = _.clone(_.filter($scope.project.roles, "computable"), true)
_.map rolePoints, (v, k) ->
name = $scope.pointsById[us.points[v.id]].name
name = "?" if not name?
v.points = name
totalTasks = $scope.tasks.length
totalClosedTasks = _.filter($scope.tasks, (task) => $scope.taskStatusById[task.status].is_closed).length
usProgress = 0
usProgress = 100 * totalClosedTasks / totalTasks if totalTasks > 0
html = template({ html = template({
owner: owner
date: date
editable: editable
is_closed: us.is_closed
status: status
totalPoints: us.total_points
rolePoints: rolePoints
totalTasks: totalTasks totalTasks: totalTasks
totalClosedTasks: totalClosedTasks totalClosedTasks: totalClosedTasks
usProgress: usProgress progress: progress
}) })
$el.html(html) $el.html(html)
$el.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList}))
bindOnce $scope, "tasks", (tasks) -> $scope.$watch $attrs.ngModel, (tasks) ->
$scope.$watch $attrs.ngModel, (us) -> render(tasks) if tasks?
if us?
renderUsstatus(us)
$scope.$on "related-tasks:update", -> $scope.$on "$destroy", ->
us = $scope.$eval $attrs.ngModel $el.off()
if us?
# Reload the us because the status could have changed
$ctrl.loadUs()
renderUsstatus(us)
if editable return {
$el.on "click", ".status-data", (event) -> link: link
event.preventDefault() restrict: "EA"
event.stopPropagation() require: "ngModel"
$el.find(".pop-status").popover().open() }
$el.on "click", ".status", (event) -> module.directive("tgUsTasksProgressDisplay", UsTasksProgressDisplayDirective)
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$model.$modelValue.status = target.data("status-id")
renderUsstatus($model.$modelValue)
$.fn.popover().closeAll()
$el.on "click", ".total.clickable", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
updatingSelectedRoleId = target.data("role-id")
target.siblings().removeClass('active')
target.addClass('active')
showSelectPoints(target)
$el.on "click", ".point", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$.fn.popover().closeAll()
$scope.$apply () ->
us = $model.$modelValue
usPoints = _.clone(us.points, true)
usPoints[updatingSelectedRoleId] = target.data("point-id")
us.points = usPoints
us.total_points = calculateTotalPoints(us)
renderUsstatus(us)
return {link:link, require:"ngModel"}
module.directive("tgUsStatusDetail", UsStatusDetailDirective)
############################################################################# #############################################################################
## User story estimation directive ## User story estimation directive
############################################################################# #############################################################################
UsEstimationDirective = ($log) -> UsEstimationDirective = ($rootScope, $repo, $confirm) ->
# Display the points of a US and you can edit it.
#
# Example:
# tg-us-estimation-progress-bar(ng-model="us")
#
# Requirements:
# - Us object (ng-model)
# - scope.project object
# Optionals:
# - save-after-modify (boolean): save object after modify
mainTemplate = _.template(""" mainTemplate = _.template("""
<ul class="points-per-role"> <ul class="points-per-role">
<li class="total"> <li class="total">
@ -394,7 +278,7 @@ UsEstimationDirective = ($log) ->
<span class="role">total</span> <span class="role">total</span>
</li> </li>
<% _.each(roles, function(role) { %> <% _.each(roles, function(role) { %>
<li class="total clickable" data-role-id="<%- role.id %>"> <li class="total <% if(editable){ %>clickable<% } %>" data-role-id="<%- role.id %>">
<span class="points"><%- role.points %></span> <span class="points"><%- role.points %></span>
<span class="role"><%- role.name %></span></li> <span class="role"><%- role.name %></span></li>
<% }); %> <% }); %>
@ -417,7 +301,14 @@ UsEstimationDirective = ($log) ->
</ul> </ul>
""") """)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs, $model) ->
saveAfterModify = $attrs.saveAfterModify or false
isEditable = ->
if $model.$modelValue.id
return $scope.project.my_permissions.indexOf("modify_us") != -1
return $scope.project.my_permissions.indexOf("add_us") != -1
render = (us) -> render = (us) ->
totalPoints = us.total_points or 0 totalPoints = us.total_points or 0
computableRoles = _.filter($scope.project.roles, "computable") computableRoles = _.filter($scope.project.roles, "computable")
@ -430,7 +321,12 @@ UsEstimationDirective = ($log) ->
role.points = if pointObj? and pointObj.name? then pointObj.name else "?" role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
return role return role
html = mainTemplate({totalPoints: totalPoints, roles: roles}) ctx = {
totalPoints: totalPoints
roles: roles
editable: isEditable()
}
html = mainTemplate(ctx)
$el.html(html) $el.html(html)
renderPoints = (target, us, roleId) -> renderPoints = (target, us, roleId) ->
@ -463,19 +359,15 @@ UsEstimationDirective = ($log) ->
return "0" return "0"
return _.reduce(values, (acc, num) -> acc + num) return _.reduce(values, (acc, num) -> acc + num)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us
$scope.$on "$destroy", ->
$el.off()
$el.on "click", ".total.clickable", (event) -> $el.on "click", ".total.clickable", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
roleId = target.data("role-id") roleId = target.data("role-id")
us = $scope.$eval($attrs.ngModel) us = $model.$modelValue
renderPoints(target, us, roleId) renderPoints(target, us, roleId)
target.siblings().removeClass('active') target.siblings().removeClass('active')
@ -484,8 +376,7 @@ UsEstimationDirective = ($log) ->
$el.on "click", ".point", (event) -> $el.on "click", ".point", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
return if not isEditable()
us = $scope.$eval($attrs.ngModel)
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
roleId = target.data("role-id") roleId = target.data("role-id")
@ -493,17 +384,262 @@ UsEstimationDirective = ($log) ->
$el.find(".popover").popover().close() $el.find(".popover").popover().close()
points = _.clone(us.points, true) # NOTE: This block of code is strange and, sometimes, repetitive
# but is the only solution I find to update the object
# corectly
us = angular.copy($model.$modelValue)
points = _.clone($model.$modelValue.points, true)
points[roleId] = pointId points[roleId] = pointId
us.setAttr('points', points) if us.setAttr?
$scope.$apply ->
us.points = points us.points = points
us.total_points = calculateTotalPoints(us) us.total_points = calculateTotalPoints(us)
render(us) $model.$setViewValue(us)
if saveAfterModify
# Edit in the detail page
onSuccess = ->
$confirm.notify("success")
$rootScope.$broadcast("history:reload")
onError = ->
us.revert()
$model.$setViewValue(us)
$confirm.notify("error")
$repo.save($model.$modelValue).then(onSuccess, onError)
else
# Create or eedit in the lightbox
render($model.$modelValue)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us
$scope.$on "$destroy", ->
$el.off()
return { return {
link: link link: link
restrict: "EA" restrict: "EA"
require: "ngModel"
} }
module.directive("tgUsEstimation", UsEstimationDirective) module.directive("tgUsEstimation", ["$rootScope", "$tgRepo", "$tgConfirm", UsEstimationDirective])
#############################################################################
## User story status button directive
#############################################################################
UsStatusButtonDirective = ($rootScope, $repo, $confirm, $loading) ->
# Display the status of a US and you can edit it.
#
# Example:
# tg-us-status-button(ng-model="us")
#
# Requirements:
# - Us object (ng-model)
# - scope.statusById object
# - $scope.project.my_permissions
template = _.template("""
<div class="status-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%= status.color %>"></span>
<span class="status-status"><%= status.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name">status</span>
<ul class="popover pop-status">
<% _.each(statuses, function(st) { %>
<li><a href="" class="status" title="<%- st.name %>"
data-status-id="<%- st.id %>"><%- st.name %></a></li>
<% }); %>
</ul>
</div>
""") #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_us") != -1
render = (us) =>
status = $scope.statusById[us.status]
html = template({
status: status
statuses: $scope.statusList
editable: isEditable()
})
$el.html(html)
$el.on "click", ".status-data", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
$el.find(".pop-status").popover().open()
$el.on "click", ".status", (event) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget)
$.fn.popover().closeAll()
us = $model.$modelValue.clone()
us.status = target.data("status-id")
$model.$setViewValue(us)
$scope.$apply()
onSuccess = ->
$confirm.notify("success")
$rootScope.$broadcast("history:reload")
$loading.finish($el.find(".level-name"))
onError = ->
$confirm.notify("error")
us.revert()
$model.$setViewValue(us)
$loading.finish($el.find(".level-name"))
$loading.start($el.find(".level-name"))
$repo.save($model.$modelValue).then(onSuccess, onError)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgUsStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading",
UsStatusButtonDirective])
#############################################################################
## User story team requirements button directive
#############################################################################
UsTeamRequirementButtonDirective = ($rootscope, $tgrepo, $confirm, $loading) ->
template = _.template("""
<label for="team-requirement"
class="button button-gray team-requirement <% if(canEdit){ %>editable<% }; %> <% if(isRequired){ %>active<% }; %>">
Team requirement
</label>
<input type="checkbox" id="team-requirement" name="team-requirement"/>
""") #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
canEdit = ->
return $scope.project.my_permissions.indexOf("modify_us") != -1
render = (us) ->
if not canEdit() and not us.team_requirement
$el.html("")
return
ctx = {
canEdit: canEdit()
isRequired: us.team_requirement
}
html = template(ctx)
$el.html(html)
$el.on "click", ".team-requirement", (event) ->
return if not canEdit()
us = $model.$modelValue.clone()
us.team_requirement = not us.team_requirement
$model.$setViewValue(us)
$loading.start($el.find("label"))
promise = $tgrepo.save($model.$modelValue)
promise.then =>
$loading.finish($el.find("label"))
$rootscope.$broadcast("history:reload")
promise.then null, ->
$loading.finish($el.find("label"))
$confirm.notify("error")
us.revert()
$model.$setViewValue(us)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgUsTeamRequirementButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", UsTeamRequirementButtonDirective])
#############################################################################
## User story client requirements button directive
#############################################################################
UsClientRequirementButtonDirective = ($rootscope, $tgrepo, $confirm, $loading) ->
template = _.template("""
<label for="client-requirement"
class="button button-gray client-requirement <% if(canEdit){ %>editable<% }; %> <% if(isRequired){ %>active<% }; %>">
Client requirement
</label>
<input type="checkbox" id="client-requirement" name="client-requirement"/>
""") #TODO: i18n
link = ($scope, $el, $attrs, $model) ->
canEdit = ->
return $scope.project.my_permissions.indexOf("modify_us") != -1
render = (us) ->
if not canEdit() and not us.client_requirement
$el.html("")
return
ctx = {
canEdit: canEdit()
isRequired: us.client_requirement
}
html = template(ctx)
$el.html(html)
$el.on "click", ".client-requirement", (event) ->
return if not canEdit()
us = $model.$modelValue.clone()
us.client_requirement = not us.client_requirement
$model.$setViewValue(us)
$loading.start($el.find("label"))
promise = $tgrepo.save($model.$modelValue)
promise.then =>
$loading.finish($el.find("label"))
$rootscope.$broadcast("history:reload")
promise.then null, ->
$loading.finish($el.find("label"))
$confirm.notify("error")
us.revert()
$model.$setViewValue(us)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgUsClientRequirementButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading",
UsClientRequirementButtonDirective])

View File

@ -38,6 +38,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"$scope", "$scope",
"$rootScope", "$rootScope",
"$tgRepo", "$tgRepo",
"$tgModel",
"$tgConfirm", "$tgConfirm",
"$tgResources", "$tgResources",
"$routeParams", "$routeParams",
@ -51,7 +52,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"tgLoader" "tgLoader"
] ]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, constructor: (@scope, @rootscope, @repo, @model, @confirm, @rs, @params, @q, @location,
@filter, @log, @appTitle, @navUrls, @analytics, tgLoader) -> @filter, @log, @appTitle, @navUrls, @analytics, tgLoader) ->
@scope.projectSlug = @params.pslug @scope.projectSlug = @params.pslug
@scope.wikiSlug = @params.slug @scope.wikiSlug = @params.slug
@ -80,7 +81,15 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.wiki = wiki @scope.wiki = wiki
return wiki return wiki
@scope.wiki = {content: ""} if @scope.project.my_permissions.indexOf("add_wiki_page") == -1
return null
data = {
project: @scope.projectId
slug: @scope.wikiSlug
content: ""
}
@scope.wiki = @model.make_model("wiki", data)
return @scope.wiki return @scope.wiki
loadWikiLinks: -> loadWikiLinks: ->
@ -109,28 +118,13 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.wikiId = data.wikipage @scope.wikiId = data.wikipage
return prom.then null, (xhr) => return prom.then null, (xhr) =>
ctx = {project: @params.pslug, slug: @params.slug} @scope.wikiId = null
@location.path(@navUrls.resolve("project-wiki-page-edit", ctx))
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles()) .then(=> @.loadUsersAndRoles())
.then(=> @q.all([@.loadWikiLinks(), .then(=> @q.all([@.loadWikiLinks(),
@.loadWiki()])) @.loadWiki()]))
edit: ->
ctx = {
project: @scope.projectSlug
slug: @scope.wikiSlug
}
@location.path(@navUrls.resolve("project-wiki-page-edit", ctx))
cancel: ->
ctx = {
project: @scope.projectSlug
slug: @scope.wikiSlug
}
@location.path(@navUrls.resolve("project-wiki-page", ctx))
delete: -> delete: ->
# TODO: i18n # TODO: i18n
title = "Delete Wiki Page" title = "Delete Wiki Page"
@ -151,95 +145,181 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
module.controller("WikiDetailController", WikiDetailController) module.controller("WikiDetailController", WikiDetailController)
#############################################################################
## Wiki Edit Controller
#############################################################################
class WikiEditController extends WikiDetailController
save: debounce 2000, ->
onSuccess = =>
ctx = {
project: @scope.projectSlug
slug: @scope.wiki.slug
}
@location.path(@navUrls.resolve("project-wiki-page", ctx))
@confirm.notify("success")
onError = =>
@confirm.notify("error")
if @scope.wiki.id
@repo.save(@scope.wiki).then onSuccess, onError
else
@analytics.trackEvent("wikipage", "create", "create wiki page", 1)
@scope.wiki.project = @scope.projectId
@scope.wiki.slug = @scope.wikiSlug
@repo.create("wiki", @scope.wiki).then onSuccess, onError
module.controller("WikiEditController", WikiEditController)
############################################################################# #############################################################################
## Wiki Main Directive ## Wiki Summary Directive
############################################################################# #############################################################################
WikiDirective = ($tgrepo, $log, $location, $confirm) -> WikiSummaryDirective = ($log) ->
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
return {link:link}
module.directive("tgWikiDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", WikiDirective])
#############################################################################
## Wiki Edit Main Directive
#############################################################################
WikiEditDirective = ($tgrepo, $log, $location, $confirm) ->
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
return {link:link}
module.directive("tgWikiEdit", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", WikiEditDirective])
#############################################################################
## Wiki User Info Directive
#############################################################################
WikiUserInfoDirective = ($log) ->
template = _.template(""" template = _.template("""
<ul>
<li>
<span class="number"><%- totalEditions %></span>
<span class="description">times <br />edited</span>
</li>
<li>
<span class="number"><%- lastModifiedDate %></span>
<span class="description"> last <br />edit</span>
</li>
<li class="username-edition">
<figure class="avatar"> <figure class="avatar">
<img src="<%= imgurl %>" alt="<%- name %>"> <img src="<%= user.imgUrl %>" alt="<%- user.name %>">
</figure> </figure>
<span class="description">last modification</span> <span class="description">last modification</span>
<span class="username"><%- name %></span> <span class="username"><%- user.name %></span>
</li>
</ul>
""") """)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs, $model) ->
if not $attrs.ngModel?
return $log.error "WikiUserDirective: no ng-model attr is defined"
render = (wiki) -> render = (wiki) ->
if not $scope.usersById? if not $scope.usersById?
$log.error "WikiUserDirective requires userById set in scope." $log.error "WikiSummaryDirective requires userById set in scope."
else else
user = $scope.usersById[wiki.last_modifier] user = $scope.usersById[wiki.last_modifier]
if user is undefined
ctx = {name: "unknown", imgurl: "/images/unnamed.png"}
else
ctx = {name: user.full_name_display, imgurl: user.photo}
if user is undefined
user = {name: "unknown", imgUrl: "/images/unnamed.png"}
else
user = {name: user.full_name_display, imgUrl: user.photo}
ctx = {
totalEditions: wiki.editions
lastModifiedDate: moment(wiki.modified_date).format("DD MMM YYYY HH:mm")
user: user
}
html = template(ctx) html = template(ctx)
$el.html(html) $el.html(html)
bindOnce($scope, $attrs.ngModel, render) $scope.$watch $attrs.ngModel, (wikiPage) ->
return if not wikiPage
render(wikiPage)
$scope.$on "$destroy", ->
$el.off()
return { return {
link: link link: link
restrict: "AE" restrict: "EA"
require: "ngModel"
} }
module.directive("tgWikiUserInfo", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", WikiUserInfoDirective]) module.directive("tgWikiSummary", ["$log", WikiSummaryDirective])
#############################################################################
## Editable Wiki Content Directive
#############################################################################
EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $location, $navUrls,
$analytics) ->
template = """
<div class="view-wiki-content">
<section class="wysiwyg"
tg-bind-html="wiki.html"></section>
<span class="edit icon icon-edit" title="Edit"></span>
</div>
<div class="edit-wiki-content" style="display: none;">
<textarea placeholder="Write your wiki page here"
ng-model="wiki.content"
tg-markitup="tg-markitup"></textarea>
<span class="action-container">
<a class="save icon icon-floppy" href="" title="Save" />
<a class="cancel icon icon-delete" href="" title="Cancel" />
</span>
</div>
""" # TODO: i18n
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_wiki_page") != -1
switchToEditMode = ->
$el.find('.edit-wiki-content').show()
$el.find('.view-wiki-content').hide()
$el.find('textarea').focus()
switchToReadMode = ->
$el.find('.edit-wiki-content').hide()
$el.find('.view-wiki-content').show()
disableEdition = ->
$el.find(".view-wiki-content .edit").remove()
$el.find(".edit-wiki-content").remove()
cancelEdition = ->
if $scope.wiki.id
$scope.wiki.revert()
switchToReadMode()
else
ctx = {project: $scope.projectSlug}
$location.path($navUrls.resolve("project-wiki", ctx))
getSelectedText = ->
if $window.getSelection
return $window.getSelection().toString()
else if $document.selection
return $document.selection.createRange().text
return null
$el.on "mouseup", ".view-wiki-content", (event) ->
# We want to dettect the a inside the div so we use the target and
# not the currentTarget
target = angular.element(event.target)
return if not isEditable()
return if target.is('a')
return if getSelectedText()
switchToEditMode()
$el.on "click", ".save", debounce 2000, ->
onSuccess = (wikiPage) ->
if not $scope.wiki.id?
$analytics.trackEvent("wikipage", "create", "create wiki page", 1)
$scope.wiki = wikiPage
$model.setModelValue = $scope.wiki
$confirm.notify("success")
switchToReadMode()
onError = ->
$confirm.notify("error")
$loading.start($el.find('.save-container'))
if $scope.wiki.id?
promise = $repo.save($scope.wiki).then(onSuccess, onError)
else
promise = $repo.create("wiki", $scope.wiki).then(onSuccess, onError)
promise.finally ->
$loading.finish($el.find('.save-container'))
$el.on "click", ".cancel", ->
cancelEdition()
$el.on "keyup", "textarea", ->
if event.keyCode == 27
cancelEdition()
$scope.$watch $attrs.ngModel, (wikiPage) ->
return if not wikiPage
$scope.wiki = wikiPage
if isEditable()
$el.addClass('editable')
if not wikiPage.id?
switchToEditMode()
else
disableEdition()
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
template: template
}
module.directive("tgEditableWikiContent", ["$window", "$document", "$tgRepo", "$tgConfirm", "$tgLoading",
"$tgLocation", "$tgNavUrls", "$tgAnalytics",
EditableWikiContentDirective])

BIN
app/images/markitup/bold.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

After

Width:  |  Height:  |  Size: 370 B

BIN
app/images/markitup/italic.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 B

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 B

After

Width:  |  Height:  |  Size: 365 B

BIN
app/images/markitup/list-bullet.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 321 B

BIN
app/images/markitup/picture.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 B

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 B

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

After

Width:  |  Height:  |  Size: 375 B

BIN
app/images/markitup/stroke.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 B

After

Width:  |  Height:  |  Size: 343 B

View File

@ -1,47 +0,0 @@
extends dummy-layout
block head
title Taiga Your agile, free, and open source project management tool
block content
form.wrapper(tg-issue-detail, ng-controller="IssueDetailController as ctrl",
ng-init="section='issues'")
div.main.us-detail
div.us-detail-header.header-with-actions
include views/components/mainTitle
.action-buttons
a.button.button-green.save-issue(href="", title="Save") Save
a.button.button-red.cancel(tg-nav="project-issues-detail:project=project.slug, ref=issue.ref", href="", title="Cancel") Cancel
section.us-story-main-data
div.us-title(ng-class="{blocked: issue.is_blocked}")
div.us-edit-name-inner
span.us-number(tg-bo-ref="issue.ref")
input(type="text", ng-model="issue.subject", data-required="true", data-maxlength="500")
p.block-desc-container(ng-show="issue.is_blocked")
span.block-description-title Blocked
span.block-description(tg-bind-html="issue.blocked_note || 'This issue is blocked'")
a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock issue") Unblock
div(tg-tag-line, editable="true", ng-model="issue.tags")
section.us-content
textarea(placeholder="Write a description of your issue", ng-model="issue.description", tg-markitup)
tg-attachments(ng-model="issue", type="issue")
tg-history(ng-model="issue", type="issue", mode="edit")
sidebar.menu-secondary.sidebar
section.us-status(tg-issue-status, ng-model="issue", editable="true")
section.us-assigned-to(tg-assigned-to, ng-model="issue", editable="true")
section.watchers(tg-watchers, ng-model="issue", editable="true")
section.us-detail-settings
a.button.button-gray.clickable(title="Click to block the issue", ng-show="!issue.is_blocked", ng-click="ctrl.block()") Block
a.button.button-red(title="Click to delete the issue", tg-check-permission="delete_issue", ng-click="ctrl.delete()", href="") Delete
div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking issue", ng-model="issue")
div.lightbox.lightbox-select-user(tg-lb-assignedto)
div.lightbox.lightbox-select-user(tg-lb-watchers)

View File

@ -4,19 +4,17 @@ block head
title Taiga Your agile, free, and open source project management tool title Taiga Your agile, free, and open source project management tool
block content block content
div.wrapper(tg-issue-detail, ng-controller="IssueDetailController as ctrl", div.wrapper(ng-controller="IssueDetailController as ctrl",
ng-init="section='issues'") ng-init="section='issues'")
div.main.us-detail div.main.us-detail
div.us-detail-header.header-with-actions div.us-detail-header.header-with-actions
include views/components/mainTitle include views/components/mainTitle
.action-buttons
a.button.button-green(tg-check-permission="modify_issue", href="", title="Edit", tg-nav="project-issues-detail-edit:project=project.slug,ref=issue.ref") Edit
section.us-story-main-data section.us-story-main-data
div.us-title(ng-class="{blocked: issue.is_blocked}") div.us-title(ng-class="{blocked: issue.is_blocked}")
h2.us-title-text h2.us-title-text
span.us-number(tg-bo-ref="issue.ref") span.us-number(tg-bo-ref="issue.ref")
span.us-name(ng-bind="issue.subject") span.us-name(tg-editable-subject, ng-model="issue", required-perm="modify_issue")
p.us-related-task(ng-if="issue.generated_user_stories") This issue has been promoted to US: p.us-related-task(ng-if="issue.generated_user_stories") This issue has been promoted to US:
a(ng-repeat="us in issue.generated_user_stories", a(ng-repeat="us in issue.generated_user_stories",
@ -30,20 +28,40 @@ block content
span.block-description(tg-bind-html="issue.blocked_note || 'This issue is blocked'") span.block-description(tg-bind-html="issue.blocked_note || 'This issue is blocked'")
div.issue-nav div.issue-nav
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous issue") a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl",
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next issue") title="previous issue")
a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl",
title="next issue")
div(tg-tag-line, ng-model="issue.tags", ng-show="issue.tags") div.tags-block(tg-tag-line, ng-model="issue", required-perm="modify_issue")
section.us-content.wysiwyg(tg-bind-html="issue.description_html") section.duty-content.wysiwyg(tg-editable-description, ng-model="issue", required-perm="modify_issue")
tg-attachments(ng-model="issue", type="issue") tg-attachments(ng-model="issue", type="issue")
tg-history(ng-model="issue", type="issue") tg-history(ng-model="issue", type="issue")
sidebar.menu-secondary.sidebar sidebar.menu-secondary.sidebar
section.us-status(tg-issue-status, ng-model="issue") section.us-status
section.us-assigned-to(tg-assigned-to, ng-model="issue") h1(tg-issue-status-display, ng-model="issue")
section.watchers(tg-watchers, ng-model="issue") tg-created-by-display.us-created-by(ng-model="issue")
div.duty-data-container
div.duty-data(tg-issue-type-button, ng-model="issue")
div.duty-data(tg-issue-severity-button, ng-model="issue")
div.duty-data(tg-issue-priority-button, ng-model="issue")
div.duty-data(tg-issue-status-button, ng-model="issue")
section.duty-assigned-to(tg-assigned-to, ng-model="issue", required-perm="modify_issue")
section.watchers(tg-watchers, ng-model="issue", required-perm="modify_issue")
section.us-detail-settings section.us-detail-settings
tg-promote-issue-to-us-button(ng-model="issue") tg-promote-issue-to-us-button(tg-check-permission="add_us", ng-model="issue")
tg-block-button(tg-check-permission="modify_issue", ng-model="issue")
tg-delete-button(tg-check-permission="delete_issue",
on-delete-title="'Delete issue'",
on-delete-go-to-url="onDeleteGoToUrl",
ng-model="issue")
div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking issue", ng-model="issue")
div.lightbox.lightbox-select-user(tg-lb-assignedto)
div.lightbox.lightbox-select-user(tg-lb-watchers)

View File

@ -1,49 +0,0 @@
extends dummy-layout
block head
title Taiga Your agile, free, and open source project management tool
block content
form.wrapper(tg-task-detail, ng-controller="TaskDetailController as ctrl",
ng-init="section='backlog'")
div.main.us-detail
div.us-detail-header.header-with-actions
include views/components/mainTitle
.action-buttons
a.button.button-green.save-task(href="", title="Save") Save
a.button.button-red.cancel(tg-nav="project-tasks-detail:project=project.slug,ref=task.ref", href="", title="Cancel") Cancel
section.us-story-main-data
div.us-title(ng-class="{blocked: task.is_blocked}")
div.us-edit-name-inner
span.us-number(tg-bo-ref="task.ref")
input(type="text", ng-model="task.subject", data-required="true", data-maxlength="500")
p.block-desc-container(ng-show="task.is_blocked")
span.block-description-title Blocked
span.block-description(tg-bind-html="task.blocked_note || 'This task is blocked'")
a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock task") Unblock
div(tg-tag-line, editable="true", ng-model="task.tags")
section.us-content
textarea(placeholder="Write a description of your task", ng-model="task.description", tg-markitup)
tg-attachments(ng-model="task", type="task")
tg-history(ng-model="task", type="task", mode="edit")
sidebar.menu-secondary.sidebar
section.us-status(tg-task-status, ng-model="task", editable="true")
section.us-assigned-to(tg-assigned-to, ng-model="task", editable="true")
section.watchers(tg-watchers, ng-model="task", editable="true")
section.us-detail-settings
fieldset(title="Feeling a bit overwhelmed by a task? Make sure others know about it by clicking on Iocaine when editing a task. It's possible to become immune to this (fictional) deadly poison by consuming small amounts over time just as it's possible to get better at what you do by occasionally taking on extra challenges!")
label.clickable.button.button-gray(for="is-iocaine", ng-class="{'active': task.is_iocaine}") Iocaine
input(ng-model="task.is_iocaine", type="checkbox", id="is-iocaine", name="is-iocaine")
a.button.button-gray.clickable(ng-show="!task.is_blocked", ng-click="ctrl.block()") Block
a.button.button-red(tg-check-permission="delete_task", ng-click="ctrl.delete()", href="") Delete
div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking task", ng-model="task")
div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto)
div.lightbox.lightbox-select-user.hidden(tg-lb-watchers)

View File

@ -4,7 +4,7 @@ block head
title Taiga Your agile, free, and open source project management tool title Taiga Your agile, free, and open source project management tool
block content block content
div.wrapper(tg-task-detail, ng-controller="TaskDetailController as ctrl", div.wrapper(ng-controller="TaskDetailController as ctrl",
ng-init="section='backlog'") ng-init="section='backlog'")
div.main.us-detail div.main.us-detail
div.us-detail-header.header-with-actions div.us-detail-header.header-with-actions
@ -15,16 +15,12 @@ block content
href="", title="Go to taskboard", href="", title="Go to taskboard",
tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug",
ng-if="sprint && project.is_backlog_activated") Taskboard ng-if="sprint && project.is_backlog_activated") Taskboard
a.button.button-green(
tg-check-permission="modify_task", href="",
title="Edit",
tg-nav="project-tasks-detail-edit:project=project.slug,ref=task.ref") Edit
section.us-story-main-data section.us-story-main-data
div.us-title(ng-class="{blocked: task.is_blocked}") div.us-title(ng-class="{blocked: task.is_blocked}")
h2.us-title-text h2.us-title-text
span.us-number(tg-bo-ref="task.ref") span.us-number(tg-bo-ref="task.ref")
span.us-name(ng-bind="task.subject") span.us-name(tg-editable-subject, ng-model="task", required-perm="modify_task")
h3.us-related-task This task belongs to h3.us-related-task This task belongs to
a(tg-check-permission="view_us", href="", title="Go to user story", a(tg-check-permission="view_us", href="", title="Go to user story",
tg-nav="project-userstories-detail:project=project.slug, ref=us.ref", tg-nav="project-userstories-detail:project=project.slug, ref=us.ref",
@ -35,20 +31,37 @@ block content
span.block-description-title Blocked span.block-description-title Blocked
span.block-description(tg-bind-html="task.blocked_note || 'This task is blocked'") span.block-description(tg-bind-html="task.blocked_note || 'This task is blocked'")
div.issue-nav div.issue-nav
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous task") a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl",
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next task") title="previous task")
a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl",
title="next task")
div(tg-tag-line, ng-model="task.tags", ng-show="task.tags") div.tags-block(tg-tag-line, ng-model="task", required-perm="modify_task")
section.us-content.wysiwyg(tg-bind-html="task.description_html") section.duty-content.wysiwyg(tg-editable-description, ng-model="task", required-perm="modify_task")
tg-attachments(ng-model="task", type="task") tg-attachments(ng-model="task", type="task")
tg-history(ng-model="task", type="task") tg-history(ng-model="task", type="task")
sidebar.menu-secondary.sidebar sidebar.menu-secondary.sidebar
section.us-status(tg-task-status, ng-model="task") section.us-status
section.us-assigned-to(tg-assigned-to, ng-model="task") h1(tg-task-status-display, ng-model="task")
section.watchers(tg-watchers, ng-model="task") div.us-created-by(tg-created-by-display, ng-model="task")
div.duty-data-container
div.duty-data(tg-task-status-button, ng-model="task")
section.duty-assigned-to(tg-assigned-to, ng-model="task", required-perm="modify_task")
section.watchers(tg-watchers, ng-model="task", required-perm="modify_task")
section.us-detail-settings section.us-detail-settings
span.button.button-gray(href="", ng-class="{'active': task.is_iocaine }", title="Feeling a bit overwhelmed by a task? Make sure others know about it by clicking on Iocaine when editing a task. It's possible to become immune to this (fictional) deadly poison by consuming small amounts over time just as it's possible to get better at what you do by occasionally taking on extra challenges!") Iocaine tg-task-is-iocaine-button(ng-model="task")
tg-block-button(tg-check-permission="modify_task", ng-model="task")
tg-delete-button(tg-check-permission="delete_task",
on-delete-title="'Delete Task'",
on-delete-go-to-url="onDeleteGoToUrl",
ng-model="task")
div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking task", ng-model="task")
div.lightbox.lightbox-select-user(tg-lb-assignedto)
div.lightbox.lightbox-select-user(tg-lb-watchers)

View File

@ -1,52 +0,0 @@
extends dummy-layout
block head
title Taiga Your agile, free, and open source project management tool
block content
form.wrapper(tg-us-detail, ng-controller="UserStoryDetailController as ctrl",
ng-init="section='backlog'")
div.main.us-detail
div.us-detail-header.header-with-actions
include views/components/mainTitle
.action-buttons
a.button.button-green.save-us(href="", title="Save") Save
a.button.button-red.cancel(tg-nav="project-userstories-detail:project=project.slug,ref=us.ref", href="", title="Cancel") Cancel
section.us-story-main-data
div.us-title(ng-class="{blocked: us.is_blocked}")
div.us-edit-name-inner
span.us-number(tg-bo-ref="us.ref")
input(type="text", ng-model="us.subject", data-required="true", data-maxlength="500")
p.block-desc-container(ng-show="us.is_blocked")
span.block-description-title Blocked
span.block-description(tg-bind-html="us.blocked_note || 'This US is blocked'")
a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock US") Unblock
div(tg-tag-line, editable="true", ng-model="us.tags")
section.us-content
textarea(placeholder="Write a description of your user story", ng-model="us.description", tg-markitup)
tg-attachments(ng-model="us", type="us")
tg-history(ng-model="us", type="us", mode="edit")
sidebar.menu-secondary.sidebar
section.us-status(tg-us-status-detail, ng-model="us", editable="true")
section.us-assigned-to(tg-assigned-to, ng-model="us", editable="true")
section.watchers(tg-watchers, ng-model="us", editable="true")
section.us-detail-settings
fieldset
label.clickable.button.button-gray(for="client-requirement", ng-class="{'active': us.client_requirement}") Client requirement
input(ng-model="us.client_requirement", type="checkbox", id="client-requirement", name="client-requirement")
fieldset
label.clickable.button.button-gray(for="team-requirement", ng-class="{'active': us.team_requirement}") Team requirement
input(ng-model="us.team_requirement", type="checkbox", id="team-requirement", name="team-requirement")
a.button.button-gray.clickable(ng-show="!us.is_blocked", ng-click="ctrl.block()") Block
a.button.button-red(tg-check-permission="delete_us", ng-click="ctrl.delete()", href="") Delete
div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking issue", ng-model="us")
div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto)
div.lightbox.lightbox-select-user.hidden(tg-lb-watchers)

View File

@ -4,7 +4,7 @@ block head
title Taiga Your agile, free, and open source project management tool title Taiga Your agile, free, and open source project management tool
block content block content
div.wrapper(tg-us-detail, ng-controller="UserStoryDetailController as ctrl", div.wrapper(ng-controller="UserStoryDetailController as ctrl",
ng-init="section='backlog'") ng-init="section='backlog'")
div.main.us-detail div.main.us-detail
div.us-detail-header.header-with-actions div.us-detail-header.header-with-actions
@ -15,16 +15,12 @@ block content
href="", title="Go to taskboard", href="", title="Go to taskboard",
tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug",
ng-if="sprint && project.is_backlog_activated") Taskboard ng-if="sprint && project.is_backlog_activated") Taskboard
a.button.button-green(
tg-check-permission="modify_us", href="",
title="Edit",
tg-nav="project-userstories-detail-edit:project=project.slug,ref=us.ref") Edit
section.us-story-main-data section.us-story-main-data
div.us-title(ng-class="{blocked: us.is_blocked}") div.us-title(ng-class="{blocked: us.is_blocked}")
h2.us-title-text h2.us-title-text
span.us-number(tg-bo-ref="us.ref") span.us-number(tg-bo-ref="us.ref")
span.us-name(ng-bind="us.subject") span.us-name(tg-editable-subject, ng-model="us", required-perm="modify_us")
p.us-related-task(ng-if="us.origin_issue") This US has been promoted from Issue p.us-related-task(ng-if="us.origin_issue") This US has been promoted from Issue
a(tg-check-permission="view_us", href="", title="Go to issue", a(tg-check-permission="view_us", href="", title="Go to issue",
@ -36,13 +32,14 @@ block content
span.block-description-title Blocked span.block-description-title Blocked
span.block-description(tg-bind-html="us.blocked_note || 'This user story is blocked'") span.block-description(tg-bind-html="us.blocked_note || 'This user story is blocked'")
div.issue-nav div.issue-nav
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl",
title="previous user story") title="previous user story")
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next user story") a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl",
title="next user story")
div(tg-tag-line, ng-model="us.tags", ng-show="us.tags") div.tags-block(tg-tag-line, ng-model="us", required-perm="modify_us")
section.us-content.wysiwyg(tg-bind-html="us.description_html") section.duty-content.wysiwyg(tg-editable-description, ng-model="us", required-perm="modify_us")
include views/modules/related-tasks include views/modules/related-tasks
@ -50,15 +47,27 @@ block content
tg-history(ng-model="us", type="us") tg-history(ng-model="us", type="us")
sidebar.menu-secondary.sidebar sidebar.menu-secondary.sidebar
section.us-status(tg-us-status-detail, ng-model="us") section.us-status
section.us-assigned-to(tg-assigned-to, ng-model="us") h1(tg-us-status-display, ng-model="us")
section.us-created-by(tg-created-by, ng-model="us") div.us-detail-progress-bar(tg-us-tasks-progress-display, ng-model="tasks")
section.watchers(tg-watchers, ng-model="us") tg-created-by-display.us-created-by(ng-model="us")
tg-us-estimation(ng-model="us", save-after-modify="true")
div.duty-data-container
div.duty-data(tg-us-status-button, ng-model="us")
section.duty-assigned-to(tg-assigned-to, ng-model="us", required-perm="modify_us")
section.watchers(tg-watchers, ng-model="us", required-perm="modify_us")
section.us-detail-settings section.us-detail-settings
span.button.button-gray(href="", title="Client requirement", tg-us-team-requirement-button(ng-model="us")
ng-class="{'active': us.client_requirement}") Client requirement tg-us-client-requirement-button(ng-model="us")
span.button.button-gray(href="", title="Team requirement", tg-block-button(tg-check-permission="modify_us", ng-model="us")
ng-class="{'active': us.team_requirement}") Team requirement tg-delete-button(tg-check-permission="delete_us",
on-delete-title="'Delete User Story'",
on-delete-go-to-url="onDeleteGoToUrl",
ng-model="us")
div.lightbox.lightbox-block.hidden(tg-lb-block, title="Blocking us", ng-model="us")
div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto) div.lightbox.lightbox-select-user.hidden(tg-lb-assignedto)
div.lightbox.lightbox-select-user.hidden(tg-lb-watchers)

View File

@ -14,7 +14,7 @@ form
select.severity(ng-model="issue.severity", ng-options="s.id as s.name for s in severityList") select.severity(ng-model="issue.severity", ng-options="s.id as s.name for s in severityList")
fieldset fieldset
div(tg-tag-line, editable="true", ng-model="issue.tags") div.tags-block(tg-lb-tag-line, ng-model="issue.tags")
fieldset fieldset
textarea.description(placeholder="Description", ng-model="issue.description") textarea.description(placeholder="Description", ng-model="issue.description")

View File

@ -16,7 +16,7 @@ form
option(value="") Unassigned option(value="") Unassigned
fieldset fieldset
div(tg-tag-line, editable="true", ng-model="task.tags") div.tags-block(tg-lb-tag-line, ng-model="task.tags")
fieldset fieldset
textarea.description(placeholder="Type a short description", ng-model="task.description") textarea.description(placeholder="Type a short description", ng-model="task.description")

View File

@ -8,14 +8,13 @@ form
fieldset.estimation fieldset.estimation
tg-us-estimation(ng-model="us") tg-us-estimation(ng-model="us")
//- Render by tg-lb-create-edit-userstory
fieldset fieldset
select(name="status", ng-model="us.status", ng-options="s.id as s.name for s in usStatusList", select(name="status", ng-model="us.status", ng-options="s.id as s.name for s in usStatusList",
tr="placeholder:common.status") tr="placeholder:common.status")
fieldset fieldset
div(tg-tag-line, editable="true", ng-model="us.tags") div.tags-block(tg-lb-tag-line, ng-model="us.tags")
fieldset fieldset
textarea.description(name="description", ng-model="us.description", textarea.description(name="description", ng-model="us.description",

View File

@ -1,9 +0,0 @@
div.summary.wiki-summary
ul
li
span.number(tg-bo-bind="wiki.editions")
span.description times <br />edited
li(ng-if="wiki.modified_date")
span.number(tg-bo-bind="wiki.modified_date|momentFormat:'DD MMM YYYY HH:mm'")
span.description last <br />edit
li.username-edition(tg-wiki-user-info, ng-model='wiki')

View File

@ -1,24 +0,0 @@
extends dummy-layout
block head
title Taiga Your agile, free, and open source project management tool
block content
div.wrapper(tg-wiki-edit, ng-controller="WikiEditController as ctrl",
ng-init="section='wiki'")
sidebar.menu-secondary.extrabar(tg-check-permission="view_wiki_links")
section.wiki-nav(tg-wiki-nav, ng-model="wikiLinks")
section.main.wiki
div.header-with-actions
h1
span(tg-bo-bind="project.name", class="project-name-short")
span.green Wiki
span.wiki-title(tg-bo-bind='wikiSlug|unslugify')
.action-buttons
a.button.button-green.save-wiki(href="", title="Save", ng-click="ctrl.save()") Save
a.button.button-red.cancel-wiki(href="", title="CAncel", ng-click="ctrl.cancel()") Cancel
section.wysiwyg
textarea(placeholder="Write a your wiki page", ng-model="wiki.content", tg-markitup)
tg-attachments(ng-model="wiki", type="wiki_page", ng-if="wiki.id")

View File

@ -4,25 +4,22 @@ block head
title Taiga Your agile, free, and open source project management tool title Taiga Your agile, free, and open source project management tool
block content block content
div.wrapper(tg-wiki-detail, ng-controller="WikiDetailController as ctrl", div.wrapper(ng-controller="WikiDetailController as ctrl",
ng-init="section='wiki'") ng-init="section='wiki'")
sidebar.menu-secondary.extrabar(tg-check-permission="view_wiki_links") sidebar.menu-secondary.extrabar(tg-check-permission="view_wiki_links")
section.wiki-nav(tg-wiki-nav, ng-model="wikiLinks") section.wiki-nav(tg-wiki-nav, ng-model="wikiLinks")
section.main.wiki section.main.wiki
.header-with-actions .header
h1 h1
span(tg-bo-bind="project.name", class="project-name-short") span(tg-bo-bind="project.name")
span.green Wiki span.green Wiki
span.wiki-title(tg-bo-bind='wiki.slug|unslugify') span.wiki-title(tg-bo-bind='wikiSlug|unslugify')
.action-buttons
a.button.button-red.delete-wiki(tg-check-permission="delete_wiki_page",
href="", title="Delete", ng-click="ctrl.delete()") Delete
a.button.button-green.edit-wiki(tg-check-permission="modify_wiki_page", div.summary.wiki-summary(tg-wiki-summary, ng-model="wiki", ng-if="wiki.id")
href="", title="Edit", ng-click="ctrl.edit()") Edit section.wiki-content(tg-editable-wiki-content, ng-model="wiki")
include views/modules/wiki-summary tg-attachments(ng-model="wiki", type="wiki_page", ng-if="wiki.id")
section.wiki-content.wysiwyg(tg-bind-html="wiki.html") a.remove(href="", ng-click="ctrl.delete()", ng-if="wiki.id", title="Remove this wiki page")
span.icon.icon-delete
tg-attachments(ng-model="wiki", type="wiki_page") span Remove this wiki page

View File

@ -0,0 +1,38 @@
.markItUpHeader {
ul {
background: $whitish;
padding: .3rem;
li {
display: inline-block;
float: none;
a {
opacity: .8;
&:hover {
@include transition(opacity .2s linear);
opacity: .3;
}
}
}
.preview-icon {
position: absolute;
right: 2.5rem;
}
}
}
.markItUpContainer {
padding: 0;
}
.markdown {
position: relative;
}
.preview {
.actions {
background: $whitish;
margin-top: .5rem;
min-height: 2rem;
padding: .3rem;
}
}

View File

@ -30,10 +30,8 @@
.tags-block { .tags-block {
.tags-container { .tags-container {
display: inline-block; display: inline-block;
vertical-align: middle;
} }
input { input {
display: inline-block;
padding: .4rem; padding: .4rem;
width: 14rem; width: 14rem;
} }
@ -42,7 +40,18 @@
margin: 0 .5rem .5rem 0; margin: 0 .5rem .5rem 0;
padding: .5rem; padding: .5rem;
} }
.save { .add-tag {
display: none; color: $gray-light;
&:hover {
color: $fresh-taiga;
}
}
.icon-plus {
@extend %large;
}
.add-tag-text {
@extend %small;
} }
} }

View File

@ -1,5 +1,5 @@
.watchers { .watchers {
margin-top: 2rem; margin-top: 1rem;
.watchers-header { .watchers-header {
border-bottom: 2px solid $gray-light; border-bottom: 2px solid $gray-light;
padding: .5rem; padding: .5rem;

View File

@ -39,6 +39,10 @@ sup {
vertical-align: middle; vertical-align: middle;
} }
.icon-spinner {
@include animation (spin 1s linear infinite);
}
.clickable { .clickable {
cursor: pointer; cursor: pointer;
} }

View File

@ -57,6 +57,12 @@
display: flex; display: flex;
margin-bottom: 0; margin-bottom: 0;
max-width: 94%; max-width: 94%;
&:hover {
.icon-edit {
@include transition(opacity .3s linear);
opacity: 1;
}
}
} }
.us-number { .us-number {
@extend %xlarge; @extend %xlarge;
@ -72,6 +78,17 @@
display: inline-block; display: inline-block;
line-height: 2.2rem; line-height: 2.2rem;
padding-right: 1rem; padding-right: 1rem;
width: 100%;
}
.icon-edit,
.icon-floppy,
.icon-spinner {
@extend %large;
color: $gray-light;
margin-left: .5rem;
}
.icon-edit {
opacity: 0;
} }
.us-related-task { .us-related-task {
@extend %small; @extend %small;
@ -133,12 +150,74 @@
} }
} }
.us-content { .duty-content {
position: relative;
&:hover {
.view-description {
.edit {
@include transition(all .2s linear);
opacity: 1;
top: -1.5rem;
}
.editable {
background: $whitish;
cursor: pointer;
.no-description {
color: $grayer;
}
}
}
}
&.wysiwyg {
overflow: visible;
}
.no-description {
color: $gray-light;
}
textarea { textarea {
background: $white; background: $white;
height: 10rem; height: 10rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.save-container {
position: absolute;
right: 1rem;
top: .2rem;
.save {
color: $blackish;
opacity: .6;
top: 0;
}
&:hover {
@include transition(opacity .2s linear);
opacity: .3;
}
}
.edit {
color: $grayer;
}
.view-description {
.edit {
@include transition(all .2s linear);
background: $whitish;
left: 0;
opacity: 0;
padding: .2rem .5rem;
position: absolute;
top: 0;
}
}
.edit-description {
.save {
top: .4rem;
}
.edit {
@include transition(all .2s linear);
position: absolute;
right: 2.5rem;
top: .4rem;
}
}
} }
.comment-list { .comment-list {
@ -230,18 +309,19 @@
} }
} }
.issue-data { .duty-data-container {
@extend %small; @extend %small;
div { margin-bottom: 1rem;
@include clearfix(); .duty-data {
@include transition(background .2s ease-in);
background: darken($whitish, 5%);
margin-bottom: .5rem; margin-bottom: .5rem;
padding: .5rem;
padding-right: 1rem;
&:last-child { &:last-child {
margin: 0; margin: 0;
} }
div {
@include transition(background .2s ease-in);
background: darken($whitish, 5%);
padding: .5rem;
padding-right: 1rem;
} }
.clickable { .clickable {
&:hover { &:hover {
@ -249,6 +329,7 @@
background: darken($whitish, 10%); background: darken($whitish, 10%);
} }
} }
}
.level { .level {
display: inline-block; display: inline-block;
margin-right: .5rem; margin-right: .5rem;
@ -257,6 +338,10 @@
.level-name { .level-name {
color: darken($whitish, 20%); color: darken($whitish, 20%);
float: right; float: right;
&.loading span {
@include animation (loading .5s linear);
@include animation (spin 1s linear infinite);
}
} }
} }
@ -271,9 +356,25 @@
} }
.button-gray { .button-gray {
background: $gray-light; background: $gray-light;
&:hover, &:hover {
&.active { background: $gray-light;
}
&.editable {
&:hover {
background: $grayer; background: $grayer;
cursor: pointer;
}
}
&.active {
background: $green-taiga;
}
}
.item-block {
&.editable {
&:hover {
background: $red;
cursor: pointer;
}
} }
} }
.button-red { .button-red {
@ -283,7 +384,9 @@
} }
} }
label { label {
&.editable {
cursor: pointer; cursor: pointer;
}
+input { +input {
display: none; display: none;
} }

View File

@ -1,3 +1,70 @@
.wiki {
.remove {
@extend %small;
color: $gray-light;
&:hover {
span {
@include transition(color .2s linear);
color: $grayer;
}
.icon {
@include transition(color .2s linear);
color: $red;
}
}
.icon {
color: $gray-light;
margin-right: .3rem;
}
}
}
.wiki-content { .wiki-content {
margin-bottom: 2rem; margin-bottom: 2rem;
position: relative;
.view-wiki-content {
&:hover {
.wysiwyg {
background: $whitish;
cursor: pointer;
}
.edit {
@include transition(all .2s linear);
opacity: 1;
top: -1.5rem;
}
}
.edit {
@include transition(all .2s linear);
background: $whitish;
left: 0;
opacity: 0;
padding: .2rem .5rem;
position: absolute;
top: 0;
}
}
.edit-wiki-content {
.icon {
&:hover {
@include transition(all .2s linear);
color: $grayer;
opacity: .3;
}
}
.preview-icon {
position: absolute;
right: 3.5rem;
}
.action-container {
position: absolute;
right: 1rem;
top: .3rem;
}
.edit {
position: absolute;
right: 3.5rem;
top: .4rem;
}
}
} }

View File

@ -1,7 +1,26 @@
.us-assigned-to { .duty-assigned-to {
@include table-flex(); @include table-flex();
margin-top: 1rem; margin-top: 1rem;
position: relative; position: relative;
&:hover {
.assigned-to {
.icon-delete {
@include transition (opacity .3s linear);
opacity: 1;
}
}
}
&.loading {
width: 100%;
span {
font-size: 30px;
padding: 20px 0;
text-align: center;
width: 100%;
@include animation (loading .5s linear);
@include animation (spin 1s linear infinite);
}
}
.user-avatar { .user-avatar {
@include table-flex-child(1, 0); @include table-flex-child(1, 0);
img { img {
@ -22,13 +41,21 @@
@extend %large; @extend %large;
color: $green-taiga; color: $green-taiga;
cursor: default; cursor: default;
line-height: 1.5rem;
&.editable { &.editable {
cursor: pointer; cursor: pointer;
} }
.icon {
vertical-align: top;
}
}
.assigned-name {
@include ellipsis(80%);
display: inline-block;
} }
.icon-delete { .icon-delete {
color: $gray-light; color: $gray-light;
opacity: 1; opacity: 0;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;

View File

@ -98,7 +98,7 @@
} }
.icon-edit, .icon-edit,
.icon-floppy { .icon-floppy {
right: 4rem; right: 3.5rem;
} }
.icon-delete { .icon-delete {
right: 2rem; right: 2rem;

View File

@ -1,6 +1,5 @@
.history { .history {
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 0 1rem;
} }
.changes-title { .changes-title {
display: block; display: block;
@ -66,7 +65,22 @@
} }
.add-comment { .add-comment {
@include clearfix; @include clearfix;
&.active {
.button-green {
display: block;
}
textarea { textarea {
@include transition(height .3s ease-in);
height: 6rem;
}
.preview-icon {
opacity: 1;
position: absolute;
right: 1rem;
}
}
textarea {
background: $white;
float: left; float: left;
height: 41px; height: 41px;
margin-bottom: .5rem; margin-bottom: .5rem;
@ -79,14 +93,13 @@
.button-green { .button-green {
display: none; display: none;
} }
&.active { .edit,
.button-green { .preview-icon {
display: block; position: absolute;
} right: 1rem;
textarea {
@include transition(height .3s ease-in);
height: 6rem;
} }
.preview-icon {
opacity: 0;
} }
} }
a.show-more-comments { a.show-more-comments {

View File

@ -61,6 +61,17 @@
.status { .status {
position: relative; position: relative;
text-align: left; text-align: left;
&:hover {
.icon {
@include transition (opacity .2s ease-in);
opacity: 1;
}
}
.not-clickable {
&:hover {
color: $grayer;
}
}
.popover { .popover {
a { a {
text-align: left; text-align: left;
@ -73,6 +84,7 @@
.icon { .icon {
color: $gray-light; color: $gray-light;
margin-left: .2rem; margin-left: .2rem;
opacity: 0;
} }
} }
.pop-status { .pop-status {
@ -160,8 +172,10 @@
text-align: left; text-align: left;
} }
.task-assignedto { .task-assignedto {
cursor: pointer;
position: relative; position: relative;
&.editable {
cursor: pointer;
}
&:hover { &:hover {
.icon { .icon {
@include transition(opacity .3s linear); @include transition(opacity .3s linear);

View File

@ -53,6 +53,8 @@ exports.files = function () {
'components/spinner', 'components/spinner',
'components/help-notion-button', 'components/help-notion-button',
'components/beta', 'components/beta',
'components/markitup',
//################################################# //#################################################
// Modules // Modules