Merge branch 'master' into stable
18
CHANGELOG.md
|
@ -1,5 +1,23 @@
|
|||
# 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)
|
||||
|
||||
### Features
|
||||
|
|
|
@ -12,6 +12,7 @@ window.taigaConfig = {
|
|||
"defaultTheme": "taiga",
|
||||
"publicRegisterEnabled": true,
|
||||
"feedbackEnabled": true,
|
||||
"supportUrl": null,
|
||||
"privacyPolicyUrl": null,
|
||||
"termsOfServiceUrl": null,
|
||||
"maxUploadFileSize": null,
|
||||
|
@ -70,10 +71,14 @@ promise.fail () ->
|
|||
console.error "Your conf.json file is not a valid json file, please review it."
|
||||
|
||||
promise.always ->
|
||||
emojisPromise = $.getJSON("/#{window._version}/emojis/emojis-data.json").then (emojis) ->
|
||||
window.emojis = emojis
|
||||
if window.taigaConfig.contribPlugins.length > 0
|
||||
loadPlugins(window.taigaConfig.contribPlugins).then () ->
|
||||
ljs.load "/#{window._version}/js/app.js", ->
|
||||
angular.bootstrap(document, ['taiga'])
|
||||
emojisPromise.then ->
|
||||
angular.bootstrap(document, ['taiga'])
|
||||
else
|
||||
ljs.load "/#{window._version}/js/app.js", ->
|
||||
angular.bootstrap(document, ['taiga'])
|
||||
emojisPromise.then ->
|
||||
angular.bootstrap(document, ['taiga'])
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
###
|
||||
|
||||
@taiga = taiga = {}
|
||||
taiga.emojis = window.emojis
|
||||
@.taigaContribPlugins = @.taigaContribPlugins or window.taigaContribPlugins or []
|
||||
|
||||
# Generic function for generate hash from a arbitrary length
|
||||
|
|
|
@ -521,6 +521,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
|
|||
@.generateFilters().then () =>
|
||||
@rootscope.$broadcast("filters:update")
|
||||
@.loadProjectStats()
|
||||
if @.isFilterDataTypeSelected('status')
|
||||
@.filtersReloadContent()
|
||||
|
||||
editUserStory: (projectId, ref, $event) ->
|
||||
target = $($event.target)
|
||||
|
|
|
@ -68,7 +68,7 @@ BacklogSortableDirective = () ->
|
|||
window.dragMultiple.start(item, container)
|
||||
|
||||
drake.on 'cloned', (item) ->
|
||||
$(item).addClass('backlog-us-mirror')
|
||||
$(item).addClass('multiple-drag-mirror')
|
||||
|
||||
drake.on 'dragend', (item) ->
|
||||
parent = $(item).parent()
|
||||
|
|
|
@ -251,8 +251,6 @@ Qqueue = ($q) ->
|
|||
bindAdd: (fn) =>
|
||||
return (args...) =>
|
||||
lastPromise = lastPromise.then () => fn.apply(@, args)
|
||||
|
||||
return qqueue
|
||||
add: (fn) =>
|
||||
if !lastPromise
|
||||
lastPromise = fn()
|
||||
|
|
|
@ -495,92 +495,6 @@ DeleteButtonDirective = ($log, $repo, $confirm, $location, $template) ->
|
|||
|
||||
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
|
||||
#############################################################################
|
||||
|
|
|
@ -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, '')
|
||||
|
||||
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)
|
|
@ -143,3 +143,12 @@ inArray = ($filter) ->
|
|||
return filter list, (listItem) ->
|
||||
return arrayFilter.indexOf(listItem[element]) != -1
|
||||
module.filter("inArray", ["$filter", inArray])
|
||||
|
||||
emojify = ($emojis) ->
|
||||
return (input) ->
|
||||
if input
|
||||
return $emojis.replaceEmojiNameByHtmlImgs(_.escape(input))
|
||||
|
||||
return ""
|
||||
|
||||
module.filter("emojify", ["$tgEmojis", emojify])
|
||||
|
|
|
@ -371,6 +371,8 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
|
|||
status: status
|
||||
is_archived: false
|
||||
tags: []
|
||||
subject: ""
|
||||
description: ""
|
||||
})
|
||||
|
||||
# Update texts for creation
|
||||
|
@ -478,24 +480,32 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
|
|||
|
||||
submitButton = $el.find(".submit-button")
|
||||
|
||||
$el.on "submit", "form", submit
|
||||
|
||||
$el.on "click", ".close", (event) ->
|
||||
event.preventDefault()
|
||||
|
||||
$scope.$apply ->
|
||||
$scope.us.revert()
|
||||
|
||||
lightboxService.close($el)
|
||||
|
||||
$el.keydown (event) ->
|
||||
code = if event.keyCode then event.keyCode else event.which
|
||||
if code == 27
|
||||
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.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", ->
|
||||
$el.find('.close').off()
|
||||
$el.off()
|
||||
|
||||
return {link: link}
|
||||
|
|
|
@ -58,14 +58,14 @@ TagsDirective = ->
|
|||
module.directive("tgTags", TagsDirective)
|
||||
|
||||
|
||||
ColorizeTagsBacklogDirective = ->
|
||||
ColorizeTagsBacklogDirective = ($emojis) ->
|
||||
template = _.template("""
|
||||
<% _.each(tags, function(tag) { %>
|
||||
<% if (tag[1] !== null) { %>
|
||||
<span class="tag"
|
||||
style="border-left: 5px solid <%- tag[1] %>"
|
||||
title="<%- tag[0] %>">
|
||||
<%- tag[0] %>
|
||||
<%= emojify(tag[0]) %>
|
||||
</span>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
|
@ -73,7 +73,7 @@ ColorizeTagsBacklogDirective = ->
|
|||
<% if (tag[1] === null) { %>
|
||||
<span class="tag"
|
||||
title="<%- tag[0] %>">
|
||||
<%- tag[0] %>
|
||||
<%= emojify(tag[0]) %>
|
||||
</span>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
|
@ -81,7 +81,7 @@ ColorizeTagsBacklogDirective = ->
|
|||
|
||||
link = ($scope, $el, $attrs, $ctrl) ->
|
||||
render = (tags) ->
|
||||
html = template({tags: tags})
|
||||
html = template({tags: tags, emojify: (text) -> $emojis.replaceEmojiNameByHtmlImgs(_.escape(text))})
|
||||
$el.html(html)
|
||||
|
||||
$scope.$watch $attrs.tgColorizeBacklogTags, (tags) ->
|
||||
|
@ -92,7 +92,7 @@ ColorizeTagsBacklogDirective = ->
|
|||
|
||||
return {link: link}
|
||||
|
||||
module.directive("tgColorizeBacklogTags", ColorizeTagsBacklogDirective)
|
||||
module.directive("tgColorizeBacklogTags", ["$tgEmojis", ColorizeTagsBacklogDirective])
|
||||
|
||||
|
||||
#############################################################################
|
||||
|
|
|
@ -194,6 +194,7 @@ class UsFiltersMixin
|
|||
filters.assigned_to = urlfilters.assigned_to
|
||||
filters.owner = urlfilters.owner
|
||||
filters.epic = urlfilters.epic
|
||||
filters.role = urlfilters.role
|
||||
|
||||
@filterRemoteStorageService.getFilters(@scope.projectId, @.storeCustomFiltersName).then (userFilters) =>
|
||||
userFilters[name] = filters
|
||||
|
@ -207,6 +208,12 @@ class UsFiltersMixin
|
|||
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.storeCustomFiltersName).then(@.generateFilters)
|
||||
@.generateFilters()
|
||||
|
||||
isFilterDataTypeSelected: (filterDataType) ->
|
||||
for filter in @.selectedFilters
|
||||
if (filter['dataType'] == filterDataType)
|
||||
return true
|
||||
return false
|
||||
|
||||
generateFilters: (milestone) ->
|
||||
@.storeFilters(@params.pslug, @location.search(), @.storeFiltersName)
|
||||
|
||||
|
@ -219,6 +226,7 @@ class UsFiltersMixin
|
|||
loadFilters.assigned_to = urlfilters.assigned_to
|
||||
loadFilters.owner = urlfilters.owner
|
||||
loadFilters.epic = urlfilters.epic
|
||||
loadFilters.role = urlfilters.role
|
||||
loadFilters.q = urlfilters.q
|
||||
|
||||
if milestone
|
||||
|
@ -249,6 +257,15 @@ class UsFiltersMixin
|
|||
|
||||
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
|
||||
owner = _.map data.owners, (it) ->
|
||||
it.id = it.id.toString()
|
||||
|
@ -287,6 +304,10 @@ class UsFiltersMixin
|
|||
selected = @.formatSelectedFilters("epic", epic, loadFilters.epic)
|
||||
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||
|
||||
if loadFilters.role
|
||||
selected = @.formatSelectedFilters("role", role, loadFilters.role)
|
||||
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||
|
||||
@.filterQ = loadFilters.q
|
||||
|
||||
@.filters = [
|
||||
|
@ -307,6 +328,11 @@ class UsFiltersMixin
|
|||
dataType: "assigned_to",
|
||||
content: assignedTo
|
||||
},
|
||||
{
|
||||
title: @translate.instant("COMMON.FILTERS.CATEGORIES.ROLE"),
|
||||
dataType: "role",
|
||||
content: role
|
||||
},
|
||||
{
|
||||
title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
|
||||
dataType: "owner",
|
||||
|
|
|
@ -127,6 +127,12 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
delete userFilters[customFilter.id]
|
||||
@filterRemoteStorageService.storeFilters(@scope.projectId, userFilters, @.myFiltersHashSuffix).then(@.generateFilters)
|
||||
|
||||
isFilterDataTypeSelected: (filterDataType) ->
|
||||
for filter in @.selectedFilters
|
||||
if (filter['dataType'] == filterDataType)
|
||||
return true
|
||||
return false
|
||||
|
||||
saveCustomFilter: (name) ->
|
||||
filters = {}
|
||||
urlfilters = @location.search()
|
||||
|
@ -137,6 +143,7 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
filters.priority = urlfilters.priority
|
||||
filters.assigned_to = urlfilters.assigned_to
|
||||
filters.owner = urlfilters.owner
|
||||
filters.role = urlfilters.role
|
||||
|
||||
@filterRemoteStorageService.getFilters(@scope.projectId, @.myFiltersHashSuffix).then (userFilters) =>
|
||||
userFilters[name] = filters
|
||||
|
@ -157,6 +164,7 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
loadFilters.priority = urlfilters.priority
|
||||
loadFilters.assigned_to = urlfilters.assigned_to
|
||||
loadFilters.owner = urlfilters.owner
|
||||
loadFilters.role = urlfilters.role
|
||||
loadFilters.q = urlfilters.q
|
||||
|
||||
return @q.all([
|
||||
|
@ -203,6 +211,15 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
it.id = it.id.toString()
|
||||
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
|
||||
|
||||
@.selectedFilters = []
|
||||
|
@ -235,6 +252,10 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
selected = @.formatSelectedFilters("priority", priority, loadFilters.priority)
|
||||
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||
|
||||
if loadFilters.role
|
||||
selected = @.formatSelectedFilters("role", role, loadFilters.role)
|
||||
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||
|
||||
@.filterQ = loadFilters.q
|
||||
|
||||
@.filters = [
|
||||
|
@ -270,6 +291,11 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
dataType: "assigned_to",
|
||||
content: assignedTo
|
||||
},
|
||||
{
|
||||
title: @translate.instant("COMMON.FILTERS.CATEGORIES.ROLE"),
|
||||
dataType: "role",
|
||||
content: role
|
||||
},
|
||||
{
|
||||
title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
|
||||
dataType: "owner",
|
||||
|
@ -485,10 +511,9 @@ IssuesDirective = ($log, $location, $template, $compile) ->
|
|||
currentOrder = $ctrl.getOrderBy()
|
||||
newOrder = target.data("fieldname")
|
||||
|
||||
if newOrder == 'total_voters'
|
||||
finalOrder = if currentOrder == newOrder then newOrder else "-#{newOrder}"
|
||||
else
|
||||
finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder
|
||||
if newOrder == 'total_voters' and currentOrder != "-total_voters"
|
||||
currentOrder = "total_voters"
|
||||
finalOrder = if currentOrder == newOrder then "-#{newOrder}" else newOrder
|
||||
|
||||
$scope.$apply ->
|
||||
$ctrl.replaceFilter("order_by", finalOrder)
|
||||
|
@ -568,8 +593,9 @@ IssueStatusInlineEditionDirective = ($repo, $template, $rootscope) ->
|
|||
|
||||
$scope.$apply () ->
|
||||
$repo.save(issue).then ->
|
||||
$ctrl.loadIssues()
|
||||
$ctrl.generateFilters()
|
||||
if $ctrl.isFilterDataTypeSelected('status')
|
||||
$ctrl.loadIssues()
|
||||
|
||||
taiga.bindOnce $scope, "project", (project) ->
|
||||
$el.append(selectionTemplate({ 'statuses': project.issue_statuses }))
|
||||
|
@ -635,13 +661,17 @@ IssueAssignedToInlineEditionDirective = ($repo, $rootscope, $translate, avatarSe
|
|||
$el.unbind("click")
|
||||
$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
|
||||
updatedIssue.assigned_to = userId
|
||||
$repo.save(updatedIssue)
|
||||
updateIssue(updatedIssue)
|
||||
$repo.save(issue).then ->
|
||||
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)
|
||||
|
||||
$scope.$on "$destroy", ->
|
||||
|
|
|
@ -93,15 +93,17 @@ class KanbanUserstoriesService extends taiga.Service
|
|||
|
||||
@.refresh()
|
||||
|
||||
move: (id, statusId, index) ->
|
||||
us = @.getUsModel(id)
|
||||
|
||||
move: (usList, statusId, index) ->
|
||||
initialLength = usList.length
|
||||
|
||||
usByStatus = _.filter @.userstoriesRaw, (it) =>
|
||||
return it.status == statusId
|
||||
|
||||
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)
|
||||
afterDestination = _.slice(usByStatusWithoutMoved, index)
|
||||
|
||||
|
@ -112,26 +114,54 @@ class KanbanUserstoriesService extends taiga.Service
|
|||
previousWithTheSameOrder = _.filter beforeDestination, (it) =>
|
||||
@.order[it.id] == @.order[previous.id]
|
||||
|
||||
|
||||
if previousWithTheSameOrder.length > 1
|
||||
for it in previousWithTheSameOrder
|
||||
setOrders[it.id] = @.order[it.id]
|
||||
|
||||
if !previous and (!afterDestination or afterDestination.length == 0)
|
||||
@.order[us.id] = 0
|
||||
else if !previous and afterDestination and afterDestination.length > 0
|
||||
@.order[us.id] = @.order[afterDestination[0].id] - 1
|
||||
modifiedUs = []
|
||||
setPreviousOrders = []
|
||||
setNextOrders = []
|
||||
|
||||
if !previous
|
||||
startIndex = 0
|
||||
else if previous
|
||||
@.order[us.id] = @.order[previous.id] + 1
|
||||
startIndex = @.order[previous.id] + 1
|
||||
|
||||
for it, key in afterDestination
|
||||
@.order[it.id] = @.order[us.id] + key + 1
|
||||
previousWithTheSameOrder = _.filter(beforeDestination, (it) =>
|
||||
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]
|
||||
|
||||
us.status = statusId
|
||||
us.kanban_order = @.order[us.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.kanban_order = startIndex + key
|
||||
@.order[us.id] = us.kanban_order
|
||||
|
||||
modifiedUs.push({us_id: us.id, order: us.kanban_order})
|
||||
|
||||
@.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) ->
|
||||
us = @.getUsModel(id)
|
||||
|
|
|
@ -71,6 +71,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
bindMethods(@)
|
||||
@kanbanUserstoriesService.reset()
|
||||
@.openFilter = false
|
||||
@.selectedUss = {}
|
||||
|
||||
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", () =>
|
||||
return @kanbanUserstoriesService.usByStatus
|
||||
|
||||
cleanSelectedUss: () ->
|
||||
for key of @.selectedUss
|
||||
@.selectedUss[key] = false
|
||||
|
||||
toggleSelectedUs: (usId) ->
|
||||
@.selectedUss[usId] = !@.selectedUss[usId]
|
||||
|
||||
firstLoad: () ->
|
||||
promise = @.loadInitialData()
|
||||
|
||||
|
@ -191,9 +199,10 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
|
||||
@kanbanUserstoriesService.replaceModel(usModel)
|
||||
|
||||
promise = @repo.save(usModel)
|
||||
promise.then null, ->
|
||||
console.log "FAIL" # TODO
|
||||
@repo.save(usModel).then =>
|
||||
@.generateFilters()
|
||||
if @.isFilterDataTypeSelected('assigned_to') || @.isFilterDataTypeSelected('role')
|
||||
@.filtersReloadContent()
|
||||
|
||||
refreshTagsColors: ->
|
||||
return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) =>
|
||||
|
@ -291,36 +300,49 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
|
|||
prepareBulkUpdateData: (uses, field="kanban_order") ->
|
||||
return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]})
|
||||
|
||||
moveUs: (ctx, us, oldStatusId, newStatusId, index) ->
|
||||
us = @kanbanUserstoriesService.getUsModel(us.get('id'))
|
||||
newStatus = @scope.usStatusById[newStatusId]
|
||||
if newStatus.is_archived and !@scope.usByStatus.get(newStatusId.toString())
|
||||
moveUpdateData = @kanbanUserstoriesService.moveToEnd(us.id, newStatusId)
|
||||
else
|
||||
moveUpdateData = @kanbanUserstoriesService.move(us.id, newStatusId, index)
|
||||
moveUs: (ctx, usList, newStatusId, index) ->
|
||||
@.cleanSelectedUss()
|
||||
|
||||
usList = _.map usList, (us) =>
|
||||
return @kanbanUserstoriesService.getUsModel(us.id)
|
||||
|
||||
params = {
|
||||
include_attachments: true,
|
||||
include_tasks: true
|
||||
}
|
||||
data = @kanbanUserstoriesService.move(usList, newStatusId, index)
|
||||
|
||||
options = {
|
||||
headers: {
|
||||
"set-orders": JSON.stringify(moveUpdateData.set_orders)
|
||||
promise = @rs.userstories.bulkUpdateKanbanOrder(@scope.projectId, data.bulkOrders)
|
||||
|
||||
promise.then () =>
|
||||
# saving
|
||||
# drag single or different status
|
||||
options = {
|
||||
headers: {
|
||||
"set-orders": JSON.stringify(data.setOrders)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
promise = @repo.save(us, true, params, options, true)
|
||||
params = {
|
||||
include_attachments: true,
|
||||
include_tasks: true
|
||||
}
|
||||
|
||||
promise = promise.then (result) =>
|
||||
headers = result[1]
|
||||
promises = _.map usList, (us) =>
|
||||
@repo.save(us, true, params, options, true)
|
||||
|
||||
if headers && headers['taiga-info-order-updated']
|
||||
order = JSON.parse(headers['taiga-info-order-updated'])
|
||||
@kanbanUserstoriesService.assignOrders(order)
|
||||
@scope.$broadcast("redraw:wip")
|
||||
promise = @q.all(promises)
|
||||
|
||||
promise.then (result) =>
|
||||
headers = result[1]
|
||||
|
||||
if headers && headers['taiga-info-order-updated']
|
||||
order = JSON.parse(headers['taiga-info-order-updated'])
|
||||
@kanbanUserstoriesService.assignOrders(order)
|
||||
@scope.$broadcast("redraw:wip")
|
||||
|
||||
@.generateFilters()
|
||||
if @.isFilterDataTypeSelected('status')
|
||||
@.filtersReloadContent()
|
||||
|
||||
return promise
|
||||
|
||||
return promise
|
||||
|
||||
module.controller("KanbanController", KanbanController)
|
||||
|
||||
|
|
|
@ -48,7 +48,6 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
if not ($scope.project.my_permissions.indexOf("modify_us") > -1)
|
||||
return
|
||||
|
||||
oldParentScope = null
|
||||
newParentScope = null
|
||||
itemEl = null
|
||||
tdom = $el
|
||||
|
@ -70,23 +69,44 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
|
|||
})
|
||||
|
||||
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) ->
|
||||
parentEl = $(item).parent()
|
||||
itemEl = $(item)
|
||||
itemUs = itemEl.scope().us
|
||||
itemIndex = itemEl.index()
|
||||
newParentScope = parentEl.scope()
|
||||
|
||||
newStatusId = newParentScope.s.id
|
||||
oldStatusId = oldParentScope.s.id
|
||||
dragMultipleItems = window.dragMultiple.stop()
|
||||
|
||||
if newStatusId != oldStatusId
|
||||
deleteElement(itemEl)
|
||||
# if it is not drag multiple
|
||||
if !dragMultipleItems.length
|
||||
dragMultipleItems = [item]
|
||||
|
||||
$scope.$apply ->
|
||||
$rootscope.$broadcast("kanban:us:move", itemUs, itemUs.getIn(['model', 'status']), newStatusId, itemIndex)
|
||||
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 ->
|
||||
_.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, {
|
||||
margin: 100,
|
||||
|
|
|
@ -29,7 +29,7 @@ debounce = @.taiga.debounce
|
|||
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)
|
||||
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
|
||||
}
|
||||
|
||||
$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", ->
|
||||
renderEdit($model.$modelValue)
|
||||
|
@ -119,7 +123,7 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading, $tem
|
|||
return {link:link, require:"ngModel"}
|
||||
|
||||
module.directive("tgRelatedTaskRow", ["$tgRepo", "$compile", "$tgConfirm", "$rootScope", "$tgLoading",
|
||||
"$tgTemplate", "$translate", RelatedTaskRowDirective])
|
||||
"$tgTemplate", "$translate", "$tgEmojis", RelatedTaskRowDirective])
|
||||
|
||||
|
||||
RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel, $loading, $analytics) ->
|
||||
|
|
|
@ -27,7 +27,7 @@ bindOnce = @.taiga.bindOnce
|
|||
debounce = @.taiga.debounce
|
||||
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) ->
|
||||
$scope.isNew = true
|
||||
|
||||
|
@ -100,15 +100,17 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
|
|||
_.pull($scope.task.tags, value)
|
||||
|
||||
$scope.$on "taskform:new", (ctx, sprintId, usId) ->
|
||||
$scope.task = {
|
||||
$scope.task = $model.make_model('tasks', {
|
||||
project: $scope.projectId
|
||||
milestone: sprintId
|
||||
user_story: usId
|
||||
is_archived: false
|
||||
status: $scope.project.default_task_status
|
||||
assigned_to: null
|
||||
tags: []
|
||||
}
|
||||
tags: [],
|
||||
subject: "",
|
||||
description: "",
|
||||
})
|
||||
$scope.isNew = true
|
||||
$scope.attachments = Immutable.List()
|
||||
|
||||
|
@ -187,7 +189,30 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
|
|||
|
||||
$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", ->
|
||||
$el.find('.close').off()
|
||||
$el.off()
|
||||
|
||||
return {link: link}
|
||||
|
@ -250,6 +275,7 @@ module.directive("tgLbCreateEditTask", [
|
|||
"lightboxService",
|
||||
"$translate",
|
||||
"$q",
|
||||
"$tgConfirm",
|
||||
"tgAttachmentsService",
|
||||
CreateEditTaskDirective
|
||||
])
|
||||
|
|
|
@ -134,6 +134,12 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
|
|||
|
||||
@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) ->
|
||||
filters = {}
|
||||
urlfilters = @location.search()
|
||||
|
@ -141,6 +147,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
|
|||
filters.status = urlfilters.status
|
||||
filters.assigned_to = urlfilters.assigned_to
|
||||
filters.owner = urlfilters.owner
|
||||
filters.role = urlfilters.role
|
||||
|
||||
@filterRemoteStorageService.getFilters(@scope.projectId, 'tasks-custom-filters').then (userFilters) =>
|
||||
userFilters[name] = filters
|
||||
|
@ -159,6 +166,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
|
|||
loadFilters.status = urlfilters.status
|
||||
loadFilters.assigned_to = urlfilters.assigned_to
|
||||
loadFilters.owner = urlfilters.owner
|
||||
loadFilters.role = urlfilters.role
|
||||
loadFilters.q = urlfilters.q
|
||||
|
||||
return @q.all([
|
||||
|
@ -188,6 +196,15 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
|
|||
|
||||
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
|
||||
owner = _.map data.owners, (it) ->
|
||||
it.id = it.id.toString()
|
||||
|
@ -213,6 +230,10 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
|
|||
selected = @.formatSelectedFilters("owner", owner, loadFilters.owner)
|
||||
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||
|
||||
if loadFilters.role
|
||||
selected = @.formatSelectedFilters("role", role, loadFilters.role)
|
||||
@.selectedFilters = @.selectedFilters.concat(selected)
|
||||
|
||||
@.filterQ = loadFilters.q
|
||||
|
||||
@.filters = [
|
||||
|
@ -233,6 +254,11 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
|
|||
dataType: "assigned_to",
|
||||
content: assignedTo
|
||||
},
|
||||
{
|
||||
title: @translate.instant("COMMON.FILTERS.CATEGORIES.ROLE"),
|
||||
dataType: "role",
|
||||
content: role
|
||||
},
|
||||
{
|
||||
title: @translate.instant("COMMON.FILTERS.CATEGORIES.CREATED_BY"),
|
||||
dataType: "owner",
|
||||
|
@ -290,9 +316,10 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
|
|||
|
||||
@taskboardTasksService.replaceModel(taskModel)
|
||||
|
||||
promise = @repo.save(taskModel)
|
||||
promise.then null, ->
|
||||
console.log "FAIL" # TODO
|
||||
@repo.save(taskModel).then =>
|
||||
@.generateFilters()
|
||||
if @.isFilterDataTypeSelected('assigned_to') || @.isFilterDataTypeSelected('role')
|
||||
@.loadTasks()
|
||||
|
||||
initializeSubscription: ->
|
||||
routingKey = "changes.project.#{@scope.projectId}.tasks"
|
||||
|
@ -438,6 +465,10 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin, taiga
|
|||
@taskboardTasksService.assignOrders(order)
|
||||
|
||||
@.loadSprintStats()
|
||||
@.generateFilters()
|
||||
if @.isFilterDataTypeSelected('status')
|
||||
@.loadTasks()
|
||||
|
||||
|
||||
## Template actions
|
||||
addNewTask: (type, us) ->
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
var reset = function(elm) {
|
||||
$(elm)
|
||||
.removeAttr('style')
|
||||
.removeClass('tg-backlog-us-mirror')
|
||||
.removeClass('backlog-us-mirror')
|
||||
.removeClass('tg-multiple-drag-mirror')
|
||||
.removeClass('multiple-drag-mirror')
|
||||
.data('dragMultipleIndex', null)
|
||||
.data('dragMultipleActive', false);
|
||||
};
|
||||
|
@ -40,6 +40,8 @@
|
|||
var currentTop = shadow.position().top;
|
||||
var height = shadow.outerHeight();
|
||||
|
||||
$('.gu-transit').addClass('gu-transit-multi');
|
||||
|
||||
_.forEach(dragMultiple.items.draggingItems, function(elm, index) {
|
||||
var elmIndex = parseInt(elm.data('dragMultipleIndex'), 10);
|
||||
var top = currentTop + (elmIndex * height);
|
||||
|
@ -57,22 +59,21 @@
|
|||
|
||||
refreshOriginal();
|
||||
|
||||
var current = dragMultiple.items.elm;
|
||||
var container = dragMultiple.items.container;
|
||||
|
||||
document.documentElement.removeEventListener('mousemove', removeEventFn);
|
||||
|
||||
// reset
|
||||
dragMultiple.items = {};
|
||||
|
||||
$('.' + mainClass).removeClass(mainClass);
|
||||
$('.tg-backlog-us-mirror').remove();
|
||||
$('.backlog-us-mirror').removeClass('backlog-us-mirror');
|
||||
$('.tg-multiple-drag-mirror').remove();
|
||||
$('.multiple-drag-mirror').removeClass('multiple-drag-mirror');
|
||||
|
||||
$('.tg-backlog-us-dragging')
|
||||
.removeClass('tg-backlog-us-dragging')
|
||||
$('.tg-multiple-drag-dragging')
|
||||
.removeClass('tg-multiple-drag-dragging')
|
||||
.show();
|
||||
|
||||
$('.gu-transit-multi').removeClass('gu-transit-multi');
|
||||
|
||||
return $('.' + multipleSortableClass);
|
||||
};
|
||||
|
||||
|
@ -180,8 +181,8 @@
|
|||
clone = $(item).clone(true);
|
||||
|
||||
clone
|
||||
.addClass('backlog-us-mirror')
|
||||
.addClass('tg-backlog-us-mirror')
|
||||
.addClass('multiple-drag-mirror')
|
||||
.addClass('tg-multiple-drag-mirror')
|
||||
.data('dragmultiple:originalPosition', $(item).position())
|
||||
.data('dragMultipleActive', true)
|
||||
.css({
|
||||
|
@ -194,7 +195,7 @@
|
|||
|
||||
$(item)
|
||||
.hide()
|
||||
.addClass('tg-backlog-us-dragging');
|
||||
.addClass('tg-multiple-drag-dragging');
|
||||
|
||||
return clone;
|
||||
});
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Prioritats",
|
||||
"TAGS": "Etiquetes",
|
||||
"ASSIGNED_TO": "Assignat a",
|
||||
"ROLE": "Rol",
|
||||
"CREATED_BY": "Creat per",
|
||||
"CUSTOM_FILTERS": "Filtres personalitzats",
|
||||
"EPIC": "Epic"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Prioritäten",
|
||||
"TAGS": "Schlagwörter",
|
||||
"ASSIGNED_TO": "Zugeordnet zu",
|
||||
"ROLE": "Rolle",
|
||||
"CREATED_BY": "Erstellt durch",
|
||||
"CUSTOM_FILTERS": "Benutzerfilter",
|
||||
"EPIC": "Epic"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Priorities",
|
||||
"TAGS": "Tags",
|
||||
"ASSIGNED_TO": "Assigned to",
|
||||
"ROLE": "Role",
|
||||
"CREATED_BY": "Created by",
|
||||
"CUSTOM_FILTERS": "Custom filters",
|
||||
"EPIC": "Epic"
|
||||
|
@ -1016,7 +1017,7 @@
|
|||
"CONFIRM": "Are you sure you want to delete your Taiga account?",
|
||||
"CANCEL": "Back to settings",
|
||||
"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": {
|
||||
"TITLE": "Delete project",
|
||||
|
@ -1065,13 +1066,15 @@
|
|||
"PLACEHOLDER_STATUS": "Task status",
|
||||
"OPTION_UNASSIGNED": "Unassigned",
|
||||
"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": {
|
||||
"TITLE": "New US",
|
||||
"PLACEHOLDER_DESCRIPTION": "Please add descriptive text to help others better understand this US",
|
||||
"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": {
|
||||
"TITLE": "Delete sprint"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Prioridades",
|
||||
"TAGS": "Etiquetas",
|
||||
"ASSIGNED_TO": "Asignado a",
|
||||
"ROLE": "Rol",
|
||||
"CREATED_BY": "Creada por",
|
||||
"CUSTOM_FILTERS": "Filtros personalizados",
|
||||
"EPIC": "Épica"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Kiireellisyydet",
|
||||
"TAGS": "Avainsanat",
|
||||
"ASSIGNED_TO": "Tekijä",
|
||||
"ROLE": "Rooli",
|
||||
"CREATED_BY": "Luoja",
|
||||
"CUSTOM_FILTERS": "Omat suodattimet",
|
||||
"EPIC": "Eepos"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Priorités",
|
||||
"TAGS": "Mots-clés",
|
||||
"ASSIGNED_TO": "Affecté à",
|
||||
"ROLE": "Rôle",
|
||||
"CREATED_BY": "Créé par",
|
||||
"CUSTOM_FILTERS": "Filtres personnalisés",
|
||||
"EPIC": "Épopée"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Priorità",
|
||||
"TAGS": "Tag",
|
||||
"ASSIGNED_TO": "Assegnato a",
|
||||
"ROLE": "Ruolo",
|
||||
"CREATED_BY": "Creato da",
|
||||
"CUSTOM_FILTERS": "Filtri personalizzati",
|
||||
"EPIC": "Epic"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "優先度",
|
||||
"TAGS": "タグ",
|
||||
"ASSIGNED_TO": "担当者",
|
||||
"ROLE": "役割",
|
||||
"CREATED_BY": "作成者",
|
||||
"CUSTOM_FILTERS": "カスタムフィルター",
|
||||
"EPIC": "エピック"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "우선순위",
|
||||
"TAGS": "태그",
|
||||
"ASSIGNED_TO": "할당됨",
|
||||
"ROLE": "역할",
|
||||
"CREATED_BY": "생성함",
|
||||
"CUSTOM_FILTERS": "사용자 정의 필터",
|
||||
"EPIC": "에픽"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Prioriteter",
|
||||
"TAGS": "Etiketter",
|
||||
"ASSIGNED_TO": "Tildelt til",
|
||||
"ROLE": "Rolle",
|
||||
"CREATED_BY": "Laget av",
|
||||
"CUSTOM_FILTERS": "Egendefinert filtre",
|
||||
"EPIC": "Epic"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Prioriteit",
|
||||
"TAGS": "Tags",
|
||||
"ASSIGNED_TO": "Toegewezen aan",
|
||||
"ROLE": "Rol",
|
||||
"CREATED_BY": "Aangemaakt door",
|
||||
"CUSTOM_FILTERS": "Eigen filters",
|
||||
"EPIC": "Epic"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Priorytety",
|
||||
"TAGS": "Tagi",
|
||||
"ASSIGNED_TO": "Przypisane do",
|
||||
"ROLE": "Rola",
|
||||
"CREATED_BY": "Stworzona przez",
|
||||
"CUSTOM_FILTERS": "Filtry niestandardowe",
|
||||
"EPIC": "Epic"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Prioridades",
|
||||
"TAGS": "Tags",
|
||||
"ASSIGNED_TO": "Atribuído a",
|
||||
"ROLE": "Função",
|
||||
"CREATED_BY": "Criado por",
|
||||
"CUSTOM_FILTERS": "Filtros personalizados",
|
||||
"EPIC": "Épico"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Приоритеты",
|
||||
"TAGS": "Тэги",
|
||||
"ASSIGNED_TO": "Назначено",
|
||||
"ROLE": "Роль",
|
||||
"CREATED_BY": "Создано",
|
||||
"CUSTOM_FILTERS": "Собственные фильтры",
|
||||
"EPIC": "Epic"
|
||||
|
@ -217,7 +218,7 @@
|
|||
"DB_CLICK": "двойной клик для редактирования",
|
||||
"SELECT_LANGUAGE_PLACEHOLDER": "Выбор языка",
|
||||
"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"
|
||||
},
|
||||
"PERMISIONS_CATEGORIES": {
|
||||
|
@ -611,7 +612,7 @@
|
|||
"WARNING_DELETE_ROLE": "Be careful! All role estimations will be removed",
|
||||
"ERROR_DELETE_ALL": "Вы не можете удалить все значения",
|
||||
"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": {
|
||||
"SECRET_KEY": "Секретный ключ",
|
||||
|
@ -689,7 +690,7 @@
|
|||
"DEFAULT_DELETE_MESSAGE": "приглашение на {{email}}"
|
||||
},
|
||||
"DEFAULT_VALUES": {
|
||||
"LABEL_EPIC_STATUS": "Default value for epic status selector",
|
||||
"LABEL_EPIC_STATUS": "Значение по умолчанию для селектора эпического статуса",
|
||||
"LABEL_US_STATUS": "Выбор значения статуса по умолчанию для пользовательской истории",
|
||||
"LABEL_POINTS": "Значения по умолчанию для выбора очков",
|
||||
"LABEL_TASK_STATUS": "Значение по умолчанию для статуса задачи",
|
||||
|
@ -875,7 +876,7 @@
|
|||
"TEMPLATE_SCRUM_LONGDESC": "Скрам это методология управления процессом разработки программного обеспечения построенная на коротких итерациях и постепенном прогрессе. Бэклог по продукту это те задачи которые нужно сделать, отсортированные в порядке приоритета. Бэклоги разбиваются на относительно короткие, управляемые куски - спринты. На основании возможностей, навыков и ресурсов команды в каждый спринт вносится несколько выбранных пользовательских историй из бэклога. На каждый спринт выделяется оговоренный период времени. С развитием проекта бэклог уменьшается.",
|
||||
"TEMPLATE_KANBAN": "Kanban",
|
||||
"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_DESC": "Начните сначала и сохраняйте вашу конфигурацию",
|
||||
"IMPORT": "Импортировать проект",
|
||||
|
@ -922,7 +923,7 @@
|
|||
"CHOOSE": "Выбрать пользователя",
|
||||
"LINKS": "Связь с {{platform}}",
|
||||
"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": "Назначить",
|
||||
"PROJECT_SELECTOR": {
|
||||
"NO_RESULTS": "It looks like nothing was found with your search criteria",
|
||||
|
@ -1140,7 +1141,7 @@
|
|||
"TITLE_DELETE_ACTION": "Удалить пользовательскую историю",
|
||||
"LIGHTBOX_TITLE_BLOKING_US": "Блокирующая ПИ",
|
||||
"NOT_ESTIMATED": "Не оценено",
|
||||
"OWNER_US": "This User Story belongs to",
|
||||
"OWNER_US": "Эта пользовательская история принадлежит",
|
||||
"TRIBE": {
|
||||
"PUBLISH": "Publish as Gig in Taiga Tribe",
|
||||
"PUBLISH_INFO": "Больше инфо",
|
||||
|
@ -1551,7 +1552,7 @@
|
|||
"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}}",
|
||||
"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_COLOR": "{{username}} обновил поле \"{{field_name}}\" эпика {{obj_name}} на <span class=\"new-color\" style=\"background: {{new_value}}\"></span>",
|
||||
"NEW_COMMENT_US": "{{username}} прокомментировал ПИ {{obj_name}}",
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Prioritet",
|
||||
"TAGS": "Etiketter",
|
||||
"ASSIGNED_TO": "Tilldelad till",
|
||||
"ROLE": "Roll",
|
||||
"CREATED_BY": "Skapad av",
|
||||
"CUSTOM_FILTERS": "Anpassad filter",
|
||||
"EPIC": "Epost"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "Öncelikler",
|
||||
"TAGS": "Etiketler ",
|
||||
"ASSIGNED_TO": "Atanmış",
|
||||
"ROLE": "Rol",
|
||||
"CREATED_BY": "Oluşturan",
|
||||
"CUSTOM_FILTERS": "Özel filtreler",
|
||||
"EPIC": "Destan"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "优先级",
|
||||
"TAGS": "标签",
|
||||
"ASSIGNED_TO": "指派给",
|
||||
"ROLE": "角色",
|
||||
"CREATED_BY": "由创建",
|
||||
"CUSTOM_FILTERS": "定制过滤器",
|
||||
"EPIC": "史诗"
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"PRIORITIES": "優先性",
|
||||
"TAGS": "標籤",
|
||||
"ASSIGNED_TO": "指派給 ",
|
||||
"ROLE": "角色",
|
||||
"CREATED_BY": "由創建",
|
||||
"CUSTOM_FILTERS": "客製過濾器 ",
|
||||
"EPIC": "Epic"
|
||||
|
|
|
@ -3,7 +3,8 @@ span.belong-to-epic-text-wrapper(tg-repeat="epic in immutable_epics track by epi
|
|||
a.belong-to-epic-text(
|
||||
href=""
|
||||
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(
|
||||
ng-style="::{'background-color': epic.get('color')}"
|
||||
translate="EPICS.EPIC"
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
tg-check-permission="{{vm.getPermissionsKey()}}"
|
||||
)
|
||||
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=""
|
||||
)
|
||||
tg-svg(svg-icon="icon-add-user")
|
||||
|
@ -31,7 +31,7 @@
|
|||
|
||||
a.e2e-edit.card-edit(
|
||||
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-svg(svg-icon="icon-edit")
|
||||
|
|
|
@ -4,4 +4,5 @@ ul.card-tasks(ng-if="vm.isRelatedTasksVisible()")
|
|||
href="#"
|
||||
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')}"
|
||||
) {{"#" + task.get('ref')}} {{task.get('subject')}}
|
||||
ng-bind-html="'#'+task.get('ref')+' '+task.get('subject') | emojify"
|
||||
)
|
||||
|
|
|
@ -6,7 +6,7 @@ h2.card-title
|
|||
title="#{{ ::vm.item.getIn(['model', 'ref']) }} {{ vm.item.getIn(['model', 'subject'])}}"
|
||||
)
|
||||
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(
|
||||
format="pill"
|
||||
ng-if="vm.item.getIn(['model', 'epics'])"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.card-unfold.ng-animate-disabled(
|
||||
ng-click="vm.toggleFold()"
|
||||
ng-click="!$event.ctrlKey && vm.toggleFold()"
|
||||
ng-if="vm.visible('unfold') && (vm.hasTasks() || vm.hasVisibleAttachments())"
|
||||
role="button"
|
||||
)
|
||||
|
|
|
@ -14,3 +14,15 @@
|
|||
images="vm.item.get('images')"
|
||||
)
|
||||
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
|
|
@ -7,10 +7,12 @@
|
|||
span.detail-subject.e2e-title-subject(
|
||||
ng-click="vm.editSubject(true)"
|
||||
ng-if="vm.permissions.canEdit"
|
||||
) {{vm.item.subject}}
|
||||
ng-bind-html="vm.item.subject | emojify"
|
||||
)
|
||||
span.detail-subject.e2e-title-subject(
|
||||
ng-if="!vm.permissions.canEdit"
|
||||
) {{vm.item.subject}}
|
||||
ng-bind-html="vm.item.subject | emojify"
|
||||
)
|
||||
a(
|
||||
href=""
|
||||
ng-if="vm.permissions.canEdit"
|
||||
|
@ -55,7 +57,7 @@
|
|||
title="{{'TASK.TITLE_LINK_GO_OWNER' | translate}}"
|
||||
)
|
||||
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(
|
||||
ng-if="::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"
|
||||
tg-check-permission="view_us"
|
||||
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-external-reference(ng-if="vm.item.external_reference")
|
||||
|
@ -91,7 +94,7 @@
|
|||
title="{{'US.TITLE_LINK_GO_TO_ISSUE' | translate}}"
|
||||
)
|
||||
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
|
||||
.block-desc-container(ng-show="vm.item.is_blocked")
|
||||
|
|
|
@ -18,7 +18,15 @@ form(name="vm.filtersForm")
|
|||
.filters-step-cat
|
||||
.filters-applied
|
||||
.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(
|
||||
ng-click="vm.unselectFilter(it)"
|
||||
href=""
|
||||
|
@ -77,7 +85,15 @@ form(name="vm.filtersForm")
|
|||
ng-if="!vm.isFilterSelected(filter, it) && !(it.count == 0 && filter.hideEmpty)"
|
||||
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}}
|
||||
|
||||
li.custom-filters.e2e-custom-filters(
|
||||
|
|
|
@ -128,6 +128,9 @@ tg-filter {
|
|||
@include ellipsis(100%);
|
||||
display: block;
|
||||
width: 100%;
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.number {
|
||||
background: darken($whitish, 20%); // Fallback
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
span {{ tag[0] }}
|
||||
span(ng-bind-html="tag[0] | emojify")
|
||||
tg-svg.icon-close.e2e-delete-tag(
|
||||
ng-if="hasPermissions"
|
||||
svg-icon="icon-close"
|
||||
|
|
|
@ -19,4 +19,7 @@
|
|||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -208,8 +208,6 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad
|
|||
$scope.codeEditorVisible = false
|
||||
$scope.codeLans = []
|
||||
|
||||
wysiwygService.loadEmojis()
|
||||
|
||||
wysiwygCodeHightlighterService.getLanguages().then (codeLans) ->
|
||||
$scope.codeLans = codeLans
|
||||
|
||||
|
|
|
@ -26,49 +26,13 @@ class WysiwygService
|
|||
@.$inject = [
|
||||
"tgWysiwygCodeHightlighterService",
|
||||
"tgProjectService",
|
||||
"$tgNavUrls"
|
||||
"$tgNavUrls",
|
||||
"$tgEmojis"
|
||||
]
|
||||
constructor: (@wysiwygCodeHightlighterService, @projectService, @navurls) ->
|
||||
constructor: (@wysiwygCodeHightlighterService, @projectService, @navurls, @emojis) ->
|
||||
|
||||
searchEmojiByName: (name) ->
|
||||
return _.filter @.emojis, (it) -> it.name.indexOf(name) != -1
|
||||
|
||||
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, '')
|
||||
|
||||
return text
|
||||
return @emojis.searchByName(name)
|
||||
|
||||
pipeLinks: (text) ->
|
||||
return text.replace /\[\[(.*?)\]\]/g, (match, p1, offset, str) ->
|
||||
|
@ -134,7 +98,7 @@ class WysiwygService
|
|||
}
|
||||
|
||||
html = html.replace(/ (<\/.*>)/g, "$1")
|
||||
html = @.replaceImgsByEmojiName(html)
|
||||
html = @emojis.replaceImgsByEmojiName(html)
|
||||
html = @.replaceUrls(html)
|
||||
html = @.removeTrailingListBr(html)
|
||||
|
||||
|
@ -211,7 +175,7 @@ class WysiwygService
|
|||
breaks: true
|
||||
}
|
||||
|
||||
text = @.replaceEmojiNameByImgs(text)
|
||||
text = @emojis.replaceEmojiNameByImgs(text)
|
||||
text = @.pipeLinks(text)
|
||||
|
||||
md = window.markdownit({
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
a(
|
||||
tg-nav="project-epics-detail:project=vm.project.slug,ref=vm.epic.get('ref')"
|
||||
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(
|
||||
ng-style="::{'background-color': vm.epic.get('color')}"
|
||||
translate="EPICS.EPIC"
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
a(
|
||||
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')}}"
|
||||
) #{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(
|
||||
ng-if="vm.story.get('epics')"
|
||||
format="pill"
|
||||
|
|
|
@ -8,7 +8,8 @@ tg-svg.icon-drag(
|
|||
a(
|
||||
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')}}"
|
||||
) #{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(
|
||||
format="pill"
|
||||
|
|
|
@ -39,6 +39,11 @@ describe "dutyDirective", () ->
|
|||
return value
|
||||
provide.value "translateFilter", mockTranslateFilter
|
||||
|
||||
_mockEmojifyFilter = () ->
|
||||
mockEmojifyFilter = (value) ->
|
||||
return value
|
||||
provide.value "emojifyFilter", mockEmojifyFilter
|
||||
|
||||
_mockTgProjectsService = () ->
|
||||
mockTgProjectsService = {
|
||||
projectsById: {
|
||||
|
@ -60,6 +65,7 @@ describe "dutyDirective", () ->
|
|||
_mockTgProjectsService()
|
||||
_mockTranslate()
|
||||
_mockTranslateFilter()
|
||||
_mockEmojifyFilter()
|
||||
return null
|
||||
|
||||
beforeEach ->
|
||||
|
|
|
@ -39,4 +39,4 @@ a.list-itemtype-ticket(
|
|||
ng-if="::vm.duty.get('is_blocked')"
|
||||
title="{{::vm.duty.get('blocked_note')}}"
|
||||
) {{ 'COMMON.BLOCKED' | translate }}
|
||||
span {{ ::duty.get('subject') }}
|
||||
span(ng-bind-html="duty.get('subject') | emojify")
|
||||
|
|
|
@ -23,6 +23,7 @@ DropdownUserDirective = (authService, configService, locationService,
|
|||
link = (scope, el, attrs, ctrl) ->
|
||||
scope.vm = {}
|
||||
scope.vm.isFeedbackEnabled = configService.get("feedbackEnabled")
|
||||
scope.vm.supportUrl = configService.get("supportUrl")
|
||||
taiga.defineImmutableProperty(scope.vm, "user", () -> authService.userData)
|
||||
|
||||
scope.vm.logout = ->
|
||||
|
|
|
@ -51,7 +51,7 @@ div.navbar-dropdown.dropdown-user
|
|||
translate="PROJECT.NAVIGATION.FEEDBACK")
|
||||
li
|
||||
a(
|
||||
href="https://tree.taiga.io/support/",
|
||||
href="{{ vm.supportUrl }}",
|
||||
target="_blank",
|
||||
title="{{'PROJECT.NAVIGATION.HELP_TITLE' | translate}}",
|
||||
translate="PROJECT.NAVIGATION.HELP")
|
||||
|
|
|
@ -53,25 +53,29 @@
|
|||
ng-if="::vm.item.get('type') === 'epic'"
|
||||
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') }}"
|
||||
) {{ ::vm.item.get('subject') }}
|
||||
ng-bind-html="vm.item.get('subject') | emojify"
|
||||
)
|
||||
a.ticket-title(
|
||||
href="#"
|
||||
ng-if="::vm.item.get('type') === 'userstory'"
|
||||
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') }}"
|
||||
) {{ ::vm.item.get('subject') }}
|
||||
ng-bind-html="vm.item.get('subject') | emojify"
|
||||
)
|
||||
a.ticket-title(
|
||||
href="#"
|
||||
ng-if="::vm.item.get('type') === 'task'"
|
||||
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') }}"
|
||||
) {{ ::vm.item.get('subject') }}
|
||||
ng-bind-html="vm.item.get('subject') | emojify"
|
||||
)
|
||||
a.ticket-title(
|
||||
href="#"
|
||||
ng-if="::vm.item.get('type') === 'issue'"
|
||||
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') }}"
|
||||
) {{ ::vm.item.get('subject') }}
|
||||
ng-bind-html="vm.item.get('subject') | emojify"
|
||||
)
|
||||
|
||||
div.list-itemtype-track
|
||||
span.list-itemtype-track-likers(
|
||||
|
|
|
@ -28,6 +28,7 @@ class WikiHistoryController
|
|||
|
||||
constructor: (@wikiHistoryService) ->
|
||||
taiga.defineImmutableProperty @, 'historyEntries', () => return @wikiHistoryService.historyEntries
|
||||
@.toggle = false
|
||||
|
||||
initializeHistoryEntries: (wikiId) ->
|
||||
if wikiId
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
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=""
|
||||
title="{{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-repeat="historyEntry in vm.historyEntries"
|
||||
history-entry="historyEntry"
|
||||
|
|
|
@ -2,7 +2,7 @@ doctype html
|
|||
|
||||
div.error-main
|
||||
div.error-container
|
||||
img(src="/#{v}/svg/logo.svg", alt="TAIGA")
|
||||
img(src="/#{v}/svg/logo.svg", alt="TAIGA", width="143px")
|
||||
h1.logo Taiga
|
||||
p.error-text(translate="ERROR.TEXT1")
|
||||
a(href="/", title="", translate="COMMON.GO_HOME")
|
||||
|
|
|
@ -2,7 +2,7 @@ doctype html
|
|||
|
||||
div.error-main
|
||||
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")
|
||||
p.error-text(translate="ERROR.NOT_FOUND_TEXT")
|
||||
a(href="/", title="", translate="COMMON.GO_HOME")
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
title="#{{ us.ref }} {{ us.subject }}"
|
||||
)
|
||||
span(tg-bo-ref="us.ref")
|
||||
span(ng-bind="us.subject")
|
||||
span(ng-bind-html="us.subject | emojify")
|
||||
tg-belong-to-epics(
|
||||
format="pill"
|
||||
ng-if="us.epics"
|
||||
|
|
|
@ -53,7 +53,7 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}")
|
|||
ng-if="issue.is_blocked"
|
||||
title="{{issue.blocked_note}}"
|
||||
) {{'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")
|
||||
|
|
|
@ -10,7 +10,15 @@ div.kanban-table(
|
|||
tg-bo-title="s.name",
|
||||
ng-class='{vfold:folds[s.id]}',
|
||||
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
|
||||
a.option(
|
||||
href=""
|
||||
|
@ -67,7 +75,7 @@ div.kanban-table(
|
|||
|
||||
tg-card.card.ng-animate-disabled(
|
||||
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-bind-scope,
|
||||
on-toggle-fold="ctrl.toggleFold(id)"
|
||||
|
@ -78,6 +86,7 @@ div.kanban-table(
|
|||
zoom="ctrl.zoom"
|
||||
zoom-level="ctrl.zoomLevel"
|
||||
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")
|
||||
|
|
|
@ -14,7 +14,7 @@ script(type="text/ng-template", id="search-issues")
|
|||
div.user-stories
|
||||
div.user-story-name
|
||||
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.assigned-to(tg-listitem-assignedto="issue")
|
||||
|
||||
|
@ -34,7 +34,7 @@ script(type="text/ng-template", id="search-epics")
|
|||
div.user-stories
|
||||
div.user-story-name
|
||||
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.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-story-name
|
||||
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-link
|
||||
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-story-name
|
||||
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.assigned-to(tg-listitem-assignedto="task")
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ div.sprint-table(tg-bind-scope, ng-class="{'sprint-empty-wrapper': !sprint.user_
|
|||
tg-bo-title="'#' + us.ref + ' ' + us.subject",
|
||||
ng-class="{closed: us.is_closed, blocked: us.is_blocked}")
|
||||
span(tg-bo-ref="us.ref")
|
||||
span(tg-bo-bind="us.subject")
|
||||
span(tg-bind-html="us.subject | emojify")
|
||||
tg-belong-to-epics(
|
||||
format="pill"
|
||||
ng-if="us.epics"
|
||||
|
|
|
@ -53,7 +53,7 @@ div.taskboard-table(
|
|||
tg-nav-get-params="{\"milestone\": {{us.milestone}}}",
|
||||
tg-bo-title="'#' + us.ref + ' ' + us.subject")
|
||||
span.us-ref(tg-bo-ref="us.ref")
|
||||
span(ng-bind="us.subject")
|
||||
span(ng-bind-html="us.subject | emojify")
|
||||
tg-belong-to-epics(
|
||||
format="pill"
|
||||
ng-if="us.epics"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
a.clickable(
|
||||
tg-nav="project-tasks-detail:project=project.slug,ref=task.ref")
|
||||
span #<%- task.ref %>
|
||||
span(ng-non-bindable) <%- task.subject %>
|
||||
span(ng-non-bindable) <%= emojify(task.subject) %>
|
||||
|
||||
.task-settings
|
||||
<% if(perms.modify_task) { %>
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.backlog-us-mirror {
|
||||
.multiple-drag-mirror.us-item-row {
|
||||
background: $white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 2px 5px $gray;
|
||||
|
|
|
@ -91,8 +91,18 @@ $column-padding: .5rem 1rem;
|
|||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
span {
|
||||
@include ellipsis(65%);
|
||||
.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%);
|
||||
}
|
||||
}
|
||||
.option {
|
||||
margin-right: .3rem;
|
||||
|
@ -148,9 +158,63 @@ $column-padding: .5rem 1rem;
|
|||
.kanban-uses-box {
|
||||
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 {
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 484 B |
Before Width: | Height: | Size: 460 B After Width: | Height: | Size: 449 B |
Before Width: | Height: | Size: 435 B After Width: | Height: | Size: 422 B |
Before Width: | Height: | Size: 277 B After Width: | Height: | Size: 268 B |
Before Width: | Height: | Size: 455 B After Width: | Height: | Size: 443 B |
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 464 B |
Before Width: | Height: | Size: 424 B After Width: | Height: | Size: 414 B |
Before Width: | Height: | Size: 469 B After Width: | Height: | Size: 458 B |
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 432 B |
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 384 B |
Before Width: | Height: | Size: 458 B After Width: | Height: | Size: 448 B |
Before Width: | Height: | Size: 446 B After Width: | Height: | Size: 440 B |
BIN
emojis/1f004.png
Before Width: | Height: | Size: 467 B After Width: | Height: | Size: 457 B |
BIN
emojis/1f0cf.png
Before Width: | Height: | Size: 607 B After Width: | Height: | Size: 603 B |
BIN
emojis/1f170.png
Before Width: | Height: | Size: 469 B After Width: | Height: | Size: 460 B |
BIN
emojis/1f171.png
Before Width: | Height: | Size: 436 B After Width: | Height: | Size: 427 B |
BIN
emojis/1f17e.png
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 505 B |
BIN
emojis/1f17f.png
Before Width: | Height: | Size: 404 B After Width: | Height: | Size: 389 B |
BIN
emojis/1f18e.png
Before Width: | Height: | Size: 596 B After Width: | Height: | Size: 595 B |
BIN
emojis/1f191.png
Before Width: | Height: | Size: 532 B After Width: | Height: | Size: 530 B |
BIN
emojis/1f192.png
Before Width: | Height: | Size: 482 B After Width: | Height: | Size: 478 B |
BIN
emojis/1f193.png
Before Width: | Height: | Size: 531 B After Width: | Height: | Size: 526 B |
BIN
emojis/1f194.png
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 463 B |
BIN
emojis/1f195.png
Before Width: | Height: | Size: 486 B After Width: | Height: | Size: 486 B |
BIN
emojis/1f196.png
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 589 B |
BIN
emojis/1f197.png
Before Width: | Height: | Size: 596 B After Width: | Height: | Size: 592 B |
BIN
emojis/1f198.png
Before Width: | Height: | Size: 587 B After Width: | Height: | Size: 586 B |
BIN
emojis/1f199.png
Before Width: | Height: | Size: 513 B After Width: | Height: | Size: 510 B |