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)
### Features
- US/Task/Issue visualization and edition refactor. Now only one view for both.
- Multiple User stories Drag & Drop in the backlog.
- Add visual difference to closed USs in backlog panel.
- 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
$routeProvider.when("/project/:pslug/us/:usref",
{templateUrl: "/partials/us-detail.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/us/:usref/edit",
{templateUrl: "/partials/us-detail-edit.html"})
# Tasks
$routeProvider.when("/project/:pslug/task/:taskref",
{templateUrl: "/partials/task-detail.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/task/:taskref/edit",
{templateUrl: "/partials/task-detail-edit.html"})
# Wiki
$routeProvider.when("/project/:pslug/wiki",
{redirectTo: (params) -> "/project/#{params.pslug}/wiki/home"}, )
$routeProvider.when("/project/:pslug/wiki/:slug",
{templateUrl: "/partials/wiki.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/wiki/:slug/edit",
{templateUrl: "/partials/wiki-edit.html"})
# Issues
$routeProvider.when("/project/:pslug/issues",
{templateUrl: "/partials/issues.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/issue/:issueref",
{templateUrl: "/partials/issues-detail.html"})
$routeProvider.when("/project/:pslug/issue/:issueref/edit",
{templateUrl: "/partials/issues-detail-edit.html"})
{templateUrl: "/partials/issues-detail.html", resolve: {loader: tgLoaderProvider.add()}})
# Admin
$routeProvider.when("/project/:pslug/admin/project-profile/details",

View File

@ -67,17 +67,13 @@ urls = {
"project-search": "/project/:project/search"
"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-edit": "/project/:project/task/:ref/edit"
"project-issues-detail": "/project/:project/issue/:ref"
"project-issues-detail-edit": "/project/:project/issue/:ref/edit"
"project-wiki": "/project/:project/wiki",
"project-wiki-page": "/project/:project/wiki/:slug",
"project-wiki-page-edit": "/project/:project/wiki/:slug/edit",
# Admin
"project-admin-home": "/project/:project/admin/project-profile/details"

View File

@ -24,6 +24,7 @@ bindOnce = @.taiga.bindOnce
module = angular.module("taigaCommon")
#############################################################################
## Date Range Directive (used mainly for sprint date range)
#############################################################################
@ -46,6 +47,33 @@ 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
#############################################################################
@ -76,92 +104,126 @@ module.directive("tgSprintProgressbar", SprintProgressBarDirective)
#############################################################################
## Date Selector Directive (using pikaday)
## Created-by display directive
#############################################################################
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?
})
CreatedByDisplayDirective = ->
# Display the owner information (full name and photo) and the date of
# creation of an object (like USs, tasks and issues).
#
# Example:
# div.us-created-by(tg-created-by-display, ng-model="us")
#
# Requirements:
# - model object must have the attributes 'created_date' and
# 'owner'(ng-model)
# - scope.usersById object is required.
$scope.$watch $attrs.ngModel, (val) ->
$el.picker.setDate(val) if val?
template = _.template("""
<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 {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgDateSelector", DateSelectorDirective)
module.directive("tgCreatedByDisplay", CreatedByDisplayDirective)
#############################################################################
## 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
template = _.template("""
<% if(isEditable){ %>
<div class="watchers-header">
<span class="title">watchers</span>
<% if (editable) { %>
<a href="" title="Add watcher" class="icon icon-plus add-watcher"></a>
<% } %>
</div>
<% } else if(watchers.length > 0){ %>
<div class="watchers-header">
<span class="title">watchers</span>
</div>
<% }; %>
<% _.each(watchers, function(watcher) { %>
<div class="watcher-single">
<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 %>">
</a>
</span>
</div>
<div class="watcher-name">
<span>
<%- watcher.full_name_display %>
</span>
<span><%- watcher.full_name_display %></span>
<% if (editable) { %>
<% if(isEditable){ %>
<a class="icon icon-delete"
data-watcher-id="<%= watcher.id %>" href="" title="delete-watcher">
</a>
<% } %>
<% }; %>
</div>
</div>
<% }); %>
""")
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) ->
html = template({watchers: watchers, editable:editable})
ctx = {
watchers: watchers
isEditable: isEditable()
}
html = template(ctx)
$el.html(html)
if watchers.length == 0
if editable
$el.find(".title").text("Add 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()
if isEditable() and watchers.length == 0
$el.find(".title").text("Add watchers")
$el.find(".watchers-header").addClass("no-watchers")
$el.on "click", ".icon-delete", (event) ->
event.preventDefault()
return if not isEditable()
target = angular.element(event.currentTarget)
watcherId = target.data("watcher-id")
@ -176,9 +238,11 @@ WatchersDirective = ($rootscope, $confirm) ->
item = $model.$modelValue.clone()
item.watchers = watcherIds
$model.$setViewValue(item)
save(item)
$el.on "click", ".add-watcher", (event) ->
event.preventDefault()
return if not isEditable()
$scope.$apply ->
$rootscope.$broadcast("watcher:add", $model.$modelValue)
@ -189,19 +253,30 @@ WatchersDirective = ($rootscope, $confirm) ->
item = $model.$modelValue.clone()
item.watchers = watchers
$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"}
module.directive("tgWatchers", ["$rootScope", "$tgConfirm", WatchersDirective])
module.directive("tgWatchers", ["$rootScope", "$tgConfirm", "$tgRepo", WatchersDirective])
#############################################################################
## 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
template = _.template("""
<% if (assignedTo) { %>
@ -213,62 +288,360 @@ AssignedToDirective = ($rootscope, $confirm) ->
<div class="assigned-to">
<span class="assigned-title">Assigned to</span>
<a href="" title="edit assignment" class="user-assigned <% if (editable) { %> editable <% } %>">
<% if (assignedTo) { %>
<%- assignedTo.full_name_display %>
<% } else { %>
Not assigned
<% } %>
<% if (editable) { %>
<span class="icon icon-arrow-bottom"></span>
<% } %>
<a href="" title="edit assignment" class="user-assigned <% if(isEditable){ %>editable<% }; %>">
<span class="assigned-name">
<% if (assignedTo) { %>
<%- assignedTo.full_name_display %>
<% } else { %>
Not assigned
<% } %>
</span>
<% if(isEditable){ %><span class="icon icon-arrow-bottom"></span><% }; %>
</a>
<% if (editable && assignedTo!==null) { %>
<% if (assignedTo!==null && isEditable) { %>
<a href="" title="delete assignment" class="icon icon-delete"></a>
<% } %>
</div>
""")
""") # TODO: i18n
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) ->
assignedToId = issue?.assigned_to
assignedTo = null
assignedTo = $scope.usersById[assignedToId] if assignedToId?
html = template({assignedTo: assignedTo, editable:editable})
assignedTo = if assignedToId? then $scope.usersById[assignedToId] else null
ctx = {
assignedTo: assignedTo
isEditable: isEditable()
}
html = template(ctx)
$el.html(html)
$el.on "click", ".user-assigned", (event) ->
event.preventDefault()
return if not isEditable()
$scope.$apply ->
$rootscope.$broadcast("assigned-to:add", $model.$modelValue)
$el.on "click", ".icon-delete", (event) ->
event.preventDefault()
return if not isEditable()
title = "Are you sure you want to leave it unassigned?" # TODO: i18n
$confirm.ask(title).then (finish) =>
finish()
$model.$modelValue.assigned_to = null
save($model.$modelValue)
$scope.$on "assigned-to:added", (ctx, userId) ->
$model.$modelValue.assigned_to = userId
save($model.$modelValue)
$scope.$watch $attrs.ngModel, (instance) ->
renderAssignedTo(instance)
if editable
$el.on "click", ".user-assigned", (event) ->
event.preventDefault()
$scope.$apply ->
$rootscope.$broadcast("assigned-to:add", $model.$modelValue)
$el.on "click", ".icon-delete", (event) ->
event.preventDefault()
title = "Delete assignetion"
message = ""
$confirm.askOnDelete(title, message).then (finish) =>
finish()
$model.$modelValue.assigned_to = null
renderAssignedTo($model.$modelValue)
$scope.$on "assigned-to:added", (ctx, userId) ->
$model.$modelValue.assigned_to = userId
renderAssignedTo($model.$modelValue)
$scope.$on "$destroy", ->
$el.off()
return {
link:link,
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
}
ListItemTypeDirective = ->
template = """
<div class="level"></div>

View File

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

View File

@ -43,6 +43,9 @@ TagsDirective = ->
$ctrl.$formatters.push(formatter)
$ctrl.$parsers.push(parser)
$scope.$on "$destroy", ->
$el.off()
return {
require: "ngModel"
link: link
@ -83,127 +86,133 @@ ColorizeTagsDirective = ->
$scope.$watch $attrs.tgColorizeTags, (tags) ->
render(tags) if tags?
$scope.$on "$destroy", ->
$el.off()
return {link: link}
module.directive("tgColorizeTags", ColorizeTagsDirective)
#############################################################################
## TagLine (possible should be moved as generic directive)
## TagLine Directive (for Lightboxes)
#############################################################################
TagLineDirective = ($log, $rs) ->
# Main directive template (rendered by angular)
LbTagLineDirective = ($rs) ->
ENTER_KEY = 13
template = """
<div class="tags-container"></div>
<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)
templateTags = _.template("""
<% _.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>
<% if (editable) { %>
<a href="" title="delete tag" class="icon icon-delete"></a>
<% } %>
</div>
<% }); %>""")
renderTags = ($el, tags, editable, tagsColors) ->
ctx = {
tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]})
editable: editable
}
html = templateTags(ctx)
$el.find("div.tags-container").html(html)
normalizeTags = (tags) ->
tags = _.map(tags, trim)
tags = _.map(tags, (x) -> x.toLowerCase())
return _.uniq(tags)
</span>
<% }); %>
""") # TODO: i18n
link = ($scope, $el, $attrs, $model) ->
editable = if $attrs.editable == "true" then true else false
$el.addClass("tags-block")
## Render
renderTags = (tags, tagsColors) ->
ctx = {
tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]})
}
html = templateTags(ctx)
$el.find("div.tags-container").html(html)
showSaveButton = -> $el.find(".save").removeClass("hidden")
hideSaveButton = -> $el.find(".save").addClass("hidden")
resetInput = ->
$el.find("input").val("")
$el.find("input").autocomplete("close")
## Aux methods
addValue = (value) ->
value = trim(value)
return if value.length <= 0
value = trim(value.toLowerCase())
return if value.length == 0
tags = _.clone($model.$modelValue, false)
tags = [] if not tags?
tags.push(value)
tags.push(value) if value not in tags
$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 = () ->
input = $el.find('input')
value = $el.find("input").val()
addValue(input.val())
input.val("")
input.autocomplete("close")
$el.find('.save').hide()
addValue(value)
resetInput()
hideSaveButton()
$scope.$watch $attrs.ngModel, (val) ->
tags_colors = if $scope.project?.tags_colors? then $scope.project.tags_colors else []
renderTags($el, val, editable, tags_colors)
## Events
$el.on "keypress", "input", (event) ->
return if event.keyCode != ENTER_KEY
event.preventDefault()
bindOnce $scope, "projectId", (projectId) ->
# If not editable, no tags preloading is needed.
return if not editable
$el.on "keyup", "input", (event) ->
target = angular.element(event.currentTarget)
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) ->
menu = elements.element.element
menu.css("width", elements.target.width)
menu.css("top", position.top)
menu.css("left", position.left)
$rs.projects.tags(projectId).then (data) ->
$el.find("input").autocomplete({
source: data
position: {
my: "left top",
using: positioningFunction
}
select: (event, ui) ->
addValue(ui.item.value)
ui.item.value = ""
})
$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 = ""
})
if not editable
$el.find("input").remove()
$scope.$watch $attrs.ngModel, (tags) ->
tagsColors = $scope.project?.tags_colors or []
renderTags(tags, tagsColors)
$el.on "keypress", "input", (event) ->
return if event.keyCode != 13
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))
$scope.$on "$destroy", ->
$el.off()
return {
link:link,
@ -211,4 +220,198 @@ TagLineDirective = ($log, $rs) ->
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("""
<div class="preview">
<div class="actions">
<a href="#" title="Edit">Edit</a>
<a href="#" title="Edit" class="icon icon-edit edit"></a>
</div>
<div class="content wysiwyg">
<%= data %>

