[AddressingBundle] The collection form type uses the new CollectionExtension

This commit is contained in:
Arnaud Langlade 2014-12-04 14:00:42 +01:00
parent 39067ead21
commit 68c2def782
16 changed files with 422 additions and 127 deletions

View file

@ -26,6 +26,7 @@ default:
contexts:
- Behat\MinkExtension\Context\MinkContext
- Sylius\Bundle\AddressingBundle\Behat\AddressingContext
- Sylius\Bundle\AddressingBundle\Behat\FormContext
- Sylius\Bundle\CoreBundle\Behat\CoreContext
- Sylius\Bundle\ResourceBundle\Behat\BaseContext
- Sylius\Bundle\WebBundle\Behat\WebContext

View file

@ -69,11 +69,13 @@ Feature: Countries and provinces
When I fill in "Name" with "Poland"
And I fill in "ISO name" with "PL"
And I click "Add province"
And I fill in province name with "Łódź"
And I click "Add province"
And I fill in the 1st province with "Lubusz"
And I fill in the 2nd province with "Łódź"
And I press "Create"
Then I should be on the page of country "Poland"
And I should see "Country has been successfully created."
Then I should see "Country has been successfully created."
And "Łódź" should appear on the page
And "Lubusz" should appear on the page
Scenario: Created countries appear in the list
Given I created country "Poland"
@ -100,6 +102,17 @@ Feature: Countries and provinces
Then I should be on the page of country "Russia"
And "Russia" should appear on the page
@javascript
Scenario: Updating the country and removing province
Given I am editing country "Ukraine"
When I fill in "Name" with "Russia"
And I fill in "ISO name" with "RU"
And I remove all the provinces
And I press "Save changes"
Then I should see "Country has been successfully updated."
And "Russia" should appear on the page
But I should not see "Provinces"
@javascript
Scenario: Deleting country via the list
Given I am on the country index page

View file

@ -44,18 +44,33 @@ Feature: Zones
Then I should still be on the zone creation page
And I should see "Please add at least 1 zone member."
@javascript
Scenario: Updating the collection form type prototype
Given I am on the zone creation page
And I click "Add member"
And "Country" should appear on the page
And I select "Province" from "Type"
And I click "Add member"
And "Province" should appear on the page
And I select "Zone" from "Type"
And I click "Add member"
And "Zone" should appear on the page
@javascript
Scenario: Creating new zone built from countries
Given I am on the zone creation page
And I fill in "Name" with "EU"
And I select "Country" from "Type"
And I click "Add member"
And I select "Estonia" from "Country"
And I select "shipping" from "Scope"
And I click "Add member"
And I click "Add member"
And I select "Estonia" from the 1st country
And I select "France" from the 2nd country
When I press "Create"
Then I should be on the page of zone "EU"
And I should see "Zone has been successfully created."
And "Estonia" should appear on the page
And "France" should appear on the page
And "shipping" should appear on the page
Scenario: Created zones appear in the list
@ -74,7 +89,17 @@ Feature: Zones
When I click "edit" near "USA GMT-8"
Then I should be editing zone "USA GMT-8"
Scenario: Updating the zone
@javascript
Scenario: Updating the zone
Given I am editing zone "USA GMT-8"
When I fill in "Name" with "USA GMT-9"
And I remove the first country
And I press "Save changes"
Then I should be on the page of zone "USA GMT-9"
And I should see "Zone has been successfully updated."
And "Washington" should not appear on the page
Scenario: Updating the zone
Given I am editing zone "USA GMT-8"
When I fill in "Name" with "USA GMT-9"
And I press "Save changes"

View file

