Merge pull request #7530 from NeverResponse/image-variant-love-hate-relationship

[Image][Variant] Brought back variant images
This commit is contained in:
Paweł Jędrzejewski 2017-03-06 16:49:32 +01:00 committed by GitHub
commit fc5086838e
18 changed files with 364 additions and 39 deletions

View file

@ -0,0 +1,36 @@
<?php
namespace Sylius\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20170301135010 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('CREATE TABLE sylius_product_image_product_variants (image_id INT NOT NULL, variant_id INT NOT NULL, INDEX IDX_8FFDAE8D3DA5256D (image_id), INDEX IDX_8FFDAE8D3B69A9AF (variant_id), PRIMARY KEY(image_id, variant_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE sylius_product_image_product_variants ADD CONSTRAINT FK_8FFDAE8D3DA5256D FOREIGN KEY (image_id) REFERENCES sylius_product_image (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE sylius_product_image_product_variants ADD CONSTRAINT FK_8FFDAE8D3B69A9AF FOREIGN KEY (variant_id) REFERENCES sylius_product_variant (id) ON DELETE CASCADE');
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');
$this->addSql('DROP TABLE sylius_product_image_product_variants');
}
}

View file

@ -113,7 +113,7 @@ class StickerProductFixture extends AbstractFixture
'name' => 'Sticker size',
'code' => 'sticker_size',
'values' => [
'sticker_size-3' => '3"',
'sticker_size_3' => '3"',
'sticker_size_5' => '5"',
'sticker_size_7' => '7"',
],

View file

@ -65,6 +65,7 @@ final class ProductTypeExtension extends AbstractTypeExtension
])
->add('images', CollectionType::class, [
'entry_type' => ProductImageType::class,
'entry_options' => ['product' => $options['data']],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,

View file

@ -12,12 +12,56 @@
namespace Sylius\Bundle\CoreBundle\Form\Type\Product;
use Sylius\Bundle\CoreBundle\Form\Type\ImageType;
use Sylius\Bundle\ProductBundle\Form\Type\ProductVariantChoiceType;
use Sylius\Component\Core\Model\ProductInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Grzegorz Sadowski <grzegorz.sadowski@lakion.com>
*/
final class ProductImageType extends ImageType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
if (isset($options['product']) && $options['product'] instanceof ProductInterface) {
$builder
->add('productVariants', ProductVariantChoiceType::class, [
'label' => 'sylius.ui.product_variants',
'multiple' => true,
'expanded' => false,
'required' => false,
'product' => $options['product'],
])
;
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
parent::buildView($view, $form, $options);
$view->vars['product'] = $options['product'];
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefined('product');
$resolver->setAllowedTypes('product', ProductInterface::class);
}
/**
* {@inheritdoc}
*/

View file

@ -16,5 +16,15 @@
<many-to-one field="owner" target-entity="Sylius\Component\Product\Model\ProductInterface" inversed-by="images">
<join-column name="owner_id" referenced-column-name="id" nullable="false" on-delete="CASCADE"/>
</many-to-one>
<many-to-many field="productVariants" target-entity="Sylius\Component\Product\Model\ProductVariantInterface" orphan-removal="true">
<join-table name="sylius_product_image_product_variants">
<join-columns>
<join-column name="image_id" referenced-column-name="id" on-delete="CASCADE" />
</join-columns>
<inverse-join-columns>
<join-column name="variant_id" referenced-column-name="id" on-delete="CASCADE" />
</inverse-join-columns>
</join-table>
</many-to-many>
</mapped-superclass>
</doctrine-mapping>

View file

@ -11,11 +11,7 @@
-->
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<mapped-superclass name="Sylius\Component\Core\Model\ProductVariant" table="sylius_product_variant">
<field name="version" type="integer" version="true" />
@ -48,5 +44,4 @@
</cascade>
</one-to-many>
</mapped-superclass>
</doctrine-mapping>

View file

@ -0,0 +1,16 @@
Sylius\Component\Core\Model\ProductImage:
exclusion_policy: ALL
xml_root_name: product-image
properties:
id:
expose: true
type: integer
xml_attribute: true
groups: [Default, Detailed]
type:
expose: true
type: string
groups: [Default, Detailed]
owner:
expose: true
groups: [Detailed]

View file

@ -14,7 +14,6 @@ namespace Sylius\Bundle\ProductBundle\Form\Type;
use Sylius\Component\Product\Model\ProductInterface;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
@ -31,7 +30,7 @@ final class ProductVariantChoiceType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options['multiple']) {
$builder->addViewTransformer(new CollectionToArrayTransformer());
$builder->addViewTransformer(new CollectionToArrayTransformer(), true);
}
}
@ -46,7 +45,9 @@ final class ProductVariantChoiceType extends AbstractType
return $options['product']->getVariants();
},
'choice_value' => 'code',
'choice_label' => 'name',
'choice_label' => function ($variant) {
return $variant;
},
'choice_translation_domain' => false,
'multiple' => false,
'expanded' => true,

