New attachments components

- Attachments image gallery.
- Upload attachments on US/issue/task lightbox.
- Drag files from desktop.
- Completly new attachment code.
- Drag files in wysiwyg
stable
Juanfran 2015-08-13 14:48:42 +02:00 committed by David Barragán Merino
parent 298b528e49
commit 50af448305
90 changed files with 2654 additions and 803 deletions

View File

@ -1,18 +1,12 @@
# Changelog #
## 1.10.0 ??? (unreleased)
## 1.10.0 ??? (Unreleased)
### Features
- ...
### Misc
- Lots of small and not so small bugfixes.
## 1.9.1 Taiga Tribe (2016-01-05)
### Features
- Statics folder hash.
- Attachments image gallery.
- Upload attachments on US/issue/task lightbox.
- Drag files from desktop.
- Drag files in wysiwyg.
- [118n] Now taiga plugins can be translatable.
- New Taiga plugins system.
- Now superadmins can send notifications (live announcement) to the user (through taiga-events).

View File

@ -54,11 +54,12 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
"$tgEvents",
"$tgAnalytics",
"$translate",
"$tgLoading"
"$tgLoading",
"tgResources"
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q,
@location, @appMetaService, @navUrls, @events, @analytics, @translate, @loading) ->
@location, @appMetaService, @navUrls, @events, @analytics, @translate, @loading, @rs2) ->
bindMethods(@)
@scope.sectionName = @translate.instant("BACKLOG.SECTION_NAME")
@ -566,9 +567,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
.timeout(200)
.start()
@rs.userstories.getByRef(projectId, ref).then (us) =>
@rootscope.$broadcast("usform:edit", us)
return @rs.userstories.getByRef(projectId, ref).then (us) =>
@rs2.attachments.list("us", us.id, projectId).then (attachments) =>
@rootscope.$broadcast("usform:edit", us, attachments.toJS())
currentLoading.finish()
deleteUserStory: (us) ->

View File

@ -96,22 +96,28 @@ module.factory("$selectedText", ["$window", "$document", SelectedText])
## Permission directive, hide elements when necessary
#############################################################################
CheckPermissionDirective = ->
CheckPermissionDirective = (projectService) ->
render = ($el, project, permission) ->
$el.removeClass('hidden') if project.my_permissions.indexOf(permission) > -1
$el.removeClass('hidden') if project.get('my_permissions').indexOf(permission) > -1
link = ($scope, $el, $attrs) ->
$el.addClass('hidden')
permission = $attrs.tgCheckPermission
$scope.$watch "project", (project) ->
render($el, project, permission) if project?
$scope.$watch ( () ->
return projectService.project
), () ->
render($el, projectService.project, permission) if projectService.project
$scope.$on "$destroy", ->
$el.off()
return {link:link}
CheckPermissionDirective.$inject = [
"tgProjectService"
]
module.directive("tgCheckPermission", CheckPermissionDirective)
#############################################################################

View File

@ -1,343 +0,0 @@
###
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
# Copyright (C) 2014-2016 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/attachments.coffee
###
taiga = @.taiga
sizeFormat = @.taiga.sizeFormat
bindOnce = @.taiga.bindOnce
bindMethods = @.taiga.bindMethods
module = angular.module("taigaCommon")
class AttachmentsController extends taiga.Controller
@.$inject = ["$scope", "$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", "$q", "$translate"]
constructor: (@scope, @rootscope, @repo, @rs, @confirm, @q, @translate) ->
bindMethods(@)
@.type = null
@.objectId = null
@.projectId = null
@.uploadingAttachments = []
@.attachments = []
@.attachmentsCount = 0
@.deprecatedAttachmentsCount = 0
@.showDeprecated = false
initialize: (type, objectId) ->
@.type = type
@.objectId = objectId
@.projectId = @scope.projectId
loadAttachments: ->
return @.attachments if not @.objectId
urlname = "attachments/#{@.type}"
return @rs.attachments.list(urlname, @.objectId, @.projectId).then (attachments) =>
@.attachments = _.sortBy(attachments, "order")
@.updateCounters()
return attachments
updateCounters: ->
@.attachmentsCount = @.attachments.length
@.deprecatedAttachmentsCount = _.filter(@.attachments, {is_deprecated: true}).length
_createAttachment: (attachment) ->
urlName = "attachments/#{@.type}"
promise = @rs.attachments.create(urlName, @.projectId, @.objectId, attachment)
promise = promise.then (data) =>
data.isCreatedRightNow = true
index = @.uploadingAttachments.indexOf(attachment)
@.uploadingAttachments.splice(index, 1)
@.attachments.push(data)
@rootscope.$broadcast("attachment:create")
promise = promise.then null, (data) =>
@scope.$emit("attachments:size-error") if data.status == 413
index = @.uploadingAttachments.indexOf(attachment)
@.uploadingAttachments.splice(index, 1)
message = @translate.instant("ATTACHMENT.ERROR_UPLOAD_ATTACHMENT", {
fileName: attachment.name, errorMessage: data.data._error_message})
@confirm.notify("error", message)
return @q.reject(data)
return promise
# Create attachments in bulk
createAttachments: (attachments) ->
promises = _.map(attachments, (x) => @._createAttachment(x))
return @q.all(promises).then =>
@.updateCounters()
# Add uploading attachment tracking.
addUploadingAttachments: (attachments) ->
@.uploadingAttachments = _.union(@.uploadingAttachments, attachments)
# Change order of attachment in a ordered list.
# This function is mainly executed after sortable ends.
reorderAttachment: (attachment, newIndex) ->
oldIndex = @.attachments.indexOf(attachment)
return if oldIndex == newIndex
@.attachments.splice(oldIndex, 1)
@.attachments.splice(newIndex, 0, attachment)
_.each(@.attachments, (x,i) -> x.order = i+1)
# Persist one concrete attachment.
# This function is mainly used when user clicks
# to save button for save one unique attachment.
updateAttachment: (attachment) ->
onSuccess = =>
@.updateCounters()
@rootscope.$broadcast("attachment:edit")
onError = (response) =>
$scope.$emit("attachments:size-error") if response.status == 413
@confirm.notify("error")
return @q.reject()
return @repo.save(attachment).then(onSuccess, onError)
# Persist all pending modifications on attachments.
# This function is used mainly for persist the order
# after sorting.
saveAttachments: ->
return @repo.saveAll(@.attachments).then null, =>
for item in @.attachments
item.revert()
@.attachments = _.sortBy(@.attachments, "order")
# Remove one concrete attachment.
removeAttachment: (attachment) ->
title = @translate.instant("ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT")
message = @translate.instant("ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT", {fileName: attachment.name})
return @confirm.askOnDelete(title, message).then (askResponse) =>
onSuccess = =>
askResponse.finish()
index = @.attachments.indexOf(attachment)
@.attachments.splice(index, 1)
@.updateCounters()
@rootscope.$broadcast("attachment:delete")
onError = =>
askResponse.finish(false)
message = @translate.instant("ATTACHMENT.ERROR_DELETE_ATTACHMENT", {errorMessage: message})
@confirm.notify("error", null, message)
return @q.reject()
return @repo.remove(attachment).then(onSuccess, onError)
# Function used in template for filter visible attachments
filterAttachments: (item) ->
if @.showDeprecated
return true
return not item.is_deprecated
AttachmentsDirective = ($config, $confirm, $templates, $translate) ->
template = $templates.get("attachment/attachments.html", true)
link = ($scope, $el, $attrs, $ctrls) ->
$ctrl = $ctrls[0]
$model = $ctrls[1]
bindOnce $scope, $attrs.ngModel, (value) ->
$ctrl.initialize($attrs.type, value.id)
$ctrl.loadAttachments()
tdom = $el.find("div.attachment-body.sortable")
tdom.sortable({
items: "div.single-attachment"
handle: "a.settings.icon.icon-drag-v"
containment: ".attachments"
dropOnEmpty: true
scroll: false
tolerance: "pointer"
placeholder: "sortable-placeholder single-attachment"
})
tdom.on "sortstop", (event, ui) ->
attachment = ui.item.scope().attach
newIndex = ui.item.index()
$ctrl.reorderAttachment(attachment, newIndex)
$ctrl.saveAttachments().then ->
$scope.$emit("attachment:edit")
showSizeInfo = ->
$el.find(".size-info").removeClass("hidden")
$scope.$on "attachments:size-error", ->
showSizeInfo()
$el.on "change", ".attachments-header input", (event) ->
files = _.toArray(event.target.files)
return if files.length < 1
$scope.$apply ->
$ctrl.addUploadingAttachments(files)
$ctrl.createAttachments(files)
$el.on "click", ".more-attachments", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
$scope.$apply ->
$ctrl.showDeprecated = not $ctrl.showDeprecated
target.find("span.text").addClass("hidden")
if $ctrl.showDeprecated
target.find("span[data-type=hide]").removeClass("hidden")
target.find("more-attachments-num").addClass("hidden")
else
target.find("span[data-type=show]").removeClass("hidden")
target.find("more-attachments-num").removeClass("hidden")
$scope.$on "$destroy", ->
$el.off()
templateFn = ($el, $attrs) ->
maxFileSize = $config.get("maxUploadFileSize", null)
maxFileSize = sizeFormat(maxFileSize) if maxFileSize
maxFileSizeMsg = if maxFileSize then $translate.instant("ATTACHMENT.MAX_UPLOAD_SIZE", {maxFileSize: maxFileSize}) else ""
ctx = {
type: $attrs.type
maxFileSize: maxFileSize
maxFileSizeMsg: maxFileSizeMsg
}
return template(ctx)
return {
require: ["tgAttachments", "ngModel"]
controller: AttachmentsController
controllerAs: "ctrl"
restrict: "AE"
scope: true
link: link
template: templateFn
}
module.directive("tgAttachments", ["$tgConfig", "$tgConfirm", "$tgTemplate", "$translate", AttachmentsDirective])
AttachmentDirective = ($template, $compile, $translate, $rootScope) ->
template = $template.get("attachment/attachment.html", true)
templateEdit = $template.get("attachment/attachment-edit.html", true)
link = ($scope, $el, $attrs, $ctrl) ->
render = (attachment, edit=false) ->
permissions = $scope.project.my_permissions
modifyPermission = permissions.indexOf("modify_#{$ctrl.type}") > -1
ctx = {
id: attachment.id
name: attachment.name
title : $translate.instant("ATTACHMENT.TITLE", {
fileName: attachment.name,
date: moment(attachment.created_date).format($translate.instant("ATTACHMENT.DATE"))})
url: attachment.url
size: sizeFormat(attachment.size)
description: attachment.description
isDeprecated: attachment.is_deprecated
modifyPermission: modifyPermission
}
if edit
html = $compile(templateEdit(ctx))($scope)
else
html = $compile(template(ctx))($scope)
$el.html(html)
if attachment.is_deprecated
$el.addClass("deprecated")
$el.find("input:checkbox").prop('checked', true)
else
$el.removeClass("deprecated")
saveAttachment = ->
attachment.description = $el.find("input[name='description']").val()
attachment.is_deprecated = $el.find("input[name='is-deprecated']").prop("checked")
attachment.isCreatedRightNow = false
$scope.$apply ->
$ctrl.updateAttachment(attachment).then ->
render(attachment, false)
## Actions (on edit mode)
$el.on "click", "a.editable-settings.icon-floppy", (event) ->
event.preventDefault()
saveAttachment()
$el.on "keyup", "input[name=description]", (event) ->
if event.keyCode == 13
saveAttachment()
else if event.keyCode == 27
$scope.$apply -> render(attachment, false)
$el.on "click", "a.editable-settings.icon-delete", (event) ->
event.preventDefault()
render(attachment, false)
## Actions (on view mode)
$el.on "click", "a.settings.icon-edit", (event) ->
event.preventDefault()
render(attachment, true)
$el.find("input[name='description']").focus().select()
$el.on "click", "a.settings.icon-delete", (event) ->
event.preventDefault()
$scope.$apply ->
$ctrl.removeAttachment(attachment)
$el.on "click", "div.attachment-name a", (event) ->
if null != attachment.name.match(/\.(jpe?g|png|gif|gifv|webm)/i)
event.preventDefault()
$scope.$apply ->
$rootScope.$broadcast("attachment:preview", attachment)
$scope.$on "$destroy", ->
$el.off()
# Bootstrap
attachment = $scope.$eval($attrs.tgAttachment)
render(attachment, attachment.isCreatedRightNow)
if attachment.isCreatedRightNow
$el.find("input[name='description']").focus().select()
return {
link: link
require: "^tgAttachments"
restrict: "AE"
}
module.directive("tgAttachment", ["$tgTemplate", "$compile", "$translate", "$rootScope", AttachmentDirective])

View File

@ -607,8 +607,79 @@ EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading,
template: template
}
module.directive("tgEditableDescription", ["$rootScope", "$tgRepo", "$tgConfirm", "$compile", "$tgLoading",
"$selectedText", "$tgQqueue", "$tgTemplate", EditableDescriptionDirective])
module.directive("tgEditableDescription", [
"$rootScope",
"$tgRepo",
"$tgConfirm",
"$compile",
"$tgLoading",
"$selectedText",
"$tgQqueue",
"$tgTemplate", EditableDescriptionDirective])
EditableWysiwyg = (attachmentsService) ->
link = ($scope, $el, $attrs, $model) ->
isInEditMode = ->
return $el.find('textarea').is(':visible')
$el.on 'dragover', (e) ->
textarea = $el.find('textarea').focus()
return false
$el.on 'drop', (e) ->
e.stopPropagation()
e.preventDefault()
if isInEditMode()
dataTransfer = e.dataTransfer || (e.originalEvent && e.originalEvent.dataTransfer)
textarea = $el.find('textarea')
textarea.addClass('in-progress')
type = $model.$modelValue['_name']
if type == "userstories"
type = "us"
else if type == "tasks"
type = "task"
else if type == "issues"
type = "issue"
else if type == "wiki"
type = "wiki_page"
file = dataTransfer.files[0]
return if !attachmentsService.validate(file)
attachmentsService.upload(
file,
$model.$modelValue.id,
$model.$modelValue.project,
type
).then (result) ->
textarea = $el.find('textarea')
if taiga.isImage(result.get('name'))
url = '![' + result.get('name') + '](' + result.get('url') + ')'
else
url = '[' + result.get('name') + '](' + result.get('url') + ')'
$.markItUp({ replaceWith: url })
textarea.removeClass('in-progress')
return {
link: link
restrict: "EA"
require: "ngModel"
}
module.directive("tgEditableWysiwyg", ["tgAttachmentsService", EditableWysiwyg])
#############################################################################

