diff --git a/features/taxonomy/managing_taxons/autocomplete_taxon_name.feature b/features/taxonomy/managing_taxons/autocomplete_taxon_name.feature new file mode 100644 index 0000000000..1c930c0be6 --- /dev/null +++ b/features/taxonomy/managing_taxons/autocomplete_taxon_name.feature @@ -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 diff --git a/features/taxonomy/managing_taxons/choosing_taxon_from_tree.feature b/features/taxonomy/managing_taxons/choosing_taxon_from_tree.feature new file mode 100644 index 0000000000..8c28054864 --- /dev/null +++ b/features/taxonomy/managing_taxons/choosing_taxon_from_tree.feature @@ -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 diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonsContext.php new file mode 100644 index 0000000000..acb639558d --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonsContext.php @@ -0,0 +1,114 @@ + + */ +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()) + ); + } + } + } +} diff --git a/src/Sylius/Behat/Context/Setup/TaxonomyContext.php b/src/Sylius/Behat/Context/Setup/TaxonomyContext.php index 674572fe3c..2de750c803 100644 --- a/src/Sylius/Behat/Context/Setup/TaxonomyContext.php +++ b/src/Sylius/Behat/Context/Setup/TaxonomyContext.php @@ -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 * diff --git a/src/Sylius/Behat/Resources/config/services/contexts.xml b/src/Sylius/Behat/Resources/config/services/contexts.xml index c97ec39bbd..da8cff6aec 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts.xml @@ -13,6 +13,7 @@ + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api.xml b/src/Sylius/Behat/Resources/config/services/contexts/api.xml new file mode 100644 index 0000000000..6110b835b8 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/services/contexts/api.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/src/Sylius/Behat/Resources/config/suites.yml b/src/Sylius/Behat/Resources/config/suites.yml index 03df09a7c8..4d25413318 100644 --- a/src/Sylius/Behat/Resources/config/suites.yml +++ b/src/Sylius/Behat/Resources/config/suites.yml @@ -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 diff --git a/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxon.yml b/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxon.yml new file mode 100644 index 0000000000..ee69c7062e --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxon.yml @@ -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" diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/taxon.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/taxon.yml index a0dd53caa6..2b13839569 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/taxon.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/taxon.yml @@ -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/ diff --git a/src/Sylius/Bundle/AdminBundle/Resources/private/js/app.js b/src/Sylius/Bundle/AdminBundle/Resources/private/js/app.js index 7e674ead6d..9eb6396ebc 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/private/js/app.js +++ b/src/Sylius/Bundle/AdminBundle/Resources/private/js/app.js @@ -51,6 +51,7 @@ $('.sylius-tabular-form').addTabErrors(); $('.ui.accordion').addAccordionErrors(); + $('#sylius-product-taxonomy-tree').choiceTree('productTaxon', true, 1); $(document).productSlugGenerator(); $(document).taxonSlugGenerator(); diff --git a/src/Sylius/Bundle/AdminBundle/Resources/private/js/sylius-lazy-choice-tree.js b/src/Sylius/Bundle/AdminBundle/Resources/private/js/sylius-lazy-choice-tree.js new file mode 100644 index 0000000000..e8c2b1c93b --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Resources/private/js/sylius-lazy-choice-tree.js @@ -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 = $('
'); + 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 $('
'); + }; + + var createLeafContainerElement = function () { + return $('
'); + }; + + var createLeafIconElement = function () { + return $(''); + }; + + var createLeafTitleElement = function () { + return $('
'); + }; + + var createLeafTitleSpan = function (displayName) { + return $(''+displayName+'') + }; + + var createLeafContentElement = function () { + return $('
'); + }; + + 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 $('
'); + } + + return $('
'); + }; + + 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); diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Tab/_taxonomy.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Tab/_taxonomy.html.twig index af00d9559b..bb6730e491 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Tab/_taxonomy.html.twig +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Tab/_taxonomy.html.twig @@ -1,8 +1,15 @@