View file

@ -55,5 +55,6 @@
$('#sylius-billing-address').addressBook();
$(document).provinceField();
$(document).variantPrices();
$(document).variantImages();
});
})(jQuery);

View file

@ -0,0 +1,89 @@
/*
* This file is part of the Sylius package.
*
* (c) Paweł Jędrzejewski
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
(function ( $ ) {
'use strict';
$.fn.extend({
variantImages: function () {
if ($('[data-variant-options]').length > 0) {
handleProductOptionImages();
handleProductOptionChange();
} else if ($('[data-variant-code]').length > 0) {
handleProductVariantImages($('[name="sylius_add_to_cart[cartItem][variant]"]'));
handleProductVariantChange();
}
}
});
})( jQuery );
function handleProductOptionChange() {
$('[name*="sylius_add_to_cart[cartItem][variant]"]').on('change', function () {
handleProductOptionImages();
});
}
function handleProductVariantChange() {
$('[name="sylius_add_to_cart[cartItem][variant]"]').on('change', function () {
handleProductVariantImages($(this))
});
}
function handleProductOptionImages() {
var options = '';
$('#sylius-product-adding-to-cart select').each(function() {
options += $(this).find('option:selected').val() + ' ';
});
var imagesWithOptions = [];
var optionsArray = options.trim().split(' ');
$('[data-variant-options]').each(function () {
var imageOptions = $(this).attr('data-variant-options');
var imageHasOptions = optionsArray.every(function(option) {
return imageOptions.indexOf(option) > -1;
});
if (imageHasOptions) {
imagesWithOptions.push($(this).closest('div.ui.image'));
}
});
changeMainImage(imagesWithOptions.shift());
}
function handleProductVariantImages(element) {
var variantCode = $(element).attr('value');
var imagesWithVariantCode = [];
$('[data-variant-code*="'+ variantCode +'"]').each(function () {
imagesWithVariantCode.push($(this).closest('div.ui.image'));
});
changeMainImage(imagesWithVariantCode.shift());
}
function changeMainImage(newImageDiv) {
var mainImageLink = $('a.ui.fluid.image');
var mainImage = $('a.ui.fluid.image > img');
var newImage = $(newImageDiv).find('img');
var newImageLink = $(newImageDiv).find('a');
if (newImage.length == 0 && newImageLink.length == 0) {
mainImage.attr('src', $('div[data-product-image]').attr('data-product-image'));
newImageLink.attr('href', $('div[data-product-link]').attr('data-product-link'));
return;
}
mainImageLink.attr('href', newImageLink.attr('href'));
mainImage.attr('src', newImage.attr('data-large-thumbnail'));
}

View file

@ -0,0 +1,11 @@
<div class="sylius-image-variants">
{% if product.getVariantSelectionMethod() == 'match' %}
{% for variant in image.productVariants %}
<div data-variant-options="{% for option in variant.optionValues %}{{ option.code }} {% endfor %}"></div>
{% endfor %}
{% else %}
{% for variant in image.productVariants %}
<div data-variant-code="{{ variant.code }}"></div>
{% endfor %}
{% endif %}
</div>

View file

@ -11,6 +11,7 @@
{% set path = original_path %}
{% endif %}
<div data-product-image="{{ path }}" data-product-link="{{ original_path }}"></div>
<a href="{{ original_path }}" class="ui fluid image" data-lightbox="sylius-product-image">
<img src="{{ path }}" id="main-image" alt="" />
</a>
@ -19,9 +20,14 @@
<div class="ui small images">
{% for image in product.images %}
{% set path = image.path is not null ? image.path|imagine_filter('sylius_shop_product_small_thumbnail') : 'http://placehold.it/200x200' %}
<a href="{{ image.path|imagine_filter('sylius_shop_product_original') }}" class="ui image" data-lightbox="sylius-product-image">
<img src="{{ path }}" />
</a>
<div class="ui image">
{% if product.isConfigurable() and product.variants|length > 0 %}
{% include '@SyliusShop/Product/Show/_imageVariants.html.twig' %}
{% endif %}
<a href="{{ image.path|imagine_filter('sylius_shop_product_original') }}" data-lightbox="sylius-product-image">
<img src="{{ path }}" data-large-thumbnail="{{ image.path|imagine_filter('sylius_shop_product_large_thumbnail') }}" />
</a>
</div>
{% endfor %}
</div>
{% endif %}

View file

@ -49,24 +49,28 @@
{% endmacro %}
{% block sylius_product_image_widget %}
{% spaceless %}
<div class="ui upload box segment">
{{ form_row(form.type) }}
{% if form.vars.value.path|default(null) is null %}
<label for="{{ form.file.vars.id }}" class="ui icon labeled button"><i class="cloud upload icon"></i> {{ 'sylius.ui.choose_file'|trans }}</label>
{% else %}
<img class="ui small bordered image" src="{{ form.vars.value.path|imagine_filter('sylius_small') }}" alt="{{ form.vars.value.type }}" />
<label for="{{ form.file.vars.id }}" class="ui icon labeled button"><i class="cloud upload icon"></i> {{ 'sylius.ui.change_file'|trans }}</label>
{% endif %}
<label for="{{ form.file.vars.id }}" class="ui icon labeled button"><i class="cloud upload icon"></i> {{ 'sylius.ui.choose_file'|trans }}</label>
{% if form.vars.value.path|default(null) is not null %}
<img class="ui small bordered image" src="{{ form.vars.value.path|imagine_filter('sylius_small') }}" alt="{{ form.vars.value.type }}" />
{% endif %}
<div class="ui hidden element">
{{ form_widget(form.file) }}
</div>
<div class="ui element">
{{- form_errors(form.file) -}}
</div>
{% if product.id is not null and 0 != product.variants|length and not product.simple %}
{{ form_row(form.productVariants) }}
{% endif %}
</div>
{% endspaceless %}
{% endblock %}
{% block sylius_taxon_image_widget %}
{% spaceless %}
<div class="ui upload box segment">
{{ form_row(form.type) }}
{% if form.vars.value.path|default(null) is null %}
@ -82,4 +86,5 @@
{{- form_errors(form.file) -}}
</div>
</div>
{% endspaceless %}
{% endblock %}

View file

@ -11,9 +11,65 @@
namespace Sylius\Component\Core\Model;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* @author Grzegorz Sadowski <grzegorz.sadowski@lakion.com>
*/
class ProductImage extends Image implements ProductImageInterface
{
/**
* @var Collection|ProductVariantInterface[]
*/
protected $productVariants;
public function __construct()
{
parent::__construct();
$this->productVariants = new ArrayCollection();
}
/**
* {@inheritdoc}
*/
public function hasProductVariants()
{
return !$this->productVariants->isEmpty();
}
/**
* {@inheritdoc}
*/
public function getProductVariants()
{
return $this->productVariants;
}
/**
* {@inheritdoc}
*/
public function hasProductVariant(ProductVariantInterface $productVariant)
{
return $this->productVariants->contains($productVariant);
}
/**
* {@inheritdoc}
*/
public function addProductVariant(ProductVariantInterface $productVariant)
{
$this->productVariants->add($productVariant);
}
/**
* {@inheritdoc}
*/
public function removeProductVariant(ProductVariantInterface $productVariant)
{
if ($this->hasProductVariant($productVariant)) {
$this->productVariants->removeElement($productVariant);
}
}
}

