Merge pull request #1036 from taigaio/us/4302/improve_tagging_system_v2

US #4302: Improve tagging system. Use generic colors for porject tags
stable
Juanfran 2016-08-23 12:46:05 +02:00 committed by GitHub
commit 36e4c2e650
65 changed files with 2068 additions and 528 deletions

2
.gitignore vendored
View File

@ -9,7 +9,7 @@ app/coffee/modules/locales/locale*.coffee
*.swp
*.swo
.#*
tags
/tags
tmp/
app/config/main.coffee
scss-lint.log

View File

@ -62,6 +62,7 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.project = {}
promise = @.loadInitialData()
@scope.projectTags = []
promise.then =>
sectionName = @translate.instant( @scope.sectionName)
@ -96,6 +97,11 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.issueTypesList = _.sortBy(project.issue_types, "order")
@scope.issueStatusList = _.sortBy(project.issue_statuses, "order")
@scope.$emit('project:loaded', project)
@scope.projectTags = _.map @scope.project.tags, (it) =>
return [it, @scope.project.tags_colors[it]]
return project
loadInitialData: ->
@ -107,6 +113,21 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin)
openDeleteLightbox: ->
@rootscope.$broadcast("deletelightbox:new", @scope.project)
addTag: (name, color) ->
tags = _.clone(@scope.project.tags)
tags.push(name)
@scope.projectTags.push([name, null])
@scope.project.tags = tags
deleteTag: (tag) ->
tags = _.clone(@scope.project.tags)
_.pull(tags, tag[0])
_.remove @scope.projectTags, (it) => it[0] == tag[0]
@scope.project.tags = tags
module.controller("ProjectProfileController", ProjectProfileController)

View File

@ -331,6 +331,10 @@ ColorSelectionDirective = () ->
## Color selection Link
link = ($scope, $el, $attrs, $model) ->
$scope.allowEmpty = false
if $attrs.tgAllowEmpty
$scope.allowEmpty = true
$ctrl = $el.controller()
$scope.$watch $attrs.ngModel, (element) ->
@ -696,59 +700,268 @@ module.directive("tgProjectCustomAttributes", ["$log", "$tgConfirm", "animationF
## Tags Controller
#############################################################################
class ProjectTagsController extends mixOf(taiga.Controller, taiga.PageMixin)
class ProjectTagsController extends taiga.Controller
@.$inject = [
"$scope",
"$rootScope",
"$tgRepo",
"tgAppMetaService",
"$translate"
"$tgConfirm",
"$tgResources",
"$tgModel",
]
constructor: (@scope, @rootscope, @repo, @appMetaService, @translate) ->
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @model) ->
@.loading = true
@rootscope.$on "project:loaded", =>
sectionName = @translate.instant(@scope.sectionName)
title = @translate.instant("ADMIN.CUSTOM_ATTRIBUTES.PAGE_TITLE", {
"sectionName": sectionName,
"projectName": @scope.project.name
})
description = @scope.project.description
@appMetaService.setAll(title, description)
@rootscope.$on("project:loaded", @.loadTags)
loadTags: =>
return @rs.projects.tagsColors(@scope.projectId).then (tags) =>
@scope.projectTagsAll = _.map(tags.getAttrs(), (color, name) => @model.make_model('tag', {name: name, color: color}))
@.filterAndSortTags()
@.loading = false
@.tagNames = Object.keys(@scope.project.tags_colors).sort()
@scope.projectTags = _.map(@.tagNames, (tagName) => {name: tagName, color: @scope.project.tags_colors[tagName]})
updateTag: (tag) ->
tags_colors = angular.copy(@scope.project.tags_colors)
tags_colors[tag.name] = tag.color
@scope.project.tags_colors = tags_colors
return @repo.save(@scope.project)
filterAndSortTags: =>
@scope.projectTags = _.filter(
_.sortBy(@scope.projectTagsAll, "name"),
(tag) => tag.name.indexOf(@scope.tagsFilter.name) != -1
)
deleteTag: (tag) =>
return @rs.projects.deleteTag(@scope.projectId, tag)
createTag: (tag, color) =>
return @rs.projects.createTag(@scope.projectId, tag, color)
editTag: (from_tag, to_tag, color) =>
if from_tag == to_tag
to_tag = null
return @rs.projects.editTag(@scope.projectId, from_tag, to_tag, color)
startMixingTags: (tag) =>
@scope.mixingTags.toTag = tag.name
toggleMixingFromTags: (tag) =>
if tag.name != @scope.mixingTags.toTag
index = @scope.mixingTags.fromTags.indexOf(tag.name)
if index == -1
@scope.mixingTags.fromTags.push(tag.name)
else
@scope.mixingTags.fromTags.splice(index, 1)
confirmMixingTags: () =>
toTag = @scope.mixingTags.toTag
fromTags = @scope.mixingTags.fromTags
@rs.projects.mixTags(@scope.projectId, toTag, fromTags).then =>
@.cancelMixingTags()
@.loadTags()
cancelMixingTags: () =>
@scope.mixingTags.toTag = null
@scope.mixingTags.fromTags = []
mixingClass: (tag) =>
if @scope.mixingTags.toTag != null
if tag.name == @scope.mixingTags.toTag
return "mixing-tags-to"
else if @scope.mixingTags.fromTags.indexOf(tag.name) != -1
return "mixing-tags-from"
module.controller("ProjectTagsController", ProjectTagsController)
#############################################################################
## Tags Directive
## Tags directive
#############################################################################
ProjectTagDirective = () ->
ProjectTagsDirective = ($log, $repo, $confirm, $location, animationFrame, $translate, $rootscope) ->
link = ($scope, $el, $attrs) ->
$el.color = $scope.tag.color
$window = $(window)
$ctrl = $el.controller()
valueType = $attrs.type
objName = $attrs.objname
initializeNewValue = ->
$scope.newValue = {
"name": ""
"color": ""
}
initializeTagsFilter = ->
$scope.tagsFilter = {
"name": ""
}
initializeMixingTags = ->
$scope.mixingTags = {
"toTag": null,
"fromTags": []
}
initializeTextTranslations = ->
$scope.addNewElementText = $translate.instant("ADMIN.PROJECT_VALUES_TAGS.ACTION_ADD")
initializeNewValue()
initializeTagsFilter()
initializeMixingTags()
initializeTextTranslations()
$rootscope.$on "$translateChangeEnd", ->
$scope.$evalAsync(initializeTextTranslations)
goToBottomList = (focus = false) =>
table = $el.find(".table-main")
$(document.body).scrollTop(table.offset().top + table.height())
if focus
$el.find(".new-value input:visible").first().focus()
saveValue = (target) ->
formEl = target.parents("form")
form = formEl.checksley()
return if not form.validate()
tag = formEl.scope().tag
originalTag = tag.clone()
originalTag.revert()
promise = $ctrl.editTag(originalTag.name, tag.name, tag.color)
promise.then =>
row = target.parents(".row.table-main")
row.addClass("hidden")
row.siblings(".visualization").removeClass('hidden')
$ctrl.loadTags()
promise.then null, (data) ->
form.setErrors(data)
saveNewValue = (target) ->
formEl = target.parents("form")
formEl = target
form = formEl.checksley()
return if not form.validate()
promise = $ctrl.createTag($scope.newValue.name, $scope.newValue.color)
promise.then (data) =>
target.addClass("hidden")
$ctrl.loadTags()
initializeNewValue()
promise.then null, (data) ->
form.setErrors(data)
cancel = (target) ->
row = target.parents(".row.table-main")
formEl = target.parents("form")
tag = formEl.scope().tag
$scope.$apply ->
row.addClass("hidden")
tag.revert()
row.siblings(".visualization").removeClass('hidden')
$scope.$watch "tagsFilter.name", (tagsFilter) ->
$ctrl.filterAndSortTags()
$window.on "keyup", (event) ->
if event.keyCode == 27
$scope.$apply ->
initializeMixingTags()
$el.on "click", ".show-add-new", (event) ->
event.preventDefault()
$el.find(".new-value").removeClass('hidden')
$el.on "click", ".add-new", debounce 2000, (event) ->
event.preventDefault()
target = $el.find(".new-value")
saveNewValue(target)
$el.on "click", ".delete-new", (event) ->
event.preventDefault()
$el.find(".new-value").addClass("hidden")
initializeNewValue()
$el.on "click", ".mix-tags", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
$scope.$apply ->
$ctrl.startMixingTags(target.parents('form').scope().tag)
$el.on "click", ".mixing-row", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
$scope.$apply ->
$ctrl.toggleMixingFromTags(target.parents('form').scope().tag)
$el.on "click", ".mixing-confirm", (event) ->
event.preventDefault()
event.stopPropagation()
$scope.$apply ->
$ctrl.confirmMixingTags()
$el.on "click", ".mixing-cancel", (event) ->
event.preventDefault()
event.stopPropagation()
$scope.$apply ->
$ctrl.cancelMixingTags()
$el.on "click", ".edit-value", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
row = target.parents(".row.table-main")
row.addClass("hidden")
editionRow = row.siblings(".edition")
editionRow.removeClass('hidden')
editionRow.find('input:visible').first().focus().select()
$el.on "keyup", ".new-value input", (event) ->
if event.keyCode == 13
target = $el.find(".new-value")
saveNewValue(target)
else if event.keyCode == 27
$el.find(".new-value").addClass("hidden")
initializeNewValue()
$el.on "keyup", ".status-name input", (event) ->
target = angular.element(event.currentTarget)
if event.keyCode == 13
saveValue(target)
else if event.keyCode == 27
cancel(target)
$el.on "click", ".save", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
saveValue(target)
$el.on "click", ".cancel", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
cancel(target)
$el.on "click", ".delete-tag", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
formEl = target.parents("form")
tag = formEl.scope().tag
title = $translate.instant("ADMIN.COMMON.TITLE_ACTION_DELETE_TAG")
$confirm.askOnDelete(title, tag.name).then (response) ->
onSucces = ->
$ctrl.loadTags().finally ->
response.finish()
onError = ->
$confirm.notify("error")
$ctrl.deleteTag(tag.name).then(onSucces, onError)
$scope.$on "$destroy", ->
$el.off()
$window.off()
$scope.$watch "tag.color", (newColor) =>
if $el.color != newColor
promise = $ctrl.updateTag($scope.tag)
promise.then null, (data) ->
form.setErrors(data)
return {link:link}
$el.color = newColor
return {link: link}
module.directive("tgProjectTag", [ProjectTagDirective])
module.directive("tgProjectTags", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "animationFrame", "$translate", "$rootScope", ProjectTagsDirective])