@ -0,0 +1,60 @@
<?php
/*
* 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.
*/
namespace Sylius\Bundle\AddressingBundle\Behat;
use Sylius\Bundle\ResourceBundle\Behat\FormContext as BaseFormContext;
/**
* @author Arnaud Langlade <arn0d.dev@gmail.com>
*/
class FormContext extends BaseFormContext
{
/**
* @Given /^I remove all the provinces$/
*/
public function iRemoveAProvince()
{
$items = count($this->getSession()->getPage()->findAll(
'xpath',
'//div[@id="sylius_country_provinces"]//div[@data-form-collection="item"]'
));
while (0 !== $items) {
$this->deleteCollectionItem($items);
$items--;
}
}
/**
* @Given /^I fill in the (\d+)(st|nd|th) province with "([^""]*)"$/
*/
public function fillProvinceName($position, $fake, $value)
{
$this->fillInField('sylius_country[provinces][' . ($position - 1) . '][name]', $value);
}
/**
* @Given /^I select "([^""]*)" from the (\d+)(st|nd|th) country$/
*/
public function fillCountryMember($value, $position, $fake)
{
$this->fillInField('sylius_country[members][' . ($position - 1) . '][country]', $value);
}
/**
* @Given /^I remove the first country$/
*/
public function iRemoveTheFirstCountryMember()
{
$this->deleteCollectionItem(1);
}
}

View file

@ -49,8 +49,13 @@ class ResizeZoneMemberCollectionListener extends ResizeFormListener
*/
protected $classMap = array();
public function __construct(FormFactoryInterface $factory, array $prototypes, array $options = array(), $allowAdd = false, $allowDelete = false)
{
public function __construct(
FormFactoryInterface $factory,
array $prototypes,
array $options = array(),
$allowAdd = false,
$allowDelete = false
) {
$this->factory = $factory;
foreach ($prototypes as $prototype) {
@ -100,7 +105,7 @@ class ResizeZoneMemberCollectionListener extends ResizeFormListener
*
* @throws UnexpectedTypeException
*/
public function preBind(FormEvent $event)
public function preSubmit(FormEvent $event)
{
$data = $event->getData();
if (null === $data || '' === $data) {
@ -145,6 +150,7 @@ class ResizeZoneMemberCollectionListener extends ResizeFormListener
* @param array $data
*
* @return string|FormTypeInterface
*
* @throws \InvalidArgumentException when _type is not present or is invalid
*/
protected function getTypeForData(array $data)
@ -169,6 +175,9 @@ class ResizeZoneMemberCollectionListener extends ResizeFormListener
), $this->options)));
}
/**
* @param FormInterface $form
*/
private function removeFormFields(FormInterface $form)
{
if ($this->allowDelete) {
@ -180,6 +189,10 @@ class ResizeZoneMemberCollectionListener extends ResizeFormListener
}
}
/**
* @param FormInterface $form
* @param array $data
*/
private function createFormFields(FormInterface $form, $data)
{
if ($this->allowAdd) {

View file

@ -34,11 +34,11 @@ class CountryType extends AbstractResourceType
'label' => 'sylius.form.country.iso_name'
))
->add('provinces', 'collection', array(
'type' => 'sylius_province',
'allow_add' => true,
'type' => 'sylius_province',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'label' => 'sylius.form.country.provinces'
'button_add_label' => 'sylius.country.add_province'
))
;
}

View file