View File

@ -46,11 +46,12 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"$log",
"$appTitle",
"$tgAnalytics",
"$tgNavUrls"
"$tgNavUrls",
"tgLoader"
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
@log, @appTitle, @analytics, @navUrls) ->
@log, @appTitle, @analytics, @navUrls, tgLoader) ->
@scope.issueRef = @params.issueref
@scope.sectionName = "Issue Details"
@.initializeEventHandlers()
@ -60,11 +61,12 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
# On Success
promise.then =>
@appTitle.set(@scope.issue.subject + " - " + @scope.project.name)
@.initializeOnDeleteGoToUrl()
tgLoader.pageLoaded()
# On Error
promise.then null, @.onInitialDataError.bind(@)
initializeEventHandlers: ->
@scope.$on "attachment:create", =>
@rootscope.$broadcast("history:reload")
@ -81,6 +83,13 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@rootscope.$broadcast("history:reload")
@.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: ->
return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project
@ -130,250 +139,432 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
.then(=> @.loadUsersAndRoles())
.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)
#############################################################################
## Issue Main Directive
## Issue status display directive
#############################################################################
IssueDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) ->
linkSidebar = ($scope, $el, $attrs, $ctrl) ->
IssueStatusDisplayDirective = ->
# 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
template = _.template("""
<span>
<% if (status.is_closed) { %>
Closed
<% } else { %>
Open
<% } %>
</span>
<span class="us-detail-status" style="color:<%= status.color %>">
<%= status.name %>
</span>
""") # TODO: i18n
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("""
<h1>
<span>
<% if (status.is_closed) { %>
Closed
<% } else { %>
Open
<% } %>
</span>
<span class="us-detail-status" style="color:<%= status.color %>"><%= status.name %></span>
</h1>
<div class="us-created-by">
<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>
</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]
render = (issue) ->
html = template({
owner: owner
date: date
editable: editable
status: status
severity: severity
priority: priority
type: type
status: $scope.statusById[issue.status]
})
$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) ->
if issue?
renderIssuestatus(issue)
render(issue) if issue?
if editable
$el.on "click", ".type-data", (event) ->
event.preventDefault()
event.stopPropagation()
$el.find(".pop-type").popover().open()
$scope.$on "$destroy", ->
$el.off()
$el.on "click", ".type", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$model.$modelValue.type = target.data("type-id")
renderIssuestatus($model.$modelValue)
$.fn.popover().closeAll()
return {
link: link
restrict: "EA"
require: "ngModel"
}
$el.on "click", ".severity-data", (event) ->
event.preventDefault()
event.stopPropagation()
$el.find(".pop-severity").popover().open()
module.directive("tgIssueStatusDisplay", IssueStatusDisplayDirective)
$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()
event.stopPropagation()
$el.find(".pop-priority").popover().open()
#############################################################################
## Issue status button directive
#############################################################################
$el.on "click", ".priority", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$model.$modelValue.priority = target.data("priority-id")
renderIssuestatus($model.$modelValue)
$.fn.popover().closeAll()
IssueStatusButtonDirective = ($rootScope, $repo, $confirm, $loading) ->
# Display the status of Issue and you can edit it.
#
# Example:
# tg-issue-status-button(ng-model="issue")
#
# Requirements:
# - Issue object (ng-model)
# - scope.statusById object
# - $scope.project.my_permissions
$el.on "click", ".status-data", (event) ->
event.preventDefault()
event.stopPropagation()
$el.find(".pop-status").popover().open()
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>
$el.on "click", ".status", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$model.$modelValue.status = target.data("status-id")
renderIssuestatus($model.$modelValue)
$.fn.popover().closeAll()
<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
return {link:link, require:"ngModel"}
link = ($scope, $el, $attrs, $model) ->
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_issue") != -1
module.directive("tgIssueStatus", IssueStatusDirective)
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) ->
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()
issue = $model.$modelValue.clone()
issue.status = target.data("status-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("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>
</div>
<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>
<% if(perms.modify_task) { %>
<span class="icon icon-arrow-bottom"></span>
@ -166,7 +166,7 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) ->
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) ->
template = _.template("""

View File

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

View File

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

View File

@ -57,6 +57,7 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
promise.then () =>
@appTitle.set(@scope.task.subject + " - " + @scope.project.name)
@.initializeOnDeleteGoToUrl()
tgLoader.pageLoaded()
promise.then null, @.onInitialDataError.bind(@)
@ -70,6 +71,21 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$on "attachment:delete", =>
@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: ->
return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project
@ -97,14 +113,19 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
ref: @scope.task.neighbors.next.ref
}
@scope.nextUrl = @navUrls.resolve("project-tasks-detail", ctx)
return task
if task.milestone
@rs.sprints.get(task.project, task.milestone).then (sprint) =>
@scope.sprint = sprint
loadSprint: ->
if @scope.task.milestone
return @rs.sprints.get(@scope.task.project, @scope.task.milestone).then (sprint) =>
@scope.sprint = sprint
return sprint
if task.user_story
@rs.userstories.get(task.project, task.user_story).then (us) =>
@scope.us = us
loadUserStory: ->
if @scope.task.user_story
return @rs.userstories.get(@scope.task.project, @scope.task.user_story).then (us) =>
@scope.us = us
return us
loadInitialData: ->
params = {
@ -119,157 +140,216 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles())
.then(=> @.loadTask())
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")
.then(=> @.loadTask().then(=> @q.all([@.loadUserStory(),
@.loadSprint()])))
module.controller("TaskDetailController", TaskDetailController)
#############################################################################
## Task Main Directive
## Task status display directive
#############################################################################
TaskDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) ->
linkSidebar = ($scope, $el, $attrs, $ctrl) ->
TaskStatusDisplayDirective = ->
# 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
template = _.template("""
<span>
<% if (status.is_closed) { %>
Closed
<% } else { %>
Open
<% } %>
</span>
<span class="us-detail-status" style="color:<%= status.color %>">
<%= status.name %>
</span>
""") # TODO: i18n
link = ($scope, $el, $attrs) ->
$ctrl = $el.controller()
linkSidebar($scope, $el, $attrs, $ctrl)
render = (task) ->
html = template({
status: $scope.statusById[task.status]
})
$el.html(html)
if $el.is("form")
form = $el.checksley()
$scope.$watch $attrs.ngModel, (task) ->
render(task) if task?
$el.on "click", ".save-task", (event) ->
if not form.validate()
return
$scope.$on "$destroy", ->
$el.off()
onSuccess = ->
$loading.finish(target)
$confirm.notify("success")
ctx = {
project: $scope.project.slug
ref: $scope.task.ref
}
$location.path($navUrls.resolve("project-tasks-detail", ctx))
return {
link: link
restrict: "EA"
require: "ngModel"
}
onError = ->
$loading.finish(target)
$confirm.notify("error")
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)
$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)
$loading.start(target)
$tgrepo.save($scope.task).then(onSuccess, onError)
return {link:link}
$.fn.popover().closeAll()
module.directive("tgTaskDetail", ["$tgRepo", "$log", "$tgLocation", "$tgConfirm", "$tgNavUrls",
"$tgLoading", TaskDirective])
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])
#############################################################################
## Task status directive
#############################################################################
TaskStatusDirective = () ->
#TODO: i18n
TaskIsIocaineButtonDirective = ($rootscope, $tgrepo, $confirm, $loading) ->
template = _.template("""
<h1>
<span>
<% if (status.is_closed) { %>
Closed
<% } else { %>
Open
<% } %>
<span class="us-detail-status" style="color:<%= status.color %>"><%= status.name %></span>
</h1>
<div class="us-created-by">
<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>
</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>
<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) ->
editable = $attrs.editable?
isEditable = ->
return $scope.project.my_permissions.indexOf("modify_task") != -1
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({
owner: owner
date: date
editable: editable
status: status
})
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.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList}))
$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) ->
if task?
renderTaskstatus(task)
render(task) if task
if editable
$el.on "click", ".status-data", (event) ->
event.preventDefault()
event.stopPropagation()
$el.find(".pop-status").popover().open()
$scope.$on "$destroy", ->
$el.off()
$el.on "click", ".status", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$model.$modelValue.status = target.data("status-id")
renderTaskstatus($model.$modelValue)
$el.find(".popover").popover().close()
return {
link: link
restrict: "EA"
require: "ngModel"
}
return {link:link, require:"ngModel"}
module.directive("tgTaskStatus", TaskStatusDirective)
module.directive("tgTaskIsIocaineButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", TaskIsIocaineButtonDirective])

View File

@ -59,12 +59,17 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
# On Success
promise.then =>
@appTitle.set(@scope.us.subject + " - " + @scope.project.name)
@.initializeOnDeleteGoToUrl()
tgLoader.pageLoaded()
# On Error
promise.then null, @.onInitialDataError.bind(@)
initializeEventHandlers: ->
@scope.$on "related-tasks:update", =>
@.loadUs()
@scope.tasks = _.clone(@scope.tasks, false)
@scope.$on "attachment:create", =>
@analytics.trackEvent("attachment", "create", "create attachment on userstory", 1)
@rootscope.$broadcast("history:reload")
@ -75,6 +80,18 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$on "attachment:delete", =>
@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: ->
return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project
@ -106,12 +123,14 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
}
@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
loadSprint: ->
if @scope.us.milestone
return @rs.sprints.get(@scope.us.project, @scope.us.milestone).then (sprint) =>
@scope.sprint = sprint
return sprint
loadTasks: ->
return @rs.tasks.list(@scope.projectId, null, @scope.usId).then (tasks) =>
@scope.tasks = tasks
@ -130,263 +149,128 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles())
.then(=> @q.all([@.loadUs(),
.then(=> @q.all([@.loadUs().then(=> @.loadSprint()),
@.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)
#############################################################################
## User story Main Directive
## User story status display directive
#############################################################################
UsDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $loading) ->
linkSidebar = ($scope, $el, $attrs, $ctrl) ->
UsStatusDisplayDirective = ->
# 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("""
<span>
<% if (is_closed) { %>
Closed
<% } else { %>
Open
<% } %>
</span>
<span class="us-detail-status" style="color:<%= status.color %>">
<%= status.name %>
</span>
""") # TODO: i18n
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
#############################################################################
UsStatusDetailDirective = () ->
#TODO: i18n
template = _.template("""
<h1>
<span>
<% if (is_closed) { %>
Closed
<% } else { %>
Open
<% } %>
<span class="us-detail-status" style="color:<%= status.color %>"><%= status.name %></span>
</h1>
<div class="us-detail-progress-bar">
<div class="current-progress" style="width:<%- usProgress %>%"/>
<span clasS="tasks-completed">
<%- totalClosedTasks %>/<%- totalTasks %> tasks completed
</span>
</div>
<div class="us-created-by">
<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>
</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
render = (us) ->
html = template({
owner: owner
date: date
editable: editable
is_closed: us.is_closed
status: status
totalPoints: us.total_points
rolePoints: rolePoints
totalTasks: totalTasks
totalClosedTasks: totalClosedTasks
usProgress: usProgress
status: $scope.statusById[us.status]
})
$el.html(html)
$el.find(".status-data").append(selectionStatusTemplate({statuses:$scope.statusList}))
bindOnce $scope, "tasks", (tasks) ->
$scope.$watch $attrs.ngModel, (us) ->
if us?
renderUsstatus(us)
$scope.$watch $attrs.ngModel, (us) ->
render(us) if us?
$scope.$on "related-tasks:update", ->
us = $scope.$eval $attrs.ngModel
if us?
# Reload the us because the status could have changed
$ctrl.loadUs()
renderUsstatus(us)
$scope.$on "$destroy", ->
$el.off()
if editable
$el.on "click", ".status-data", (event) ->
event.preventDefault()
event.stopPropagation()
$el.find(".pop-status").popover().open()
return {
link: link
restrict: "EA"
require: "ngModel"
}
$el.on "click", ".status", (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
$model.$modelValue.status = target.data("status-id")
renderUsstatus($model.$modelValue)
$.fn.popover().closeAll()
module.directive("tgUsStatusDisplay", UsStatusDisplayDirective)
$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()
#############################################################################
## User story related tasts progress splay Directive
#############################################################################
target = angular.element(event.currentTarget)
$.fn.popover().closeAll()
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
$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)
template = _.template("""
<div class="current-progress" style="width:<%- progress %>%" />
<span clasS="tasks-completed">
<%- totalClosedTasks %>/<%- totalTasks %> tasks completed
</span>
""") # TODO: i18n
return {link:link, require:"ngModel"}
link = ($scope, $el, $attrs) ->
render = (tasks) ->
totalTasks = tasks.length
totalClosedTasks = _.filter(tasks, (task) => $scope.taskStatusById[task.status].is_closed).length
progress = if totalTasks > 0 then 100 * totalClosedTasks / totalTasks else 0
html = template({
totalTasks: totalTasks
totalClosedTasks: totalClosedTasks
progress: progress
})
$el.html(html)
$scope.$watch $attrs.ngModel, (tasks) ->
render(tasks) if tasks?
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgUsTasksProgressDisplay", UsTasksProgressDisplayDirective)
module.directive("tgUsStatusDetail", UsStatusDetailDirective)
#############################################################################
## 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("""
<ul class="points-per-role">
<li class="total">
@ -394,7 +278,7 @@ UsEstimationDirective = ($log) ->
<span class="role">total</span>
</li>
<% _.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="role"><%- role.name %></span></li>
<% }); %>
@ -417,7 +301,14 @@ UsEstimationDirective = ($log) ->
</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) ->
totalPoints = us.total_points or 0
computableRoles = _.filter($scope.project.roles, "computable")
@ -430,7 +321,12 @@ UsEstimationDirective = ($log) ->
role.points = if pointObj? and pointObj.name? then pointObj.name else "?"
return role
html = mainTemplate({totalPoints: totalPoints, roles: roles})
ctx = {
totalPoints: totalPoints
roles: roles
editable: isEditable()
}
html = mainTemplate(ctx)
$el.html(html)
renderPoints = (target, us, roleId) ->
@ -463,19 +359,15 @@ UsEstimationDirective = ($log) ->
return "0"
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) ->
event.preventDefault()
event.stopPropagation()
return if not isEditable()
target = angular.element(event.currentTarget)
roleId = target.data("role-id")
us = $scope.$eval($attrs.ngModel)
us = $model.$modelValue
renderPoints(target, us, roleId)
target.siblings().removeClass('active')
@ -484,8 +376,7 @@ UsEstimationDirective = ($log) ->
$el.on "click", ".point", (event) ->
event.preventDefault()
event.stopPropagation()
us = $scope.$eval($attrs.ngModel)
return if not isEditable()
target = angular.element(event.currentTarget)
roleId = target.data("role-id")
@ -493,17 +384,262 @@ UsEstimationDirective = ($log) ->
$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
us.setAttr('points', points) if us.setAttr?
us.points = points
us.total_points = calculateTotalPoints(us)
$model.$setViewValue(us)
$scope.$apply ->
us.points = points
us.total_points = calculateTotalPoints(us)
render(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 {
link: link
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",
"$rootScope",
"$tgRepo",
"$tgModel",
"$tgConfirm",
"$tgResources",
"$routeParams",
@ -51,7 +52,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"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) ->
@scope.projectSlug = @params.pslug
@scope.wikiSlug = @params.slug
@ -80,7 +81,15 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.wiki = 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
loadWikiLinks: ->
@ -109,28 +118,13 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.wikiId = data.wikipage
return prom.then null, (xhr) =>
ctx = {project: @params.pslug, slug: @params.slug}
@location.path(@navUrls.resolve("project-wiki-page-edit", ctx))
@scope.wikiId = null
return promise.then(=> @.loadProject())
.then(=> @.loadUsersAndRoles())
.then(=> @q.all([@.loadWikiLinks(),
@.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: ->
# TODO: i18n
title = "Delete Wiki Page"
@ -151,95 +145,181 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
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) ->
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) ->
WikiSummaryDirective = ($log) ->
template = _.template("""
<figure class="avatar">
<img src="<%= imgurl %>" alt="<%- name %>">
</figure>
<span class="description">last modification</span>
<span class="username"><%- name %></span>
<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">
<img src="<%= user.imgUrl %>" alt="<%- user.name %>">
</figure>
<span class="description">last modification</span>
<span class="username"><%- user.name %></span>
</li>
</ul>
""")
link = ($scope, $el, $attrs) ->
if not $attrs.ngModel?
return $log.error "WikiUserDirective: no ng-model attr is defined"
link = ($scope, $el, $attrs, $model) ->
render = (wiki) ->
if not $scope.usersById?
$log.error "WikiUserDirective requires userById set in scope."
$log.error "WikiSummaryDirective requires userById set in scope."
else
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)
$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 {
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
block content
div.wrapper(tg-issue-detail, ng-controller="IssueDetailController as ctrl",
div.wrapper(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(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
div.us-title(ng-class="{blocked: issue.is_blocked}")
h2.us-title-text
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:
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'")
div.issue-nav
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous issue")
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next issue")
a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl",
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-history(ng-model="issue", type="issue")
sidebar.menu-secondary.sidebar
section.us-status(tg-issue-status, ng-model="issue")
section.us-assigned-to(tg-assigned-to, ng-model="issue")
section.watchers(tg-watchers, ng-model="issue")
section.us-status
h1(tg-issue-status-display, 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
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
block content
div.wrapper(tg-task-detail, ng-controller="TaskDetailController as ctrl",
div.wrapper(ng-controller="TaskDetailController as ctrl",
ng-init="section='backlog'")
div.main.us-detail
div.us-detail-header.header-with-actions
@ -15,16 +15,12 @@ block content
href="", title="Go to taskboard",
tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug",
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
div.us-title(ng-class="{blocked: task.is_blocked}")
h2.us-title-text
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
a(tg-check-permission="view_us", href="", title="Go to user story",
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(tg-bind-html="task.blocked_note || 'This task is blocked'")
div.issue-nav
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous task")
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next task")
a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl",
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-history(ng-model="task", type="task")
sidebar.menu-secondary.sidebar
section.us-status(tg-task-status, ng-model="task")
section.us-assigned-to(tg-assigned-to, ng-model="task")
section.watchers(tg-watchers, ng-model="task")
section.us-status
h1(tg-task-status-display, 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
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
block content
div.wrapper(tg-us-detail, ng-controller="UserStoryDetailController as ctrl",
div.wrapper(ng-controller="UserStoryDetailController as ctrl",
ng-init="section='backlog'")
div.main.us-detail
div.us-detail-header.header-with-actions
@ -15,16 +15,12 @@ block content
href="", title="Go to taskboard",
tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug",
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
div.us-title(ng-class="{blocked: us.is_blocked}")
h2.us-title-text
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
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(tg-bind-html="us.blocked_note || 'This user story is blocked'")
div.issue-nav
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}",
title="previous user story")
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next user story")
a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl",
title="previous 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
@ -50,15 +47,27 @@ block content
tg-history(ng-model="us", type="us")
sidebar.menu-secondary.sidebar
section.us-status(tg-us-status-detail, ng-model="us")
section.us-assigned-to(tg-assigned-to, ng-model="us")
section.us-created-by(tg-created-by, ng-model="us")
section.watchers(tg-watchers, ng-model="us")
section.us-status
h1(tg-us-status-display, ng-model="us")
div.us-detail-progress-bar(tg-us-tasks-progress-display, ng-model="tasks")
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
span.button.button-gray(href="", title="Client requirement",
ng-class="{'active': us.client_requirement}") Client requirement
span.button.button-gray(href="", title="Team requirement",
ng-class="{'active': us.team_requirement}") Team requirement
tg-us-team-requirement-button(ng-model="us")
tg-us-client-requirement-button(ng-model="us")
tg-block-button(tg-check-permission="modify_us", ng-model="us")
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-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")
fieldset
div(tg-tag-line, editable="true", ng-model="issue.tags")
div.tags-block(tg-lb-tag-line, ng-model="issue.tags")
fieldset
textarea.description(placeholder="Description", ng-model="issue.description")

View File

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

View File

@ -8,14 +8,13 @@ form
fieldset.estimation
tg-us-estimation(ng-model="us")
//- Render by tg-lb-create-edit-userstory
fieldset
select(name="status", ng-model="us.status", ng-options="s.id as s.name for s in usStatusList",
tr="placeholder:common.status")
fieldset
div(tg-tag-line, editable="true", ng-model="us.tags")
div.tags-block(tg-lb-tag-line, ng-model="us.tags")
fieldset
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
block content
div.wrapper(tg-wiki-detail, ng-controller="WikiDetailController as ctrl",
div.wrapper(ng-controller="WikiDetailController 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
.header-with-actions
.header
h1
span(tg-bo-bind="project.name", class="project-name-short")
span(tg-bo-bind="project.name")
span.green Wiki
span.wiki-title(tg-bo-bind='wiki.slug|unslugify')
.action-buttons
a.button.button-red.delete-wiki(tg-check-permission="delete_wiki_page",
href="", title="Delete", ng-click="ctrl.delete()") Delete
span.wiki-title(tg-bo-bind='wikiSlug|unslugify')
a.button.button-green.edit-wiki(tg-check-permission="modify_wiki_page",
href="", title="Edit", ng-click="ctrl.edit()") Edit
div.summary.wiki-summary(tg-wiki-summary, ng-model="wiki", ng-if="wiki.id")
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")
tg-attachments(ng-model="wiki", type="wiki_page")
a.remove(href="", ng-click="ctrl.delete()", ng-if="wiki.id", title="Remove this wiki page")
span.icon.icon-delete
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-container {
display: inline-block;
vertical-align: middle;
}
input {
display: inline-block;
padding: .4rem;
width: 14rem;
}
@ -42,7 +40,18 @@
margin: 0 .5rem .5rem 0;
padding: .5rem;
}
.save {
display: none;
.add-tag {
color: $gray-light;
&:hover {
color: $fresh-taiga;
}
}
.icon-plus {
@extend %large;
}
.add-tag-text {
@extend %small;
}
}

View File

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

View File

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

View File

@ -57,6 +57,12 @@
display: flex;
margin-bottom: 0;
max-width: 94%;
&:hover {
.icon-edit {
@include transition(opacity .3s linear);
opacity: 1;
}
}
}
.us-number {
@extend %xlarge;
@ -72,6 +78,17 @@
display: inline-block;
line-height: 2.2rem;
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 {
@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 {
background: $white;
height: 10rem;
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 {
@ -230,23 +309,25 @@
}
}
.issue-data {
.duty-data-container {
@extend %small;
div {
@include clearfix();
@include transition(background .2s ease-in);
background: darken($whitish, 5%);
margin-bottom: 1rem;
.duty-data {
margin-bottom: .5rem;
padding: .5rem;
padding-right: 1rem;
&:last-child {
margin: 0;
}
}
.clickable {
&:hover {
div {
@include transition(background .2s ease-in);
background: darken($whitish, 10%);
background: darken($whitish, 5%);
padding: .5rem;
padding-right: 1rem;
}
.clickable {
&:hover {
@include transition(background .2s ease-in);
background: darken($whitish, 10%);
}
}
}
.level {
@ -257,6 +338,10 @@
.level-name {
color: darken($whitish, 20%);
float: right;
&.loading span {
@include animation (loading .5s linear);
@include animation (spin 1s linear infinite);
}
}
}
@ -271,9 +356,25 @@
}
.button-gray {
background: $gray-light;
&:hover,
&:hover {
background: $gray-light;
}
&.editable {
&:hover {
background: $grayer;
cursor: pointer;
}
}
&.active {
background: $grayer;
background: $green-taiga;
}
}
.item-block {
&.editable {
&:hover {
background: $red;
cursor: pointer;
}
}
}
.button-red {
@ -283,7 +384,9 @@
}
}
label {
cursor: pointer;
&.editable {
cursor: pointer;
}
+input {
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 {
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();
margin-top: 1rem;
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 {
@include table-flex-child(1, 0);
img {
@ -22,13 +41,21 @@
@extend %large;
color: $green-taiga;
cursor: default;
line-height: 1.5rem;
&.editable {
cursor: pointer;
}
.icon {
vertical-align: top;
}
}
.assigned-name {
@include ellipsis(80%);
display: inline-block;
}
.icon-delete {
color: $gray-light;
opacity: 1;
opacity: 0;
position: absolute;
right: 0;
top: 0;

View File

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

View File

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

View File

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

View File

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