View File

@ -28,6 +28,7 @@ bindOnce = @.taiga.bindOnce
timeout = @.taiga.timeout
debounce = @.taiga.debounce
sizeFormat = @.taiga.sizeFormat
trim = @.taiga.trim
#############################################################################
## Common Lightbox Services
@ -295,6 +296,42 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
if attachment.get("id")
attachmentsToDelete = attachmentsToDelete.push(attachment)
$scope.addTag = (tag, color) ->
value = trim(tag.toLowerCase())
tags = $scope.project.tags
projectTags = $scope.project.tags_colors
tags = [] if not tags?
projectTags = {} if not projectTags?
if value not in tags
tags.push(value)
projectTags[tag] = color || null
$scope.project.tags = tags
itemtags = _.clone($scope.us.tags)
inserted = _.find itemtags, (it) -> it[0] == value
if !inserted
itemtags.push([tag , color])
$scope.us.tags = itemtags
$scope.deleteTag = (tag) ->
value = trim(tag[0].toLowerCase())
tags = $scope.project.tags
itemtags = _.clone($scope.us.tags)
_.remove itemtags, (tag) -> tag[0] == value
$scope.us.tags = itemtags
_.pull($scope.us.tags, value)
$scope.$on "usform:new", (ctx, projectId, status, statusList) ->
form.reset() if form
$scope.isNew = true

View File

@ -26,6 +26,7 @@ taiga = @.taiga
trim = @.taiga.trim
bindOnce = @.taiga.bindOnce
module = angular.module("taigaCommon")
# Directive that parses/format tags inputfield.
@ -61,28 +62,38 @@ ColorizeTagsDirective = ->
templates = {
backlog: _.template("""
<% _.each(tags, function(tag) { %>
<span class="tag" style="border-left: 5px solid <%- tag.color %>"><%- tag.name %></span>
<span class="tag"
<% if (tag[1] === null || tag[1] == undefined) { %>
style="border-left: 5px solid <%- tag[1] %>"
<% } %>
title="<%- tag[0] %>"><%- tag[0] %></span>
<% }) %>
""")
kanban: _.template("""
<% _.each(tags, function(tag) { %>
<a class="kanban-tag" href="" style="border-color: <%- tag.color %>" title="<%- tag.name %>" />
<a class="kanban-tag"
href=""
<% if (tag[1] === null || tag[1] == undefined) { %>
style="border-color: <%- tag[1] %>"
<% } %>
title="<%- tag[0] %>" />
<% }) %>
""")
taskboard: _.template("""
<% _.each(tags, function(tag) { %>
<a class="taskboard-tag" href="" style="border-color: <%- tag.color %>" title="<%- tag.name %>" />
<a class="taskboard-tag"
href=""
<% if (tag[1] === null || tag[1] == undefined) { %>
style="border-color: <%- tag[1] %>"
<% } %>
title="<%- tag[0] %>" />
<% }) %>
""")
}
link = ($scope, $el, $attrs, $ctrl) ->
render = (srcTags) ->
render = (tags) ->
template = templates[$attrs.tgColorizeTagsType]
srcTags.sort()
tags = _.map srcTags, (tag) ->
color = $scope.project.tags_colors[tag]
return {name: tag, color: color}
html = template({tags: tags})
$el.html(html)
@ -111,15 +122,18 @@ LbTagLineDirective = ($rs, $template, $compile) ->
autocomplete = null
link = ($scope, $el, $attrs, $model) ->
withoutColors = _.has($attrs, "withoutColors")
## Render
renderTags = (tags, tagsColors = []) ->
ctx = {
tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]})
}
color = if not withoutColors then tagsColors[t] else null
_.map ctx.tags, (tag) =>
if tag.color
tag.style = "border-left: 5px solid #{tag.color}"
ctx = {
tags: _.map(tags, (t) -> {
name: t,
style: if color then "border-left: 5px solid #{color}" else ""
})
}
html = $compile(templateTags(ctx))($scope)
$el.find(".tags-container").html(html)
@ -196,7 +210,7 @@ LbTagLineDirective = ($rs, $template, $compile) ->
autocomplete = new Awesomplete(input[0], {
list: _.keys(project.tags_colors)
});
})
input.on "awesomplete-selectcomplete", () ->
addValue(input.val())
@ -216,204 +230,3 @@ LbTagLineDirective = ($rs, $template, $compile) ->
}
module.directive("tgLbTagLine", ["$tgResources", "$tgTemplate", "$compile", LbTagLineDirective])
#############################################################################
## TagLine Directive (for detail pages)
#############################################################################
TagLineDirective = ($rootScope, $repo, $rs, $confirm, $modelTransform, $template, $compile) ->
ENTER_KEY = 13
ESC_KEY = 27
COMMA_KEY = 188
templateTags = $template.get("common/tag/tags-line-tags.html", true)
link = ($scope, $el, $attrs, $model) ->
autocomplete = null
loading = false
deleteTagLoading = null
isEditable = ->
if $attrs.requiredPerm?
return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
return true
## Render
renderTags = (tags, tagsColors) ->
ctx = {
tags: _.map(tags, (t) -> {name: t, color: tagsColors[t]})
isEditable: isEditable()
loading: loading
deleteTagLoading: deleteTagLoading
}
html = $compile(templateTags(ctx))($scope)
$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").focus()
hideInput = -> $el.find("input").addClass("hidden").blur()
resetInput = ->
$el.find("input").val("")
autocomplete.close()
## Aux methods
addValue = (value) ->
loading = true
value = trim(value.toLowerCase())
return if value.length == 0
renderTags($model.$modelValue.tags, $scope.project?.tags_colors)
transform = $modelTransform.save (item) ->
if not item.tags
item.tags = []
tags = _.clone(item.tags)
tags.push(value) if value not in tags
item.tags = tags
return item
onSuccess = ->
$rootScope.$broadcast("object:updated")
loading = false
renderTags($model.$modelValue.tags, $scope.project?.tags_colors)
onError = ->
$confirm.notify("error")
hideSaveButton()
return transform.then(onSuccess, onError)
deleteValue = (value) ->
value = trim(value.toLowerCase())
return if value.length == 0
deleteTagLoading = value
renderTags($model.$modelValue.tags, $scope.project?.tags_colors)
transform = $modelTransform.save (item) ->
tags = _.clone(item.tags, false)
item.tags = _.pull(tags, value)
return item
onSuccess = ->
$rootScope.$broadcast("object:updated")
renderTags($model.$modelValue.tags, $scope.project?.tags_colors)
deleteTagLoading = null
onError = ->
$confirm.notify("error")
deleteTagLoading = null
return transform.then(onSuccess, onError)
saveInputTag = () ->
value = $el.find("input").val()
addValue(value)
resetInput()
## Events
$el.on "keypress", "input", (event) ->
target = angular.element(event.currentTarget)
if event.keyCode == ENTER_KEY
saveInputTag()
else if String.fromCharCode(event.keyCode) == ','
event.preventDefault()
saveInputTag()
else
if target.val().length
showSaveButton()
else
hideSaveButton()
$el.on "keyup", "input", (event) ->
if event.keyCode == ESC_KEY
resetInput()
hideInput()
hideSaveButton()
showAddTagButton()
$el.on "click", ".save", (event) ->
event.preventDefault()
saveInputTag()
$el.on "click", ".add-tag", (event) ->
event.preventDefault()
hideAddTagButton()
showInput()
$el.on "click", ".remove-tag", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
value = target.siblings(".tag-name").text()
deleteValue(value)
$scope.$digest()
bindOnce $scope, "project.tags_colors", (tags_colors) ->
if not isEditable()
renderInReadModeOnly()
return
showAddTagButton()
input = $el.find("input")
autocomplete = new Awesomplete(input[0], {
list: _.keys(tags_colors)
});
input.on "awesomplete-selectcomplete", () ->
addValue(input.val())
input.val("")
$scope.$watchCollection () ->
return $model.$modelValue?.tags
, () ->
model = $model.$modelValue
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"
templateUrl: "common/tag/tag-line.html"
}
module.directive("tgTagLine", ["$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", "$tgQueueModelTransformation",
"$tgTemplate", "$compile", TagLineDirective])

View File

@ -25,6 +25,7 @@
taiga = @.taiga
bindOnce = @.taiga.bindOnce
debounce = @.taiga.debounce
trim = @.taiga.trim
module = angular.module("taigaIssues")
@ -76,6 +77,42 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading,
$scope.addAttachment = (attachment) ->
attachmentsToAdd = attachmentsToAdd.push(attachment)
$scope.addTag = (tag, color) ->
value = trim(tag.toLowerCase())
tags = $scope.project.tags
projectTags = $scope.project.tags_colors
tags = [] if not tags?
projectTags = {} if not projectTags?
if value not in tags
tags.push(value)
projectTags[tag] = color || null
$scope.project.tags = tags
itemtags = _.clone($scope.issue.tags)
inserted = _.find itemtags, (it) -> it[0] == value
if !inserted
itemtags.push([tag , color])
$scope.issue.tags = itemtags
$scope.deleteTag = (tag) ->
value = trim(tag[0].toLowerCase())
tags = $scope.project.tags
itemtags = _.clone($scope.us.tags)
_.remove itemtags, (tag) -> tag[0] == value
$scope.us.tags = itemtags
_.pull($scope.issue.tags, value)
submit = debounce 2000, (event) =>
event.preventDefault()
@ -101,7 +138,6 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading,
currentLoading.finish()
$confirm.notify("error")
submitButton = $el.find(".submit-button")
$el.on "submit", "form", submit