View File

@ -68,3 +68,9 @@ momentFromNow = ->
return ""
module.filter("momentFromNow", momentFromNow)
sizeFormat = =>
return @.taiga.sizeFormat
module.filter("sizeFormat", sizeFormat)

View File

@ -27,6 +27,7 @@ module = angular.module("taigaCommon")
bindOnce = @.taiga.bindOnce
timeout = @.taiga.timeout
debounce = @.taiga.debounce
sizeFormat = @.taiga.sizeFormat
#############################################################################
## Common Lightbox Services
@ -265,13 +266,30 @@ module.directive("tgBlockingMessageInput", ["$log", "$tgTemplate", "$compile", B
## Create/Edit Userstory Lightbox Directive
#############################################################################
CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $loading, $translate) ->
CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService, $loading, $translate, $confirm, $q, attachmentsService) ->
link = ($scope, $el, attrs) ->
$scope.createEditUs = {}
$scope.isNew = true
attachmentsToAdd = Immutable.List()
attachmentsToDelete = Immutable.List()
resetAttachments = () ->
attachmentsToAdd = Immutable.List()
attachmentsToDelete = Immutable.List()
$scope.addAttachment = (attachment) ->
attachmentsToAdd = attachmentsToAdd.push(attachment)
$scope.deleteAttachment = (attachment) ->
attachmentsToDelete = attachmentsToDelete.push(attachment)
$scope.$on "usform:new", (ctx, projectId, status, statusList) ->
$scope.isNew = true
$scope.usStatusList = statusList
$scope.attachments = Immutable.List()
resetAttachments()
$scope.us = $model.make_model("userstories", {
project: projectId
@ -293,10 +311,13 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
lightboxService.open($el)
$scope.$on "usform:edit", (ctx, us) ->
$scope.$on "usform:edit", (ctx, us, attachments) ->
$scope.us = us
$scope.attachments = Immutable.fromJS(attachments)
$scope.isNew = false
resetAttachments()
# Update texts for edition
$el.find(".button-green").html($translate.instant("COMMON.SAVE"))
$el.find(".title").html($translate.instant("LIGHTBOX.CREATE_EDIT_US.EDIT_US"))
@ -321,6 +342,18 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
lightboxService.open($el)
createAttachments = (obj) ->
promises = _.map attachmentsToAdd.toJS(), (attachment) ->
attachmentsService.upload(attachment.file, obj.id, $scope.us.project, 'us')
return $q.all(promises)
deleteAttachments = (obj) ->
promises = _.map attachmentsToDelete.toJS(), (attachment) ->
return attachmentsService.delete("us", attachment.id)
return $q.all(promises)
submit = debounce 2000, (event) =>
event.preventDefault()
@ -339,6 +372,12 @@ CreateEditUserstoryDirective = ($repo, $model, $rs, $rootScope, lightboxService,
promise = $repo.save($scope.us)
broadcastEvent = "usform:edit:success"
promise.then (data) ->
createAttachments(data)
deleteAttachments(data)
return data
promise.then (data) ->
currentLoading.finish()
lightboxService.close($el)
@ -380,6 +419,9 @@ module.directive("tgLbCreateEditUserstory", [
"lightboxService",
"$tgLoading",
"$translate",
"$tgConfirm",
"$q",
"tgAttachmentsService",
CreateEditUserstoryDirective
])
@ -632,30 +674,14 @@ module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboar
## Attachment Preview Lighbox
#############################################################################
AttachmentPreviewLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationService, $template, $compile) ->
AttachmentPreviewLightboxDirective = (lightboxService, $template, $compile) ->
link = ($scope, $el, attrs) ->
template = $template.get("common/lightbox/lightbox-attachment-preview.html", true)
$scope.$on "attachment:preview", (event, attachment) ->
lightboxService.open($el)
render(attachment)
$scope.$on "$destroy", ->
$el.off()
render = (attachment) ->
ctx = {
url: attachment.url,
title: attachment.description,
name: attachment.name
}
html = template(ctx)
html = $compile(html)($scope)
$el.html(html)
return {
link: link
templateUrl: 'common/lightbox/lightbox-attachment-preview.html',
link: link,
scope: true
}
module.directive("tgLbAttachmentPreview", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", AttachmentPreviewLightboxDirective])
module.directive("tgLbAttachmentPreview", ["lightboxService", "$tgTemplate", "$compile", AttachmentPreviewLightboxDirective])

View File

@ -32,12 +32,15 @@ module = angular.module("taigaIssues")
## Issue Create Lightbox Directive
#############################################################################
CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading) ->
CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading, $q, attachmentsService) ->
link = ($scope, $el, $attrs) ->
form = $el.find("form").checksley()
$scope.issue = {}
$scope.attachments = Immutable.List()
$scope.$on "issueform:new", (ctx, project)->
attachmentsToAdd = Immutable.List()
$el.find(".tag-input").val("")
lightboxService.open($el)
@ -55,6 +58,21 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading)
$scope.$on "$destroy", ->
$el.off()
createAttachments = (obj) ->
promises = _.map attachmentsToAdd.toJS(), (attachment) ->
return attachmentsService.upload(attachment.file, obj.id, $scope.issue.project, 'issue')
return $q.all(promises)
attachmentsToAdd = Immutable.List()
resetAttachments = () ->
attachmentsToAdd = Immutable.List()
$scope.addAttachment = (attachment) ->
attachmentsToAdd = attachmentsToAdd.push(attachment)
submit = debounce 2000, (event) =>
event.preventDefault()
@ -67,6 +85,9 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading)
promise = $repo.create("issues", $scope.issue)
promise.then (data) ->
return createAttachments(data)
promise.then (data) ->
currentLoading.finish()
$rootscope.$broadcast("issueform:new:success", data)
@ -85,7 +106,7 @@ CreateIssueDirective = ($repo, $confirm, $rootscope, lightboxService, $loading)
return {link:link}
module.directive("tgLbCreateIssue", ["$tgRepo", "$tgConfirm", "$rootScope", "lightboxService", "$tgLoading",
module.directive("tgLbCreateIssue", ["$tgRepo", "$tgConfirm", "$rootScope", "lightboxService", "$tgLoading", "$q", "tgAttachmentsService",
CreateIssueDirective])

View File

@ -416,7 +416,7 @@ module.directive("tgKanbanArchivedStatusIntro", ["$translate", KanbanArchivedSta
## Kanban User Story Directive
#############################################################################
KanbanUserstoryDirective = ($rootscope, $loading, $rs) ->
KanbanUserstoryDirective = ($rootscope, $loading, $rs, $rs2) ->
link = ($scope, $el, $attrs, $model) ->
$el.disableSelection()
@ -440,7 +440,8 @@ KanbanUserstoryDirective = ($rootscope, $loading, $rs) ->
us = $model.$modelValue
$rs.userstories.getByRef(us.project, us.ref).then (editingUserStory) =>
$rootscope.$broadcast("usform:edit", editingUserStory)
$rs2.attachments.list("us", us.id, us.project).then (attachments) =>
$rootscope.$broadcast("usform:edit", editingUserStory, attachments.toJS())
currentLoading.finish()
$scope.getTemplateUrl = () ->
@ -458,7 +459,7 @@ KanbanUserstoryDirective = ($rootscope, $loading, $rs) ->
require: "ngModel"
}
module.directive("tgKanbanUserstory", ["$rootScope", "$tgLoading", "$tgResources", KanbanUserstoryDirective])
module.directive("tgKanbanUserstory", ["$rootScope", "$tgLoading", "$tgResources", "tgResources", KanbanUserstoryDirective])
#############################################################################
## Kanban Squish Column Directive

View File

@ -193,10 +193,8 @@ RelatedTaskCreateFormDirective = ($repo, $compile, $confirm, $tgmodel, $loading,
return {link: link}
module.directive("tgRelatedTaskCreateForm", ["$tgRepo", "$compile", "$tgConfirm", "$tgModel", "$tgLoading", "$tgAnalytics", "$tgTemplate", RelatedTaskCreateFormDirective])
RelatedTaskCreateButtonDirective = ($repo, $compile, $confirm, $tgmodel) ->
template = _.template("""
<a ng-show="!newRelatedTaskFormOpen" class="icon icon-plus related-tasks-buttons ng-animate-disabled"></a>
""")
RelatedTaskCreateButtonDirective = ($repo, $compile, $confirm, $tgmodel, $template) ->
template = $template.get("common/components/add-button.html", true)
link = ($scope, $el, $attrs) ->
$scope.$watch "project", (val) ->
@ -207,14 +205,14 @@ RelatedTaskCreateButtonDirective = ($repo, $compile, $confirm, $tgmodel) ->
else
$el.html("")
$el.on "click", ".icon", (event)->
$el.on "click", ".add-button", (event)->
$scope.$emit("related-tasks:add-new-clicked")
$scope.$on "$destroy", ->
$el.off()
return {link: link}
module.directive("tgRelatedTaskCreateButton", ["$tgRepo", "$compile", "$tgConfirm", "$tgModel", RelatedTaskCreateButtonDirective])
module.directive("tgRelatedTaskCreateButton", ["$tgRepo", "$compile", "$tgConfirm", "$tgModel", "$tgTemplate", RelatedTaskCreateButtonDirective])
RelatedTasksDirective = ($repo, $rs, $rootscope) ->
link = ($scope, $el, $attrs) ->

View File

@ -213,7 +213,6 @@ module.run([
"$tgIssuesResourcesProvider",
"$tgWikiResourcesProvider",
"$tgSearchResourcesProvider",
"$tgAttachmentsResourcesProvider",
"$tgMdRenderResourcesProvider",
"$tgHistoryResourcesProvider",
"$tgKanbanResourcesProvider",

View File

@ -26,10 +26,36 @@ taiga = @.taiga
bindOnce = @.taiga.bindOnce
debounce = @.taiga.debounce
CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxService, $translate) ->
CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxService, $translate, $q, attachmentsService) ->
link = ($scope, $el, attrs) ->
$scope.isNew = true
attachmentsToAdd = Immutable.List()
attachmentsToDelete = Immutable.List()
resetAttachments = () ->
attachmentsToAdd = Immutable.List()
attachmentsToDelete = Immutable.List()
$scope.addAttachment = (attachment) ->
attachmentsToAdd = attachmentsToAdd.push(attachment)
$scope.deleteAttachment = (attachment) ->
attachmentsToDelete = attachmentsToDelete.push(attachment)
createAttachments = (obj) ->
promises = _.map attachmentsToAdd.toJS(), (attachment) ->
attachmentsService.upload(attachment.file, obj.id, $scope.task.project, 'task')
return $q.all(promises)
deleteAttachments = (obj) ->
console.log attachmentsToDelete.toJS()
promises = _.map attachmentsToDelete.toJS(), (attachment) ->
return attachmentsService.delete("task", attachment.id)
return $q.all(promises)
$scope.$on "taskform:new", (ctx, sprintId, usId) ->
$scope.task = {
project: $scope.projectId
@ -41,6 +67,9 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
tags: []
}
$scope.isNew = true
$scope.attachments = Immutable.List()
resetAttachments()
# Update texts for creation
create = $translate.instant("COMMON.CREATE")
@ -52,10 +81,14 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
$el.find(".tag-input").val("")
lightboxService.open($el)
$scope.$on "taskform:edit", (ctx, task) ->
$scope.$on "taskform:edit", (ctx, task, attachments) ->
$scope.task = task
$scope.isNew = false
$scope.attachments = Immutable.fromJS(attachments)
resetAttachments()
# Update texts for edition
save = $translate.instant("COMMON.SAVE")
edit = $translate.instant("LIGHTBOX.CREATE_EDIT_TASK.ACTION_EDIT")
@ -83,6 +116,12 @@ CreateEditTaskDirective = ($repo, $model, $rs, $rootscope, $loading, lightboxSer
promise = $repo.save($scope.task)
broadcastEvent = "taskform:edit:success"
promise.then (data) ->
createAttachments(data)
deleteAttachments(data)
return data
currentLoading = $loading()
.target(submitButton)
.start()
@ -155,7 +194,9 @@ module.directive("tgLbCreateEditTask", [
"$rootScope",
"$tgLoading",
"lightboxService",
"$translate"
"$translate",
"$q",
"tgAttachmentsService",
CreateEditTaskDirective
])

View File

@ -307,7 +307,7 @@ module.directive("tgTaskboard", ["$rootScope", TaskboardDirective])
## Taskboard Task Directive
#############################################################################
TaskboardTaskDirective = ($rootscope, $loading, $rs) ->
TaskboardTaskDirective = ($rootscope, $loading, $rs, $rs2) ->
link = ($scope, $el, $attrs, $model) ->
$el.disableSelection()
@ -330,14 +330,16 @@ TaskboardTaskDirective = ($rootscope, $loading, $rs) ->
.start()
task = $scope.task
$rs.tasks.getByRef(task.project, task.ref).then (editingTask) =>
$rootscope.$broadcast("taskform:edit", editingTask)
$rs2.attachments.list("task", editingTask.id, editingTask.project).then (attachments) =>
$rootscope.$broadcast("taskform:edit", editingTask, attachments.toJS())
currentLoading.finish()
return {link:link}
module.directive("tgTaskboardTask", ["$rootScope", "$tgLoading", "$tgResources", TaskboardTaskDirective])
module.directive("tgTaskboardTask", ["$rootScope", "$tgLoading", "$tgResources", "tgResources", TaskboardTaskDirective])
#############################################################################
## Taskboard Squish Column Directive

View File

@ -195,6 +195,21 @@ _.mixin
delete obj[key]; obj
, obj).value()
isImage = (name) ->
return name.match(/\.(jpe?g|png|gif|gifv|webm)/i) != null
patch = (oldImmutable, newImmutable) ->
pathObj = {}
newImmutable.forEach (newValue, key) ->
if newValue != oldImmutable.get(key)
if newValue.toJS
pathObj[key] = newValue.toJS()
else
pathObj[key] = newValue
return pathObj
taiga = @.taiga
taiga.nl2br = nl2br
taiga.bindMethods = bindMethods
@ -218,3 +233,5 @@ taiga.sizeFormat = sizeFormat
taiga.stripTags = stripTags
taiga.replaceTags = replaceTags
taiga.defineImmutableProperty = defineImmutableProperty
taiga.isImage = isImage
taiga.patch = patch

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -361,6 +361,7 @@
"DEPRECATED": "(deprecated)",
"DEPRECATED_FILE": "Deprecated?",
"ADD": "Add new attachment. {{maxFileSizeMsg}}",
"DROP": "Drop attachments here!",
"MAX_FILE_SIZE": "[Max. size: {{maxFileSize}}]",
"SHOW_DEPRECATED": "+ show deprecated atachments",
"HIDE_DEPRECATED": "- hide deprecated atachments",
@ -371,6 +372,7 @@
"TITLE_LIGHTBOX_DELETE_ATTACHMENT": "Delete attachment...",
"MSG_LIGHTBOX_DELETE_ATTACHMENT": "the attachment '{{fileName}}'",
"ERROR_DELETE_ATTACHMENT": "We have not been able to delete: {{errorMessage}}",
"ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) is too heavy for our Oompa Loompas, try it with a smaller than ({{maxFileSize}})",
"FIELDS": {
"IS_DEPRECATED": "is deprecated"
}

View File

@ -0,0 +1,70 @@
.attachment-gallery {
display: flex;
flex-basis: 25%;
flex-wrap: wrap;
justify-content: flex-start;
margin-top: 1rem;
.single-attachment {
margin-bottom: .5rem;
margin-right: .5rem;
max-width: 200px;
&:hover {
.icon-delete {
opacity: 1;
}
}
.attachment-image {
display: inline-block;
}
img {
height: 150px;
margin-bottom: .2rem;
width: 200px;
&:hover {
filter: saturate(150%) hue-rotate(60deg);
transition: all .3s cubic-bezier(.01, .7, 1, 1);
}
}
&.deprecated {
img {
opacity: .5;
}
.attachment-name {
color: $gray-light;
}
}
.attachment-data {
align-content: center;
display: flex;
justify-content: space-between;
}
.attachment-name {
@extend %light;
@include ellipsis(175px);
display: inline-block;
}
.icon-delete {
color: $red-light;
margin-left: auto;
opacity: 0;
transition: opacity .3s ease-in;
transition-delay: .2s;
&:hover {
color: $red;
}
}
.loading-container {
align-items: center;
display: flex;
height: 150px;
justify-content: center;
margin: 0 .5rem .5rem 0;
width: 200px;
}
.loading-spinner {
margin: 0 auto;
max-height: 3rem;
max-width: 3rem;
}
}
}

View File

@ -0,0 +1,103 @@
.attachment-list {
.single-attachment {
align-items: center;
border-bottom: 1px solid $whitish;
display: flex;
padding: .5rem 0 .5rem .5rem;
position: relative;
&:hover {
.settings {
opacity: 1;
transition: opacity .2s ease-in;
}
}
&.deprecated {
color: $gray-light;
.attachment-name a {
color: $gray-light;
}
}
}
.attachment-name {
@include ellipsis(90%);
flex-basis: 25%;
flex-grow: 1;
flex-shrink: 0;
padding-right: 1rem;
}
.attachment-comments,
.editable-attachment-comment {
flex: 2;
flex-basis: 50%;
margin-right: .5rem;
span {
color: $gray;
}
}
.attachment-size {
flex-basis: 125px;
flex-grow: 0;
flex-shrink: 0;
}
.attachment-settings {
align-items: center;
display: flex;
flex-basis: 10%;
flex-grow: 0;
flex-shrink: 0;
justify-content: space-around;
margin-left: auto;
.settings,
.editable-settings {
@extend %large;
color: $gray-light;
&:hover {
color: $primary;
}
}
.settings {
opacity: 0;
}
.editable-settings {
opacity: 1;
}
.icon-delete {
&:hover {
color: $red;
}
}
.icon-drag-v {
cursor: move;
}
}
.icon-delete {
@extend %large;
color: $gray-light;
&:hover {
color: $red;
}
}
.editable-attachment-deprecated {
display: flex;
padding-left: 1rem;
span {
color: $gray-light;
}
input {
margin-right: .2rem;
vertical-align: middle;
&:checked+span {
color: $grayer;
}
}
}
.percentage {
background: rgba($primary, .1);
bottom: 0;
height: 40px;
left: 0;
position: absolute;
top: 0;
width: 45%;
}
}

View File

@ -0,0 +1,120 @@
.attachments {
margin-bottom: 2rem;
}
.attachments-header {
align-content: space-between;
align-items: center;
background: $whitish;
display: flex;
justify-content: space-between;
.attachments-title {
@extend %medium;
@extend %bold;
color: $grayer;
padding: 0 1rem;
}
.options {
display: flex;
}
label {
cursor: pointer;
margin-left: 1rem;
&.add-attachment-button {
background: $gray;
border: 0;
display: inline-block;
padding: .5rem;
transition: background .25s;
&:hover {
background: $primary-light;
}
}
svg {
fill: $white;
height: 1.25rem;
margin-bottom: -.2rem;
width: 1.25rem;
}
}
button {
margin-right: .2rem;
&:hover,
&.is-active {
svg {
fill: $primary-light;
}
}
svg {
fill: $gray-light;
height: 1.6rem;
margin-bottom: -.2rem;
width: 1.6rem;
}
}
input {
display: none;
}
}
.attachments-empty {
@extend %large;
@extend %bold;
border: 3px dashed $whitish;
color: $gray-light;
margin-top: .5rem;
padding: 1rem;
text-align: center;
}
.single-attachment {
@extend %small;
&.ui-sortable-helper {
background: lighten($primary, 60%);
box-shadow: 1px 1px 10px rgba($black, .1);
transition: background .2s ease-in;
}
&.sortable-placeholder {
background: $whitish;
height: 40px;
}
.attachment-name {
@extend %bold;
padding-right: 1rem;
.icon {
margin-right: .25rem;
}
svg {
height: 1.2rem;
width: 1.2rem;
}
}
.attachment-size {
color: $gray-light;
}
}
.more-attachments {
@extend %small;
border-bottom: 1px solid $gray-light;
display: block;
padding: 1rem 0 1rem 1rem;
span {
color: $gray-light;
}
.more-attachments-num {
color: $primary;
margin-left: .5rem;
}
&:hover {
background: lighten($primary, 60%);
transition: background .2s ease-in;
}
}
.attachment-preview {
img {
max-height: 80vh;
max-width: 80vw;
}
}

View File

@ -0,0 +1,45 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachment-link.directive.coffee
###
AttachmentLinkDirective = ($parse, lightboxFactory) ->
link = (scope, el, attrs) ->
attachment = $parse(attrs.tgAttachmentLink)(scope)
el.on "click", (event) ->
if taiga.isImage(attachment.getIn(['file', 'name']))
event.preventDefault()
scope.$apply ->
lightboxFactory.create('tg-lb-attachment-preview', {
class: 'lightbox lightbox-block'
}, {
file: attachment.get('file')
})
scope.$on "$destroy", -> el.off()
return {
link: link
}
AttachmentLinkDirective.$inject = [
"$parse",
"tgLightboxFactory"
]
angular.module("taigaComponents").directive("tgAttachmentLink", AttachmentLinkDirective)

View File

@ -0,0 +1,39 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachment-gallery.directive.coffee
###
AttachmentGalleryDirective = () ->
link = (scope, el, attrs, ctrl) ->
return {
scope: {},
bindToController: {
attachment: "=",
onDelete: "&",
onUpdate: "&",
type: "="
},
controller: "Attachment",
controllerAs: "vm",
templateUrl: "components/attachment/attachment-gallery.html",
link: link
}
AttachmentGalleryDirective.$inject = []
angular.module("taigaComponents").directive("tgAttachmentGallery", AttachmentGalleryDirective)

View File

@ -0,0 +1,36 @@
.single-attachment(
ng-class="{deprecated: vm.attachment.getIn(['file', 'is_deprecated'])}",
ng-if="vm.attachment.getIn(['file', 'id'])",
)
a.attachment-image(
tg-attachment-link="vm.attachment"
href="{{::vm.attachment.getIn(['file', 'url'])}}"
title="{{::vm.attachment.getIn(['file', 'name'])}}"
target="_blank"
download="{{::vm.attachment.getIn(['file', 'name'])}}"
)
img(
alt="{{::vm.attachment.getIn(['file', 'name'])}}"
ng-src="{{::vm.attachment.getIn(['file', 'thumbnail_card_url'])}}"
ng-if="vm.attachment.getIn(['file', 'thumbnail_card_url'])"
)
img(
alt="{{::vm.attachment.getIn(['file', 'name'])}}"
src="/#{v}/images/attachment-gallery.png"
ng-if="!vm.attachment.getIn(['file', 'thumbnail_card_url'])"
)
.attachment-data
a.attachment-name(
tg-attachment-link="vm.attachment"
href="{{::vm.attachment.getIn(['file', 'url'])}}"
title="{{::vm.attachment.get(['file', 'name'])}}"
target="_blank"
download="{{::vm.attachment.getIn(['file', 'name'])}}"
)
span {{::vm.attachment.getIn(['file', 'name'])}}
a.icon-delete(
href=""
title="{{'COMMON.DELETE' | translate}}"
ng-click="vm.delete()"
)

View File

@ -0,0 +1,59 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attchment.controller.coffee
###
class AttachmentController
@.$inject = [
'tgAttachmentsService',
'$translate'
]
constructor: (@attachmentsService, @translate) ->
@.form = {}
@.form.description = @.attachment.getIn(['file', 'description'])
@.form.is_deprecated = @.attachment.get(['file', 'is_deprecated'])
@.loading = false
@.title = @translate.instant("ATTACHMENT.TITLE", {
fileName: @.attachment.get('name'),
date: moment(@.attachment.get('created_date')).format(@translate.instant("ATTACHMENT.DATE"))
})
editMode: (mode) ->
@.attachment = @.attachment.set('editable', mode)
delete: () ->
@.onDelete({attachment: @.attachment})
save: () ->
@.attachment = @.attachment.set('loading', true)
attachment = @.attachment.merge({
editable: false,
loading: false
})
attachment = attachment.mergeIn(['file'], {
description: @.form.description,
is_deprecated: !!@.form.is_deprecated
})
@.onUpdate({attachment: attachment})
angular.module('taigaComponents').controller('Attachment', AttachmentController)

View File

@ -0,0 +1,140 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attchment.controller.spec.coffee
###
describe "AttachmentController", ->
$provide = null
$controller = null
scope = null
mocks = {}
_mockAttachmentsService = ->
mocks.attachmentsService = {}
$provide.value("tgAttachmentsService", mocks.attachmentsService)
_mockTranslate = ->
mocks.translate = {
instant: sinon.stub()
}
$provide.value("$translate", mocks.translate)
_mocks = ->
module (_$provide_) ->
$provide = _$provide_
_mockAttachmentsService()
_mockTranslate()
return null
_inject = ->
inject (_$controller_, $rootScope) ->
$controller = _$controller_
scope = $rootScope.$new()
_setup = ->
_mocks()
_inject()
beforeEach ->
module "taigaComponents"
_setup()
it "change edit mode", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
}
})
ctrl = $controller("Attachment", {
$scope: scope
}, {
attachment : attachment
})
ctrl.editable = false
ctrl.editMode(true)
expect(ctrl.attachment.get('editable')).to.be.true
it "delete", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
}
})
ctrl = $controller("Attachment", {
$scope: scope
}, {
attachment : attachment
})
ctrl.onDelete = sinon.spy()
onDelete = sinon.match (value) ->
return value.attachment == attachment
, "onDelete"
ctrl.delete()
expect(ctrl.onDelete).to.be.calledWith(onDelete)
it "save", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
},
loading: false,
editable: false
})
ctrl = $controller("Attachment", {
$scope: scope
}, {
attachment : attachment
})
ctrl.onUpdate = sinon.spy()
onUpdate = sinon.match (value) ->
value = value.attachment.toJS()
return (
value.file.description == "ok" &&
value.file.is_deprecated
)
, "onUpdate"
ctrl.form = {
description: "ok"
is_deprecated: true
}
ctrl.save()
attachment = ctrl.attachment.toJS()
expect(ctrl.onUpdate).to.be.calledWith(onUpdate)

