Merge branch 'master' into stable

stable
Miguel Gonzalez 2018-03-07 07:00:32 +01:00
commit f8ba446334
1560 changed files with 645 additions and 301 deletions

View File

@ -1,5 +1,23 @@
# Changelog # # Changelog #
## 3.2.0 Betula nana (2018-03-07)
### Features
- Emojis support on subjects and tags.
- Add "confirm dialog" before closing edit lightboxes.
- Wiki activity hidden by default.
- Allow ascending votes ordering in issues list.
- Add multiple drag in Kanban.
- Show US counter and wip limit in Kanban columns title.
- Add role filtering in US.
## 3.1.3 (2018-02-28)
### Features
- Minor bug fixes.
## 3.1.0 Perovskia Atriplicifolia (2017-03-10) ## 3.1.0 Perovskia Atriplicifolia (2017-03-10)
### Features ### Features

View File

@ -12,6 +12,7 @@ window.taigaConfig = {
"defaultTheme": "taiga", "defaultTheme": "taiga",
"publicRegisterEnabled": true, "publicRegisterEnabled": true,
"feedbackEnabled": true, "feedbackEnabled": true,
"supportUrl": null,
"privacyPolicyUrl": null, "privacyPolicyUrl": null,
"termsOfServiceUrl": null, "termsOfServiceUrl": null,
"maxUploadFileSize": null, "maxUploadFileSize": null,
@ -70,10 +71,14 @@ promise.fail () ->
console.error "Your conf.json file is not a valid json file, please review it." console.error "Your conf.json file is not a valid json file, please review it."
promise.always -> promise.always ->
emojisPromise = $.getJSON("/#{window._version}/emojis/emojis-data.json").then (emojis) ->
window.emojis = emojis
if window.taigaConfig.contribPlugins.length > 0 if window.taigaConfig.contribPlugins.length > 0
loadPlugins(window.taigaConfig.contribPlugins).then () -> loadPlugins(window.taigaConfig.contribPlugins).then () ->
ljs.load "/#{window._version}/js/app.js", -> ljs.load "/#{window._version}/js/app.js", ->
emojisPromise.then ->
angular.bootstrap(document, ['taiga']) angular.bootstrap(document, ['taiga'])
else else
ljs.load "/#{window._version}/js/app.js", -> ljs.load "/#{window._version}/js/app.js", ->
emojisPromise.then ->
angular.bootstrap(document, ['taiga']) angular.bootstrap(document, ['taiga'])

View File

@ -23,6 +23,7 @@
### ###
@taiga = taiga = {} @taiga = taiga = {}
taiga.emojis = window.emojis
@.taigaContribPlugins = @.taigaContribPlugins or window.taigaContribPlugins or [] @.taigaContribPlugins = @.taigaContribPlugins or window.taigaContribPlugins or []
# Generic function for generate hash from a arbitrary length # Generic function for generate hash from a arbitrary length

View File

@ -521,6 +521,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@.generateFilters().then () => @.generateFilters().then () =>
@rootscope.$broadcast("filters:update") @rootscope.$broadcast("filters:update")
@.loadProjectStats() @.loadProjectStats()
if @.isFilterDataTypeSelected('status')
@.filtersReloadContent()
editUserStory: (projectId, ref, $event) -> editUserStory: (projectId, ref, $event) ->
target = $($event.target) target = $($event.target)

View File

@ -68,7 +68,7 @@ BacklogSortableDirective = () ->
window.dragMultiple.start(item, container) window.dragMultiple.start(item, container)
drake.on 'cloned', (item) -> drake.on 'cloned', (item) ->
$(item).addClass('backlog-us-mirror') $(item).addClass('multiple-drag-mirror')
drake.on 'dragend', (item) -> drake.on 'dragend', (item) ->
parent = $(item).parent() parent = $(item).parent()

View File

@ -251,8 +251,6 @@ Qqueue = ($q) ->
bindAdd: (fn) => bindAdd: (fn) =>
return (args...) => return (args...) =>
lastPromise = lastPromise.then () => fn.apply(@, args) lastPromise = lastPromise.then () => fn.apply(@, args)
return qqueue
add: (fn) => add: (fn) =>
if !lastPromise if !lastPromise
lastPromise = fn() lastPromise = fn()

View File

@ -495,92 +495,6 @@ DeleteButtonDirective = ($log, $repo, $confirm, $location, $template) ->
module.directive("tgDeleteButton", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "$tgTemplate", DeleteButtonDirective]) module.directive("tgDeleteButton", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "$tgTemplate", DeleteButtonDirective])
#############################################################################
## Editable subject directive
#############################################################################
EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading, $modelTransform, $template) ->
template = $template.get("common/components/editable-subject.html")
link = ($scope, $el, $attrs, $model) ->
$scope.$on "object:updated", () ->
$el.find('.edit-subject').hide()
$el.find('.view-subject').show()
isEditable = ->
return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
save = (subject) ->
currentLoading = $loading()
.target($el.find('.save-container'))
.start()
transform = $modelTransform.save (item) ->
item.subject = subject
return item
transform.then =>
$confirm.notify("success")
$rootscope.$broadcast("object:updated")
$el.find('.edit-subject').hide()
$el.find('.view-subject').show()
transform.then null, ->
$confirm.notify("error")
transform.finally ->
currentLoading.finish()
return transform
$el.click ->
return if not isEditable()
$el.find('.edit-subject').show()
$el.find('.view-subject').hide()
$el.find('input').focus()
$el.on "click", ".save", (e) ->
e.preventDefault()
subject = $scope.item.subject
save(subject)
$el.on "keyup", "input", (event) ->
if event.keyCode == 13
subject = $scope.item.subject
save(subject)
else if event.keyCode == 27
$scope.$apply () => $model.$modelValue.revert()
$el.find('.edit-subject').hide()
$el.find('.view-subject').show()
$el.find('.edit-subject').hide()
$scope.$watch $attrs.ngModel, (value) ->
return if not value
$scope.item = value
if not isEditable()
$el.find('.view-subject .edit').remove()
$scope.$on "$destroy", ->
$el.off()
return {
link: link
restrict: "EA"
require: "ngModel"
template: template
}
module.directive("tgEditableSubject", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQueueModelTransformation",
"$tgTemplate", EditableSubjectDirective])
############################################################################# #############################################################################
## Common list directives ## Common list directives
############################################################################# #############################################################################

View File

@ -0,0 +1,84 @@
###
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2017 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014-2017 David Barragán Merino <bameda@dbarragan.com>
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2017 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
# Copyright (C) 2014-2017 Xavi Julian <xavier.julian@kaleidos.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: modules/common/analytics.coffee
###
taiga = @.taiga
module = angular.module("taigaCommon")
class EmojisService extends taiga.Service
@.$inject = []
constructor: () ->
@.emojis = _.map taiga.emojis, (it) ->
it.image = "/#{window._version}/emojis/" + it.image
return it
@.emojisById = _.keyBy(@.emojis, 'id')
@.emojisByName = _.keyBy(@.emojis, 'name')
searchByName: (name) =>
return _.filter @.emojis, (it) -> it.name.indexOf(name) != -1
getEmojiById: (id) =>
return @.emojisById[id]
getEmojiByName: (name) =>
return @.emojisByName[name]
replaceImgsByEmojiName: (html) =>
emojiIds = taiga.getMatches(html, /emojis\/([^"]+).png"/gi)
for emojiId in emojiIds
regexImgs = new RegExp('<img(.*)' + emojiId + '[^>]+\>', 'g')
emoji = @.getEmojiById(emojiId)
html = html.replace(regexImgs, ':' + emoji.name + ':')
return html
replaceEmojiNameByImgs: (text) =>
emojiIds = taiga.getMatches(text, /:([\w +-]*):/g)
for emojiId in emojiIds
regexImgs = new RegExp(':' + emojiId + ':', 'g')
emoji = @.getEmojiByName(emojiId)
if emoji
text = text.replace(regexImgs, '![alt](' + emoji.image + ')')
return text
replaceEmojiNameByHtmlImgs: (text) =>
emojiIds = taiga.getMatches(text, /:([\w +-]*):/g)
for emojiId in emojiIds
regexImgs = new RegExp(':' + _.escapeRegExp(emojiId) + ':', 'g')
emoji = @.getEmojiByName(emojiId)
if emoji
text = text.replace(regexImgs, '<img src="' + emoji.image + '" />')
return text
module.service("$tgEmojis", EmojisService)

View File

@ -143,3 +143,12 @@ inArray = ($filter) ->
return filter list, (listItem) -> return filter list, (listItem) ->
return arrayFilter.indexOf(listItem[element]) != -1 return arrayFilter.indexOf(listItem[element]) != -1
module.filter("inArray", ["$filter", inArray]) module.filter("inArray", ["$filter", inArray])
emojify = ($emojis) ->
return (input) ->
if input
return $emojis.replaceEmojiNameByHtmlImgs(_.escape(input))
return ""
module.filter("emojify", ["$tgEmojis", emojify])

View File

@ -371,6 +371,8 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
status: status status: status
is_archived: false is_archived: false
tags: [] tags: []
subject: ""
description: ""
}) })
# Update texts for creation # Update texts for creation
@ -478,24 +480,32 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
submitButton = $el.find(".submit-button") submitButton = $el.find(".submit-button")
close = () =>
if !$scope.us.isModified()
lightboxService.close($el)
$scope.$apply ->
$scope.us.revert()
else
$confirm.ask($translate.instant("LIGHTBOX.CREATE_EDIT_US.CONFIRM_CLOSE")).then (result) ->
lightboxService.close($el)
$scope.us.revert()
result.finish()
$el.on "submit", "form", submit $el.on "submit", "form", submit
$el.on "click", ".close", (event) -> $el.find('.close').on "click", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation()
$scope.$apply -> close()
$scope.us.revert()
lightboxService.close($el)
$el.keydown (event) -> $el.keydown (event) ->
event.stopPropagation()
code = if event.keyCode then event.keyCode else event.which code = if event.keyCode then event.keyCode else event.which
if code == 27 if code == 27
lightboxService.close($el) close()
$scope.$apply ->
$scope.us.revert()
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.find('.close').off()
$el.off() $el.off()
return {link: link} return {link: link}

View File

@ -58,14 +58,14 @@ TagsDirective = ->
module.directive("tgTags", TagsDirective) module.directive("tgTags", TagsDirective)
ColorizeTagsBacklogDirective = -> ColorizeTagsBacklogDirective = ($emojis) ->
template = _.template(""" template = _.template("""
<% _.each(tags, function(tag) { %> <% _.each(tags, function(tag) { %>
<% if (tag[1] !== null) { %> <% if (tag[1] !== null) { %>
<span class="tag" <span class="tag"
style="border-left: 5px solid <%- tag[1] %>" style="border-left: 5px solid <%- tag[1] %>"
title="<%- tag[0] %>"> title="<%- tag[0] %>">
<%- tag[0] %> <%= emojify(tag[0]) %>
</span> </span>
<% } %> <% } %>
<% }) %> <% }) %>
@ -73,7 +73,7 @@ ColorizeTagsBacklogDirective = ->
<% if (tag[1] === null) { %> <% if (tag[1] === null) { %>
<span class="tag" <span class="tag"
title="<%- tag[0] %>"> title="<%- tag[0] %>">
<%- tag[0] %> <%= emojify(tag[0]) %>
</span> </span>
<% } %> <% } %>
<% }) %> <% }) %>
@ -81,7 +81,7 @@ ColorizeTagsBacklogDirective = ->
link = ($scope, $el, $attrs, $ctrl) -> link = ($scope, $el, $attrs, $ctrl) ->
render = (tags) -> render = (tags) ->
html = template({tags: tags}) html = template({tags: tags, emojify: (text) -> $emojis.replaceEmojiNameByHtmlImgs(_.escape(text))})
$el.html(html) $el.html(html)
$scope.$watch $attrs.tgColorizeBacklogTags, (tags) -> $scope.$watch $attrs.tgColorizeBacklogTags, (tags) ->
@ -92,7 +92,7 @@ ColorizeTagsBacklogDirective = ->
return {link: link} return {link: link}
module.directive("tgColorizeBacklogTags", ColorizeTagsBacklogDirective) module.directive("tgColorizeBacklogTags", ["$tgEmojis", ColorizeTagsBacklogDirective])
############################################################################# #############################################################################

