[Js][Product] Main taxon and product taxon Tree choice

This commit is contained in:
Arminek 2017-02-23 10:10:10 +01:00
parent 95b1cbc000
commit 8e62a7b480
19 changed files with 488 additions and 29 deletions

View file

@ -0,0 +1,33 @@
@managing_taxons
Feature: Taxons autocomplete
In order to getting hint when looking for taxons
As an Administrator
I want to get taxon
Background:
Given the store classifies its products as "T-Shirts", "Watches", "Belts" and "Wallets"
And I am logged in as an administrator
@api
Scenario: Getting hint when looking for taxons
When I look for a taxon with "b" in name
Then I should see 1 taxons on the list
And I should see the taxon named "Belts" in the list
@api
Scenario: Getting hint when looking for taxons
When I look for a taxon with "shi" in name
Then I should see 1 taxons on the list
And I should see the taxon named "T-Shirts" in the list
@api
Scenario: Getting hint when looking for taxons
When I look for a taxon with "e" in name
Then I should see 3 taxons on the list
And I should see the taxon named "Belts", "Wallets" and "Watches" in the list
@api
Scenario: Getting taxon from its code
When I want to get taxon with "belts" code
Then I should see 1 taxons on the list
And I should see the taxon named "Belts" in the list

View file

@ -0,0 +1,22 @@
@managing_taxons
Feature: Browsing taxons tree
In order to see all taxons in the store
As an Administrator
I want to browse taxons
Background:
Given the store classifies its products as "T-Shirts", "Watches", "Belts" and "Wallets"
And the "Watches" taxon has children taxon "Digital" and "Analog"
And I am logged in as an administrator
@api
Scenario: Getting taxon root
When I want to get taxon root
Then I should see 4 taxons on the list
And I should see the taxon named "Belts", "Wallets", "Watches" and "T-Shirts" in the list
@api
Scenario: Getting taxon leafs
When I want to get "Watches" taxon leafs
Then I should see 2 taxons on the list
And I should see the taxon named "Digital" and "Analog" in the list

View file

@ -0,0 +1,114 @@
<?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\Behat\Context\Api\Admin;
use Behat\Behat\Context\Context;
use Sylius\Component\Core\Model\TaxonInterface;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Webmozart\Assert\Assert;
/**
* @author Arkadiusz Krakowiak <arkadiusz.krakowiak@lakion.com>
*/
final class ManagingTaxonsContext implements Context
{
/**
* @var Client
*/
private $client;
/**
* @var SessionInterface
*/
private $session;
/**
* @param Client $client
* @param SessionInterface $session
*/
public function __construct(Client $client, SessionInterface $session)
{
$this->client = $client;
$this->session = $session;
}
/**
* @When I look for a taxon with :phrase in name
*/
public function iTypeIn($phrase)
{
$this->client->getCookieJar()->set(new Cookie($this->session->getName(), $this->session->getId()));
$this->client->request('GET', '/admin/ajax/taxons/search', ['phrase' => $phrase], [], ['ACCEPT' => 'application/json']);
}
/**
* @When I want to get taxon with :code code
*/
public function iWantToGetTaxonWithCode($code)
{
$this->client->getCookieJar()->set(new Cookie($this->session->getName(), $this->session->getId()));
$this->client->request('GET', '/admin/ajax/taxons/leaf', ['code' => $code], [], ['ACCEPT' => 'application/json']);
}
/**
* @When /^I want to get ("[^"]+" taxon) leafs$/
*/
public function iWantToGetTaxonLeafs(TaxonInterface $taxon)
{
$this->client->getCookieJar()->set(new Cookie($this->session->getName(), $this->session->getId()));
$this->client->request('GET', '/admin/ajax/taxons/leafs', ['parentCode' => $taxon->getCode()], [], ['ACCEPT' => 'application/json']);
}
/**
* @When I want to get taxon root
*/
public function iWantToGetTaxonRoot()
{
$this->client->getCookieJar()->set(new Cookie($this->session->getName(), $this->session->getId()));
$this->client->request('GET', '/admin/ajax/taxons/root-nodes', [], [], ['ACCEPT' => 'application/json']);
}
/**
* @Then /^I should see (\d+) taxons on the list$/
*/
public function iShouldSeeTaxonsInTheList($number)
{
$response = json_decode($this->client->getResponse()->getContent(), true);
Assert::eq($number, count($response));
}
/**
* @Then I should see the taxon named :firstName in the list
* @Then I should see the taxon named :firstName and :secondName in the list
* @Then I should see the taxon named :firstName, :secondName and :thirdName in the list
* @Then I should see the taxon named :firstName, :secondName, :thirdName and :fourthName in the list
*/
public function iShouldSeeTheTaxonNamedAnd(... $taxonNames)
{
foreach ($taxonNames as $name) {
$response = json_decode($this->client->getResponse()->getContent(), true);
$taxonNames = array_map(function ($item) {
return $item['name'];
}, $response);
if (!in_array($name, $taxonNames)) {
throw new \InvalidArgumentException(
sprintf('There should be taxon named "%s", but got only "%s"', $name, $this->client->getResponse()->getContent())
);
}
}
}
}