View File

@ -0,0 +1,39 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachment.directive.coffee
###
AttachmentDirective = () ->
link = (scope, el, attrs, ctrl) ->
return {
scope: {},
bindToController: {
attachment: "=",
onDelete: "&",
onUpdate: "&",
type: "="
},
controller: "Attachment",
controllerAs: "vm",
templateUrl: "components/attachment/attachment.html",
link: link
}
AttachmentDirective.$inject = []
angular.module("taigaComponents").directive("tgAttachment", AttachmentDirective)

View File

@ -0,0 +1,80 @@
form.single-attachment(
ng-class="{deprecated: vm.attachment.getIn(['file', 'is_deprecated'])}",
ng-if="vm.attachment.getIn(['file', 'id'])",
ng-submit="vm.save()"
)
.attachment-name
a(
tg-attachment-link="vm.attachment"
href="{{::vm.attachment.getIn(['file', 'url'])}}"
title="{{::vm.attachment.get(['file', 'name'])}}"
target="_blank"
download="{{::vm.attachment.getIn(['file', 'name'])}}"
)
span.icon
include ../../../svg/attachment.svg
span {{::vm.attachment.getIn(['file', 'name'])}}
.attachment-comments(ng-if="!vm.attachment.get('editable') && vm.attachment.getIn(['file', 'description'])")
span.deprecated-file(ng-if="vm.attachment.getIn(['file', 'is_deprecated'])") {{'ATTACHMENT.DEPRECATED' | translate}}
span {{vm.attachment.getIn(['file', 'description'])}}
.attachment-size
span {{::vm.attachment.getIn(['file', 'size']) | sizeFormat}}
.editable.editable-attachment-comment(ng-if="vm.attachment.get('editable')")
input(
type="text",
name="description",
maxlength="140",
ng-model="vm.form.description",
tg-auto-select,
ng-keydown="$event.which === 27 && vm.editMode(false)"
placeholder="{{'ATTACHMENT.DESCRIPTION' | translate}}"
)
.editable.editable-attachment-deprecated(ng-if="vm.attachment.get('editable')")
input(
type="checkbox"
ng-model="vm.form.is_deprecated"
name="is-deprecated"
id="attach-{{::vm.attachment.getIn(['file', 'id'])}}-is-deprecated"
)
label.is_deprecated(
for="attach-{{::vm.attachment.getIn(['file', 'id'])}}-is-deprecated"
translate="{{'ATTACHMENT.DEPRECATED_FILE' | translate}}")
.attachment-settings(ng-if="vm.attachment.get('editable')")
div(tg-loading="vm.attachment.get('loading')")
a.editable-settings.icon.icon-floppy(
href=""
title="{{'COMMON.SAVE' | translate}}"
ng-click="vm.save()"
)
div
a.editable-settings.icon.icon-delete(
href=""
title="{{'COMMON.CANCEL' | translate}}"
ng-click="vm.editMode(false)"
)
.attachment-settings(
ng-if="!vm.attachment.get('editable')"
tg-check-permission="modify_{{vm.type}}"
)
a.settings.icon.icon-edit(
href=""
title="{{'COMMON.EDIT' | translate}}"
ng-click="vm.editMode(true)"
)
a.settings.icon.icon-delete(
href=""
title="{{'COMMON.DELETE' | translate}}"
ng-click="vm.delete()"
)
a.settings.icon.icon-drag-v(
href=""
title="{{'COMMON.DRAG' | translate}}"
)

