Add tags autocompletion directive
parent
682f6492b3
commit
743d19ea6f
|
@ -173,7 +173,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
||||||
@scope.projectId = data.project
|
@scope.projectId = data.project
|
||||||
return data
|
return data
|
||||||
|
|
||||||
return promise.then(=> @loadProject())
|
return promise.then(=> @.loadProject())
|
||||||
.then(=> @.loadUsersAndRoles())
|
.then(=> @.loadUsersAndRoles())
|
||||||
.then(=> @.loadBacklog())
|
.then(=> @.loadBacklog())
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
###
|
###
|
||||||
|
|
||||||
taiga = @.taiga
|
taiga = @.taiga
|
||||||
|
trim = @.taiga.trim
|
||||||
|
|
||||||
module = angular.module("taigaBase")
|
module = angular.module("taigaBase")
|
||||||
|
|
||||||
|
@ -88,3 +89,117 @@ ColorizeTagsDirective = ->
|
||||||
return {link: link}
|
return {link: link}
|
||||||
|
|
||||||
module.directive("tgColorizeTags", ColorizeTagsDirective)
|
module.directive("tgColorizeTags", ColorizeTagsDirective)
|
||||||
|
|
||||||
|
#############################################################################
|
||||||
|
## TagLine (possible should be moved as generic directive)
|
||||||
|
#############################################################################
|
||||||
|
|
||||||
|
TagLineDirective = ($log, $rs) ->
|
||||||
|
# Main directive template (rendered by angular)
|
||||||
|
template = """
|
||||||
|
<div class="tags-container"></div>
|
||||||
|
<input type="text" placeholder="Write tag..." class="tag-input" />
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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-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)
|
||||||
|
|
||||||
|
link = ($scope, $el, $attrs, $model) ->
|
||||||
|
editable = if $attrs.editable == "true" then true else false
|
||||||
|
$el.addClass('tags-block')
|
||||||
|
|
||||||
|
addValue = (value) ->
|
||||||
|
value = trim(value)
|
||||||
|
return if value.length <= 0
|
||||||
|
|
||||||
|
tags = _.clone($model.$modelValue, false)
|
||||||
|
tags = [] if not tags?
|
||||||
|
tags.push(value)
|
||||||
|
|
||||||
|
$scope.$apply ->
|
||||||
|
$model.$setViewValue(normalizeTags(tags))
|
||||||
|
|
||||||
|
|
||||||
|
$scope.$watch $attrs.ngModel, (val) ->
|
||||||
|
return if not val
|
||||||
|
renderTags($el, val, editable, $scope.project.tags_colors)
|
||||||
|
|
||||||
|
$scope.$watch "projectId", (val) ->
|
||||||
|
return if not val?
|
||||||
|
positioningFunction = (position, elements) ->
|
||||||
|
menu = elements.element.element
|
||||||
|
menu.css 'width', elements.target.width
|
||||||
|
menu.css 'top', position.top
|
||||||
|
menu.css 'left', position.left
|
||||||
|
|
||||||
|
promise = $rs.projects.tags($scope.projectId)
|
||||||
|
promise.then (data) ->
|
||||||
|
if editable
|
||||||
|
$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").remove() if not editable
|
||||||
|
|
||||||
|
$el.on "keypress", "input", (event) ->
|
||||||
|
return if event.keyCode != 13
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
$el.on "keyup", "input", (event) ->
|
||||||
|
return if event.keyCode != 13
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
target = angular.element(event.currentTarget)
|
||||||
|
addValue(target.val())
|
||||||
|
target.val("")
|
||||||
|
$el.find("input").autocomplete("close")
|
||||||
|
|
||||||
|
|
||||||
|
$el.on "click", ".icon-delete", (event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
target = angular.element(event.currentTarget)
|
||||||
|
value = trim(target.siblings(".tag-name").text())
|
||||||
|
|
||||||
|
if value.length <= 0
|
||||||
|
return
|
||||||
|
|
||||||
|
tags = _.clone($model.$modelValue, false)
|
||||||
|
tags = _.pull(tags, value)
|
||||||
|
|
||||||
|
$scope.$apply ->
|
||||||
|
$model.$setViewValue(normalizeTags(tags))
|
||||||
|
|
||||||
|
return {
|
||||||
|
link:link,
|
||||||
|
require:"ngModel"
|
||||||
|
template: template
|
||||||
|
}
|
||||||
|
|
||||||
|
module.directive("tgTagLine", ["$log", "$tgResources", TagLineDirective])
|
||||||
|
|
|
@ -27,89 +27,6 @@ typeIsArray = @.taiga.typeIsArray
|
||||||
module = angular.module("taigaCommon", [])
|
module = angular.module("taigaCommon", [])
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## TagLine (possible should be moved as generic directive)
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
TagLineDirective = ($log) ->
|
|
||||||
# Main directive template (rendered by angular)
|
|
||||||
template = """
|
|
||||||
<div class="tags-container"></div>
|
|
||||||
<input type="text" placeholder="Write tag..." class="hidden"/>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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-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)
|
|
||||||
|
|
||||||
link = ($scope, $el, $attrs, $model) ->
|
|
||||||
editable = if $attrs.editable == "true" then true else false
|
|
||||||
|
|
||||||
$scope.$watch $attrs.ngModel, (val) ->
|
|
||||||
return if not val
|
|
||||||
renderTags($el, val, editable, $scope.project.tags_colors)
|
|
||||||
|
|
||||||
$el.find("input").remove() if not editable
|
|
||||||
|
|
||||||
$el.on "keyup", "input", (event) ->
|
|
||||||
return if event.keyCode != 13
|
|
||||||
target = angular.element(event.currentTarget)
|
|
||||||
value = trim(target.val())
|
|
||||||
|
|
||||||
if value.length <= 0
|
|
||||||
return
|
|
||||||
|
|
||||||
tags = _.clone($model.$modelValue, false)
|
|
||||||
tags = [] if not tags?
|
|
||||||
tags.push(value)
|
|
||||||
|
|
||||||
target.val("")
|
|
||||||
|
|
||||||
$scope.$apply ->
|
|
||||||
$model.$setViewValue(normalizeTags(tags))
|
|
||||||
|
|
||||||
$el.on "click", ".icon-delete", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
target = angular.element(event.currentTarget)
|
|
||||||
value = trim(target.siblings(".tag-name").text())
|
|
||||||
|
|
||||||
if value.length <= 0
|
|
||||||
return
|
|
||||||
|
|
||||||
tags = _.clone($model.$modelValue, false)
|
|
||||||
tags = _.pull(tags, value)
|
|
||||||
|
|
||||||
$scope.$apply ->
|
|
||||||
$model.$setViewValue(normalizeTags(tags))
|
|
||||||
|
|
||||||
return {
|
|
||||||
link:link,
|
|
||||||
require:"ngModel"
|
|
||||||
template: template
|
|
||||||
}
|
|
||||||
|
|
||||||
module.directive("tgTagLine", ["$log", TagLineDirective])
|
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Change (comment and history mode) directive
|
## Change (comment and history mode) directive
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
|
@ -45,6 +45,9 @@ resourceProvider = ($repo) ->
|
||||||
service.stats = (projectId) ->
|
service.stats = (projectId) ->
|
||||||
return $repo.queryOneRaw("projects", "#{projectId}/stats")
|
return $repo.queryOneRaw("projects", "#{projectId}/stats")
|
||||||
|
|
||||||
|
service.tags = (projectId) ->
|
||||||
|
return $repo.queryOneRaw("projects", "#{projectId}/tags")
|
||||||
|
|
||||||
service.tagsColors = (id) ->
|
service.tagsColors = (id) ->
|
||||||
return $repo.queryOne("projects", "#{id}/tags_colors")
|
return $repo.queryOne("projects", "#{id}/tags_colors")
|
||||||
|
|
||||||
|
|
|
@ -35,11 +35,11 @@ block content
|
||||||
sidebar.menu-secondary.sidebar
|
sidebar.menu-secondary.sidebar
|
||||||
include views/modules/sprints
|
include views/modules/sprints
|
||||||
|
|
||||||
div.lightbox.lightbox-generic-form(tg-lb-create-edit-userstory)
|
div.lightbox.lightbox-generic-form(tg-lb-create-edit-userstory)
|
||||||
include views/modules/lightbox-us-create-edit
|
include views/modules/lightbox-us-create-edit
|
||||||
|
|
||||||
div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories)
|
div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories)
|
||||||
include views/modules/lightbox-us-bulk
|
include views/modules/lightbox-us-bulk
|
||||||
|
|
||||||
div.lightbox.lightbox-sprint-add-edit(tg-lb-create-edit-sprint)
|
div.lightbox.lightbox-sprint-add-edit(tg-lb-create-edit-sprint)
|
||||||
include views/modules/lightbox-sprint-add-edit
|
include views/modules/lightbox-sprint-add-edit
|
||||||
|
|
|
@ -23,7 +23,7 @@ block content
|
||||||
span.block-description(tg-bind-html="issue.blocked_note || 'This issue is blocked'")
|
span.block-description(tg-bind-html="issue.blocked_note || 'This issue is blocked'")
|
||||||
a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock issue") Unblock
|
a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock issue") Unblock
|
||||||
|
|
||||||
div.user-story-tags(tg-tag-line, editable="true", ng-model="issue.tags")
|
div(tg-tag-line, editable="true", ng-model="issue.tags")
|
||||||
|
|
||||||
section.us-content
|
section.us-content
|
||||||
textarea(placeholder="Write a description of your issue", ng-model="issue.description", tg-markitup)
|
textarea(placeholder="Write a description of your issue", ng-model="issue.description", tg-markitup)
|
||||||
|
|
|
@ -24,7 +24,7 @@ block content
|
||||||
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous issue")
|
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-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next issue")
|
||||||
|
|
||||||
div.user-story-tags(tg-tag-line, ng-model="issue.tags", ng-show="issue.tags")
|
div(tg-tag-line, ng-model="issue.tags", ng-show="issue.tags")
|
||||||
|
|
||||||
section.us-content.wysiwyg(tg-bind-html="issue.description_html")
|
section.us-content.wysiwyg(tg-bind-html="issue.description_html")
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ block content
|
||||||
span.block-description(tg-bind-html="task.blocked_note || 'This task is blocked'")
|
span.block-description(tg-bind-html="task.blocked_note || 'This task is blocked'")
|
||||||
a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock task") Unblock
|
a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock task") Unblock
|
||||||
|
|
||||||
div.user-story-tags(tg-tag-line, editable="true", ng-model="task.tags")
|
div(tg-tag-line, editable="true", ng-model="task.tags")
|
||||||
|
|
||||||
section.us-content
|
section.us-content
|
||||||
textarea(placeholder="Write a description of your task", ng-model="task.description", tg-markitup)
|
textarea(placeholder="Write a description of your task", ng-model="task.description", tg-markitup)
|
||||||
|
|
|
@ -24,7 +24,7 @@ block content
|
||||||
a.icon.icon-arrow-left(ng-show="previousUrl",href="{{ previousUrl }}", title="previous task")
|
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-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next task")
|
||||||
|
|
||||||
div.user-story-tags(tg-tag-line, ng-model="task.tags", ng-show="task.tags")
|
div(tg-tag-line, ng-model="task.tags", ng-show="task.tags")
|
||||||
|
|
||||||
section.us-content.wysiwyg(tg-bind-html="task.description_html")
|
section.us-content.wysiwyg(tg-bind-html="task.description_html")
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ block content
|
||||||
span.block-description(tg-bind-html="us.blocked_note || 'This US is 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
|
a.unblock(ng-click="ctrl.unblock()", href="", title="Unblock US") Unblock
|
||||||
|
|
||||||
div.user-story-tags(tg-tag-line, editable="true", ng-model="us.tags")
|
div(tg-tag-line, editable="true", ng-model="us.tags")
|
||||||
|
|
||||||
section.us-content
|
section.us-content
|
||||||
textarea(placeholder="Write a description of your user story", ng-model="us.description", tg-markitup)
|
textarea(placeholder="Write a description of your user story", ng-model="us.description", tg-markitup)
|
||||||
|
|
|
@ -26,7 +26,7 @@ block content
|
||||||
title="previous user story")
|
title="previous user story")
|
||||||
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next user story")
|
a.icon.icon-arrow-right(ng-show="nextUrl", href="{{ nextUrl }}", title="next user story")
|
||||||
|
|
||||||
div.user-story-tags(tg-tag-line, ng-model="us.tags", ng-show="us.tags")
|
div(tg-tag-line, ng-model="us.tags", ng-show="us.tags")
|
||||||
|
|
||||||
section.us-content.wysiwyg(tg-bind-html="us.description_html")
|
section.us-content.wysiwyg(tg-bind-html="us.description_html")
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
div.row.us-item-row(ng-repeat="us in visibleUserstories|orderBy:order track by us.id", tg-draggable, ng-class="{blocked: us.is_blocked}")
|
div.row.us-item-row(ng-repeat="us in visibleUserstories|orderBy:order track by us.id", tg-draggable, ng-class="{blocked: us.is_blocked}")
|
||||||
div.user-stories
|
div.user-stories
|
||||||
div.user-story-tags(tg-colorize-tags="us.tags", tg-colorize-tags-type="backlog")
|
div.tags-block(tg-colorize-tags="us.tags", tg-colorize-tags-type="backlog")
|
||||||
div.user-story-name
|
div.user-story-name
|
||||||
input(tg-check-permission, permission="modify_us", type="checkbox", name="")
|
input(tg-check-permission, permission="modify_us", type="checkbox", name="")
|
||||||
a.clickable(tg-nav="project-userstories-detail:project=project.slug,ref=us.ref",
|
a.clickable(tg-nav="project-userstories-detail:project=project.slug,ref=us.ref",
|
||||||
|
|
|
@ -14,7 +14,7 @@ form
|
||||||
select.severity(ng-model="issue.severity", ng-options="s.id as s.name for s in severityList")
|
select.severity(ng-model="issue.severity", ng-options="s.id as s.name for s in severityList")
|
||||||
|
|
||||||
fieldset
|
fieldset
|
||||||
input(type="text", placeholder="Tags", tg-tags, ng-model="issue.tags")
|
div(tg-tag-line, editable="true", ng-model="issue.tags")
|
||||||
|
|
||||||
fieldset
|
fieldset
|
||||||
textarea.description(placeholder="Description", ng-model="issue.description")
|
textarea.description(placeholder="Description", ng-model="issue.description")
|
||||||
|
|
|
@ -16,7 +16,7 @@ form
|
||||||
option(value="") Unassigned
|
option(value="") Unassigned
|
||||||
|
|
||||||
fieldset
|
fieldset
|
||||||
input(type="text", placeholder="Tags", tg-tags, ng-model="task.tags")
|
div(tg-tag-line, editable="true", ng-model="task.tags")
|
||||||
|
|
||||||
fieldset
|
fieldset
|
||||||
textarea.description(placeholder="Type a short description", ng-model="task.description")
|
textarea.description(placeholder="Type a short description", ng-model="task.description")
|
||||||
|
|
|
@ -9,7 +9,7 @@ form
|
||||||
select(name="status", ng-model="us.status", ng-options="s.id as s.name for s in usStatusList",
|
select(name="status", ng-model="us.status", ng-options="s.id as s.name for s in usStatusList",
|
||||||
tg-i18n="placeholder:common.status")
|
tg-i18n="placeholder:common.status")
|
||||||
fieldset
|
fieldset
|
||||||
input(type="text", name="tags", placeholder="Tags", tg-tags, ng-model="us.tags")
|
div(tg-tag-line, editable="true", ng-model="us.tags")
|
||||||
|
|
||||||
fieldset
|
fieldset
|
||||||
textarea.description(name="description", ng-model="us.description",
|
textarea.description(name="description", ng-model="us.description",
|
||||||
|
|
|
@ -13,3 +13,33 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-autocomplete {
|
||||||
|
z-index: 99910;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid $gray-light;
|
||||||
|
.ui-state-focus {
|
||||||
|
background: $fresh-taiga;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-helper-hidden-accessible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-block {
|
||||||
|
.tags-container {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
display: inline-block;
|
||||||
|
padding: .4rem;
|
||||||
|
width: 14rem;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
@extend %small;
|
||||||
|
margin: 0 .5rem .5rem 0;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -114,23 +114,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-story-tags {
|
|
||||||
.tags-container {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
display: inline-block;
|
|
||||||
padding: .4rem;
|
|
||||||
width: 14rem;
|
|
||||||
}
|
|
||||||
.tag {
|
|
||||||
@extend %small;
|
|
||||||
margin: 0 .5rem .5rem 0;
|
|
||||||
padding: .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.us-activity-tabs {
|
.us-activity-tabs {
|
||||||
@extend %title;
|
@extend %title;
|
||||||
border-bottom: 3px solid $gray-light;
|
border-bottom: 3px solid $gray-light;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
@include table-flex();
|
@include table-flex();
|
||||||
width: 100%;
|
width: 100%;
|
||||||
&.show-tags {
|
&.show-tags {
|
||||||
.user-story-tags {
|
.tags-block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.user-story-tags {
|
.tags-block {
|
||||||
display: none;
|
display: none;
|
||||||
margin-bottom: .3rem;
|
margin-bottom: .3rem;
|
||||||
.tag {
|
.tag {
|
||||||
|
|
|
@ -165,7 +165,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.us-item-row {
|
.us-item-row {
|
||||||
.user-story-tags,
|
.tags-block,
|
||||||
.us-settings,
|
.us-settings,
|
||||||
.status,
|
.status,
|
||||||
.icon-drag-v,
|
.icon-drag-v,
|
||||||
|
|
Loading…
Reference in New Issue