View file

@ -145,6 +145,17 @@ final class TaxonomyContext implements Context
$this->objectManager->flush($taxon);
}
/**
* @Given /^the ("[^"]+" taxon) has children taxon "([^"]+)" and "([^"]+)"$/
*/
public function theTaxonHasChildrenTaxonAnd(TaxonInterface $taxon, $firstTaxonName, $secondTaxonName)
{
$taxon->addChild($this->createTaxon($firstTaxonName));
$taxon->addChild($this->createTaxon($secondTaxonName));
$this->objectManager->flush($taxon);
}
/**
* @param string $name
*

View file

@ -13,6 +13,7 @@
<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<imports>
<import resource="contexts/api.xml" />
<import resource="contexts/cli.xml" />
<import resource="contexts/domain.xml" />
<import resource="contexts/hook.xml" />

View file

@ -0,0 +1,25 @@
<?xml version="1.0" ?>
<!--
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.
-->
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="sylius.behat.context.api.admin.managing_taxons" class="Sylius\Behat\Context\Api\Admin\ManagingTaxonsContext">
<argument type="service" id="__symfony__.test.client" />
<argument type="service" id="__symfony__.session" />
<tag name="fob.context_service" />
</service>
</services>
</container>

View file

@ -2,6 +2,8 @@
# (c) Paweł Jędrzejewski
imports:
- suites/api/taxon/managing_taxon.yml
- suites/cli/installer.yml
- suites/domain/cart/shopping_cart.yml

View file

@ -0,0 +1,20 @@
# This file is part of the Sylius package.
# (c) Paweł Jędrzejewski
default:
suites:
api_managing_taxons:
contexts_services:
- sylius.behat.context.hook.doctrine_orm
- sylius.behat.context.transform.locale
- sylius.behat.context.transform.shared_storage
- sylius.behat.context.transform.taxon
- sylius.behat.context.setup.locale
- sylius.behat.context.setup.admin_security
- sylius.behat.context.setup.taxonomy
- sylius.behat.context.api.admin.managing_taxons
filters:
tags: "@managing_taxons && @api"

View file

@ -1,27 +1,49 @@
sylius_admin_ajax_taxon_index:
path: /
sylius_admin_ajax_taxon_root_nodes:
path: /root-nodes
methods: [GET]
defaults:
_controller: sylius.controller.taxon:indexAction
_sylius:
serialization_groups: [Ajax]
serialization_groups: [Autocomplete]
repository:
method: searchByName
arguments:
- $name
- expr:service('sylius.context.locale').getLocaleCode()
method: findRootNodes
sylius_admin_ajax_taxon_in_criteria:
path: /in
sylius_admin_ajax_taxon_leafs:
path: /leafs
methods: [GET]
defaults:
_controller: sylius.controller.taxon:indexAction
_sylius:
serialization_groups: [Ajax]
serialization_groups: [Autocomplete]
repository:
method: findChildren
arguments:
parentCode: $parentCode
locale: expr:service('sylius.context.locale').getLocaleCode()
sylius_admin_ajax_taxon_by_code:
path: /leaf
methods: [GET]
defaults:
_controller: sylius.controller.taxon:indexAction
_sylius:
serialization_groups: [Autocomplete]
repository:
method: findBy
arguments: [code: $code]
sylius_admin_ajax_taxon_by_name_phrase:
path: /search
methods: [GET]
defaults:
_controller: sylius.controller.taxon:indexAction
_sylius:
serialization_groups: [Autocomplete]
repository:
method: findByNamePart
arguments:
- $criteria
phrase: $phrase
locale: expr:service('sylius.context.locale').getLocaleCode()
sylius_admin_ajax_generate_taxon_slug:
path: /generate-slug/

View file

@ -51,6 +51,7 @@
$('.sylius-tabular-form').addTabErrors();
$('.ui.accordion').addAccordionErrors();
$('#sylius-product-taxonomy-tree').choiceTree('productTaxon', true, 1);
$(document).productSlugGenerator();
$(document).taxonSlugGenerator();

View file

@ -0,0 +1,176 @@
/*
* 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({
choiceTree: function (type, multiple, defaultLevel) {
var tree = $(this);
var loader = $(this).find('.dimmer');
var loadedLeafs = [];
var $input = $(this).find('input[type="hidden"]');
tree.api({
on: 'now',
method: 'GET',
url: tree.data('taxon-root-nodes-url'),
cache: false,
beforeSend: function(settings) {
loader.addClass('active');
return settings;
},
onSuccess: function (response) {
var rootContainer = createRootContainer();
$.each(response, function (rootNodeIndex, rootNode) {
rootContainer.append(createLeaf(rootNode.name, rootNode.code, rootNode.hasChildren, multiple, rootNode.level));
});
tree.append(rootContainer);
loader.removeClass('active');
}
});
var createLeaf = function (name, code, hasChildren, multipleChoice, level) {
var displayNameElement = createLeafTitleSpan(name);
var titleElement = createLeafTitleElement();
var iconElement = createLeafIconElement();
var checkboxElement = createCheckboxElement(name, code, multipleChoice);
bindCheckboxAction(checkboxElement);
var leafElement = $('<div class="item"></div>');
var leafContentElement = createLeafContentElement();
leafElement.append(iconElement);
titleElement.append(displayNameElement);
titleElement.append(checkboxElement);
leafContentElement.append(titleElement);
if (!hasChildren) {
iconElement.addClass('outline');
}
if (hasChildren) {
bindExpandLeafAction(code, displayNameElement, leafContentElement, iconElement, level);
}
leafElement.append(leafContentElement);
return leafElement;
};
var createRootContainer = function () {
return $('<div class="ui list"></div>');
};
var createLeafContainerElement = function () {
return $('<div class="list"></div>');
};
var createLeafIconElement = function () {
return $('<i class="folder icon"></i>');
};
var createLeafTitleElement = function () {
return $('<div class="header"></div>');
};
var createLeafTitleSpan = function (displayName) {
return $('<span style="margin-right: 5px; cursor: pointer;">'+displayName+'</span>')
};
var createLeafContentElement = function () {
return $('<div class="content"></div>');
};
var createCheckboxElement = function (name, code, multiple) {
var chosenNodes = $input.val().split(',');
var checked = '';
if (chosenNodes.some(function (chosenCode) {return chosenCode === code})) {
checked = 'checked="checked"';
}
if (multiple) {
return $('<div class="ui checkbox" data-value="'+code+'"><input '+checked+' type="checkbox" name="'+type+'"></div>');
}
return $('<div class="ui radio checkbox" data-value="'+code+'"><input '+checked+' type="radio" name="'+type+'"></div>');
};
var isLeafLoaded = function (code) {
return loadedLeafs.some(function (leafCode) {
return leafCode === code;
})
};
var bindExpandLeafAction = function (parentCode, expandButton, content, icon, level) {
var leafContainerElement = createLeafContainerElement();
if (defaultLevel > level) {
loadLeafAction(parentCode, expandButton, content, leafContainerElement);
icon.addClass('open');
}
expandButton.click(function () {
loadLeafAction(parentCode, expandButton, content, leafContainerElement);
leafContainerElement.toggle(200, function () {
if (icon.hasClass('open')) {
icon.removeClass('open');
return;
}
icon.addClass('open');
});
});
};
var loadLeafAction = function (parentCode, expandButton, content, leafContainerElement) {
if (!isLeafLoaded(parentCode)) {
expandButton.api({
on: 'now',
url: tree.data('taxon-leafs-url'),
method: 'GET',
cache: false,
data: {
parentCode: parentCode
},
beforeSend: function(settings) {
loader.addClass('active');
return settings;
},
onSuccess: function (response) {
$.each(response, function (leafIndex, leafNode) {
leafContainerElement.append(createLeaf(leafNode.name, leafNode.code, leafNode.hasChildren, multiple, leafNode.level));
});
content.append(leafContainerElement);
loader.removeClass('active');
loadedLeafs.push(parentCode);
}
});
}
};
var bindCheckboxAction = function (checkboxElement) {
checkboxElement.checkbox({
onChange: function () {
var $checkboxes = tree.find('.checkbox');
var checkedValues = [];
$checkboxes.each(function () {
if ($(this).checkbox('is checked')) {
checkedValues.push($(this).data('value'));
}
});
$input.val(checkedValues.join());
}
});
};
}
});
})(jQuery);

View file

@ -1,8 +1,15 @@
<div class="ui tab" data-tab="taxonomy">
<h3 class="ui dividing header">{{ 'sylius.ui.taxonomy'|trans }}</h3>
<div class="field">
{{ form_row(form.mainTaxon, {'remote_url': path('sylius_admin_ajax_taxon_index'), 'load_edit_url': path('sylius_admin_ajax_taxon_in_criteria'), 'remote_criteria_type': 'contains', 'remote_criteria_name': 'name'}) }}
{{ form_row(form.mainTaxon, {'remote_url': path('sylius_admin_ajax_taxon_by_name_phrase'), 'remote_criteria_type': 'contains', 'remote_criteria_name': 'phrase', 'load_edit_url': path('sylius_admin_ajax_taxon_by_code')}) }}
<h4>Product Taxon</h4>
<div id="sylius-product-taxonomy-tree"
data-taxon-root-nodes-url="{{ path('sylius_admin_ajax_taxon_root_nodes') }}"
data-taxon-leafs-url="{{ path('sylius_admin_ajax_taxon_leafs') }}"
>
{{ form_widget(form.productTaxons) }}
<div class="ui inverted dimmer">
<div class="ui loader"></div>
</div>
</div>
<div class="ui divider"></div>
{{ form_row(form.productTaxons, {'remote_url': path('sylius_admin_ajax_taxon_index'), 'load_edit_url': path('sylius_admin_ajax_taxon_in_criteria'), 'remote_criteria_type': 'contains', 'remote_criteria_name': 'name'}) }}
</div>

View file

@ -113,14 +113,14 @@ class TaxonRepository extends EntityRepository implements TaxonRepositoryInterfa
/**
* {@inheritdoc}
*/
public function searchByName($name, $locale)
public function findByNamePart($phrase, $locale)
{
return $this->createQueryBuilder('o')
->addSelect('translation')
->innerJoin('o.translations', 'translation')
->andWhere('translation.name LIKE :name')
->andWhere('translation.locale = :locale')
->setParameter('name', '%'.$name.'%')
->setParameter('name', '%'.$phrase.'%')
->setParameter('locale', $locale)
->getQuery()
->getResult()

View file

@ -6,11 +6,11 @@ Sylius\Component\Taxonomy\Model\Taxon:
expose: true
type: integer
xml_attribute: true
groups: [Default, Detailed, Ajax]
groups: [Default, Detailed, Autocomplete]
code:
expose: true
type: string
groups: [Default, Detailed, Ajax]
groups: [Default, Detailed, Autocomplete]
children:
expose: true
type: array
@ -18,11 +18,11 @@ Sylius\Component\Taxonomy\Model\Taxon:
root:
expose: true
max_depth: 2
groups: [Default, Detailed]
groups: [Default, Detailed, Autocomplete]
parent:
expose: true
max_depth: 2
groups: [Default, Detailed, Ajax]
groups: [Default, Detailed, Autocomplete]
left:
expose: true
type: integer
@ -34,7 +34,7 @@ Sylius\Component\Taxonomy\Model\Taxon:
level:
expose: true
type: integer
groups: [Detailed, Ajax]
groups: [Detailed, Autocomplete]
position:
expose: true
type: integer
@ -46,7 +46,10 @@ Sylius\Component\Taxonomy\Model\Taxon:
virtual_properties:
getName:
serialized_name: name
groups: [Ajax]
groups: [Default, Detailed, Autocomplete]
hasChildren:
serialized_name: hasChildren
groups: [Autocomplete]
relations:
- rel: self
href:

View file

@ -59,11 +59,8 @@
on: 'now',
method: 'GET',
url: loadForEditUrl,
data: {
criteria: {}
},
beforeSend: function (settings) {
settings.data.criteria[choiceValue] = autocompleteValue.split(',');
settings.data[choiceValue] = autocompleteValue.split(',');
return settings;
},

View file

@ -178,6 +178,14 @@ class Taxon implements TaxonInterface
return $this->children->contains($taxon);
}
/**
* {@inheritdoc}
*/
public function hasChildren()
{
return !$this->children->isEmpty();
}
/**
* {@inheritdoc}
*/

View file

@ -58,6 +58,11 @@ interface TaxonInterface extends CodeAwareInterface, TaxonTranslationInterface,
*/
public function hasChild(TaxonInterface $taxon);
/**
* @return bool
*/
public function hasChildren();
/**
* @param TaxonInterface $taxon
*/

View file

@ -60,12 +60,12 @@ interface TaxonRepositoryInterface extends RepositoryInterface
public function findByName($name, $locale);
/**
* @param string $name
* @param string $phrase
* @param string $locale
*
* @return TaxonInterface[]
*/
public function searchByName($name, $locale);
public function findByNamePart($phrase, $locale);
/**
* @return QueryBuilder

View file

@ -182,4 +182,16 @@ final class TaxonSpec extends ObjectBehavior
$this->setPosition(0);
$this->getPosition()->shouldReturn(0);
}
function it_has_not_children_by_default()
{
$this->hasChildren()->shouldReturn(false);
}
function it_has_children_when_you_have_added_child(TaxonInterface $taxon)
{
$this->addChild($taxon);
$this->hasChildren()->shouldReturn(true);
}
}