├── 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 | 22 | -------------------------------------------------------------------------------- /tests/Form/Extension/AttributeOptionTypeExtensionTest.php: -------------------------------------------------------------------------------- 1 | get('form.extension'); 17 | 18 | $typeExtension = $formExtension->getTypeExtensions(AttributeOptionType::class); 19 | 20 | self::assertCount(1, $typeExtension); 21 | 22 | self::assertContainsOnlyInstancesOf(AttributeOptionTypeExtension::class, $typeExtension); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Resources/config/attribute_types.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | Flagbit\Bundle\TableAttributeBundle\AttributeType\TableType 8 | 9 | 10 | 11 | 12 | text 13 | textarea 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /jest/integration/formextensions.test.js: -------------------------------------------------------------------------------- 1 | describe('Form Extensions', function () { 2 | it('table attribute overrides akeneo-attribute-select-filter', function () { 3 | const formExtensions = require('../../tests/public/js/extensions.json'); 4 | 5 | const expected = { 6 | module: 'pim/filter/attribute/select', 7 | parent: null, 8 | targetZone: 'self', 9 | zones: [], 10 | aclResourceId: null, 11 | config: { 12 | url: 'pim_ui_ajaxentity_list', 13 | entityClass: 'Flagbit\\Bundle\\TableAttributeBundle\\Entity\\AttributeOption', 14 | operators: [ 'IN', 'EMPTY', 'NOT EMPTY' ] 15 | }, 16 | position: 100, 17 | feature: null, 18 | code: 'akeneo-attribute-select-filter' 19 | }; 20 | 21 | expect(formExtensions.extensions).toContainEqual(expected); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/Component/Product/Completeness/MaskItemGenerator/TableMaskItem.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/Flagbit/Bundle/TableAttributeBundle/Http/Select2JsonResponseSpec.php: -------------------------------------------------------------------------------- 1 | 1, 14 | 'b' => 2, 15 | 3 => 'c', 16 | ]; 17 | $this->beConstructedWith($array); 18 | } 19 | 20 | public function it_is_initializable() 21 | { 22 | $this->shouldHaveType(Select2JsonResponse::class); 23 | } 24 | 25 | public function it_creates_select2_response_body() 26 | { 27 | $this->getContent()->shouldReturn('{"results":[{"id":"a","text":"1"},{"id":"b","text":"2"},{"id":"3","text":"c"}]}'); 28 | } 29 | 30 | public function it_contains_json_response_header() 31 | { 32 | $this->headers->get('Content-Type')->shouldReturn('application/json'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Resources/public/less/tableattribute.less: -------------------------------------------------------------------------------- 1 | .AknFormContainer { 2 | &.flagbit-table-attribute { 3 | width: 100%; 4 | } 5 | } 6 | 7 | .attribute-option-view { 8 | textarea { 9 | width: 100%; 10 | } 11 | 12 | .select-options-config-label { 13 | margin-top: 10px; 14 | display: inline-block; 15 | } 16 | 17 | .select-options-table { 18 | .AknGrid-headerCell { 19 | background: none; 20 | } 21 | } 22 | } 23 | 24 | .flagbit-table-field { 25 | max-width: 100%; 26 | 27 | .asset-gallery { 28 | header { 29 | button { 30 | padding: 0 12px; 31 | } 32 | } 33 | } 34 | 35 | .select2-container { 36 | width: 100%; 37 | max-width: 100%; 38 | } 39 | 40 | &.AknComparableFields-item { 41 | flex-basis: 100%; 42 | 43 | .flagbit-table-attribute { 44 | width: 100%; 45 | } 46 | } 47 | } 48 | 49 | .json-constraint-generator ~ .select2-container { 50 | width: 100% !important; 51 | } 52 | 53 | .AknButton-squareIcon--table { 54 | background-image: url('../images/attribute/icon-table.svg'); 55 | } 56 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 12 | 15 | 16 | %akeneo_storage_utils.mapping_overrides% 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /spec/Flagbit/Bundle/TableAttributeBundle/Entity/AttributeOptionSpec.php: -------------------------------------------------------------------------------- 1 | setAttribute($attribute); 14 | $attribute->getType()->willReturn('foo'); 15 | $this->isTableAttribute()->shouldReturn(false); 16 | } 17 | 18 | function it_is_an_attribute_option_of_a_table_attribute(AttributeInterface $attribute) 19 | { 20 | $this->setAttribute($attribute); 21 | $attribute->getType()->willReturn(TableType::FLAGBIT_CATALOG_TABLE); 22 | $this->isTableAttribute()->shouldReturn(true); 23 | } 24 | 25 | function it_is_an_attribute_option_without_an_attribute() 26 | { 27 | $this->isTableAttribute()->shouldReturn(false); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Flagbit GmbH & Co. KG 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/Resources/public/images/attribute/icon-table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | icon-date 9 | Created with Sketch. 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Resources/public/js/Json/Renderer/Default.js: -------------------------------------------------------------------------------- 1 | define( 2 | [ 3 | 'flagbit/JsonGenerator/Observer', 4 | 'oro/translator', 5 | ], 6 | function (JsonGeneratorObserver, __) { 7 | 8 | /** 9 | * @class 10 | */ 11 | var JsonGeneratorRendererDefault = 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 | var $text = document.createElement('span'); 25 | $text.innerText = __('flagbit.table_attribute.no_configuration.text'); 26 | $container.appendChild($text); 27 | }; 28 | 29 | /** 30 | * @public 31 | * @returns {Object} 32 | */ 33 | this.read = function () { 34 | return {}; 35 | }; 36 | 37 | }; 38 | 39 | return JsonGeneratorRendererDefault; 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /spec/Flagbit/Bundle/TableAttributeBundle/Form/TableJsonTransformerSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(TableJsonTransformer::class); 13 | } 14 | 15 | public function it_keeps_transform_value() 16 | { 17 | $originalValue = '{"Blank":{},"NotNull":{}}'; 18 | $expectedvalue = '{"Blank":{},"NotNull":{}}'; 19 | 20 | $this->transform($originalValue)->shouldBe($expectedvalue); 21 | } 22 | 23 | public function it_reverse_transforms_to_constraints_array_format() 24 | { 25 | $originalValue = '{"Blank":{},"NotNull":{}}'; 26 | $expectedvalue = [ 27 | 'Blank' => [], 28 | 'NotNull' => [], 29 | ]; 30 | 31 | $this->reverseTransform($originalValue)->shouldBe($expectedvalue); 32 | } 33 | 34 | public function it_reverse_transform_null() 35 | { 36 | $originalValue = null; 37 | 38 | $this->reverseTransform($originalValue)->shouldBeNull(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spec/Flagbit/Bundle/TableAttributeBundle/Provider/Field/TableFieldProviderSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(TableFieldProvider::class); 14 | } 15 | 16 | public function it_returns_flagbit_table_field() 17 | { 18 | $element = [ 19 | 'foo' => 'bar', 20 | ]; 21 | $this->getField($element)->shouldReturn('flagbit-table-field'); 22 | } 23 | 24 | public function it_checks_correct_support( 25 | AttributeInterface $attributeInterface 26 | ) { 27 | $attributeInterface->getType()->willReturn('flagbit_catalog_table'); 28 | $this->supports($attributeInterface)->shouldReturn(true); 29 | } 30 | 31 | public function it_checks_incorrect_support( 32 | AttributeInterface $attributeInterface 33 | ) { 34 | $attributeInterface->getType()->willReturn('foo_bar'); 35 | $this->supports($attributeInterface)->shouldReturn(false); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Component/Product/Factory/Value/TableValueFactory.php: -------------------------------------------------------------------------------- 1 | code(), 22 | TableValueFactory::class, 23 | $data 24 | ); 25 | } 26 | 27 | return parent::createWithoutCheckingData($attribute, $channelCode, $localeCode, $data); 28 | } 29 | 30 | public function supportedAttributeType(): string 31 | { 32 | return TableType::FLAGBIT_CATALOG_TABLE; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Resources/public/js/Json/Observer.js: -------------------------------------------------------------------------------- 1 | define( 2 | function () { 3 | 4 | /** 5 | * @class 6 | */ 7 | var JsonGeneratorObserver = function () { 8 | 9 | /** 10 | * @protected 11 | * @type {Object} 12 | */ 13 | var $watcher = {}; 14 | 15 | 16 | /** 17 | * @public 18 | * @param {String} $action 19 | * @param {Function} $callable 20 | */ 21 | this.watch = function ($action, $callable) { 22 | 23 | if (!$watcher[$action]) { 24 | $watcher[$action] = []; 25 | } 26 | 27 | $watcher[$action].push($callable); 28 | }; 29 | 30 | 31 | /** 32 | * @public 33 | * @param {String} $action 34 | */ 35 | this.notify = function ($action) { 36 | if ($watcher[$action]) { 37 | for (var $i in $watcher[$action]) { 38 | if ($watcher[$action].hasOwnProperty($i)) { 39 | var $callable = $watcher[$action][$i]; 40 | 41 | $callable(); 42 | } 43 | } 44 | } 45 | } 46 | }; 47 | 48 | return JsonGeneratorObserver; 49 | } 50 | ); -------------------------------------------------------------------------------- /src/Resources/public/js/Json/Generator.js: -------------------------------------------------------------------------------- 1 | define( 2 | [ 3 | 'flagbit/JsonGenerator/Storage', 4 | 'flagbit/JsonGenerator/Input', 5 | 'flagbit/JsonGenerator/Renderer' 6 | ], 7 | function (JsonGeneratorStorage, JsonGeneratorInput, JsonGeneratorRenderer) { 8 | 9 | /** 10 | * @class 11 | * @param {(HTMLElement|HTMLTextAreaElement)} $element 12 | */ 13 | var JsonGenerator = function ($element, $types) { 14 | 15 | var $source = new JsonGeneratorStorage(); 16 | var $input = new JsonGeneratorInput($element); 17 | var $renderer = new JsonGeneratorRenderer($input.isEditable(), $element.parentNode, $types); 18 | 19 | $input.observer.watch( 20 | 'load', function () { 21 | $source.write($input.read()); 22 | $renderer.render($source.read()); 23 | } 24 | ); 25 | 26 | $renderer.observer.watch( 27 | 'persist', function () { 28 | $source.write($renderer.read()); 29 | } 30 | ); 31 | 32 | $renderer.observer.watch( 33 | 'save', function () { 34 | $input.write($source.read()); 35 | } 36 | ); 37 | 38 | $input.hide(); 39 | $input.observer.notify('load'); 40 | }; 41 | 42 | return JsonGenerator; 43 | } 44 | ); -------------------------------------------------------------------------------- /src/Normalizer/AttributeOptionNormalizer.php: -------------------------------------------------------------------------------- 1 | baseNormalizer = $baseNormalizer; 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function normalize($object, $format = null, array $context = []) 24 | { 25 | $normalizedValues = $this->baseNormalizer->normalize($object, $format, $context); 26 | 27 | /** 28 | * @var AttributeOption $object 29 | */ 30 | if ($object->isTableAttribute()) { 31 | $normalizedValues['type'] = $object->getType(); 32 | $normalizedValues['type_config'] = $object->getTypeConfig(); 33 | $normalizedValues['constraints'] = $object->getConstraints(); 34 | } 35 | 36 | return $normalizedValues; 37 | } 38 | 39 | /** 40 | * @param mixed $data 41 | * @param null $format 42 | * @return bool 43 | */ 44 | public function supportsNormalization($data, $format = null) 45 | { 46 | return $this->baseNormalizer->supportsNormalization($data, $format); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/DependencyInjection/FlagbitTableAttributeExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 27 | 28 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 29 | $loader->load('controller.xml'); 30 | $loader->load('services.xml'); 31 | $loader->load('array_converters.xml'); 32 | $loader->load('attribute_types.xml'); 33 | $loader->load('comparators.xml'); 34 | $loader->load('updaters.xml'); 35 | $loader->load('entities.xml'); 36 | $loader->load('validators.xml'); 37 | $loader->load('query_builders.xml'); 38 | $loader->load('factories.xml'); 39 | $loader->load('doctrine.xml'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Controller/AjaxOptionController.php: -------------------------------------------------------------------------------- 1 | baseController = $baseController; 18 | } 19 | 20 | /** 21 | * Because of another hardcoded PHP class name in 22 | * 23 | * @see vendor/akeneo/pim-enterprise-dev/src/Akeneo/Pim/Automation/RuleEngine/front/src/fetch/AttributeOptionFetcher.ts 24 | * 25 | * the AttributeOption class can and should not be overridden in Akeneo PIM, we came up with this workaround to 26 | * support the legacy code of the Table attribute. 27 | * 28 | * @param Request $request 29 | * 30 | * @return JsonResponse 31 | */ 32 | public function listAction(Request $request): JsonResponse 33 | { 34 | $class = $request->query->get('class'); 35 | if ($class === AttributeOption::class) { 36 | $request->query->set('class', TableAttributeOption::class); 37 | } 38 | 39 | return $this->baseController->listAction($request); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Resources/config/array_converters.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | flagbit_catalog_table 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | flagbit_catalog_table 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/Kernel/TestKernel.php: -------------------------------------------------------------------------------- 1 | $envs) { 18 | if ($envs[$this->environment] ?? $envs['all'] ?? false) { 19 | yield new $class(); 20 | } 21 | } 22 | } 23 | 24 | public function getRootDir(): string 25 | { 26 | return __DIR__; 27 | } 28 | 29 | public function getProjectDir(): string 30 | { 31 | return __DIR__; 32 | } 33 | 34 | protected function build(ContainerBuilder $container) 35 | { 36 | $serviceIds = [ 37 | 'pim_catalog.validator.constraint_guesser.chained_attribute', 38 | 'akeneo.pim.enrichment.factory.value', 39 | 'pim_catalog.repository.attribute', 40 | 'pim_catalog.repository.cached_attribute', 41 | 'form.extension', 42 | ]; 43 | $container->addCompilerPass(new PublicServiceCompilerPass($serviceIds)); 44 | $container->addCompilerPass(new EnterpriseFilterStubPass('product_proposal')); 45 | $container->addCompilerPass(new EnterpriseFilterStubPass('published_product')); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Resources/config/query_builders.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | flagbit_catalog_table 14 | 15 | 16 | IN 17 | EMPTY 18 | NOT EMPTY 19 | NOT IN 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flagbit/table-attribute-bundle", 3 | "description": "The Flagbit Table Attribute Bundle for Akeneo PIM gives you the possibility to enrich your product with multi-dimensional data presentation in the form of tables, allowing you maximum flexibility within the PIM.", 4 | "keywords": [ 5 | "akeneo", 6 | "table", 7 | "attribute", 8 | "pim", 9 | "multidimensional" 10 | ], 11 | "type": "library", 12 | "minimum-stability": "stable", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Claudio Zizza", 17 | "email": "claudio.zizza@flagbit.de", 18 | "role": "Developer" 19 | }, 20 | { 21 | "name": "Antonio Mansilla", 22 | "email": "antonio.mansilla@flagbit.de", 23 | "role": "Developer" 24 | }, 25 | { 26 | "name": "Angel Vazquez", 27 | "email": "angel.vazquez@flagbit.de", 28 | "role": "Developer" 29 | } 30 | ], 31 | "autoload": { 32 | "psr-4": { 33 | "Flagbit\\Bundle\\TableAttributeBundle\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Flagbit\\Bundle\\TableAttributeBundle\\Test\\": "tests/" 39 | } 40 | }, 41 | "require": { 42 | "php": "8.0.*", 43 | "ext-json": "*", 44 | "akeneo/pim-community-dev": "^6.0" 45 | }, 46 | "require-dev": { 47 | "phpspec/phpspec": "^7.2", 48 | "phpunit/phpunit": "^9.0", 49 | "squizlabs/php_codesniffer": "^3.7", 50 | "overtrue/phplint": "^4.3", 51 | "symfony/debug-bundle": "^5.4.0", 52 | "symfony/web-profiler-bundle": "^5.4.0" 53 | }, 54 | "config": { 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "symfony/flex": true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /phpunit.xml.dist.bak: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | ./tests 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | src 34 | 35 | src/Resources 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Form/Extension/AttributeOptionTypeExtension.php: -------------------------------------------------------------------------------- 1 | add( 23 | 'type', 24 | TextType::class, 25 | [ 26 | 'required' => true, 27 | 'constraints' => [ 28 | new Choice(['select', 'select_from_url', 'text', 'number']), 29 | ], 30 | ] 31 | ); 32 | $builder->add('constraints', TextType::class, ['required' => true]); 33 | $builder->add('type_config', TextType::class, ['required' => true]); 34 | 35 | $transformer = new TableJsonTransformer(); 36 | 37 | $builder->get('constraints')->addModelTransformer($transformer); 38 | $builder->get('type_config')->addModelTransformer($transformer); 39 | } 40 | 41 | /** 42 | * Returns the name of the type being extended. 43 | * 44 | * @return array 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 | '<%= code_label %>' + 378 | '<% _.each(locales, function (locale) { %>' + 379 | '' + 380 | '<%= locale %>' + 381 | '' + 382 | '<% }); %>' + 383 | '<%= type_label %>' + 384 | '<%= constraint_label %>' + 385 | '<%= config_label %>' + 386 | 'Action' + 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 | --------------------------------------------------------------------------------