View File

@ -83,6 +83,34 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $translate) ->
service.tagsColors = (projectId) ->
return $repo.queryOne("projects", "#{projectId}/tags_colors")
service.deleteTag = (projectId, tag) ->
url = "#{$urls.resolve("projects")}/#{projectId}/delete_tag"
return $http.post(url, {tag: tag})
service.createTag = (projectId, tag, color) ->
url = "#{$urls.resolve("projects")}/#{projectId}/create_tag"
data = {}
data.tag = tag
data.color = null
if color
data.color = color
return $http.post(url, data)
service.editTag = (projectId, from_tag, to_tag, color) ->
url = "#{$urls.resolve("projects")}/#{projectId}/edit_tag"
data = {}
data.from_tag = from_tag
if to_tag
data.to_tag = to_tag
data.color = null
if color
data.color = color
return $http.post(url, data)
service.mixTags = (projectId, to_tag, from_tags) ->
url = "#{$urls.resolve("projects")}/#{projectId}/mix_tags"
return $http.post(url, {to_tag: to_tag, from_tags: from_tags})
service.export = (projectId) ->
url = "#{$urls.resolve("exporter")}/#{projectId}"
return $http.get(url)

View File

@ -25,6 +25,7 @@
taiga = @.taiga
bindOnce = @.taiga.bindOnce
debounce = @.taiga.debounce
trim = @.taiga.trim
CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxService, $translate, $q, attachmentsService) ->
link = ($scope, $el, attrs) ->
@ -56,6 +57,45 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
return $q.all(promises)
tagsToAdd = []
$scope.addTag = (tag, color) ->
value = trim(tag.toLowerCase())
tags = $scope.project.tags
projectTags = $scope.project.tags_colors
tags = [] if not tags?
projectTags = {} if not projectTags?
if value not in tags
tags.push(value)
projectTags[tag] = color || null
$scope.project.tags = tags
itemtags = _.clone($scope.task.tags)
inserted = _.find itemtags, (it) -> it[0] == value
if !inserted
itemtags.push([tag , color])
$scope.task.tags = itemtags
$scope.deleteTag = (tag) ->
value = trim(tag[0].toLowerCase())
tags = $scope.project.tags
itemtags = _.clone($scope.task.tags)
_.remove itemtags, (tag) -> tag[0] == value
$scope.task.tags = itemtags
_.pull($scope.task.tags, value)
$scope.$on "taskform:new", (ctx, sprintId, usId) ->
$scope.task = {
project: $scope.projectId

View File

@ -28,10 +28,12 @@ addClass = (el, className) ->
else
el.className += ' ' + className
nl2br = (str) =>
breakTag = '<br />'
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + breakTag + '$2')
bindMethods = (object) =>
dependencies = _.keys(object)
@ -43,6 +45,7 @@ bindMethods = (object) =>
_.bindAll(object, methods)
bindOnce = (scope, attr, continuation) =>
val = scope.$eval(attr)
if val != undefined
@ -75,6 +78,7 @@ slugify = (data) ->
.replace(/[^\w\-]+/g, '')
.replace(/\-\-+/g, '-')
unslugify = (data) ->
if data
return _.capitalize(data.replace(/-/g, ' '))
@ -165,6 +169,7 @@ sizeFormat = (input, precision=1) ->
size = (input / Math.pow(1024, number)).toFixed(precision)
return "#{size} #{units[number]}"
stripTags = (str, exception) ->
if exception
pattern = new RegExp('<(?!' + exception + '\s*\/?)[^>]+>', 'gi')
@ -172,6 +177,7 @@ stripTags = (str, exception) ->
else
return String(str).replace(/<\/?[^>]+>/g, '')
replaceTags = (str, tags, replace) ->
# open tag
pattern = new RegExp('<(' + tags + ')>', 'gi')
@ -183,6 +189,7 @@ replaceTags = (str, tags, replace) ->
return str
defineImmutableProperty = (obj, name, fn) =>
Object.defineProperty obj, name, {
get: () =>
@ -197,6 +204,7 @@ defineImmutableProperty = (obj, name, fn) =>
return fn_result
}
_.mixin
removeKeys: (obj, keys) ->
_.chain([keys]).flatten().reduce(
@ -211,13 +219,14 @@ _.mixin
, [ [] ])
isImage = (name) ->
return name.match(/\.(jpe?g|png|gif|gifv|webm)/i) != null
isPdf = (name) ->
return name.match(/\.(pdf)/i) != null
patch = (oldImmutable, newImmutable) ->
pathObj = {}
@ -230,6 +239,7 @@ patch = (oldImmutable, newImmutable) ->
return pathObj
taiga = @.taiga
taiga.addClass = addClass
taiga.nl2br = nl2br

View File