View File

@ -0,0 +1,46 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachments-drop.directive.coffee
###
AttachmentsDropDirective = ($parse) ->
link = (scope, el, attrs) ->
eventAttr = $parse(attrs.tgAttachmentsDrop)
el.on 'dragover', (e) ->
e.preventDefault()
return false
el.on 'drop', (e) ->
e.stopPropagation()
e.preventDefault()
dataTransfer = e.dataTransfer || (e.originalEvent && e.originalEvent.dataTransfer);
scope.$apply () -> eventAttr(scope, {files: dataTransfer.files})
scope.$on "$destroy", -> el.off()
return {
link: link
}
AttachmentsDropDirective.$inject = [
"$parse"
]
angular.module("taigaComponents").directive("tgAttachmentsDrop", AttachmentsDropDirective)

View File

@ -0,0 +1,150 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attchments-full.controller.coffee
###
sizeFormat = @.taiga.sizeFormat
class AttachmentsFullController
@.$inject = [
"tgAttachmentsService",
"$rootScope",
"$translate",
"$tgConfirm",
"$tgConfig",
"$tgStorage"
]
constructor: (@attachmentsService, @rootScope, @translate, @confirm, @config, @storage) ->
@.deprecatedsVisible = false
@.uploadingAttachments = []
@.mode = @storage.get('attachment-mode', 'list')
@.maxFileSize = @config.get("maxUploadFileSize", null)
@.maxFileSize = sizeFormat(@.maxFileSize) if @.maxFileSize
@.maxFileSizeMsg = if @.maxFileSize then @translate.instant("ATTACHMENT.MAX_UPLOAD_SIZE", {maxFileSize: @.maxFileSize}) else ""
loadAttachments: ->
@attachmentsService.list(@.type, @.objId, @.projectId).then (files) =>
@.attachments = files.map (file) ->
attachment = Immutable.Map()
return attachment.merge({
loading: false,
editable: false,
file: file
})
@.generate()
setMode: (mode) ->
@.mode = mode
@storage.set('attachment-mode', mode)
generate: () ->
@.deprecatedsCount = @.attachments.count (it) -> it.getIn(['file', 'is_deprecated'])
if @.deprecatedsVisible
@.attachmentsVisible = @.attachments
else
@.attachmentsVisible = @.attachments.filter (it) -> !it.getIn(['file', 'is_deprecated'])
toggleDeprecatedsVisible: () ->
@.deprecatedsVisible = !@.deprecatedsVisible
@.generate()
addAttachment: (file) ->
return new Promise (resolve, reject) =>
if @attachmentsService.validate(file)
@.uploadingAttachments.push(file)
promise = @attachmentsService.upload(file, @.objId, @.projectId, @.type)
promise.then (file) =>
@.uploadingAttachments = @.uploadingAttachments.filter (uploading) ->
return uploading.name != file.get('name')
attachment = Immutable.Map()
attachment = attachment.merge({
file: file,
editable: true,
loading: false
})
@.attachments = @.attachments.push(attachment)
@.generate()
@rootScope.$broadcast("attachment:create")
resolve(@.attachments)
else
reject(file)
addAttachments: (files) ->
_.forEach files, @.addAttachment.bind(this)
deleteAttachment: (toDeleteAttachment) ->
title = @translate.instant("ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT")
message = @translate.instant("ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT", {
fileName: toDeleteAttachment.getIn(['file', 'name'])
})
return @confirm.askOnDelete(title, message)
.then (askResponse) =>
onError = () =>
message = @translate.instant("ATTACHMENT.ERROR_DELETE_ATTACHMENT", {errorMessage: message})
@confirm.notify("error", null, message)
askResponse.finish(false)
onSuccess = () =>
@.attachments = @.attachments.filter (attachment) -> attachment != toDeleteAttachment
@.generate()
askResponse.finish()
return @attachmentsService.delete(@.type, toDeleteAttachment.getIn(['file', 'id'])).then(onSuccess, onError)
reorderAttachment: (attachment, newIndex) ->
oldIndex = @.attachments.findIndex (it) -> it == attachment
return if oldIndex == newIndex
attachments = @.attachments.remove(oldIndex)
attachments = attachments.splice(newIndex, 0, attachment)
attachments = attachments.map (x, i) -> x.setIn(['file', 'order'], i + 1)
promises = attachments.map (attachment) =>
patch = {order: attachment.getIn(['file', 'order'])}
return @attachmentsService.patch(attachment.getIn(['file', 'id']), @.type, patch)
return Promise.all(promises.toJS()).then () =>
@.attachments = attachments
@.generate()
updateAttachment: (toUpdateAttachment) ->
index = @.attachments.findIndex (attachment) ->
return attachment.getIn(['file', 'id']) == toUpdateAttachment.getIn(['file', 'id'])
oldAttachment = @.attachments.get(index)
patch = taiga.patch(oldAttachment.get('file'), toUpdateAttachment.get('file'))
return @attachmentsService.patch(toUpdateAttachment.getIn(['file', 'id']), @.type, patch).then () =>
@.attachments = @.attachments.set(index, toUpdateAttachment)
@.generate()
angular.module("taigaComponents").controller("AttachmentsFull", AttachmentsFullController)

View File

@ -0,0 +1,328 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attchments.controller.spec.coffee
###
describe "AttachmentsController", ->
$provide = null
$controller = null
mocks = {}
_mockAttachmentsService = ->
mocks.attachmentsService = {
upload: sinon.stub()
}
$provide.value("tgAttachmentsService", mocks.attachmentsService)
_mockConfirm = ->
mocks.confirm = {}
$provide.value("$tgConfirm", mocks.confirm)
_mockTranslate = ->
mocks.translate = {
instant: sinon.stub()
}
$provide.value("$translate", mocks.translate)
_mockConfig = ->
mocks.config = {
get: sinon.stub()
}
$provide.value("$tgConfig", mocks.config)
_mockStorage = ->
mocks.storage = {
get: sinon.stub()
}
$provide.value("$tgStorage", mocks.storage)
_mockRootScope = ->
mocks.rootScope = {}
$provide.value("$rootScope", mocks.rootScope)
_mocks = ->
module (_$provide_) ->
$provide = _$provide_
_mockAttachmentsService()
_mockConfirm()
_mockTranslate()
_mockConfig()
_mockStorage()
_mockRootScope()
return null
_inject = ->
inject (_$controller_) ->
$controller = _$controller_
_setup = ->
_mocks()
_inject()
beforeEach ->
module "taigaComponents"
_setup()
it "generate, refresh deprecated counter", () ->
attachments = Immutable.fromJS([
{
file: {
is_deprecated: false
}
},
{
file: {
is_deprecated: true
}
},
{
file: {
is_deprecated: true
}
},
{
file: {
is_deprecated: false
}
},
{
file: {
is_deprecated: true
}
}
])
ctrl = $controller("AttachmentsFull")
ctrl.attachments = attachments
ctrl.generate()
expect(ctrl.deprecatedsCount).to.be.equal(3)
it "toggle deprecated visibility", () ->
ctrl = $controller("AttachmentsFull")
ctrl.deprecatedsVisible = false
ctrl.generate = sinon.spy()
ctrl.toggleDeprecatedsVisible()
expect(ctrl.deprecatedsVisible).to.be.true
expect(ctrl.generate).to.be.calledOnce
describe "add attachments", () ->
it "valid attachment", (done) ->
file = Immutable.fromJS({
file: {},
name: 'test',
size: 3000
})
mocks.attachmentsService.validate = sinon.stub()
mocks.attachmentsService.validate.withArgs(file).returns(true)
mocks.attachmentsService.upload = sinon.stub()
mocks.attachmentsService.upload.promise().resolve(file)
mocks.rootScope.$broadcast = sinon.spy()
ctrl = $controller("AttachmentsFull")
ctrl.attachments = Immutable.List()
ctrl.addAttachment(file).then () ->
expect(mocks.rootScope.$broadcast).have.been.calledWith('attachment:create')
expect(ctrl.attachments.count()).to.be.equal(1)
done()
it "invalid attachment", () ->
file = Immutable.fromJS({
file: {},
name: 'test',
size: 3000
})
mocks.attachmentsService.validate = sinon.stub()
mocks.attachmentsService.validate.withArgs(file).returns(false)
mocks.attachmentsService.upload = sinon.stub()
mocks.attachmentsService.upload.promise().resolve(file)
mocks.rootScope.$broadcast = sinon.spy()
ctrl = $controller("AttachmentsFull")
ctrl.attachments = Immutable.List()
ctrl.addAttachment(file).then null, () ->
expect(ctrl.attachments.count()).to.be.equal(0)
it "add attachments", () ->
ctrl = $controller("AttachmentsFull")
ctrl.attachments = Immutable.List()
ctrl.addAttachment = sinon.spy()
files = [
{},
{},
{}
]
ctrl.addAttachments(files)
expect(ctrl.addAttachment).to.have.callCount(3)
describe "deleteattachments", () ->
it "success attachment", (done) ->
askResponse = {
finish: sinon.spy()
}
mocks.translate.instant.withArgs('ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT').returns('title')
mocks.translate.instant.withArgs('ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT').returns('message')
mocks.confirm.askOnDelete = sinon.stub()
mocks.confirm.askOnDelete.withArgs('title', 'message').promise().resolve(askResponse)
mocks.attachmentsService.delete = sinon.stub()
mocks.attachmentsService.delete.withArgs('us', 2).promise().resolve()
ctrl = $controller("AttachmentsFull")
ctrl.type = 'us'
ctrl.generate = sinon.spy()
ctrl.attachments = Immutable.fromJS([
{
file: {id: 1}
},
{
file: {id: 2}
},
{
file: {id: 3}
},
{
file: {id: 4}
}
])
deleteFile = ctrl.attachments.get(1)
ctrl.deleteAttachment(deleteFile).then () ->
expect(ctrl.generate).have.been.calledOnce
expect(ctrl.attachments.size).to.be.equal(3)
expect(askResponse.finish).have.been.calledOnce
done()
it "error attachment", (done) ->
askResponse = {
finish: sinon.spy()
}
mocks.translate.instant.withArgs('ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT').returns('title')
mocks.translate.instant.withArgs('ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT').returns('message')
mocks.translate.instant.withArgs('ATTACHMENT.ERROR_DELETE_ATTACHMENT').returns('error')
mocks.confirm.askOnDelete = sinon.stub()
mocks.confirm.askOnDelete.withArgs('title', 'message').promise().resolve(askResponse)
mocks.confirm.notify = sinon.spy()
mocks.attachmentsService.delete = sinon.stub()
mocks.attachmentsService.delete.promise().reject()
ctrl = $controller("AttachmentsFull")
ctrl.type = 'us'
ctrl.generate = sinon.spy()
ctrl.attachments = Immutable.fromJS([
{
file: {id: 1}
},
{
file: {id: 2}
},
{
file: {id: 3}
},
{
file: {id: 4}
}
])
deleteFile = ctrl.attachments.get(1)
ctrl.deleteAttachment(deleteFile).then () ->
expect(ctrl.attachments.size).to.be.equal(4)
expect(askResponse.finish.withArgs(false)).have.been.calledOnce
expect(mocks.confirm.notify.withArgs('error', null, 'error'))
done()
it "reorder attachments", (done) ->
attachments = Immutable.fromJS([
{file: {id: 0, is_deprecated: false, order: 0}},
{file: {id: 1, is_deprecated: true, order: 1}},
{file: {id: 2, is_deprecated: true, order: 2}},
{file: {id: 3, is_deprecated: false, order: 3}},
{file: {id: 4, is_deprecated: true, order: 4}}
])
mocks.attachmentsService.patch = sinon.stub()
mocks.attachmentsService.patch.promise().resolve()
ctrl = $controller("AttachmentsFull")
ctrl.type = 'us'
ctrl.attachments = attachments
ctrl.reorderAttachment(attachments.get(1), 0).then () ->
expect(ctrl.attachments.get(0)).to.be.equal(attachments.get(1))
done()
it "update attachment", () ->
attachments = Immutable.fromJS([
{file: {id: 0, is_deprecated: false, order: 0}},
{file: {id: 1, is_deprecated: true, order: 1}},
{file: {id: 2, is_deprecated: true, order: 2}},
{file: {id: 3, is_deprecated: false, order: 3}},
{file: {id: 4, is_deprecated: true, order: 4}}
])
attachment = attachments.get(1)
attachment = attachment.setIn(['file', 'is_deprecated'], false)
mocks.attachmentsService.patch = sinon.stub()
mocks.attachmentsService.patch.withArgs(1, 'us', {is_deprecated: false}).promise().resolve()
ctrl = $controller("AttachmentsFull")
ctrl.type = 'us'
ctrl.attachments = attachments
ctrl.updateAttachment(attachment).then () ->
expect(ctrl.attachments.get(1).toJS()).to.be.eql(attachment.toJS())

View File

@ -0,0 +1,42 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attchments-full.directive.coffee
###
bindOnce = @.taiga.bindOnce
AttachmentsFullDirective = () ->
link = (scope, el, attrs, ctrl) ->
bindOnce scope, 'vm.objId', (value) ->
ctrl.loadAttachments()
return {
scope: {},
bindToController: {
type: "@",
objId: "="
projectId: "="
},
controller: "AttachmentsFull",
controllerAs: "vm",
templateUrl: "components/attachments-full/attachments-full.html",
link: link
}
AttachmentsFullDirective.$inject = []
angular.module("taigaComponents").directive("tgAttachmentsFull", AttachmentsFullDirective)

View File