@ -24,13 +24,14 @@ use Symfony\Component\OptionsResolver\OptionsResolverInterface;
*
* @author Tim Nagel <t.nagel@infinite.net.au>
* @author Saša Stamenković <umpirsky@gmail.com>
* @author Arnaud Langlade <arn0d.dev@gmail.com>
*/
class ZoneMemberCollectionType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options = array())
{
$prototypes = $this->buildPrototypes($builder, $options);
@ -49,55 +50,11 @@ class ZoneMemberCollectionType extends AbstractType
$builder->addEventSubscriber($resizeListener);
}
/**
* Builds prototypes for each of the form types used for the collection.
*
* @param FormBuilderInterface $builder
* @param array $options
*
* @return array
*/
protected function buildPrototypes(FormBuilderInterface $builder, array $options)
{
$prototypes = array();
$types = array(
'sylius_zone_member_country',
'sylius_zone_member_province',
'sylius_zone_member_zone',
);
foreach ($types as $type) {
$prototype = $this->buildPrototype($builder, $options['prototype_name'], $type, $options['options'])->getForm();
$prototypes[$type] = $prototype;
}
return $prototypes;
}
/**
* Builds an individual prototype.
*
* @param FormBuilderInterface $builder
* @param string $name
* @param string|FormTypeInterface $type
* @param array $options
*
* @return FormBuilderInterface
*/
protected function buildPrototype(FormBuilderInterface $builder, $name, $type, array $options)
{
return $builder->create($name, $type, array_replace(array(
'label' => $name . 'label__',
), $options));
}
/**
* {@inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['allow_add'] = $options['allow_add'];
$view->vars['allow_delete'] = $options['allow_delete'];
if ($form->getConfig()->hasAttribute('prototypes')) {
$view->vars['prototypes'] = array_map(function (FormInterface $prototype) use ($view) {
return $prototype->createView($view);
@ -111,13 +68,9 @@ class ZoneMemberCollectionType extends AbstractType
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'prototype' => true,
'prototype_name' => '__name__',
'type_name' => '_type',
'options' => array(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
));
}
@ -128,4 +81,36 @@ class ZoneMemberCollectionType extends AbstractType
{
return 'sylius_zone_member_collection';
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return 'collection';
}
/**
* Builds prototypes for each of the form types used for the collection.
*
* @param FormBuilderInterface $builder
* @param array $options
*
* @return array
*/
protected function buildPrototypes(FormBuilderInterface $builder, array $options)
{
$types = array(
'sylius_zone_member_country',
'sylius_zone_member_province',
'sylius_zone_member_zone',
);
$prototypes = array();
foreach ($types as $type) {
$prototypes[$type] = $builder->create($options['prototype_name'], $type, $options['options'])->getForm();
}
return $prototypes;
}
}

View file

@ -54,7 +54,8 @@ class ZoneType extends AbstractResourceType
))
->add('type', 'sylius_zone_type_choice')
->add('members', 'sylius_zone_member_collection', array(
'label' => 'sylius.form.zone.members',
'label' => false,
'button_add_label' => 'sylius.zone.add_member',
))
;

View file

@ -33,7 +33,6 @@
<services>
<service id="sylius.form.type.zone_member_collection" class="%sylius.form.type.zone_member_collection.class%">
<argument>%sylius.model.zone.class%</argument>
<tag name="form.type" alias="sylius_zone_member_collection" />
</service>
<service id="sylius.form.type.address" class="%sylius.form.type.address.class%">

View file

@ -10,22 +10,8 @@
'use strict';
$(document).ready(function() {
var typeSelect = $('#sylius_zone_type');
$('form.form-horizontal').on('submit', function(e) {
$('div[id^="sylius-zone-members-"]').not('[id$="'+ typeSelect.val() +'"]').each(function () {
$(this).remove();
});
});
typeSelect.on('change', function() {
var value = $(this).val();
$('div[id^="sylius-zone-members-"]').hide();
$('#sylius-zone-members-' + value).show();
$('a[data-collection-button="add"]')
.data('collection', 'sylius-zone-members-' + value)
.data('prototype', 'sylius-zone-members-' + value)
;
}).trigger('change');
if (0 == $('[data-form-collection="item"]').length) {
$('#sylius_zone_type').trigger('change');
}
});
})( jQuery );

View file