@ -426,7 +426,8 @@
"ADMIN": {
"COMMON": {
"TITLE_ACTION_EDIT_VALUE": "Edit value",
"TITLE_ACTION_DELETE_VALUE": "Delete value"
"TITLE_ACTION_DELETE_VALUE": "Delete value",
"TITLE_ACTION_DELETE_TAG": "Delete tag"
},
"HELP": "Do you need help? Check out our support page!",
"PROJECT_DEFAULT_VALUES": {
@ -582,9 +583,14 @@
},
"PROJECT_VALUES_TAGS": {
"TITLE": "Tags",
"SUBTITLE": "View and edit the color of your user stories",
"SUBTITLE": "View and edit the color of your tags",
"EMPTY": "Currently there are no tags",
"EMPTY_SEARCH": "It looks like nothing was found with your search criteria"
"EMPTY_SEARCH": "It looks like nothing was found with your search criteria",
"ACTION_ADD": "Add tag",
"NEW_TAG": "New tag",
"MIXING_HELP_TEXT": "Select the tags that you want to merge",
"MIXING_MERGE": "Merge Tags",
"SELECTED": "Selected"
},
"ROLES": {
"PAGE_TITLE": "Roles - {{projectName}}",

View File

@ -0,0 +1,57 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: color-selector.controller.coffee
###
module = angular.module('taigaCommon')
class ColorSelectorController
constructor: () ->
@.colorList = [
'#fce94f',
'#edd400',
'#c4a000',
'#8ae234',
'#73d216',
'#4e9a06',
'#d3d7cf',
'#fcaf3e',
'#f57900',
'#ce5c00',
'#729fcf',
'#3465a4',
'#204a87',
'#888a85',
'#ad7fa8',
'#75507b',
'#5c3566',
'#ef2929',
'#cc0000',
'#a40000'
]
@.displaycolorList = false
toggleColorList: () ->
@.displaycolorList = !@.displaycolorList
onSelectDropdownColor: (color) ->
@.onSelectColor({color: color})
@.toggleColorList()
module.controller("ColorSelectorCtrl", ColorSelectorController)

View File

@ -0,0 +1,60 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: color-selector.controller.spec.coffee
###
describe "ColorSelector", ->
provide = null
controller = null
colorSelectorCtrl = null
mocks = {}
_mocks = () ->
module ($provide) ->
provide = $provide
return null
beforeEach ->
module "taigaCommon"
_mocks()
inject ($controller) ->
controller = $controller
colorSelectorCtrl = controller "ColorSelectorCtrl"
colorSelectorCtrl.colorList = [
'#fce94f',
'#edd400',
'#c4a000',
]
colorSelectorCtrl.displaycolorList = false
it "display Color Selector", () ->
colorSelectorCtrl.toggleColorList()
expect(colorSelectorCtrl.displaycolorList).to.be.true
it "on select Color", () ->
colorSelectorCtrl.toggleColorList = sinon.stub()
color = '#FFFFFF'
colorSelectorCtrl.onSelectColor = sinon.spy()
colorSelectorCtrl.onSelectDropdownColor(color)
expect(colorSelectorCtrl.toggleColorList).have.been.called
expect(colorSelectorCtrl.onSelectColor).to.have.been.calledWith({color: color})

View File

@ -0,0 +1,61 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: color-selector.directive.coffee
###
module = angular.module('taigaCommon')
ColorSelectorDirective = ($timeout) ->
link = (scope, el) ->
timeout = null
cancel = () ->
$timeout.cancel(timeout)
timeout = null
close = () ->
return if timeout
timeout = $timeout (() ->
scope.vm.displaycolorList = false
), 400
el.find('.color-selector')
.mouseenter(cancel)
.mouseleave(close)
el.find('.color-selector-dropdown')
.mouseenter(cancel)
.mouseleave(close)
return {
link: link,
scope:{
onSelectColor: "&",
color: "="
},
templateUrl:"components/tags/color-selector/color-selector.html",
controller: "ColorSelectorCtrl",
controllerAs: "vm",
bindToController: true
}
ColorSelectorDirective.$inject = [
"$timeout"
]
module.directive("tgColorSelector", ColorSelectorDirective)

View File

@ -0,0 +1,30 @@
.color-selector
.tag-color.e2e-open-color-selector(
ng-click="vm.toggleColorList()"
ng-class="{'empty-color': !vm.color}"
ng-style="{'background': vm.color}"
)
.color-selector-dropdown(ng-show="vm.displaycolorList")
ul.color-selector-dropdown-list.e2e-color-dropdown
li.color-selector-option(
ng-repeat="color in vm.colorList"
ng-style="{'background': color}"
ng-title="color"
ng-click="vm.onSelectDropdownColor(color)"
)
li.empty-color(ng-click="vm.onSelectDropdownColor(null)")
.custom-color-selector
.display-custom-color.empty-color(
ng-if="!customColor.color || customColor.color.length < 7"
)
.display-custom-color(
ng-if="customColor.color.length === 7"
ng-style="{'background': customColor.color}"
ng-click="vm.onSelectDropdownColor(customColor.color)"
)
input.custom-color-input(
type="text"
maxlength="7"
placeholder="#000000"
ng-model="customColor.color"
)

View File

@ -0,0 +1,68 @@
@mixin color-selector-option {
border-radius: 2px;
cursor: pointer;
height: 2.25rem;
width: 2.25rem;
min-width: 2.25rem;
margin: 0 .5rem .5rem 0;
&:nth-child(7n) {
margin-right: 0;
}
}
.color-selector {
position: relative;
.tag-color {
@include color-selector-option;
border: 1px solid $gray-light;
border-left: 0;
border-radius: 0;
margin: 0;
transition: background .3s ease-out;
&.empty-color {
@include empty-color(34);
}
}
}
.color-selector-dropdown {
background: $blackish;
left: 0;
padding: 1rem;
position: absolute;
top: 2.25rem;
width: 332px;
z-index: 99;
}
.color-selector-dropdown-list {
display: flex;
flex-wrap: wrap;
list-style-type: none;
margin-bottom: 0;
.color-selector-option {
@include color-selector-option;
}
.empty-color {
@include color-selector-option;
@include empty-color(34);
}
}
.custom-color-selector {
display: flex;
.custom-color-input {
margin: 0;
width: 100%;
}
.display-custom-color {
@include color-selector-option;
flex-shrink: 0;
margin: 0;
margin-right: .5rem;
&.empty-color {
@include empty-color(34);
cursor: default;
}
}
}

View File

@ -0,0 +1,11 @@
a.add-tag-button.ng-animate-disabled.e2e-show-tag-input(
ng-if="!vm.addTag && vm.checkPermissions()"
href="#"
title="{{'COMMON.TAGS.ADD' | translate}}"
ng-click="vm.displayTagInput()"
)
tg-svg(
svg-icon="icon-add"
svg-title-translate="COMMON.TAGS.ADD"
)
span.add-tag-text(translate="COMMON.TAGS.ADD")

View File

@ -0,0 +1,33 @@
.add-tag-input(
novalidate
ng-if="vm.addTag && vm.checkPermissions()"
tg-loading="vm.loadingAddTag"
)
input.tag-input.e2e-add-tag-input(
type="text"
placeholder="{{'COMMON.TAGS.PLACEHOLDER' | translate}}"
autofocus
ng-model="vm.newTag.name"
ng-model-options="{debounce: 200}"
)
tg-tags-dropdown(
ng-if="!vm.disableColorSelection"
ng-show="vm.newTag.name.length",
color-array="vm.colorArray",
tag="vm.newTag",
on-select-tag="vm.addNewTag(name, color)"
)
tg-color-selector(
ng-if="!vm.disableColorSelection"
color="vm.newTag.color",
on-select-color="vm.selectColor(color)"
)
tg-svg.save(
ng-show="vm.newTag.name.length"
svg-icon="icon-save"
svg-title-translate="COMMON.TAGS.ADD"
ng-click="vm.addNewTag(vm.newTag.name, vm.newTag.color)"
)

View File

@ -0,0 +1,59 @@
$tag-input-width: 250px;
.add-tag-input {
align-items: flex-start;
display: flex;
flex-grow: 0;
flex-shrink: 0;
position: relative;
width: $tag-input-width;
input {
border-color: $gray-light;
padding: 6px;
width: 14rem;
}
.save {
cursor: pointer;
display: inline-block;
fill: $grayer;
margin: .5rem 0 0 .5rem;
transition: .2s linear;
&:hover {
fill: $primary;
}
}
.tags-dropdown {
@include font-size(small);
background: $white;
border: 1px solid $gray-light;
border-top: 0;
box-shadow: 2px 2px 3px rgba($black, .2);
left: 0;
max-height: 20vh;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
position: absolute;
top: 2.25rem;
width: 85%;
z-index: 99;
}
.tags-dropdown-option {
display: flex;
justify-content: space-between;
padding: .5rem;
}
.tags-dropdown-color {
height: 1rem;
width: 1rem;
}
li {
&:hover,
&.selected {
background: lighten($primary-light, 50%);
cursor: pointer;
transition: .2s;
transition-delay: .1s;
}
}
}

View File

@ -0,0 +1,84 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: tag-line.directive.coffee
###
module = angular.module('taigaCommon')
TagOptionDirective = () ->
select = (selected) ->
selected.addClass('selected')
selectedPosition = selected.position().top + selected.outerHeight()
containerHeight = selected.parent().outerHeight()
if selectedPosition > containerHeight
diff = selectedPosition - containerHeight
selected.parent().scrollTop(selected.parent().scrollTop() + diff)
else if selected.position().top < 0
selected.parent().scrollTop(selected.parent().scrollTop() + selected.position().top)
dispatch = (el, code, scope) ->
activeElement = el.find(".selected")
# Key: down
if code == 40
if not activeElement.length
select(el.find('li:first'))
else
next = activeElement.next('li')
if next.length
activeElement.removeClass('selected')
select(next)
# Key: up
else if code == 38
if not activeElement.length
select(el.find('li:last'))
else
prev = activeElement.prev('li')
if prev.length
activeElement.removeClass('selected')
select(prev)
stop = ->
$(document).off(".tags-keyboard-navigation")
link = (scope, el) ->
stop()
$(document).on "keydown.tags-keyboard-navigation", (event) =>
code = if event.keyCode then event.keyCode else event.which
if code == 40 || code == 38
event.preventDefault()
dispatch(el, code, scope)
scope.$on("$destroy", stop)
return {
link: link,
templateUrl:"components/tags/tag-dropdown/tag-dropdown.html",
scope: {
onSelectTag: "&",
colorArray: "=",
tag: "="
}
}
module.directive("tgTagsDropdown", TagOptionDirective)

View File

@ -0,0 +1,12 @@
ul.tags-dropdown
li(
ng-repeat="tag in colorArray | filter: tag.name",
ng-click="onSelectTag({name: tag[0], color: tag[1]})"
)
.tags-dropdown-option
span.tags-dropdown-name {{tag[0]}}
span.tags-dropdown-color(
ng-if="tag[1]"
ng-style="{'background': tag[1]}"
ng-title="tag[1]"
)

View File

@ -0,0 +1,56 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: tag-line.controller.coffee
###
trim = @.taiga.trim
module = angular.module('taigaCommon')
class TagLineCommonController
@.$inject = [
"tgTagLineService"
]
constructor: (@tagLineService) ->
@.newTag = {name: "", color: null}
@.colorArray = []
@.addTag = false
checkPermissions: () ->
return @tagLineService.checkPermissions(@.project.my_permissions, @.permissions)
_createColorsArray: (projectTagColors) ->
@.colorArray = @tagLineService.createColorsArray(projectTagColors)
displayTagInput: () ->
@.addTag = true
addNewTag: (name, color) ->
@.newTag.name = ""
@.newTag.color = null
if @.project.tags_colors[name]
color = @.project.tags_colors[name]
@.onAddTag({name: name, color: color}) if name.length
selectColor: (color) ->
@.newTag.color = color
module.controller("TagLineCommonCtrl", TagLineCommonController)

View File

@ -0,0 +1,96 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File:tag-line-common.controller.spec.coffee
###
describe "TagLineCommon", ->
provide = null
controller = null
TagLineCommonCtrl = null
mocks = {}
_mockTgTagLineService = () ->
mocks.tgTagLineService = {
checkPermissions: sinon.stub()
createColorsArray: sinon.stub()
renderTags: sinon.stub()
}
provide.value "tgTagLineService", mocks.tgTagLineService
_mocks = () ->
module ($provide) ->
provide = $provide
_mockTgTagLineService()
return null
beforeEach ->
module "taigaCommon"
_mocks()
inject ($controller) ->
controller = $controller
TagLineCommonCtrl = controller "TagLineCommonCtrl"
TagLineCommonCtrl.tags = []
TagLineCommonCtrl.colorArray = []
TagLineCommonCtrl.addTag = false
it "check permissions", () ->
TagLineCommonCtrl.project = {
}
TagLineCommonCtrl.project.my_permissions = [
'permission1',
'permission2'
]
TagLineCommonCtrl.permissions = 'permissions1'
TagLineCommonCtrl.checkPermissions()
expect(mocks.tgTagLineService.checkPermissions).have.been.calledWith(TagLineCommonCtrl.project.my_permissions, TagLineCommonCtrl.permissions)
it "create Colors Array", () ->
projectTagColors = 'string'
mocks.tgTagLineService.createColorsArray.withArgs(projectTagColors).returns(true)
TagLineCommonCtrl._createColorsArray(projectTagColors)
expect(TagLineCommonCtrl.colorArray).to.be.equal(true)
it "display tag input", () ->
TagLineCommonCtrl.addTag = false
TagLineCommonCtrl.displayTagInput()
expect(TagLineCommonCtrl.addTag).to.be.true
it "on add tag", () ->
TagLineCommonCtrl.loadingAddTag = true
tag = 'tag1'
tags = ['tag1', 'tag2']
color = "CC0000"
TagLineCommonCtrl.project = {
tags: ['tag1', 'tag2'],
tags_colors: ["#CC0000", "CCBB00"]
}
TagLineCommonCtrl.onAddTag = sinon.spy()
TagLineCommonCtrl.newTag = {name: "11", color: "22"}
TagLineCommonCtrl.addNewTag(tag, color)
expect(TagLineCommonCtrl.onAddTag).have.been.calledWith({name: tag, color: color})
expect(TagLineCommonCtrl.newTag.name).to.be.eql("")
expect(TagLineCommonCtrl.newTag.color).to.be.null

View File

@ -0,0 +1,69 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: tag-line.directive.coffee
###
module = angular.module('taigaCommon')
TagLineCommonDirective = () ->
link = (scope, el, attr, ctrl) ->
if !_.isUndefined(attr.disableColorSelection)
ctrl.disableColorSelection = true
unwatch = scope.$watch "vm.project", (project) ->
return if !project || !Object.keys(project).length
unwatch()
ctrl.colorArray = ctrl._createColorsArray(ctrl.project.tags_colors)
el.on "keydown", ".tag-input", (event) ->
if event.keyCode == 27 && ctrl.newTag.name.length
ctrl.addTag = false
ctrl.newTag.name = ""
ctrl.newTag.color = ""
event.stopPropagation()
else if event.keyCode == 13
event.preventDefault()
if el.find('.tags-dropdown .selected').length
tagName = $('.tags-dropdown .selected .tags-dropdown-name').text()
ctrl.addNewTag(tagName, null)
else
ctrl.addNewTag(ctrl.newTag.name, ctrl.newTag.color)
scope.$apply()
return {
link: link,
scope: {
permissions: "@",
loadingAddTag: "=",
loadingRemoveTag: "=",
tags: "=",
project: "=",
onAddTag: "&",
onDeleteTag: "&"
},
templateUrl:"components/tags/tag-line-common/tag-line-common.html",
controller: "TagLineCommonCtrl",
controllerAs: "vm",
bindToController: true
}
module.directive("tgTagLineCommon", TagLineCommonDirective)

View File

@ -0,0 +1,26 @@
.tags-container
.tag(
ng-if="tag[1]"
ng-repeat="tag in vm.tags"
ng-style="{'border-left': '.3rem solid' + tag[1]}"
)
tg-tag(
tag="tag"
loading-remove-tag="vm.loadingRemoveTag"
project="vm.project"
on-delete-tag="vm.onDeleteTag({tag: tag})"
has-permissions="{{vm.checkPermissions()}}"
)
.tag(
ng-if="!tag[1]"
ng-repeat="tag in vm.tags"
)
tg-tag(
tag="tag"
loading-remove-tag="vm.loadingRemoveTag"
on-delete-tag="vm.onDeleteTag({tag: tag})"
has-permissions="{{vm.checkPermissions()}}"
)
include ../components/add-tag-button
include ../components/add-tag-input

View File

@ -0,0 +1,88 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: tag-line.controller.coffee
###
trim = @.taiga.trim
module = angular.module('taigaCommon')
class TagLineController
@.$inject = [
"$rootScope",
"$tgConfirm",
"$tgQueueModelTransformation",
]
constructor: (@rootScope, @confirm, @modelTransform) ->
@.loadingAddTag = false
onDeleteTag: (tag) ->
@.loadingRemoveTag = tag[0]
onDeleteTagSuccess = (item) =>
@rootScope.$broadcast("object:updated")
@.loadingRemoveTag = false
return item
onDeleteTagError = () =>
@confirm.notify("error")
@.loadingRemoveTag = false
tagName = trim(tag[0].toLowerCase())
transform = @modelTransform.save (item) ->
itemtags = _.clone(item.tags)
_.remove itemtags, (tag) -> tag[0] == tagName
item.tags = itemtags
return item
return transform.then(onDeleteTagSuccess, onDeleteTagError)
onAddTag: (tag, color) ->
@.loadingAddTag = true
onAddTagSuccess = (item) =>
@rootScope.$broadcast("object:updated") #its a kind of magic.
@.addTag = false
@.loadingAddTag = false
return item
onAddTagError = () =>
@.loadingAddTag = false
@confirm.notify("error")
transform = @modelTransform.save (item) =>
value = trim(tag.toLowerCase())
itemtags = _.clone(item.tags)
itemtags.push([tag , color])
item.tags = itemtags
return item
return transform.then(onAddTagSuccess, onAddTagError)
module.controller("TagLineCtrl", TagLineController)

View File

@ -0,0 +1,157 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File:tag-line-detail.controller.spec.coffee
###
describe "TagLineDetail", ->
provide = null
controller = null
TagLineController = null
mocks = {}
_mockRootScope = () ->
mocks.rootScope = {
$broadcast: sinon.stub()
}
provide.value "$rootScope", mocks.rootScope
_mockTgConfirm = () ->
mocks.tgConfirm = {
notify: sinon.stub()
}
provide.value "$tgConfirm", mocks.tgConfirm
_mockTgQueueModelTransformation = () ->
mocks.tgQueueModelTransformation = {
save: sinon.stub()
}
provide.value "$tgQueueModelTransformation", mocks.tgQueueModelTransformation
_mocks = () ->
module ($provide) ->
provide = $provide
_mockRootScope()
_mockTgConfirm()
_mockTgQueueModelTransformation()
return null
beforeEach ->
module "taigaCommon"
_mocks()
inject ($controller) ->
controller = $controller
TagLineController = controller "TagLineCtrl"
it "on delete tag success", (done) ->
tag = {
name: 'tag1'
}
tagName = tag.name
item = {
tags: [
['tag1'],
['tag2'],
['tag3']
]
}
mocks.tgQueueModelTransformation.save.callsArgWith(0, item)
mocks.tgQueueModelTransformation.save.promise().resolve(item)
TagLineController.onDeleteTag(['tag1', '#000']).then (item) ->
expect(item.tags).to.be.eql([
['tag2'],
['tag3']
])
expect(TagLineController.loadingRemoveTag).to.be.false
expect(mocks.rootScope.$broadcast).to.be.calledWith("object:updated")
done()
it "on delete tag error", (done) ->
mocks.tgQueueModelTransformation.save.promise().reject(new Error('error'))
TagLineController.onDeleteTag(['tag1']).finally () ->
expect(TagLineController.loadingRemoveTag).to.be.false
expect(mocks.tgConfirm.notify).to.be.calledWith("error")
done()
it "on add tag success", (done) ->
tag = 'tag1'
tagColor = '#eee'
item = {
tags: [
['tag2'],
['tag3']
]
}
mockPromise = mocks.tgQueueModelTransformation.save.promise()
mocks.tgQueueModelTransformation.save.callsArgWith(0, item)
promise = TagLineController.onAddTag(tag, tagColor)
expect(TagLineController.loadingAddTag).to.be.true
mockPromise.resolve(item)
promise.then (item) ->
expect(item.tags).to.be.eql([
['tag2'],
['tag3'],
['tag1', '#eee']
])
expect(mocks.rootScope.$broadcast).to.be.calledWith("object:updated")
expect(TagLineController.addTag).to.be.false
expect(TagLineController.loadingAddTag).to.be.false
done()
it "on add tag error", (done) ->
tag = 'tag1'
tagColor = '#eee'
item = {
tags: [
['tag2'],
['tag3']
]
}
mockPromise = mocks.tgQueueModelTransformation.save.promise()
mocks.tgQueueModelTransformation.save.callsArgWith(0, item)
promise = TagLineController.onAddTag(tag, tagColor)
expect(TagLineController.loadingAddTag).to.be.true
mockPromise.reject(new Error('error'))
promise.then (item) ->
expect(TagLineController.loadingAddTag).to.be.false
expect(mocks.tgConfirm.notify).to.be.calledWith("error")
done()

View File

@ -0,0 +1,35 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: tag-line.directive.coffee
###
module = angular.module('taigaCommon')
TagLineDirective = () ->
return {
scope: {
item: "=",
permissions: "@",
project: "="
},
templateUrl:"components/tags/tag-line-detail/tag-line-detail.html",
controller: "TagLineCtrl",
controllerAs: "vm",
bindToController: true
}
module.directive("tgTagLine", TagLineDirective)

View File

@ -0,0 +1,9 @@
tg-tag-line-common.tags-block(
project="vm.project"
tags="vm.item.tags"
permissions="{{vm.permissions}}"
loading-remove-tag="vm.loadingRemoveTag"
loading-add-tag="vm.loadingAddTag"
on-add-tag="vm.onAddTag(name, color)"
on-delete-tag="vm.onDeleteTag(tag)"
)

View File

@ -0,0 +1,22 @@
.tags-block {
align-content: center;
display: flex;
flex-wrap: wrap;
}
.add-tag-button {
color: $gray-light;
cursor: pointer;
display: inline-block;
&:hover {
color: $primary-light;
}
.icon-add {
@include svg-size(.9rem);
fill: currentColor;
margin: .5rem .25rem 0 0;
}
.add-tag-text {
@include font-size(small);
}
}

View File

@ -0,0 +1,35 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: tag-line.service.coffee
###
module = angular.module('taigaCommon')
class TagLineService extends taiga.Service
@.$inject = []
constructor: () ->
checkPermissions: (myPermissions, projectPermissions) ->
return _.includes(myPermissions, projectPermissions)
createColorsArray: (projectTagColors) ->
return _.map(projectTagColors, (index, value) ->
return [value, index]
)
module.service("tgTagLineService", TagLineService)

View File

@ -0,0 +1,33 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: tag-line.directive.coffee
###
module = angular.module('taigaCommon')
TagDirective = () ->
return {
templateUrl:"components/tags/tag/tag.html",
scope: {
tag: "<",
loadingRemoveTag: "<",
onDeleteTag: "&",
hasPermissions: "@"
}
}
module.directive("tgTag", TagDirective)

View File

@ -0,0 +1,8 @@
span {{ tag[0] }}
tg-svg.icon-close.e2e-delete-tag(
ng-if="hasPermissions"
svg-icon="icon-close"
svg-title-translate="COMMON.TAG.DELETE"
ng-click="onDeleteTag(tag)"
tg-loading="loadingRemoveTag == tag[0]"
)

View File

@ -0,0 +1,21 @@
.tag {
@include font-type(light);
@include font-size(small);
background: $mass-white;
border-radius: 0 5px 5px 0;
color: $grayer;
display: inline-block;
margin: 0 .5rem .5rem 0;
padding: .5rem;
text-align: center;
.icon-close {
@include svg-size(.7rem);
cursor: pointer;
fill: $red-light;
margin-left: .25rem;
}
.loading-spinner {
height: 1rem;
width: 1rem;
}
}

View File

@ -5,12 +5,6 @@ section.home-project-list(ng-if="vm.projects.size")
tg-repeat="project in vm.projects"
ng-class="{'blocked-project': project.get('blocked_code')}"
)
.tags-container
.project-tag(
style="background: {{tag.get('color')}}"
title="{{tag.get('name')}}"
tg-repeat="tag in project.get('colorized_tags') track by tag.get('name')"
)
.project-card-inner(
href="#"
tg-nav="project:project=project.get('slug')"

View File

@ -43,11 +43,8 @@ div.wrapper
p.description {{vm.project.get('description')}}
div.single-project-tags.tags-container(ng-if="::vm.project.get('tags').size")
span.tag(
style='border-left: 5px solid {{::tag.get("color")}};',
tg-repeat="tag in ::vm.project.get('colorized_tags')"
)
span.tag-name {{::tag.get('name')}}
span.tag(tg-repeat="tag in ::vm.project.get('tags')")
span.tag-name {{::tag}}
div.project-data
section.timeline(ng-if="vm.project")
@ -60,7 +57,9 @@ div.wrapper
title="{{'PROJECT.LOOKING_FOR_PEOPLE' | translate}}"
)
h3 {{'PROJECT.LOOKING_FOR_PEOPLE' | translate}}
p(ng-if="vm.project.get('looking_for_people_note')") {{::vm.project.get('looking_for_people_note')}}
p(ng-if="vm.project.get('looking_for_people_note')")
| {{::vm.project.get('looking_for_people_note')}}
h2.title {{"PROJECT.SECTION.TEAM" | translate}}
ul.involved-team
li(tg-repeat="member in vm.members")

View File

@ -20,6 +20,7 @@
taiga = @.taiga
groupBy = @.taiga.groupBy
class ProjectsService extends taiga.Service
@.$inject = ["tgResources", "$projectUrl", "tgLightboxFactory"]
@ -42,16 +43,6 @@ class ProjectsService extends taiga.Service
url = @projectUrl.get(project.toJS())
project = project.set("url", url)
colorized_tags = []
if project.get("tags")
tags = project.get("tags").sort()
colorized_tags = tags.map (tag) ->
color = project.get("tags_colors").get(tag)
return Immutable.fromJS({name: tag, color: color})
project = project.set("colorized_tags", colorized_tags)
return project

View File

@ -129,8 +129,7 @@ describe "tgProjectsService", ->
id: 2,
url: 'url-2',
tags: ['xx', 'yy', 'aa'],
tags_colors: {xx: "red", yy: "blue", aa: "white"},
colorized_tags: [{name: 'aa', color: 'white'}, {name: 'xx', color: 'red'}, {name: 'yy', color: 'blue'}]
tags_colors: {xx: "red", yy: "blue", aa: "white"}
}
)
@ -157,8 +156,7 @@ describe "tgProjectsService", ->
id: 2,
url: 'url-2',
tags: ['xx', 'yy', 'aa'],
tags_colors: {xx: "red", yy: "blue", aa: "white"},
colorized_tags: [{name: 'aa', color: 'white'}, {name: 'xx', color: 'red'}, {name: 'yy', color: 'blue'}]
tags_colors: {xx: "red", yy: "blue", aa: "white"}
}
])

