diff --git a/.gitignore b/.gitignore
index e2d57d5c..b5e83639 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,7 @@ app/coffee/modules/locales/locale*.coffee
*.swp
*.swo
.#*
-tags
+/tags
tmp/
app/config/main.coffee
scss-lint.log
diff --git a/app/coffee/modules/admin/project-profile.coffee b/app/coffee/modules/admin/project-profile.coffee
index 3ab3ca98..06fba778 100644
--- a/app/coffee/modules/admin/project-profile.coffee
+++ b/app/coffee/modules/admin/project-profile.coffee
@@ -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)
diff --git a/app/coffee/modules/admin/project-values.coffee b/app/coffee/modules/admin/project-values.coffee
index ba9533d1..fade389f 100644
--- a/app/coffee/modules/admin/project-values.coffee
+++ b/app/coffee/modules/admin/project-values.coffee
@@ -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])
diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee
index f31c9d4a..0f149171 100644
--- a/app/coffee/modules/common/lightboxes.coffee
+++ b/app/coffee/modules/common/lightboxes.coffee
@@ -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
diff --git a/app/coffee/modules/common/tags.coffee b/app/coffee/modules/common/tags.coffee
index 22e7dc30..faa3ec3a 100644
--- a/app/coffee/modules/common/tags.coffee
+++ b/app/coffee/modules/common/tags.coffee
@@ -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) { %>
- <%- tag.name %>
+
+ style="border-left: 5px solid <%- tag[1] %>"
+ <% } %>
+ title="<%- tag[0] %>"><%- tag[0] %>
<% }) %>
""")
kanban: _.template("""
<% _.each(tags, function(tag) { %>
-
+
+ style="border-color: <%- tag[1] %>"
+ <% } %>
+ title="<%- tag[0] %>" />
<% }) %>
""")
taskboard: _.template("""
<% _.each(tags, function(tag) { %>
-
+
+ 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])
diff --git a/app/coffee/modules/issues/lightboxes.coffee b/app/coffee/modules/issues/lightboxes.coffee
index 3be9bb6b..94ee2d33 100644
--- a/app/coffee/modules/issues/lightboxes.coffee
+++ b/app/coffee/modules/issues/lightboxes.coffee
@@ -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
diff --git a/app/coffee/modules/resources/projects.coffee b/app/coffee/modules/resources/projects.coffee
index b93912f2..108012cd 100644
--- a/app/coffee/modules/resources/projects.coffee
+++ b/app/coffee/modules/resources/projects.coffee
@@ -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)
diff --git a/app/coffee/modules/taskboard/lightboxes.coffee b/app/coffee/modules/taskboard/lightboxes.coffee
index caf92987..9626e245 100644
--- a/app/coffee/modules/taskboard/lightboxes.coffee
+++ b/app/coffee/modules/taskboard/lightboxes.coffee
@@ -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
diff --git a/app/coffee/utils.coffee b/app/coffee/utils.coffee
index d69a91c3..1bffd7c7 100644
--- a/app/coffee/utils.coffee
+++ b/app/coffee/utils.coffee
@@ -28,10 +28,12 @@ addClass = (el, className) ->
else
el.className += ' ' + className
+
nl2br = (str) =>
breakTag = '
'
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
diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json
index 84bc19e5..96549209 100644
--- a/app/locales/taiga/locale-en.json
+++ b/app/locales/taiga/locale-en.json
@@ -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}}",
diff --git a/app/modules/components/tags/color-selector/color-selector.controller.coffee b/app/modules/components/tags/color-selector/color-selector.controller.coffee
new file mode 100644
index 00000000..2d817a15
--- /dev/null
+++ b/app/modules/components/tags/color-selector/color-selector.controller.coffee
@@ -0,0 +1,57 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# 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 .
+#
+# 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)
diff --git a/app/modules/components/tags/color-selector/color-selector.controller.spec.coffee b/app/modules/components/tags/color-selector/color-selector.controller.spec.coffee
new file mode 100644
index 00000000..ec212331
--- /dev/null
+++ b/app/modules/components/tags/color-selector/color-selector.controller.spec.coffee
@@ -0,0 +1,60 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# 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 .
+#
+# 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})
diff --git a/app/modules/components/tags/color-selector/color-selector.directive.coffee b/app/modules/components/tags/color-selector/color-selector.directive.coffee
new file mode 100644
index 00000000..67e02f57
--- /dev/null
+++ b/app/modules/components/tags/color-selector/color-selector.directive.coffee
@@ -0,0 +1,61 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# 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 .
+#
+# 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)
diff --git a/app/modules/components/tags/color-selector/color-selector.jade b/app/modules/components/tags/color-selector/color-selector.jade
new file mode 100644
index 00000000..c66e83aa
--- /dev/null
+++ b/app/modules/components/tags/color-selector/color-selector.jade
@@ -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"
+ )
diff --git a/app/modules/components/tags/color-selector/color-selector.scss b/app/modules/components/tags/color-selector/color-selector.scss
new file mode 100644
index 00000000..2f06ccb0
--- /dev/null
+++ b/app/modules/components/tags/color-selector/color-selector.scss
@@ -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;
+ }
+ }
+}
diff --git a/app/modules/components/tags/components/add-tag-button.jade b/app/modules/components/tags/components/add-tag-button.jade
new file mode 100644
index 00000000..88bf6bd6
--- /dev/null
+++ b/app/modules/components/tags/components/add-tag-button.jade
@@ -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")
diff --git a/app/modules/components/tags/components/add-tag-input.jade b/app/modules/components/tags/components/add-tag-input.jade
new file mode 100644
index 00000000..7e1b11ca
--- /dev/null
+++ b/app/modules/components/tags/components/add-tag-input.jade
@@ -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)"
+ )
diff --git a/app/modules/components/tags/components/add-tag.scss b/app/modules/components/tags/components/add-tag.scss
new file mode 100644
index 00000000..bccccb5b
--- /dev/null
+++ b/app/modules/components/tags/components/add-tag.scss
@@ -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;
+ }
+ }
+}
diff --git a/app/modules/components/tags/tag-dropdown/tag-dropdown.directive.coffee b/app/modules/components/tags/tag-dropdown/tag-dropdown.directive.coffee
new file mode 100644
index 00000000..c1386e2f
--- /dev/null
+++ b/app/modules/components/tags/tag-dropdown/tag-dropdown.directive.coffee
@@ -0,0 +1,84 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# 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 .
+#
+# 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)
diff --git a/app/modules/components/tags/tag-dropdown/tag-dropdown.jade b/app/modules/components/tags/tag-dropdown/tag-dropdown.jade
new file mode 100644
index 00000000..e25ad740
--- /dev/null
+++ b/app/modules/components/tags/tag-dropdown/tag-dropdown.jade
@@ -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]"
+ )
diff --git a/app/modules/components/tags/tag-line-common/tag-line-common.controller.coffee b/app/modules/components/tags/tag-line-common/tag-line-common.controller.coffee
new file mode 100644
index 00000000..26082ae3
--- /dev/null
+++ b/app/modules/components/tags/tag-line-common/tag-line-common.controller.coffee
@@ -0,0 +1,56 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# 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 .
+#
+# 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)
diff --git a/app/modules/components/tags/tag-line-common/tag-line-common.controller.spec.coffee b/app/modules/components/tags/tag-line-common/tag-line-common.controller.spec.coffee
new file mode 100644
index 00000000..188df081
--- /dev/null
+++ b/app/modules/components/tags/tag-line-common/tag-line-common.controller.spec.coffee
@@ -0,0 +1,96 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# 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 .
+#
+# 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
diff --git a/app/modules/components/tags/tag-line-common/tag-line-common.directive.coffee b/app/modules/components/tags/tag-line-common/tag-line-common.directive.coffee
new file mode 100644
index 00000000..668a899f
--- /dev/null
+++ b/app/modules/components/tags/tag-line-common/tag-line-common.directive.coffee
@@ -0,0 +1,69 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# 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 .
+#
+# 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)
diff --git a/app/modules/components/tags/tag-line-common/tag-line-common.jade b/app/modules/components/tags/tag-line-common/tag-line-common.jade
new file mode 100644
index 00000000..a1fd6848
--- /dev/null
+++ b/app/modules/components/tags/tag-line-common/tag-line-common.jade
@@ -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
diff --git a/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.coffee b/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.coffee
new file mode 100644
index 00000000..5130cac6
--- /dev/null
+++ b/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.coffee
@@ -0,0 +1,88 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# 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 .
+#
+# 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)
diff --git a/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.spec.coffee b/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.spec.coffee
new file mode 100644
index 00000000..8b323169
--- /dev/null
+++ b/app/modules/components/tags/tag-line-detail/tag-line-detail.controller.spec.coffee
@@ -0,0 +1,157 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# 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 .
+#
+# 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()
diff --git a/app/modules/components/tags/tag-line-detail/tag-line-detail.directive.coffee b/app/modules/components/tags/tag-line-detail/tag-line-detail.directive.coffee
new file mode 100644
index 00000000..50976ab6
--- /dev/null
+++ b/app/modules/components/tags/tag-line-detail/tag-line-detail.directive.coffee
@@ -0,0 +1,35 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# 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 .
+#
+# 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)
diff --git a/app/modules/components/tags/tag-line-detail/tag-line-detail.jade b/app/modules/components/tags/tag-line-detail/tag-line-detail.jade
new file mode 100644
index 00000000..3a688aa6
--- /dev/null
+++ b/app/modules/components/tags/tag-line-detail/tag-line-detail.jade
@@ -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)"
+)
diff --git a/app/modules/components/tags/tag-line.scss b/app/modules/components/tags/tag-line.scss
new file mode 100644
index 00000000..292458b0
--- /dev/null
+++ b/app/modules/components/tags/tag-line.scss
@@ -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);
+ }
+}
diff --git a/app/modules/components/tags/tag-line.service.coffee b/app/modules/components/tags/tag-line.service.coffee
new file mode 100644
index 00000000..f8257b8a
--- /dev/null
+++ b/app/modules/components/tags/tag-line.service.coffee
@@ -0,0 +1,35 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# 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 .
+#
+# 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)
diff --git a/app/modules/components/tags/tag/tag.directive.coffee b/app/modules/components/tags/tag/tag.directive.coffee
new file mode 100644
index 00000000..ccd366f1
--- /dev/null
+++ b/app/modules/components/tags/tag/tag.directive.coffee
@@ -0,0 +1,33 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# 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 .
+#
+# 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)
diff --git a/app/modules/components/tags/tag/tag.jade b/app/modules/components/tags/tag/tag.jade
new file mode 100644
index 00000000..acd7692a
--- /dev/null
+++ b/app/modules/components/tags/tag/tag.jade
@@ -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]"
+)
diff --git a/app/modules/components/tags/tag/tag.scss b/app/modules/components/tags/tag/tag.scss
new file mode 100644
index 00000000..185940e8
--- /dev/null
+++ b/app/modules/components/tags/tag/tag.scss
@@ -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;
+ }
+}
diff --git a/app/modules/home/projects/home-project-list.jade b/app/modules/home/projects/home-project-list.jade
index 9ed81a65..beecb13c 100644
--- a/app/modules/home/projects/home-project-list.jade
+++ b/app/modules/home/projects/home-project-list.jade
@@ -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')"
diff --git a/app/modules/projects/project/project.jade b/app/modules/projects/project/project.jade
index cc3acb5a..01feadbc 100644
--- a/app/modules/projects/project/project.jade
+++ b/app/modules/projects/project/project.jade
@@ -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")
diff --git a/app/modules/projects/projects.service.coffee b/app/modules/projects/projects.service.coffee
index ab3973a3..3c1415d5 100644
--- a/app/modules/projects/projects.service.coffee
+++ b/app/modules/projects/projects.service.coffee
@@ -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
diff --git a/app/modules/projects/projects.service.spec.coffee b/app/modules/projects/projects.service.spec.coffee
index 1829e8a8..80a98389 100644
--- a/app/modules/projects/projects.service.spec.coffee
+++ b/app/modules/projects/projects.service.spec.coffee
@@ -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"}
}
])
diff --git a/app/partials/admin/admin-project-profile.jade b/app/partials/admin/admin-project-profile.jade
index b03e2890..8adefd42 100644
--- a/app/partials/admin/admin-project-profile.jade
+++ b/app/partials/admin/admin-project-profile.jade
@@ -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")
diff --git a/app/partials/admin/admin-project-values-tags.jade b/app/partials/admin/admin-project-values-tags.jade
index fc52290b..ff8830d9 100644
--- a/app/partials/admin/admin-project-values-tags.jade
+++ b/app/partials/admin/admin-project-values-tags.jade
@@ -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
diff --git a/app/partials/common/tag/lb-tag-line-tags.jade b/app/partials/common/tag/lb-tag-line-tags.jade
index cfe107d7..af7568f9 100644
--- a/app/partials/common/tag/lb-tag-line-tags.jade
+++ b/app/partials/common/tag/lb-tag-line-tags.jade
@@ -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")
diff --git a/app/partials/common/tag/tags-line-tags.jade b/app/partials/common/tag/tags-line-tags.jade
index 53ee66ac..07f3a981 100644
--- a/app/partials/common/tag/tags-line-tags.jade
+++ b/app/partials/common/tag/tags-line-tags.jade
@@ -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(
diff --git a/app/partials/includes/components/select-color.jade b/app/partials/includes/components/select-color.jade
index 3f9f4306..448455fa 100644
--- a/app/partials/includes/components/select-color.jade
+++ b/app/partials/includes/components/select-color.jade
@@ -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")
diff --git a/app/partials/includes/modules/admin/project-tags.jade b/app/partials/includes/modules/admin/project-tags.jade
new file mode 100644
index 00000000..3952d08c
--- /dev/null
+++ b/app/partials/includes/modules/admin/project-tags.jade
@@ -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"
+ )
diff --git a/app/partials/includes/modules/lightbox-create-issue.jade b/app/partials/includes/modules/lightbox-create-issue.jade
index d1f4f37b..69209b57 100644
--- a/app/partials/includes/modules/lightbox-create-issue.jade
+++ b/app/partials/includes/modules/lightbox-create-issue.jade
@@ -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
diff --git a/app/partials/includes/modules/lightbox-task-create-edit.jade b/app/partials/includes/modules/lightbox-task-create-edit.jade
index 41fcf957..3cd129b5 100644
--- a/app/partials/includes/modules/lightbox-task-create-edit.jade
+++ b/app/partials/includes/modules/lightbox-task-create-edit.jade
@@ -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
diff --git a/app/partials/includes/modules/lightbox-us-create-edit.jade b/app/partials/includes/modules/lightbox-us-create-edit.jade
index 7345e627..d33b86b1 100644
--- a/app/partials/includes/modules/lightbox-us-create-edit.jade
+++ b/app/partials/includes/modules/lightbox-us-create-edit.jade
@@ -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
diff --git a/app/partials/issue/issues-detail.jade b/app/partials/issue/issues-detail.jade
index ab8bce2c..1b4185f0 100644
--- a/app/partials/issue/issues-detail.jade
+++ b/app/partials/issue/issues-detail.jade
@@ -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"
diff --git a/app/partials/task/task-detail.jade b/app/partials/task/task-detail.jade
index 68d7dff9..6ec12a6b 100644
--- a/app/partials/task/task-detail.jade
+++ b/app/partials/task/task-detail.jade
@@ -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"
diff --git a/app/partials/us/us-detail.jade b/app/partials/us/us-detail.jade
index 54976f35..8baf2bec 100644
--- a/app/partials/us/us-detail.jade
+++ b/app/partials/us/us-detail.jade
@@ -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")
diff --git a/app/styles/components/select-color.scss b/app/styles/components/select-color.scss
index dc329393..92941c0c 100644
--- a/app/styles/components/select-color.scss
+++ b/app/styles/components/select-color.scss
@@ -19,6 +19,9 @@
height: 35px;
width: 35px;
}
+ .empty-color {
+ @include empty-color(33);
+ }
ul {
float: left;
margin-bottom: 1rem;
diff --git a/app/styles/components/tag.scss b/app/styles/components/tag.scss
deleted file mode 100644
index 810eb23d..00000000
--- a/app/styles/components/tag.scss
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/styles/dependencies/mixins/empty-color.scss b/app/styles/dependencies/mixins/empty-color.scss
new file mode 100644
index 00000000..9c49729b
--- /dev/null
+++ b/app/styles/dependencies/mixins/empty-color.scss
@@ -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;
+ }
+}
diff --git a/app/styles/dependencies/mixins/popover.scss b/app/styles/dependencies/mixins/popover.scss
index 19fd9038..595896a7 100644
--- a/app/styles/dependencies/mixins/popover.scss
+++ b/app/styles/dependencies/mixins/popover.scss
@@ -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;
diff --git a/app/styles/extras/dependencies.scss b/app/styles/extras/dependencies.scss
index a7fff44b..2d94ceff 100644
--- a/app/styles/extras/dependencies.scss
+++ b/app/styles/extras/dependencies.scss
@@ -21,3 +21,4 @@
@import '../dependencies/mixins/slide';
@import '../dependencies/mixins/svg';
@import '../dependencies/mixins/track-buttons';
+@import '../dependencies/mixins/empty-color';
diff --git a/app/styles/layout/admin-project-tags.scss b/app/styles/layout/admin-project-tags.scss
index f341214f..c4c1b136 100644
--- a/app/styles/layout/admin-project-tags.scss
+++ b/app/styles/layout/admin-project-tags.scss
@@ -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);
+ }
+ }
}
diff --git a/app/styles/layout/admin-project-values.scss b/app/styles/layout/admin-project-values.scss
index cae0e99f..2d17fb9e 100644
--- a/app/styles/layout/admin-project-values.scss
+++ b/app/styles/layout/admin-project-values.scss
@@ -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;
diff --git a/app/styles/modules/common/colors-table.scss b/app/styles/modules/common/colors-table.scss
index bc5d0d36..42df79c6 100644
--- a/app/styles/modules/common/colors-table.scss
+++ b/app/styles/modules/common/colors-table.scss
@@ -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 {
diff --git a/app/svg/sprite.svg b/app/svg/sprite.svg
index ff8ef151..8fa6cc2a 100644
--- a/app/svg/sprite.svg
+++ b/app/svg/sprite.svg
@@ -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">
+<<<<<<< b929b5ecdaefcb0f2430ea5c8e41ce8fdcdbe761
Add user
View more
+
+ Merge
+
+
+
+ Fill
+
diff --git a/e2e/helpers/backlog-helper.js b/e2e/helpers/backlog-helper.js
index 954ec830..932e1c34 100644
--- a/e2e/helpers/backlog-helper.js
+++ b/e2e/helpers/backlog-helper.js
@@ -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"]');
diff --git a/e2e/helpers/common-helper.js b/e2e/helpers/common-helper.js
index d2e24891..83762161 100644
--- a/e2e/helpers/common-helper.js
+++ b/e2e/helpers/common-helper.js
@@ -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);
+}
diff --git a/e2e/helpers/detail-helper.js b/e2e/helpers/detail-helper.js
index 7e2a4ee2..1148d21c 100644
--- a/e2e/helpers/detail-helper.js
+++ b/e2e/helpers/detail-helper.js
@@ -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();
}
diff --git a/e2e/suites/backlog.e2e.js b/e2e/suites/backlog.e2e.js
index 9df1235b..9ec2b4ee 100644
--- a/e2e/suites/backlog.e2e.js
+++ b/e2e/suites/backlog.e2e.js
@@ -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();
diff --git a/e2e/suites/issues/issues.e2e.js b/e2e/suites/issues/issues.e2e.js
index 785badac..264b4300 100644
--- a/e2e/suites/issues/issues.e2e.js
+++ b/e2e/suites/issues/issues.e2e.js
@@ -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);
diff --git a/e2e/suites/kanban.e2e.js b/e2e/suites/kanban.e2e.js
index 3ea516c4..85d100f3 100644
--- a/e2e/suites/kanban.e2e.js
+++ b/e2e/suites/kanban.e2e.js
@@ -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);
diff --git a/e2e/suites/tasks/taskboard.e2e.js b/e2e/suites/tasks/taskboard.e2e.js
index 3220860b..61a66b08 100644
--- a/e2e/suites/tasks/taskboard.e2e.js
+++ b/e2e/suites/tasks/taskboard.e2e.js
@@ -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);