@ -0,0 +1,97 @@
section.attachments(tg-attachments-drop="vm.addAttachments(files)")
.attachments-header
h3.attachments-title #[span.attachments-num {{vm.attachments.size}}] #[span.attachments-text(translate="ATTACHMENT.SECTION_NAME")]
.options
button.view-gallery(
ng-class="{'is-active': vm.mode == 'gallery'}"
ng-if="vm.attachments.size",
ng-click="vm.setMode('gallery')"
)
include ../../../svg/gallery.svg
button.view-list(
ng-class="{'is-active': vm.mode == 'list'}"
ng-if="vm.attachments.size",
ng-click="vm.setMode('list')"
)
include ../../../svg/list.svg
.add-attach(
tg-check-permission="modify_{{vm.type}}"
title!="{{'ATTACHMENT.ADD' | translate}}"
)
span.size-info(
ng-if="vm.maxFileSize",
translate="ATTACHMENT.MAX_FILE_SIZE",
translate-values="{ 'maxFileSize': vm.maxFileSize}"
)
label.add-attachment-button(for="add-attach")
include ../../../svg/add.svg
input(
id="add-attach",
type="file",
ng-model="files",
multiple="multiple",
tg-file-change="vm.addAttachments(files)"
)
.attachments-empty(ng-if="!vm.attachments.size")
div {{'ATTACHMENT.DROP' | translate}}
.attachment-list.sortable(ng-if="vm.mode == 'list'")
div(tg-attachments-sortable="vm.reorderAttachment(attachment, index)")
div(
tg-repeat="attachment in vm.attachmentsVisible track by attachment.getIn(['file', 'id'])",
tg-bind-scope
)
tg-attachment(
attachment="attachment",
on-delete="vm.deleteAttachment(attachment)",
on-update="vm.updateAttachment(attachment)",
type="vm.type"
)
.single-attachment(ng-repeat="file in vm.uploadingAttachments")
.attachment-name
span.icon
include ../../../svg/attachment.svg
span {{attachment.get('name')}}
.attachment-size
span {{attachment.get('size') | sizeFormat}}
.attachment-comments
span {{file.progressMessage}}
.percentage(ng-style="{'width': file.progressPercent}")
a.more-attachments(
href="",
title="{{'ATTACHMENT.SHOW_DEPRECATED' | translate}}",
ng-if="vm.deprecatedsCount > 0",
ng-click="vm.toggleDeprecatedsVisible()"
)
span.text(
ng-show="!vm.deprecatedsVisible",
translate="ATTACHMENT.SHOW_DEPRECATED"
)
span.text(
ng-show="vm.deprecatedsVisible",
translate="ATTACHMENT.HIDE_DEPRECATED"
)
span.more-attachments-num(
translate="ATTACHMENT.COUNT_DEPRECATED",
translate-values="{counter: '{{vm.deprecatedsCount}}'}"
)
.attachment-gallery(ng-if="vm.mode == 'gallery'")
tg-attachment-gallery.attachment-gallery-container(
tg-repeat="attachment in vm.attachmentsVisible track by attachment.getIn(['file', 'id'])"
attachment="attachment",
on-delete="vm.deleteAttachment(attachment)",
on-update="vm.updateAttachment(attachment)",
type="vm.type"
)
.single-attachment(ng-repeat="file in vm.uploadingAttachments")
.loading-container
img.loading-spinner(
src="/#{v}/svg/spinner-circle.svg",
alt="{{'COMMON.LOADING' | translate}}"
)
.attachment-data {{file.progressMessage}}

View File

@ -0,0 +1,47 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attchments-simple.controller.coffee
###
class AttachmentsSimpleController
@.$inject = [
"tgAttachmentsService"
]
constructor: (@attachmentsService) ->
addAttachment: (file) ->
attachment = Immutable.fromJS({
file: file,
name: file.name,
size: file.size
})
if @attachmentsService.validate(file)
@.attachments = @.attachments.push(attachment)
@.onAdd({attachment: attachment}) if @.onAdd
addAttachments: (files) ->
_.forEach files, @.addAttachment.bind(this)
deleteAttachment: (toDeleteAttachment) ->
@.attachments = @.attachments.filter (attachment) -> attachment != toDeleteAttachment
@.onDelete({attachment: toDeleteAttachment}) if @.onDelete
angular.module("taigaComponents").controller("AttachmentsSimple", AttachmentsSimpleController)

View File

@ -0,0 +1,96 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attchment.controller.spec.coffee
###
describe "AttachmentsSimple", ->
$provide = null
$controller = null
mocks = {}
scope = null
_mockAttachmentsService = ->
mocks.attachmentsService = {}
$provide.value("tgAttachmentsService", mocks.attachmentsService)
_mocks = ->
module (_$provide_) ->
$provide = _$provide_
_mockAttachmentsService()
return null
_inject = ->
inject (_$controller_, $rootScope) ->
$controller = _$controller_
scope = $rootScope.$new()
_setup = ->
_mocks()
_inject()
beforeEach ->
module "taigaComponents"
_setup()
it "add attachment", () ->
file = {
name: 'name',
size: 1000
}
mocks.attachmentsService.validate = sinon.stub()
mocks.attachmentsService.validate.withArgs(file).returns(true)
ctrl = $controller("AttachmentsSimple", {
$scope: scope
}, {
attachments: Immutable.List()
})
ctrl.onAdd = sinon.spy()
ctrl.addAttachment(file)
expect(ctrl.attachments.size).to.be.equal(1)
expect(ctrl.onAdd).to.have.been.calledOnce
it "delete attachment", () ->
attachments = Immutable.fromJS([
{id: 1},
{id: 2},
{id: 3}
])
ctrl = $controller("AttachmentsSimple", {
$scope: scope
}, {
attachments: attachments
})
ctrl.onDelete = sinon.spy()
attachment = attachments.get(1)
ctrl.deleteAttachment(attachment)
expect(ctrl.attachments.size).to.be.equal(2)
expect(ctrl.onDelete.withArgs({attachment: attachment})).to.have.been.calledOnce

View File

@ -0,0 +1,38 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attchments-simple.directive.coffee
###
AttachmentsSimpleDirective = () ->
link = (scope, el, attrs, ctrl) ->
return {
scope: {},
bindToController: {
attachments: "=",
onAdd: "&",
onDelete: "&"
},
controller: "AttachmentsSimple",
controllerAs: "vm",
templateUrl: "components/attachments-simple/attachments-simple.html",
link: link
}
AttachmentsSimpleDirective.$inject = []
angular.module("taigaComponents").directive("tgAttachmentsSimple", AttachmentsSimpleDirective)

View File

@ -0,0 +1,38 @@
//- section.attachments(tg-attachments-drop="vm.addAttachments(files)")
section.attachments(tg-attachments-drop="vm.addAttachments(files)")
.attachments-header
h3.attachments-title #[span.attachments-num {{vm.attachments.size}}] #[span.attachments-text(translate="ATTACHMENT.SECTION_NAME")]
.add-attach(title!="{{'ATTACHMENT.ADD' | translate}}")
span.size-info(
translate="ATTACHMENT.MAX_FILE_SIZE"
translate-values="{ 'maxFileSize': vm.maxFileSizeFormated}"
ng-if="vm.maxFileSize"
)
label.add-attachment-button(for="add-attach")
include ../../../svg/add.svg
input(
id="add-attach"
type="file"
multiple="multiple"
ng-model="files"
tg-file-change="vm.addAttachments(files)"
)
.attachments-empty(ng-if="!vm.attachments.size")
div {{'ATTACHMENT.DROP' | translate}}
.attachment-body.attachment-list
.single-attachment(tg-repeat="attachment in vm.attachments track by $index")
.attachment-name
span.icon
include ../../../svg/attachment.svg
span {{attachment.get('name')}}
.attachment-size
span {{attachment.get('size') | sizeFormat}}
.attachment-settings
a.settings.attachment-delete(
href="#"
title="{{'COMMON.DELETE' | translate}}"
ng-click="vm.deleteAttachment(attachment)"
)
include ../../../svg/remove.svg

View File

@ -0,0 +1,52 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachments-sortable.directive.coffee
###
AttachmentSortableDirective = ($parse) ->
link = (scope, el, attrs) ->
callback = $parse(attrs.tgAttachmentsSortable)
el.sortable({
items: "div[tg-bind-scope]"
handle: "a.settings.icon.icon-drag-v"
containment: ".attachments"
dropOnEmpty: true
helper: 'clone'
scroll: false
tolerance: "pointer"
placeholder: "sortable-placeholder single-attachment"
})
el.on "sortstop", (event, ui) ->
attachment = ui.item.scope().attachment
newIndex = ui.item.index()
scope.$apply () ->
callback(scope, {attachment: attachment, index: newIndex})
scope.$on "$destroy", -> el.off()
return {
link: link
}
AttachmentSortableDirective.$inject = [
"$parse"
]
angular.module("taigaComponents").directive("tgAttachmentsSortable", AttachmentSortableDirective)

View File

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

View File

@ -0,0 +1,39 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: file-change.directive.coffee
###
FileChangeDirective = ($parse) ->
link = (scope, el, attrs, ctrl) ->
eventAttr = $parse(attrs.tgFileChange)
el.on 'change', (event) ->
scope.$apply () -> eventAttr(scope, {files: event.currentTarget.files})
scope.$on "$destroy", -> el.off()
return {
require: "ngModel",
restrict: "A",
link: link
}
FileChangeDirective.$inject = [
"$parse"
]
angular.module("taigaComponents").directive("tgFileChange", FileChangeDirective)

View File

