mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Bulk Edit image upload
Image Upload Translations Squashme Squashme Squashme Code review tweaks
This commit is contained in:
@@ -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, */*"
|
||||
|
||||
@@ -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 .
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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, */*"
|
||||
|
||||
@@ -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"}
|
||||
198
app/assets/stylesheets/admin/modals.css.scss
Normal file
198
app/assets/stylesheets/admin/modals.css.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
app/controllers/spree/api/images_controller_decorator.rb
Normal file
15
app/controllers/spree/api/images_controller_decorator.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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' }
|
||||
|
||||
5
app/views/spree/api/images/update_product_image.v1.rabl
Normal file
5
app/views/spree/api/images/update_product_image.v1.rabl
Normal file
@@ -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) }
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user