@ -12,6 +12,9 @@
namespace spec\Sylius\Bundle\AddressingBundle\Form\Type;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
/**
@ -36,19 +39,11 @@ class ZoneMemberCollectionTypeSpec extends ObjectBehavior
function it_defines_assigned_data_class(OptionsResolverInterface $resolver)
{
$resolver
->setDefaults(
array(
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'prototype' => true,
'prototype_name' => '__name__',
'type_name' => '_type',
'options' => array(),
)
)
->shouldBeCalled();
$resolver->setDefaults(array(
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
))->shouldBeCalled();
$this->setDefaultOptions($resolver);
}

View file

@ -0,0 +1,235 @@
<?php
/*
* 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.
*/
namespace Sylius\Bundle\ResourceBundle\Behat;
use Behat\Mink\Element\Element;
use Behat\Mink\Element\NodeElement;
use Behat\Mink\Exception\ElementNotFoundException;
use Behat\MinkExtension\Context\RawMinkContext;
/**
* @author Arnaud Langlade <arn0d.dev@gmail.com>
*/
abstract class FormContext extends RawMinkContext
{
/**
* @return string
*/
protected function getDefaultCollectionSelector()
{
return 'div[@data-form-type="collection"]';
}
/**
* @param string $position
*
* @return string
*/
protected function getDefaultCollectionItemSelector($position)
{
return sprintf(
'*[contains(@data-form-collection, "item") and position()=%d]',
$position
);
}
/**
* Add an item of the collection type
*
* @param string $collectionSelector
* @param null $buttonName
* @param string $buttonType
*/
protected function addCollectionItem($collectionSelector = null, $buttonName = null, $buttonType = 'click')
{
$collectionSelector = null !== $collectionSelector ? $collectionSelector : $this->getDefaultCollectionSelector();
$buttonSelector = null === $buttonName ? '' : sprintf('and text()[contains(., "%s")]', $buttonName);
$button = $this->findElement(
sprintf(
'//%s//*[contains(@data-form-collection, "add") %s]',
$collectionSelector,
$buttonSelector
)
);
$button->$buttonType();
}
/**
* Delete an item of the collection type
*
* @param string $collectionSelector
* @param integer $position
* @param null $buttonName
* @param string $buttonType
*/
protected function deleteCollectionItem(
$position,
$buttonName = null,
$buttonType = 'click',
$collectionSelector = null
) {
$collectionSelector = null !== $collectionSelector ? $collectionSelector : $this->getDefaultCollectionSelector();
$buttonSelector = null === $buttonName ? '' : sprintf('and text()[contains(., "%s")', $buttonName);
$button = $this->findElement(
sprintf(
'//%s//%s//*[contains(@data-form-collection, "delete") %s]',
$collectionSelector,
$this->getDefaultCollectionItemSelector($position),
$buttonSelector
)
);
$button->$buttonType();
}
/**
* Check if the value of the field is valid
*
* @param string $position
* @param string $label
* @param string $collectionSelector
* @return NodeElement|null
*/
protected function isInvalidCollectionField($position, $label, $collectionSelector = null)
{
$collectionSelector = null !== $collectionSelector ? $collectionSelector : $this->getDefaultCollectionSelector();
try {
return $this->findElement(
sprintf(
'//%s//%s//%s',
$collectionSelector,
$this->getDefaultCollectionItemSelector($position),
sprintf('*[contains(@class, "has-error")]//*[contains(@name, "%s")]', strtolower($label))
)
);
} catch (ElementNotFoundException $e) {
return $this->findElement(
sprintf(
'//%s//%s//%s',
$collectionSelector,
$this->getDefaultCollectionItemSelector($position),
sprintf('*[contains(@class, "has-error")]//label[text()[contains(., "%s")]]', $label)
)
);
}
}
/**
* Fill a collection form field
*
* @param integer $position
* @param string $field
* @param mixed $value
* @param string $collectionSelector
*/
protected function fillCollectionField($position, $field, $value, $collectionSelector = null)
{
$collectionSelector = null !== $collectionSelector ? $collectionSelector : $this->getDefaultCollectionSelector();
$collectionElement = $this->findElement(
sprintf(
'//%s//%s',
$collectionSelector,
$this->getDefaultCollectionItemSelector($position)
)
);
$this->fillInField($field, $value, $collectionElement);
}
/**
* @param string $locator
* @param string $value
* @param Element $container
*/
protected function fillInField($locator, $value, Element $container = null)
{
try {
$field = $this->findField($locator, $container);
} catch (ElementNotFoundException $e) {
$field = $this->findElement(
sprintf('//*[contains(@name, "%s")]', strtolower($locator)),
'xpath',
$container
);
}
if ($field->getTagName() === 'select') {
$field->selectOption($value);
} else {
$field->setValue($value);
}
}
/**
* @param string $locator
* @param Element $container
*
* @return NodeElement
*
* @throws ElementNotFoundException
*/
protected function findField($locator, Element $container = null)
{
if (null !== $container) {
$field = $container->find('named', array(
'field', $this->getSession()->getSelectorsHandler()->xpathLiteral($locator)
));
} else {
$field = $this->getSession()->getPage()->findField($locator);
}
if (null === $field) {
throw new ElementNotFoundException(
$this->getSession(),
'element',
'xpath',
$this->getSession()->getSelectorsHandler()->xpathLiteral($locator)
);
}
return $field;
}
/**
* @param string $locator
* @param string $selector
* @param Element $container
*
* @return NodeElement
*
* @throws ElementNotFoundException
*/
protected function findElement($locator, $selector = 'xpath', Element $container = null)
{
if (null !== $container) {
$field = $container->find($selector, $locator);
} else {
$field = $this->getSession()->getPage()->find($selector, $locator);
}
if (null === $field) {
throw new ElementNotFoundException(
$this->getSession(),
'element',
'xpath',
$this->getSession()->getSelectorsHandler()->xpathLiteral($locator)
);
}
return $field;
}
}

