diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..93045150 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.1.0 diff --git a/app/assets/javascripts/init.js b/app/assets/javascripts/init.js index 23422cee..6996b0e0 100644 --- a/app/assets/javascripts/init.js +++ b/app/assets/javascripts/init.js @@ -45,6 +45,8 @@ $('#template_flow button.preview').previewTemplate(); + $('#images_flow section.image').imageActions(); + $('header.application h1 li:last-of-type').editApplicationName(); var enableNewItem = function(addedItem) { diff --git a/app/assets/javascripts/jquery.image_actions.js b/app/assets/javascripts/jquery.image_actions.js index 2648ba37..822bcb04 100644 --- a/app/assets/javascripts/jquery.image_actions.js +++ b/app/assets/javascripts/jquery.image_actions.js @@ -1,22 +1,61 @@ (function($) { - $.PMX.MultiImageDestroyer = function(el, options) { + $.PMX.WatchImageSelections = function($el, options) { var base = this; - base.$el = $(el); - base.xhr = null; + base.$el = $el; base.defaultOptions = { + allSelector: 'input#all', + observerSelector: 'input[type=checkbox]', + submitSelector: 'button[type=submit]', + deleteCheckbox: '.images input[type=checkbox]' }; base.init = function () { base.options = $.extend({}, base.defaultOptions, options); + base.bindEvents(); }; - }; - $.fn.imageActions = function(options) { - return this.each(function() { - (new $.PMX.MultiImageDestroyer(this, options)).init(); - }); + base.bindEvents = function() { + base.$el.on('click', base.options.submitSelector, base.submitForm); + base.$el.on('change', base.options.allSelector, base.toggleCheckboxes); + base.$el.on('change', base.options.observerSelector, base.submitState); + }; + + base.submitForm = function(e) { + var submitButton = $(e.currentTarget); + + if (submitButton.hasClass('disabled')) { + e.preventDefault(); + } + }; + + base.submitState = function(e) { + var checked = base.$el.find(base.options.deleteCheckbox).filter(':checked'), + submitButton = base.$el.find(base.options.submitSelector); + + if (checked.length > 0) { + submitButton.removeClass('disabled'); + } else { + submitButton.addClass('disabled'); + } }; + base.toggleCheckboxes = function(e) { + var $target = $(e.currentTarget); + + if($target.is(':checked')) { + base.$el.find(base.options.deleteCheckbox).prop('checked', true); + } else { + base.$el.find(base.options.deleteCheckbox).prop('checked', false); + } + }; + }; + + $.fn.imageActions = function(options) { + return this.each(function() { + (new $.PMX.WatchImageSelections($(this), options)).init(); + }); + }; + })(jQuery); diff --git a/app/assets/stylesheets/panamax/images.css.scss b/app/assets/stylesheets/panamax/images.css.scss index c4ad78d5..dd04ffdf 100644 --- a/app/assets/stylesheets/panamax/images.css.scss +++ b/app/assets/stylesheets/panamax/images.css.scss @@ -7,25 +7,54 @@ } } -#images_flow .submit-button { - margin-top: 20px; - width: 165px; - position: relative; - padding-left: 25px; - text-align: left; - text-decoration: none; - float: right; - - &::after { - @include icon-white; - @extend .icon-x; - content: ''; - position: absolute; - left: 9px; - top: 14px; - display: block; - height: 10px; - width: 10px; +#images_flow { + .select-all { + margin-top: 30px; + float: right; + height: 36px; + width: 110px; + + .label { + float: left; + color: $grey; + padding: 5px 5px; + } + + .styled-check { + float: left; + } + } + + .submit-button { + margin-top: 20px; + width: 165px; + position: relative; + padding-left: 25px; + text-align: left; + text-decoration: none; + float: right; + + &::after { + @include icon-white; + @extend .icon-x; + content: ''; + position: absolute; + left: 9px; + top: 14px; + display: block; + height: 10px; + width: 10px; + } + } + + .disabled { + background-color: $grey; + + &:hover { + @include box-shadow(0, 0, 0, 0); + border: none; + cursor: default; + } } } diff --git a/app/assets/stylesheets/panamax/service_details.scss b/app/assets/stylesheets/panamax/service_details.scss index 2028b9db..d8cd61d9 100644 --- a/app/assets/stylesheets/panamax/service_details.scss +++ b/app/assets/stylesheets/panamax/service_details.scss @@ -5,6 +5,12 @@ .service-details { padding-bottom: 20px; + + a.button-add { + margin: 10px 0; + padding-left: 40px; + padding-right: 20px; + } } .service-help-icon { diff --git a/app/controllers/images_controller.rb b/app/controllers/images_controller.rb index 220b18f2..db08afc0 100644 --- a/app/controllers/images_controller.rb +++ b/app/controllers/images_controller.rb @@ -19,7 +19,7 @@ def destroy_multiple if params[:delete] count = params[:delete].size params[:delete].keys.each do |id| - image = LocalImage.find(id).destroy + LocalImage.find(id).destroy end flash[:notice] = "#{count} #{'image'.pluralize(count)} successfully removed" end diff --git a/app/views/images/index.html.haml b/app/views/images/index.html.haml index 1614ef70..25be7d03 100644 --- a/app/views/images/index.html.haml +++ b/app/views/images/index.html.haml @@ -7,7 +7,13 @@ - unless @images.blank? = form_tag destroy_multiple_images_path, method: :delete do %section.image - = button_tag 'Remove Selected', type: 'submit', class: 'button-negative submit-button' + = button_tag 'Remove Selected', type: 'submit', class: 'button-negative submit-button disabled' + .select-all + .label + Select All + .styled-check + = check_box_tag 'all', 1 + %label{ :for => 'all' } %ul.images - @images.each do |image| %li @@ -27,4 +33,4 @@ = check_box_tag "delete[#{image.id}]", 1 %label{ :for => "delete_#{image.id}" } %section - = button_tag 'Remove Selected', type: 'submit', class: 'button-negative submit-button' + = button_tag 'Remove Selected', type: 'submit', class: 'button-negative submit-button disabled' diff --git a/spec/controllers/images_controller_spec.rb b/spec/controllers/images_controller_spec.rb index 19adbbb5..99f031be 100644 --- a/spec/controllers/images_controller_spec.rb +++ b/spec/controllers/images_controller_spec.rb @@ -84,5 +84,22 @@ expect(flash[:notice]).to eq '2 images successfully removed' end end + + context 'when an error occurs' do + + before do + LocalImage.stub(:find).and_raise(StandardError, 'oops') + end + + it 'redirects the user to the image page' do + delete :destroy_multiple, delete: { 'key_1' => 1, 'key_2' => 2 } + expect(response).to redirect_to images_url + end + + it 'flashes the error message' do + delete :destroy_multiple, delete: { 'key_1' => 1, 'key_2' => 2 } + expect(flash[:alert]).to eq 'oops' + end + end end end diff --git a/spec/javascripts/fixtures/images.html.haml b/spec/javascripts/fixtures/images.html.haml new file mode 100644 index 00000000..e2247068 --- /dev/null +++ b/spec/javascripts/fixtures/images.html.haml @@ -0,0 +1,15 @@ +#images_flow + %section.image + = button_tag 'Remove Selected', type: 'submit', class: 'button-negative submit-button disabled' + .select-all + .label + Select All + .styled-check + = check_box_tag 'all', 1 + %label{ :for => 'all' } + %ul.images + %li + .actions + .styled-check + = check_box_tag "delete[1]", 1 + %label{ :for => "delete_1" } diff --git a/spec/javascripts/jquery.image_actions_spec.js b/spec/javascripts/jquery.image_actions_spec.js new file mode 100644 index 00000000..cd588576 --- /dev/null +++ b/spec/javascripts/jquery.image_actions_spec.js @@ -0,0 +1,87 @@ +describe('$.fn.imageActions', function() { + var subject; + + beforeEach(function() { + fixture.load('images.html'); + subject = new $.PMX.WatchImageSelections($('#images_flow section.image')); + subject.init(); + }); + + describe('clicking "Select All" ', function() { + describe('when not checked', function() { + it('check all of the delete image checkboxes', function() { + var selectAll = $('input#all'); + selectAll.prop('checked', false); + + selectAll.click(); + expect($('.images input[type=checkbox]').filter(':checked').length).toEqual(1); + }); + + it('enables the submit button', function() { + var selectAll = $('input#all'); + selectAll.prop('checked', false); + + selectAll.click(); + expect($('button[type=submit]').hasClass('disabled')).toBeFalsy(); + }); + }); + + describe('when checked', function() { + it('uncheck all of the delete image checkboxes', function() { + var selectAll = $('input#all'), + allBoxes = $('.images input[type=checkbox]'); + + selectAll.prop('checked', true); + allBoxes.prop('checked', true); + + selectAll.click(); + expect($('.images input[type=checkbox]').filter(':checked').length).toEqual(0); + }); + + it('disables the submit button', function() { + var selectAll = $('input#all'); + selectAll.prop('checked', true); + + selectAll.click(); + expect($('button[type=submit]').first().hasClass('disabled')).toBeTruthy(); + }); + }); + }); + + describe('clicking an image checkbox', function() { + describe('when not checked', function() { + it('enables the submit button', function() { + var checkbox = $('.images input[type=checkbox]').first(), + submitButton = $('button[type=submit]'); + + checkbox.prop('checked', false); + + checkbox.click(); + expect(submitButton.hasClass('disabled')).toBeFalsy() + }); + }); + + describe('when checked', function() { + it('disables the submit button', function() { + var checkbox = $('.images input[type=checkbox]').first(), + submitButton = $('button[type=submit]'); + + checkbox.prop('checked', true); + + checkbox.click(); + expect(submitButton.hasClass('disabled')).toBeTruthy() + }); + }); + }); + + describe('clicking "Remove Selected"', function() { + it ('prevents default when disabled', function() { + var submitButton = $('button[type=submit]'), + clickEvent = $.Event('click'); + + submitButton.addClass('disabled'); + submitButton.trigger(clickEvent); + expect(clickEvent.isDefaultPrevented()).toBeTruthy(); + }); + }) +}); diff --git a/spec/support/fixtures/images_representation.json b/spec/support/fixtures/images_representation.json index a0087b96..e42d542e 100644 --- a/spec/support/fixtures/images_representation.json +++ b/spec/support/fixtures/images_representation.json @@ -3,13 +3,15 @@ "id": "65af5f136e080ca65af2c35d2b5d11730913484f0ada631caafef44d3cfc62c6", "tags": [ "socialize_api:latest" - ] + ], + "virtual_size": 100 }, { "id": "156edca13ead93fe108e3432294a10d49771822aa802baf8435661ca8f1c0c26", "tags": [ "centurylinklabs/socialize-api:latest" - ] + ], + "virtual_size": 100 } ]