View file

@ -11,9 +11,37 @@
namespace Sylius\Component\Core\Model;
use Doctrine\Common\Collections\Collection;
/**
* @author Saidul Islam <saidul.04@gmail.com>
*/
interface ProductImageInterface extends ImageInterface
{
/**
* @return bool
*/
public function hasProductVariants();
/**
* @return Collection|ProductVariantInterface[]
*/
public function getProductVariants();
/**
* @param ProductVariantInterface $productVariant
*/
public function addProductVariant(ProductVariantInterface $productVariant);
/**
* @param ProductVariantInterface $productVariant
*/
public function removeProductVariant(ProductVariantInterface $productVariant);
/**
* @param ProductVariantInterface $productVariant
*
* @return bool
*/
public function hasProductVariant(ProductVariantInterface $productVariant);
}

View file

@ -95,7 +95,7 @@ class ProductVariant extends BaseVariant implements ProductVariantInterface
*/
public function __toString()
{
$string = $this->getProduct()->getName();
$string = (string) $this->getProduct()->getName();
if (!$this->getOptionValues()->isEmpty()) {
$string .= '(';

View file

@ -11,10 +11,13 @@
namespace spec\Sylius\Component\Core\Model;
use Doctrine\Common\Collections\Collection;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\Image;
use Sylius\Component\Core\Model\ImageAwareInterface;
use Sylius\Component\Core\Model\ProductImage;
use Sylius\Component\Core\Model\ProductImageInterface;
use Sylius\Component\Core\Model\ProductVariantInterface;
/**
* @author Grzegorz Sadowski <grzegorz.sadowski@lakion.com>
@ -26,6 +29,11 @@ final class ProductImageSpec extends ObjectBehavior
$this->shouldHaveType(ProductImage::class);
}
function it_implements_product_image_interface()
{
$this->shouldImplement(ProductImageInterface::class);
}
function it_extends_an_image()
{
$this->shouldHaveType(Image::class);
@ -76,4 +84,37 @@ final class ProductImageSpec extends ObjectBehavior
$this->setOwner($owner);
$this->getOwner()->shouldReturn($owner);
}
function it_initializes_product_variants_collection_by_default()
{
$this->getProductVariants()->shouldHaveType(Collection::class);
}
function it_does_not_have_any_product_variants_by_default()
{
$this->hasProductVariants()->shouldReturn(false);
}
function it_adds_product_variants(ProductVariantInterface $firstVariant, ProductVariantInterface $secondVariant)
{
$this->addProductVariant($firstVariant);
$this->hasProductVariant($firstVariant)->shouldReturn(true);
$this->hasProductVariants()->shouldReturn(true);
$this->hasProductVariant($secondVariant)->shouldReturn(false);
}
function it_removes_product_variants(ProductVariantInterface $firstVariant, ProductVariantInterface $secondVariant)
{
$this->addProductVariant($firstVariant);
$this->addProductVariant($secondVariant);
$this->removeProductVariant($firstVariant);
$this->hasProductVariant($firstVariant)->shouldReturn(false);
$this->hasProductVariants()->shouldReturn(true);
$this->hasProductVariant($secondVariant)->shouldReturn(true);
}
}

View file

@ -12,7 +12,6 @@
namespace spec\Sylius\Component\Core\Model;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use PhpSpec\ObjectBehavior;
use Sylius\Component\Core\Model\ChannelInterface;
use Sylius\Component\Core\Model\ChannelPricingInterface;
@ -192,7 +191,10 @@ final class ProductVariantSpec extends ObjectBehavior
$this->addChannelPricing($firstChannelPricing);
$this->addChannelPricing($secondChannelPricing);
$this->getChannelPricings()->shouldBeSameAs(new ArrayCollection(['WEB' => $firstChannelPricing, 'MOB' => $secondChannelPricing]));
$this->getChannelPricings()->shouldBeLike(new ArrayCollection([
'WEB' => $firstChannelPricing->getWrappedObject(),
'MOB' => $secondChannelPricing->getWrappedObject(),
]));
}
function it_checks_if_contains_channel_pricing_for_given_channel(
@ -238,21 +240,4 @@ final class ProductVariantSpec extends ObjectBehavior
$this->setShippingRequired(false);
$this->isShippingRequired()->shouldReturn(false);
}
public function getMatchers()
{
return [
'beSameAs' => function ($subject, $key) {
if (!$subject instanceof Collection || !$key instanceof Collection) {
return false;
}
foreach ($subject as $code => $value) {
if ($value !== $key->get($code)->getWrappedObject()) {
return false;
}
}
return true;
},
];
}
}