View File

@ -194,6 +194,7 @@ class UsFiltersMixin
filters.assigned_to = urlfilters.assigned_to filters.assigned_to = urlfilters.assigned_to
filters.owner = urlfilters.owner filters.owner = urlfilters.owner
filters.epic = urlfilters.epic filters.epic = urlfilters.epic
filters.role = urlfilters.role
@filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName).then (userFilters) => @filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName).then (userFilters) =>
userFilters[name] = filters userFilters[name] = filters
@ -207,6 +208,12 @@ class UsFiltersMixin
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.storeCustomFiltersName).then(@.generateFilters) @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.storeCustomFiltersName).then(@.generateFilters)
@.generateFilters() @.generateFilters()
isFilterDataTypeSelected: (filterDataType) ->
for filter in @.selectedFilters
if (filter['dataType'] == filterDataType)
return true
return false
generateFilters: (milestone) -> generateFilters: (milestone) ->
@.storeFilters(@params.pslug, @location.search(), @.storeFiltersName) @.storeFilters(@params.pslug, @location.search(), @.storeFiltersName)
@ -219,6 +226,7 @@ class UsFiltersMixin
loadFilters.assigned_to = urlfilters.assigned_to loadFilters.assigned_to = urlfilters.assigned_to
loadFilters.owner = urlfilters.owner loadFilters.owner = urlfilters.owner
loadFilters.epic = urlfilters.epic loadFilters.epic = urlfilters.epic
loadFilters.role = urlfilters.role
loadFilters.q = urlfilters.q loadFilters.q = urlfilters.q
if milestone if milestone
@ -249,6 +257,15 @@ class UsFiltersMixin
it.name = it.full_name || "Unassigned" it.name = it.full_name || "Unassigned"
return it
role = _.map data.roles, (it) ->
if it.id
it.id = it.id.toString()
else
it.id = "null"
it.name = it.name || "Unassigned"
return it return it
owner = _.map data.owners, (it) -> owner = _.map data.owners, (it) ->
it.id = it.id.toString() it.id = it.id.toString()
@ -287,6 +304,10 @@ class UsFiltersMixin
selected = @.formatSelectedFilters("epic", epic, loadFilters.epic) selected = @.formatSelectedFilters("epic", epic, loadFilters.epic)
@.selectedFilters = @.selectedFilters.concat(selected) @.selectedFilters = @.selectedFilters.concat(selected)
if loadFilters.role
selected = @.formatSelectedFilters("role", role, loadFilters.role)
@.selectedFilters = @.selectedFilters.concat(selected)
@.filterQ = loadFilters.q @.filterQ = loadFilters.q
@.filters = [ @.filters = [
@ -307,6 +328,11 @@ class UsFiltersMixin
dataType: "assigned_to", dataType: "assigned_to",
content: assignedTo content: assignedTo
}, },
{
title: @translate.instant("COMMON.FILTERS.CATEGORIES.ROLE"),
dataType: "role",
content: role
},
{ {
title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"), title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
dataType: "owner", dataType: "owner",

View File

@ -127,6 +127,12 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
delete userFilters[customFilter.id] delete userFilters[customFilter.id]
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.myFiltersHashSuffix).then(@.generateFilters) @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.myFiltersHashSuffix).then(@.generateFilters)
isFilterDataTypeSelected: (filterDataType) ->
for filter in @.selectedFilters
if (filter['dataType'] == filterDataType)
return true
return false
saveCustomFilter: (name) -> saveCustomFilter: (name) ->
filters = {} filters = {}
urlfilters = @location.search() urlfilters = @location.search()
@ -137,6 +143,7 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
filters.priority = urlfilters.priority filters.priority = urlfilters.priority
filters.assigned_to = urlfilters.assigned_to filters.assigned_to = urlfilters.assigned_to
filters.owner = urlfilters.owner filters.owner = urlfilters.owner
filters.role = urlfilters.role
@filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix).then (userFilters) => @filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix).then (userFilters) =>
userFilters[name] = filters userFilters[name] = filters
@ -157,6 +164,7 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
loadFilters.priority = urlfilters.priority loadFilters.priority = urlfilters.priority
loadFilters.assigned_to = urlfilters.assigned_to loadFilters.assigned_to = urlfilters.assigned_to
loadFilters.owner = urlfilters.owner loadFilters.owner = urlfilters.owner
loadFilters.role = urlfilters.role
loadFilters.q = urlfilters.q loadFilters.q = urlfilters.q
return @q.all([ return @q.all([
@ -203,6 +211,15 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
it.id = it.id.toString() it.id = it.id.toString()
it.name = it.full_name it.name = it.full_name
return it
role = _.map data.roles, (it) ->
if it.id
it.id = it.id.toString()
else
it.id = "null"
it.name = it.name || "Unassigned"
return it return it
@.selectedFilters = [] @.selectedFilters = []
@ -235,6 +252,10 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
selected = @.formatSelectedFilters("priority", priority, loadFilters.priority) selected = @.formatSelectedFilters("priority", priority, loadFilters.priority)
@.selectedFilters = @.selectedFilters.concat(selected) @.selectedFilters = @.selectedFilters.concat(selected)
if loadFilters.role
selected = @.formatSelectedFilters("role", role, loadFilters.role)
@.selectedFilters = @.selectedFilters.concat(selected)
@.filterQ = loadFilters.q @.filterQ = loadFilters.q
@.filters = [ @.filters = [
@ -270,6 +291,11 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
dataType: "assigned_to", dataType: "assigned_to",
content: assignedTo content: assignedTo
}, },
{
title: @translate.instant("COMMON.FILTERS.CATEGORIES.ROLE"),
dataType: "role",
content: role
},
{ {
title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"), title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
dataType: "owner", dataType: "owner",
@ -485,9 +511,8 @@ IssuesDirective = ($log, $location, $template, $compile) ->
currentOrder = $ctrl.getOrderBy() currentOrder = $ctrl.getOrderBy()
newOrder = target.data("fieldname") newOrder = target.data("fieldname")
if newOrder == 'total_voters' if newOrder == 'total_voters' and currentOrder != "-total_voters"
finalOrder = if currentOrder == newOrder then newOrder else "-#{newOrder}" currentOrder = "total_voters"
else
finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder
$scope.$apply -> $scope.$apply ->
@ -568,8 +593,9 @@ IssueStatusInlineEditionDirective = ($repo, $template, $rootscope) ->
$scope.$apply () -> $scope.$apply () ->
$repo.save(issue).then -> $repo.save(issue).then ->
$ctrl.loadIssues()
$ctrl.generateFilters() $ctrl.generateFilters()
if $ctrl.isFilterDataTypeSelected('status')
$ctrl.loadIssues()
taiga.bindOnce $scope, "project", (project) -> taiga.bindOnce $scope, "project", (project) ->
$el.append(selectionTemplate({ 'statuses': project.issue_statuses })) $el.append(selectionTemplate({ 'statuses': project.issue_statuses }))
@ -635,13 +661,17 @@ IssueAssignedToInlineEditionDirective = ($repo, $rootscope, $translate, avatarSe
$el.unbind("click") $el.unbind("click")
$el.find("a").addClass("not-clickable") $el.find("a").addClass("not-clickable")
$scope.$on "assigned-to:added", (ctx, userId, updatedIssue) => $scope.$on "assigned-to:added", (ctx, userId, updatedIssue) ->
if updatedIssue.id == issue.id if updatedIssue.id == issue.id
updatedIssue.assigned_to = userId updatedIssue.assigned_to = userId
$repo.save(updatedIssue) $repo.save(issue).then ->
updateIssue(updatedIssue) updateIssue(updatedIssue)
$ctrl.generateFilters()
if $ctrl.isFilterDataTypeSelected('assigned_to') \
|| $ctrl.isFilterDataTypeSelected('role')
$ctrl.loadIssues()
$scope.$watch $attrs.tgIssueAssignedToInlineEdition, (val) => $scope.$watch $attrs.tgIssueAssignedToInlineEdition, (val) ->
updateIssue(val) updateIssue(val)
$scope.$on "$destroy", -> $scope.$on "$destroy", ->

View File

@ -93,15 +93,17 @@ class KanbanUserstoriesService extends taiga.Service
@.refresh() @.refresh()
move: (id, statusId, index) -> move: (usList, statusId, index) ->
us = @.getUsModel(id) initialLength = usList.length
usByStatus = _.filter @.userstoriesRaw, (it) => usByStatus = _.filter @.userstoriesRaw, (it) =>
return it.status == statusId return it.status == statusId
usByStatus = _.sortBy usByStatus, (it) => @.order[it.id] usByStatus = _.sortBy usByStatus, (it) => @.order[it.id]
usByStatusWithoutMoved = _.filter usByStatus, (it) => it.id != id usByStatusWithoutMoved = _.filter usByStatus, (listIt) ->
return !_.find usList, (moveIt) -> return listIt.id == moveIt.id
beforeDestination = _.slice(usByStatusWithoutMoved, 0, index) beforeDestination = _.slice(usByStatusWithoutMoved, 0, index)
afterDestination = _.slice(usByStatusWithoutMoved, index) afterDestination = _.slice(usByStatusWithoutMoved, index)
@ -112,26 +114,54 @@ class KanbanUserstoriesService extends taiga.Service
previousWithTheSameOrder = _.filter beforeDestination, (it) => previousWithTheSameOrder = _.filter beforeDestination, (it) =>
@.order[it.id] == @.order[previous.id] @.order[it.id] == @.order[previous.id]
if previousWithTheSameOrder.length > 1 if previousWithTheSameOrder.length > 1
for it in previousWithTheSameOrder for it in previousWithTheSameOrder
setOrders[it.id] = @.order[it.id] setOrders[it.id] = @.order[it.id]
if !previous and (!afterDestination or afterDestination.length == 0) modifiedUs = []
@.order[us.id] = 0 setPreviousOrders = []
else if !previous and afterDestination and afterDestination.length > 0 setNextOrders = []
@.order[us.id] = @.order[afterDestination[0].id] - 1
if !previous
startIndex = 0
else if previous else if previous
@.order[us.id] = @.order[previous.id] + 1 startIndex = @.order[previous.id] + 1
for it, key in afterDestination previousWithTheSameOrder = _.filter(beforeDestination, (it) =>
@.order[it.id] = @.order[us.id] + key + 1 it.kanban_order == @.order[previous.id]
)
for it, key in afterDestination # increase position of the us after the dragged us's
@.order[it.id] = @.order[previous.id] + key + initialLength + 1
it.kanban_order = @.order[it.id]
setNextOrders = _.map(afterDestination, (it) =>
{us_id: it.id, order: @.order[it.id]}
)
# we must send the USs previous to the dropped USs to tell the backend
# which USs are before the dropped USs, if they have the same value to
# order, the backend doens't know after which one do you want to drop
# the USs
if previousWithTheSameOrder.length > 1
setPreviousOrders = _.map(previousWithTheSameOrder, (it) =>
{us_id: it.id, order: @.order[it.id]}
)
for us, key in usList
us.status = statusId us.status = statusId
us.kanban_order = @.order[us.id] us.kanban_order = startIndex + key
@.order[us.id] = us.kanban_order
modifiedUs.push({us_id: us.id, order: us.kanban_order})
@.refresh() @.refresh()
return {"us_id": us.id, "order": @.order[us.id], "set_orders": setOrders} return {
bulkOrders: modifiedUs.concat(setPreviousOrders, setNextOrders),
usList: modifiedUs,
set_orders: setOrders
}
moveToEnd: (id, statusId) -> moveToEnd: (id, statusId) ->
us = @.getUsModel(id) us = @.getUsModel(id)

View File

@ -71,6 +71,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
bindMethods(@) bindMethods(@)
@kanbanUserstoriesService.reset() @kanbanUserstoriesService.reset()
@.openFilter = false @.openFilter = false
@.selectedUss = {}
return if @.applyStoredFilters(@params.pslug, "kanban-filters") return if @.applyStoredFilters(@params.pslug, "kanban-filters")
@ -80,6 +81,13 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
taiga.defineImmutableProperty @.scope, "usByStatus", () => taiga.defineImmutableProperty @.scope, "usByStatus", () =>
return @kanbanUserstoriesService.usByStatus return @kanbanUserstoriesService.usByStatus
cleanSelectedUss: () ->
for key of @.selectedUss
@.selectedUss[key] = false
toggleSelectedUs: (usId) ->
@.selectedUss[usId] = !@.selectedUss[usId]
firstLoad: () -> firstLoad: () ->
promise = @.loadInitialData() promise = @.loadInitialData()
@ -191,9 +199,10 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@kanbanUserstoriesService.replaceModel(usModel) @kanbanUserstoriesService.replaceModel(usModel)
promise = @repo.save(usModel) @repo.save(usModel).then =>
promise.then null, -> @.generateFilters()
console.log "FAIL" # TODO if @.isFilterDataTypeSelected('assigned_to') || @.isFilterDataTypeSelected('role')
@.filtersReloadContent()
refreshTagsColors: -> refreshTagsColors: ->
return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) => return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) =>
@ -291,28 +300,36 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
prepareBulkUpdateData: (uses, field="kanban_order") -> prepareBulkUpdateData: (uses, field="kanban_order") ->
return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]}) return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]})
moveUs: (ctx, us, oldStatusId, newStatusId, index) -> moveUs: (ctx, usList, newStatusId, index) ->
us = @kanbanUserstoriesService.getUsModel(us.get('id')) @.cleanSelectedUss()
newStatus = @scope.usStatusById[newStatusId]
if newStatus.is_archived and !@scope.usByStatus.get(newStatusId.toString()) usList = _.map usList, (us) =>
moveUpdateData = @kanbanUserstoriesService.moveToEnd(us.id, newStatusId) return @kanbanUserstoriesService.getUsModel(us.id)
else
moveUpdateData = @kanbanUserstoriesService.move(us.id, newStatusId, index) data = @kanbanUserstoriesService.move(usList, newStatusId, index)
promise = @rs.userstories.bulkUpdateKanbanOrder(@scope.projectId, data.bulkOrders)
promise.then () =>
# saving
# drag single or different status
options = {
headers: {
"set-orders": JSON.stringify(data.setOrders)
}
}
params = { params = {
include_attachments: true, include_attachments: true,
include_tasks: true include_tasks: true
} }
options = { promises = _.map usList, (us) =>
headers: { @repo.save(us, true, params, options, true)
"set-orders": JSON.stringify(moveUpdateData.set_orders)
}
}
promise = @repo.save(us, true, params, options, true) promise = @q.all(promises)
promise = promise.then (result) => promise.then (result) =>
headers = result[1] headers = result[1]
if headers && headers['taiga-info-order-updated'] if headers && headers['taiga-info-order-updated']
@ -320,8 +337,13 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@kanbanUserstoriesService.assignOrders(order) @kanbanUserstoriesService.assignOrders(order)
@scope.$broadcast("redraw:wip") @scope.$broadcast("redraw:wip")
@.generateFilters()
if @.isFilterDataTypeSelected('status')
@.filtersReloadContent()
return promise return promise
module.controller("KanbanController", KanbanController) module.controller("KanbanController", KanbanController)
############################################################################# #############################################################################

