├── tests
├── public
│ ├── .gitkeep
│ └── js
│ │ └── require-paths.js
├── var
│ └── cache
│ │ └── test
│ │ └── Flagbit_Bundle_TableAttributeBundle_Test_Kernel_TestKernelTestContainer.php.lock
├── Kernel
│ ├── config
│ │ ├── bundles.php
│ │ └── packages
│ │ │ └── test
│ │ │ ├── tableattribute.yml
│ │ │ ├── services.yml
│ │ │ ├── ee-services.yml
│ │ │ └── imports.yml
│ ├── PublicServiceCompilerPass.php
│ ├── TestKernel.php
│ └── EnterpriseFilterStubPass.php
├── AttributeType
│ └── TableTypeTest.php
├── Provider
│ └── Field
│ │ └── TableFieldProviderTest.php
├── Form
│ └── Extension
│ │ └── AttributeOptionTypeExtensionTest.php
├── Validator
│ └── ConstraintGuesser
│ │ └── TableGuesserTest.php
└── Pim
│ └── TagsAndServiceOverridesTest.php
├── CHANGELOG-6.0.md
├── CHANGELOG-2.0.md
├── src
├── Resources
│ ├── public
│ │ ├── less
│ │ │ ├── index.less
│ │ │ └── tableattribute.less
│ │ ├── js
│ │ │ ├── Json
│ │ │ │ ├── Storage.js
│ │ │ │ ├── Renderer
│ │ │ │ │ ├── Default.js
│ │ │ │ │ ├── SelectFromUrl.js
│ │ │ │ │ ├── Constraint.js
│ │ │ │ │ ├── Number.js
│ │ │ │ │ └── Select.js
│ │ │ │ ├── Observer.js
│ │ │ │ ├── Generator.js
│ │ │ │ ├── Input.js
│ │ │ │ └── Renderer.js
│ │ │ ├── product
│ │ │ │ └── field
│ │ │ │ │ ├── choices.js
│ │ │ │ │ └── table-field.js
│ │ │ ├── options-grid.js
│ │ │ ├── inittable.js
│ │ │ └── tablecolumnview.js
│ │ ├── templates
│ │ │ └── product
│ │ │ │ └── field
│ │ │ │ └── table.html
│ │ └── images
│ │ │ └── attribute
│ │ │ └── icon-table.svg
│ ├── config
│ │ ├── routing.yml
│ │ ├── form_extensions.yml
│ │ ├── entities.xml
│ │ ├── form_extensions
│ │ │ └── attribute
│ │ │ │ └── edit.yml
│ │ ├── factories.xml
│ │ ├── comparators.xml
│ │ ├── controller.xml
│ │ ├── updaters.xml
│ │ ├── attribute_types.xml
│ │ ├── doctrine
│ │ │ └── AttributeOption.orm.xml
│ │ ├── doctrine.xml
│ │ ├── array_converters.xml
│ │ ├── query_builders.xml
│ │ ├── requirejs.yml
│ │ ├── validators.xml
│ │ └── services.xml
│ ├── translations
│ │ ├── messages.en.xlf
│ │ ├── messages.de.xlf
│ │ ├── jsmessages.en.xlf
│ │ └── jsmessages.de.xlf
│ └── views
│ │ └── Attribute
│ │ └── Tab
│ │ └── value.html.twig
├── FlagbitTableAttributeBundle.php
├── Validator
│ ├── Constraints
│ │ ├── Table.php
│ │ └── TableValidator.php
│ ├── ConstraintGuesser
│ │ └── TableGuesser.php
│ └── ConstraintFactory.php
├── Entity
│ ├── ConstraintConfigInterface.php
│ └── AttributeOption.php
├── Form
│ ├── TableJsonTransformer.php
│ └── Extension
│ │ └── AttributeOptionTypeExtension.php
├── AttributeType
│ └── TableType.php
├── Http
│ └── Select2JsonResponse.php
├── Provider
│ └── Field
│ │ └── TableFieldProvider.php
├── Component
│ └── Product
│ │ ├── Completeness
│ │ └── MaskItemGenerator
│ │ │ └── TableMaskItem.php
│ │ └── Factory
│ │ └── Value
│ │ └── TableValueFactory.php
├── DependencyInjection
│ ├── Configuration.php
│ └── FlagbitTableAttributeExtension.php
├── Normalizer
│ └── AttributeOptionNormalizer.php
└── Controller
│ └── AjaxOptionController.php
├── jest.config.js
├── CHANGELOG-3.1.md
├── phpcs.xml
├── CHANGELOG-2.1.md
├── CHANGELOG-4.0.md
├── .gitignore
├── CHANGELOG-5.0.md
├── CHANGELOG-3.0.md
├── .scrutinizer.yml
├── CONTRIBUTING.md
├── spec
└── Flagbit
│ └── Bundle
│ └── TableAttributeBundle
│ ├── DependencyInjection
│ └── ConfigurationSpec.php
│ ├── AttributeType
│ └── TableTypeSpec.php
│ ├── Component
│ └── Product
│ │ ├── Completeness
│ │ └── MaskItemGenerator
│ │ │ └── TableMaskItemSpec.php
│ │ └── Factory
│ │ └── Value
│ │ └── TableValueFactorySpec.php
│ ├── Http
│ └── Select2JsonResponseSpec.php
│ ├── Entity
│ └── AttributeOptionSpec.php
│ ├── Form
│ └── TableJsonTransformerSpec.php
│ ├── Provider
│ └── Field
│ │ └── TableFieldProviderSpec.php
│ ├── Controller
│ └── AjaxOptionControllerSpec.php
│ ├── Validator
│ ├── Constraints
│ │ └── TableValidatorSpec.php
│ ├── ConstraintGuesser
│ │ └── TableGuesserSpec.php
│ └── ConstraintFactorySpec.php
│ └── Normalizer
│ └── AttributeOptionNormalizerSpec.php
├── package.json
├── jest
└── integration
│ └── formextensions.test.js
├── LICENSE
├── composer.json
├── phpunit.xml.dist.bak
├── phpunit.xml.dist
├── .github
└── workflows
│ └── main.yml
├── CODE_OF_CONDUCT.md
└── README.md
/tests/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CHANGELOG-6.0.md:
--------------------------------------------------------------------------------
1 | # 6.0.0
2 |
3 | - Add support for Akeneo 6.0.0.
4 |
--------------------------------------------------------------------------------
/tests/var/cache/test/Flagbit_Bundle_TableAttributeBundle_Test_Kernel_TestKernelTestContainer.php.lock:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/CHANGELOG-2.0.md:
--------------------------------------------------------------------------------
1 | # 2.0.1
2 |
3 | ## Bug fixes
4 |
5 | - AKTA-51: Fix wrong ES query filter
6 |
7 | # 2.0.0
8 |
--------------------------------------------------------------------------------
/src/Resources/public/less/index.less:
--------------------------------------------------------------------------------
1 | @import "./public/bundles/flagbittableattribute/less/tableattribute.less";
2 |
--------------------------------------------------------------------------------
/tests/public/js/require-paths.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | "../vendor/akeneo/pim-community-dev/src/Akeneo/Platform/Bundle/UIBundle",
3 | "../src"]
4 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | verbose: true,
3 | rootDir: './',
4 | testURL: 'http://localhost/',
5 | testMatch: ['**/jest/**/*.test.js'],
6 | };
7 |
--------------------------------------------------------------------------------
/src/Resources/config/routing.yml:
--------------------------------------------------------------------------------
1 | pim_ui_ajaxentity_list:
2 | path: /list.json
3 | defaults: { _controller: flagbit_table_attribute.controller.ajax_option:listAction }
4 |
--------------------------------------------------------------------------------
/CHANGELOG-3.1.md:
--------------------------------------------------------------------------------
1 | # 3.1.0
2 |
3 | ## BC Break
4 |
5 | - Replace deprecated assetic by lessjs. [#35][pr35]
6 |
7 | [pr29]: https://github.com/flagbit/akeneo-table-attribute-bundle/pull/35
8 |
--------------------------------------------------------------------------------
/tests/Kernel/config/bundles.php:
--------------------------------------------------------------------------------
1 | ['all' => true],
6 | ];
7 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | ./src
4 | ./tests
5 | ./vendor/*
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CHANGELOG-2.1.md:
--------------------------------------------------------------------------------
1 | # 2.1.2
2 |
3 | ## Bug fixes
4 |
5 | - AKTA-63: Fix issues with the form filters.
6 |
7 | # 2.1.1
8 |
9 | ## Bug fixes
10 |
11 | - AKTA-51: Fix wrong ES query filter.
12 |
13 | # 2.1.0
14 |
--------------------------------------------------------------------------------
/CHANGELOG-4.0.md:
--------------------------------------------------------------------------------
1 | # 4.0.1
2 |
3 | ## Bug fixes
4 |
5 | - Fix of select form extension in product export filters [#54][pr54]
6 |
7 | # 4.0.0
8 |
9 | [pr54]: https://github.com/flagbit/akeneo-table-attribute-bundle/pull/54
10 |
--------------------------------------------------------------------------------
/src/FlagbitTableAttributeBundle.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | flagbit_catalog_table
7 | Table
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Resources/translations/messages.de.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | flagbit_catalog_table
7 | Tabelle
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/CHANGELOG-3.0.md:
--------------------------------------------------------------------------------
1 | # 3.0.1
2 |
3 | ## Bug fixes
4 |
5 | - Fixes issue saving Akeneo AttributeOptions. [#29][pr29]
6 |
7 | # 3.0.0
8 |
9 | - Add support for Akeneo 3.0.0.
10 |
11 | ## BC breaks
12 |
13 | - `Flagbit\Bundle\TableAttributeBundle\Normalizer\StructuredAttributeOptionNormalizer` was deleted. `Flagbit\Bundle\TableAttributeBundle\Normalizer\AttributeOptionNormalizer` is now used for its service.
14 |
15 | [pr29]: https://github.com/flagbit/akeneo-table-attribute-bundle/pull/29
16 |
--------------------------------------------------------------------------------
/src/AttributeType/TableType.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Flagbit\Bundle\TableAttributeBundle\Entity\AttributeOption
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/Resources/config/form_extensions/attribute/edit.yml:
--------------------------------------------------------------------------------
1 | extensions:
2 | flagbit-catalog-table-attribute-edit-form:
3 | module: flagbit/product/field/choices
4 | parent: pim-attribute-edit-form-form-tabs
5 | targetZone: container
6 | position: 110
7 |
8 | flagbit-catalog-table-attribute-edit-form-options-grid:
9 | module: flagbit/options-grid
10 | parent: flagbit-catalog-table-attribute-edit-form
11 | targetZone: form-container
12 |
--------------------------------------------------------------------------------
/src/Resources/config/factories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in
4 | this project you agree to abide by its terms.
5 |
6 | ## Branch Compatibility
7 |
8 | | Branch | Akeneo Compatibility |
9 | |----------------|:--------------------:|
10 | | master / `6.0` | `>= 6.0.0` |
11 | | `5.0` | `>= 5.0.0` |
12 | | `4.0` | `>= 4.0.0` |
13 | | `3.0` | `>= 3.0.0` |
14 | | `2.X` | `>= 2.0.5 & < 3.0.0` |
15 | | `2.0` | `>= 2.0.0 & < 2.0.5` |
16 | | `1.X` | `>= 1.6.0 & < 2.0.0` |
17 |
--------------------------------------------------------------------------------
/tests/Kernel/config/packages/test/ee-services.yml:
--------------------------------------------------------------------------------
1 | # Because an Open Source Akeneo project can't' install Enterprise dependencies
2 | # this service file is using placeholder to make enterprise features testable.
3 | services:
4 | pimee_workflow.query.filter.product_proposal_registry:
5 | class: '%pim_catalog.query.filter.registry.class%'
6 | public: true
7 | arguments:
8 | - '@pim_catalog.repository.attribute'
9 |
10 | pimee_workflow.query.filter.published_product_registry:
11 | class: '%pim_catalog.query.filter.registry.class%'
12 | public: true
13 | arguments:
14 | - '@pim_catalog.repository.attribute'
15 |
--------------------------------------------------------------------------------
/src/Http/Select2JsonResponse.php:
--------------------------------------------------------------------------------
1 | $option) {
18 | $select2Options[] = ['id' => (string) $key, 'text' => (string) $option];
19 | }
20 |
21 | parent::__construct(['results' => $select2Options], $status, $headers);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/spec/Flagbit/Bundle/TableAttributeBundle/DependencyInjection/ConfigurationSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(Configuration::class);
14 | }
15 |
16 | public function it_returns_Treebuilder_object()
17 | {
18 | $this->getConfigTreeBuilder()->shouldBeAnInstanceOf(TreeBuilder::Class);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Provider/Field/TableFieldProvider.php:
--------------------------------------------------------------------------------
1 | getType();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Resources/config/comparators.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 | flagbit_catalog_table
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pim-enterprise-standard",
3 | "description": "Akeneo PIM Enterprise Standard Edition",
4 | "homepage": "http://www.akeneo.com",
5 | "private": true,
6 | "config": {
7 | "source": "vendor/akeneo/pim-community-dev",
8 | "styles": "vendor/akeneo/pim-community-dev/frontend/build/compile-less.js"
9 | },
10 | "scripts": {
11 | "update-extensions": "cd tests && node ../vendor/akeneo/pim-community-dev/frontend/build/update-extensions.js",
12 | "test": "yarn update-extensions && jest --no-cache --config jest.config.js"
13 | },
14 | "workspaces": [
15 | "vendor/akeneo/pim-community-dev",
16 | "vendor/akeneo/pim-community-dev/src/Akeneo/Connectivity/Connection/front"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/tests/AttributeType/TableTypeTest.php:
--------------------------------------------------------------------------------
1 | get('pim_catalog.registry.attribute_type');
16 |
17 | $attributeType = $attributeTypeRegistry->get('flagbit_catalog_table');
18 |
19 | self::assertInstanceOf(TableType::class, $attributeType);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Resources/config/controller.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/Resources/config/updaters.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
10 | flagbit_catalog_table
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/Kernel/PublicServiceCompilerPass.php:
--------------------------------------------------------------------------------
1 | serviceIds = $serviceIds;
19 | }
20 |
21 | public function process(ContainerBuilder $container)
22 | {
23 | foreach ($this->serviceIds as $serviceId) {
24 | $container->getDefinition($serviceId)->setPublic(true);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Validator/Constraints/TableValidator.php:
--------------------------------------------------------------------------------
1 | get('pim_enrich.provider.field.chained');
16 |
17 | $attribute = new Attribute();
18 | $attribute->setType('flagbit_catalog_table');
19 |
20 | $fieldProvider = $chainedFieldProvider->getField($attribute);
21 |
22 | self::assertSame('flagbit-table-field', $fieldProvider);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/spec/Flagbit/Bundle/TableAttributeBundle/AttributeType/TableTypeSpec.php:
--------------------------------------------------------------------------------
1 | beConstructedWith('text');
13 | }
14 |
15 | public function it_is_initializable()
16 | {
17 | $this->shouldHaveType(TableType::class);
18 | }
19 |
20 | public function it_returns_flagbit_catalog_table()
21 | {
22 | $this->getName()->shouldReturn('flagbit_catalog_table');
23 | }
24 |
25 | public function it_returns_text_forbackend_type()
26 | {
27 | $this->getBackendType()->shouldReturn('text');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Resources/public/js/Json/Storage.js:
--------------------------------------------------------------------------------
1 | define(
2 | function () {
3 |
4 | /**
5 | * @class
6 | */
7 | var JsonGeneratorStorage = function () {
8 |
9 | /**
10 | * @protected
11 | * @type {Object}
12 | */
13 | var $data = {};
14 |
15 | /**
16 | * @public
17 | * @param {Object} $_data
18 | */
19 | this.write = function ($_data) {
20 |
21 | $data = $_data;
22 | };
23 |
24 |
25 | /**
26 | * @public
27 | * @returns {Object}
28 | */
29 | this.read = function () {
30 |
31 | return $data;
32 | };
33 | };
34 |
35 | return JsonGeneratorStorage;
36 | }
37 | );
--------------------------------------------------------------------------------
/spec/Flagbit/Bundle/TableAttributeBundle/Component/Product/Completeness/MaskItemGenerator/TableMaskItemSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(TableMaskItem::class);
13 | }
14 |
15 | public function it_masks_for_raw_value()
16 | {
17 | $this->forRawValue('a', 'b', 'c', 'd')->shouldBe(['a-b-c']);
18 | }
19 |
20 | public function it_supports_attribute_types()
21 | {
22 | $this->supportedAttributeTypes()->shouldBe(['flagbit_catalog_table']);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Resources/public/templates/product/field/table.html:
--------------------------------------------------------------------------------
1 |
The names of the types being extended
45 | */
46 | public static function getExtendedTypes(): iterable
47 | {
48 | return [
49 | AttributeOptionType::class
50 | ];
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/Kernel/EnterpriseFilterStubPass.php:
--------------------------------------------------------------------------------
1 | type = $type;
22 | }
23 |
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function process(ContainerBuilder $container)
28 | {
29 | $registry = $container->getDefinition(sprintf('pimee_workflow.query.filter.%s_registry', $this->type));
30 | $filterTag = sprintf('pimee_workflow.elasticsearch.query.%s_filter', $this->type);
31 |
32 | $filters = $this->findTaggedServices($filterTag, $container);
33 | foreach ($filters as $filter) {
34 | $registry->addMethodCall('register', [$filter]);
35 | }
36 | }
37 |
38 | private function findTaggedServices(string $tagName, ContainerBuilder $container): array
39 | {
40 | $services = $container->findTaggedServiceIds($tagName);
41 |
42 | $sortedServices = [];
43 | foreach ($services as $serviceId => $tags) {
44 | foreach ($tags as $tag) {
45 | $priority = $tag['priority'] ?? 30;
46 | $sortedServices[$priority][] = new Reference($serviceId);
47 | }
48 | }
49 | krsort($sortedServices);
50 |
51 | return count($sortedServices) > 0 ? array_merge(...$sortedServices) : [];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Resources/config/requirejs.yml:
--------------------------------------------------------------------------------
1 | config:
2 | paths:
3 | flagbit/table-field: flagbittableattribute/js/product/field/table-field
4 | flagbit/template/product/field/table: flagbittableattribute/templates/product/field/table.html
5 | flagbit/JsonGenerator: flagbittableattribute/js/Json/Generator
6 | flagbit/JsonGenerator/Input: flagbittableattribute/js/Json/Input
7 | flagbit/JsonGenerator/Observer: flagbittableattribute/js/Json/Observer
8 | flagbit/JsonGenerator/Renderer: flagbittableattribute/js/Json/Renderer
9 | flagbit/JsonGenerator/Renderer/Number: flagbittableattribute/js/Json/Renderer/Number
10 | flagbit/JsonGenerator/Renderer/Select: flagbittableattribute/js/Json/Renderer/Select
11 | flagbit/JsonGenerator/Renderer/SelectFromUrl: flagbittableattribute/js/Json/Renderer/SelectFromUrl
12 | flagbit/JsonGenerator/Renderer/Default: flagbittableattribute/js/Json/Renderer/Default
13 | flagbit/JsonGenerator/Renderer/Constraint: flagbittableattribute/js/Json/Renderer/Constraint
14 | flagbit/JsonGenerator/Storage: flagbittableattribute/js/Json/Storage
15 | flagbit/tablecolumnview: flagbittableattribute/js/tablecolumnview
16 | flagbit/inittable: flagbittableattribute/js/inittable
17 | flagbit/options-grid: flagbittableattribute/js/options-grid
18 | flagbit/product/field/choices: flagbittableattribute/js/product/field/choices
19 |
20 | config:
21 | pim/form/common/attributes/create-button:
22 | attribute_icons:
23 | flagbit_catalog_table: table
24 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 | src
17 |
18 |
19 | src/Resources
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ./tests
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/Entity/AttributeOption.php:
--------------------------------------------------------------------------------
1 | type;
34 | }
35 |
36 | /**
37 | * @param string $type
38 | */
39 | public function setType(string $type)
40 | {
41 | $this->type = $type;
42 | }
43 |
44 | /**
45 | * @return array
46 | */
47 | public function getConstraints(): array
48 | {
49 | return $this->constraints;
50 | }
51 |
52 | /**
53 | * @param array $constraints
54 | */
55 | public function setConstraints(array $constraints)
56 | {
57 | $this->constraints = $constraints;
58 | }
59 |
60 | /**
61 | * @return array
62 | */
63 | public function getTypeConfig(): array
64 | {
65 | return $this->typeConfig;
66 | }
67 |
68 | /**
69 | * @param array $typeConfig
70 | */
71 | public function setTypeConfig(array $typeConfig)
72 | {
73 | $this->typeConfig = $typeConfig;
74 | }
75 |
76 | public function isTableAttribute() : bool
77 | {
78 | return null !== $this->attribute && TableType::FLAGBIT_CATALOG_TABLE === $this->attribute->getType();
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/Resources/public/js/product/field/choices.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(
4 | [
5 | 'underscore',
6 | 'oro/translator',
7 | 'pim/form',
8 | 'pim/template/common/form-container'
9 | ],
10 | function (
11 | _,
12 | __,
13 | BaseForm,
14 | template
15 | ) {
16 | return BaseForm.extend(
17 | {
18 | className: 'tab-content',
19 | template: _.template(template),
20 |
21 | /**
22 | * {@inheritdoc}
23 | */
24 | initialize: function () {
25 | BaseForm.prototype.initialize.apply(this, arguments);
26 | },
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | configure: function () {
32 | if (!this.isActive()) {
33 | return;
34 | }
35 |
36 | this.trigger(
37 | 'tab:register', {
38 | code: this.code,
39 | label: __('flagbit.table_attribute.form.attribute.tab.title')
40 | }
41 | );
42 |
43 | return BaseForm.prototype.configure.apply(this, arguments);
44 | },
45 |
46 | /**
47 | * {@inheritdoc}
48 | */
49 | render: function () {
50 | if (!this.isActive()) {
51 | return;
52 | }
53 |
54 | this.$el.html(this.template());
55 |
56 | this.renderExtensions();
57 | },
58 |
59 | isActive: function () {
60 | return ['flagbit_catalog_table'].includes((this.getRoot()).getType());
61 | }
62 | }
63 | );
64 | }
65 | );
66 |
--------------------------------------------------------------------------------
/src/Resources/config/validators.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | Flagbit\Bundle\TableAttributeBundle\Validator\ConstraintGuesser\TableGuesser
9 | Flagbit\Bundle\TableAttributeBundle\Validator\ConstraintFactory
10 |
11 |
12 |
13 |
15 |
16 | pim_catalog_simpleselect
17 | pim_catalog_multiselect
18 | flagbit_catalog_table
19 |
20 |
21 |
22 |
23 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/spec/Flagbit/Bundle/TableAttributeBundle/Controller/AjaxOptionControllerSpec.php:
--------------------------------------------------------------------------------
1 | beConstructedWith($controller);
19 | }
20 |
21 | public function it_transforms_attribute_option_class(
22 | Request $request,
23 | ParameterBag $bag,
24 | AjaxOptionController $controller,
25 | JsonResponse $response
26 | ): void {
27 | $bag->get('class')->willReturn(AttributeOption::class);
28 | $bag->set('class', TableAttributeOption::class)->shouldBeCalledOnce();
29 | $request->query = $bag;
30 |
31 | $controller->listAction($request)->shouldBeCalledOnce()->willReturn($response);
32 |
33 | $this->listAction($request)->shouldReturn($response);
34 | }
35 |
36 | public function it_keeps_other_classes(
37 | Request $request,
38 | ParameterBag $bag,
39 | AjaxOptionController $controller,
40 | JsonResponse $response
41 | ): void {
42 | $bag->get('class')->willReturn(EmptyIterator::class);
43 | $bag->set('class', TableAttributeOption::class)->shouldNotBeCalled();
44 | $request->query = $bag;
45 |
46 | $controller->listAction($request)->shouldBeCalledOnce()->willReturn($response);
47 |
48 | $this->listAction($request)->shouldReturn($response);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Validator/ConstraintGuesser/TableGuesser.php:
--------------------------------------------------------------------------------
1 | constraintFactory = $constraintFactory;
25 | }
26 |
27 | /**
28 | * {@inheritdoc}
29 | */
30 | public function supportAttribute(AttributeInterface $attribute)
31 | {
32 | return TableType::FLAGBIT_CATALOG_TABLE === $attribute->getType();
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | *
38 | * @throws ExceptionInterface
39 | */
40 | public function guessConstraints(AttributeInterface $attribute): array
41 | {
42 | $constraints = [];
43 |
44 | $fieldConstraints = [];
45 | /**
46 | * @var AttributeOption $option
47 | */
48 | // DocBlock of getOptions() claims to be only ArrayAccess, but Options are a Doctrine Collection
49 | foreach ($attribute->getOptions() as $option) {
50 | $fieldConstraints[$option->getCode()] = $this->constraintFactory->createByConstraintConfig($option);
51 | }
52 |
53 | $constraints[] = $this->constraintFactory->createTableConstraint($fieldConstraints);
54 |
55 | return $constraints;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Resources/public/js/options-grid.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | define(
4 | [
5 | 'jquery',
6 | 'underscore',
7 | 'pim/form',
8 | 'pim/fetcher-registry',
9 | 'flagbit/tablecolumnview',
10 | 'pim/template/attribute/tab/choices/options-grid'
11 | ],
12 | function (
13 | $,
14 | _,
15 | BaseForm,
16 | fetcherRegistry,
17 | AttributeOptionGrid,
18 | template
19 | ) {
20 | return BaseForm.extend(
21 | {
22 | template: _.template(template),
23 | locales: [],
24 |
25 | /**
26 | * {@inheritdoc}
27 | */
28 | configure: function () {
29 | return $.when(
30 | BaseForm.prototype.configure.apply(this, arguments),
31 | fetcherRegistry.getFetcher('locale').fetchActivated()
32 | .then(
33 | function (locales) {
34 | this.locales = locales;
35 | }.bind(this)
36 | )
37 | );
38 | },
39 |
40 | /**
41 | * {@inheritdoc}
42 | */
43 | render: function () {
44 | this.$el.html(
45 | this.template(
46 | {
47 | attributeId: this.getFormData().meta.id,
48 | sortable: !this.getFormData().auto_option_sorting,
49 | localeCodes: _.pluck(this.locales, 'code')
50 | }
51 | )
52 | );
53 |
54 | AttributeOptionGrid(this.$('.attribute-option-grid'));
55 | $('.AknFormContainer').addClass('flagbit-table-attribute');
56 |
57 | this.renderExtensions();
58 | }
59 | }
60 | );
61 | }
62 | );
63 |
--------------------------------------------------------------------------------
/src/Resources/public/js/Json/Input.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'flagbit/JsonGenerator/Observer'
4 | ],
5 | function (JsonGeneratorObserver) {
6 |
7 | /**
8 | * @class
9 | * @param {(HTMLElement|HTMLTextAreaElement)} $element
10 | */
11 | var JsonGeneratorInput = function ($element) {
12 |
13 | /**
14 | * @public
15 | * @type {JsonGeneratorObserver}
16 | */
17 | this.observer = new JsonGeneratorObserver();
18 |
19 |
20 | /**
21 | * @public
22 | * @param {Object} $data
23 | */
24 | this.write = function ($data) {
25 |
26 | $element.value = JSON.stringify($data);
27 |
28 | this.observer.notify('write');
29 | };
30 |
31 |
32 | /**
33 | * @public
34 | * @returns {Object}
35 | */
36 | this.read = function () {
37 | var $data = {};
38 |
39 | if (this.isEditable()) {
40 | $data = JSON.parse($element.value);
41 | } else {
42 | $data = JSON.parse($element.innerText);
43 | }
44 |
45 | return $data;
46 | };
47 |
48 |
49 | /**
50 | * @public
51 | * @returns {Boolean}
52 | */
53 | this.isEditable = function () {
54 |
55 | return $element instanceof HTMLTextAreaElement || ($element instanceof HTMLInputElement && $element.type.toLowerCase() === 'text');
56 | };
57 |
58 |
59 | /**
60 | * @public
61 | */
62 | this.hide = function () {
63 | $element.style.display = 'none';
64 | };
65 |
66 |
67 | /**
68 | * @public
69 | */
70 | this.show = function () {
71 | $element.style.display = '';
72 | };
73 | };
74 |
75 | return JsonGeneratorInput;
76 | }
77 | );
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Tests for Quality Assurance
2 |
3 | on:
4 | - "push"
5 | - "pull_request"
6 |
7 | jobs:
8 | backend-tests:
9 | runs-on: "ubuntu-20.04"
10 | strategy:
11 | matrix:
12 | php-versions: [ '8.0' ]
13 |
14 | steps:
15 | - uses: "actions/checkout@v3"
16 | - name: "Cache dependencies installed with composer"
17 | uses: "actions/cache@v3"
18 | with:
19 | path: "~/.composer/cache"
20 | key: "composer-${{ matrix.php-version }}-${{ hashFiles('composer.json') }}"
21 | restore-keys: "composer-${{ matrix.php-version }}-"
22 |
23 | - name: "Setup PHP Action"
24 | uses: "shivammathur/setup-php@v2"
25 | with:
26 | php-version: "${{ matrix.php-versions }}"
27 | extensions: "intl, xdebug, imagick, apcu, mbstring, bcmath, zip, curl, xsl"
28 |
29 | - name: "Install PHP dependencies"
30 | run: "composer install --prefer-dist --no-interaction --optimize-autoloader --no-suggest --no-progress"
31 |
32 | - name: "Linting"
33 | run: "vendor/bin/phplint ./src"
34 |
35 | - name: "Code Sniffer"
36 | run: "vendor/bin/phpcs -d memory_limit=-1 --standard=PSR2 --extensions=php ./src"
37 |
38 | - name: "PHPSpec"
39 | run: "vendor/bin/phpspec run"
40 |
41 | - name: "Integration tests"
42 | run: "vendor/bin/phpunit"
43 |
44 | frontend-tests:
45 | runs-on: "ubuntu-20.04"
46 | strategy:
47 | matrix:
48 | php-versions: [ '8.0' ]
49 |
50 | steps:
51 | - uses: "actions/checkout@v3"
52 |
53 | - name: "Setup PHP Action"
54 | uses: "shivammathur/setup-php@v2"
55 | with:
56 | php-version: "${{ matrix.php-versions }}"
57 | extensions: "intl, xdebug, imagick, apcu, mbstring, bcmath, zip, curl, xsl"
58 |
59 | - name: "Install PHP dependencies"
60 | run: "composer install --prefer-dist --no-interaction --optimize-autoloader --no-suggest --no-progress"
61 |
62 | - name: "Setup Node with specific version"
63 | uses: actions/setup-node@v3
64 | with:
65 | node-version: 12
66 |
67 | - name: "yarn install"
68 | uses: "borales/actions-yarn@v3.0.0"
69 | with:
70 | cmd: "install"
71 |
72 | - name: "Run frontend tests"
73 | run: "yarn run test"
74 |
--------------------------------------------------------------------------------
/src/Resources/views/Attribute/Tab/value.html.twig:
--------------------------------------------------------------------------------
1 |
2 | {% spaceless %}
3 | {% if elements is not defined %}
4 | {% import 'PimUIBundle:Default:page_elements.html.twig' as elements %}
5 | {% endif %}
6 | {% endspaceless %}
7 | {% set accordionContent = { 'pane.accordion.label_translations': form_row(form.label) } %}
8 | {% if form['autoOptionSorting'] is defined %}
9 | {% set optionsContent %}
10 | {% if form.vars.value.id is not null %}
11 | {% set sortable = form['autoOptionSorting'] is defined ? (not form['autoOptionSorting'].vars.data ? '1' : '0') : '1' %}
12 |
13 |
14 |
19 | {% else %}
20 |
{{ 'save_attribute_before_manage_option'|trans }}
21 | {% endif %}
22 | {% endset %}
23 | {% set accordionContent = accordionContent|merge({ 'pane.accordion.options': optionsContent }) %}
24 | {% endif %}
25 |
26 | {% if form['columns'] is defined %}
27 | {% set columnsContent %}
28 | {% if form.vars.value.id is not null %}
29 |
30 |
31 |
36 | {% else %}
37 |
{{ 'save_attribute_before_manage_option'|trans }}
38 | {% endif %}
39 | {% endset %}
40 | {% set accordionContent = accordionContent|merge({ 'pane.accordion.column': columnsContent }) %}
41 | {% endif %}
42 |
43 | {{ elements.tabSections(accordionContent, 2) }}
44 |
45 |
--------------------------------------------------------------------------------
/src/Resources/public/js/Json/Renderer/SelectFromUrl.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'flagbit/JsonGenerator/Observer',
4 | 'oro/translator',
5 | ],
6 | function (JsonGeneratorObserver, __) {
7 |
8 | /**
9 | * @class
10 | * @param {Boolean} $editable
11 | * @param {HTMLElement} $container
12 | */
13 | var JsonGeneratorRendererSelectFromUrl = function ($editable, $container) {
14 |
15 | /**
16 | * @public
17 | * @type {JsonGeneratorObserver}
18 | */
19 | this.observer = new JsonGeneratorObserver();
20 |
21 | /**
22 | * @public
23 | * @param {Object} $data
24 | */
25 | this.render = function ($data) {
26 |
27 | var $label = document.createElement('label');
28 | $label.innerText = __('flagbit.table_attribute.simpleselect_options_url.label');
29 | var $input = document.createElement('input');
30 | $input.type = 'text';
31 | $input.className = 'AknTextField';
32 | $input.name = 'options_url';
33 | $input.value = $data['options_url'] ? $data['options_url'] : '';
34 | if (!$editable) {
35 | $input.disabled = true;
36 | }
37 | observeChanges($input);
38 |
39 | $container.appendChild($label);
40 | $container.appendChild($input);
41 | };
42 |
43 |
44 | /**
45 | * @public
46 | * @returns {Object}
47 | */
48 | this.read = function () {
49 |
50 | var $data = {};
51 |
52 | $data['options_url'] = $container.querySelector('input[name="options_url"]').value;
53 |
54 | return $data;
55 | };
56 |
57 | /**
58 | * @protected
59 | * @param {HTMLInputElement} $input
60 | */
61 | var observeChanges = function ($input) {
62 | $input.addEventListener('keyup', notify);
63 | $input.addEventListener('blur', notify);
64 | }.bind(this);
65 |
66 |
67 | /**
68 | * @protected
69 | */
70 | var notify = function () {
71 |
72 | this.observer.notify('update');
73 | }.bind(this);
74 | };
75 |
76 | return JsonGeneratorRendererSelectFromUrl;
77 | }
78 | );
79 |
--------------------------------------------------------------------------------
/src/Validator/ConstraintFactory.php:
--------------------------------------------------------------------------------
1 | getConstraints() as $class => $params) {
24 | try {
25 | $constraints[] = $this->createInstance($class, $params);
26 | } catch (ExceptionInterface $e) {
27 | // TODO Create log entry for failing instantiation.
28 | continue;
29 | }
30 | }
31 |
32 | return $constraints;
33 | }
34 |
35 | /**
36 | * Creates one Contraint for a collection containing multiple field.
37 | *
38 | * The given $constraints array must be a associative array where the keys are the same as the keys of the value
39 | * array that needs to be validated.
40 | *
41 | * @param Constraint[] $constraints
42 | *
43 | * @return Constraint
44 | *
45 | * @throws ExceptionInterface
46 | */
47 | public function createTableConstraint(array $constraints)
48 | {
49 | return new Table(
50 | ['constraints' => [
51 | new Collection(
52 | [
53 | 'fields' => $constraints,
54 | // This is due to missing fields created by the older TableAttribute versions, that were allowed before
55 | 'allowMissingFields' => true,
56 | ]
57 | )
58 | ]]
59 | );
60 | }
61 |
62 | /**
63 | * @param string $class
64 | * @param int|array|string|null $params
65 | *
66 | * @return Constraint
67 | */
68 | private function createInstance(string $class, int|array|string|null $params): Constraint
69 | {
70 | if (false === class_exists($class)) {
71 | $class = '\\Symfony\\Component\\Validator\\Constraints\\'.$class;
72 | }
73 |
74 | if (false === class_exists($class) || false === in_array(Constraint::class, class_parents($class), true)) {
75 | throw new RuntimeException(sprintf('Invalid class %s', $class));
76 | }
77 |
78 | return new $class($params);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/spec/Flagbit/Bundle/TableAttributeBundle/Validator/Constraints/TableValidatorSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(TableValidator::class);
19 | }
20 |
21 | public function it_is_invalid_on_wrong_value()
22 | {
23 | $validator = (new ValidatorBuilder())->getValidator();
24 | $executionContext = (new ExecutionContextFactory(new IdentityTranslator()))->createContext($validator, '');
25 |
26 | $this->initialize($executionContext);
27 | $this->validate('[{"foo":1,"bar":"bar"}]', $this->createTableConstraint())->shouldHaveViolations($executionContext, 2);
28 | }
29 |
30 | public function it_is_valid_on_correct_value()
31 | {
32 | $validator = (new ValidatorBuilder())->getValidator();
33 | $executionContext = (new ExecutionContextFactory(new IdentityTranslator()))->createContext($validator, '');
34 | $value = '[{"foo":null,"bar":false},{"foo":null,"bar":false},{"foo":null,"bar":false},{"foo":null,"bar":false}]';
35 |
36 | $this->initialize($executionContext);
37 | $this->validate($value, $this->createTableConstraint())->shouldHaveViolations($executionContext, 0);
38 | }
39 |
40 | /**
41 | * @return array
42 | */
43 | public function getMatchers(): array
44 | {
45 | return [
46 | 'haveViolations' => function ($subject, $context, $count) {
47 | $violationCount = count($context->getViolations());
48 | if ($violationCount !== $count) {
49 | throw new FailureException(sprintf('Expected violations: %d, but %d occured', $count, $violationCount));
50 | }
51 | return true;
52 | }
53 | ];
54 | }
55 |
56 | /**
57 | * @return Table
58 | */
59 | private function createTableConstraint()
60 | {
61 | return new Table(
62 | ['constraints' => [
63 | new C\Collection(
64 | [
65 | 'fields' => [
66 | 'foo' => [new C\IsNull()],
67 | 'bar' => [new C\IsFalse()],
68 | ],
69 | ]
70 | )
71 | ]]
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Resources/config/services.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | Flagbit\Bundle\TableAttributeBundle\Provider\Field\TableFieldProvider
9 | Flagbit\Bundle\TableAttributeBundle\Validator\Constraints\AttributeTypeForOptionValidator
10 | Flagbit\Bundle\TableAttributeBundle\Form\Extension\AttributeOptionTypeExtension
11 | FlagbitTableAttributeBundle:Attribute:Tab/value.html.twig
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/spec/Flagbit/Bundle/TableAttributeBundle/Component/Product/Factory/Value/TableValueFactorySpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(TableValueFactory::class);
17 | }
18 |
19 | public function it_creates_scopable_and_localizable_table_value()
20 | {
21 | $attribute = $this->createAttribute(true, true);
22 |
23 | $this->createByCheckingData($attribute, 'channelCode', 'de_DE', 'data')
24 | ->shouldBeLike(ScalarValue::scopableLocalizableValue('code', 'data', 'channelCode', 'de_DE'));
25 | }
26 |
27 | public function it_creates_scopable_table_value()
28 | {
29 | $attribute = $this->createAttribute(true, false);
30 |
31 | $this->createByCheckingData($attribute, 'channelCode', null, 'data')
32 | ->shouldBeLike(ScalarValue::scopableValue('code', 'data', 'channelCode'));
33 | }
34 |
35 | public function it_creates_localizable_table_value()
36 | {
37 | $attribute = $this->createAttribute(false, true);
38 |
39 | $this->createByCheckingData($attribute, null, 'de_DE', 'data')
40 | ->shouldBeLike(ScalarValue::localizableValue('code', 'data', 'de_DE'));
41 | }
42 |
43 | public function it_creates_table_value()
44 | {
45 | $attribute = $this->createAttribute(false, false);
46 |
47 | $this->createByCheckingData($attribute, null, null, 'data')
48 | ->shouldBeLike(ScalarValue::value('code', 'data'));
49 | }
50 |
51 | public function it_throws_exception_on_nonscalar_data()
52 | {
53 | $attribute = $this->createAttribute(false, false);
54 |
55 | $this->shouldThrow()->during('createByCheckingData', [$attribute, null, null, new EmptyIterator()]);
56 | }
57 |
58 | public function it_throws_exception_on_empty_data()
59 | {
60 | $attribute = $this->createAttribute(false, false);
61 |
62 | $this->shouldThrow(InvalidPropertyTypeException::class)->during('createByCheckingData', [$attribute, null, null, "\0\n "]);
63 | }
64 |
65 | private function createAttribute(bool $isScopable, bool $isLocalizable): Attribute
66 | {
67 | return new Attribute('code', 'flagbit_catalog_table', [], $isLocalizable, $isScopable, null, null, false, 'backend', ['de_DE', 'en_US']);
68 | }
69 |
70 | public function it_supports_attribute_type()
71 | {
72 | $this->supportedAttributeType()->shouldBe('flagbit_catalog_table');
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/Validator/ConstraintGuesser/TableGuesserTest.php:
--------------------------------------------------------------------------------
1 | get('pim_catalog.validator.constraint_guesser.chained_attribute');
21 |
22 | $attribute = new Attribute();
23 | $attribute->setType('flagbit_catalog_table');
24 |
25 | $constraintGuesser = $chainedAttributeConstraintGuesser->guessConstraints($attribute);
26 |
27 | self::assertCount(1, $constraintGuesser);
28 | self::assertInstanceOf(Table::class, $constraintGuesser[0]);
29 | }
30 |
31 | /**
32 | * @dataProvider provideValidTableValues
33 | */
34 | public function testValidTableData(string $tableValue): void
35 | {
36 | self::bootKernel();
37 | $container = self::getContainer();
38 |
39 | $guesser = $container->get('flagbit_table_attribute.validator.constraint_guesser.table');
40 | $validator = $container->get('validator');
41 |
42 | $jsonConfig = [
43 | 'Type' => 'int',
44 | 'Range' => ['max' => 10, 'min' => 1]
45 | ];
46 |
47 | $option1 = $this->createMock(AttributeOption::class);
48 | $option1->method('getConstraints')->willReturn($jsonConfig);
49 | $option1->method('getCode')->willReturn('foo');
50 | $option2 = $this->createMock(AttributeOption::class);
51 | // No constraints configured for "bar"
52 | $option2->method('getConstraints')->willReturn([]);
53 | $option2->method('getCode')->willReturn('bar');
54 | $collection = new ArrayCollection([$option1, $option2]);
55 |
56 | $attribute = $this->createMock(AttributeInterface::class);
57 | $attribute->method('getType')->willReturn('flagbit_catalog_table');
58 | $attribute->method('getOptions')->willReturn($collection);
59 |
60 | $constraints = $guesser->guessConstraints($attribute);
61 |
62 | $violations = $validator->validate($tableValue, $constraints);
63 |
64 | self::assertCount(0, $violations);
65 | }
66 |
67 | public function provideValidTableValues(): array
68 | {
69 | return [
70 | 'complete' => ['[{"foo": 5, "bar": "text"},{"foo": 10, "bar": "text2"}]'],
71 | 'missing bar field' => ['[{"foo": 5}]'],
72 | 'completely empty' => ['[]'],
73 | ];
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/spec/Flagbit/Bundle/TableAttributeBundle/Normalizer/AttributeOptionNormalizerSpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(AttributeOptionNormalizer::class);
16 | }
17 |
18 | public function let(NormalizerInterface $baseNormalizer)
19 | {
20 | $this->beConstructedWith($baseNormalizer);
21 | }
22 |
23 | /**
24 | * @throws ExceptionInterface
25 | */
26 | public function it_checks_return_type_array_and_right_values(
27 | AttributeOption $attributeOption,
28 | $baseNormalizer
29 | ) {
30 | $constraints = [
31 | 'NotBlank' => [],
32 | 'Email' => [],
33 | ];
34 |
35 | $activatedLocales = [
36 | 'onlyActivatedLocales' => true,
37 | ];
38 |
39 | $baseNormalizer->normalize($attributeOption, 'array', $activatedLocales)
40 | ->willReturn([])
41 | ->shouldBeCalled();
42 | $attributeOption->getType()->willReturn('text');
43 | $attributeOption->getTypeConfig()->willReturn([]);
44 | $attributeOption->getConstraints()->willReturn($constraints);
45 | $attributeOption->isTableAttribute()->willReturn(true);
46 |
47 | $normalizedValues = $this->normalize($attributeOption, 'array', $activatedLocales);
48 |
49 | $normalizedValues->shouldBeArray();
50 |
51 | $normalizedValues->shouldHaveKey('type');
52 | $normalizedValues->shouldHaveKey('type_config');
53 | $normalizedValues->shouldHaveKey('constraints');
54 |
55 | $normalizedValues->shouldHaveKeyWithValue('type', 'text');
56 | $normalizedValues->shouldHaveKeyWithValue('type_config', []);
57 | $normalizedValues->shouldHaveKeyWithValue('constraints', $constraints);
58 | }
59 |
60 | /**
61 | * @throws ExceptionInterface
62 | */
63 | public function it_checks_return_type_for_default_akeneo_attribute_options(
64 | AttributeOption $attributeOption,
65 | $baseNormalizer
66 | ) {
67 | $baseNormalizer->normalize($attributeOption, 'array', [])
68 | ->willReturn([])
69 | ->shouldBeCalled();
70 | $attributeOption->getType()->willReturn(null);
71 | $attributeOption->getTypeConfig()->willReturn([]);
72 | $attributeOption->getConstraints()->willReturn([]);
73 | $attributeOption->isTableAttribute()->willReturn(false);
74 |
75 | $normalizedValues = $this->normalize($attributeOption, 'array');
76 |
77 | $normalizedValues->shouldBeArray();
78 |
79 | $normalizedValues->shouldNotHaveKey('type');
80 | $normalizedValues->shouldNotHaveKey('type_config');
81 | $normalizedValues->shouldNotHaveKey('constraints');
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/spec/Flagbit/Bundle/TableAttributeBundle/Validator/ConstraintGuesser/TableGuesserSpec.php:
--------------------------------------------------------------------------------
1 | beConstructedWith($constraintFactory);
21 | }
22 |
23 | public function it_is_initializable()
24 | {
25 | $this->shouldHaveType(TableGuesser::class);
26 | }
27 |
28 | public function it_supports_table_attribute(AttributeInterface $attribute)
29 | {
30 | $attribute->getType()->willReturn(TableType::FLAGBIT_CATALOG_TABLE);
31 | $this->supportAttribute($attribute)->shouldReturn(true);
32 | }
33 |
34 | public function it_not_supports_wrong_type(AttributeInterface $attribute)
35 | {
36 | $attribute->getType()->willReturn('not_existing_type');
37 | $this->supportAttribute($attribute)->shouldReturn(false);
38 | }
39 |
40 | /**
41 | * @throws ExceptionInterface
42 | */
43 | public function it_creates_constraint_array(
44 | AttributeInterface $attribute,
45 | AttributeOption $attributeOption,
46 | ConstraintFactory $constraintFactory,
47 | Constraint $notBlank,
48 | Constraint $email,
49 | Table $tableConstraint
50 | ) {
51 | $attribute->getOptions()->willReturn(
52 | new ArrayCollection(
53 | [
54 | $attributeOption->getWrappedObject(),
55 | ]
56 | )
57 | );
58 | $constraints = [
59 | $notBlank,
60 | $email
61 | ];
62 |
63 | $attributeOption->getCode()->willReturn('foo');
64 | $constraintFactory->createByConstraintConfig($attributeOption)->willReturn($constraints);
65 |
66 | $fieldConstraints['foo'] = $constraints;
67 | $constraintFactory->createTableConstraint($fieldConstraints)->willReturn($tableConstraint);
68 | $this->guessConstraints($attribute)->shouldBeArray();
69 | $this->guessConstraints($attribute)->shouldReturn([$tableConstraint]);
70 | }
71 |
72 | /**
73 | * @throws ExceptionInterface
74 | */
75 | public function it_creates_constraint_array_without_options(
76 | AttributeInterface $attribute,
77 | ConstraintFactory $constraintFactory,
78 | Table $tableConstraint
79 | ) {
80 | $attribute->getOptions()->willReturn([]);
81 | $fieldConstraints = [];
82 | $constraintFactory->createTableConstraint($fieldConstraints)->willReturn($tableConstraint);
83 | $this->guessConstraints($attribute)->shouldBeArray();
84 | $this->guessConstraints($attribute)->shouldReturn([$tableConstraint]);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies within all project spaces, and it also applies when
49 | an individual is representing the project or its community in public spaces.
50 | Examples of representing a project or community include using an official
51 | project e-mail address, posting via an official social media account, or acting
52 | as an appointed representative at an online or offline event. Representation of
53 | a project may be further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at info@flagbit.de. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/src/Resources/public/js/inittable.js:
--------------------------------------------------------------------------------
1 | define(
2 | ['jquery', 'underscore'],
3 | function ($, _) {
4 | 'use strict';
5 | return {
6 | init: function ($target, columns) {
7 |
8 | var $headerRow = $target.find('thead tr');
9 | if ($headerRow[0].innerHTML && $headerRow[0].innerHTML.length != 0) {
10 | return;
11 | }
12 |
13 | var $footerRow = $target.find('tfoot tr');
14 | var $tbody = $target.find('tbody');
15 | var values = $target.find('input.table-data').val();
16 |
17 | values = $.parseJSON(values?values:'{}');
18 |
19 | var emptyTitle = '';
20 | // Title for reorder column
21 | $headerRow.append(emptyTitle);
22 | $footerRow.append(emptyTitle);
23 | _.each(
24 | columns, function (column) {
25 | var th = ""+column.text+" | ";
26 | $headerRow.append(th);
27 | $footerRow.append(th);
28 | }.bind(this)
29 | );
30 | // Title for delete button column
31 | $headerRow.append(emptyTitle);
32 | $footerRow.append(emptyTitle);
33 |
34 | _.each(
35 | values, function (row) {
36 | var htmlColumns = [];
37 | _.each(
38 | columns, function (column) {
39 | var value = "";
40 | if (column.id in row) {
41 | value = row[column.id];
42 | }
43 |
44 | htmlColumns.push(this.createColumn(column, value));
45 | }.bind(this)
46 | );
47 | $tbody.append(this.createRow(htmlColumns));
48 | }.bind(this)
49 | );
50 | },
51 | createColumn: function (column, value) {
52 | var td = $(""+column.func.renderField({column: column, value: value})+" | ");
53 | column.func.init(td, column, value);
54 |
55 | return td;
56 | },
57 | createRow: function (htmlColumns) {
58 | var row = $('
');
59 | row.append($(' | '));
60 | _.each(
61 | htmlColumns, function (htmlColumn) {
62 | row.append(htmlColumn);
63 | }
64 | );
65 | row.append($(' | '));
66 |
67 | return row;
68 | },
69 | createEmptyRow: function (columns) {
70 | var htmlColumns = [];
71 | _.each(
72 | columns, function (column) {
73 | htmlColumns.push(this.createColumn(column, ''));
74 | }.bind(this)
75 | );
76 |
77 | return this.createRow(htmlColumns);
78 | }
79 | };
80 | }
81 | );
82 |
--------------------------------------------------------------------------------
/src/Resources/public/js/Json/Renderer/Constraint.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'flagbit/JsonGenerator/Observer',
4 | 'jquery'
5 | ],
6 | function (JsonGeneratorObserver, jQuery) {
7 |
8 | /**
9 | * @class
10 | */
11 | var JsonGeneratorRendererConstraint = function ($editable, $container) {
12 |
13 | /**
14 | * @public
15 | * @type {JsonGeneratorObserver}
16 | */
17 | this.observer = new JsonGeneratorObserver();
18 |
19 | /**
20 | * @public
21 | * @param {Object} $data
22 | */
23 | this.render = function ($data) {
24 |
25 | var $options = ['NotBlank', 'Blank', 'NotNull', 'IsNull', 'IsTrue', 'IsFalse', 'Email', 'Url', 'Ip', 'Uuid', 'Date', 'DateTime', 'Time', 'Language', 'Locale', 'Country', 'Currency', 'Luhn', 'Iban', 'Isbn', 'Issn'];
26 |
27 | var $dropdown = createDropdown();
28 |
29 | $options.forEach(
30 | function ($value) {
31 |
32 | var $option = document.createElement('option');
33 | $option.value = $value;
34 | $option.innerText = $value;
35 |
36 | if ($value in $data) {
37 | $option.selected = true;
38 | }
39 |
40 | $dropdown.appendChild($option);
41 | }
42 | );
43 |
44 | var $select2 = jQuery($dropdown).select2({dropdownAutoWidth: true});
45 |
46 | observeChanges($select2);
47 | };
48 |
49 |
50 | /**
51 | * @public
52 | * @returns {Object}
53 | */
54 | this.read = function () {
55 |
56 | var $data = {};
57 |
58 | var $collection = $container.querySelector('select').querySelectorAll('option');
59 |
60 | for (var $i in $collection) {
61 | if ($collection.hasOwnProperty($i)) {
62 | var $option = $collection[$i];
63 | if ($option.selected) {
64 | $data[$option.value] = {};
65 | }
66 | }
67 | }
68 |
69 | return $data;
70 | };
71 |
72 |
73 | /**
74 | * @protected
75 | * @param {String} $name
76 | * @return {HTMLSelectElement}
77 | */
78 | var createDropdown = function () {
79 |
80 | var $dropdown = document.createElement('select');
81 | $dropdown.style.display = 'block';
82 | $dropdown.multiple = true;
83 | $container.appendChild($dropdown);
84 |
85 | if (!$editable) {
86 | $dropdown.disabled = true;
87 | }
88 |
89 | return $dropdown;
90 | }.bind(this);
91 |
92 |
93 | /**
94 | * @protected
95 | * @param {HTMLSelectElement} $dropdown
96 | */
97 | var observeChanges = function ($select) {
98 | $select.on('change', notify);
99 | }.bind(this);
100 |
101 |
102 | /**
103 | * @protected
104 | */
105 | var notify = function () {
106 |
107 | this.observer.notify('update');
108 | }.bind(this);
109 | };
110 |
111 | return JsonGeneratorRendererConstraint;
112 | }
113 | );
--------------------------------------------------------------------------------
/src/Resources/public/js/Json/Renderer/Number.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'flagbit/JsonGenerator/Observer',
4 | 'oro/translator',
5 | ],
6 | function (JsonGeneratorObserver, __) {
7 |
8 | /**
9 | * @class
10 | */
11 | var JsonGeneratorRendererNumber = function ($editable, $container) {
12 |
13 | /**
14 | * @public
15 | * @type {JsonGeneratorObserver}
16 | */
17 | this.observer = new JsonGeneratorObserver();
18 |
19 | /**
20 | * @public
21 | * @param {Object} $data
22 | */
23 | this.render = function ($data) {
24 |
25 | if (!$data['is_decimal']) {
26 | $data['is_decimal'] = 'false';
27 | }
28 |
29 | var $value = $data['is_decimal'];
30 |
31 | var $label = document.createElement('label');
32 | $label.innerText = __('flagbit.table_attribute.number_is_decimal.label');
33 | $container.appendChild($label);
34 |
35 | var $dropdown = createDropdown('is_decimal');
36 |
37 | var $options = {
38 | 'true': __('Yes'),
39 | 'false': __('No')
40 | };
41 |
42 | for (var $i in $options) {
43 | if ($options.hasOwnProperty($i)) {
44 | var $option = document.createElement('option');
45 | $option.value = $i;
46 | $option.innerText = $options[$i];
47 | $dropdown.appendChild($option);
48 | }
49 | }
50 |
51 | $dropdown.value = $value;
52 |
53 | };
54 |
55 |
56 | /**
57 | * @public
58 | * @returns {Object}
59 | */
60 | this.read = function () {
61 |
62 | var $data = {};
63 |
64 | var $collection = $container.querySelectorAll('select');
65 | for (var $i in $collection) {
66 | if ($collection.hasOwnProperty($i)) {
67 | var $dropdown = $collection[$i];
68 | $data[$dropdown.name] = $dropdown.value === 'true';
69 | }
70 | }
71 |
72 | return $data;
73 | };
74 |
75 |
76 | /**
77 | * @protected
78 | * @param {String} $name
79 | * @return {HTMLSelectElement}
80 | */
81 | var createDropdown = function ($name) {
82 |
83 | var $dropdown = document.createElement('select');
84 | $dropdown.name = $name;
85 | $dropdown.style.display = 'block';
86 | $container.appendChild($dropdown);
87 |
88 | observeChanges($dropdown);
89 |
90 | if (!$editable) {
91 | $dropdown.disabled = true;
92 | }
93 |
94 | return $dropdown;
95 | }.bind(this);
96 |
97 |
98 | /**
99 | * @protected
100 | * @param {HTMLSelectElement} $dropdown
101 | */
102 | var observeChanges = function ($dropdown) {
103 | $dropdown.addEventListener('change', notify);
104 | }.bind(this);
105 |
106 |
107 | /**
108 | * @protected
109 | */
110 | var notify = function () {
111 |
112 | this.observer.notify('update');
113 | }.bind(this);
114 | };
115 |
116 | return JsonGeneratorRendererNumber;
117 | }
118 | );
119 |
--------------------------------------------------------------------------------
/spec/Flagbit/Bundle/TableAttributeBundle/Validator/ConstraintFactorySpec.php:
--------------------------------------------------------------------------------
1 | shouldHaveType(ConstraintFactory::class);
18 | }
19 |
20 | // ConstraintFactory::createByConstraintConfig()
21 |
22 | public function it_creates_symfony_constraints_by_config(ConstraintConfigInterface $constraintConfig)
23 | {
24 | $jsonConfig = [
25 | 'Email' => null,
26 | 'Range' => ['max' => 10, 'min' => 1]
27 | ];
28 |
29 | $constraintConfig->getConstraints()->willReturn($jsonConfig);
30 |
31 | $this->createByConstraintConfig($constraintConfig)->shouldHaveCount(2);
32 | $result = $this->createByConstraintConfig($constraintConfig);
33 |
34 | $result[0]->shouldBeAnInstanceOf(C\Email::class);
35 |
36 | $result[1]->shouldBeAnInstanceOf(C\Range::class);
37 | $result[1]->min->shouldBe(1);
38 | $result[1]->max->shouldBe(10);
39 | }
40 |
41 | public function it_creates_custom_constraints_by_config(ConstraintConfigInterface $constraintConfig)
42 | {
43 | $jsonConfig = [
44 | C\Email::class => null
45 | ];
46 |
47 | $constraintConfig->getConstraints()->willReturn($jsonConfig);
48 |
49 | $this->createByConstraintConfig($constraintConfig)->shouldHaveCount(1);
50 | $result = $this->createByConstraintConfig($constraintConfig);
51 |
52 | $result[0]->shouldBeAnInstanceOf(C\Email::class);
53 | }
54 |
55 | public function it_skips_on_unknown_constraints_by_config(ConstraintConfigInterface $constraintConfig)
56 | {
57 | $jsonConfig = [
58 | 'Foo' => null,
59 | 'Symfony\\Component\\Validator\\Constraints\\Foo' => null
60 | ];
61 |
62 | $constraintConfig->getConstraints()->willReturn($jsonConfig);
63 |
64 | $this->createByConstraintConfig($constraintConfig)->shouldHaveCount(0);
65 | }
66 |
67 | public function it_skips_on_other_classes_than_constraints(ConstraintConfigInterface $constraintConfig)
68 | {
69 | $jsonConfig = [
70 | 'ArrayObject' => null
71 | ];
72 |
73 | $constraintConfig->getConstraints()->willReturn($jsonConfig);
74 |
75 | $this->createByConstraintConfig($constraintConfig)->shouldHaveCount(0);
76 | }
77 |
78 | // ConstraintFactory::createCollectionConstraint()
79 |
80 | /**
81 | * @throws ExceptionInterface
82 | */
83 | public function it_creates_a_constraint_out_of_a_collection()
84 | {
85 | $constraints = [
86 | 'foo' => [new C\Email(), new C\IsNull()],
87 | ];
88 |
89 | $constraint = $this->createTableConstraint($constraints);
90 | $constraint->shouldBeAnInstanceOf(Table::class);
91 | $constraint->constraints->shouldHaveCount(1);
92 | $constraint->constraints[0]->shouldBeAnInstanceOf(C\Collection::class);
93 |
94 | $constraint->constraints[0]->fields->shouldHaveCount(1);
95 | $constraint->constraints[0]->fields->shouldHaveKey('foo');
96 | $constraint->constraints[0]->fields['foo']->constraints[0]->shouldBeAnInstanceOf(C\Email::class);
97 | $constraint->constraints[0]->fields['foo']->constraints[1]->shouldBeAnInstanceOf(C\IsNull::class);
98 | }
99 |
100 | public function it_throws_an_exception_on_invalid_collection_elements()
101 | {
102 | $constraints = [
103 | 'foo' => [new C\Email(), 'foo'],
104 | ];
105 |
106 | $this->shouldThrow(ConstraintDefinitionException::class)->during('createTableConstraint', [$constraints]);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/Resources/translations/jsmessages.en.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | flagbit.table_attribute.add_column.label
7 | Add a column
8 |
9 |
10 |
11 | flagbit.table_attribute.alert.json_error.text
12 | You have an error in your JSON format.
13 |
14 |
15 |
16 | flagbit.table_attribute.alert.json_error.title
17 | JSON format error
18 |
19 |
20 |
21 | flagbit.table_attribute.code.label
22 | Code
23 |
24 |
25 |
26 | flagbit.table_attribute.simpleselect_options.label
27 | Options
28 |
29 |
30 |
31 | flagbit.table_attribute.simpleselect_options_url.label
32 | Options URL
33 |
34 |
35 |
36 | flagbit.table_attribute.config.label
37 | Configuration
38 |
39 |
40 |
41 | flagbit.table_attribute.type.label
42 | Type
43 |
44 |
45 |
46 | flagbit.table_attribute.validation.label
47 | Validation
48 |
49 |
50 |
51 | flagbit.table_attribute.no_configuration.text
52 | There is no configuration options.
53 |
54 |
55 |
56 | flagbit.table_attribute.add_new_row.label
57 | Add a row
58 |
59 |
60 |
61 | flagbit.table_attribute.number_is_decimal.label
62 | Decimal
63 |
64 |
65 |
66 | flagbit.table_attribute.simpleselect_from_url.label
67 | Simple select from URL
68 |
69 |
70 |
71 | pim_enrich.entity.attribute.property.type.flagbit_catalog_table
72 | Table
73 |
74 |
75 |
76 | flagbit.table_attribute.form.attribute.tab.title
77 | Columns
78 |
79 |
80 |
81 | flagbit.table_attribute.simpleselect.key.label
82 | Key
83 |
84 |
85 |
86 | flagbit.table_attribute.simpleselect.value.label
87 | Value
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/tests/Kernel/config/packages/test/imports.yml:
--------------------------------------------------------------------------------
1 | # Imports every packages and packages/test YAML, except doctrine.yml and test/oneup_flysystem.yml
2 | imports:
3 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/akeneo_api.yml' }
4 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/akeneo_batch.yml' }
5 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/akeneo_elasticsearch.yml' }
6 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/akeneo_pim_enrichment.yml' }
7 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/akeneo_pim_user.yml' }
8 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/akeneo_storage_utils.yml' }
9 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/fos_auth_server.yml' }
10 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/fos_js_routing.yml' }
11 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/fos_rest.yml' }
12 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/framework.yml' }
13 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/test/framework.yml' }
14 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/liip_imagine.yml' }
15 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/oneup_flysystem.yml' }
16 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/test_fake/oneup_flysystem.yml' }
17 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/oro_filter.yml' }
18 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/oro_translation.yml' }
19 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/services/test/storage.yml' }
20 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/security.yml' }
21 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/test/security.yml' }
22 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/sensio_framework_extra.yml' }
23 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/swiftmailer.yml' }
24 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/twig.yml' }
25 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/services/gedmo_doctrine_extensions.yml' }
26 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/services/pim_parameters.yml' }
27 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/services/services.yml' }
28 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/services/pim.yml' }
29 | - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/monolog.yml' }
30 | # - { resource: '../../../../../vendor/akeneo/pim-community-dev/config/packages/doctrine.yml' }
31 |
32 | doctrine:
33 | dbal:
34 | default_connection: default
35 | connections:
36 | default:
37 | driver: 'pdo_mysql'
38 | dbname: '%env(APP_DATABASE_NAME)%'
39 | host: '%env(APP_DATABASE_HOST)%'
40 | port: '%env(APP_DATABASE_PORT)%'
41 | user: '%env(APP_DATABASE_USER)%'
42 | password: '%env(APP_DATABASE_PASSWORD)%'
43 | charset: utf8mb4
44 | default_table_options:
45 | charset: utf8mb4
46 | collate: utf8mb4_unicode_ci
47 | row_format: DYNAMIC
48 | server_version: '8.0'
49 | mapping_types:
50 | json: string
51 | types:
52 | datetime: Akeneo\Tool\Bundle\StorageUtilsBundle\Doctrine\DBAL\Types\UTCDateTimeType
53 | orm:
54 | auto_generate_proxy_classes: '%kernel.debug%'
55 | auto_mapping: true
56 | resolve_target_entities:
57 | placeholder: placeholder
58 | mappings:
59 | tree:
60 | type: annotation
61 | alias: Gedmo
62 | prefix: Gedmo\Tree\Entity
63 | dir: '%kernel.project_dir%/../../vendor/gedmo/doctrine-extensions/src/Tree/Entity'
64 |
--------------------------------------------------------------------------------
/src/Resources/translations/jsmessages.de.xlf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | flagbit.table_attribute.add_column.label
7 | Spalte hinzufügen
8 |
9 |
10 |
11 | flagbit.table_attribute.alert.json_error.text
12 | Ein Fehler wurde bei einem der JSON Felder festgestellt.
13 |
14 |
15 |
16 | flagbit.table_attribute.alert.json_error.title
17 | JSON Formatfehler
18 |
19 |
20 |
21 | flagbit.table_attribute.code.label
22 | Kode
23 |
24 |
25 |
26 | flagbit.table_attribute.simpleselect_options.label
27 | Optionen
28 |
29 |
30 |
31 | flagbit.table_attribute.simpleselect_options_url.label
32 | Optionen URL
33 |
34 |
35 |
36 | flagbit.table_attribute.config.label
37 | Konfiguration
38 |
39 |
40 |
41 | flagbit.table_attribute.type.label
42 | Typ
43 |
44 |
45 |
46 | flagbit.table_attribute.validation.label
47 | Validierung
48 |
49 |
50 |
51 | flagbit.table_attribute.no_configuration.text
52 | Es gibt keine Konfigurationsmöglichkeiten.
53 |
54 |
55 |
56 | flagbit.table_attribute.add_new_row.label
57 | Zeile hinzufügen
58 |
59 |
60 |
61 | flagbit.table_attribute.number_is_decimal.label
62 | Dezimal
63 |
64 |
65 |
66 | flagbit.table_attribute.simpleselect_from_url.label
67 | Einfachauswahl aus URL
68 |
69 |
70 |
71 | pim_enrich.entity.attribute.property.type.flagbit_catalog_table
72 | Tabelle
73 |
74 |
75 |
76 | flagbit.table_attribute.form.attribute.tab.title
77 | Spalten
78 |
79 |
80 |
81 | flagbit.table_attribute.simpleselect.key.label
82 | Key
83 |
84 |
85 |
86 | flagbit.table_attribute.simpleselect.value.label
87 | Wert
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/Resources/public/js/Json/Renderer.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'flagbit/JsonGenerator/Observer',
4 | 'flagbit/JsonGenerator/Renderer/Number',
5 | 'flagbit/JsonGenerator/Renderer/Select',
6 | 'flagbit/JsonGenerator/Renderer/SelectFromUrl',
7 | 'flagbit/JsonGenerator/Renderer/Constraint',
8 | 'flagbit/JsonGenerator/Renderer/Default'
9 | ],
10 | function (
11 | JsonGeneratorObserver,
12 | JsonGeneratorRendererNumber,
13 | JsonGeneratorRendererSelect,
14 | JsonGeneratorRendererSelectFromUrl,
15 | JsonGeneratorRendererConstraint,
16 | JsonGeneratorRendererDefault
17 | ) {
18 |
19 | /**
20 | * @class
21 | * @param {Boolean} $editable
22 | * @param {HTMLElement} $container
23 | */
24 | var JsonGeneratorRenderer = function ($editable, $container, $types) {
25 |
26 | /**
27 | * @public
28 | * @type {JsonGeneratorObserver}
29 | */
30 | this.observer = new JsonGeneratorObserver();
31 |
32 | var $renderer = null;
33 |
34 |
35 | /**
36 | * @public
37 | */
38 | this.render = function ($data) {
39 |
40 | return getRenderer().render($data);
41 | };
42 |
43 |
44 | /**
45 | * @public
46 | * @returns {Object}
47 | */
48 | this.read = function () {
49 |
50 | return getRenderer().read();
51 | };
52 |
53 |
54 | /**
55 | * @protected
56 | * @returns {*}
57 | */
58 | var getRenderer = function () {
59 |
60 | var renderers = {
61 | 'select': JsonGeneratorRendererSelect,
62 | 'select_from_url': JsonGeneratorRendererSelectFromUrl,
63 | 'text': JsonGeneratorRendererDefault,
64 | 'number': JsonGeneratorRendererNumber
65 | };
66 |
67 | if ($renderer === null) {
68 | if ($container.querySelector('.json-select-generator')) {
69 | $renderer = new JsonGeneratorRendererSelect($editable, $container);
70 | } else if ($container.querySelector('.json-select_from_url-generator')) {
71 | $renderer = new JsonGeneratorRendererSelectFromUrl($editable, $container);
72 | } else if ($container.querySelector('.json-number-generator')) {
73 | $renderer = new JsonGeneratorRendererNumber($editable, $container);
74 | } else if ($container.querySelector('.json-constraint-generator')) {
75 | $renderer = new JsonGeneratorRendererConstraint($editable, $container);
76 | } else if ($container.querySelector('.json-text-generator')) {
77 | $renderer = new JsonGeneratorRendererDefault($editable, $container);
78 | } else {
79 | $renderer = new renderers[$types[0].type]($editable, $container);
80 | }
81 | }
82 |
83 | return $renderer;
84 | }.bind(this);
85 |
86 |
87 | /**
88 | * @protected
89 | */
90 | var persist = function () {
91 |
92 | this.observer.notify('persist');
93 | }.bind(this);
94 |
95 |
96 | /**
97 | * @protected
98 | */
99 | var save = function () {
100 |
101 | this.observer.notify('save');
102 | }.bind(this);
103 |
104 |
105 | /**
106 | * @protected
107 | */
108 | var addObserver = function () {
109 |
110 | getRenderer().observer.watch('update', persist);
111 | getRenderer().observer.watch('update', callDebounce(save));
112 | }.bind(this);
113 |
114 |
115 | /**
116 | * @protected
117 | * @param {Function} $callable
118 | */
119 | var callDebounce = function ($callable) {
120 |
121 | var $debounceTimer = null;
122 |
123 | return function () {
124 |
125 | if ($debounceTimer) {
126 | window.clearTimeout($debounceTimer);
127 | }
128 |
129 | $debounceTimer = window.setTimeout($callable, 300);
130 | }.bind(this)
131 | }.bind(this);
132 |
133 |
134 | addObserver();
135 | };
136 |
137 | return JsonGeneratorRenderer;
138 | }
139 | );
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Flagbit Table Attribute for Akeneo PIM
3 |
4 |
5 |
6 | Adds the new attribute type Table for Akeneo products.
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Key Features •
26 | Installation •
27 | Compatibility •
28 | Development •
29 | Contributing
30 |
31 |
32 | ## Key Features
33 |
34 | Provides a _table_ as attribute type where you can define a set of columns of different types and validation rules.
35 |
36 | #### Column Types
37 |
38 | * Text
39 | * Number (Integer or Decimal)
40 | * Simple select
41 | * Simple select from URL
42 |
43 | #### Import/Export
44 |
45 | The extension supports the standard Akeneo product import/export, so you don't need to create any special import/export
46 | profile for table information.
47 |
48 | All product information related to attributes of type _table_ will be imported/exported as JSON.
49 |
50 | ## Installation
51 |
52 | Simply install the package with the following command:
53 |
54 | ``` bash
55 | composer require flagbit/table-attribute-bundle
56 | ```
57 |
58 | ### Enable the bundle
59 |
60 | Enable the bundle in the kernel:
61 |
62 | ``` php
63 | ['all' => true],
69 | ];
70 | ```
71 |
72 | #### Configuration
73 |
74 | Add `mapping_overrides` in a new `config/packages/table.yml` file or an existing one:
75 |
76 | ``` yml
77 | akeneo_storage_utils:
78 | mapping_overrides:
79 | -
80 | original: Akeneo\Pim\Structure\Component\Model\AttributeOption
81 | override: Flagbit\Bundle\TableAttributeBundle\Entity\AttributeOption
82 | ```
83 |
84 | #### Import the routing
85 |
86 | Now that you have activated and configured the bundle, you need to import the routing files.
87 |
88 | ``` yml
89 | # config/routes/flagbit_table_attribute.yml
90 | flagbit_table_attribute:
91 | resource: "@FlagbitTableAttributeBundle/Resources/config/routing.yml"
92 | ```
93 |
94 | Clear the cache:
95 |
96 | ``` bash
97 | php bin/console --env=prod cache:clear
98 | ```
99 |
100 | Update the database schema:
101 |
102 | ``` bash
103 | php bin/console --env=prod doctrine:schema:update --force
104 | ```
105 |
106 | Build and install the new front-end dependencies (new icon, etc.)
107 |
108 | ``` bash
109 | make cache assets css javascript-prod javascript-extensions
110 | ```
111 |
112 | In case you're using Doctrine migrations, you have to create a new migration class
113 |
114 | ``` bash
115 | php bin/console --env=prod doctrine:migration:diff
116 | ```
117 |
118 | and migrate the schema updates:
119 |
120 | ``` bash
121 | php bin/console --env=prod doctrine:migrations:migrate
122 | ```
123 |
124 | ## Compatibility
125 |
126 | This extension supports the latest Akeneo PIM CE/EE stable versions:
127 |
128 | * 6.0
129 | * 5.0
130 | * 4.0
131 | * 3.2 (LTS)
132 | * 3.0 (LTS)
133 | * 2.3 (LTS)
134 |
135 | ## Development
136 |
137 | ### Running Test-Suits
138 |
139 | The TableAttributeBundle is covered with tests and every change and addition has also to be covered with
140 | unit or/and integration tests. It uses two testing suits: [PHPSpec](https://www.phpspec.net) and
141 | [PHPUnit](https://phpunit.de/).
142 |
143 | To run the tests you have to change to this project's root directory and run the following commands in your console:
144 |
145 | ``` bash
146 | vendor/bin/phpunit
147 | vendor/bin/phpspec run
148 | ```
149 |
150 | ### Coding style
151 |
152 | TableAttributeBundle uses the [PSR-2](https://www.php-fig.org/psr/psr-2/) coding style and can be checked with
153 | [Codesniffer](https://github.com/squizlabs/PHP_CodeSniffer).
154 |
155 | ``` bash
156 | vendor/bin/phpcs --standard=PSR2 --extensions=php ./src
157 | ```
158 |
159 | ## Contributing
160 |
161 | Contributions are always welcome! Please have a look at the [contribution guidelines](CONTRIBUTING.md) first.
162 |
163 | ## License
164 |
165 | The TableAttributeBundle is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
166 |
167 | #
168 |
169 |
170 | Supported with ❤ by Flagbit GmbH & Co. KG
171 |
172 |
--------------------------------------------------------------------------------
/src/Resources/public/js/Json/Renderer/Select.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'flagbit/JsonGenerator/Observer',
4 | 'oro/translator',
5 | ],
6 | function (JsonGeneratorObserver, __) {
7 |
8 | /**
9 | * @class
10 | * @param {Boolean} $editable
11 | * @param {HTMLElement} $container
12 | */
13 | var JsonGeneratorRendererSelect = function ($editable, $container) {
14 |
15 | /**
16 | * @protected
17 | * @type {HTMLTableElement}
18 | */
19 | var $table = null;
20 | /**
21 | * @public
22 | * @type {JsonGeneratorObserver}
23 | */
24 | this.observer = new JsonGeneratorObserver();
25 |
26 | /**
27 | * @public
28 | * @param {Object} $data
29 | */
30 | this.render = function ($data) {
31 |
32 | var $label = document.createElement('label');
33 | $label.innerText = __('flagbit.table_attribute.simpleselect_options.label');
34 | $label.className = 'select-options-config-label';
35 | $container.appendChild($label);
36 |
37 | // needed!
38 | this.getTable();
39 |
40 | for (var $key in $data.options) {
41 | if ($data.options.hasOwnProperty($key)) {
42 | var $value = $data.options[$key];
43 |
44 | var $keyCol = createTableColumn($key);
45 | var $valCol = createTableColumn($value);
46 | var $row = document.createElement('tr');
47 |
48 | $row.appendChild($keyCol);
49 | $row.appendChild($valCol);
50 | if ($editable) {
51 | $row.appendChild(createDeleteButton($row));
52 | }
53 | this.getTable().appendChild($row);
54 | }
55 | }
56 | };
57 |
58 |
59 | /**
60 | * @public
61 | * @returns {Object}
62 | */
63 | this.read = function () {
64 |
65 | var $data = {options: {}};
66 |
67 | var $collection = this.getTable().querySelectorAll('tr');
68 | for (var $i in $collection) {
69 | if ($collection.hasOwnProperty($i)) {
70 | var $tr = $collection[$i];
71 | $data.options[$tr.querySelectorAll('td input')[0].value] = $tr.querySelectorAll('td input')[1].value;
72 | }
73 | }
74 |
75 | return $data;
76 | };
77 |
78 | /**
79 | * @public
80 | * @returns {HTMLTableSectionElement}
81 | */
82 | this.getTable = function () {
83 |
84 | if ($table === null) {
85 | $table = document.createElement('table');
86 | $table.className = 'AknGrid AknGrid--unclickable select-options-table';
87 |
88 | var $thead = document.createElement('thead');
89 | var $thRow = document.createElement('tr');
90 | $thRow.className = 'AknGrid-bodyRow';
91 | var $thCl1 = document.createElement('th');
92 | $thCl1.className = 'AknGrid-headerCell';
93 | $thCl1.innerText = __('flagbit.table_attribute.simpleselect.key.label');
94 | var $thCl2 = document.createElement('th');
95 | $thCl2.className = 'AknGrid-headerCell';
96 | $thCl2.innerText = __('flagbit.table_attribute.simpleselect.value.label');
97 |
98 | $thRow.appendChild($thCl1);
99 | $thRow.appendChild($thCl2);
100 | $thead.appendChild($thRow);
101 | $table.appendChild($thead);
102 |
103 | var $tbody = document.createElement('tbody');
104 | $table.appendChild($tbody);
105 |
106 | if ($editable) {
107 | var $tfoot = document.createElement('tfoot');
108 | var $tfRow = document.createElement('tr');
109 | var $tfCol = document.createElement('td');
110 | $tfCol.className = 'AknGrid-bodyCell field-cell';
111 | var $tfBut = document.createElement('button');
112 |
113 | $tfBut.innerText = __('pim_enrich.entity.product.module.attribute.add_option');
114 | $tfBut.type = 'button';
115 | $tfBut.className = 'btn AknButton AknButton--small pull-right';
116 | $tfBut.addEventListener('click', addRow);
117 | $tfCol.colSpan = 3;
118 | $tfCol.appendChild($tfBut);
119 | $tfRow.appendChild($tfCol);
120 | $tfoot.appendChild($tfRow);
121 | $table.appendChild($tfoot);
122 | }
123 |
124 | $container.appendChild($table);
125 | }
126 |
127 | return $table.querySelector('tbody');
128 | };
129 |
130 |
131 | /**
132 | * @protected
133 | */
134 | var addRow = function () {
135 |
136 | var $row = document.createElement('tr');
137 | $row.appendChild(createTableColumn(''));
138 | $row.appendChild(createTableColumn(''));
139 | if ($editable) {
140 | $row.appendChild(createDeleteButton($row));
141 | }
142 |
143 | this.getTable().appendChild($row);
144 |
145 | notify();
146 | }.bind(this);
147 |
148 |
149 | /**
150 | * @protected
151 | * @param {String} $text
152 | */
153 | var createTableColumn = function ($text) {
154 |
155 | var $column = document.createElement('td');
156 |
157 | if ($editable) {
158 | var $input = document.createElement('input');
159 | $input.type = 'text';
160 | $input.value = $text;
161 | $input.className = 'AknTextField';
162 | observeChanges($input);
163 | $column.appendChild($input);
164 | } else {
165 | $column.innerText = $text;
166 | }
167 |
168 | return $column;
169 | }.bind(this);
170 |
171 |
172 | /**
173 | * @protected
174 | * @param {HTMLTableRowElement} $row
175 | */
176 | var createDeleteButton = function ($row) {
177 | var $col = createTableColumn();
178 | $col.innerHTML = '';
179 | $col.querySelector('span').addEventListener(
180 | 'click', function () {
181 | $row.parentNode.removeChild($row);
182 | notify();
183 | }
184 | );
185 |
186 | return $col;
187 | }.bind(this);
188 |
189 |
190 | /**
191 | * @protected
192 | * @param {HTMLInputElement} $input
193 | */
194 | var observeChanges = function ($input) {
195 | $input.addEventListener('keyup', notify);
196 | $input.addEventListener('blur', notify);
197 | }.bind(this);
198 |
199 |
200 | /**
201 | * @protected
202 | */
203 | var notify = function () {
204 |
205 | this.observer.notify('update');
206 | }.bind(this);
207 | };
208 |
209 | return JsonGeneratorRendererSelect;
210 | }
211 | );
212 |
--------------------------------------------------------------------------------
/tests/Pim/TagsAndServiceOverridesTest.php:
--------------------------------------------------------------------------------
1 | get('pim_connector.array_converter.flat_to_standard.product.value_converter.registry');
28 |
29 | $converter = $registryValueConverter->getConverter('flagbit_catalog_table');
30 |
31 | self::assertInstanceOf(FlatToStandardTextConverter::class, $converter);
32 | }
33 |
34 | public function testStandardToFlatConverterRegisters()
35 | {
36 | self::bootKernel();
37 | $container = self::getContainer();
38 |
39 | $registryValueConverter = $container->get('pim_connector.array_converter.standard_to_flat.product.value_converter.registry');
40 |
41 | $attribute = new Attribute();
42 | $attribute->setType('flagbit_catalog_table');
43 |
44 | $converter = $registryValueConverter->getConverter($attribute);
45 |
46 | self::assertInstanceOf(StandardToFlatTextConverter::class, $converter);
47 | }
48 |
49 | public function testAttributeComparedSuccessfully()
50 | {
51 | self::bootKernel();
52 | $container = self::getContainer();
53 |
54 | $registryComparator = $container->get('pim_catalog.comparator.registry');
55 |
56 | $comparator = $registryComparator->getAttributeComparator('flagbit_catalog_table');
57 |
58 | self::assertInstanceOf(ScalarComparator::class, $comparator);
59 | }
60 |
61 | public function testScalarValueCreated()
62 | {
63 | self::bootKernel();
64 | $container = self::getContainer();
65 |
66 | $valueFactory = $container->get('akeneo.pim.enrichment.factory.value');
67 |
68 | $attribute = new PublicApiAttribute(
69 | 'foo',
70 | 'flagbit_catalog_table',
71 | [],
72 | false,
73 | false,
74 | null,
75 | null,
76 | null,
77 | 'flagbit_catalog_table',
78 | []
79 | );
80 |
81 | $value = $valueFactory->createWithoutCheckingData($attribute, null, null, '{}');
82 |
83 | self::assertEquals(ScalarValue::value('foo', '{}'), $value);
84 | }
85 |
86 | public function testProductUpdatedSuccessfully()
87 | {
88 | self::bootKernel();
89 | $container = self::getContainer();
90 |
91 | $repository = $this->createMock(IdentifiableObjectRepositoryInterface::class);
92 |
93 | $container->set('pim_catalog.repository.cached_attribute', $repository);
94 |
95 | $registryUpdater = $container->get('pim_catalog.updater.setter.registry');
96 |
97 | $attribute = new Attribute();
98 | $attribute->setType('flagbit_catalog_table');
99 |
100 | $updater = $registryUpdater->getAttributeSetter($attribute);
101 |
102 | self::assertInstanceOf(AttributeSetter::class, $updater);
103 | }
104 |
105 | public function testSupportsTableMaskItem()
106 | {
107 | self::bootKernel();
108 | $container = self::getContainer();
109 |
110 | $maskItemGenerator = $container->get('akeneo.pim.enrichment.completeness.mask_item_generator.generator');
111 |
112 | $tableMask = $maskItemGenerator->generate('code', TableType::FLAGBIT_CATALOG_TABLE, 'channel', 'locale', 'value');
113 |
114 | self::assertSame(['code-channel-locale'], $tableMask);
115 | }
116 |
117 | public function testSupportsTableValueFactory()
118 | {
119 | self::bootKernel();
120 | $container = self::getContainer();
121 |
122 | $attribute = new PublicApiAttribute(
123 | 'foo',
124 | 'flagbit_catalog_table',
125 | [],
126 | false,
127 | false,
128 | null,
129 | null,
130 | null,
131 | 'flagbit_catalog_table',
132 | []
133 | );
134 |
135 | $valueFactory = $container->get('akeneo.pim.enrichment.factory.value');
136 |
137 | $tableValue = $valueFactory->createByCheckingData($attribute, null, null, '{}');
138 |
139 | self::assertInstanceOf(ScalarValue::class, $tableValue);
140 | self::assertSame('foo', $tableValue->getAttributeCode());
141 | self::assertSame('{}', $tableValue->getData());
142 |
143 | $tableValue = $valueFactory->createWithoutCheckingData($attribute, null, null, '{}');
144 |
145 | self::assertInstanceOf(ScalarValue::class, $tableValue);
146 | self::assertSame('foo', $tableValue->getAttributeCode());
147 | self::assertSame('{}', $tableValue->getData());
148 | }
149 |
150 | /**
151 | * @dataProvider queryBuildersProvider
152 | */
153 | public function testQueryBuilderFiltersCorrectly($operator, $service)
154 | {
155 | self::bootKernel();
156 | $container = self::getContainer();
157 |
158 | $attribute = new Attribute();
159 | $attribute->setType('flagbit_catalog_table');
160 |
161 | $repository = $this->createMock(AttributeRepositoryInterface::class);
162 | $repository->expects(self::once())
163 | ->method('findOneBy')
164 | ->willReturn($attribute);
165 |
166 | $container->set('pim_catalog.repository.attribute', $repository);
167 |
168 | /**
169 | * @var FilterRegistry $filterRegistry
170 | */
171 | $filterRegistry = $container->get($service);
172 |
173 | $filter = $filterRegistry->getFilter('flagbit_catalog_table', $operator);
174 |
175 | self::assertInstanceOf(OptionFilter::class, $filter);
176 | }
177 |
178 | /**
179 | * @dataProvider queryBuildersProvider
180 | */
181 | public function testQueryBuilderAttributeFiltersCorrectly($operator, $service)
182 | {
183 | self::bootKernel();
184 | $container = self::getContainer();
185 |
186 | $attribute = new Attribute();
187 | $attribute->setType('flagbit_catalog_table');
188 |
189 | /**
190 | * @var FilterRegistry $filterRegistry
191 | */
192 | $filterRegistry = $container->get($service);
193 |
194 | $filter = $filterRegistry->getAttributeFilter($attribute, $operator);
195 |
196 | self::assertInstanceOf(OptionFilter::class, $filter);
197 | }
198 |
199 | public function queryBuildersProvider(): array
200 | {
201 | return [
202 | ['IN', 'pim_catalog.query.filter.product_registry'],
203 | ['EMPTY', 'pim_catalog.query.filter.product_registry'],
204 | ['NOT EMPTY', 'pim_catalog.query.filter.product_registry'],
205 | ['NOT IN', 'pim_catalog.query.filter.product_registry'],
206 | ['IN', 'pim_catalog.query.filter.product_model_registry'],
207 | ['EMPTY', 'pim_catalog.query.filter.product_model_registry'],
208 | ['NOT EMPTY', 'pim_catalog.query.filter.product_model_registry'],
209 | ['NOT IN', 'pim_catalog.query.filter.product_model_registry'],
210 | ['IN', 'pim_catalog.query.filter.product_and_product_model_registry'],
211 | ['EMPTY', 'pim_catalog.query.filter.product_and_product_model_registry'],
212 | ['NOT EMPTY', 'pim_catalog.query.filter.product_and_product_model_registry'],
213 | ['NOT IN', 'pim_catalog.query.filter.product_and_product_model_registry'],
214 | // service doesn't exist in community. See tests/Kernel/config/packages/test/ee-services.yml
215 | ['IN', 'pimee_workflow.query.filter.product_proposal_registry'],
216 | ['EMPTY', 'pimee_workflow.query.filter.product_proposal_registry'],
217 | ['NOT EMPTY', 'pimee_workflow.query.filter.product_proposal_registry'],
218 | ['NOT IN', 'pimee_workflow.query.filter.product_proposal_registry'],
219 | // service doesn't exist in community. See tests/Kernel/config/packages/test/ee-services.yml
220 | ['IN', 'pimee_workflow.query.filter.published_product_registry'],
221 | ['EMPTY', 'pimee_workflow.query.filter.published_product_registry'],
222 | ['NOT EMPTY', 'pimee_workflow.query.filter.published_product_registry'],
223 | ['NOT IN', 'pimee_workflow.query.filter.published_product_registry'],
224 | ];
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/Resources/public/js/product/field/table-field.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | define(
3 | [
4 | 'pim/field',
5 | 'underscore',
6 | 'jquery',
7 | 'flagbit/template/product/field/table',
8 | 'routing',
9 | 'flagbit/inittable',
10 | 'pim/user-context',
11 | 'pim/i18n'
12 | ], function (
13 | Field,
14 | _,
15 | $,
16 | fieldTemplate,
17 | Routing,
18 | initTable,
19 | UserContext,
20 | i18n
21 | ) {
22 | return Field.extend(
23 | {
24 | fieldTemplate: _.template(fieldTemplate),
25 | events: {
26 | 'change .field-input:first .table-data': 'updateModel',
27 | 'change .field-input:first .flagbit-table-values': 'updateJson',
28 | 'click .field-input:first .flagbit-table-values .delete-row': 'deleteItem',
29 | 'click .field-input:first .flagbit-table-attribute .item-add': 'addItem'
30 | },
31 | columns: {},
32 | renderInput: function (context) {
33 | return this.fieldTemplate(context);
34 | },
35 | postRender: function () {
36 | this.getColumnUrl().then(
37 | function (columnUrl) {
38 |
39 | $.ajax(
40 | {
41 | async: true,
42 | type: 'GET',
43 | url: columnUrl,
44 | success: function (response) {
45 | if (response) {
46 | this.columns = {};
47 | _.each(
48 | response, function (value) {
49 | var column = this.convertBackendItem(value);
50 | this.columns[column.id] = column;
51 | }.bind(this)
52 | );
53 | initTable.init(this.$('.flagbit-table-attribute'), this.columns);
54 | // initialize dran & drop sorting
55 | this.$('.flagbit-table-values tbody').sortable(
56 | {
57 | handle: ".handle",
58 | stop: function () {
59 | this.updateJson();
60 | }.bind(this)
61 | }
62 | );
63 | }
64 | }.bind(this)
65 | }
66 | );
67 | }.bind(this)
68 | );
69 | },
70 | getColumnUrl: function () {
71 | return $.Deferred().resolve(
72 | Routing.generate(
73 | 'pim_enrich_attributeoption_get',
74 | {
75 | identifier: this.attribute.code
76 | }
77 | )
78 | ).promise();
79 | },
80 | updateModel: function () {
81 | var data = this.$('.field-input:first .table-data').val();
82 |
83 | this.setCurrentValue(data);
84 | },
85 | updateJson: function () {
86 | var rows = this.$('.flagbit-table-values tr.flagbit-table-row');
87 |
88 | var values = [];
89 | var columns = this.columns;
90 | _.each(
91 | rows, function (row) {
92 | var fields = {};
93 |
94 | _.each(
95 | $('td[data-code]', row), function (td) {
96 | var id = $(td).data('code');
97 | fields[id] = columns[id].func.parseValue($(td));
98 | }
99 | );
100 |
101 | values.push(fields);
102 | }
103 | );
104 |
105 | var valuesAsJson = JSON.stringify(values);
106 | this.$('.field-input:first .table-data').val(valuesAsJson);
107 | this.setCurrentValue(valuesAsJson);
108 | },
109 | deleteItem: function (event) {
110 | $(event.currentTarget).closest('tr').remove();
111 | this.updateJson();
112 | },
113 | addItem: function () {
114 | this.$('table.flagbit-table-values').append(initTable.createEmptyRow(this.columns));
115 | },
116 | convertBackendItem: function (item) {
117 | return {
118 | id: item.code,
119 | text: i18n.getLabel(item.labels, UserContext.get('catalogLocale'), item.code),
120 | config: item.type_config,
121 | type: item.type,
122 | func: this.createColumnFunctions(item)
123 | };
124 | },
125 | createColumnFunctions: function (item) {
126 | var fieldTemplate;
127 | var parser = function (td) {
128 | return $('input', td).val();
129 | };
130 | var init = function (td, column, value) {
131 | };
132 | var formTypeValue = function (value) {
133 | if (typeof value === 'undefined' || null === value) {
134 | return '';
135 | }
136 |
137 | return value.toString();
138 | };
139 |
140 | switch (item.type) {
141 | case "text":
142 | fieldTemplate = "";
143 | break;
144 | case "number":
145 | fieldTemplate = "' />";
146 | if ('is_decimal' in item.type_config && item.type_config.is_decimal === true) {
147 | parser = function (td) {
148 | return parseFloat($('input', td).val());
149 | };
150 | } else {
151 | parser = function (td) {
152 | return parseInt($('input', td).val());
153 | };
154 | }
155 | break;
156 | case "select":
157 | fieldTemplate = "";
158 |
159 | parser = function (td) {
160 | var option = $('input', td).select2('data');
161 |
162 | if (Array.isArray(option)) {
163 | return null;
164 | }
165 |
166 | return option.id;
167 | };
168 |
169 | init = function (td, column, value) {
170 | var select2Config = {
171 | placeholder: ' ',
172 | dropdownAutoWidth: true
173 | };
174 | if ('options' in column.config) {
175 | var options = [];
176 | _.each(
177 | column.config.options, function (option, key) {
178 | options.push({ id: key, text: option });
179 | }
180 | );
181 | select2Config.data = options;
182 | }
183 |
184 | var select2 = $('input', td).select2(select2Config);
185 | select2.on(
186 | 'select2-close', function () {
187 | this.updateJson();
188 | }.bind(this)
189 | );
190 | }.bind(this);
191 | break;
192 | case "select_from_url":
193 | fieldTemplate = "";
194 |
195 | parser = function (td) {
196 | var option = $('input', td).select2('data');
197 |
198 | if (Array.isArray(option)) {
199 | return null;
200 | }
201 |
202 | return option.id;
203 | };
204 |
205 | init = function (td, column, value) {
206 | var select2Config = {
207 | placeholder: ' ',
208 | dropdownAutoWidth: true
209 | };
210 | if ('options_url' in column.config) {
211 | select2Config.ajax = {
212 | url: column.config.options_url,
213 | cache: true,
214 | minimumInputLength: 0,
215 | dataType: 'json',
216 | quietMillis: 1000,
217 | results: function (data) {
218 | return data;
219 | },
220 | data: function (term) {
221 | return {
222 | q: term
223 | };
224 | }
225 | };
226 | // initSelection needs to be cleaned up in the future without forcing a whole API
227 | select2Config.initSelection = function (element, callback) {
228 | var option = $(element).val();
229 |
230 | if (option !== '') {
231 | $.ajax(
232 | column.config.options_url, {
233 | dataType: "json",
234 | cache: true
235 | }
236 | ).done(
237 | function (data) {
238 | var selected = _.find(
239 | data.results, function (row) {
240 | return row.id === option;
241 | }
242 | );
243 | callback(selected);
244 | }
245 | );
246 | }
247 | };
248 | }
249 |
250 | var select2 = $('input', td).select2(select2Config);
251 | select2.on(
252 | 'select2-close', function () {
253 | this.updateJson();
254 | }.bind(this)
255 | );
256 | }.bind(this);
257 | break;
258 | default:
259 | throw "Unknown type '"+item.type+"'";
260 | }
261 |
262 | return {
263 | renderField: _.template(fieldTemplate), // renders the template of the field
264 | parseValue: parser, // parses the value into the proper type for the json result
265 | init: init, // an optional function that allows to initialize third party plugins
266 | formTypeValue: formTypeValue // changes the value when it is put to the form field(s)
267 | };
268 | }
269 | }
270 | );
271 | }
272 | );
273 |
--------------------------------------------------------------------------------
/src/Resources/public/js/tablecolumnview.js:
--------------------------------------------------------------------------------
1 | define(
2 | [
3 | 'jquery',
4 | 'underscore',
5 | 'backbone',
6 | 'oro/translator',
7 | 'routing',
8 | 'oro/mediator',
9 | 'oro/loading-mask',
10 | 'pim/dialog',
11 | 'flagbit/JsonGenerator',
12 | 'jquery-ui'
13 | ],
14 | function ($, _, Backbone, __, Routing, mediator, LoadingMask, Dialog, JsonGenerator) {
15 | 'use strict';
16 |
17 | var AttributeOptionItem = Backbone.Model.extend(
18 | {
19 | defaults: {
20 | code: '',
21 | optionValues: {},
22 | constraints: {},
23 | type_config: {}
24 | },
25 | attributesToJson: function () {
26 | if (typeof this.get('constraints') === 'object') {
27 | this.set('constraints', JSON.stringify(this.get('constraints')));
28 | }
29 | if (typeof this.get('type_config') === 'object') {
30 | this.set('type_config', JSON.stringify(this.get('type_config')));
31 | }
32 | }
33 | }
34 | );
35 |
36 | var ItemCollection = Backbone.Collection.extend(
37 | {
38 | model: AttributeOptionItem,
39 | initialize: function (options) {
40 | this.url = options.url;
41 | }
42 | }
43 | );
44 |
45 | var EditableItemView = Backbone.View.extend(
46 | {
47 | tagName: 'tr',
48 | className: 'editable-item-row AknGrid-bodyRow',
49 | showTemplate: _.template(
50 | '' +
51 | '' +
52 | '<%= item.code %>' +
53 | ' | ' +
54 | '<% _.each(locales, function (locale) { %>' +
55 | '' +
56 | '<% if (item.optionValues[locale]) { %>' +
57 | '' +
58 | '<%= item.optionValues[locale].value %>' +
59 | '' +
60 | '<% } %>' +
61 | ' | ' +
62 | '<% }); %>' +
63 | '' +
64 | '<% _.each(types, function (type) { %>' +
65 | '<% if (item.type == type.type) { %>' +
66 | '' +
67 | '<%= type.label %>' +
68 | '' +
69 | '<% } %>' +
70 | '<% }); %>' +
71 | ' | ' +
72 | '' +
73 | '<%= item.constraints %>' +
74 | ' | ' +
75 | '' +
76 | '<%= item.type_config %>' +
77 | ' | ' +
78 | '' +
79 | ' ' +
80 | '' +
81 | '' +
82 | ' ' +
83 | ' | '
84 | ),
85 | editTemplate: _.template(
86 | '' +
87 | ' ' +
88 | '' +
89 | '' +
90 | ' ' +
91 | ' | ' +
92 | '<% _.each(locales, function (locale) { %>' +
93 | '' +
94 | '<% if (item.optionValues[locale]) { %>' +
95 | '"/>' +
97 | '<% } else { %>' +
98 | '' +
100 | '<% } %>' +
101 | ' | ' +
102 | '<% }); %>' +
103 | '' +
104 | '' +
109 | ' | ' +
110 | '' +
111 | '' +
112 | ' | ' +
113 | '' +
114 | '' +
115 | ' | ' +
116 | '' +
117 | ' ' +
118 | '' +
119 | '' +
120 | ' ' +
121 | ' | '
122 | ),
123 | events: {
124 | 'click .show-row': 'stopEditItem',
125 | 'click .edit-row': 'startEditItem',
126 | 'click .delete-row': 'deleteItem',
127 | 'click .update-row': 'updateItem',
128 | 'keyup input': 'soil',
129 | 'keydown': 'cancelSubmit',
130 | 'change .attribute_option_type': 'changeType'
131 | },
132 | editable: false,
133 | parent: null,
134 | loading: false,
135 | locales: [],
136 | initialize: function (options) {
137 | this.locales = options.locales;
138 | this.parent = options.parent;
139 | this.model.urlRoot = this.parent.updateUrl;
140 | this.types = [{
141 | 'type': 'text',
142 | 'label': __('pim_enrich.entity.attribute.property.type.pim_catalog_text')
143 | }, {
144 | 'type': 'number',
145 | 'label': __('pim_enrich.entity.attribute.property.type.pim_catalog_number')
146 | }, {
147 | 'type': 'select',
148 | 'label': __('pim_enrich.entity.attribute.property.type.pim_catalog_simpleselect')
149 | }, {
150 | 'type': 'select_from_url',
151 | 'label': __('flagbit.table_attribute.simpleselect_from_url.label')
152 | }];
153 |
154 | this.render();
155 | },
156 | render: function () {
157 | var template = null;
158 | var types = this.types;
159 |
160 | if (this.editable) {
161 | this.clean();
162 | this.$el.addClass('in-edition');
163 | template = this.editTemplate;
164 | } else {
165 | this.$el.removeClass('in-edition');
166 | template = this.showTemplate;
167 | }
168 |
169 | this.model.attributesToJson();
170 | this.$el.html(
171 | template(
172 | {
173 | item: this.model.toJSON(),
174 | locales: this.locales,
175 | types: types
176 | }
177 | )
178 | );
179 |
180 | this.$el.find('.json-generator').each(
181 | function () {
182 | new JsonGenerator(this, types);
183 | }
184 | ).bind(types);
185 |
186 | this.$el.attr('data-item-id', this.model.id);
187 |
188 | return this;
189 | },
190 | changeType: function () {
191 | this.inLoading(true);
192 | var editedModel = this.loadModelFromView();
193 | this.model.set(editedModel.attributes);
194 | this.render();
195 | this.inLoading(false);
196 | this.clean();
197 | },
198 | showReadableItem: function () {
199 | this.editable = false;
200 | this.parent.showReadableItem(this);
201 | this.clean();
202 | this.render();
203 | },
204 | showEditableItem: function () {
205 | this.editable = true;
206 | this.render();
207 | this.model.set(this.loadModelFromView().attributes);
208 | },
209 | startEditItem: function () {
210 | var rowIsEditable = this.parent.requestRowEdition(this);
211 |
212 | if (rowIsEditable) {
213 | this.showEditableItem();
214 | }
215 | },
216 | stopEditItem: function () {
217 | if (!this.model.id || this.dirty) {
218 | if (this.dirty) {
219 | Dialog.confirm(
220 | __('pim_enrich.entity.attribute_option.module.edit.cancel_description'),
221 | __('pim_enrich.entity.attribute_option.module.edit.cancel_title'),
222 | function () {
223 | this.showReadableItem(this);
224 | if (!this.model.id) {
225 | this.deleteItem();
226 | }
227 | }.bind(this)
228 | );
229 | } else {
230 | if (!this.model.id) {
231 | this.deleteItem();
232 | } else {
233 | this.showReadableItem();
234 | }
235 | }
236 | } else {
237 | this.showReadableItem();
238 | }
239 | },
240 | deleteItem: function () {
241 | var itemCode = this.el.firstChild.innerText;
242 |
243 | Dialog.confirm(
244 | __('pim_enrich.entity.fallback.module.delete.item_placeholder', {'itemName': itemCode}),
245 | __('pim_enrich.entity.fallback.module.delete.title', {'itemName': itemCode}),
246 | function () {
247 | this.parent.deleteItem(this);
248 | }.bind(this)
249 | );
250 | },
251 | updateItem: function () {
252 | this.inLoading(true);
253 |
254 | var editedModel = this.loadModelFromView();
255 |
256 | editedModel.save(
257 | {},
258 | {
259 | url: this.model.url(),
260 | success: function () {
261 | this.inLoading(false);
262 | this.model.set(editedModel.attributes);
263 | this.clean();
264 | this.stopEditItem();
265 | }.bind(this),
266 | error: function (data, xhr) {
267 | this.inLoading(false);
268 |
269 | var response = xhr.responseJSON;
270 | var _response = response;
271 | if (_response.children) {
272 | _response = _response.children;
273 | }
274 | if (_response && _response.code) {
275 | var error = _response.code;
276 | var message = '';
277 | if (_response.code) {
278 | if (_response.code.errors) {
279 | message = _response.code.errors.join('
');
280 | } else {
281 | message = _response.code;
282 | }
283 | }
284 | this.$el.find('.validation-tooltip')
285 | .addClass('visible')
286 | .tooltip('destroy')
287 | .tooltip({title: message})
288 | .tooltip('show');
289 |
290 | this.$el.find('.AknIconButton--hide')
291 | .removeClass('AknIconButton--hide')
292 | .addClass('icon-warning-sign');
293 | } else {
294 | Dialog.alert(
295 | __('alert.attribute_option.error_occured_during_submission'),
296 | __('pim_enrich.entity.attribute_option.flash.update.fail')
297 | );
298 | }
299 | }.bind(this)
300 | }
301 | );
302 | },
303 | cancelSubmit: function (e) {
304 | if (e.keyCode === 13) {
305 | this.updateItem();
306 |
307 | return false;
308 | }
309 | },
310 | loadModelFromView: function () {
311 | var attributeOptions = {};
312 | var editedModel = this.model.clone();
313 |
314 | editedModel.urlRoot = this.model.urlRoot;
315 |
316 | _.each(
317 | this.$el.find('.attribute-option-value'), function (input) {
318 | var locale = input.dataset.locale;
319 |
320 | attributeOptions[locale] = {
321 | locale: locale,
322 | value: input.value,
323 | id: this.model.get('optionValues')[locale] ?
324 | this.model.get('optionValues')[locale].id :
325 | null
326 | };
327 | }.bind(this)
328 | );
329 |
330 | editedModel.set('code', this.$el.find('.attribute_option_code').val());
331 | editedModel.set('optionValues', attributeOptions);
332 | editedModel.set('type', this.$el.find('.attribute_option_type').val());
333 | try {
334 | editedModel.set('constraints', this.$el.find('.attribute_option_constraints').val());
335 | editedModel.set('type_config', this.$el.find('.attribute_option_config').val());
336 | } catch (e) {
337 | Dialog.alert(
338 | __('flagbit.table_attribute.alert.json_error_text'),
339 | __('flagbit.table_attribute.alert.json_error_title')
340 | );
341 | }
342 |
343 | return editedModel;
344 | },
345 | inLoading: function (loading) {
346 | this.parent.inLoading(loading);
347 | },
348 | soil: function () {
349 | if (JSON.stringify(this.model.attributes) !== JSON.stringify(this.loadModelFromView().attributes)) {
350 | this.dirty = true;
351 | } else {
352 | this.dirty = false;
353 | }
354 | },
355 | clean: function () {
356 | this.dirty = false;
357 | }
358 | }
359 | );
360 |
361 | var ItemCollectionView = Backbone.View.extend(
362 | {
363 | tagName: 'table',
364 | className: 'table table-bordered table-stripped attribute-option-view AknGrid AknGrid--unclickable',
365 | template: _.template(
366 | '' +
367 | '' +
368 | '' +
369 | '' +
370 | '' +
371 | '' +
372 | '' +
373 | '' +
374 | '' +
375 | '' +
376 | '' +
377 | '' +
378 | '<% _.each(locales, function (locale) { %>' +
379 | '' +
382 | '<% }); %>' +
383 | '' +
384 | '' +
385 | '' +
386 | '' +
387 | '
' +
388 | '' +
389 | '' +
390 | '' +
391 | '' +
392 | '| ' +
393 | '<%= add_column_label %>' +
394 | ' | ' +
395 | '
' +
396 | ''
397 | ),
398 | events: {
399 | 'click .option-add': 'addItem'
400 | },
401 | $target: null,
402 | locales: [],
403 | sortable: true,
404 | sortingUrl: '',
405 | updateUrl: '',
406 | currentlyEditedItemView: null,
407 | itemViews: [],
408 | rendered: false,
409 | initialize: function (options) {
410 | this.$target = options.$target;
411 | this.collection = new ItemCollection({url: options.updateUrl});
412 | this.locales = options.locales;
413 | this.updateUrl = options.updateUrl;
414 | this.sortingUrl = options.sortingUrl;
415 | this.sortable = options.sortable;
416 |
417 | this.render();
418 | this.load();
419 | },
420 | render: function () {
421 | this.$el.empty();
422 |
423 | this.currentlyEditedItemView = null;
424 | this.updateEditionStatus();
425 |
426 | this.$el.html(
427 | this.template(
428 | {
429 | locales: this.locales,
430 | add_column_label: __('flagbit.table_attribute.add_column.label'),
431 | code_label: __('flagbit.table_attribute.code.label'),
432 | type_label: __('flagbit.table_attribute.type.label'),
433 | constraint_label: __('flagbit.table_attribute.validation.label'),
434 | config_label: __('flagbit.table_attribute.config.label')
435 | }
436 | )
437 | );
438 |
439 | _.each(
440 | this.collection.models, function (attributeOptionItem) {
441 | this.addItem({item: attributeOptionItem});
442 | }.bind(this)
443 | );
444 |
445 | if (0 === this.collection.length) {
446 | this.addItem();
447 | }
448 |
449 | if (!this.rendered) {
450 | this.$target.html(this.$el);
451 |
452 | this.rendered = true;
453 | }
454 |
455 | this.tbodyElement = this.$el.find('tbody');
456 |
457 | this.tbodyElement.sortable(
458 | {
459 | axis: 'y',
460 | distance: 5,
461 | cursor: 'move',
462 | scroll: true,
463 | start: function (e, ui) {
464 | ui.placeholder.height($(ui.item[0]).height());
465 | },
466 | helper: function (e, ui) {
467 | ui.children().each(
468 | function () {
469 | $(this).width($(this).width());
470 | }
471 | );
472 |
473 | return ui;
474 | },
475 | stop: function () {
476 | this.updateSorting();
477 | }.bind(this)
478 | }
479 | );
480 |
481 | this.updateSortableStatus(this.sortable);
482 |
483 | return this;
484 | },
485 | load: function () {
486 | this.itemViews = [];
487 | this.inLoading(true);
488 | this.collection
489 | .fetch(
490 | {
491 | success: function () {
492 | this.inLoading(false);
493 | this.render();
494 | }.bind(this)
495 | }
496 | );
497 | },
498 | addItem: function (opts) {
499 | var options = opts || {};
500 |
501 | //If no item model provided we create one
502 | var itemToAdd;
503 | if (!options.item) {
504 | itemToAdd = new AttributeOptionItem();
505 | } else {
506 | itemToAdd = options.item;
507 | }
508 |
509 | var newItemView = this.createItemView(itemToAdd);
510 |
511 | if (newItemView) {
512 | this.$el.children('tbody').append(newItemView.$el);
513 | }
514 | },
515 | createItemView: function (item) {
516 | var itemView = new EditableItemView(
517 | {
518 | model: item,
519 | url: this.updateUrl,
520 | locales: this.locales,
521 | parent: this
522 | }
523 | );
524 |
525 | //If the item is new the view is changed to edit mode
526 | if (!item.id) {
527 | if (!this.requestRowEdition(itemView)) {
528 | return;
529 | } else {
530 | itemView.showEditableItem();
531 | }
532 | }
533 |
534 | this.collection.add(item);
535 | this.itemViews.push(itemView);
536 |
537 | return itemView;
538 | },
539 | requestRowEdition: function (attributeOptionRow) {
540 | if (this.currentlyEditedItemView) {
541 | if (this.currentlyEditedItemView.dirty) {
542 | Dialog.alert(__('alert.attribute_option.save_before_edit_other'));
543 |
544 | return false;
545 | } else {
546 | this.currentlyEditedItemView.stopEditItem();
547 | this.currentlyEditedItemView = null;
548 | this.updateEditionStatus();
549 | }
550 | }
551 |
552 | if (attributeOptionRow.model.id) {
553 | this.currentlyEditedItemView = attributeOptionRow;
554 | }
555 |
556 | this.updateEditionStatus();
557 |
558 | return true;
559 | },
560 | showReadableItem: function (item) {
561 | if (item === this.currentlyEditedItemView) {
562 | this.currentlyEditedItemView = null;
563 | this.updateEditionStatus();
564 | }
565 | },
566 | deleteItem: function (item) {
567 | this.inLoading(true);
568 |
569 | item.model.destroy(
570 | {
571 | success: function () {
572 | this.inLoading(false);
573 |
574 | this.collection.remove(item);
575 | this.currentlyEditedItemView = null;
576 | this.updateEditionStatus();
577 |
578 | if (0 === this.collection.length) {
579 | this.addItem();
580 | item.$el.hide(0);
581 | } else if (!item.model.id) {
582 | item.$el.hide(0);
583 | } else {
584 | item.$el.hide(500);
585 | }
586 | }.bind(this),
587 | error: function (data, response) {
588 | this.inLoading(false);
589 | var message;
590 |
591 | if (response.responseJSON) {
592 | message = response.responseJSON.message;
593 | } else {
594 | message = response.responseText;
595 | }
596 |
597 | Dialog.alert(message, __('pim_enrich.entity.attribute_option.flash.delete.fail'));
598 | }.bind(this)
599 | }
600 | );
601 | },
602 | updateEditionStatus: function () {
603 | if (this.currentlyEditedItemView) {
604 | this.$el.addClass('in-edition');
605 | } else {
606 | this.$el.removeClass('in-edition');
607 | }
608 | },
609 | updateSortableStatus: function (sortable) {
610 | this.sortable = sortable;
611 |
612 | if (sortable) {
613 | this.tbodyElement.sortable('enable');
614 | } else {
615 | this.tbodyElement.sortable('disable');
616 | }
617 | },
618 | updateSorting: function () {
619 | this.inLoading(true);
620 | var sorting = [];
621 |
622 | var rows = this.$el.find('tbody tr.editable-item-row');
623 | for (var i = rows.length - 1; i >= 0; i--) {
624 | sorting[i] = rows[i].dataset.itemId;
625 | }
626 |
627 | $.ajax(
628 | {
629 | url: this.sortingUrl,
630 | type: 'PUT',
631 | data: JSON.stringify(sorting)
632 | }
633 | ).done(
634 | function () {
635 | this.inLoading(false);
636 | }.bind(this)
637 | );
638 | },
639 | inLoading: function (loading) {
640 | if (loading) {
641 | var loadingMask = new LoadingMask();
642 | loadingMask.render().$el.appendTo(this.$el);
643 | loadingMask.show();
644 | } else {
645 | this.$el.find('.loading-mask').remove();
646 | }
647 | }
648 | }
649 | );
650 |
651 | return function ($element) {
652 | var itemCollectionView = new ItemCollectionView(
653 | {
654 | $target: $element,
655 | updateUrl: Routing.generate(
656 | 'pim_enrich_attributeoption_index',
657 | {attributeId: $element.data('attribute-id')}
658 | ),
659 | sortingUrl: Routing.generate(
660 | 'pim_enrich_attributeoption_update_sorting',
661 | {attributeId: $element.data('attribute-id')}
662 | ),
663 | locales: $element.data('locales'),
664 | sortable: $element.data('sortable')
665 | }
666 | );
667 |
668 | mediator.on(
669 | 'attribute:auto_option_sorting:changed', function (autoSorting) {
670 | itemCollectionView.updateSortableStatus(!autoSorting);
671 | }.bind(this)
672 | );
673 | };
674 | }
675 | );
676 |
--------------------------------------------------------------------------------