View file

@ -836,4 +836,12 @@ class WebContext extends DefaultContext
{
$this->assertSession()->statusCodeEquals($code);
}
/**
* @Given /^I wait (\d+) (seconds|second)$/
*/
public function iWait($time)
{
$this->getSession()->wait($time*1000);
}
}

View file

@ -1,6 +1,5 @@
{% form_theme form 'SyliusWebBundle::forms.html.twig' %}
{{ form_errors(form) }}
<fieldset>
<div class="row">
<div class="col-md-6">
@ -9,9 +8,6 @@
</div>
<div class="col-md-6">
{{ form_widget(form.provinces) }}
<a href="#" class="btn btn-success btn-block" data-collection-button="add" data-prototype="sylius_country_provinces" data-collection="sylius_country_provinces">
{{ 'sylius.country.add_province'|trans }}
</a>
</div>
</div>
</fieldset>

View file

@ -1,39 +1,17 @@
{% form_theme form 'SyliusWebBundle::forms.html.twig' %}
{{ form_errors(form) }}
<fieldset>
<div class="row">
<div class="col-md-6">
{{ form_row(form.name, {'attr': {'class': 'input-lg'}}) }}
{{ form_row(form.type) }}
{{ form_row(form.type, {'attr': {'data-form-prototype': 'update', 'data-form-prototype-prefix': 'sylius_zone_member_'}}) }}
{% if form.scope is defined %}
{{ form_row(form.scope) }}
{% endif %}
</div>
<div class="col-md-6">
<div class="control-group">
{% for type in ['country', 'province', 'zone'] %}
<div id="sylius-zone-members-{{ type }}" data-prototype="{{ form_widget(form.members.vars.prototypes['sylius_zone_member_' ~ type], {'attr': {'class': 'select2 input-large'}})|e }}">
{% if form.type.vars.value == type %}
{% for member in form.members %}
{% if member.offsetExists(form.type.vars.value) %}
<div class="sylius-zone-members-{{ form.type.vars.value }} row" style="margin-bottom: 10px;">
<div class="col-md-11">
{{ form_widget(member[form.type.vars.value], {'attr': {'class': 'select2 float-right input-large'}}) }}
</div>
<div class="col-md-1">
<a href="#" class="btn btn-danger float-left" data-collection-button="delete" data-collection="sylius-zone-members" data-collection-item="{{ form.type.vars.value }}"><i class="glyphicon glyphicon-trash icon-white"></i></a>
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
</div>
{% endfor %}
{% set prototypeId = 'sylius-zone-members-' ~ form.type.vars.value|default(constant('Sylius\\Component\\Addressing\\Model\\ZoneInterface::TYPE_COUNTRY')) %}
<a href="#" class="btn btn-success btn-block" data-collection-button="add" data-prototype="{{ prototypeId }}" data-collection="{{ prototypeId }}">{{ 'sylius.zone.add_member'|trans }}</a>
</div>
{{ form_row(form.members) }}
</div>
</div>
</fieldset>

View file

@ -89,7 +89,7 @@
{% spaceless %}
<div class="form-group{% if errors|length > 0 %} has-error{% endif %}">
{{ form_label(form) }}
<div class="col-lg-10">
<div {% if label %}class="col-lg-10"{% endif %}>
{{ form_widget(form) }}
{% for error in errors %}
<span class="help-block form-error">