View File

@ -48,7 +48,6 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
if not ($scope.project.my_permissions.indexOf("modify_us") > -1) if not ($scope.project.my_permissions.indexOf("modify_us") > -1)
return return
oldParentScope = null
newParentScope = null newParentScope = null
itemEl = null itemEl = null
tdom = $el tdom = $el
@ -70,23 +69,44 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
}) })
drake.on 'drag', (item) -> drake.on 'drag', (item) ->
oldParentScope = $(item).parent().scope() window.dragMultiple.start(item, containers)
drake.on 'cloned', (item, dropTarget) ->
$(item).addClass('multiple-drag-mirror')
drake.on 'dragend', (item) -> drake.on 'dragend', (item) ->
parentEl = $(item).parent() parentEl = $(item).parent()
itemEl = $(item)
itemUs = itemEl.scope().us
itemIndex = itemEl.index()
newParentScope = parentEl.scope() newParentScope = parentEl.scope()
newStatusId = newParentScope.s.id newStatusId = newParentScope.s.id
oldStatusId = oldParentScope.s.id dragMultipleItems = window.dragMultiple.stop()
if newStatusId != oldStatusId # if it is not drag multiple
deleteElement(itemEl) if !dragMultipleItems.length
dragMultipleItems = [item]
firstElement = dragMultipleItems[0]
index = $(firstElement).index()
newStatus = newParentScope.s.id
usList = _.map dragMultipleItems, (item) -> $(item).scope().us
finalUsList = _.map usList, (item) ->
return {
id: item.get('id'),
oldStatusId: item.getIn(['model', 'status'])
}
$scope.$apply -> $scope.$apply ->
$rootscope.$broadcast("kanban:us:move", itemUs, itemUs.getIn(['model', 'status']), newStatusId, itemIndex) _.each usList, (item, key) =>
oldStatus = item.getIn(['model', 'status'])
sameContainer = newStatus == oldStatus
if !sameContainer
itemEl = $(dragMultipleItems[key])
deleteElement(itemEl)
$rootscope.$broadcast("kanban:us:move", finalUsList, newStatus, index)
scroll = autoScroll(containers, { scroll = autoScroll(containers, {
margin: 100, margin: 100,

View File

@ -29,7 +29,7 @@ debounce = @.taiga.debounce
module = angular.module("taigaRelatedTasks", []) module = angular.module("taigaRelatedTasks", [])
RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading, $template, $translate) -> RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading, $template, $translate, $emojis) ->
templateView = $template.get("task/related-task-row.html", true) templateView = $template.get("task/related-task-row.html", true)
templateEdit = $template.get("task/related-task-row-edit.html", true) templateEdit = $template.get("task/related-task-row-edit.html", true)
@ -82,7 +82,11 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading, $tem
delete_task: $scope.project.my_permissions.indexOf("delete_task") != -1 delete_task: $scope.project.my_permissions.indexOf("delete_task") != -1
} }
$el.html($compile(templateView({task: task, perms: perms}))($scope)) $el.html($compile(templateView({
task: task,
perms: perms,
emojify: (text) -> $emojis.replaceEmojiNameByHtmlImgs(_.escape(text))
}))($scope))
$el.on "click", ".edit-task", -> $el.on "click", ".edit-task", ->
renderEdit($model.$modelValue) renderEdit($model.$modelValue)
@ -119,7 +123,7 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading, $tem
return {link:link, require:"ngModel"} return {link:link, require:"ngModel"}
module.directive("tgRelatedTaskRow", ["$tgRepo", "$compile", "$tgConfirm", "$rootScope", "$tgLoading", module.directive("tgRelatedTaskRow", ["$tgRepo", "$compile", "$tgConfirm", "$rootScope", "$tgLoading",
"$tgTemplate", "$translate", RelatedTaskRowDirective]) "$tgTemplate", "$translate", "$tgEmojis", RelatedTaskRowDirective])
RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel, $loading, $analytics) -> RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel, $loading, $analytics) ->

View File

@ -27,7 +27,7 @@ bindOnce = @.taiga.bindOnce
debounce = @.taiga.debounce debounce = @.taiga.debounce
trim = @.taiga.trim trim = @.taiga.trim
CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxService, $translate, $q, attachmentsService) -> CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxService, $translate, $q, $confirm, attachmentsService) ->
link = ($scope, $el, attrs) -> link = ($scope, $el, attrs) ->
$scope.isNew = true $scope.isNew = true
@ -100,15 +100,17 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
_.pull($scope.task.tags, value) _.pull($scope.task.tags, value)
$scope.$on "taskform:new", (ctx, sprintId, usId) -> $scope.$on "taskform:new", (ctx, sprintId, usId) ->
$scope.task = { $scope.task = $model.make_model('tasks', {
project: $scope.projectId project: $scope.projectId
milestone: sprintId milestone: sprintId
user_story: usId user_story: usId
is_archived: false is_archived: false
status: $scope.project.default_task_status status: $scope.project.default_task_status
assigned_to: null assigned_to: null
tags: [] tags: [],
} subject: "",
description: "",
})
$scope.isNew = true $scope.isNew = true
$scope.attachments = Immutable.List() $scope.attachments = Immutable.List()
@ -187,7 +189,30 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
$el.on "submit", "form", submit $el.on "submit", "form", submit
close = () =>
if !$scope.task.isModified()
lightboxService.close($el)
$scope.$apply ->
$scope.task.revert()
else
$confirm.ask($translate.instant("LIGHTBOX.CREATE_EDIT_TASK.CONFIRM_CLOSE")).then (result) ->
lightboxService.close($el)
$scope.task.revert()
result.finish()
$el.find('.close').on "click", (event) ->
event.preventDefault()
event.stopPropagation()
close()
$el.keydown (event) ->
event.stopPropagation()
code = if event.keyCode then event.keyCode else event.which
if code == 27
close()
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.find('.close').off()
$el.off() $el.off()
return {link: link} return {link: link}
@ -250,6 +275,7 @@ module.directive("tgLbCreateEditTask", [
"lightboxService", "lightboxService",
"$translate", "$translate",
"$q", "$q",
"$tgConfirm",
"tgAttachmentsService", "tgAttachmentsService",
CreateEditTaskDirective CreateEditTaskDirective
]) ])

View File