{{ 'sylius.ui.taxonomy'|trans }}

-
- {{ 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')}) }} + +

Product Taxon

+
+ {{ form_widget(form.productTaxons) }} +
+
+
-
- {{ 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'}) }}
diff --git a/src/Sylius/Bundle/TaxonomyBundle/Doctrine/ORM/TaxonRepository.php b/src/Sylius/Bundle/TaxonomyBundle/Doctrine/ORM/TaxonRepository.php index 238da9bb6b..27a1bb02af 100644 --- a/src/Sylius/Bundle/TaxonomyBundle/Doctrine/ORM/TaxonRepository.php +++ b/src/Sylius/Bundle/TaxonomyBundle/Doctrine/ORM/TaxonRepository.php @@ -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() diff --git a/src/Sylius/Bundle/TaxonomyBundle/Resources/config/serializer/Model.Taxon.yml b/src/Sylius/Bundle/TaxonomyBundle/Resources/config/serializer/Model.Taxon.yml index 5a697eabe5..6b9699e1fe 100644 --- a/src/Sylius/Bundle/TaxonomyBundle/Resources/config/serializer/Model.Taxon.yml +++ b/src/Sylius/Bundle/TaxonomyBundle/Resources/config/serializer/Model.Taxon.yml @@ -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: diff --git a/src/Sylius/Bundle/UiBundle/Resources/private/js/sylius-auto-complete.js b/src/Sylius/Bundle/UiBundle/Resources/private/js/sylius-auto-complete.js index e209ea475c..50c51b2487 100644 --- a/src/Sylius/Bundle/UiBundle/Resources/private/js/sylius-auto-complete.js +++ b/src/Sylius/Bundle/UiBundle/Resources/private/js/sylius-auto-complete.js @@ -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; }, diff --git a/src/Sylius/Component/Taxonomy/Model/Taxon.php b/src/Sylius/Component/Taxonomy/Model/Taxon.php index 7bcc4ec5e9..5f0288fe47 100644 --- a/src/Sylius/Component/Taxonomy/Model/Taxon.php +++ b/src/Sylius/Component/Taxonomy/Model/Taxon.php @@ -178,6 +178,14 @@ class Taxon implements TaxonInterface return $this->children->contains($taxon); } + /** + * {@inheritdoc} + */ + public function hasChildren() + { + return !$this->children->isEmpty(); + } + /** * {@inheritdoc} */ diff --git a/src/Sylius/Component/Taxonomy/Model/TaxonInterface.php b/src/Sylius/Component/Taxonomy/Model/TaxonInterface.php index 24b7a2b585..08d5b17af9 100644 --- a/src/Sylius/Component/Taxonomy/Model/TaxonInterface.php +++ b/src/Sylius/Component/Taxonomy/Model/TaxonInterface.php @@ -58,6 +58,11 @@ interface TaxonInterface extends CodeAwareInterface, TaxonTranslationInterface, */ public function hasChild(TaxonInterface $taxon); + /** + * @return bool + */ + public function hasChildren(); + /** * @param TaxonInterface $taxon */ diff --git a/src/Sylius/Component/Taxonomy/Repository/TaxonRepositoryInterface.php b/src/Sylius/Component/Taxonomy/Repository/TaxonRepositoryInterface.php index e744e20b7d..9efb918b23 100644 --- a/src/Sylius/Component/Taxonomy/Repository/TaxonRepositoryInterface.php +++ b/src/Sylius/Component/Taxonomy/Repository/TaxonRepositoryInterface.php @@ -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 diff --git a/src/Sylius/Component/Taxonomy/spec/Model/TaxonSpec.php b/src/Sylius/Component/Taxonomy/spec/Model/TaxonSpec.php index eeba90c33c..8bc45eabec 100644 --- a/src/Sylius/Component/Taxonomy/spec/Model/TaxonSpec.php +++ b/src/Sylius/Component/Taxonomy/spec/Model/TaxonSpec.php @@ -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); + } }