diff --git a/CHANGELOG.md b/CHANGELOG.md index 1da0e824..c7263396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Show a confirmation notice when you exit edit mode by pressing ESC in the markdown inputs. - Add the tribe button to link stories from tree.taiga.io with gigs in tribe.taiga.io. - Errors (not found, server error, permissions and blocked project) don't change the current url. +- Attachments image slider ### Misc - Lots of small and not so small bugfixes. diff --git a/app/coffee/modules/common.coffee b/app/coffee/modules/common.coffee index d7c660cf..3f36dd4d 100644 --- a/app/coffee/modules/common.coffee +++ b/app/coffee/modules/common.coffee @@ -395,3 +395,50 @@ Autofocus = ($timeout) -> } module.directive('tgAutofocus', ['$timeout', Autofocus]) + +module.directive 'tgPreloadImage', () -> + spinner = "loading..." + + template = """ +
+ +
+ """ + + preload = (src, onLoad) -> + image = new Image() + image.onload = onLoad + image.src = src + + return image + + return { + template: template, + transclude: true, + replace: true, + link: (scope, el, attrs) -> + image = el.find('img:last') + timeout = null + + onLoad = () -> + el.find('.loading-spinner').remove() + image.show() + + if timeout + clearTimeout(timeout) + timeout = null + + attrs.$observe 'preloadSrc', (src) -> + if timeout + clearTimeout(timeout) + + el.find('.loading-spinner').remove() + + timeout = setTimeout () -> + el.prepend(spinner) + , 200 + + image.hide() + + preload(src, onLoad) + } diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index c658a710..205b51bb 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -686,22 +686,6 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", WatchersLightboxDirective]) -############################################################################# -## Attachment Preview Lighbox -############################################################################# - -AttachmentPreviewLightboxDirective = (lightboxService, $template, $compile) -> - link = ($scope, $el, attrs) -> - lightboxService.open($el) - - return { - templateUrl: 'common/lightbox/lightbox-attachment-preview.html', - link: link, - scope: true - } - -module.directive("tgLbAttachmentPreview", ["lightboxService", "$tgTemplate", "$compile", AttachmentPreviewLightboxDirective]) - LightboxLeaveProjectWarningDirective = (lightboxService, $template, $compile) -> link = ($scope, $el, attrs) -> lightboxService.open($el) diff --git a/app/modules/attachments/attachments.scss b/app/modules/attachments/attachments.scss index 16e465cf..f715d93e 100644 --- a/app/modules/attachments/attachments.scss +++ b/app/modules/attachments/attachments.scss @@ -127,6 +127,26 @@ } .attachment-preview { + .attachment-preview-container { + svg { + @include svg-size(3rem); + fill: $gray-light; + &:hover { + fill: $primary-light; + transition: fill .3s linear; + } + } + } + .previous { + left: 3rem; + position: absolute; + top: calc(50% - 3rem); + } + .next { + position: absolute; + right: 3rem; + top: calc(50% - 3rem); + } img { max-height: 80vh; max-width: 80vw; diff --git a/app/modules/components/attachment-link/attachment-link.directive.coffee b/app/modules/components/attachment-link/attachment-link.directive.coffee index 50cd109e..2e357f15 100644 --- a/app/modules/components/attachment-link/attachment-link.directive.coffee +++ b/app/modules/components/attachment-link/attachment-link.directive.coffee @@ -17,7 +17,7 @@ # File: attachment-link.directive.coffee ### -AttachmentLinkDirective = ($parse, lightboxFactory) -> +AttachmentLinkDirective = ($parse, attachmentsPreviewService, lightboxService) -> link = (scope, el, attrs) -> attachment = $parse(attrs.tgAttachmentLink)(scope) @@ -26,11 +26,8 @@ AttachmentLinkDirective = ($parse, lightboxFactory) -> event.preventDefault() scope.$apply -> - lightboxFactory.create('tg-lb-attachment-preview', { - class: 'lightbox lightbox-block' - }, { - file: attachment.get('file') - }) + lightboxService.open($('tg-attachments-preview')) + attachmentsPreviewService.fileId = attachment.getIn(['file', 'id']) scope.$on "$destroy", -> el.off() return { @@ -39,7 +36,8 @@ AttachmentLinkDirective = ($parse, lightboxFactory) -> AttachmentLinkDirective.$inject = [ "$parse", - "tgLightboxFactory" + "tgAttachmentsPreviewService", + "lightboxService" ] angular.module("taigaComponents").directive("tgAttachmentLink", AttachmentLinkDirective) diff --git a/app/modules/components/attachment/attachment-gallery.jade b/app/modules/components/attachment/attachment-gallery.jade index eda74610..4a131ef2 100644 --- a/app/modules/components/attachment/attachment-gallery.jade +++ b/app/modules/components/attachment/attachment-gallery.jade @@ -2,7 +2,7 @@ ng-class="{deprecated: vm.attachment.getIn(['file', 'is_deprecated'])}", ng-if="vm.attachment.getIn(['file', 'id'])", ) - a.attachment-image( + a.attachment-image.e2e-attachment-link( tg-attachment-link="vm.attachment" href="{{::vm.attachment.getIn(['file', 'url'])}}" title="{{::vm.attachment.getIn(['file', 'name'])}}" diff --git a/app/modules/components/attachment/attachment.jade b/app/modules/components/attachment/attachment.jade index ec3b885b..842a9a7f 100644 --- a/app/modules/components/attachment/attachment.jade +++ b/app/modules/components/attachment/attachment.jade @@ -5,7 +5,7 @@ form.single-attachment( ) .attachment-name - a( + a.e2e-attachment-link( tg-attachment-link="vm.attachment" href="{{::vm.attachment.getIn(['file', 'url'])}}" title="{{::vm.attachment.get(['file', 'name'])}}" diff --git a/app/modules/components/attachments-full/attachments-full.controller.coffee b/app/modules/components/attachments-full/attachments-full.controller.coffee index 400ee65f..32a10cb6 100644 --- a/app/modules/components/attachments-full/attachments-full.controller.coffee +++ b/app/modules/components/attachments-full/attachments-full.controller.coffee @@ -26,10 +26,11 @@ class AttachmentsFullController "$tgConfig", "$tgStorage", "tgAttachmentsFullService", - "tgProjectService" + "tgProjectService", + "tgAttachmentsPreviewService" ] - constructor: (@translate, @confirm, @config, @storage, @attachmentsFullService, @projectService) -> + constructor: (@translate, @confirm, @config, @storage, @attachmentsFullService, @projectService, @attachmentsPreviewService) -> @.mode = @storage.get('attachment-mode', 'list') @.maxFileSize = @config.get("maxUploadFileSize", null) @@ -64,6 +65,8 @@ class AttachmentsFullController @attachmentsFullService.loadAttachments(@.type, @.objId, @.projectId) deleteAttachment: (toDeleteAttachment) -> + @attachmentsPreviewService.fileId = null + title = @translate.instant("ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT") message = @translate.instant("ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT", { fileName: toDeleteAttachment.getIn(['file', 'name']) diff --git a/app/modules/components/attachments-full/attachments-full.jade b/app/modules/components/attachments-full/attachments-full.jade index d441def1..ce78603f 100644 --- a/app/modules/components/attachments-full/attachments-full.jade +++ b/app/modules/components/attachments-full/attachments-full.jade @@ -94,3 +94,8 @@ section.attachments( alt="{{'COMMON.LOADING' | translate}}" ) .attachment-data {{file.progressMessage}} + +tg-attachments-preview.lightbox.lightbox-block( + ng-show="vm.showAttachments()", + attachments="vm.attachments" +) diff --git a/app/modules/components/attachments-preview/attachments-preview.controller.coffee b/app/modules/components/attachments-preview/attachments-preview.controller.coffee new file mode 100644 index 00000000..b1876b5d --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.controller.coffee @@ -0,0 +1,75 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: attchments-preview.controller.coffee +### + +class AttachmentsPreviewController + @.$inject = [ + "tgAttachmentsPreviewService" + ] + + constructor: (@attachmentsPreviewService) -> + taiga.defineImmutableProperty @, "current", () => + if !@attachmentsPreviewService.fileId + return null + + return @.getCurrent() + + hasPagination: () -> + images = @.attachments.filter (attachment) => + return taiga.isImage(attachment.getIn(['file', 'name'])) + + return images.size > 1 + + getCurrent: () -> + attachment = @.attachments.find (attachment) => + @attachmentsPreviewService.fileId == attachment.getIn(['file', 'id']) + + file = attachment.get('file') + + return file + + getIndex: () -> + return @.attachments.findIndex (attachment) => + @attachmentsPreviewService.fileId == attachment.getIn(['file', 'id']) + + next: () -> + attachmentIndex = @.getIndex() + + image = @.attachments.slice(attachmentIndex + 1).find (attachment) -> + return taiga.isImage(attachment.getIn(['file', 'name'])) + + if !image + image = @.attachments.find (attachment) -> + return taiga.isImage(attachment.getIn(['file', 'name'])) + + + @attachmentsPreviewService.fileId = image.getIn(['file', 'id']) + + previous: () -> + attachmentIndex = @.getIndex() + + image = @.attachments.slice(0, attachmentIndex).findLast (attachment) -> + return taiga.isImage(attachment.getIn(['file', 'name'])) + + if !image + image = @.attachments.findLast (attachment) -> + return taiga.isImage(attachment.getIn(['file', 'name'])) + + @attachmentsPreviewService.fileId = image.getIn(['file', 'id']) + +angular.module('taigaComponents').controller('AttachmentsPreview', AttachmentsPreviewController) diff --git a/app/modules/components/attachments-preview/attachments-preview.controller.spec.coffee b/app/modules/components/attachments-preview/attachments-preview.controller.spec.coffee new file mode 100644 index 00000000..a33464fe --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.controller.spec.coffee @@ -0,0 +1,346 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: attachments-preview.controller.spec.coffee +### + +describe "AttachmentsPreviewController", -> + $provide = null + $controller = null + scope = null + mocks = {} + + _mockAttachmentsPreviewService = -> + mocks.attachmentsPreviewService = {} + + $provide.value("tgAttachmentsPreviewService", mocks.attachmentsPreviewService) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockAttachmentsPreviewService() + + return null + + _inject = -> + inject (_$controller_, $rootScope) -> + $controller = _$controller_ + scope = $rootScope.$new() + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaComponents" + + _setup() + + it "get current file", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1 + } + }, + { + file: { + id: 2 + } + }, + { + file: { + id: 3 + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 2; + + current = ctrl.getCurrent() + + expect(current.get('id')).to.be.equal(2) + expect(ctrl.current.get('id')).to.be.equal(2) + + + it "has pagination", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(0) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 1; + + pagination = ctrl.hasPagination() + + expect(pagination).to.be.false + + ctrl.attachments = ctrl.attachments.push(Immutable.fromJS({ + file: { + id: 4, + name: "xx.jpg" + } + })) + + pagination = ctrl.hasPagination() + + expect(pagination).to.be.true + + it "get index", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1 + } + }, + { + file: { + id: 2 + } + }, + { + file: { + id: 3 + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 2; + + currentIndex = ctrl.getIndex() + + expect(currentIndex).to.be.equal(1) + + it "next", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(0) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 1; + + currentIndex = ctrl.next() + + expect(mocks.attachmentsPreviewService.fileId).to.be.equal(3) + + it "next infinite", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(2) + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx.jpg" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 3; + + currentIndex = ctrl.next() + + expect(mocks.attachmentsPreviewService.fileId).to.be.equal(1) + + it "previous", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(2) + + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx.jpg" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 3; + + currentIndex = ctrl.previous() + + expect(mocks.attachmentsPreviewService.fileId).to.be.equal(1) + + it "previous infinite", () -> + attachment = Immutable.fromJS({ + file: { + description: 'desc', + is_deprecated: false + } + }) + + ctrl = $controller("AttachmentsPreview", { + $scope: scope + }) + + ctrl.getIndex = sinon.stub().returns(0) + + ctrl.attachments = Immutable.fromJS([ + { + file: { + id: 1, + name: "xx.jpg" + } + }, + { + file: { + id: 2, + name: "xx" + } + }, + { + file: { + id: 3, + name: "xx.jpg" + } + } + ]) + + mocks.attachmentsPreviewService.fileId = 1; + + currentIndex = ctrl.previous() + + expect(mocks.attachmentsPreviewService.fileId).to.be.equal(3) diff --git a/app/modules/components/attachments-preview/attachments-preview.directive.coffee b/app/modules/components/attachments-preview/attachments-preview.directive.coffee new file mode 100644 index 00000000..4e6b48cf --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.directive.coffee @@ -0,0 +1,48 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: attachments-preview.directive.coffee +### + +AttachmentPreviewLightboxDirective = (lightboxService, attachmentsPreviewService) -> + link = ($scope, el, attrs, ctrl) -> + $(document.body).on "keydown.image-preview", (e) -> + if attachmentsPreviewService.fileId + if e.keyCode == 39 + ctrl.next() + else if e.keyCode == 37 + ctrl.previous() + + $scope.$digest() + + $scope.$on '$destroy', () -> + $(document.body).off('.image-preview') + + return { + scope: {}, + controller: 'AttachmentsPreview', + templateUrl: 'components/attachments-preview/attachments-preview.html', + link: link, + controllerAs: "vm", + bindToController: { + attachments: "=" + } + } + +angular.module('taigaComponents').directive("tgAttachmentsPreview", [ + "lightboxService", + "tgAttachmentsPreviewService", + AttachmentPreviewLightboxDirective]) diff --git a/app/modules/components/attachments-preview/attachments-preview.jade b/app/modules/components/attachments-preview/attachments-preview.jade new file mode 100644 index 00000000..0be41189 --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.jade @@ -0,0 +1,21 @@ +.attachment-preview(ng-if="vm.attachments.size && vm.current") + tg-lightbox-close + + .attachment-preview-container + a.previous( + href="#", + ng-click="vm.previous()", + ng-if="vm.hasPagination()" + ) + tg-svg(svg-icon="icon-arrow-left") + + a(href="{{vm.current.get('url')}}", title="{{vm.current.get('description')}}", target="_blank", download="{{vm.current.get('name')}}") + tg-preload-image(preload-src="{{vm.getCurrent().get('url')}}") + img(ng-src="{{vm.getCurrent().get('url')}}") + + a.next( + href="#", + ng-click="vm.next()", + ng-if="vm.hasPagination()" + ) + tg-svg(svg-icon="icon-arrow-right") diff --git a/app/modules/components/attachments-preview/attachments-preview.service.coffee b/app/modules/components/attachments-preview/attachments-preview.service.coffee new file mode 100644 index 00000000..5739e8a0 --- /dev/null +++ b/app/modules/components/attachments-preview/attachments-preview.service.coffee @@ -0,0 +1,25 @@ +### +# Copyright (C) 2014-2016 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: attachments-preview.service.coffee +### + +class AttachmentsPreviewService extends taiga.Service + @.$inject = [] + + constructor: () -> + +angular.module("taigaComponents").service("tgAttachmentsPreviewService", AttachmentsPreviewService) diff --git a/app/partials/common/lightbox/lightbox-attachment-preview.jade b/app/partials/common/lightbox/lightbox-attachment-preview.jade deleted file mode 100644 index 84223a72..00000000 --- a/app/partials/common/lightbox/lightbox-attachment-preview.jade +++ /dev/null @@ -1,5 +0,0 @@ -.attachment-preview - tg-lightbox-close - - a(href="{{::file.get('url')}}", title="{{::file.get('description')}}", target="_blank", download="{{::file.get('name')}}") - img(src="{{::file.get('url')}}") diff --git a/e2e/helpers/detail-helper.js b/e2e/helpers/detail-helper.js index bad59b39..eb6c77f9 100644 --- a/e2e/helpers/detail-helper.js +++ b/e2e/helpers/detail-helper.js @@ -417,6 +417,18 @@ helper.attachment = function() { list: function() { $('.view-list').click(); }, + previewLightbox: function() { + return utils.lightbox.open($('tg-attachments-preview')); + }, + getPreviewSrc: function() { + return $('tg-attachments-preview img').getAttribute('src'); + }, + nextPreview: function() { + return $('tg-attachments-preview .next').click(); + }, + attachmentLinks: function() { + return $$('.e2e-attachment-link'); + } }; return obj; diff --git a/e2e/shared/detail.js b/e2e/shared/detail.js index ca61a479..43f2525f 100644 --- a/e2e/shared/detail.js +++ b/e2e/shared/detail.js @@ -315,6 +315,28 @@ shared.attachmentTesting = async function() { attachmentHelper.list(); + // Gallery images + var fileToUploadImage = commonUtil.uploadImagePath(); + + await attachmentHelper.upload(fileToUploadImage, 'testing image ' + date); + + await attachmentHelper.upload(fileToUpload, 'testing image ' + date); + + await attachmentHelper.upload(fileToUploadImage, 'testing image ' + date); + await browser.sleep(5000); + + attachmentHelper.attachmentLinks().last().click(); + + await attachmentHelper.previewLightbox(); + let previewSrc = await attachmentHelper.getPreviewSrc(); + + await attachmentHelper.nextPreview(); + + let previewSrc2 = await attachmentHelper.getPreviewSrc(); + + expect(previewSrc).not.to.be.equal(previewSrc2); + await lightbox.exit(); + // Deleting attachmentsLength = await attachmentHelper.countAttachments(); await attachmentHelper.deleteLastAttachment(); diff --git a/e2e/utils/lightbox.js b/e2e/utils/lightbox.js index 9b8fe6d8..2070f026 100644 --- a/e2e/utils/lightbox.js +++ b/e2e/utils/lightbox.js @@ -4,6 +4,10 @@ var lightbox = module.exports; var transition = 300; lightbox.exit = function(el) { + if (!el) { + el = $('.lightbox.open'); + } + if (typeof el === 'string' || el instanceof String) { el = $(el); }