@ -134,6 +134,12 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, 'tasks-custom-filters').then(@.generateFilters) @filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, 'tasks-custom-filters').then(@.generateFilters)
isFilterDataTypeSelected: (filterDataType) ->
for filter in @.selectedFilters
if (filter['dataType'] == filterDataType)
return true
return false
saveCustomFilter: (name) -> saveCustomFilter: (name) ->
filters = {} filters = {}
urlfilters = @location.search() urlfilters = @location.search()
@ -141,6 +147,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
filters.status = urlfilters.status filters.status = urlfilters.status
filters.assigned_to = urlfilters.assigned_to filters.assigned_to = urlfilters.assigned_to
filters.owner = urlfilters.owner filters.owner = urlfilters.owner
filters.role = urlfilters.role
@filterRemoteStorageService.getFilters(@scope.projectId, 'tasks-custom-filters').then (userFilters) => @filterRemoteStorageService.getFilters(@scope.projectId, 'tasks-custom-filters').then (userFilters) =>
userFilters[name] = filters userFilters[name] = filters
@ -159,6 +166,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
loadFilters.status = urlfilters.status loadFilters.status = urlfilters.status
loadFilters.assigned_to = urlfilters.assigned_to loadFilters.assigned_to = urlfilters.assigned_to
loadFilters.owner = urlfilters.owner loadFilters.owner = urlfilters.owner
loadFilters.role = urlfilters.role
loadFilters.q = urlfilters.q loadFilters.q = urlfilters.q
return @q.all([ return @q.all([
@ -188,6 +196,15 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
it.name = it.full_name || "Unassigned" it.name = it.full_name || "Unassigned"
return it
role = _.map data.roles, (it) ->
if it.id
it.id = it.id.toString()
else
it.id = "null"
it.name = it.name || "Unassigned"
return it return it
owner = _.map data.owners, (it) -> owner = _.map data.owners, (it) ->
it.id = it.id.toString() it.id = it.id.toString()
@ -213,6 +230,10 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
selected = @.formatSelectedFilters("owner", owner, loadFilters.owner) selected = @.formatSelectedFilters("owner", owner, loadFilters.owner)
@.selectedFilters = @.selectedFilters.concat(selected) @.selectedFilters = @.selectedFilters.concat(selected)
if loadFilters.role
selected = @.formatSelectedFilters("role", role, loadFilters.role)
@.selectedFilters = @.selectedFilters.concat(selected)
@.filterQ = loadFilters.q @.filterQ = loadFilters.q
@.filters = [ @.filters = [
@ -233,6 +254,11 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
dataType: "assigned_to", dataType: "assigned_to",
content: assignedTo content: assignedTo
}, },
{
title: @translate.instant("COMMON.FILTERS.CATEGORIES.ROLE"),
dataType: "role",
content: role
},
{ {
title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"), title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
dataType: "owner", dataType: "owner",
@ -290,9 +316,10 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
@taskboardTasksService.replaceModel(taskModel) @taskboardTasksService.replaceModel(taskModel)
promise = @repo.save(taskModel) @repo.save(taskModel).then =>
promise.then null, -> @.generateFilters()
console.log "FAIL" # TODO if @.isFilterDataTypeSelected('assigned_to') || @.isFilterDataTypeSelected('role')
@.loadTasks()
initializeSubscription: -> initializeSubscription: ->
routingKey = "changes.project.#{@scope.projectId}.tasks" routingKey = "changes.project.#{@scope.projectId}.tasks"
@ -438,6 +465,10 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
@taskboardTasksService.assignOrders(order) @taskboardTasksService.assignOrders(order)
@.loadSprintStats() @.loadSprintStats()
@.generateFilters()
if @.isFilterDataTypeSelected('status')
@.loadTasks()
## Template actions ## Template actions
addNewTask: (type, us) -> addNewTask: (type, us) ->

View File

@ -7,8 +7,8 @@
var reset = function(elm) { var reset = function(elm) {
$(elm) $(elm)
.removeAttr('style') .removeAttr('style')
.removeClass('tg-backlog-us-mirror') .removeClass('tg-multiple-drag-mirror')
.removeClass('backlog-us-mirror') .removeClass('multiple-drag-mirror')
.data('dragMultipleIndex', null) .data('dragMultipleIndex', null)
.data('dragMultipleActive', false); .data('dragMultipleActive', false);
}; };
@ -40,6 +40,8 @@
var currentTop = shadow.position().top; var currentTop = shadow.position().top;
var height = shadow.outerHeight(); var height = shadow.outerHeight();
$('.gu-transit').addClass('gu-transit-multi');
_.forEach(dragMultiple.items.draggingItems, function(elm, index) { _.forEach(dragMultiple.items.draggingItems, function(elm, index) {
var elmIndex = parseInt(elm.data('dragMultipleIndex'), 10); var elmIndex = parseInt(elm.data('dragMultipleIndex'), 10);
var top = currentTop + (elmIndex * height); var top = currentTop + (elmIndex * height);
@ -57,22 +59,21 @@
refreshOriginal(); refreshOriginal();
var current = dragMultiple.items.elm;
var container = dragMultiple.items.container;
document.documentElement.removeEventListener('mousemove', removeEventFn); document.documentElement.removeEventListener('mousemove', removeEventFn);
// reset // reset
dragMultiple.items = {}; dragMultiple.items = {};
$('.' + mainClass).removeClass(mainClass); $('.' + mainClass).removeClass(mainClass);
$('.tg-backlog-us-mirror').remove(); $('.tg-multiple-drag-mirror').remove();
$('.backlog-us-mirror').removeClass('backlog-us-mirror'); $('.multiple-drag-mirror').removeClass('multiple-drag-mirror');
$('.tg-backlog-us-dragging') $('.tg-multiple-drag-dragging')
.removeClass('tg-backlog-us-dragging') .removeClass('tg-multiple-drag-dragging')
.show(); .show();
$('.gu-transit-multi').removeClass('gu-transit-multi');
return $('.' + multipleSortableClass); return $('.' + multipleSortableClass);
}; };
@ -180,8 +181,8 @@
clone = $(item).clone(true); clone = $(item).clone(true);
clone clone
.addClass('backlog-us-mirror') .addClass('multiple-drag-mirror')
.addClass('tg-backlog-us-mirror') .addClass('tg-multiple-drag-mirror')
.data('dragmultiple:originalPosition', $(item).position()) .data('dragmultiple:originalPosition', $(item).position())
.data('dragMultipleActive', true) .data('dragMultipleActive', true)
.css({ .css({
@ -194,7 +195,7 @@
$(item) $(item)
.hide() .hide()
.addClass('tg-backlog-us-dragging'); .addClass('tg-multiple-drag-dragging');
return clone; return clone;
}); });

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Prioritats", "PRIORITIES": "Prioritats",
"TAGS": "Etiquetes", "TAGS": "Etiquetes",
"ASSIGNED_TO": "Assignat a", "ASSIGNED_TO": "Assignat a",
"ROLE": "Rol",
"CREATED_BY": "Creat per", "CREATED_BY": "Creat per",
"CUSTOM_FILTERS": "Filtres personalitzats", "CUSTOM_FILTERS": "Filtres personalitzats",
"EPIC": "Epic" "EPIC": "Epic"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Prioritäten", "PRIORITIES": "Prioritäten",
"TAGS": "Schlagwörter", "TAGS": "Schlagwörter",
"ASSIGNED_TO": "Zugeordnet zu", "ASSIGNED_TO": "Zugeordnet zu",
"ROLE": "Rolle",
"CREATED_BY": "Erstellt durch", "CREATED_BY": "Erstellt durch",
"CUSTOM_FILTERS": "Benutzerfilter", "CUSTOM_FILTERS": "Benutzerfilter",
"EPIC": "Epic" "EPIC": "Epic"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Priorities", "PRIORITIES": "Priorities",
"TAGS": "Tags", "TAGS": "Tags",
"ASSIGNED_TO": "Assigned to", "ASSIGNED_TO": "Assigned to",
"ROLE": "Role",
"CREATED_BY": "Created by", "CREATED_BY": "Created by",
"CUSTOM_FILTERS": "Custom filters", "CUSTOM_FILTERS": "Custom filters",
"EPIC": "Epic" "EPIC": "Epic"
@ -1016,7 +1017,7 @@
"CONFIRM": "Are you sure you want to delete your Taiga account?", "CONFIRM": "Are you sure you want to delete your Taiga account?",
"CANCEL": "Back to settings", "CANCEL": "Back to settings",
"ACCEPT": "Delete account", "ACCEPT": "Delete account",
"BLOCK_PROJECT": "Note that all the projects you own projects will be <strong>blocked</strong> after you delete your account. If you do want a project blocked, transfer ownership to another member of each project prior to deleting your account." "BLOCK_PROJECT": "Note that all the projects you own projects will be <strong>blocked</strong> after you delete your account. If you do not want a project blocked, transfer ownership to another member of each project prior to deleting your account. "
}, },
"DELETE_PROJECT": { "DELETE_PROJECT": {
"TITLE": "Delete project", "TITLE": "Delete project",
@ -1065,13 +1066,15 @@
"PLACEHOLDER_STATUS": "Task status", "PLACEHOLDER_STATUS": "Task status",
"OPTION_UNASSIGNED": "Unassigned", "OPTION_UNASSIGNED": "Unassigned",
"PLACEHOLDER_SHORT_DESCRIPTION": "Type a short description", "PLACEHOLDER_SHORT_DESCRIPTION": "Type a short description",
"ACTION_EDIT": "Edit task" "ACTION_EDIT": "Edit task",
"CONFIRM_CLOSE": "You have not saved changes.\nAre you sure you want to close the form?"
}, },
"CREATE_EDIT_US": { "CREATE_EDIT_US": {
"TITLE": "New US", "TITLE": "New US",
"PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this US", "PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this US",
"NEW_US": "New user story", "NEW_US": "New user story",
"EDIT_US": "Edit user story" "EDIT_US": "Edit user story",
"CONFIRM_CLOSE": "You have not saved changes.\nAre you sure you want to close the form?"
}, },
"DELETE_SPRINT": { "DELETE_SPRINT": {
"TITLE": "Delete sprint" "TITLE": "Delete sprint"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Prioridades", "PRIORITIES": "Prioridades",
"TAGS": "Etiquetas", "TAGS": "Etiquetas",
"ASSIGNED_TO": "Asignado a", "ASSIGNED_TO": "Asignado a",
"ROLE": "Rol",
"CREATED_BY": "Creada por", "CREATED_BY": "Creada por",
"CUSTOM_FILTERS": "Filtros personalizados", "CUSTOM_FILTERS": "Filtros personalizados",
"EPIC": "Épica" "EPIC": "Épica"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Kiireellisyydet", "PRIORITIES": "Kiireellisyydet",
"TAGS": "Avainsanat", "TAGS": "Avainsanat",
"ASSIGNED_TO": "Tekijä", "ASSIGNED_TO": "Tekijä",
"ROLE": "Rooli",
"CREATED_BY": "Luoja", "CREATED_BY": "Luoja",
"CUSTOM_FILTERS": "Omat suodattimet", "CUSTOM_FILTERS": "Omat suodattimet",
"EPIC": "Eepos" "EPIC": "Eepos"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Priorités", "PRIORITIES": "Priorités",
"TAGS": "Mots-clés", "TAGS": "Mots-clés",
"ASSIGNED_TO": "Affecté à", "ASSIGNED_TO": "Affecté à",
"ROLE": "Rôle",
"CREATED_BY": "Créé par", "CREATED_BY": "Créé par",
"CUSTOM_FILTERS": "Filtres personnalisés", "CUSTOM_FILTERS": "Filtres personnalisés",
"EPIC": "Épopée" "EPIC": "Épopée"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Priorità", "PRIORITIES": "Priorità",
"TAGS": "Tag", "TAGS": "Tag",
"ASSIGNED_TO": "Assegnato a", "ASSIGNED_TO": "Assegnato a",
"ROLE": "Ruolo",
"CREATED_BY": "Creato da", "CREATED_BY": "Creato da",
"CUSTOM_FILTERS": "Filtri personalizzati", "CUSTOM_FILTERS": "Filtri personalizzati",
"EPIC": "Epic" "EPIC": "Epic"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "優先度", "PRIORITIES": "優先度",
"TAGS": "タグ", "TAGS": "タグ",
"ASSIGNED_TO": "担当者", "ASSIGNED_TO": "担当者",
"ROLE": "役割",
"CREATED_BY": "作成者", "CREATED_BY": "作成者",
"CUSTOM_FILTERS": "カスタムフィルター", "CUSTOM_FILTERS": "カスタムフィルター",
"EPIC": "エピック" "EPIC": "エピック"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "우선순위", "PRIORITIES": "우선순위",
"TAGS": "태그", "TAGS": "태그",
"ASSIGNED_TO": "할당됨", "ASSIGNED_TO": "할당됨",
"ROLE": "역할",
"CREATED_BY": "생성함", "CREATED_BY": "생성함",
"CUSTOM_FILTERS": "사용자 정의 필터", "CUSTOM_FILTERS": "사용자 정의 필터",
"EPIC": "에픽" "EPIC": "에픽"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Prioriteter", "PRIORITIES": "Prioriteter",
"TAGS": "Etiketter", "TAGS": "Etiketter",
"ASSIGNED_TO": "Tildelt til", "ASSIGNED_TO": "Tildelt til",
"ROLE": "Rolle",
"CREATED_BY": "Laget av", "CREATED_BY": "Laget av",
"CUSTOM_FILTERS": "Egendefinert filtre", "CUSTOM_FILTERS": "Egendefinert filtre",
"EPIC": "Epic" "EPIC": "Epic"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Prioriteit", "PRIORITIES": "Prioriteit",
"TAGS": "Tags", "TAGS": "Tags",
"ASSIGNED_TO": "Toegewezen aan", "ASSIGNED_TO": "Toegewezen aan",
"ROLE": "Rol",
"CREATED_BY": "Aangemaakt door", "CREATED_BY": "Aangemaakt door",
"CUSTOM_FILTERS": "Eigen filters", "CUSTOM_FILTERS": "Eigen filters",
"EPIC": "Epic" "EPIC": "Epic"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Priorytety", "PRIORITIES": "Priorytety",
"TAGS": "Tagi", "TAGS": "Tagi",
"ASSIGNED_TO": "Przypisane do", "ASSIGNED_TO": "Przypisane do",
"ROLE": "Rola",
"CREATED_BY": "Stworzona przez", "CREATED_BY": "Stworzona przez",
"CUSTOM_FILTERS": "Filtry niestandardowe", "CUSTOM_FILTERS": "Filtry niestandardowe",
"EPIC": "Epic" "EPIC": "Epic"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Prioridades", "PRIORITIES": "Prioridades",
"TAGS": "Tags", "TAGS": "Tags",
"ASSIGNED_TO": "Atribuído a", "ASSIGNED_TO": "Atribuído a",
"ROLE": "Função",
"CREATED_BY": "Criado por", "CREATED_BY": "Criado por",
"CUSTOM_FILTERS": "Filtros personalizados", "CUSTOM_FILTERS": "Filtros personalizados",
"EPIC": "Épico" "EPIC": "Épico"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Приоритеты", "PRIORITIES": "Приоритеты",
"TAGS": "Тэги", "TAGS": "Тэги",
"ASSIGNED_TO": "Назначено", "ASSIGNED_TO": "Назначено",
"ROLE": "Роль",
"CREATED_BY": "Создано", "CREATED_BY": "Создано",
"CUSTOM_FILTERS": "Собственные фильтры", "CUSTOM_FILTERS": "Собственные фильтры",
"EPIC": "Epic" "EPIC": "Epic"
@ -217,7 +218,7 @@
"DB_CLICK": "двойной клик для редактирования", "DB_CLICK": "двойной клик для редактирования",
"SELECT_LANGUAGE_PLACEHOLDER": "Выбор языка", "SELECT_LANGUAGE_PLACEHOLDER": "Выбор языка",
"SELECT_LANGUAGE_REMOVE_FORMATING": "Убрать форматирование", "SELECT_LANGUAGE_REMOVE_FORMATING": "Убрать форматирование",
"OUTDATED": "Another person has made changes while you were editing. Check the new version on the activity tab before you save your changes.", "OUTDATED": "Другой пользователь внес изменения в систему. Проверьте новую версию на вкладке активности, прежде чем вы сохраните ваши изменения.",
"MARKDOWN_HELP": "Помощь по синтаксису Markdown" "MARKDOWN_HELP": "Помощь по синтаксису Markdown"
}, },
"PERMISIONS_CATEGORIES": { "PERMISIONS_CATEGORIES": {
@ -611,7 +612,7 @@
"WARNING_DELETE_ROLE": "Be careful! All role estimations will be removed", "WARNING_DELETE_ROLE": "Be careful! All role estimations will be removed",
"ERROR_DELETE_ALL": "Вы не можете удалить все значения", "ERROR_DELETE_ALL": "Вы не можете удалить все значения",
"EXTERNAL_USER": "Внешний пользователь", "EXTERNAL_USER": "Внешний пользователь",
"NOTE_EXTERNAL_USERS": "<strong>Note:</strong> by External User we mean any anonymous user not belonging to the Taiga platform, including search engines. Please use this role with care." "NOTE_EXTERNAL_USERS": "<strong>Примечание:</strong> под 'внешним пользователем' мы имеем ввиду анонимного пользователя, не имеющего платформы Taiga, включая поисковые движки. Пожалуйста, используйте этот функционал с осторожностью."
}, },
"THIRD_PARTIES": { "THIRD_PARTIES": {
"SECRET_KEY": "Секретный ключ", "SECRET_KEY": "Секретный ключ",
@ -689,7 +690,7 @@
"DEFAULT_DELETE_MESSAGE": "приглашение на {{email}}" "DEFAULT_DELETE_MESSAGE": "приглашение на {{email}}"
}, },
"DEFAULT_VALUES": { "DEFAULT_VALUES": {
"LABEL_EPIC_STATUS": "Default value for epic status selector", "LABEL_EPIC_STATUS": "Значение по умолчанию для селектора эпического статуса",
"LABEL_US_STATUS": "Выбор значения статуса по умолчанию для пользовательской истории", "LABEL_US_STATUS": "Выбор значения статуса по умолчанию для пользовательской истории",
"LABEL_POINTS": "Значения по умолчанию для выбора очков", "LABEL_POINTS": "Значения по умолчанию для выбора очков",
"LABEL_TASK_STATUS": "Значение по умолчанию для статуса задачи", "LABEL_TASK_STATUS": "Значение по умолчанию для статуса задачи",
@ -875,7 +876,7 @@
"TEMPLATE_SCRUM_LONGDESC": "Скрам это методология управления процессом разработки программного обеспечения построенная на коротких итерациях и постепенном прогрессе. Бэклог по продукту это те задачи которые нужно сделать, отсортированные в порядке приоритета. Бэклоги разбиваются на относительно короткие, управляемые куски - спринты. На основании возможностей, навыков и ресурсов команды в каждый спринт вносится несколько выбранных пользовательских историй из бэклога. На каждый спринт выделяется оговоренный период времени. С развитием проекта бэклог уменьшается.", "TEMPLATE_SCRUM_LONGDESC": "Скрам это методология управления процессом разработки программного обеспечения построенная на коротких итерациях и постепенном прогрессе. Бэклог по продукту это те задачи которые нужно сделать, отсортированные в порядке приоритета. Бэклоги разбиваются на относительно короткие, управляемые куски - спринты. На основании возможностей, навыков и ресурсов команды в каждый спринт вносится несколько выбранных пользовательских историй из бэклога. На каждый спринт выделяется оговоренный период времени. С развитием проекта бэклог уменьшается.",
"TEMPLATE_KANBAN": "Kanban", "TEMPLATE_KANBAN": "Kanban",
"TEMPLATE_KANBAN_DESC": "Придерживайтесь одинаковых принципов работы над независимыми задачами", "TEMPLATE_KANBAN_DESC": "Придерживайтесь одинаковых принципов работы над независимыми задачами",
"TEMPLATE_KANBAN_LONGDESC": "The Kanban methodology is used to divide project development (any sort of project) into stages.\nA kanban card is like an index card or post-it note that details every task (or user story) in a project that needs to be completed. The Kanban board is used to move each card from one state of completion to the next and in so doing, helps track progress.", "TEMPLATE_KANBAN_LONGDESC": "Метод \"Канбан\" используется, чтобы разделить разработку проекта (проекта любого типа) на стадии. В этой методологоии, карточки похожи на примечания, которые описывают каждую задачу (или пользовательскую историю) проекта, который должен быть выполнен. Карточки в системе, построенной на методе \"Канбан\", обычно переводятся из одного состояния завершенности в другое, таким образом позволяя отслеживать прогресс всего проекта.",
"DUPLICATE": "Клонировать проект", "DUPLICATE": "Клонировать проект",
"DUPLICATE_DESC": "Начните сначала и сохраняйте вашу конфигурацию", "DUPLICATE_DESC": "Начните сначала и сохраняйте вашу конфигурацию",
"IMPORT": "Импортировать проект", "IMPORT": "Импортировать проект",
@ -922,7 +923,7 @@
"CHOOSE": "Выбрать пользователя", "CHOOSE": "Выбрать пользователя",
"LINKS": "Связь с {{platform}}", "LINKS": "Связь с {{platform}}",
"LINKS_DESCRIPTION": "Do you want to keep the link of each item with the original {{platform}} card?", "LINKS_DESCRIPTION": "Do you want to keep the link of each item with the original {{platform}} card?",
"WARNING_MAIL_USER": "Note that if the user does not have a Taiga account we will not be able to assign the tasks to him.", "WARNING_MAIL_USER": "Заметьте, если у пользователя нету аккаунта Taiga, мы не сможем назначить ему задачи.",
"ASSIGN": "Назначить", "ASSIGN": "Назначить",
"PROJECT_SELECTOR": { "PROJECT_SELECTOR": {
"NO_RESULTS": "It looks like nothing was found with your search criteria", "NO_RESULTS": "It looks like nothing was found with your search criteria",
@ -1140,7 +1141,7 @@
"TITLE_DELETE_ACTION": "Удалить пользовательскую историю", "TITLE_DELETE_ACTION": "Удалить пользовательскую историю",
"LIGHTBOX_TITLE_BLOKING_US": "Блокирующая ПИ", "LIGHTBOX_TITLE_BLOKING_US": "Блокирующая ПИ",
"NOT_ESTIMATED": "Не оценено", "NOT_ESTIMATED": "Не оценено",
"OWNER_US": "This User Story belongs to", "OWNER_US": "Эта пользовательская история принадлежит",
"TRIBE": { "TRIBE": {
"PUBLISH": "Publish as Gig in Taiga Tribe", "PUBLISH": "Publish as Gig in Taiga Tribe",
"PUBLISH_INFO": "Больше инфо", "PUBLISH_INFO": "Больше инфо",
@ -1551,7 +1552,7 @@
"TASK_UPDATED_WITH_US": "{{username}} изменил атрибут \"{{field_name}}\" задачи {{obj_name}}, которая принадлежит ПИ {{us_name}}", "TASK_UPDATED_WITH_US": "{{username}} изменил атрибут \"{{field_name}}\" задачи {{obj_name}}, которая принадлежит ПИ {{us_name}}",
"TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} установил атрибут \"{{field_name}}\" задачи {{obj_name}}, которая принадлежит ПИ {{us_name}}, на {{new_value}}", "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} установил атрибут \"{{field_name}}\" задачи {{obj_name}}, которая принадлежит ПИ {{us_name}}, на {{new_value}}",
"WIKI_UPDATED": "{{username}} обновил вики-страницу {{obj_name}}", "WIKI_UPDATED": "{{username}} обновил вики-страницу {{obj_name}}",
"EPIC_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the epic {{obj_name}}", "EPIC_UPDATED": "{{username}} обновил атрибут \"{{field_name}}\" ПИ {{obj_name}}",
"EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} обновил атрибут поля \"{{field_name}}\" эпика {{obj_name}} на {{new_value}}", "EPIC_UPDATED_WITH_NEW_VALUE": "{{username}} обновил атрибут поля \"{{field_name}}\" эпика {{obj_name}} на {{new_value}}",
"EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} обновил поле \"{{field_name}}\" эпика {{obj_name}} на <span class=\"new-color\" style=\"background: {{new_value}}\"></span>", "EPIC_UPDATED_WITH_NEW_COLOR": "{{username}} обновил поле \"{{field_name}}\" эпика {{obj_name}} на <span class=\"new-color\" style=\"background: {{new_value}}\"></span>",
"NEW_COMMENT_US": "{{username}} прокомментировал ПИ {{obj_name}}", "NEW_COMMENT_US": "{{username}} прокомментировал ПИ {{obj_name}}",

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Prioritet", "PRIORITIES": "Prioritet",
"TAGS": "Etiketter", "TAGS": "Etiketter",
"ASSIGNED_TO": "Tilldelad till", "ASSIGNED_TO": "Tilldelad till",
"ROLE": "Roll",
"CREATED_BY": "Skapad av", "CREATED_BY": "Skapad av",
"CUSTOM_FILTERS": "Anpassad filter", "CUSTOM_FILTERS": "Anpassad filter",
"EPIC": "Epost" "EPIC": "Epost"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "Öncelikler", "PRIORITIES": "Öncelikler",
"TAGS": "Etiketler ", "TAGS": "Etiketler ",
"ASSIGNED_TO": "Atanmış", "ASSIGNED_TO": "Atanmış",
"ROLE": "Rol",
"CREATED_BY": "Oluşturan", "CREATED_BY": "Oluşturan",
"CUSTOM_FILTERS": "Özel filtreler", "CUSTOM_FILTERS": "Özel filtreler",
"EPIC": "Destan" "EPIC": "Destan"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "优先级", "PRIORITIES": "优先级",
"TAGS": "标签", "TAGS": "标签",
"ASSIGNED_TO": "指派给", "ASSIGNED_TO": "指派给",
"ROLE": "角色",
"CREATED_BY": "由创建", "CREATED_BY": "由创建",
"CUSTOM_FILTERS": "定制过滤器", "CUSTOM_FILTERS": "定制过滤器",
"EPIC": "史诗" "EPIC": "史诗"

View File

@ -207,6 +207,7 @@
"PRIORITIES": "優先性", "PRIORITIES": "優先性",
"TAGS": "標籤", "TAGS": "標籤",
"ASSIGNED_TO": "指派給 ", "ASSIGNED_TO": "指派給 ",
"ROLE": "角色",
"CREATED_BY": "由創建", "CREATED_BY": "由創建",
"CUSTOM_FILTERS": "客製過濾器 ", "CUSTOM_FILTERS": "客製過濾器 ",
"EPIC": "Epic" "EPIC": "Epic"

View File

@ -3,7 +3,8 @@ span.belong-to-epic-text-wrapper(tg-repeat="epic in immutable_epics track by epi
a.belong-to-epic-text( a.belong-to-epic-text(
href="" href=""
tg-nav="project-epics-detail:project=epic.getIn(['project', 'slug']),ref=epic.get('ref')" tg-nav="project-epics-detail:project=epic.getIn(['project', 'slug']),ref=epic.get('ref')"
) #{hash}{{epic.get('id')}} {{epic.get('subject')}} ng-bind-html="'#'+epic.get('ref')+' '+epic.get('subject') | emojify"
)
span.belong-to-epic-label( span.belong-to-epic-label(
ng-style="::{'background-color': epic.get('color')}" ng-style="::{'background-color': epic.get('color')}"
translate="EPICS.EPIC" translate="EPICS.EPIC"

View File

@ -23,7 +23,7 @@
tg-check-permission="{{vm.getPermissionsKey()}}" tg-check-permission="{{vm.getPermissionsKey()}}"
) )
a.e2e-assign.card-owner-assign( a.e2e-assign.card-owner-assign(
ng-click="vm.onClickAssignedTo({id: vm.item.get('id')})" ng-click="!$event.ctrlKey && vm.onClickAssignedTo({id: vm.item.get('id')})"
href="" href=""
) )
tg-svg(svg-icon="icon-add-user") tg-svg(svg-icon="icon-add-user")
@ -31,7 +31,7 @@
a.e2e-edit.card-edit( a.e2e-edit.card-edit(
href="" href=""
ng-click="vm.onClickEdit({id: vm.item.get('id')})" ng-click="!$event.ctrlKey && vm.onClickEdit({id: vm.item.get('id')})"
tg-loading="vm.item.get('loading')" tg-loading="vm.item.get('loading')"
) )
tg-svg(svg-icon="icon-edit") tg-svg(svg-icon="icon-edit")

View File

@ -4,4 +4,5 @@ ul.card-tasks(ng-if="vm.isRelatedTasksVisible()")
href="#" href="#"
tg-nav="project-tasks-detail:project=vm.project.slug,ref=task.get('ref')", tg-nav="project-tasks-detail:project=vm.project.slug,ref=task.get('ref')",
ng-class="{'closed-task': task.get('is_closed'), 'blocked-task': task.get('is_blocked')}" ng-class="{'closed-task': task.get('is_closed'), 'blocked-task': task.get('is_blocked')}"
) {{"#" + task.get('ref')}} {{task.get('subject')}} ng-bind-html="'#'+task.get('ref')+' '+task.get('subject') | emojify"
)

View File

@ -6,7 +6,7 @@ h2.card-title
title="#{{ ::vm.item.getIn(['model', 'ref']) }} {{ vm.item.getIn(['model', 'subject'])}}" title="#{{ ::vm.item.getIn(['model', 'ref']) }} {{ vm.item.getIn(['model', 'subject'])}}"
) )
span(ng-if="vm.visible('ref')") {{::"#" + vm.item.getIn(['model', 'ref'])}} span(ng-if="vm.visible('ref')") {{::"#" + vm.item.getIn(['model', 'ref'])}}
span.e2e-title(ng-if="vm.visible('subject')") {{vm.item.getIn(['model', 'subject'])}} span.e2e-title(ng-if="vm.visible('subject')", ng-bind-html="vm.item.getIn(['model', 'subject']) | emojify")
tg-belong-to-epics( tg-belong-to-epics(
format="pill" format="pill"
ng-if="vm.item.getIn(['model', 'epics'])" ng-if="vm.item.getIn(['model', 'epics'])"

View File

@ -1,5 +1,5 @@
.card-unfold.ng-animate-disabled( .card-unfold.ng-animate-disabled(
ng-click="vm.toggleFold()" ng-click="!$event.ctrlKey && vm.toggleFold()"
ng-if="vm.visible('unfold') && (vm.hasTasks() || vm.hasVisibleAttachments())" ng-if="vm.visible('unfold') && (vm.hasTasks() || vm.hasVisibleAttachments())"
role="button" role="button"
) )

View File

@ -14,3 +14,15 @@
images="vm.item.get('images')" images="vm.item.get('images')"
) )
include card-templates/card-unfold include card-templates/card-unfold
.card-transit-multi
div.fake-us
div.fake-img
div.column
div.fake-text
div.fake-text
div.fake-us
div.fake-img
div.column
div.fake-text
div.fake-text

View File

@ -7,10 +7,12 @@
span.detail-subject.e2e-title-subject( span.detail-subject.e2e-title-subject(
ng-click="vm.editSubject(true)" ng-click="vm.editSubject(true)"
ng-if="vm.permissions.canEdit" ng-if="vm.permissions.canEdit"
) {{vm.item.subject}} ng-bind-html="vm.item.subject | emojify"
)
span.detail-subject.e2e-title-subject( span.detail-subject.e2e-title-subject(
ng-if="!vm.permissions.canEdit" ng-if="!vm.permissions.canEdit"
) {{vm.item.subject}} ng-bind-html="vm.item.subject | emojify"
)
a( a(
href="" href=""
ng-if="vm.permissions.canEdit" ng-if="vm.permissions.canEdit"
@ -55,7 +57,7 @@
title="{{'TASK.TITLE_LINK_GO_OWNER' | translate}}" title="{{'TASK.TITLE_LINK_GO_OWNER' | translate}}"
) )
span.item-ref {{'#' + vm.item.user_story_extra_info.ref}} span.item-ref {{'#' + vm.item.user_story_extra_info.ref}}
span {{::vm.item.user_story_extra_info.subject}} span(ng-bind-html="vm.item.user_story_extra_info.subject | emojify")
tg-belong-to-epics( tg-belong-to-epics(
ng-if="::vm.item.user_story_extra_info.epics" ng-if="::vm.item.user_story_extra_info.epics"
epics="::vm.item.user_story_extra_info.epics" epics="::vm.item.user_story_extra_info.epics"
@ -69,7 +71,8 @@
ng-repeat="userstory in vm.item.generated_user_stories track by userstory.id" ng-repeat="userstory in vm.item.generated_user_stories track by userstory.id"
tg-check-permission="view_us" tg-check-permission="view_us"
tg-nav="project-userstories-detail:project=vm.project.slug,ref=userstory.ref" tg-nav="project-userstories-detail:project=vm.project.slug,ref=userstory.ref"
) {{'#' + userstory.ref}} {{userstory.subject}} ng-bind-html="'#'+userstory.ref+' '+userstory.subject | emojify"
)
//- Issue origin from github //- Issue origin from github
.issue-external-reference(ng-if="vm.item.external_reference") .issue-external-reference(ng-if="vm.item.external_reference")
@ -91,7 +94,7 @@
title="{{'US.TITLE_LINK_GO_TO_ISSUE' | translate}}" title="{{'US.TITLE_LINK_GO_TO_ISSUE' | translate}}"
) )
span.item-ref {{'#' + vm.item.origin_issue.ref}} span.item-ref {{'#' + vm.item.origin_issue.ref}}
span {{vm.item.origin_issue.subject}} span(ng-bind-html="vm.item.origin_issue.subject | emojify")
//- Blocked description //- Blocked description
.block-desc-container(ng-show="vm.item.is_blocked") .block-desc-container(ng-show="vm.item.is_blocked")

View File

@ -18,7 +18,15 @@ form(name="vm.filtersForm")
.filters-step-cat .filters-step-cat
.filters-applied .filters-applied
.single-filter.ng-animate-disabled(ng-repeat="it in vm.selectedFilters track by it.key") .single-filter.ng-animate-disabled(ng-repeat="it in vm.selectedFilters track by it.key")
span.name(ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}") {{it.name}} span.name(
ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}"
ng-if="it.dataType === 'tags'"
ng-bind-html="it.name | emojify"
)
span.name(
ng-if="it.dataType !== 'tags'"
ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}"
) {{it.name}}
a.remove-filter.e2e-remove-filter( a.remove-filter.e2e-remove-filter(
ng-click="vm.unselectFilter(it)" ng-click="vm.unselectFilter(it)"
href="" href=""
@ -77,7 +85,15 @@ form(name="vm.filtersForm")
ng-if="!vm.isFilterSelected(filter, it) && !(it.count == 0 && filter.hideEmpty)" ng-if="!vm.isFilterSelected(filter, it) && !(it.count == 0 && filter.hideEmpty)"
ng-click="vm.selectFilter(filter, it)" ng-click="vm.selectFilter(filter, it)"
) )
span.name(ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}") {{it.name}} span.name(
ng-if="filter.dataType === 'tags'",
ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}"
ng-bind-html="it.name | emojify"
)
span.name(
ng-if="filter.dataType !== 'tags'",
ng-attr-style="{{it.color ? 'border-left: 3px solid ' + it.color: ''}}"
) {{it.name}}
span.number.e2e-filter-count(ng-if="it.count > 0") {{it.count}} span.number.e2e-filter-count(ng-if="it.count > 0") {{it.count}}
li.custom-filters.e2e-custom-filters( li.custom-filters.e2e-custom-filters(

View File

@ -128,6 +128,9 @@ tg-filter {
@include ellipsis(100%); @include ellipsis(100%);
display: block; display: block;
width: 100%; width: 100%;
img {
vertical-align: middle;
}
} }
.number { .number {
background: darken($whitish, 20%); // Fallback background: darken($whitish, 20%); // Fallback

View File

@ -1,4 +1,4 @@
span {{ tag[0] }} span(ng-bind-html="tag[0] | emojify")
tg-svg.icon-close.e2e-delete-tag( tg-svg.icon-close.e2e-delete-tag(
ng-if="hasPermissions" ng-if="hasPermissions"
svg-icon="icon-close" svg-icon="icon-close"

View File

@ -19,4 +19,7 @@
height: 1rem; height: 1rem;
width: 1rem; width: 1rem;
} }
img {
vertical-align: middle;
}
} }

View File

@ -208,8 +208,6 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad
$scope.codeEditorVisible = false $scope.codeEditorVisible = false
$scope.codeLans = [] $scope.codeLans = []
wysiwygService.loadEmojis()
wysiwygCodeHightlighterService.getLanguages().then (codeLans) -> wysiwygCodeHightlighterService.getLanguages().then (codeLans) ->
$scope.codeLans = codeLans $scope.codeLans = codeLans

View File

@ -26,49 +26,13 @@ class WysiwygService
@.$inject = [ @.$inject = [
"tgWysiwygCodeHightlighterService", "tgWysiwygCodeHightlighterService",
"tgProjectService", "tgProjectService",
"$tgNavUrls" "$tgNavUrls",
"$tgEmojis"
] ]
constructor: (@wysiwygCodeHightlighterService, @projectService, @navurls) -> constructor: (@wysiwygCodeHightlighterService, @projectService, @navurls, @emojis) ->
searchEmojiByName: (name) -> searchEmojiByName: (name) ->
return _.filter @.emojis, (it) -> it.name.indexOf(name) != -1 return @emojis.searchByName(name)
setEmojiImagePath: (emojis) ->
@.emojis = _.map emojis, (it) ->
it.image = "/#{window._version}/emojis/" + it.image
return it
loadEmojis: () ->
$.getJSON("/#{window._version}/emojis/emojis-data.json").then(@.setEmojiImagePath.bind(this))
getEmojiById: (id) ->
return _.find @.emojis, (it) -> it.id == id
getEmojiByName: (name) ->
return _.find @.emojis, (it) -> it.name == name
replaceImgsByEmojiName: (html) ->
emojiIds = taiga.getMatches(html, /emojis\/([^"]+).png"/gi)
for emojiId in emojiIds
regexImgs = new RegExp('<img(.*)' + emojiId + '[^>]+\>', 'g')
emoji = @.getEmojiById(emojiId)
html = html.replace(regexImgs, ':' + emoji.name + ':')
return html
replaceEmojiNameByImgs: (text) ->
emojiIds = taiga.getMatches(text, /:([\w ]*):/g)
for emojiId in emojiIds
regexImgs = new RegExp(':' + emojiId + ':', 'g')
emoji = @.getEmojiByName(emojiId)
if emoji
text = text.replace(regexImgs, '![alt](' + emoji.image + ')')
return text
pipeLinks: (text) -> pipeLinks: (text) ->
return text.replace /\[\[(.*?)\]\]/g, (match, p1, offset, str) -> return text.replace /\[\[(.*?)\]\]/g, (match, p1, offset, str) ->
@ -134,7 +98,7 @@ class WysiwygService
} }
html = html.replace(/&nbsp;(<\/.*>)/g, "$1") html = html.replace(/&nbsp;(<\/.*>)/g, "$1")
html = @.replaceImgsByEmojiName(html) html = @emojis.replaceImgsByEmojiName(html)
html = @.replaceUrls(html) html = @.replaceUrls(html)
html = @.removeTrailingListBr(html) html = @.removeTrailingListBr(html)
@ -211,7 +175,7 @@ class WysiwygService
breaks: true breaks: true
} }
text = @.replaceEmojiNameByImgs(text) text = @emojis.replaceEmojiNameByImgs(text)
text = @.pipeLinks(text) text = @.pipeLinks(text)
md = window.markdownit({ md = window.markdownit({

View File

@ -20,7 +20,8 @@
a( a(
tg-nav="project-epics-detail:project=vm.project.slug,ref=vm.epic.get('ref')" tg-nav="project-epics-detail:project=vm.project.slug,ref=vm.epic.get('ref')"
ng-attr-title="{{::vm.epic.get('subject')}}" ng-attr-title="{{::vm.epic.get('subject')}}"
) #{hash}{{::vm.epic.get('ref')}} {{::vm.epic.get('subject')}} ng-bind-html="'#'+vm.epic.get('ref')+' '+vm.epic.get('subject') | emojify"
)
span.epic-pill( span.epic-pill(
ng-style="::{'background-color': vm.epic.get('color')}" ng-style="::{'background-color': vm.epic.get('color')}"
translate="EPICS.EPIC" translate="EPICS.EPIC"

View File

@ -13,7 +13,8 @@
a( a(
tg-nav="project-userstories-detail:project=vm.story.getIn(['project_extra_info', 'slug']),ref=vm.story.get('ref')" tg-nav="project-userstories-detail:project=vm.story.getIn(['project_extra_info', 'slug']),ref=vm.story.get('ref')"
ng-attr-title="{{::vm.story.get('subject')}}" ng-attr-title="{{::vm.story.get('subject')}}"
) #{hash}{{::vm.story.get('ref')}} {{::vm.story.get('subject')}} ng-bind-html="'#'+vm.story.get('ref')+' '+vm.story.get('subject') | emojify"
)
tg-belong-to-epics( tg-belong-to-epics(
ng-if="vm.story.get('epics')" ng-if="vm.story.get('epics')"
format="pill" format="pill"

View File

@ -8,7 +8,8 @@ tg-svg.icon-drag(
a( a(
tg-nav="project-userstories-detail:project=vm.userstory.getIn(['project_extra_info', 'slug']),ref=vm.userstory.get('ref')" tg-nav="project-userstories-detail:project=vm.userstory.getIn(['project_extra_info', 'slug']),ref=vm.userstory.get('ref')"
ng-attr-title="{{vm.userstory.get('subject')}}" ng-attr-title="{{vm.userstory.get('subject')}}"
) #{hash}{{vm.userstory.get('ref')}} {{vm.userstory.get('subject')}} ng-bind-html="'#'+vm.userstory.get('ref')+' '+vm.userstory.get('subject') | emojify"
)
tg-belong-to-epics( tg-belong-to-epics(
format="pill" format="pill"

View File

@ -39,6 +39,11 @@ describe "dutyDirective", () ->
return value return value
provide.value "translateFilter", mockTranslateFilter provide.value "translateFilter", mockTranslateFilter
_mockEmojifyFilter = () ->
mockEmojifyFilter = (value) ->
return value
provide.value "emojifyFilter", mockEmojifyFilter
_mockTgProjectsService = () -> _mockTgProjectsService = () ->
mockTgProjectsService = { mockTgProjectsService = {
projectsById: { projectsById: {
@ -60,6 +65,7 @@ describe "dutyDirective", () ->
_mockTgProjectsService() _mockTgProjectsService()
_mockTranslate() _mockTranslate()
_mockTranslateFilter() _mockTranslateFilter()
_mockEmojifyFilter()
return null return null
beforeEach -> beforeEach ->

View File

@ -39,4 +39,4 @@ a.list-itemtype-ticket(
ng-if="::vm.duty.get('is_blocked')" ng-if="::vm.duty.get('is_blocked')"
title="{{::vm.duty.get('blocked_note')}}" title="{{::vm.duty.get('blocked_note')}}"
) {{ 'COMMON.BLOCKED' | translate }} ) {{ 'COMMON.BLOCKED' | translate }}
span {{ ::duty.get('subject') }} span(ng-bind-html="duty.get('subject') | emojify")

View File

@ -23,6 +23,7 @@ DropdownUserDirective = (authService, configService, locationService,
link = (scope, el, attrs, ctrl) -> link = (scope, el, attrs, ctrl) ->
scope.vm = {} scope.vm = {}
scope.vm.isFeedbackEnabled = configService.get("feedbackEnabled") scope.vm.isFeedbackEnabled = configService.get("feedbackEnabled")
scope.vm.supportUrl = configService.get("supportUrl")
taiga.defineImmutableProperty(scope.vm, "user", () -> authService.userData) taiga.defineImmutableProperty(scope.vm, "user", () -> authService.userData)
scope.vm.logout = -> scope.vm.logout = ->

View File

@ -51,7 +51,7 @@ div.navbar-dropdown.dropdown-user
translate="PROJECT.NAVIGATION.FEEDBACK") translate="PROJECT.NAVIGATION.FEEDBACK")
li li
a( a(
href="https://tree.taiga.io/support/", href="{{ vm.supportUrl }}",
target="_blank", target="_blank",
title="{{'PROJECT.NAVIGATION.HELP_TITLE' | translate}}", title="{{'PROJECT.NAVIGATION.HELP_TITLE' | translate}}",
translate="PROJECT.NAVIGATION.HELP") translate="PROJECT.NAVIGATION.HELP")

View File

@ -53,25 +53,29 @@
ng-if="::vm.item.get('type') === 'epic'" ng-if="::vm.item.get('type') === 'epic'"
tg-nav="project-epics-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')" tg-nav="project-epics-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')"
title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}" title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}"
) {{ ::vm.item.get('subject') }} ng-bind-html="vm.item.get('subject') | emojify"
)
a.ticket-title( a.ticket-title(
href="#" href="#"
ng-if="::vm.item.get('type') === 'userstory'" ng-if="::vm.item.get('type') === 'userstory'"
tg-nav="project-userstories-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')" tg-nav="project-userstories-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')"
title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}" title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}"
) {{ ::vm.item.get('subject') }} ng-bind-html="vm.item.get('subject') | emojify"
)
a.ticket-title( a.ticket-title(
href="#" href="#"
ng-if="::vm.item.get('type') === 'task'" ng-if="::vm.item.get('type') === 'task'"
tg-nav="project-tasks-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')" tg-nav="project-tasks-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')"
title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}" title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}"
) {{ ::vm.item.get('subject') }} ng-bind-html="vm.item.get('subject') | emojify"
)
a.ticket-title( a.ticket-title(
href="#" href="#"
ng-if="::vm.item.get('type') === 'issue'" ng-if="::vm.item.get('type') === 'issue'"
tg-nav="project-issues-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')" tg-nav="project-issues-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')"
title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}" title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}"
) {{ ::vm.item.get('subject') }} ng-bind-html="vm.item.get('subject') | emojify"
)
div.list-itemtype-track div.list-itemtype-track
span.list-itemtype-track-likers( span.list-itemtype-track-likers(

View File

@ -28,6 +28,7 @@ class WikiHistoryController
constructor: (@wikiHistoryService) -> constructor: (@wikiHistoryService) ->
taiga.defineImmutableProperty @, 'historyEntries', () => return @wikiHistoryService.historyEntries taiga.defineImmutableProperty @, 'historyEntries', () => return @wikiHistoryService.historyEntries
@.toggle = false
initializeHistoryEntries: (wikiId) -> initializeHistoryEntries: (wikiId) ->
if wikiId if wikiId

View File

@ -1,11 +1,13 @@
nav.history-tabs(ng-if="vm.historyEntries.count()>0") nav.history-tabs(ng-if="vm.historyEntries.count()>0")
a.history-tab.active( a.history-tab(
ng-class="{active: vm.toggle}"
ng-click="vm.toggle = !vm.toggle"
href="" href=""
title="{{ACTIVITY.TITLE}}" title="{{ACTIVITY.TITLE}}"
translate="ACTIVITY.TITLE" translate="ACTIVITY.TITLE"
) )
section.wiki-history(ng-if="vm.historyEntries.count()>0") section.wiki-history(ng-if="vm.historyEntries.count()>0 && vm.toggle")
tg-wiki-history-entry.wiki-history-entry( tg-wiki-history-entry.wiki-history-entry(
tg-repeat="historyEntry in vm.historyEntries" tg-repeat="historyEntry in vm.historyEntries"
history-entry="historyEntry" history-entry="historyEntry"

View File

@ -2,7 +2,7 @@ doctype html
div.error-main div.error-main
div.error-container div.error-container
img(src="/#{v}/svg/logo.svg", alt="TAIGA") img(src="/#{v}/svg/logo.svg", alt="TAIGA", width="143px")
h1.logo Taiga h1.logo Taiga
p.error-text(translate="ERROR.TEXT1") p.error-text(translate="ERROR.TEXT1")
a(href="/", title="", translate="COMMON.GO_HOME") a(href="/", title="", translate="COMMON.GO_HOME")

View File

@ -2,7 +2,7 @@ doctype html
div.error-main div.error-main
div.error-container div.error-container
img(src="/#{v}/svg/logo.svg", alt="TAIGA") img(src="/#{v}/svg/logo.svg", alt="TAIGA", width="143px")
h1.logo(translate="ERROR.NOT_FOUND") h1.logo(translate="ERROR.NOT_FOUND")
p.error-text(translate="ERROR.NOT_FOUND_TEXT") p.error-text(translate="ERROR.NOT_FOUND_TEXT")
a(href="/", title="", translate="COMMON.GO_HOME") a(href="/", title="", translate="COMMON.GO_HOME")

View File

@ -26,7 +26,7 @@
title="#{{ us.ref }} {{ us.subject }}" title="#{{ us.ref }} {{ us.subject }}"
) )
span(tg-bo-ref="us.ref") span(tg-bo-ref="us.ref")
span(ng-bind="us.subject") span(ng-bind-html="us.subject | emojify")
tg-belong-to-epics( tg-belong-to-epics(
format="pill" format="pill"
ng-if="us.epics" ng-if="us.epics"

View File

@ -53,7 +53,7 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}")
ng-if="issue.is_blocked" ng-if="issue.is_blocked"
title="{{issue.blocked_note}}" title="{{issue.blocked_note}}"
) {{'ISSUES.TABLE.BLOCKED' | translate}} ) {{'ISSUES.TABLE.BLOCKED' | translate}}
span(ng-bind="issue.subject") span(ng-bind-html="issue.subject | emojify")
div.issue-field(tg-issue-status-inline-edition="issue") div.issue-field(tg-issue-status-inline-edition="issue")

View File

@ -10,7 +10,15 @@ div.kanban-table(
tg-bo-title="s.name", tg-bo-title="s.name",
ng-class='{vfold:folds[s.id]}', ng-class='{vfold:folds[s.id]}',
tg-class-permission="{'readonly': '!modify_task'}") tg-class-permission="{'readonly': '!modify_task'}")
span(tg-bo-bind="s.name") div.title
div.name(tg-bo-bind="s.name")
div.counter
span(ng-if="usByStatus.get(s.id.toString()).size") {{ usByStatus.get(s.id.toString()).size }}
span(
ng-if="!usByStatus.get(s.id.toString()).size"
ng-class='{hidden:!s.wip_limit}'
) 0
span(tg-bo-bind="' / ' + s.wip_limit", ng-if="s.wip_limit")
div.options div.options
a.option( a.option(
href="" href=""
@ -67,7 +75,7 @@ div.kanban-table(
tg-card.card.ng-animate-disabled( tg-card.card.ng-animate-disabled(
tg-repeat="us in usByStatus.get(s.id.toString()) track by us.getIn(['model', 'id'])", tg-repeat="us in usByStatus.get(s.id.toString()) track by us.getIn(['model', 'id'])",
ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id)}" ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id), 'kanban-task-selected': ctrl.selectedUss[us.get('id')], 'ui-multisortable-multiple': ctrl.selectedUss[us.get('id')]}"
tg-class-permission="{'readonly': '!modify_task'}" tg-class-permission="{'readonly': '!modify_task'}"
tg-bind-scope, tg-bind-scope,
on-toggle-fold="ctrl.toggleFold(id)" on-toggle-fold="ctrl.toggleFold(id)"
@ -78,6 +86,7 @@ div.kanban-table(
zoom="ctrl.zoom" zoom="ctrl.zoom"
zoom-level="ctrl.zoomLevel" zoom-level="ctrl.zoomLevel"
archived="ctrl.isUsInArchivedHiddenStatus(us.get('id'))" archived="ctrl.isUsInArchivedHiddenStatus(us.get('id'))"
ng-click="$event.ctrlKey && ctrl.toggleSelectedUs(us.get('id'))"
) )
div.kanban-column-intro(ng-if="s.is_archived", tg-kanban-archived-status-intro="s") div.kanban-column-intro(ng-if="s.is_archived", tg-kanban-archived-status-intro="s")

View File

@ -14,7 +14,7 @@ script(type="text/ng-template", id="search-issues")
div.user-stories div.user-stories
div.user-story-name div.user-story-name
a(href="", tg-nav="project-issues-detail:project=project.slug,ref=issue.ref", a(href="", tg-nav="project-issues-detail:project=project.slug,ref=issue.ref",
tg-bo-bind="issue.subject") tg-bind-html="issue.subject | emojify")
div.status(tg-listitem-issue-status="issue") div.status(tg-listitem-issue-status="issue")
div.assigned-to(tg-listitem-assignedto="issue") div.assigned-to(tg-listitem-assignedto="issue")
@ -34,7 +34,7 @@ script(type="text/ng-template", id="search-epics")
div.user-stories div.user-stories
div.user-story-name div.user-story-name
a(href="", tg-nav="project-epics-detail:project=project.slug,ref=epic.ref", a(href="", tg-nav="project-epics-detail:project=project.slug,ref=epic.ref",
tg-bo-bind="epic.subject") tg-bind-html="epic.subject | emojify")
div.status(tg-listitem-epic-status="epic") div.status(tg-listitem-epic-status="epic")
div.empty-search-results(ng-class="{'hidden': epics.length}") div.empty-search-results(ng-class="{'hidden': epics.length}")
@ -56,7 +56,7 @@ script(type="text/ng-template", id="search-userstories")
div.user-stories div.user-stories
div.user-story-name div.user-story-name
a(href="", tg-nav="project-userstories-detail:project=project.slug,ref=us.ref", a(href="", tg-nav="project-userstories-detail:project=project.slug,ref=us.ref",
tg-bo-bind="us.subject") tg-bind-html="us.subject | emojify")
div.sprint div.sprint
div.sprint-link div.sprint-link
a(href="", tg-nav="project-taskboard:project=project.slug,sprint=us.milestone_slug", a(href="", tg-nav="project-taskboard:project=project.slug,sprint=us.milestone_slug",
@ -81,7 +81,7 @@ script(type="text/ng-template", id="search-tasks")
div.user-stories div.user-stories
div.user-story-name div.user-story-name
a(href="", tg-nav="project-tasks-detail:project=project.slug,ref=task.ref", a(href="", tg-nav="project-tasks-detail:project=project.slug,ref=task.ref",
tg-bo-bind="task.subject") tg-bind-html="task.subject | emojify")
div.status(tg-listitem-task-status="task") div.status(tg-listitem-task-status="task")
div.assigned-to(tg-listitem-assignedto="task") div.assigned-to(tg-listitem-assignedto="task")

View File

@ -18,7 +18,7 @@ div.sprint-table(tg-bind-scope, ng-class="{'sprint-empty-wrapper': !sprint.user_
tg-bo-title="'#' + us.ref + ' ' + us.subject", tg-bo-title="'#' + us.ref + ' ' + us.subject",
ng-class="{closed: us.is_closed, blocked: us.is_blocked}") ng-class="{closed: us.is_closed, blocked: us.is_blocked}")
span(tg-bo-ref="us.ref") span(tg-bo-ref="us.ref")
span(tg-bo-bind="us.subject") span(tg-bind-html="us.subject | emojify")
tg-belong-to-epics( tg-belong-to-epics(
format="pill" format="pill"
ng-if="us.epics" ng-if="us.epics"

View File

@ -53,7 +53,7 @@ div.taskboard-table(
tg-nav-get-params="{\"milestone\": {{us.milestone}}}", tg-nav-get-params="{\"milestone\": {{us.milestone}}}",
tg-bo-title="'#' + us.ref + ' ' + us.subject") tg-bo-title="'#' + us.ref + ' ' + us.subject")
span.us-ref(tg-bo-ref="us.ref") span.us-ref(tg-bo-ref="us.ref")
span(ng-bind="us.subject") span(ng-bind-html="us.subject | emojify")
tg-belong-to-epics( tg-belong-to-epics(
format="pill" format="pill"
ng-if="us.epics" ng-if="us.epics"

View File

@ -2,7 +2,7 @@
a.clickable( a.clickable(
tg-nav="project-tasks-detail:project=project.slug,ref=task.ref") tg-nav="project-tasks-detail:project=project.slug,ref=task.ref")
span #<%- task.ref %> span #<%- task.ref %>
span(ng-non-bindable) <%- task.subject %> span(ng-non-bindable) <%= emojify(task.subject) %>
.task-settings .task-settings
<% if(perms.modify_task) { %> <% if(perms.modify_task) { %>

View File

@ -41,7 +41,7 @@
} }
} }
.backlog-us-mirror { .multiple-drag-mirror.us-item-row {
background: $white; background: $white;
border-radius: 4px; border-radius: 4px;
box-shadow: 2px 2px 5px $gray; box-shadow: 2px 2px 5px $gray;

View File

@ -91,9 +91,19 @@ $column-padding: .5rem 1rem;
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
span { .title {
align-items: flex-end;
display: flex;
width: 100%;
.counter {
@include font-size(xsmall);
line-height: 1.6;
margin: 0 .5rem;
}
.name {
@include ellipsis(65%); @include ellipsis(65%);
} }
}
.option { .option {
margin-right: .3rem; margin-right: .3rem;
} }
@ -148,9 +158,63 @@ $column-padding: .5rem 1rem;
.kanban-uses-box { .kanban-uses-box {
background: $mass-white; background: $mass-white;
} }
.kanban-task-selected {
&.card:not(.gu-transit-multi) {
// border: 1px solid $primary-light;
box-shadow: 0 0 0 1px $primary-light, 2px 2px 4px darken($whitish, 10%);
}
}
} }
.kanban-table-inner { .kanban-table-inner {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.card-transit-multi {
background: darken($whitish, 2%);
border: 1px dashed darken($whitish, 8%);
display: none;
opacity: 1;
padding: 1rem;
.fake-img,
.fake-text {
background: darken($whitish, 8%);
}
.fake-us {
display: flex;
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
.column {
padding-left: .5rem;
width: 100%;
}
.fake-img {
flex-basis: 48px;
flex-shrink: 0;
height: 48px;
width: 48px;
}
.fake-text {
height: 1rem;
margin-bottom: 1rem;
width: 80%;
&:last-child {
margin-bottom: 0;
width: 40%;
}
}
}
.card.gu-transit-multi {
.card-transit-multi {
display: block;
}
.card-inner {
display: none;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 B

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 422 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 B

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 455 B

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 B

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 B

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 B

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 B

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 B

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 B

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 B

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 B

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 482 B

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 B

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 B

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 B

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 596 B

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 510 B

Some files were not shown because too many files have changed in this diff Show More