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);