├── Api └── CustomEntityProductLinkManagementInterface.php ├── Block └── Entity │ └── Attribute │ └── CustomEntity │ └── Renderer.php ├── Helper ├── Data.php └── Product.php ├── Model ├── CustomEntityProductLinkManagement.php ├── Entity │ └── Attribute │ │ ├── Frontend │ │ └── CustomEntity.php │ │ └── Source │ │ └── CustomEntity.php ├── Layer │ ├── CustomEntity.php │ └── CustomEntity │ │ └── CollectionFilter.php └── Product │ └── CustomEntity │ └── ReadHandler.php ├── Observer ├── Adminhtml │ ├── AddFieldsToAttributeObserver.php │ ├── CatalogProductFormExcludedAttributesObserver.php │ └── CustomEntityAttributeSaveBeforeObserver.php └── Catalog │ └── Product │ ├── AddCustomEntitiesInformation.php │ └── AddCustomEntityAttributeTypeObserver.php ├── Plugin ├── Block │ └── Product │ │ ├── ListProductPlugin.php │ │ └── ViewPlugin.php ├── Catalog │ ├── Controller │ │ └── Adminhtml │ │ │ └── Product │ │ │ └── Initialization │ │ │ └── HelperPlugin.php │ └── Ui │ │ └── DataProvider │ │ └── Product │ │ └── Form │ │ └── Modifier │ │ └── EavPlugin.php ├── Controller │ └── Entity │ │ └── ViewPlugin.php └── Model │ ├── Condition │ └── Product │ │ └── AbstractProduct.php │ ├── ProductPlugin.php │ └── ResourceModel │ └── Eav │ └── Attribute.php ├── Setup └── Patch │ └── Data │ ├── MoveCustomEntityLink.php │ └── UpdateProductCustomEntityAttribute.php ├── composer.json ├── etc ├── adminhtml │ ├── di.xml │ └── events.xml ├── di.xml ├── elasticsuite_indices.xml ├── extension_attributes.xml ├── frontend │ ├── di.xml │ └── events.xml └── module.xml ├── phpstan.neon.dist ├── registration.php └── view ├── adminhtml ├── requirejs-config.js ├── ui_component │ └── product_attribute_add_form.xml └── web │ └── js │ └── product-attributes.js └── frontend ├── layout └── smile_custom_entity_entity_view_product_list.xml └── templates └── entity └── attribute └── custom_entity └── renderer.phtml /Api/CustomEntityProductLinkManagementInterface.php: -------------------------------------------------------------------------------- 1 | initAttributes($productAttributeCollectionFactory); 34 | } 35 | 36 | /** 37 | * List of product attributes using custom entities as frontend input. 38 | * 39 | * @return ProductAttributeInterface[] 40 | */ 41 | public function getCustomEntityProductAttributes(): array 42 | { 43 | return $this->customEntityProductAttributes; 44 | } 45 | 46 | /** 47 | * Init attribute list. 48 | * 49 | * @param ProductAttributeCollectionFactory $attributeCollectionFactory Product attribute collection factory. 50 | */ 51 | private function initAttributes(ProductAttributeCollectionFactory $attributeCollectionFactory): void 52 | { 53 | $attributeCollection = $attributeCollectionFactory->create(); 54 | $attributeCollection->addFieldToFilter(ProductAttributeInterface::FRONTEND_INPUT, 'smile_custom_entity'); 55 | 56 | /** @var ProductAttributeInterface[] $attributeCollectionItems */ 57 | $attributeCollectionItems = $attributeCollection->getItems(); 58 | $this->customEntityProductAttributes = $attributeCollectionItems; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Helper/Product.php: -------------------------------------------------------------------------------- 1 | filterableAttributeList = $filterableAttributeList; 38 | } 39 | 40 | /** 41 | * Return custom entities for product and attribute code. 42 | * 43 | * @param ProductInterface $product Product. 44 | * @param string $attributeCode Attribuce code. 45 | * @return CustomEntityInterface[] 46 | */ 47 | public function getCustomEntities(ProductInterface $product, string $attributeCode): array 48 | { 49 | $result = []; 50 | /** @var DataObject $extensionAttributes */ 51 | $extensionAttributes = $product->getExtensionAttributes(); 52 | $customEntities = $extensionAttributes->getCustomEntities(); 53 | if ($customEntities) { 54 | foreach ($customEntities as $customEntity) { 55 | if ($customEntity->getProductAttributeCode() !== $attributeCode || !$customEntity->getIsActive()) { 56 | continue; 57 | } 58 | $result[] = $customEntity; 59 | } 60 | } 61 | 62 | return $result; 63 | } 64 | 65 | /** 66 | * Return filterable attribute code for a custom entity. 67 | * 68 | * @param CustomEntityInterface $customEntity Custom entity. 69 | */ 70 | public function getFilterableAttributeCode(CustomEntityInterface $customEntity): ?string 71 | { 72 | if (!array_key_exists($customEntity->getId(), $this->filterableAttributeCodes)) { 73 | $this->filterableAttributeCodes[$customEntity->getId()] = ''; 74 | foreach ($this->filterableAttributeList->getList() as $attribute) { 75 | if ( 76 | $attribute->getFrontendInput() == 'smile_custom_entity' && 77 | $attribute->getCustomEntityAttributeSetId() == $customEntity->getAttributeSetId() 78 | ) { 79 | $this->filterableAttributeCodes[$customEntity->getId()] = $attribute->getAttributeCode(); 80 | break; 81 | } 82 | } 83 | } 84 | 85 | return $this->filterableAttributeCodes[$customEntity->getId()]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Model/CustomEntityProductLinkManagement.php: -------------------------------------------------------------------------------- 1 | helper = $helper; 30 | $this->customEntityCollectionFactory = $customEntityCollectionFactory; 31 | } 32 | 33 | /** 34 | * Return custom entities assigned to a product. 35 | * 36 | * @return CustomEntityInterface[][]|null 37 | * @throws LocalizedException 38 | */ 39 | public function getCustomEntities(Product $product): ?array 40 | { 41 | /** @var CustomEntityInterface[][] $entities */ 42 | $entities = []; 43 | $entityIds = []; 44 | 45 | foreach ($this->helper->getCustomEntityProductAttributes() as $customEntityAttribute) { 46 | $customEntityAttributeCode = $customEntityAttribute->getAttributeCode(); 47 | $productCustomEntityIds = $product->getData($customEntityAttributeCode); 48 | if ($productCustomEntityIds) { 49 | if (is_string($productCustomEntityIds)) { 50 | $productCustomEntityIds = explode(',', $productCustomEntityIds); 51 | } 52 | foreach ($productCustomEntityIds ?? [] as $entityId) { 53 | $entityIds[$entityId][] = $customEntityAttributeCode; 54 | } 55 | } 56 | } 57 | 58 | if (!empty($entityIds)) { 59 | $collection = $this->customEntityCollectionFactory->create() 60 | ->addAttributeToSelect('*') 61 | ->addAttributeToFilter('entity_id', ['in' => array_keys($entityIds)]); 62 | 63 | foreach ($collection->getItems() as $customEntity) { 64 | foreach ($entityIds[$customEntity->getId()] as $customEntityAttributeCode) { 65 | $entities[$customEntityAttributeCode][] = $customEntity; 66 | } 67 | } 68 | } 69 | 70 | return $entities; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Model/Entity/Attribute/Frontend/CustomEntity.php: -------------------------------------------------------------------------------- 1 | renderers = $renderers; 55 | $this->productHelper = $productHelper; 56 | } 57 | 58 | /** 59 | * Get value of object 60 | * 61 | * @param DataObject $object Object. 62 | * @throws NoSuchEntityException 63 | */ 64 | public function getValue(DataObject $object): string 65 | { 66 | /** @var ProductInterface $object */ 67 | $value = []; 68 | $customEntities = $this->productHelper->getCustomEntities($object, $this->getAttribute()->getAttributeCode()); 69 | foreach ($customEntities as $entity) { 70 | $value[] = $this->getRenderer($entity)->toHtml(); 71 | } 72 | 73 | return implode(' ', $value); 74 | } 75 | 76 | /** 77 | * Return custom entity renderer. 78 | * 79 | * @param CustomEntityInterface $customEntity Custom entity. 80 | * @throws NoSuchEntityException 81 | */ 82 | private function getRenderer(CustomEntityInterface $customEntity): Renderer 83 | { 84 | /** @var \Smile\CustomEntity\Model\CustomEntity $customEntity */ 85 | $renderer = $this->renderers[$customEntity->getAttributeSetUrlKey()] ?? $this->renderers['default']; 86 | $renderer->setCustomEntity($customEntity); 87 | 88 | return $renderer; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Model/Entity/Attribute/Source/CustomEntity.php: -------------------------------------------------------------------------------- 1 | customEntityCollectionFactory = $customEntityCollectionFactory; 20 | } 21 | 22 | /** 23 | * Get all options 24 | */ 25 | public function getAllOptions(): array 26 | { 27 | $options = []; 28 | $collection = $this->customEntityCollectionFactory->create() 29 | ->addAttributeToSelect('name') 30 | ->addAttributeToFilter('attribute_set_id', $this->getAttribute()->getCustomEntityAttributeSetId()); 31 | 32 | foreach ($collection->getItems() as $customEntity) { 33 | $options[] = [ 34 | 'value' => $customEntity->getId(), 35 | 'label' => $customEntity->getName(), 36 | ]; 37 | } 38 | 39 | return $options; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Model/Layer/CustomEntity.php: -------------------------------------------------------------------------------- 1 | productHelper = $productHelper; 48 | $this->registry = $registry; 49 | $this->queryFactory = $queryFactory; 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function filter($collection, Category $category): void 56 | { 57 | parent::filter($collection, $category); 58 | $currentCustomEntity = $this->getCurrentCustomEntity(); 59 | if (null !== $currentCustomEntity && $currentCustomEntity->getId() && $this->getAttributeCode()) { 60 | $query = $this->queryFactory->create( 61 | QueryInterface::TYPE_TERM, 62 | ['field' => $this->getAttributeCode(), 'value' => $currentCustomEntity->getId()] 63 | ); 64 | 65 | /** @var Collection $collection */ 66 | $collection->addQueryFilter($query); 67 | } 68 | } 69 | 70 | /** 71 | * Return current custom entity interface. 72 | */ 73 | private function getCurrentCustomEntity(): ?CustomEntityInterface 74 | { 75 | return $this->registry->registry('current_custom_entity'); 76 | } 77 | 78 | /** 79 | * Return attribute code link to current custom entity. 80 | */ 81 | private function getAttributeCode(): ?string 82 | { 83 | return $this->productHelper->getFilterableAttributeCode($this->getCurrentCustomEntity()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Model/Product/CustomEntity/ReadHandler.php: -------------------------------------------------------------------------------- 1 | customEntityLinkManager = $customEntityLinkManager; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 32 | */ 33 | public function execute($entity, $arguments = []) 34 | { 35 | if ($entity->getId()) { 36 | $customEntitiesByCode = $this->customEntityLinkManager->getCustomEntities($entity); 37 | $attributeValues = []; 38 | $productCustomEntities = []; 39 | 40 | foreach ($customEntitiesByCode as $attributeCode => $customEntities) { 41 | /** @var DataObject $customEntity */ 42 | foreach ($customEntities as $customEntity) { 43 | $customEntityClone = clone $customEntity; 44 | $customEntityClone->setProductAttributeCode($attributeCode); 45 | $attributeValues[$attributeCode][] = $customEntity->getId(); 46 | $productCustomEntities[] = $customEntityClone; 47 | } 48 | } 49 | 50 | $entity->addData($attributeValues); 51 | 52 | $entityExtension = $entity->getExtensionAttributes(); 53 | $entityExtension->setCustomEntities($productCustomEntities); 54 | $entity->setExtensionAttributes($entityExtension); 55 | } 56 | 57 | return $entity; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Observer/Adminhtml/AddFieldsToAttributeObserver.php: -------------------------------------------------------------------------------- 1 | moduleManager = $moduleManager; 37 | $this->attributeSetOptions = $attributeSetOptions; 38 | $this->registry = $registry; 39 | } 40 | 41 | /** 42 | * Append custom_entity_attribute_set_id field. 43 | * 44 | * @param Observer $observer Observer 45 | */ 46 | public function execute(Observer $observer): void 47 | { 48 | if (!$this->moduleManager->isOutputEnabled('Smile_CustomEntityProductLink')) { 49 | return; 50 | } 51 | 52 | /** @var Form $form */ 53 | $form = $observer->getForm(); 54 | $fieldset = $form->getElement('base_fieldset'); 55 | $fieldset->addField( 56 | 'custom_entity_attribute_set_id', 57 | 'select', 58 | [ 59 | 'name' => 'custom_entity_attribute_set_id', 60 | 'label' => __('Custom entity type'), 61 | 'title' => __('Custom entity type'), 62 | 'values' => $this->attributeSetOptions->toOptionArray(), 63 | ] 64 | ); 65 | if ($this->getAttributeObject() && $this->getAttributeObject()->getAttributeId()) { 66 | $form->getElement('custom_entity_attribute_set_id')->setDisabled(1); 67 | } 68 | } 69 | 70 | /** 71 | * Get attribute object. 72 | */ 73 | private function getAttributeObject(): ?AttributeInterface 74 | { 75 | return $this->registry->registry('entity_attribute'); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Observer/Adminhtml/CatalogProductFormExcludedAttributesObserver.php: -------------------------------------------------------------------------------- 1 | attributeAction = $attributeAction; 28 | } 29 | 30 | /** 31 | * Excluded smile custom entity attributes. 32 | * 33 | * @param Observer $observer Observer. 34 | */ 35 | public function execute(Observer $observer): void 36 | { 37 | /** @var Attributes $attributesTab */ 38 | $attributesTab = $observer->getEvent()->getData('object'); 39 | $attributesTab->setFormExcludedFieldList( 40 | array_merge($attributesTab->getFormExcludedFieldList(), $this->getCustomEntityAttributeCodes()) 41 | ); 42 | } 43 | 44 | /** 45 | * Return attribute codes. 46 | * 47 | * @return array 48 | */ 49 | private function getCustomEntityAttributeCodes(): array 50 | { 51 | return array_map(function ($attribute) { 52 | return $attribute->getAttributeCode(); 53 | }, $this->getCustomEntityAttributes()); 54 | } 55 | 56 | /** 57 | * Return custom entity attributes. 58 | * 59 | * @return array|DataObject[] 60 | */ 61 | private function getCustomEntityAttributes(): array 62 | { 63 | $attributes = $this->attributeAction->getAttributes()->getItems(); 64 | 65 | return array_filter($attributes, function ($attribute) { 66 | return $attribute->getFrontendInput() == 'smile_custom_entity'; 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Observer/Adminhtml/CustomEntityAttributeSaveBeforeObserver.php: -------------------------------------------------------------------------------- 1 | getEvent()->getData('attribute'); 27 | 28 | if ($attribute->getFrontendInput() == 'smile_custom_entity') { 29 | $attribute->setBackendType('text'); 30 | $attribute->setFrontendModel(CustomEntity::class); 31 | $attribute->setBackendModel(ArrayBackend::class); 32 | $attribute->setSourceModel(Source::class); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Observer/Catalog/Product/AddCustomEntitiesInformation.php: -------------------------------------------------------------------------------- 1 | catalogConfig = $catalogConfig; 27 | $this->customEntityCollectionFactory = $customEntityCollectionFactory; 28 | } 29 | 30 | /** 31 | * Add custom entities information on product collection. 32 | * 33 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 34 | */ 35 | public function execute(Observer $observer): void 36 | { 37 | /** @var Collection $collection */ 38 | $collection = $observer->getEvent()->getData('collection'); 39 | $attributeCodes = $this->getCustomEntityProductAttribute(); 40 | $productCustomEntityList = []; 41 | $customEntityIds = []; 42 | 43 | if (empty($attributeCodes)) { 44 | return; 45 | } 46 | 47 | foreach ($collection as $product) { 48 | foreach ($attributeCodes as $attributeCode) { 49 | if (!$product->hasData($attributeCode)) { 50 | continue; 51 | } 52 | $productCustomEntityIds = $product->getData($attributeCode); 53 | if (is_string($productCustomEntityIds)) { 54 | $productCustomEntityIds = explode(',', $productCustomEntityIds); 55 | } 56 | foreach ($productCustomEntityIds ?? [] as $productCustomEntityId) { 57 | $productCustomEntityList[$product->getId()][$attributeCode][] = $productCustomEntityId; 58 | $customEntityIds[$productCustomEntityId] = $productCustomEntityId; 59 | } 60 | } 61 | } 62 | 63 | if (empty($customEntityIds)) { 64 | return; 65 | } 66 | 67 | $customEntityList = $this->getCustomEntityByIds($customEntityIds); 68 | 69 | foreach (array_keys($productCustomEntityList) as $productId) { 70 | $product = $collection->getItemById($productId); 71 | $productCustomEntities = []; 72 | foreach ($productCustomEntityList[$product->getId()] as $attributeCode => $customEntityIds) { 73 | foreach ($customEntityIds as $customEntityId) { 74 | $customEntity = $customEntityList[$customEntityId]; 75 | $customEntity->setProductAttributeCode($attributeCode); 76 | $productCustomEntities[] = $customEntity; 77 | } 78 | } 79 | $attributeValues = $productCustomEntityList[$product->getId()]; 80 | $product->addData($attributeValues); 81 | 82 | $entityExtension = $product->getExtensionAttributes(); 83 | $entityExtension->setCustomEntities($productCustomEntities); 84 | $product->setExtensionAttributes($entityExtension); 85 | } 86 | } 87 | 88 | /** 89 | * Return an array of custom entity matching the given ids, index by the ids. 90 | */ 91 | private function getCustomEntityByIds(array $customEntityIds): array 92 | { 93 | $customEntityList = []; 94 | $customEntityCollection = $this->customEntityCollectionFactory->create() 95 | ->addAttributeToSelect('*') 96 | ->addAttributeToFilter('entity_id', ['in' => $customEntityIds]); 97 | 98 | foreach ($customEntityCollection->getItems() as $customEntity) { 99 | $customEntityList[$customEntity->getId()] = $customEntity; 100 | } 101 | 102 | return $customEntityList; 103 | } 104 | 105 | /** 106 | * Get custom entity product attribute code. 107 | * 108 | * @return string[] 109 | */ 110 | private function getCustomEntityProductAttribute(): array 111 | { 112 | return array_keys(array_filter( 113 | $this->catalogConfig->getAttributesUsedInProductListing(), 114 | function (AbstractAttribute $attribute) { 115 | return $attribute->getFrontendInput() == 'smile_custom_entity'; 116 | } 117 | )); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Observer/Catalog/Product/AddCustomEntityAttributeTypeObserver.php: -------------------------------------------------------------------------------- 1 | moduleManager = $moduleManager; 27 | } 28 | 29 | /** 30 | * Add custom entities attribute type observer. 31 | * 32 | * @param Observer $observer Observer. 33 | */ 34 | public function execute(Observer $observer): void 35 | { 36 | if (!$this->moduleManager->isOutputEnabled('Smile_CustomEntityProductLink')) { 37 | return; 38 | } 39 | 40 | /** @var DataObject $response */ 41 | $response = $observer->getEvent()->getResponse(); 42 | $types = $response->getTypes(); 43 | 44 | $types[] = [ 45 | 'value' => 'smile_custom_entity', 46 | 'label' => __('Custom Entity'), 47 | 'hide_fields' => [ 48 | 'is_unique', 49 | 'is_required', 50 | 'frontend_class', 51 | '_default_value', 52 | ], 53 | ]; 54 | 55 | $response->setTypes($types); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Plugin/Block/Product/ListProductPlugin.php: -------------------------------------------------------------------------------- 1 | getLoadedProductCollection() as $product) { 26 | // @phpstan-ignore-next-line 27 | $customEntities = $product->getExtensionAttributes()->getCustomEntities(); 28 | $identities = []; 29 | if ($customEntities) { 30 | foreach ($customEntities as $customEntity) { 31 | // @codingStandardsIgnoreLine 32 | $identities = array_merge($identities, $customEntity->getIdentities()); 33 | } 34 | } 35 | } 36 | 37 | return $identities; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Plugin/Block/Product/ViewPlugin.php: -------------------------------------------------------------------------------- 1 | getProduct(); 27 | if (!$product) { 28 | return $identities; 29 | } 30 | 31 | // @todo Optimization: only custom entities if is visible on front 32 | // @phpstan-ignore-next-line 33 | $customEntities = $product->getExtensionAttributes()->getCustomEntities(); 34 | if ($customEntities) { 35 | /** @var CustomEntityInterface $customEntity */ 36 | foreach ($customEntities as $customEntity) { 37 | // @codingStandardsIgnoreLine 38 | $identities = array_merge($identities, $customEntity->getIdentities()); 39 | } 40 | } 41 | 42 | return $identities; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Plugin/Catalog/Controller/Adminhtml/Product/Initialization/HelperPlugin.php: -------------------------------------------------------------------------------- 1 | helper = $helper; 26 | } 27 | 28 | /** 29 | * Clean custom entity input of the product edit form. 30 | * 31 | * @param Helper $helper Original helper. 32 | * @param Product $product Product. 33 | * @param array $productData Post product data. 34 | * @return array|null 35 | * @SuppressWarnings(PHPMD.UnusedFormalParameters) 36 | */ 37 | // @codingStandardsIgnoreLine 38 | public function beforeInitializeFromData( 39 | Helper $helper, 40 | Product $product, 41 | array $productData 42 | ): ?array { 43 | foreach ($this->helper->getCustomEntityProductAttributes() as $attribute) { 44 | if (!isset($productData[$attribute->getAttributeCode()])) { 45 | $productData[$attribute->getAttributeCode()] = []; 46 | } 47 | } 48 | 49 | return [$product, $productData]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Plugin/Catalog/Ui/DataProvider/Product/Form/Modifier/EavPlugin.php: -------------------------------------------------------------------------------- 1 | arrayManager = $arrayManager; 27 | } 28 | 29 | /** 30 | * Fix custom entity field meta. 31 | * 32 | * @param EavModifier $subject Object. 33 | * @param callable $proceed Original method. 34 | * @param ProductAttributeInterface $attribute Attribute. 35 | * @param string $groupCode Group code. 36 | * @param int $sortOrder Sort order. 37 | * @return array|null 38 | */ 39 | public function aroundSetupAttributeMeta( 40 | EavModifier $subject, 41 | callable $proceed, 42 | ProductAttributeInterface $attribute, 43 | string $groupCode, 44 | int $sortOrder 45 | ): ?array { 46 | $meta = $proceed($attribute, $groupCode, $sortOrder); 47 | 48 | if ($attribute->getFrontendInput() == "smile_custom_entity") { 49 | $configPath = ltrim($subject::META_CONFIG_PATH, ArrayManager::DEFAULT_PATH_DELIMITER); 50 | 51 | /** @var Attribute $attribute */ 52 | $fieldConfig = [ 53 | 'component' => 'Magento_Ui/js/form/element/ui-select', 54 | 'formElement' => 'multiselect', 55 | 'elementTmpl' => 'ui/grid/filters/elements/ui-select', 56 | 'filterOptions' => true, 57 | 'multiple' => true, 58 | 'options' => $attribute->getSource()->getAllOptions(), 59 | 'disableLabel' => true, 60 | 'required' => false, 61 | ]; 62 | 63 | $meta = $this->arrayManager->merge($configPath, $meta, $fieldConfig); 64 | } 65 | 66 | return $meta; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Plugin/Controller/Entity/ViewPlugin.php: -------------------------------------------------------------------------------- 1 | layerResolver = $layerResolver; 36 | $this->registry = $registry; 37 | $this->productHelper = $productHelper; 38 | } 39 | 40 | /** 41 | * Create an layer context and add body class. 42 | * 43 | * @param View $subject Custom entity view controller. 44 | * @param callable $proceed Callable method. 45 | * @return mixed 46 | * @SuppressWarnings(PHPMD.UnusedFormalParameters) 47 | */ 48 | public function aroundExecute(View $subject, callable $proceed) 49 | { 50 | $this->layerResolver->create('smile_custom_entity'); 51 | /** @var \Magento\Framework\View\Result\Page $page */ 52 | $page = $proceed(); 53 | if ($this->hasFilterableAttribute()) { 54 | $page->addPageLayoutHandles(['product' => 'list']); 55 | $page->getConfig()->addBodyClass('page-products'); 56 | } 57 | 58 | return $page; 59 | } 60 | 61 | /** 62 | * Return true if has filterable attribute for current custom entity. 63 | */ 64 | private function hasFilterableAttribute(): bool 65 | { 66 | return $this->productHelper->getFilterableAttributeCode( 67 | $this->registry->registry('current_custom_entity') 68 | ) !== ''; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Plugin/Model/Condition/Product/AbstractProduct.php: -------------------------------------------------------------------------------- 1 | getAttributeObject())) { 20 | if ($subject->getAttributeObject()->getFrontendInput() == 'smile_custom_entity') { 21 | $result = 'multiselect'; 22 | } 23 | } 24 | 25 | return $result; 26 | } 27 | 28 | /** 29 | * Return multiselect for smile_custom_entity attribute 30 | */ 31 | public function afterGetValueElementType(Subject $subject, string $result): string 32 | { 33 | if (is_object($subject->getAttributeObject())) { 34 | if ($subject->getAttributeObject()->getFrontendInput() == 'smile_custom_entity') { 35 | $result = 'multiselect'; 36 | } 37 | } 38 | 39 | return $result; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Plugin/Model/ProductPlugin.php: -------------------------------------------------------------------------------- 1 | helper = $helper; 26 | } 27 | 28 | /** 29 | * Append custom entity identities when update product attributes. 30 | * 31 | * @param Product $source Product model. 32 | * @param array $identities Identities. 33 | * @return array|null 34 | */ 35 | public function afterGetIdentities(Product $source, array $identities): ?array 36 | { 37 | $customEntityProductAttributes = $this->helper->getCustomEntityProductAttributes(); 38 | 39 | foreach ($customEntityProductAttributes as $customEntityProductAttribute) { 40 | $attributeCode = $customEntityProductAttribute->getAttributeCode(); 41 | if (!$source->hasData($attributeCode) || !$source->dataHasChangedFor($attributeCode)) { 42 | continue; 43 | } 44 | $customEntityIds = $source->getData($attributeCode); 45 | if (is_string($customEntityIds)) { 46 | $customEntityIds = explode(',', $customEntityIds); 47 | } 48 | foreach ($customEntityIds ?? [] as $customEntityId) { 49 | $identities[] = CustomEntity::CACHE_TAG . '_' . $customEntityId; 50 | } 51 | } 52 | 53 | return array_unique($identities); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Plugin/Model/ResourceModel/Eav/Attribute.php: -------------------------------------------------------------------------------- 1 | getFrontendInput() === 'smile_custom_entity') { 17 | $result = (bool) $subject->getIsVisible(); 18 | } 19 | 20 | return $result; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Setup/Patch/Data/MoveCustomEntityLink.php: -------------------------------------------------------------------------------- 1 | resourceConnection = $resourceConnection; 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | public function apply() 31 | { 32 | $connection = $this->resourceConnection->getConnection(); 33 | $customEntityLink = $connection->getTableName('catalog_product_custom_entity_link'); 34 | 35 | if ($connection->isTableExists($customEntityLink)) { 36 | $catalogText = $connection->getTableName('catalog_product_entity_text'); 37 | $fields = ['attribute_id', 'value', 'entity_id']; 38 | 39 | $select = $connection->select() 40 | ->from( 41 | $customEntityLink, 42 | [ 43 | 'attribute_id' => 'attribute_id', 44 | 'value' => 'GROUP_CONCAT(custom_entity_id SEPARATOR ",")', 45 | 'entity_id' => 'product_id', 46 | ] 47 | ) 48 | ->group(['product_id', 'attribute_id']); 49 | 50 | if ($connection->tableColumnExists($catalogText, 'row_id')) { 51 | $fields = ['attribute_id', 'value', 'row_id']; 52 | $select = $connection->select() 53 | ->from( 54 | ['link' => $customEntityLink], 55 | [ 56 | 'attribute_id' => 'link.attribute_id', 57 | 'value' => 'GROUP_CONCAT(link.custom_entity_id SEPARATOR ",")', 58 | ] 59 | ) 60 | ->join( 61 | ['product' => $connection->getTableName('catalog_product_entity')], 62 | 'product.entity_id = link.product_id', 63 | ['row_id' => 'product.row_id'] 64 | ) 65 | ->group(['link.product_id', 'link.attribute_id']); 66 | } 67 | 68 | $connection->query( 69 | $connection->insertFromSelect($select, $catalogText, $fields, AdapterInterface::INSERT_IGNORE) 70 | ); 71 | } 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * @inheritDoc 78 | */ 79 | public static function getDependencies() 80 | { 81 | return []; 82 | } 83 | 84 | /** 85 | * @inheritDoc 86 | */ 87 | public function getAliases() 88 | { 89 | return []; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Setup/Patch/Data/UpdateProductCustomEntityAttribute.php: -------------------------------------------------------------------------------- 1 | resourceConnection = $resourceConnection; 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | public function apply() 35 | { 36 | $connection = $this->resourceConnection->getConnection(); 37 | 38 | $connection->update( 39 | $connection->getTableName('eav_attribute'), 40 | ['backend_model' => ArrayBackend::class, 'backend_type' => 'text', 'source_model' => Source::class], 41 | ['frontend_input = ?' => 'smile_custom_entity'] 42 | ); 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | public static function getDependencies() 51 | { 52 | return []; 53 | } 54 | 55 | /** 56 | * @inheritDoc 57 | */ 58 | public function getAliases() 59 | { 60 | return []; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smile/module-custom-entity-product-link", 3 | "type": "magento2-module", 4 | "description": "Smile - Custom Entity Product link Module", 5 | "keywords": ["magento2", "custom", "entity", "link", "product"], 6 | "authors": [ 7 | { 8 | "name": "Aurélien FOUCRET", 9 | "email": "aurelien.foucret@smile.fr" 10 | }, 11 | { 12 | "name": "Maxime LECLERCQ", 13 | "email": "maxime.leclercq@smile.fr" 14 | }, 15 | { 16 | "name": "Cédric MAGREZ", 17 | "email": "cedric.magrez@smile.fr" 18 | } 19 | ], 20 | "license": "OSL-3.0", 21 | "config": { 22 | "allow-plugins": { 23 | "dealerdirect/phpcodesniffer-composer-installer": true, 24 | "magento/composer-dependency-version-audit-plugin": true, 25 | "magento/magento-composer-installer": false, 26 | "php-http/discovery": true 27 | }, 28 | "sort-packages": true 29 | }, 30 | "require": { 31 | "php": "^7.4 || ^8.1", 32 | "smile/module-custom-entity": "^1.3", 33 | "smile/elasticsuite": "^2.8.0" 34 | }, 35 | "require-dev": { 36 | "smile/magento2-smilelab-quality-suite": "^3.0" 37 | }, 38 | "autoload": { 39 | "files": [ 40 | "registration.php" 41 | ], 42 | "psr-4": { 43 | "Smile\\CustomEntityProductLink\\" : "" 44 | } 45 | }, 46 | "repositories": [ 47 | { 48 | "type": "composer", 49 | "url": "https://repo.magento.com/" 50 | } 51 | ], 52 | "minimum-stability": "dev", 53 | "prefer-stable": true 54 | } 55 | -------------------------------------------------------------------------------- /etc/adminhtml/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /etc/adminhtml/events.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Smile\CustomEntityProductLink\Model\Product\CustomEntity\ReadHandler 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Smile\CustomEntityProductLink\Model\Layer\CustomEntity 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /etc/elasticsuite_indices.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /etc/extension_attributes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /etc/frontend/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Smile\CustomEntityProductLink\Block\Entity\Attribute\CustomEntity\Renderer 7 | 8 | 9 | 10 | 11 | 12 | 13 | Magento\Catalog\Model\Layer\Search 14 | 15 | 16 | 17 | 18 | Smile\ElasticsuiteCatalog\Model\Layer\Category\ItemCollectionProvider 19 | Magento\Catalog\Model\Layer\Category\StateKey 20 | Smile\CustomEntityProductLink\Model\Layer\CustomEntity\CollectionFilter 21 | 22 | 23 | 24 | 25 | Smile\CustomEntityProductLink\Model\Layer\Context 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Smile\ElasticsuiteCatalog\Model\Layer\Category\FilterableAttributeList 35 | 36 | Smile\ElasticsuiteCatalog\Model\Layer\Filter\Attribute 37 | Smile\ElasticsuiteCatalog\Model\Layer\Filter\Price 38 | Smile\ElasticsuiteCatalog\Model\Layer\Filter\Decimal 39 | Smile\ElasticsuiteCatalog\Model\Layer\Filter\Category 40 | Smile\ElasticsuiteCatalog\Model\Layer\Filter\Boolean 41 | 42 | 43 | 44 | 45 | 46 | customEntityFilterList 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /etc/frontend/events.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | phpVersion: 70400 4 | checkMissingIterableValueType: false 5 | paths: 6 | - . 7 | excludePaths: 8 | - 'vendor/*' 9 | 10 | includes: 11 | - %currentWorkingDirectory%/vendor/smile/magento2-smilelab-phpstan/extension.neon 12 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 | 7 | smile_custom_entity 8 | 9 | 10 | 11 | 12 | true 13 | 14 | 15 | 16 | 17 | 18 | product_attribute 19 | 20 | 21 | 22 | string 23 | 24 | custom_entity_attribute_set_id 25 | 26 | 27 | 32 | 33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /view/adminhtml/web/js/product-attributes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @api 3 | */ 4 | define([ 5 | 'jquery', 6 | 'Magento_Ui/js/modal/alert', 7 | 'Magento_Ui/js/modal/prompt', 8 | 'uiRegistry', 9 | 'collapsable' 10 | ], function ($, alert, prompt, rg) { 11 | 'use strict'; 12 | 13 | return function (optionConfig) { 14 | var activePanelClass = 'selected-type-options', 15 | swatchProductAttributes = { 16 | frontendInput: $('#frontend_input'), 17 | isFilterable: $('#is_filterable'), 18 | isFilterableInSearch: $('#is_filterable_in_search'), 19 | backendType: $('#backend_type'), 20 | usedForSortBy: $('#used_for_sort_by'), 21 | frontendClass: $('#frontend_class'), 22 | isWysiwygEnabled: $('#is_wysiwyg_enabled'), 23 | isHtmlAllowedOnFront: $('#is_html_allowed_on_front'), 24 | isRequired: $('#is_required'), 25 | isUnique: $('#is_unique'), 26 | defaultValueText: $('#default_value_text'), 27 | defaultValueTextarea: $('#default_value_textarea'), 28 | defaultValueDate: $('#default_value_date'), 29 | defaultValueYesno: $('#default_value_yesno'), 30 | isGlobal: $('#is_global'), 31 | useProductImageForSwatch: $('#use_product_image_for_swatch'), 32 | updateProductPreviewImage: $('#update_product_preview_image'), 33 | usedInProductListing: $('#used_in_product_listing'), 34 | isVisibleOnFront: $('#is_visible_on_front'), 35 | position: $('#position'), 36 | attrTabsFront: $('#product_attribute_tabs_front'), 37 | customEntityAttributeSetIdField: $('#custom_entity_attribute_set_id'), 38 | 39 | /** 40 | * @returns {*|jQuery|HTMLElement} 41 | */ 42 | get tabsFront() { 43 | return this.attrTabsFront.length ? this.attrTabsFront.closest('li') : $('#front_fieldset-wrapper'); 44 | }, 45 | selectFields: ['select', 'multiselect', 'price', 'swatch_text', 'swatch_visual'], 46 | 47 | /** 48 | * @this {swatchProductAttributes} 49 | */ 50 | toggleApplyVisibility: function (select) { 51 | if ($(select).val() === 1) { 52 | $(select).next('select').removeClass('no-display'); 53 | $(select).next('select').removeClass('ignore-validate'); 54 | } else { 55 | $(select).next('select').addClass('no-display'); 56 | $(select).next('select').addClass('ignore-validate'); 57 | $(select).next('select option:selected').each(function () { 58 | this.selected = false; 59 | }); 60 | } 61 | }, 62 | 63 | /** 64 | * @this {swatchProductAttributes} 65 | */ 66 | checkOptionsPanelVisibility: function () { 67 | var selectOptionsPanel = $('#manage-options-panel'), 68 | visualOptionsPanel = $('#swatch-visual-options-panel'), 69 | textOptionsPanel = $('#swatch-text-options-panel'); 70 | 71 | this._hidePanel(selectOptionsPanel); 72 | this._hidePanel(visualOptionsPanel); 73 | this._hidePanel(textOptionsPanel); 74 | 75 | switch (this.frontendInput.val()) { 76 | case 'swatch_visual': 77 | this._showPanel(visualOptionsPanel); 78 | break; 79 | 80 | case 'swatch_text': 81 | this._showPanel(textOptionsPanel); 82 | break; 83 | 84 | case 'select': 85 | case 'multiselect': 86 | this._showPanel(selectOptionsPanel); 87 | break; 88 | } 89 | }, 90 | 91 | /** 92 | * @this {swatchProductAttributes} 93 | */ 94 | bindAttributeInputType: function () { 95 | this.checkOptionsPanelVisibility(); 96 | this.switchDefaultValueField(); 97 | 98 | var filterableFrontendInputs = $.merge(this.selectFields, ['text', 'boolean', 'smile_custom_entity']); 99 | if (!~$.inArray(this.frontendInput.val(), filterableFrontendInputs)) { 100 | // not in array 101 | this.isFilterable.selectedIndex = 0; 102 | this._disable(this.isFilterable); 103 | this._disable(this.isFilterableInSearch); 104 | } else { 105 | // in array 106 | this._enable(this.isFilterable); 107 | this._enable(this.isFilterableInSearch); 108 | this.backendType.val('int'); 109 | } 110 | 111 | if (this.frontendInput.val() === 'multiselect' || 112 | this.frontendInput.val() === 'gallery' || 113 | this.frontendInput.val() === 'textarea' 114 | ) { 115 | this._disable(this.usedForSortBy); 116 | } else { 117 | this._enable(this.usedForSortBy); 118 | } 119 | 120 | if (this.frontendInput.val() === 'swatch_text') { 121 | $('.swatch-text-field-0').addClass('required-option'); 122 | } else { 123 | $('.swatch-text-field-0').removeClass('required-option'); 124 | } 125 | 126 | this.setRowVisibility(this.isWysiwygEnabled, false); 127 | this.setRowVisibility(this.isHtmlAllowedOnFront, false); 128 | 129 | switch (this.frontendInput.val()) { 130 | case 'textarea': 131 | this.setRowVisibility(this.isWysiwygEnabled, true); 132 | 133 | if (this.isWysiwygEnabled.val() === '0') { 134 | this._enable(this.isHtmlAllowedOnFront); 135 | } 136 | this.frontendClass.val(''); 137 | this._disable(this.frontendClass); 138 | break; 139 | 140 | case 'text': 141 | this.setRowVisibility(this.isHtmlAllowedOnFront, true); 142 | this._enable(this.frontendClass); 143 | break; 144 | 145 | case 'select': 146 | case 'multiselect': 147 | this.setRowVisibility(this.isHtmlAllowedOnFront, true); 148 | this.frontendClass.val(''); 149 | this._disable(this.frontendClass); 150 | break; 151 | default: 152 | this.frontendClass.val(''); 153 | this._disable(this.frontendClass); 154 | } 155 | 156 | this.switchIsFilterable(); 157 | }, 158 | 159 | /** 160 | * @this {swatchProductAttributes} 161 | */ 162 | switchIsFilterable: function () { 163 | if (this.isFilterable.selectedIndex === 0) { 164 | this._disable(this.position); 165 | } else { 166 | this._enable(this.position); 167 | } 168 | }, 169 | 170 | /** 171 | * @this {swatchProductAttributes} 172 | */ 173 | switchDefaultValueField: function () { 174 | var currentValue = this.frontendInput.val(), 175 | defaultValueTextVisibility = false, 176 | defaultValueTextareaVisibility = false, 177 | defaultValueDateVisibility = false, 178 | defaultValueYesnoVisibility = false, 179 | scopeVisibility = true, 180 | useProductImageForSwatch = false, 181 | defaultValueUpdateImage = false, 182 | defaultCustomEntityAttributeSetId = false, 183 | optionDefaultInputType = '', 184 | isFrontTabHidden = false, 185 | thing = this; 186 | 187 | if (!this.frontendInput.length) { 188 | return; 189 | } 190 | 191 | switch (currentValue) { 192 | case 'select': 193 | optionDefaultInputType = 'radio'; 194 | break; 195 | 196 | case 'multiselect': 197 | optionDefaultInputType = 'checkbox'; 198 | break; 199 | 200 | case 'date': 201 | defaultValueDateVisibility = true; 202 | break; 203 | 204 | case 'boolean': 205 | defaultValueYesnoVisibility = true; 206 | break; 207 | 208 | case 'textarea': 209 | case 'texteditor': 210 | defaultValueTextareaVisibility = true; 211 | break; 212 | 213 | case 'media_image': 214 | defaultValueTextVisibility = false; 215 | break; 216 | 217 | case 'price': 218 | scopeVisibility = false; 219 | break; 220 | 221 | case 'swatch_visual': 222 | useProductImageForSwatch = true; 223 | defaultValueUpdateImage = true; 224 | defaultValueTextVisibility = false; 225 | break; 226 | 227 | case 'swatch_text': 228 | useProductImageForSwatch = false; 229 | defaultValueUpdateImage = true; 230 | defaultValueTextVisibility = false; 231 | break; 232 | 233 | case 'smile_custom_entity': 234 | defaultCustomEntityAttributeSetId = true; 235 | break; 236 | default: 237 | defaultValueTextVisibility = true; 238 | break; 239 | } 240 | 241 | delete optionConfig.hiddenFields['swatch_visual']; 242 | delete optionConfig.hiddenFields['swatch_text']; 243 | 244 | if (currentValue === 'media_image') { 245 | this.tabsFront.hide(); 246 | this.setRowVisibility(this.isRequired, false); 247 | this.setRowVisibility(this.isUnique, false); 248 | this.setRowVisibility(this.frontendClass, false); 249 | } else if (optionConfig.hiddenFields[currentValue]) { 250 | $.each(optionConfig.hiddenFields[currentValue], function (key, option) { 251 | switch (option) { 252 | case '_front_fieldset': 253 | thing.tabsFront.hide(); 254 | isFrontTabHidden = true; 255 | break; 256 | 257 | case '_default_value': 258 | defaultValueTextVisibility = false; 259 | defaultValueTextareaVisibility = false; 260 | defaultValueDateVisibility = false; 261 | defaultValueYesnoVisibility = false; 262 | break; 263 | 264 | case '_scope': 265 | scopeVisibility = false; 266 | break; 267 | default: 268 | thing.setRowVisibility($('#' + option), false); 269 | } 270 | }); 271 | 272 | if (!isFrontTabHidden) { 273 | thing.tabsFront.show(); 274 | } 275 | 276 | } else { 277 | this.tabsFront.show(); 278 | this.showDefaultRows(); 279 | } 280 | 281 | this.setRowVisibility(this.defaultValueText, defaultValueTextVisibility); 282 | this.setRowVisibility(this.defaultValueTextarea, defaultValueTextareaVisibility); 283 | this.setRowVisibility(this.defaultValueDate, defaultValueDateVisibility); 284 | this.setRowVisibility(this.defaultValueYesno, defaultValueYesnoVisibility); 285 | this.setRowVisibility(this.isGlobal, scopeVisibility); 286 | 287 | /* swatch attributes */ 288 | this.setRowVisibility(this.useProductImageForSwatch, useProductImageForSwatch); 289 | this.setRowVisibility(this.updateProductPreviewImage, defaultValueUpdateImage); 290 | 291 | /* Smile - custom entity attributes */ 292 | this.setRowVisibility(this.customEntityAttributeSetIdField, defaultCustomEntityAttributeSetId); 293 | 294 | $('input[name=\'default[]\']').each(function () { 295 | $(this).attr('type', optionDefaultInputType); 296 | }); 297 | }, 298 | 299 | /** 300 | * @this {swatchProductAttributes} 301 | */ 302 | showDefaultRows: function () { 303 | this.setRowVisibility(this.isRequired, true); 304 | this.setRowVisibility(this.isUnique, true); 305 | this.setRowVisibility(this.frontendClass, true); 306 | }, 307 | 308 | /** 309 | * @param {Object} el 310 | * @param {Boolean} isVisible 311 | * @this {swatchProductAttributes} 312 | */ 313 | setRowVisibility: function (el, isVisible) { 314 | if (isVisible) { 315 | el.show(); 316 | el.closest('.field').show(); 317 | } else { 318 | el.hide(); 319 | el.closest('.field').hide(); 320 | } 321 | }, 322 | 323 | /** 324 | * @param {Object} el 325 | * @this {swatchProductAttributes} 326 | */ 327 | _disable: function (el) { 328 | el.attr('disabled', 'disabled'); 329 | }, 330 | 331 | /** 332 | * @param {Object} el 333 | * @this {swatchProductAttributes} 334 | */ 335 | _enable: function (el) { 336 | if (!el.attr('readonly')) { 337 | el.removeAttr('disabled'); 338 | } 339 | }, 340 | 341 | /** 342 | * @param {Object} el 343 | * @this {swatchProductAttributes} 344 | */ 345 | _showPanel: function (el) { 346 | el.closest('.fieldset').show(); 347 | el.addClass(activePanelClass); 348 | this._render(el.attr('id')); 349 | }, 350 | 351 | /** 352 | * @param {Object} el 353 | * @this {swatchProductAttributes} 354 | */ 355 | _hidePanel: function (el) { 356 | el.closest('.fieldset').hide(); 357 | el.removeClass(activePanelClass); 358 | }, 359 | 360 | /** 361 | * @param {String} id 362 | * @this {swatchProductAttributes} 363 | */ 364 | _render: function (id) { 365 | rg.get(id, function () { 366 | $('#' + id).trigger('render'); 367 | }); 368 | }, 369 | 370 | /** 371 | * @param {String} promptMessage 372 | * @this {swatchProductAttributes} 373 | */ 374 | saveAttributeInNewSet: function (promptMessage) { 375 | 376 | prompt({ 377 | content: promptMessage, 378 | actions: { 379 | 380 | /** 381 | * @param {String} val 382 | * @this {actions} 383 | */ 384 | confirm: function (val) { 385 | var rules = ['required-entry', 'validate-no-html-tags'], 386 | newAttributeSetNameInputId = $('#new_attribute_set_name'), 387 | editForm = $('#edit_form'), 388 | newAttributeSetName = val, 389 | i; 390 | 391 | if (!newAttributeSetName) { 392 | return; 393 | } 394 | 395 | for (i = 0; i < rules.length; i++) { 396 | if (!$.validator.methods[rules[i]](newAttributeSetName)) { 397 | alert({ 398 | content: $.validator.messages[rules[i]] 399 | }); 400 | 401 | return; 402 | } 403 | } 404 | 405 | if (newAttributeSetNameInputId.length) { 406 | newAttributeSetNameInputId.val(newAttributeSetName); 407 | } else { 408 | editForm.append(new Element('input', { 409 | type: 'hidden', 410 | id: newAttributeSetNameInputId, 411 | name: 'new_attribute_set_name', 412 | value: newAttributeSetName 413 | }) 414 | ); 415 | } 416 | // Temporary solution will replaced after refactoring of attributes functionality 417 | editForm.triggerHandler('save'); 418 | } 419 | } 420 | }); 421 | } 422 | }; 423 | 424 | $(function () { 425 | var editForm = $('#edit_form'), 426 | swatchVisualPanel = $('#swatch-visual-options-panel'), 427 | swatchTextPanel = $('#swatch-text-options-panel'), 428 | tableBody = $(), 429 | activePanel = $(); 430 | 431 | $('#frontend_input').bind('change', function () { 432 | swatchProductAttributes.bindAttributeInputType(); 433 | }); 434 | $('#is_filterable').bind('change', function () { 435 | swatchProductAttributes.switchIsFilterable(); 436 | }); 437 | 438 | swatchProductAttributes.bindAttributeInputType(); 439 | 440 | // @todo: refactor collapsable component 441 | $('.attribute-popup .collapse, [data-role="advanced_fieldset-content"]') 442 | .collapsable() 443 | .collapse('hide'); 444 | 445 | editForm.on('beforeSubmit', function () { 446 | var optionContainer, optionsValues; 447 | 448 | activePanel = swatchTextPanel.hasClass(activePanelClass) ? swatchTextPanel : swatchVisualPanel; 449 | optionContainer = activePanel.find('table tbody'); 450 | 451 | if (activePanel.hasClass(activePanelClass)) { 452 | optionsValues = $.map( 453 | optionContainer.find('tr'), 454 | function (row) { 455 | return $(row).find('input, select, textarea').serialize(); 456 | } 457 | ); 458 | $('') 459 | .attr({ 460 | type: 'hidden', 461 | name: 'serialized_options' 462 | }) 463 | .val(JSON.stringify(optionsValues)) 464 | .prependTo(editForm); 465 | } 466 | 467 | tableBody = optionContainer.detach(); 468 | }); 469 | 470 | editForm.on('afterValidate.error highlight.validate', function () { 471 | if (activePanel.hasClass(activePanelClass)) { 472 | activePanel.find('table').append(tableBody); 473 | $('input[name="serialized_options"]').remove(); 474 | } 475 | }); 476 | }); 477 | 478 | window.saveAttributeInNewSet = swatchProductAttributes.saveAttributeInNewSet; 479 | window.toggleApplyVisibility = swatchProductAttributes.toggleApplyVisibility; 480 | }; 481 | }); 482 | -------------------------------------------------------------------------------- /view/frontend/layout/smile_custom_entity_entity_view_product_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | positions:list-secondary 12 | Magento\Catalog\ViewModel\Product\OptionsData 13 | 14 | 15 | 16 | 17 | 18 | product_list_toolbar 19 | 20 | 21 | 22 | 23 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 | 41 | 42 | 45 | 46 | 49 | 50 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /view/frontend/templates/entity/attribute/custom_entity/renderer.phtml: -------------------------------------------------------------------------------- 1 | getCustomEntity(); 10 | ?> 11 | 12 | 13 | getImage()): ?> 14 | <?= $escaper->escapeHtml($customEntity->getName()); ?> 18 | 19 | escapeHtml($customEntity->getName()); ?> 20 | 21 | 22 | --------------------------------------------------------------------------------