@ -5,6 +5,7 @@
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
# Copyright (C) 2014-2016 Xavi Julian <xavier.julian@kaleidos.net>
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -19,29 +20,57 @@
# 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/resources/attachments.coffee
# File: attachments-resource.service.coffee
###
taiga = @.taiga
sizeFormat = @.taiga.sizeFormat
resourceProvider = ($rootScope, $config, $urls, $model, $repo, $auth, $q) ->
Resource = (urlsService, http, config, $rootScope, $q, storage) ->
service = {}
service.list = (urlName, objectId, projectId) ->
params = {object_id: objectId, project: projectId}
return $repo.queryMany(urlName, params)
service.list = (type, objectId, projectId) ->
urlname = "attachments/#{type}"
params = {object_id: objectId, project: projectId}
httpOptions = {
headers: {
"x-disable-pagination": "1"
}
}
url = urlsService.resolve(urlname)
return http.get(url, params, httpOptions)
.then (result) -> Immutable.fromJS(result.data)
service.delete = (type, id) ->
urlname = "attachments/#{type}"
url = urlsService.resolve(urlname) + "/#{id}"
return http.delete(url)
service.patch = (type, id, patch) ->
urlname = "attachments/#{type}"
url = urlsService.resolve(urlname) + "/#{id}"
return http.patch(url, patch)
service.create = (type, projectId, objectId, file) ->
urlname = "attachments/#{type}"
url = urlsService.resolve(urlname)
service.create = (urlName, projectId, objectId, file) ->
defered = $q.defer()
if file is undefined
defered.reject(null)
return defered.promise
maxFileSize = $config.get("maxUploadFileSize", null)
maxFileSize = config.get("maxUploadFileSize", null)
if maxFileSize and file.size > maxFileSize
response = {
status: 413,
@ -64,13 +93,13 @@ resourceProvider = ($rootScope, $config, $urls, $model, $repo, $auth, $q) ->
status = evt.target.status
try
data = JSON.parse(evt.target.responseText)
attachment = JSON.parse(evt.target.responseText)
catch
data = {}
attachment = {}
if status >= 200 and status < 400
model = $model.make_model(urlName, data)
defered.resolve(model)
attachment = Immutable.fromJS(attachment)
defered.resolve(attachment)
else
response = {
status: status,
@ -93,17 +122,26 @@ resourceProvider = ($rootScope, $config, $urls, $model, $repo, $auth, $q) ->
xhr.addEventListener("load", uploadComplete, false)
xhr.addEventListener("error", uploadFailed, false)
xhr.open("POST", $urls.resolve(urlName))
xhr.setRequestHeader("Authorization", "Bearer #{$auth.getToken()}")
token = storage.get('token')
xhr.open("POST", url)
xhr.setRequestHeader("Authorization", "Bearer #{token}")
xhr.setRequestHeader('Accept', 'application/json')
xhr.send(data)
return defered.promise
return (instance) ->
instance.attachments = service
return () ->
return {"attachments": service}
Resource.$inject = [
"$tgUrls",
"$tgHttp",
"$tgConfig",
"$rootScope",
"$q",
"$tgStorage"
]
module = angular.module("taigaResources")
module.factory("$tgAttachmentsResourcesProvider", ["$rootScope", "$tgConfig", "$tgUrls", "$tgModel", "$tgRepo",
"$tgAuth", "$q", resourceProvider])
module = angular.module("taigaResources2")
module.factory("tgAttachmentsResource", Resource)

View File

@ -24,7 +24,8 @@ services = [
"tgUserstoriesResource",
"tgTasksResource",
"tgIssuesResource",
"tgExternalAppsResource"
"tgExternalAppsResource",
"tgAttachmentsResource"
]
Resources = ($injector) ->

View File

@ -0,0 +1,87 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachments.service.coffee
###
sizeFormat = @.taiga.sizeFormat
class AttachmentsService
@.$inject = [
"$tgConfirm",
"$tgConfig",
"$translate",
"tgResources"
]
constructor: (@confirm, @config, @translate, @rs) ->
@.maxFileSize = @.getMaxFileSize()
if @.maxFileSize
@.maxFileSizeFormated = sizeFormat(@.maxFileSize)
sizeError: (file) ->
message = @translate.instant("ATTACHMENT.ERROR_MAX_SIZE_EXCEEDED", {
fileName: file.name,
fileSize: sizeFormat(file.size),
maxFileSize: @.maxFileSizeFormated
})
@confirm.notify("error", message)
validate: (file) ->
if @.maxFileSize && file.size > @.maxFileSize
@.sizeError(file)
return false
return true
getMaxFileSize: () ->
return @config.get("maxUploadFileSize", null)
list: (type, objId, projectId) ->
return @rs.attachments.list(type, objId, projectId).then (attachments) =>
return attachments.sortBy (attachment) => attachment.get('order')
delete: (type, id) ->
return @rs.attachments.delete(type, id)
saveError: (file, data) ->
message = ""
if file
message = @translate.instant("ATTACHMENT.ERROR_UPLOAD_ATTACHMENT", {
fileName: file.name, errorMessage: data.data._error_message
})
@confirm.notify("error", message)
upload: (file, objId, projectId, type) ->
promise = @rs.attachments.create(type, projectId, objId, file)
promise.then null, @.saveError.bind(this, file)
return promise
patch: (id, type, patch) ->
promise = @rs.attachments.patch(type, id, patch)
promise.then null, @.saveError.bind(this, null)
return promise
angular.module("taigaCommon").service("tgAttachmentsService", AttachmentsService)

View File

@ -0,0 +1,178 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachments.service.spec.coffee
###
describe "tgAttachmentsService", ->
attachmentsService = provide = null
mocks = {}
_mockTgConfirm = () ->
mocks.confirm = {
notify: sinon.stub()
}
provide.value "$tgConfirm", mocks.confirm
_mockTgConfig = () ->
mocks.config = {
get: sinon.stub()
}
mocks.config.get.withArgs('maxUploadFileSize', null).returns(3000)
provide.value "$tgConfig", mocks.config
_mockRs = () ->
mocks.rs = {}
provide.value "tgResources", mocks.rs
_mockTranslate = () ->
mocks.translate = {
instant: sinon.stub()
}
provide.value "$translate", mocks.translate
_inject = (callback) ->
inject (_tgAttachmentsService_) ->
attachmentsService = _tgAttachmentsService_
callback() if callback
_mocks = () ->
module ($provide) ->
provide = $provide
_mockTgConfirm()
_mockTgConfig()
_mockRs()
_mockTranslate()
return null
_setup = ->
_mocks()
beforeEach ->
module "taigaCommon"
_setup()
_inject()
it "maxFileSize formated", () ->
expect(attachmentsService.maxFileSizeFormated).to.be.equal("2.9 KB")
it "sizeError, send notification", () ->
file = {
name: 'test',
size: 3000
}
mocks.translate.instant.withArgs('ATTACHMENT.ERROR_MAX_SIZE_EXCEEDED').returns('message')
attachmentsService.sizeError(file)
expect(mocks.confirm.notify).to.have.been.calledWith('error', 'message')
it "invalid, validate", () ->
file = {
name: 'test',
size: 4000
}
result = attachmentsService.validate(file)
expect(result).to.be.false
it "valid, validate", () ->
file = {
name: 'test',
size: 1000
}
result = attachmentsService.validate(file)
expect(result).to.be.true
it "get max file size", () ->
result = attachmentsService.getMaxFileSize()
expect(result).to.be.equal(3000)
it "delete", () ->
mocks.rs.attachments = {
delete: sinon.stub()
}
attachmentsService.delete('us', 2)
expect(mocks.rs.attachments.delete).to.have.been.calledWith('us', 2)
it "upload", (done) ->
file = {
id: 1
}
objId = 2
projectId = 2
type = 'us'
mocks.rs.attachments = {
create: sinon.stub().promise()
}
mocks.rs.attachments.create.withArgs('us', type, objId, file).resolve()
attachmentsService.sizeError = sinon.spy()
attachmentsService.upload(file, objId, projectId, 'us').then () ->
expect(mocks.rs.attachments.create).to.have.been.calledOnce
done()
it "patch", (done) ->
file = {
id: 1
}
objId = 2
type = 'us'
patch = {
id: 2
}
mocks.rs.attachments = {
patch: sinon.stub().promise()
}
mocks.rs.attachments.patch.withArgs('us', objId, patch).resolve()
attachmentsService.sizeError = sinon.spy()
attachmentsService.patch(objId, 'us', patch).then () ->
expect(mocks.rs.attachments.patch).to.have.been.calledOnce
done()
it "error", () ->
mocks.translate.instant.withArgs("ATTACHMENT.ERROR_MAX_SIZE_EXCEEDED").returns("msg")
attachmentsService.sizeError({
name: 'name',
size: 123
})
expect(mocks.confirm.notify).to.have.been.calledWith('error', 'msg')

View File

@ -21,9 +21,11 @@ class LightboxFactory
@.$inject = ["$rootScope", "$compile"]
constructor: (@rootScope, @compile) ->
create: (name, attrs) ->
create: (name, attrs, scopeAttrs) ->
scope = @rootScope.$new()
scope = _.merge(scope, scopeAttrs)
elm = $("<div>")
.attr(name, true)
.attr("tg-bind-scope", true)

View File

@ -67,6 +67,9 @@ class ProjectService
@._section = null
@._sectionsBreadcrumb = Immutable.List()
hasPermission: (permission) ->
return @._project.get('my_permissions').indexOf(permission) != -1
fetchProject: () ->
pslug = @.project.get('slug')

View File

@ -140,3 +140,20 @@ describe "tgProjectService", ->
expect(projectService.activeMembers.size).to.be.equal(0);
expect(projectService.section).to.be.null;
expect(projectService.sectionsBreadcrumb.size).to.be.equal(0);
it "has permissions", () ->
project = Immutable.Map({
id: 1,
my_permissions: [
'test1',
'test2'
]
})
projectService._project = project
perm1 = projectService.hasPermission('test2')
perm2 = projectService.hasPermission('test3')
expect(perm1).to.be.true
expect(perm2).to.be.false

View File

@ -1,19 +0,0 @@
.attachment-name
span.icon.icon-document
a(href!="<%- url %>", title!="<%- title %>", target="_blank", download!="<%- name %>")
| <%- name %>
.attachment-size
span <%- size %>
.editable.editable-attachment-comment
input(type="text", name="description", maxlength="140",
value!="<%- description %>", placeholder="{{'ATTACHMENT.DESCRIPTION' | translate}}")
.editable.editable-attachment-deprecated
input(type="checkbox", name="is-deprecated",
id!="attach-<%- id %>-is-deprecated")
label(for!="attach-<%- id %>-is-deprecated", translate="{{'ATTACHMENT.DEPRECATED_FILE' | translate}}")
.attachment-settings
a.editable-settings.icon.icon-floppy(href="", title="{{'COMMON.SAVE' | translate}}")
a.editable-settings.icon.icon-delete(href="", title="{{'COMMON.CANCEL' | translate}}")

View File

@ -1,19 +0,0 @@
.attachment-name
a(href!="<%- url %>", title!="<%- title %>", target="_blank", download!="<%- name %>")
span.icon.icon-documents
span <%- name %>
.attachment-size
span <%- size %>
.attachment-comments
<% if (isDeprecated){ %>
span.deprecated-file {{'ATTACHMENT.DEPRECATED' | translate}}
<% } %>
span <%- description %>
<% if (modifyPermission) {%>
.attachment-settings
a.settings.icon.icon-edit(href="", title="{{'COMMON.EDIT' | translate}}")
a.settings.icon.icon-delete(href="", title="{{'COMMON.DELETE' | translate}}")
a.settings.icon.icon-drag-v(href="", title="{{'COMMON.DRAG' | translate}}")
<% } %>

View File

@ -1,30 +0,0 @@
section.attachments
.attachments-header
h3.attachments-title
span.attachments-num(tg-bind-html="ctrl.attachmentsCount")
span.attachments-text(translate="ATTACHMENT.SECTION_NAME")
.add-attach(tg-check-permission!="modify_<%- type %>", title!="{{'ATTACHMENT.ADD' | translate}}")
<% if (maxFileSize){ %>
span.size-info.hidden(translate="ATTACHMENT.MAX_FILE_SIZE", translate-values!="{ 'maxFileSize': '<%- maxFileSize %>'}")
<% }; %>
label(for="add-attach", class="icon icon-plus related-tasks-buttons")
input(id="add-attach", type="file", multiple="multiple")
.attachment-body.sortable
.single-attachment(ng-repeat="attach in ctrl.attachments|filter:ctrl.filterAttachments track by attach.id" tg-attachment="attach", tg-bind-scope)
.single-attachment(ng-repeat="file in ctrl.uploadingAttachments")
.attachment-name
a(href="", tg-bo-title="file.name", tg-bo-bind="file.name")
.attachment-size
span.attachment-size(tg-bo-bind="file.size")
.attachment-comments
span(ng-bind="file.progressMessage")
.percentage(ng-style="{'width': file.progressPercent}")
a.more-attachments(href="", title="{{'ATTACHMENT.SHOW_DEPRECATED' | translate}}", ng-if="ctrl.deprecatedAttachmentsCount > 0")
span.text(data-type="show", translate="ATTACHMENT.SHOW_DEPRECATED")
span.text.hidden(data-type="hide", translate="ATTACHMENT.HIDE_DEPRECATED")
span.more-attachments-num(translate="ATTACHMENT.COUNT_DEPRECATED", translate-values="{counter: '{{ctrl.deprecatedAttachmentsCount}}'}")
div.lightbox.lightbox-block(tg-lb-attachment-preview)

View File

@ -0,0 +1,4 @@
a.add-button(
href=""
)
include ../../../svg/add.svg

View File

@ -1,11 +1,11 @@
include wysiwyg.jade
.view-description
section.us-content.wysiwyg(tg-bind-html="item.description_html || noDescriptionMsg")
span.edit.icon.icon-edit
.edit-description
textarea(ng-attr-placeholder="{{'COMMON.DESCRIPTION.EMPTY' | translate}}", ng-model="item.description", tg-markitup="tg-markitup")
a.help-markdown(href="https://taiga.io/support/taiga-markdown-syntax/", target="_blank", title="{{'COMMON.WYSIWYG.MARKDOWN_HELP' | translate}}")
span.icon.icon-help
span(translate="COMMON.WYSIWYG.MARKDOWN_HELP")
+wysihelp
span.save-container
a.save.icon.icon-floppy(href="", title="{{'COMMON.SAVE' | translate}}")

View File

@ -0,0 +1,6 @@
mixin wysihelp
div.wysiwyg-help
span.drag-drop-help Attach files by dragging & dropping on the textarea above.
a.help-markdown(href="https://taiga.io/support/taiga-markdown-syntax/", target="_blank", title="{{'COMMON.WYSIWYG.MARKDOWN_HELP' | translate}}")
span.icon.icon-help
span(translate="COMMON.WYSIWYG.MARKDOWN_HELP")

View File

@ -1,4 +1,6 @@
section.history(tg-check-permission!="modify_<%- type %>")
include ../components/wysiwyg.jade
section.history
ul.history-tabs
li
a(href="#", class="active", data-section-class="history-comments")
@ -10,12 +12,11 @@ section.history(tg-check-permission!="modify_<%- type %>")
span.tab-title(translate="ACTIVITY.TITLE")
section.history-comments
.comments-list
div(tg-toggle-comment, class="add-comment")
div(tg-editable-wysiwyg, ng-model!="<%- ngmodel %>")
div(tg-check-permission!="modify_<%- type %>", tg-toggle-comment, class="add-comment")
textarea(ng-attr-placeholder="{{'COMMENTS.TYPE_NEW_COMMENT' | translate}}", ng-model!="<%- ngmodel %>.comment", tg-markitup="tg-markitup")
<% if (mode !== "edit") { %>
a(class="help-markdown", href="https://taiga.io/support/taiga-markdown-syntax/", target="_blank", title="{{'COMMON.WYSIWYG.MARKDOWN_HELP' | translate}}")
span.icon.icon-help
span(translate="COMMON.WYSIWYG.MARKDOWN_HELP")
+wysihelp
button(type="button", ng-disabled!="!<%- ngmodel %>.comment.length" title="{{'COMMENTS.COMMENT' | translate}}", translate="COMMENTS.COMMENT", class="button button-green save-comment")
<% } %>
section.history-activity.hidden

View File

@ -2,5 +2,5 @@
a.close(href="", title="{{'COMMON.CLOSE' | translate}}")
span.icon.icon-delete
a(href!="<%- url %>", title!="<%- title %>", target="_blank", download!="<%- name %>")
img(src!="<%- url %>")
a(href="{{::file.get('url')}}", title="{{::file.get('description')}}", target="_blank", download="{{::file.get('name')}}")
img(src="{{::file.get('url')}}")

View File

@ -1,4 +1,7 @@
fieldset.blocked-note.hidden
textarea(name="blocked_note",
ng-attr-placeholder="{{'COMMON.BLOCKED_NOTE' | translate}}",
ng-model!="<%- ngmodel %>")
input(
type="text"
name="blocked_note"
ng-attr-placeholder="{{'COMMON.BLOCKED_NOTE' | translate}}"
ng-model!="<%- ngmodel %>"
)

View File

@ -16,6 +16,14 @@ form
fieldset
div.tags-block(tg-lb-tag-line, ng-model="issue.tags")
fieldset
section
tg-attachments-simple(
attachments="attachments",
on-add="addAttachment(attachment)"
on-delete="deleteAttachment(attachment)"
)
fieldset
textarea.description(ng-attr-placeholder="{{'COMMON.FIELDS.DESCRIPTION' | translate}}", ng-model="issue.description")

View File

@ -18,6 +18,14 @@ form
fieldset
div.tags-block(tg-lb-tag-line, ng-model="task.tags")
fieldset
section
tg-attachments-simple(
attachments="attachments",
on-add="addAttachment(attachment)"
on-delete="deleteAttachment(attachment)"
)
fieldset
textarea.description(ng-attr-placeholder="{{'LIGHTBOX.CREATE_EDIT_TASK.PLACEHOLDER_SHORT_DESCRIPTION' | translate}}", ng-model="task.description")

View File

@ -18,6 +18,14 @@ form
fieldset
textarea.description(name="description", ng-model="us.description",
ng-attr-placeholder="{{'LIGHTBOX.CREATE_EDIT_US.PLACEHOLDER_DESCRIPTION' | translate}}")
fieldset
section
tg-attachments-simple(
attachments="attachments",
on-add="addAttachment(attachment)"
on-delete="deleteAttachment(attachment)"
)
div.settings
fieldset.team-requirement
input(type="checkbox", name="team_requirement", ng-model="us.team_requirement",

View File

@ -1,8 +1,12 @@
section.related-tasks(tg-related-tasks)
div.related-tasks-header
.related-tasks-header
span.related-tasks-title(translate="COMMON.RELATED_TASKS")
div(tg-related-task-create-button)
div.related-tasks-body
div.row.single-related-task(ng-repeat="task in tasks", ng-class="{closed: task.is_closed, blocked: task.is_blocked, iocaine: task.is_iocaine}",
tg-related-task-row, ng-model="task")
div.row.single-related-task.related-task-create-form(tg-related-task-create-form)
.related-tasks-body
.row.single-related-task(
ng-repeat="task in tasks"
ng-class="{closed: task.is_closed, blocked: task.is_blocked, iocaine: task.is_iocaine}"
tg-related-task-row
ng-model="task"
)
.row.single-related-task.related-task-create-form(tg-related-task-create-form)

View File

@ -60,7 +60,7 @@ div.wrapper(
div.tags-block(tg-tag-line, ng-model="issue", required-perm="modify_issue")
section.duty-content(tg-editable-description, ng-model="issue", required-perm="modify_issue")
section.duty-content(tg-editable-description, tg-editable-wysiwyg, ng-model="issue", required-perm="modify_issue")
// Custom Fields
tg-custom-attributes-values(
@ -70,7 +70,12 @@ div.wrapper(
required-edition-perm="modify_issue"
)
tg-attachments(ng-model="issue", type="issue")
tg-attachments-full(
obj-id="issue.id"
type="issue",
project-id="projectId"
)
tg-history(ng-model="issue", type="issue")
sidebar.menu-secondary.sidebar.ticket-data

View File

@ -1,6 +1,6 @@
doctype html
div.wrapper.issues(tg-issues, ng-controller="IssuesController as ctrl", ng-init="section='issues'")
div.wrapper.issues.lightbox-generic-form(tg-issues, ng-controller="IssuesController as ctrl", ng-init="section='issues'")
tg-project-menu
sidebar.menu-secondary.extrabar.filters-bar(tg-issues-filters)
include ../includes/modules/issues-filters

View File

@ -1,27 +1,46 @@
div(class="tasks")
div(class="task-name")
span(class="icon icon-iocaine")
a(tg-nav="project-tasks-detail:project=project.slug,ref=task.ref" title!="#<%- task.ref %> <%- task.subject %>" class="clickable")
span #<%- task.ref %>&nbsp;
.tasks
.task-name
.icon.icon-iocaine
a.clickable(
tg-nav="project-tasks-detail:project=project.slug,ref=task.ref"
title!="#<%- task.ref %> <%- task.subject %>")
span #<%- task.ref %>
span <%- task.subject %>
div(class="task-settings")
.task-settings
<% if(perms.modify_task) { %>
a(href="" title="{{'COMMON.EDIT' | translate}}" class="icon icon-edit")
a.icon.icon-edit(
href=""
title="{{'COMMON.EDIT' | translate}}"
)
<% } %>
<% if(perms.delete_task) { %>
a(href="" title="{{'COMMON.DELETE' | translate}}" class="icon icon-delete delete-task")
a.icon.icon-delete.delete-task(
href=""
title="{{'COMMON.DELETE' | translate}}"
)
<% } %>
div(tg-related-task-status="task" ng-model="task" class="status")
a(href="" title="{{'TASK.TITLE_SELECT_STATUS' | translate}}" class="task-status")
span(class="task-status-bind")
.status(
tg-related-task-status="task"
ng-model="task"
)
a.task-status(
href=""
title="{{'TASK.TITLE_SELECT_STATUS' | translate}}"
)
span.task-status-bind
<% if(perms.modify_task) { %>
span(class="icon icon-arrow-bottom")
span.icon.icon-arrow-bottom
<% } %>
div(tg-related-task-assigned-to-inline-edition="task" class="assigned-to")
div(title="{{'COMMON.FIELDS.ASSIGNED_TO' | translate}}" class="task-assignedto <% if(perms.modify_task) { %>editable<% } %>")
figure(class="avatar")
.assigned-to(
tg-related-task-assigned-to-inline-edition="task"
)
.task-assignedto(
title="{{'COMMON.FIELDS.ASSIGNED_TO' | translate}}"
class="<% if(perms.modify_task) { %>editable<% } %>"
)
figure.avatar
<% if(perms.modify_task) { %>
span(class="icon icon-arrow-bottom")
span.icon.icon-arrow-bottom
<% } %>

View File

@ -75,7 +75,7 @@ div.wrapper(
div.tags-block(tg-tag-line, ng-model="task", required-perm="modify_task")
section.duty-content(tg-editable-description, ng-model="task", required-perm="modify_task")
section.duty-content(tg-editable-description, tg-editable-wysiwyg, ng-model="task", required-perm="modify_task")
// Custom Fields
tg-custom-attributes-values(
@ -85,7 +85,12 @@ div.wrapper(
required-edition-perm="modify_task"
)
tg-attachments(ng-model="task", type="task")
tg-attachments-full(
obj-id="task.id"
type="task",
project-id="projectId"
)
tg-history(ng-model="task", type="task")
sidebar.menu-secondary.sidebar.ticket-data

View File

@ -68,7 +68,7 @@ div.wrapper(
div.tags-block(tg-tag-line, ng-model="us", required-perm="modify_us")
section.duty-content(tg-editable-description, ng-model="us", required-perm="modify_us")
section.duty-content(tg-editable-description, tg-editable-wysiwyg, ng-model="us", required-perm="modify_us")
// Custom Fields
tg-custom-attributes-values(
@ -80,10 +80,12 @@ div.wrapper(
include ../includes/modules/related-tasks
tg-attachments(
ng-model="us"
type="us"
tg-attachments-full(
obj-id="us.id"
type="us",
project-id="projectId"
)
tg-history(
ng-model="us"
type="us"

View File

@ -1,3 +1,5 @@
include ../common/components/wysiwyg.jade
.view-wiki-content
section.wysiwyg(tg-bind-html='wiki.html')
span.edit.icon.icon-edit(title="{{'COMMON.EDIT' | translate}}", ng-if="wiki")
@ -5,11 +7,7 @@
.edit-wiki-content(style='display: none;')
textarea(ng-attr-placeholder="{{'WIKI.PLACEHOLDER_PAGE' | translate}}",
ng-model='wiki.content', tg-markitup='tg-markitup')
a.help-markdown(href='https://taiga.io/support/taiga-markdown-syntax/', target='_blank',
title="{{'COMMON.WYSIWYG.MARKDOWN_HELP' | translate}}")
span.icon.icon-help
span(translate="COMMON.WYSIWYG.MARKDOWN_HELP")
+wysihelp
span.action-container
a.save.icon.icon-floppy(href='', title="{{'COMMON.SAVE' | translate}}")

View File

@ -16,7 +16,12 @@ div.wrapper(ng-controller="WikiDetailController as ctrl",
h2.wiki-title(ng-bind='wikiTitle')
section.wiki-content(tg-editable-wiki-content, ng-model="wiki")
tg-attachments(ng-model="wiki", type="wiki_page", ng-if="wiki.id")
tg-attachments-full(
ng-if="wiki.id"
obj-id="wiki.id"
type="wiki_page",
project-id="projectId"
)
a.remove(href="", ng-click="ctrl.delete()", ng-if="wiki.id", title="{{'WIKI.REMOVE' | translate}}", tg-check-permission="delete_wiki_page")
span.icon.icon-delete

View File

@ -0,0 +1,29 @@
.wysiwyg-help {
background: $whitish;
display: flex;
justify-content: space-between;
margin-top: -.5rem;
padding: .25rem .5rem;
}
.drag-drop-help {
@extend %xsmall;
color: $gray;
}
.help-markdown,
.help-button {
@extend %xsmall;
&:hover {
span {
transition: color .2s linear;
}
.icon {
color: $primary-light;
transition: color .2s linear;
}
}
.icon {
margin-right: .2rem;
}
}

View File

@ -1,19 +0,0 @@
a.help-markdown,
a.help-button {
@extend %small;
color: $gray-light;
&:hover {
span {
color: $grayer;
transition: color .2s linear;
}
.icon {
color: $primary-light;
transition: color .2s linear;
}
}
.icon {
color: $gray-light;
margin-right: .2rem;
}
}

View File

@ -21,6 +21,15 @@ textarea {
transition: border .3s linear;
}
}
button,
button:active,
button:focus {
background: none;
border: 0;
outline: 0;
outline-style: none;
outline-width: 0;
}
textarea {
min-height: 10rem;
resize: vertical;

View File

@ -141,3 +141,9 @@
}
}
}
@mixin cursor-progress {
.in-progress {
cursor: progress;
}
}

View File

@ -166,6 +166,7 @@
}
.duty-content {
@include cursor-progress;
position: relative;
&:hover {
.view-description {

View File

@ -24,6 +24,7 @@
}
.wiki-content {
@include cursor-progress;
margin-bottom: 2rem;
position: relative;
&.editable {

View File

@ -1,190 +0,0 @@
.attachments {
margin-bottom: 2rem;
}
.attachments-header {
align-content: space-between;
align-items: center;
background: $whitish;
display: flex;
justify-content: space-between;
padding: .5rem 1rem;
.attachments-title {
@extend %medium;
@extend %bold;
color: $grayer;
}
.attachments-num,
.attachments-text {
margin-right: .1rem;
}
.icon {
@extend %large;
color: $grayer;
cursor: pointer;
&:hover {
color: $primary;
transition: color .2s ease-in;
}
}
}
.single-attachment {
@extend %small;
align-items: center;
border-bottom: 1px solid $whitish;
display: flex;
padding: .5rem 0 .5rem 1rem;
position: relative;
&:hover {
.attachment-settings {
.settings {
opacity: 1;
transition: opacity .2s ease-in;
}
}
}
&.ui-sortable-helper {
background: lighten($primary, 60%);
box-shadow: 1px 1px 10px rgba($black, .1);
transition: background .2s ease-in;
}
&.deprecated {
color: $gray-light;
.attachment-name a {
color: $gray-light;
}
}
&.sortable-placeholder {
background: $whitish;
height: 40px;
}
.attachment-name {
@extend %bold;
@include ellipsis(200px);
flex-basis: 35%;
flex-grow: 1;
padding-right: 1rem;
.icon {
margin-right: .5rem;
}
}
.attachment-size {
color: $gray-light;
flex-basis: 15%;
flex-grow: 1;
margin-right: .5rem;
}
.attachment-comments,
.editable-attachment-comment {
flex-basis: 35%;
flex-grow: 1;
span {
color: $gray;
}
}
.editable-attachment-comment {
@extend %small;
}
.attachment-settings {
flex-basis: 15%;
flex-grow: 1;
.settings,
.editable-settings {
@extend %large;
color: $gray-light;
display: block;
position: absolute;
&:hover {
color: $primary;
}
}
.settings {
opacity: 0;
top: .5rem;
}
.editable-settings {
opacity: 1;
top: 1rem;
}
.icon-edit,
.icon-floppy {
right: 3.5rem;
}
.icon-delete {
right: 2rem;
&:hover {
color: $red;
}
}
.icon-drag-v {
cursor: move;
right: 0;
}
}
.icon-delete {
@extend %large;
color: $gray-light;
&:hover {
color: $red;
}
}
.editable-attachment-deprecated {
padding-left: 1rem;
span {
color: $gray-light;
}
input {
margin-right: .2rem;
vertical-align: middle;
&:checked+span {
color: $grayer;
}
}
}
.percentage {
background: rgba($primary, .1);
bottom: 0;
height: 40px;
left: 0;
position: absolute;
top: 0;
width: 45%;
}
}
.more-attachments {
@extend %small;
border-bottom: 1px solid $gray-light;
display: block;
padding: 1rem 0 1rem 1rem;
span {
color: $gray-light;
}
.more-attachments-num {
color: $primary;
margin-left: .5rem;
}
&:hover {
background: lighten($primary, 60%);
transition: background .2s ease-in;
}
}
.add-attach {
cursor: pointer;
overflow: hidden;
position: relative;
input {
display: none;
}
span {
@extend %small;
color: $gray-light;
}
}
.attachment-preview img {
max-height: 95vh;
max-width: 95vw;
}

View File

@ -67,10 +67,12 @@
}
}
.add-comment {
@include cursor-progress;
@include clearfix;
&.active {
.button-green {
display: block;
margin-top: .5rem;
}
textarea {
height: 6rem;
@ -89,7 +91,6 @@
textarea {
background: $white;
height: 5rem;
margin-bottom: .5rem;
min-height: 41px;
}
.help-markdown {

View File

@ -18,12 +18,9 @@
}
textarea {
margin-bottom: 1rem;
max-height: 9rem;
min-height: 7rem;
min-height: 4.5rem;
resize: vertical;
}
label {
@extend %xsmall;
background: $whitish;
@ -47,12 +44,12 @@
.settings {
display: flex;
justify-content: center;
margin-bottom: 1rem;
fieldset {
margin-right: .5rem;
&:hover {
color: $white;
transition: all .2s ease-in;
transition-delay: .2s;
}
&:last-child {
margin: 0;
@ -98,6 +95,26 @@
display: none;
}
}
.attachments {
margin-bottom: 0;
}
.attachment-body {
max-height: 7.5rem;
overflow-y: auto;
}
.attachment-delete {
right: .5rem;
svg {
fill: $gray-light;
height: 1.25rem;
width: 1.25rem;
}
&:hover {
svg {
fill: $red;
}
}
}
}
.lightbox-generic-bulk {
@ -493,7 +510,7 @@
.ticket-role-points {
flex-grow: 1;
flex-shrink: 1;
max-width: calc(100% * (1/5) - .2rem);
max-width: calc(100% * (1/6) - .2rem);
&:first-child {
margin-left: 0;
}
@ -502,6 +519,6 @@
}
}
.points-per-role {
margin-bottom: 1rem;
margin-bottom: 0;
}
}

View File

@ -9,18 +9,26 @@
background: $whitish;
display: flex;
justify-content: space-between;
padding: .5rem 1rem;
.related-tasks-title {
@extend %medium;
@extend %bold;
margin-left: 1rem;
}
.icon {
@extend %large;
color: $grayer;
cursor: pointer;
&:hover {
color: $primary;
transition: color .2s ease-in;
.add-button {
background: $grayer;
border: 0;
display: inline-block;
padding: .5rem;
transition: background .25s;
&:hover,
&.is-active {
background: $primary-light;
}
svg {
fill: $white;
height: 1.25rem;
margin-bottom: -.2rem;
width: 1.25rem;
}
}
}

3
app/svg/attachment.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1000 1000">
<path transform="translate(91.675 74.66) scale(17.01355)" d="M15 36C8.92 36 4 31.07 4 25s4.92-11 11-11h21c4.42 0 8 3.58 8 8s-3.58 8-8 8H19c-2.76 0-5-2.24-5-5s2.24-5 5-5h15v3H19c-1.1 0-2 .89-2 2s.9 2 2 2h17c2.76 0 5-2.24 5-5s-2.24-5-5-5H15c-4.42 0-8 3.58-8 8s3.58 8 8 8h19v3H15z"/>
</svg>

After

Width:  |  Height:  |  Size: 400 B

3
app/svg/gallery.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<path d="M107.855 133.83h359.073v333.794H107.855zM533.072 133.83h359.074v333.794H533.072zM107.855 532.376h359.073V866.17H107.855zM533.072 532.376h359.074V866.17H533.072z" />
</svg>

After

Width:  |  Height:  |  Size: 250 B

3
app/svg/list.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<path transform="matrix(31.97294 0 0 31.97294 5313.926 639.882)" d="M-162.75-2.5h3.75v-3.75h-3.75zm0 7.5h3.75V1.25h-3.75zm0-15h3.75v-3.75h-3.75zm7.5 7.5h16.875v-3.75h-16.875zm0 7.5h16.875V1.25h-16.875zm0-18.75V-10h16.875v-3.75z"/>
</svg>

After

Width:  |  Height:  |  Size: 307 B

1
app/svg/pattern.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200"><path d="M211,504L-64,554L273,510Z" fill="#448e54" stroke="#448e54" stroke-width="1.51"/><path d="M395,96L413,270L481,52Z" fill="#449c57" stroke="#449c57" stroke-width="1.51"/><path d="M481,52L413,270L488,358Z" fill="#2b8e4c" stroke="#2b8e4c" stroke-width="1.51"/><path d="M273,510L-64,554L495,543Z" fill="#2d8249" stroke="#2d8249" stroke-width="1.51"/><path d="M-302,-260L105,-294L513,-357Z" fill="#c4e6a4" stroke="#c4e6a4" stroke-width="1.51"/><path d="M105,-294L298,-267L513,-357Z" fill="#9fcf8d" stroke="#9fcf8d" stroke-width="1.51"/><path d="M282,-153L481,52L534,-109Z" fill="#72af69" stroke="#72af69" stroke-width="1.51"/><path d="M513,-357L298,-267L534,-109Z" fill="#8bb777" stroke="#8bb777" stroke-width="1.51"/><path d="M298,-267L282,-153L534,-109Z" fill="#8abe77" stroke="#8abe77" stroke-width="1.51"/><path d="M481,52L488,358L534,-109Z" fill="#469757" stroke="#469757" stroke-width="1.51"/><path d="M488,358L495,543L534,-109Z" fill="#218346" stroke="#218346" stroke-width="1.51"/><path d="M282,-153L395,96L481,52Z" fill="#69af67" stroke="#69af67" stroke-width="1.51"/><path d="M488,358L273,510L495,543Z" fill="#10723b" stroke="#10723b" stroke-width="1.51"/><path d="M413,270L273,510L488,358Z" fill="#208041" stroke="#208041" stroke-width="1.51"/><path d="M282,-153L148,102L395,96Z" fill="#72c072" stroke="#72c072" stroke-width="1.51"/><path d="M181,356L273,510L413,270Z" fill="#2d904c" stroke="#2d904c" stroke-width="1.51"/><path d="M395,96L148,102L413,270Z" fill="#4cab5f" stroke="#4cab5f" stroke-width="1.51"/><path d="M148,102L181,356L413,270Z" fill="#45aa5d" stroke="#45aa5d" stroke-width="1.51"/><path d="M-302,-260L-45,-237L105,-294Z" fill="#e6f5ad" stroke="#e6f5ad" stroke-width="1.51"/><path d="M181,356L211,504L273,510Z" fill="#348f4f" stroke="#348f4f" stroke-width="1.51"/><path d="M161,-135L148,102L282,-153Z" fill="#92d081" stroke="#92d081" stroke-width="1.51"/><path d="M161,-135L282,-153L298,-267Z" fill="#9cd386" stroke="#9cd386" stroke-width="1.51"/><path d="M105,-294L161,-135L298,-267Z" fill="#aedc91" stroke="#aedc91" stroke-width="1.51"/><path d="M148,102L-14,145L181,356Z" fill="#6ebf71" stroke="#6ebf71" stroke-width="1.51"/><path d="M-14,145L-48,271L181,356Z" fill="#74bd70" stroke="#74bd70" stroke-width="1.51"/><path d="M181,356L-64,554L211,504Z" fill="#4e995a" stroke="#4e995a" stroke-width="1.51"/><path d="M-111,-28L-14,145L148,102Z" fill="#9cd687" stroke="#9cd687" stroke-width="1.51"/><path d="M-111,-28L148,102L161,-135Z" fill="#a5da8a" stroke="#a5da8a" stroke-width="1.51"/><path d="M-48,271L-64,554L181,356Z" fill="#69aa65" stroke="#69aa65" stroke-width="1.51"/><path d="M105,-294L-45,-237L161,-135Z" fill="#c3e69b" stroke="#c3e69b" stroke-width="1.51"/><path d="M-45,-237L-111,-28L161,-135Z" fill="#c6e89a" stroke="#c6e89a" stroke-width="1.51"/><path d="M-274,131L-276,222L-48,271Z" fill="#a2d48a" stroke="#a2d48a" stroke-width="1.51"/><path d="M-217,430L-64,554L-48,271Z" fill="#7eb16f" stroke="#7eb16f" stroke-width="1.51"/><path d="M-218,-122L-111,-28L-45,-237Z" fill="#dbf1a4" stroke="#dbf1a4" stroke-width="1.51"/><path d="M-274,131L-48,271L-14,145Z" fill="#9cd385" stroke="#9cd385" stroke-width="1.51"/><path d="M-111,-28L-274,131L-14,145Z" fill="#b2df93" stroke="#b2df93" stroke-width="1.51"/><path d="M-276,222L-217,430L-48,271Z" fill="#92c47d" stroke="#92c47d" stroke-width="1.51"/><path d="M-302,-260L-218,-122L-45,-237Z" fill="#ecf8b1" stroke="#ecf8b1" stroke-width="1.51"/><path d="M-218,-122L-274,131L-111,-28Z" fill="#cdeba0" stroke="#cdeba0" stroke-width="1.51"/><path d="M-302,-260L-274,131L-218,-122Z" fill="#dff2b2" stroke="#dff2b2" stroke-width="1.51"/><path d="M-302,-260L-276,222L-274,131Z" fill="#cae9ab" stroke="#cae9ab" stroke-width="1.51"/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -56,7 +56,7 @@ $dropdown-color: rgba(darken($primary-dark, 20%), 1);
%mono {font-family: 'courier new', 'monospace';}
%lightbox {
background: rgba($white, .95);
background: rgba($white, 1);
}
// Background images

View File

@ -53,7 +53,7 @@ $dropdown-color: rgba(darken($primary-dark, 20%), 1);
// lightbox
%lightbox {
background: rgba($white, .95);
background: rgba($white, .98);
}
// Background images

View File

@ -62,7 +62,7 @@ $dropdown-color: rgba(darken($grayer, 20%), 1);
// lightbox
%lightbox {
background: rgba($white, .95);
background: rgba($white, .98);
}
// Background images

View File

@ -1,6 +1,12 @@
var utils = require('../utils');
var helper = module.exports;
var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
var expect = chai.expect;
helper.assignToLightbox = function() {
let el = $('div[tg-lb-assignedto]');
@ -37,3 +43,23 @@ helper.assignToLightbox = function() {
return obj;
};
helper.lightboxAttachment = async function() {
let el = $('tg-attachments-simple');
let addAttachment = el.$('#add-attach');
let countAttachments = await el.$$('.single-attachment').count();
var fileToUpload1 = utils.common.uploadImagePath();
var fileToUpload2 = utils.common.uploadFilePath();
await utils.common.uploadFile(addAttachment, fileToUpload1)
await utils.common.uploadFile(addAttachment, fileToUpload2)
el.$$('.attachment-delete').get(0).click()
let newCountAttachments = await el.$$('.single-attachment').count();
expect(countAttachments + 1).to.be.equal(newCountAttachments);
}

View File

@ -256,14 +256,14 @@ helper.delete = function() {
};
helper.attachment = function() {
let el = $('tg-attachments');
let el = $('tg-attachments-full');
let obj = {
el:el,
upload: async function(filePath, name) {
let addAttach = el.$('#add-attach');
let countAttachments = await $$('div[tg-attachment]').count();
let countAttachments = await $$('tg-attachment').count();
let toggleInput = function() {
$('#add-attach').toggle();
@ -274,37 +274,37 @@ helper.attachment = function() {
await browser.waitForAngular();
await browser.wait(async () => {
let newCountAttachments = await $$('div[tg-attachment]').count();
let newCountAttachments = await $$('tg-attachment').count();
return newCountAttachments == countAttachments + 1;
}, 5000);
await el.$$('div[tg-attachment] .editable-attachment-comment input').last().sendKeys(name);
await el.$$('tg-attachment .editable-attachment-comment input').last().sendKeys(name);
await browser.actions().sendKeys(protractor.Key.ENTER).perform();
await browser.executeScript(toggleInput);
await browser.waitForAngular();
},
renameLastAttchment: async function (name) {
await browser.actions().mouseMove(el.$$('div[tg-attachment]').last()).perform();
await el.$$('div[tg-attachment] .attachment-settings .icon-edit').last().click();
await el.$$('div[tg-attachment] .editable-attachment-comment input').last().sendKeys(name);
await browser.actions().mouseMove(el.$$('tg-attachment').last()).perform();
await el.$$('tg-attachment .attachment-settings .icon-edit').last().click();
await el.$$('tg-attachment .editable-attachment-comment input').last().sendKeys(name);
await browser.actions().sendKeys(protractor.Key.ENTER).perform();
return browser.waitForAngular();
},
getFirstAttachmentName: async function () {
let name = await el.$$('div[tg-attachment] .attachment-comments').first().getText();
let name = await el.$$('tg-attachment .attachment-comments').first().getText();
return name;
},
getLastAttachmentName: async function () {
let name = await el.$$('div[tg-attachment] .attachment-comments').last().getText();
let name = await el.$$('tg-attachment .attachment-comments').last().getText();
return name;
},
countAttachments: async function(){
return await el.$$('div[tg-attachment]').count();
return await el.$$('tg-attachment').count();
},
countDeprecatedAttachments: async function(){
@ -321,10 +321,10 @@ helper.attachment = function() {
},
deprecateLastAttachment: async function() {
await browser.actions().mouseMove(el.$$('div[tg-attachment]').last()).perform();
await el.$$('div[tg-attachment] .attachment-settings .icon-edit').last().click();
await el.$$('div[tg-attachment] .editable-attachment-deprecated input').last().click();
await el.$$('div[tg-attachment] .attachment-settings .editable-settings.icon-floppy').last().click();
await browser.actions().mouseMove(el.$$('tg-attachment').last()).perform();
await el.$$('tg-attachment .attachment-settings .icon-edit').last().click();
await el.$$('tg-attachment .editable-attachment-deprecated input').last().click();
await el.$$('tg-attachment .attachment-settings .editable-settings.icon-floppy').last().click();
await browser.waitForAngular();
},
@ -333,7 +333,7 @@ helper.attachment = function() {
},
deleteLastAttachment: async function() {
let attachment = await $$('div[tg-attachment]').last();
let attachment = await $$('tg-attachment').last();
await browser.actions().mouseMove(attachment).perform();
@ -359,11 +359,23 @@ helper.attachment = function() {
},
dragLastAttchmentToFirstPosition: async function() {
await browser.actions().mouseMove(el.$$('div[tg-attachment]').last()).perform();
let lastDraggableAttachment = el.$$('div[tg-attachment] .attachment-settings .icon-drag-v').last();
let destination = el.$$('div[tg-attachment] .attachment-settings .icon-drag-v').first();
await browser.actions().mouseMove(el.$$('tg-attachment').last()).perform();
let lastDraggableAttachment = el.$$('tg-attachment .attachment-settings .icon-drag-v').last();
let destination = el.$$('tg-attachment .attachment-settings .icon-drag-v').first();
await utils.common.drag(lastDraggableAttachment, destination);
}
},
galleryImages: function() {
return $$('tg-attachment-gallery');
},
gallery: function() {
$('.view-gallery').click();
},
list: function() {
$('.view-list').click();
},
};
return obj;

View File

@ -249,6 +249,18 @@ shared.attachmentTesting = async function() {
newAttachmentsLength = await attachmentHelper.countAttachments();
expect(newAttachmentsLength).to.be.equal(attachmentsLength + deprecatedAttachmentsLength);
// Gallery
attachmentHelper.gallery();
let countImages = await attachmentHelper.galleryImages().count();
commonUtil.takeScreenshot('attachments', 'gallery');
expect(countImages).to.be.above(0);
attachmentHelper.list();
// Deleting
attachmentsLength = await attachmentHelper.countAttachments();
await attachmentHelper.deleteLastAttachment();

View File

@ -1,5 +1,6 @@
var utils = require('../utils');
var backlogHelper = require('../helpers').backlog;
var commonHelper = require('../helpers').common;
var chai = require('chai');
var chaiAsPromised = require('chai-as-promised');
@ -57,9 +58,12 @@ describe('backlog', function() {
//settings
createUSLightbox.settings(0).click();
await utils.common.waitTransitionTime(createUSLightbox.settings(0));
});
it('upload attachments', commonHelper.lightboxAttachment);
it('screenshots', function() {
utils.common.takeScreenshot('backlog', 'create-us-filled');
});
@ -150,6 +154,8 @@ describe('backlog', function() {
editUSLightbox.settings(1).click();
});
it('upload attachments', commonHelper.lightboxAttachment);
it('send form', async function() {
editUSLightbox.submit();

View File

@ -41,7 +41,11 @@ describe('issues list', function() {
await createIssueLightbox.tags().sendKeys('bbb');
browser.actions().sendKeys(protractor.Key.ENTER).perform();
});
it('upload attachments', commonHelper.lightboxAttachment);
it('screenshots', function() {
utils.common.takeScreenshot('issues', 'create-issue-filled');
});

View File

@ -67,6 +67,12 @@ describe('kanban', function() {
createUSLightbox.settings(1).click();
});
it('upload attachments', commonHelper.lightboxAttachment);
it('screenshots', function() {
utils.common.takeScreenshot('kanban', 'create-us-filled');
})
it('send form', async function() {
createUSLightbox.submit();
@ -134,6 +140,8 @@ describe('kanban', function() {
createUSLightbox.settings(1).click();
});
it('upload attachments', commonHelper.lightboxAttachment);
it('send form', async function() {
createUSLightbox.submit();

View File

@ -438,7 +438,7 @@ gulp.task("jslibs-deploy", function() {
.pipe(gulp.dest(paths.distVersion + "js/"));
});
gulp.task("app-watch", ["coffee-lint", "coffee", "conf", "locales", "app-loader"]);
gulp.task("app-watch", ["coffee", "conf", "locales", "app-loader"]);
gulp.task("app-deploy", ["coffee", "conf", "locales", "app-loader"], function() {
return gulp.src(paths.distVersion + "js/app.js")