From 7d27d1325e4cb390b1f76faa40bf16a8a2652676 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley Date: Wed, 22 Feb 2017 00:09:54 +0000 Subject: [PATCH] Bulk Edit image upload Image Upload Translations Squashme Squashme Squashme Code review tweaks --- .../javascripts/admin/admin_ofn.js.coffee | 13 +- app/assets/javascripts/admin/all.js | 3 +- .../product_image_controller.js.coffee | 6 + .../products/directives/image_modal.js.coffee | 6 + .../services/product_image_service.js.coffee | 15 ++ .../javascripts/darkswarm/darkswarm.js.coffee | 5 +- .../admin/modals/image_upload.html.haml | 10 + app/assets/stylesheets/admin/modals.css.scss | 198 ++++++++++++++++++ .../stylesheets/admin/products.css.scss | 45 ++++ .../spree/api/images_controller_decorator.rb | 15 ++ .../api/admin/product_serializer.rb | 10 +- .../bulk_edit/_products_product.html.haml | 3 +- .../api/images/update_product_image.v1.rabl | 5 + config/locales/en.yml | 9 + config/routes.rb | 1 + 15 files changed, 338 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee create mode 100644 app/assets/javascripts/admin/products/directives/image_modal.js.coffee create mode 100644 app/assets/javascripts/admin/products/services/product_image_service.js.coffee create mode 100644 app/assets/javascripts/templates/admin/modals/image_upload.html.haml create mode 100644 app/assets/stylesheets/admin/modals.css.scss create mode 100644 app/controllers/spree/api/images_controller_decorator.rb create mode 100644 app/views/spree/api/images/update_product_image.v1.rabl diff --git a/app/assets/javascripts/admin/admin_ofn.js.coffee b/app/assets/javascripts/admin/admin_ofn.js.coffee index d617722175..1a5e1c79bf 100644 --- a/app/assets/javascripts/admin/admin_ofn.js.coffee +++ b/app/assets/javascripts/admin/admin_ofn.js.coffee @@ -1,3 +1,14 @@ -angular.module("ofn.admin", ["ngResource", "ngAnimate", "admin.utils", "admin.indexUtils", "admin.dropdown", "admin.products", "admin.taxons", "infinite-scroll"]).config ($httpProvider) -> +angular.module("ofn.admin", [ + "ngResource", + "mm.foundation", + "angularFileUpload", + "ngAnimate", + "admin.utils", + "admin.indexUtils", + "admin.dropdown", + "admin.products", + "admin.taxons", + "infinite-scroll" +]).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content") $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 366f7344e7..688494f870 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -51,7 +51,6 @@ //= require textAngular.min.js //= require i18n/translations //= require darkswarm/i18n.translate.js -// //= require moment //= require moment/en-gb.js //= require moment/es.js @@ -60,5 +59,7 @@ //= require moment/nb.js //= require moment/pt-br.js //= require moment/sv.js +//= require ../shared/mm-foundation-tpls-0.8.0.min.js +//= require angularjs-file-upload //= require_tree . diff --git a/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee b/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee new file mode 100644 index 0000000000..1fb2ceea06 --- /dev/null +++ b/app/assets/javascripts/admin/products/controllers/product_image_controller.js.coffee @@ -0,0 +1,6 @@ +angular.module("ofn.admin").controller "ProductImageCtrl", ($scope, ProductImageService) -> + $scope.imageUploader = ProductImageService.imageUploader + $scope.imagePreview = ProductImageService.imagePreview + + $scope.$watch 'product.image_url', (newValue) -> + $scope.imagePreview = newValue if newValue diff --git a/app/assets/javascripts/admin/products/directives/image_modal.js.coffee b/app/assets/javascripts/admin/products/directives/image_modal.js.coffee new file mode 100644 index 0000000000..526c14bf2a --- /dev/null +++ b/app/assets/javascripts/admin/products/directives/image_modal.js.coffee @@ -0,0 +1,6 @@ +angular.module("ofn.admin").directive "imageModal", ($modal, ProductImageService) -> + restrict: 'C' + link: (scope, elem, attrs, ctrl) -> + elem.on "click", (ev) => + scope.uploadModal = $modal.open(templateUrl: 'admin/modals/image_upload.html', controller: ctrl, scope: scope, windowClass: 'product-image-upload') + ProductImageService.configure(scope.product) diff --git a/app/assets/javascripts/admin/products/services/product_image_service.js.coffee b/app/assets/javascripts/admin/products/services/product_image_service.js.coffee new file mode 100644 index 0000000000..8bad8746ec --- /dev/null +++ b/app/assets/javascripts/admin/products/services/product_image_service.js.coffee @@ -0,0 +1,15 @@ +angular.module("ofn.admin").factory "ProductImageService", (FileUploader, SpreeApiKey) -> + new class ProductImageService + imagePreview: null + + imageUploader: new FileUploader + headers: + 'X-Spree-Token': SpreeApiKey + autoUpload: true + + configure: (product) => + @imageUploader.url = "/api/images/product/#{product.id}" + @imagePreview = product.image_url + @imageUploader.onSuccessItem = (image, response) => + product.thumb_url = response.thumb_url + product.image_url = response.image_url diff --git a/app/assets/javascripts/darkswarm/darkswarm.js.coffee b/app/assets/javascripts/darkswarm/darkswarm.js.coffee index 6d75916894..01a93a3dd1 100644 --- a/app/assets/javascripts/darkswarm/darkswarm.js.coffee +++ b/app/assets/javascripts/darkswarm/darkswarm.js.coffee @@ -1,4 +1,5 @@ -window.Darkswarm = angular.module("Darkswarm", ["ngResource", +window.Darkswarm = angular.module("Darkswarm", [ + 'ngResource', 'mm.foundation', 'LocalStorageModule', 'infinite-scroll', @@ -10,7 +11,7 @@ window.Darkswarm = angular.module("Darkswarm", ["ngResource", 'duScroll', 'angularFileUpload', 'angularSlideables' - ]).config ($httpProvider, $tooltipProvider, $locationProvider, $anchorScrollProvider) -> +]).config ($httpProvider, $tooltipProvider, $locationProvider, $anchorScrollProvider) -> $httpProvider.defaults.headers['common']['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content') $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest' $httpProvider.defaults.headers.common.Accept = "application/json, text/javascript, */*" diff --git a/app/assets/javascripts/templates/admin/modals/image_upload.html.haml b/app/assets/javascripts/templates/admin/modals/image_upload.html.haml new file mode 100644 index 0000000000..76a96805ef --- /dev/null +++ b/app/assets/javascripts/templates/admin/modals/image_upload.html.haml @@ -0,0 +1,10 @@ +%a.close-reveal-modal{"ng-click" => "$close()"} + %i.fa.fa-times-circle{'aria-hidden' => "true"} + +%form#image_upload{ name: 'form', novalidate: true, enctype: 'multipart/form-data', multipart: true, ng: { controller: "ProductImageCtrl" } } + %div.image-preview + %img.spinner{ src: "/assets/spinning-circles.svg", ng: { hide: "!imageUploader.isUploading" }} + %img.preview{ng: {src: "{{imagePreview}}", class: "{'faded': imageUploader.isUploading}"}} + + %label{for: 'image-upload', class: 'button'} #{t('admin.products.bulk_edit.upload_an_image')} + %input#image-upload{hidden: true, type: 'file', 'nv-file-select' => true, uploader: "imageUploader"} diff --git a/app/assets/stylesheets/admin/modals.css.scss b/app/assets/stylesheets/admin/modals.css.scss new file mode 100644 index 0000000000..a543f15ee3 --- /dev/null +++ b/app/assets/stylesheets/admin/modals.css.scss @@ -0,0 +1,198 @@ +.reveal-modal-bg { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: black; + z-index: 1004; + display: none; + left: 0; +} + +.reveal-modal, dialog { + visibility: hidden; + display: none; + position: absolute; + z-index: 1005; + width: 100vw; + top: 0; + border-radius: 0.4em; + border: 0px none; + left: 0; + background-color: white; + padding: 1.25rem; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); + padding: 1.875rem; +} + +.reveal-modal .column, dialog .column, .reveal-modal .columns, dialog .columns { + min-width: 0; +} + +.reveal-modal > :first-child, dialog > :first-child { + margin-top: 0; +} + +.reveal-modal > :last-child, dialog > :last-child { + margin-bottom: 0; +} + +@media only screen and (min-width: 40.063em) { + .reveal-modal, dialog { + width: 80%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } +} + +.reveal-modal.radius, dialog.radius { + border-radius: 3px; +} + +.reveal-modal.round, dialog.round { + border-radius: 1000px; +} + +.reveal-modal.collapse, dialog.collapse { + padding: 0; +} + +.reveal-modal.full, dialog.full { + top: 0; + left: 0; + height: 100%; + height: 100vh; + min-height: 100vh; + max-width: none !important; + margin-left: 0 !important; +} + +.reveal-modal .close-reveal-modal, dialog .close-reveal-modal { + font-size: 2.5rem; + line-height: 1; + position: absolute; + top: 0.625rem; + right: 1.375rem; + color: #aaaaaa; + font-weight: bold; + cursor: pointer; +} + +dialog::backdrop, dialog + .backdrop { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background: black; + background: rgba(0, 0, 0, 0.45); + z-index: auto; + display: none; + left: 0; +} + +dialog[open] { + display: block; +} + +@media print { + dialog, .reveal-modal, dialog { + display: none; + background: white !important; + } +} + +// ANIMATION CLASSES + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.reveal-modal.fade { + -webkit-transition: -webkit-transform 0.2s ease-out; + -moz-transition: -moz-transform 0.2s ease-out; + -o-transition: -o-transform 0.2s ease-out; + transition: transform 0.2s ease-out; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + transform: translate(0, -25%); +} + +.reveal-modal.in { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} + +.reveal-modal-bg.fade { + filter: alpha(opacity = 0); + opacity: 0; +} + +.reveal-modal-bg.in { + filter: alpha(opacity = 50); + opacity: 0.5; +} + +@media only screen and (max-width: 40em) { + .reveal-modal, dialog { + min-height: 100vh; + } +} + +@media only screen and (min-width: 40.063em) { + .reveal-modal, dialog { + top: 6.25rem; + } + .reveal-modal.tiny, dialog.tiny { + width: 30%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.small, dialog.small { + width: 40%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.medium, dialog.medium { + width: 60%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.large, dialog.large { + width: 70%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.xlarge, dialog.xlarge { + width: 95%; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } + .reveal-modal.full, dialog.full { + width: 100vw; + max-width: 62.5rem; + left: 0; + right: 0; + margin: 0 auto; + } +} diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index b3fbf00886..e7e780ecf3 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -66,8 +66,10 @@ table#listing_products.bulk { td.image { padding: 10px; + text-align: center; img { border: 1px solid transparent; + border-radius: 0.4em; } img:hover { opacity: 0.8; @@ -92,3 +94,46 @@ table#listing_products.bulk { } } } + +.reveal-modal.product-image-upload { + width: 300px; + a.close-reveal-modal { + font-size: 23px; + color: #de6060; + right: 0.45rem; + top: 0.35rem; + :hover { + color: #bf4545; + } + } + div.image-preview { + //float: left; + } + label { + margin-top: 20px; + } +} + +form#image_upload { + text-align: center; + .spinner { + width: 160px; + height: 65%; + position: absolute; + //background-color: rgba(255, 255, 255, 0.75); + padding: 32px; + margin: 0px -80px; + left: 50%; + z-index: 100; + } + .preview { + width: 240px; + } + .faded { + opacity: 0.25; + } + .button:hover { + cursor: pointer; + } +} + diff --git a/app/controllers/spree/api/images_controller_decorator.rb b/app/controllers/spree/api/images_controller_decorator.rb new file mode 100644 index 0000000000..3f5f121eaa --- /dev/null +++ b/app/controllers/spree/api/images_controller_decorator.rb @@ -0,0 +1,15 @@ +Spree::Api::ImagesController.class_eval do + def update_product_image + @product = Spree::Product.find(params[:product_id]) + authorize! :update, @product + + if @product.images.first.nil? + @image = Spree::Image.create(attachment: params[:file], viewable_id: @product.master.id, viewable_type: 'Spree::Variant') + respond_with(@image, status: 201) + else + @image = @product.images.first + @image.update_attributes(attachment: params[:file]) + respond_with(@image, status: 200) + end + end +end diff --git a/app/serializers/api/admin/product_serializer.rb b/app/serializers/api/admin/product_serializer.rb index fb340a61e7..dc88a03448 100644 --- a/app/serializers/api/admin/product_serializer.rb +++ b/app/serializers/api/admin/product_serializer.rb @@ -1,13 +1,21 @@ class Api::Admin::ProductSerializer < ActiveModel::Serializer attributes :id, :name, :sku, :variant_unit, :variant_unit_scale, :variant_unit_name, :on_demand, :inherits_properties - attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id + attributes :on_hand, :price, :available_on, :permalink_live, :tax_category_id, :image_url, :thumb_url has_one :supplier, key: :producer_id, embed: :id has_one :primary_taxon, key: :category_id, embed: :id has_many :variants, key: :variants, serializer: Api::Admin::VariantSerializer # embed: ids has_one :master, serializer: Api::Admin::VariantSerializer + def image_url + object.images.present? ? object.images.first.attachment.url(:product) : "/assets/noimage/product.png" + end + + def thumb_url + object.images.present? ? object.images.first.attachment.url(:mini) : "/assets/noimage/mini.png" + end + def on_hand object.on_hand.nil? ? 0 : object.on_hand.to_f.finite? ? object.on_hand : I18n.t(:on_demand) end diff --git a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml index 3134fd00dd..e947f2eba2 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_product.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_product.html.haml @@ -3,7 +3,8 @@ %a{ 'ofn-toggle-variants' => 'true', :class => "view-variants", 'ng-show' => 'hasVariants(product)' } %a{ :class => "add-variant icon-plus-sign", 'ng-click' => "addVariant(product)", 'ng-show' => "!hasVariants(product) && hasUnit(product)" } %td.image{ 'ng-show' => 'columns.image.visible' } - %img{'ng-src' => '{{ product.image_url }}'} + %a{class: 'image-modal'} + %img{'ng-src' => '{{ product.thumb_url }}'} %td.producer{ 'ng-show' => 'columns.producer.visible' } %select.select2.fullwidth{ 'ng-model' => 'product.producer_id', :name => 'producer_id', 'ofn-track-product' => 'producer_id', 'ng-options' => 'producer.id as producer.name for producer in producers' } %td.sku{ 'ng-show' => 'columns.sku.visible' } diff --git a/app/views/spree/api/images/update_product_image.v1.rabl b/app/views/spree/api/images/update_product_image.v1.rabl new file mode 100644 index 0000000000..c9e62475dc --- /dev/null +++ b/app/views/spree/api/images/update_product_image.v1.rabl @@ -0,0 +1,5 @@ +object @image +attributes(*image_attributes) +attributes :viewable_type, :viewable_id +node( :thumb_url ) { @product.images.first.attachment.url(:mini) } +node( :image_url ) { @product.images.first.attachment.url(:product) } diff --git a/config/locales/en.yml b/config/locales/en.yml index 213ff26d14..c472e7f860 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -441,6 +441,15 @@ en: products: unit_name_placeholder: 'eg. bunches' + bulk_edit: + unit: Unit + display_as: Display As + category: Category + tax_category: Tax Category + inherits_properties?: Inherits Properties? + available_on: Available On + av_on: "Av. On" + upload_an_image: Upload an image properties: property_name: Property Name inherited_property: Inherited Property diff --git a/config/routes.rb b/config/routes.rb index 85276ad0b4..358daf5dcb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -286,6 +286,7 @@ Spree::Core::Engine.routes.prepend do get :managed, on: :collection end + post '/images/product/:product_id', to: 'images#update_product_image' end namespace :admin do