View File

@ -72,10 +72,15 @@ div.wrapper(
)
fieldset
label(for="tags") {{ 'ADMIN.PROJECT_PROFILE.TAGS' | translate }}
div.tags-block(
ng-if="project.id"
tg-lb-tag-line
ng-model="project.tags"
tg-tag-line-common.tags-block(
disable-color-selection
ng-if="project"
project="project"
tags="projectTags"
permissions="modify_project"
on-add-tag="ctrl.addTag(name, color)"
on-delete-tag="ctrl.deleteTag(tag)"
)
fieldset(ng-if="project.owner.id != user.id")

View File

@ -13,55 +13,20 @@ div.wrapper(
sidebar.menu-tertiary.sidebar(tg-admin-navigation="values-tags")
include ../includes/modules/admin-submenu-project-values
section.main.admin-common.admin-attributes.colors-table
include ../includes/components/mainTitle
p.admin-subtitle(translate="ADMIN.PROJECT_VALUES_TAGS.SUBTITLE")
.admin-attributes-section(
ng-controller="ProjectTagsController as ctrl"
)
.admin-attributes-section-wrapper-empty(
ng-if="!projectTags.length"
tg-loading="ctrl.loading"
)
p(translate="ADMIN.PROJECT_VALUES_TAGS.EMPTY")
.admin-attributes-section-wrapper(
ng-if="projectTags.length"
)
.table-header.table-tags-editor
.row
.color-column(translate="COMMON.FIELDS.COLOR")
.color-name(translate="COMMON.FIELDS.NAME")
.color-filter
input.e2e-tags-filter(
type="text"
name="name"
ng-model="tagsFilter.name"
ng-model-options="{debounce: 200}"
)
tg-svg(
svg-icon="icon-search"
)
p.admin-attributes-section-wrapper-empty(
tg-loading="ctrl.loading"
translate="ADMIN.PROJECT_VALUES_TAGS.EMPTY_SEARCH"
ng-if="!(projectTags | filter:tagsFilter).length"
section.main.admin-common.admin-attributes.colors-table.tags-table(
tg-project-tags,
ng-controller="ProjectTagsController as ctrl"
)
header.header-with-actions
.title
include ../includes/components/mainTitle
p.admin-subtitle(translate="ADMIN.PROJECT_VALUES_TAGS.SUBTITLE")
.action-buttons
a.button.button-green.show-add-new(
href=""
title="{{ 'ADMIN.PROJECT_VALUES_TAGS.NEW_TAG'|translate }}"
translate="ADMIN.PROJECT_VALUES_TAGS.NEW_TAG"
)
.table-main(
ng-repeat="tag in projectTags | filter:tagsFilter"
tg-bind-scope
)
form(
tg-project-tag
ng-model="tag"
)
.row.edition.no-draggable
.color-column(
tg-color-selection
ng-model="tag"
)
.current-color(ng-style="{background: tag.color}")
include ../includes/components/select-color
.color-name {{ tag.name }}
.admin-attributes-section
include ../includes/modules/admin/project-tags

View File

@ -1,5 +1,8 @@
<% _.each(tags, function(tag) { %>
span(class="tag", style!="<%- tag.style %>")
span(
class="tag"
style!="<%- tag.style %>"
)
span.tag-name <%- tag.name %>
a.remove-tag(href="", title="{{'COMMON.TAGS.DELETE' | translate}}")
tg-svg(svg-icon="icon-close")

View File

@ -2,7 +2,7 @@
<% if (tag.name == deleteTagLoading) { %>
div(tg-loading="true")
<% } else { %>
span.tag(style!="border-left: 5px solid <%- tag.color %>;")
span.tag(style!="border-left: 5px solid <%- tag.style %>;")
span.tag-name <%- tag.name %>
<% if (isEditable) { %>
a.remove-tag(

View File

@ -20,7 +20,9 @@ div.popover.select-color
li.color(style="background: #ef2929", data-color="#ef2929")
li.color(style="background: #cc0000", data-color="#cc0000")
li.color(style="background: #a40000", data-color="#a40000")
li.color(style="background: #2e3436", data-color="#2e3436")
li.color(style="background: #2e3436", data-color="#2e3436", ng-if="!allowEmpty")
li.color(data-color="", ng-class="{'empty-color': allowEmpty}")
input(type="text", placeholder="personalized colors", ng-model="color")
div.selected-color(ng-style="{'background-color': color}")
div.selected-color(ng-style="{'background-color': color}", ng-if="color !== null")
div.selected-color(ng-style="{'background-color': none}", ng-if="color === null")

View File

@ -0,0 +1,175 @@
section
.admin-tags-section-wrapper-empty(
ng-show="!projectTagsAll.length"
tg-loading="ctrl.loading"
)
p(translate="ADMIN.PROJECT_VALUES_TAGS.EMPTY")
.admin-tags-section-wrapper(
ng-show="projectTagsAll.length"
)
form.add-tag-container.new-value.hidden
tg-color-selection.color-column(
tg-allow-empty="true"
ng-model="newValue"
)
.current-color(
ng-style="{background: newValue.color}"
ng-if="newValue.color"
)
.current-color.empty-color(ng-if="!newValue.color")
include ../../components/select-color
.tag-name
input(
name="name"
type="text"
placeholder="{{'ADMIN.TYPES.PLACEHOLDER_WRITE_NAME' | translate}}",
ng-model="newValue.name"
data-required="true"
data-maxlength="255"
)
.options-column
a.add-new.e2e-save(href="")
tg-svg(
title="{{'COMMON.ADD' | translate}}",
svg-icon="icon-save"
)
a.delete-new(href="")
tg-svg(
title="{{'COMMON.CANCEL' | translate}}",
svg-icon="icon-close"
)
.table-header.table-tags-editor
div.row.header-tag-row
.color-column(translate="COMMON.FIELDS.COLOR")
.status-name(translate="COMMON.FIELDS.NAME")
.color-filter
input.e2e-tags-filter(
type="text"
name="name"
ng-model="tagsFilter.name"
ng-model-options="{debounce: 200}"
)
tg-svg(
svg-icon="icon-search"
)
.table-main.table-admin-tags
div(ng-show="!mixingTags.toTag")
.admin-attributes-section-wrapper-empty(
ng-show="!projectTags.length"
tg-loading="ctrl.loading"
)
p(translate="ADMIN.PROJECT_VALUES_TAGS.EMPTY_SEARCH")
p lalalaal
div(
ng-repeat="tag in projectTags"
tg-bind-scope
)
form(tg-bind-scope)
.row.tag-row.table-main.visualization(ng-class="{{ ctrl.mixingClass(tag) }}")
.color-column
.current-color(
ng-style="{background: tag.color}"
ng-if="tag.color"
)
.current-color.empty-color(ng-if="!tag.color")
.status-name
span(tg-bo-html="tag.name")
.options-column
a.mix-tags(href="")
tg-svg(
title="{{'ADMIN.PROJECT_VALUES_TAGS.MIXING_MERGE' | translate}}"
svg-icon="icon-merge"
)
div.popover.merge-explanation
span(translate="ADMIN.PROJECT_VALUES_TAGS.MIXING_MERGE")
a.edit-value(href="")
tg-svg(
svg-icon="icon-edit"
title="{{'ADMIN.COMMON.TITLE_ACTION_EDIT_VALUE' | translate}}"
)
a.delete-tag(href="")
tg-svg(
svg-icon="icon-trash"
title="{{'ADMIN.COMMON.TITLE_ACTION_DELETE_VALUE' | translate}}"
)
.row.tag-row.table-main.edition.hidden
.color-column(
tg-color-selection
tg-allow-empty="true"
ng-model="tag"
)
.current-color(
ng-style="{background: tag.color}"
ng-if="tag.color"
)
.current-color.empty-color(ng-if="!tag.color")
include ../../components/select-color
.status-name
input(
name="name"
type="text"
placeholder="{{'ADMIN.TYPES.PLACEHOLDER_WRITE_NAME' | translate}}",
ng-model="tag.name"
data-required="true"
data-maxlength="255"
)
.options-column
a.save.e2e-save(href="")
tg-svg(
title="{{'COMMON.SAVE' | translate}}"
svg-icon="icon-save"
)
a.cancel(href="")
tg-svg(
title="{{'COMMON.CANCEL' | translate}}"
svg-icon="icon-close"
)
div(ng-show="mixingTags.toTag")
div(
ng-repeat="tag in projectTags"
tg-bind-scope
)
form(tg-bind-scope)
.row.mixing-row.table-main.visualization(class="{{ ctrl.mixingClass(tag) }}")
.color-column
.current-color(ng-style="{background: tag.color}")
.status-name
span(tg-bo-html="tag.name")
.mixing-options-column
.mixing-help-text(
ng-if="mixingTags.toTag === tag.name"
translate="ADMIN.PROJECT_VALUES_TAGS.MIXING_HELP_TEXT"
)
a.mixing-confirm.button-green(
href=""
ng-if="mixingTags.toTag === tag.name && mixingTags.fromTags.length"
translate="ADMIN.PROJECT_VALUES_TAGS.MIXING_MERGE"
)
a.mixing-cancel.button-gray(
href=""
ng-if="mixingTags.toTag === tag.name"
translate="COMMON.CANCEL"
)
span.mixing-selected(
ng-if="mixingTags.fromTags.indexOf(tag.name) !== -1"
)
tg-svg(
title="{{'ADMIN.PROJECT_VALUES_TAGS.SELECTED' | translate}}"
svg-icon="icon-merge"
)

View File

@ -29,9 +29,13 @@ form
)
fieldset
.tags-block(
tg-lb-tag-line
ng-model="issue.tags"
tg-tag-line-common.tags-block(
ng-if="project"
project="project"
tags="issue.tags"
permissions="add_issue"
on-add-tag="addTag(name, color)"
on-delete-tag="deleteTag(tag)"
)
fieldset

View File

@ -30,9 +30,13 @@ form
)
fieldset
div.tags-block(
tg-lb-tag-line
ng-model="task.tags"
tg-tag-line-common.tags-block(
ng-if="project"
project="project"
tags="task.tags"
permissions="add_task"
on-add-tag="addTag(name, color)"
on-delete-tag="deleteTag(tag)"
)
fieldset

View File

@ -24,9 +24,13 @@ form
)
fieldset
div.tags-block(
tg-lb-tag-line
ng-model="us.tags"
tg-tag-line-common.tags-block(
ng-if="project"
project="project"
tags="us.tags"
permissions="add_us"
on-add-tag="addTag(name, color)"
on-delete-tag="deleteTag(tag)"
)
fieldset

View File

@ -64,10 +64,11 @@ div.wrapper(
svg-icon="icon-arrow-right"
)
.subheader
.tags-block(
tg-tag-line
ng-model="issue"
required-perm="modify_issue"
tg-tag-line.tags-block(
ng-if="issue && project"
project="project"
item="issue"
permissions="modify_issue"
)
tg-created-by-display.ticket-created-by(ng-model="issue")
@ -92,7 +93,7 @@ div.wrapper(
project-id="projectId"
edit-permission = "modify_issue"
)
tg-history-section(
ng-if="issue"
type="issue"

View File

@ -75,7 +75,12 @@ div.wrapper(
)
tg-svg(svg-icon="icon-arrow-right")
.subheader
div.tags-block(tg-tag-line, ng-model="task", required-perm="modify_task")
tg-tag-line.tags-block(
ng-if="task && project"
project="project"
item="task"
permissions="modify_task"
)
tg-created-by-display.ticket-created-by(ng-model="task")
section.duty-content(tg-editable-description, tg-editable-wysiwyg, ng-model="task", required-perm="modify_task")
@ -94,7 +99,7 @@ div.wrapper(
project-id="projectId"
edit-permission = "modify_task"
)
tg-history-section(
ng-if="task"
type="task"

View File

@ -68,7 +68,12 @@ div.wrapper(
)
tg-svg(svg-icon="icon-arrow-right")
.subheader
.tags-block(tg-tag-line, ng-model="us", required-perm="modify_us")
tg-tag-line.tags-block(
ng-if="us && project"
project="project"
item="us"
permissions="modify_us"
)
tg-created-by-display.ticket-created-by(ng-model="us")
section.duty-content(tg-editable-description, tg-editable-wysiwyg, ng-model="us", required-perm="modify_us")

View File

@ -19,6 +19,9 @@
height: 35px;
width: 35px;
}
.empty-color {
@include empty-color(33);
}
ul {
float: left;
margin-bottom: 1rem;

View File

@ -1,108 +0,0 @@
.tag {
@include font-type(light);
@include font-size(small);
background: $mass-white;
border-radius: 0 5px 5px 0;
color: $grayer;
display: inline-block;
margin: 0 .5rem .5rem 0;
padding: .5rem;
text-align: center;
.icon-delete {
color: $gray-light;
margin-left: 1rem;
&:hover {
color: $red;
}
}
}
.ui-autocomplete {
background: $white;
border: 1px solid $gray-light;
z-index: 99910;
.ui-state-focus {
background: $primary-light;
}
li {
cursor: pointer;
}
}
.ui-helper-hidden-accessible {
display: none;
}
.tags-block {
align-content: center;
display: flex;
flex-wrap: wrap;
.tags-container {
align-items: center;
display: flex;
flex-wrap: wrap;
}
.add-tag-input {
align-items: flex-start;
display: flex;
flex-grow: 0;
flex-shrink: 0;
width: 250px;
.icon-save {
margin-top: .5rem;
}
}
input {
margin-right: .25rem;
padding: .4rem;
width: 14rem;
}
.save {
cursor: pointer;
display: inline-block;
margin-left: .5rem;
}
.icon-save {
@include svg-size();
fill: $grayer;
&:hover {
fill: $primary;
transition: .2s linear;
}
}
.loading-spinner {
margin-right: .5rem;
}
.tag {
@include font-size(small);
margin: 0 .5rem .5rem 0;
padding: .5rem;
}
.icon-close {
@include svg-size(.7rem);
fill: $gray-light;
margin-left: .25rem;
&:hover {
cursor: pointer;
fill: $red;
}
}
.add-tag {
color: $gray-light;
display: inline-block;
&:hover {
color: $primary-light;
}
}
.icon-add {
@include svg-size(.9rem);
margin-right: .25rem;
margin-top: .5rem;
}
.add-tag-text {
@include font-size(small);
}
.remove-tag {
display: inline-block;
}
}

View File

@ -0,0 +1,39 @@
@function sqrt($r) {
$x0: 1;
$x1: $x0;
@for $i from 1 through 10 {
$x1: $x0 - ($x0 * $x0 - abs($r)) / (2 * $x0);
$x0: $x1;
}
@return round($x1);
}
@mixin empty-color($width) {
background: $mass-white;
border: 1px solid $whitish;
position: relative;
&:after {
content: "";
width: 2px;
height: #{sqrt(2*$width*$width)}px;
background: #ff8282;
transform: rotate(-45deg);
position: absolute;
top: 0;
left: 0;
transform-origin: top;
}
&:before {
content: "";
width: 2px;
height: #{sqrt(2*$width*$width)}px;
background: #ff8282;
transform: rotate(45deg);
position: absolute;
top: 0;
right: 0;
transform-origin: top;
}
}

View File

@ -1,4 +1,15 @@
@mixin popover($width, $top: '', $left: '', $bottom: '', $right: '', $arrow-width: 0, $arrow-top: '', $arrow-left: '', $arrow-bottom: '', $arrow-height: 15px) {
@mixin popover(
$width,
$top: '',
$left: '',
$bottom: '',
$right: '',
$arrow-width: 0,
$arrow-top: '',
$arrow-left: '',
$arrow-bottom: '',
$arrow-height: 15px
) {
@include font-type(light);
@include font-size(small);
background: $blackish;
@ -14,6 +25,7 @@
top: #{$top};
width: $width;
z-index: 99;
text-align: center;
a {
@include font-size(small);
border-bottom: 1px solid $grayer;

View File

@ -21,3 +21,4 @@
@import '../dependencies/mixins/slide';
@import '../dependencies/mixins/svg';
@import '../dependencies/mixins/track-buttons';
@import '../dependencies/mixins/empty-color';

View File

@ -1,20 +1,109 @@
.table-tags-editor {
input[type="text"] {
background-color: transparent;
border: 0;
border-bottom: 1px solid transparent;
box-shadow: none;
transition: border-bottom .2s linear;
&:focus {
border-bottom: 1px solid $gray;
outline: none;
}
}
.color-filter {
align-items: center;
display: flex;
flex-grow: 1;
padding: 0 10px;
.add-tag-container {
align-items: center;
background: $mass-white;
display: flex;
margin: .5rem 0;
padding: 1rem;
.color-column {
cursor: pointer;
flex-basis: 60px;
flex-grow: 0;
flex-shrink: 0;
position: relative;
}
.tag-name {
flex-basis: 80%;
margin-right: 1rem;
}
.options-column {
display: flex;
}
.current-color {
&.empty-color {
@include empty-color(38);
}
}
input[type="text"] {
background: $white;
}
.icon {
opacity: 1;
}
}
.tags-table {
.table-tags-editor {
input[type="text"] {
background-color: transparent;
border: 0;
border-bottom: 1px solid transparent;
box-shadow: none;
transition: border-bottom .2s linear;
&:focus {
border-bottom: 1px solid $gray;
outline: none;
}
}
.row {
&.header-tag-row {
padding-left: 1rem;
}
}
.color-filter {
align-items: center;
display: flex;
flex-grow: 1;
padding: 0 10px;
position: relative;
input {
padding: 0;
}
}
}
.row {
&.tag-row {
margin: .3rem 0;
padding: .7rem;
&:hover {
cursor: default;
}
}
}
.mix-tags {
position: relative;
.popover {
@include popover(120px, '', '', 2rem, -85%, 1rem, '', 50%, -5px);
}
&:hover {
.popover {
display: block;
}
}
}
.mixing-options-column {
text-align: right;
}
.mixing-tags-from,
.mixing-tags-to {
background: lighten(rgba($primary-light, .2), 30%);
}
.mixing-confirm {
margin: 0 .5rem;
}
.mixing-help-text {
@include font-size(xsmall);
color: $primary-dark;
display: inline;
padding-right: .5rem;
text-align: center;
@include breakpoint(laptop) {
display: block;
padding: .5rem;
}
}
.current-color {
&.empty-color {
@include empty-color(38);
}
}
}

View File

@ -11,7 +11,7 @@
width: 100%;
}
}
.admin-attributes-section-wrapper-empty {
.admin-tags-section-wrapper-empty {
color: $gray-light;
padding: 10vh 0 0;
text-align: center;

View File

@ -15,7 +15,6 @@
}
.row {
align-items: center;
border-bottom: 1px solid $whitish;
display: flex;
justify-content: center;
padding: 1rem;
@ -27,15 +26,11 @@
cursor: pointer;
}
}
&.edition,
&.new-value {
padding-left: 50px;
}
&.hidden {
display: none;
}
&:hover {
background: lighten($primary, 60%);
background: lighten(rgba($primary-light, .2), 30%);
cursor: move;
transition: background .2s ease-in;
.icon {
@ -110,16 +105,10 @@
}
.options-column {
a {
cursor: pointer;
display: inline-block;
}
}
form {
&:last-child {
.row {
border: 0;
}
}
}
.row-edit {
.options-column {
@ -133,13 +122,14 @@
height: 40px;
width: 40px;
}
.icon {
cursor: pointer;
fill: $gray-light;
margin-right: 1rem;
opacity: 0;
&:hover {
fill: $primary;
fill: $primary-light;
transition: all .2s ease-in;
}
&.icon-check {
@ -147,6 +137,10 @@
fill: $primary;
opacity: 1;
}
&.icon-merge {
cursor: default;
opacity: 1;
}
&.icon-search {
cursor: none;
fill: $primary;
@ -156,9 +150,7 @@
cursor: move;
}
&.icon-trash {
&:hover {
fill: $red-light;
}
fill: $red-light;
}
}
.gu-mirror {

View File

@ -429,6 +429,7 @@
fill="#fff"
d="M511.998 107.939c-222.856 0-404.061 181.204-404.061 404.061s181.205 404.061 404.061 404.061c222.856 0 404.061-181.203 404.061-404.061s-181.205-404.061-404.061-404.061zM511.998 158.447c88.671 0 169.621 32.484 231.616 86.222l-498.947 498.948c-53.74-61.998-86.223-142.945-86.223-231.617 0-195.561 157.992-353.553 353.553-353.553zM779.328 280.383c53.74 61.998 86.223 142.945 86.223 231.617 0 195.561-157.992 353.553-353.553 353.553-88.671 0-169.617-32.484-231.616-86.222l498.947-498.948z"></path>
</symbol>
<<<<<<< b929b5ecdaefcb0f2430ea5c8e41ce8fdcdbe761
<symbol id="icon-add-user" viewBox="0 0 470 350">
<title>Add user</title>
<path
@ -437,6 +438,13 @@
<symbol id="icon-view-more" viewBox="0 0 66.3 16">
<title>View more</title>
<path d="M16 8a8 8 0 0 1-8 8 8 8 0 0 1-8-8 8 8 0 0 1 8-8 8 8 0 0 1 8 8zM41.2 8a8 8 0 0 1-8 8 8 8 0 0 1-8-8 8 8 0 0 1 8-8 8 8 0 0 1 8 8zM66.3 8a8 8 0 0 1-8 8 8 8 0 0 1-8-8 8 8 0 0 1 8-8 8 8 0 0 1 8 8z"/>
<symbol id="icon-merge" viewBox="0 0 400 400">
<title>Merge</title>
<path d="M201.2 5.8l-100 100.7h81.4v126L45 371l23.3 22.7L216.8 243V106.4h85zm51 269.8l-24 24.2 102.6 94.4 24-24.2z" />
</symbol>
<symbol id="icon-fill" viewBox="0 0 400 400">
<title>Fill</title>
<path d="M106.3 1.4l-9.8 9.8L171 85.7 20.8 236l160 160 160-160-160-160-74.5-74.6zm74.5 94.2L321 236l-1 .8H41.6l-1-1L180.8 95.7zM341.2 291s-38 41.2-38 66.5c0 21 17 38 38 38s38-17 38-38c0-25.3-38-66.5-38-66.5z"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -19,8 +19,21 @@ helper.getCreateEditUsLightbox = function() {
subject: function() {
return el.$('input[name="subject"]');
},
tags: function() {
return el.$('.tag-input');
tags: async function() {
$('.e2e-show-tag-input').click();
$('.e2e-open-color-selector').click();
$$('.e2e-color-dropdown li').get(1).click();
$('.e2e-add-tag-input')
.sendKeys('xxxyy')
.sendKeys(protractor.Key.ENTER);
$$('.e2e-delete-tag').last().click();
$('.e2e-add-tag-input')
.sendKeys('a')
.sendKeys(protractor.Key.ARROW_DOWN)
.sendKeys(protractor.Key.ENTER);
},
description: function() {
return el.$('textarea[name="description"]');

View File

@ -63,3 +63,20 @@ helper.lightboxAttachment = async function() {
expect(countAttachments + 1).to.be.equal(newCountAttachments);
};
helper.tags = function() {
$('.e2e-show-tag-input').click();
$('.e2e-open-color-selector').click();
$$('.e2e-color-dropdown li').get(1).click();
$('.e2e-add-tag-input')
.sendKeys('xxxyy')
.sendKeys(protractor.Key.ENTER);
$$('.e2e-delete-tag').last().click();
$('.e2e-add-tag-input')
.sendKeys('a')
.sendKeys(protractor.Key.ARROW_DOWN)
.sendKeys(protractor.Key.ENTER);
}

View File

@ -57,34 +57,35 @@ helper.description = function(){
helper.tags = function() {
let el = $('div[tg-tag-line]');
let el = $('tg-tag-line-common');
let obj = {
el:el,
clearTags: async function() {
let tags = await el.$$('.icon-delete');
let tags = await el.$$('.e2e-delete-tag');
let totalTags = tags.length;
let htmlChanges = null;
while (totalTags > 0) {
htmlChanges = await utils.common.outerHtmlChanges(el.$(".tags-container"));
await el.$$('.icon-delete').first().click();
await el.$$('.e2e-delete-tag').first().click();
totalTags --;
await htmlChanges();
}
},
getTagsText: function() {
return el.$$('.tag-name').getText();
return el.$$('tg-tag span').getText();
},
addTags: async function(tags) {
let htmlChanges = null
el.$('.add-tag').click();
$('.e2e-show-tag-input').click();
for (let tag of tags){
htmlChanges = await utils.common.outerHtmlChanges(el.$(".tags-container"));
el.$('.tag-input').sendKeys(tag);
el.$('.e2e-add-tag-input').sendKeys(tag);
await browser.actions().sendKeys(protractor.Key.ENTER).perform();
await htmlChanges();
}

View File

@ -49,11 +49,7 @@ describe('backlog', function() {
createUSLightbox.status(2).click();
// tags
createUSLightbox.tags().sendKeys('aaa');
browser.actions().sendKeys(protractor.Key.ENTER).perform();
createUSLightbox.tags().sendKeys('bbb');
browser.actions().sendKeys(protractor.Key.ENTER).perform();
commonHelper.tags();
// description
createUSLightbox.description().sendKeys('test test');
@ -245,7 +241,7 @@ describe('backlog', function() {
expect(elementRef1).to.be.equal(draggedRefs[1]);
});
it.skip('drag multiple us to milestone', async function() {
it('drag multiple us to milestone', async function() {
let sprint = backlogHelper.sprints().get(0);
let initUssSprintCount = await backlogHelper.getSprintUsertories(sprint).count();

View File

@ -38,11 +38,7 @@ describe('issues list', function() {
createIssueLightbox.subject().sendKeys('subject');
// tags
await createIssueLightbox.tags().sendKeys('aaa');
browser.actions().sendKeys(protractor.Key.ENTER).perform();
await createIssueLightbox.tags().sendKeys('bbb');
browser.actions().sendKeys(protractor.Key.ENTER).perform();
commonHelper.tags();
});
it('upload attachments', commonHelper.lightboxAttachment);

View File

@ -74,11 +74,7 @@ describe('kanban', function() {
expect(totalPoints).to.be.equal('4');
// tags
createUSLightbox.tags().sendKeys('www');
browser.actions().sendKeys(protractor.Key.ENTER).perform();
createUSLightbox.tags().sendKeys('xxx');
browser.actions().sendKeys(protractor.Key.ENTER).perform();
commonHelper.tags();
// description
createUSLightbox.description().sendKeys(formFields.description);

View File

@ -66,11 +66,7 @@ describe('taskboard', function() {
createTaskLightbox.subject().sendKeys(formFields.subject);
createTaskLightbox.description().sendKeys(formFields.description);
createTaskLightbox.tags().sendKeys('aaa');
browser.actions().sendKeys(protractor.Key.ENTER).perform();
createTaskLightbox.tags().sendKeys('bbb');
browser.actions().sendKeys(protractor.Key.ENTER).perform();
commonHelper.tags();
await createTaskLightbox.blocked().click();
await createTaskLightbox.blockedNote().sendKeys(formFields.blockedNote);