From 05ce8dba2a38395a488c4d6044dab7e0569ada35 Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 20 Mar 2026 11:42:53 +0100 Subject: [PATCH 01/13] Made start on refactoring attribute landing admin functionalities --- .../Adminhtml/Ajax/FacetAttributes.php | 96 ++++++++++++ src/Controller/Adminhtml/Ajax/Facets.php | 81 ++++++++++ src/etc/adminhtml/di.xml | 18 +++ src/etc/adminhtml/routes.xml | 9 ++ .../emico_attributelanding_page_form.xml | 128 ++-------------- src/view/adminhtml/web/js/category-select.js | 64 -------- .../web/js/dynamic-rows/dynamic-rows.js | 39 +++++ .../web/js/filter-attribute-values.js | 39 ----- .../adminhtml/web/js/filter-attributes.js | 128 ---------------- .../js/form/element/filter-attribute-value.js | 82 +++++++++++ .../web/js/form/element/filter-attribute.js | 139 ++++++++++++++++++ .../web/template/attribute_values.html | 16 -- .../adminhtml/web/template/attributes.html | 16 -- 13 files changed, 479 insertions(+), 376 deletions(-) create mode 100644 src/Controller/Adminhtml/Ajax/FacetAttributes.php create mode 100644 src/Controller/Adminhtml/Ajax/Facets.php create mode 100644 src/etc/adminhtml/di.xml create mode 100644 src/etc/adminhtml/routes.xml delete mode 100644 src/view/adminhtml/web/js/category-select.js create mode 100644 src/view/adminhtml/web/js/dynamic-rows/dynamic-rows.js delete mode 100644 src/view/adminhtml/web/js/filter-attribute-values.js delete mode 100644 src/view/adminhtml/web/js/filter-attributes.js create mode 100644 src/view/adminhtml/web/js/form/element/filter-attribute-value.js create mode 100644 src/view/adminhtml/web/js/form/element/filter-attribute.js delete mode 100644 src/view/adminhtml/web/template/attribute_values.html delete mode 100644 src/view/adminhtml/web/template/attributes.html diff --git a/src/Controller/Adminhtml/Ajax/FacetAttributes.php b/src/Controller/Adminhtml/Ajax/FacetAttributes.php new file mode 100644 index 0000000..e87fec0 --- /dev/null +++ b/src/Controller/Adminhtml/Ajax/FacetAttributes.php @@ -0,0 +1,96 @@ +resultJsonFactory->create(); + $facetKey = $this->request->getParam('facet_key'); + $otherAttributeOption = ['value' => Facets::OTHER_ATTRIBUTE_VALUE, 'label' => 'Other (text field)']; + + if ($facetKey === Facets::OTHER_ATTRIBUTE_VALUE) { + return $result->setData([$otherAttributeOption]); + } + + $facetAttributeRequest = $this->requestFactory->create(); + + $filterTemplate = $this->request->getParam('filter_template'); + if ($filterTemplate) { + $facetAttributeRequest->setParameter('tn_ft', $filterTemplate); + } + + if ($facetKey && $facetAttributeRequest instanceof FacetAttributeRequest) { + $facetAttributeRequest->addFacetKey($facetKey); + } + + $allStores = $facetAttributeRequest->getStores(); + $attributes = []; + foreach ($allStores as $store) { + $categoryId = $this->helper->getTweakwiseId( + (int)$store->getId(), + (int) $this->request->getParam('category_id') + ); + if ($categoryId) { + $facetAttributeRequest->setParameter('tn_cid', $categoryId); + } + + /** @var FacetAttributesResponse $response */ + $response = $this->client->request($facetAttributeRequest); + + // @phpstan-ignore-next-line + if (!$response) { + return $result->setData([$otherAttributeOption]); + } + + foreach ($response->getAttributes() as $attribute) { + $attributes[] = [ + 'value' => $attribute['title'], + 'label' => $attribute['title'] + ]; + } + } + + $attributes[] = $otherAttributeOption; + + $attributes = array_values(array_unique($attributes, SORT_REGULAR)); + + return $result->setData($attributes); + } +} diff --git a/src/Controller/Adminhtml/Ajax/Facets.php b/src/Controller/Adminhtml/Ajax/Facets.php new file mode 100644 index 0000000..2897db3 --- /dev/null +++ b/src/Controller/Adminhtml/Ajax/Facets.php @@ -0,0 +1,81 @@ +resultJsonFactory->create(); + $facetRequest = $this->requestFactory->create(); + + $filterTemplate = $this->request->getParam('filter_template'); + if ($filterTemplate) { + $facetRequest->setParameter('tn_ft', $filterTemplate); + } + + $allStores = $facetRequest->getStores(); + $facets = []; + foreach ($allStores as $store) { + $categoryId = $this->helper->getTweakwiseId( + (int)$store->getId(), + (int) $this->request->getParam('category_id') + ); + if ($categoryId) { + $facetRequest->setParameter('tn_cid', $categoryId); + } + + /** @var FacetResponse $response */ + $response = $this->client->request($facetRequest); + + foreach ($response->getFacets() as $facet) { + $facets[] = [ + 'value' => $facet->getFacetSettings()->getUrlKey(), + 'label' => $facet->getFacetSettings()->getTitle() + ]; + } + } + + $facets[] = ['value' => self::OTHER_ATTRIBUTE_VALUE, 'label' => 'Other (text field)']; + + $facets = array_values(array_unique($facets, SORT_REGULAR)); + + return $result->setData($facets); + } +} diff --git a/src/etc/adminhtml/di.xml b/src/etc/adminhtml/di.xml new file mode 100644 index 0000000..f012830 --- /dev/null +++ b/src/etc/adminhtml/di.xml @@ -0,0 +1,18 @@ + + + + + + Tweakwise\Magento2Tweakwise\Model\Client\RequestFactory\FacetRequest + + + + + + + Tweakwise\Magento2Tweakwise\Model\Client\RequestFactory\FacetAttributeRequest + + + + diff --git a/src/etc/adminhtml/routes.xml b/src/etc/adminhtml/routes.xml new file mode 100644 index 0000000..8f123ff --- /dev/null +++ b/src/etc/adminhtml/routes.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/view/adminhtml/ui_component/emico_attributelanding_page_form.xml b/src/view/adminhtml/ui_component/emico_attributelanding_page_form.xml index be6931e..ddc501b 100644 --- a/src/view/adminhtml/ui_component/emico_attributelanding_page_form.xml +++ b/src/view/adminhtml/ui_component/emico_attributelanding_page_form.xml @@ -1,127 +1,29 @@
-
- - - Emico\AttributeLanding\Ui\Component\Product\Form\Categories\Options - - field - Tweakwise_AttributeLandingTweakwise/js/category-select - ui/grid/filters/elements/ui-select - true - false - true - false - 1 - 20 - true - - setParsed - - - - - - text - - category_id - - false - - - - - - - - - - Add Value - - false - true - - dynamicRows - - false - - - - - - true - true - Magento_Ui/js/dynamic-rows/record - container - filter_attributes - - - - - - false - Attribute - text - Tweakwise_AttributeLandingTweakwise/attributes - - - - - - - false - Value - text - Tweakwise_AttributeLandingTweakwise/attribute_values - - - - +
+ + + + - attribute - option_ - false - text - attribute - false + attribute_other - + + - value - option_ - false - text - value - false + attribute_value_other - - - - true - option_ - Params.delete - text - - - - text - actionDelete - - - + + - + Page @@ -149,7 +51,7 @@ - + Page @@ -177,7 +79,7 @@ - + Page @@ -200,5 +102,5 @@ -
+
diff --git a/src/view/adminhtml/web/js/category-select.js b/src/view/adminhtml/web/js/category-select.js deleted file mode 100644 index a39743a..0000000 --- a/src/view/adminhtml/web/js/category-select.js +++ /dev/null @@ -1,64 +0,0 @@ -define([ - 'Magento_Ui/js/form/element/ui-select', - 'jquery' -], function (Select, $) { - 'use strict'; - - return Select.extend({ - - initialize: function () { - this._super(); - this.onUpdate(this.value()); - return this; - }, - - /** - * Parse data and set it to options. - * - * @param {Object} data - Response data object. - * @returns {Object} - */ - setParsed: function (data) { - var option = this.parseData(data); - - if (data.error) { - return this; - } - - this.options([]); - this.setOption(option); - this.set('newOption', option); - }, - - /** - * Normalize option object. - * - * @param {Object} data - Option object. - * @returns {Object} - */ - parseData: function (data) { - return { - 'is_active': data.category['is_active'], - level: data.category.level, - value: data.category['entity_id'], - label: data.category.name, - parent: data.category.parent - }; - }, - - /** - * Change currently selected option - * - * @param {String} id - */ - onUpdate: function(value){ - setTimeout(function() { - if((value == '')||(value == undefined)) { - $('.filter_attributes').hide(); - } else { - $('.filter_attributes').show(); - } - }, 1000); - }, - }); -}); diff --git a/src/view/adminhtml/web/js/dynamic-rows/dynamic-rows.js b/src/view/adminhtml/web/js/dynamic-rows/dynamic-rows.js new file mode 100644 index 0000000..0f78c28 --- /dev/null +++ b/src/view/adminhtml/web/js/dynamic-rows/dynamic-rows.js @@ -0,0 +1,39 @@ +define([ + 'Magento_Ui/js/dynamic-rows/dynamic-rows', + 'uiRegistry', + 'ko' +], function (DynamicRows, registry, ko) { + 'use strict'; + + return DynamicRows.extend({ + /** + * @returns {Object} + */ + initialize: function () { + this._super(); + + this.visible = ko.observable(false); + + registry.get(`${this.parentName}.category_id`, function (categoryField) { + this.updateVisibility(categoryField.value()); + + categoryField.value.subscribe(this.updateVisibility.bind(this)); + }.bind(this)); + + return this; + }, + + /** + * Only show filter_attributes rows when a category is selected + * @param {Array|String} categoryValue + */ + updateVisibility: function (categoryValue) { + if (Array.isArray(categoryValue)) { + this.visible(false); + return; + } + + this.visible(true); + } + }); +}); diff --git a/src/view/adminhtml/web/js/filter-attribute-values.js b/src/view/adminhtml/web/js/filter-attribute-values.js deleted file mode 100644 index 2a895c5..0000000 --- a/src/view/adminhtml/web/js/filter-attribute-values.js +++ /dev/null @@ -1,39 +0,0 @@ -define([ - 'jquery', - 'uiRegistry', - 'mage/url', -], function($, registery, url) { - 'use strict'; - - $.widget('tweakwise.filter_attribute_values', { - /** - * Bind handlers to events - */ - _create: function(config, element) { - this._bindEvents(this.options.name); - }, - - _bindEvents(name) { - var input = $('input[name="' + name.replace('[value-tmp]', '[value]') + '"]'); - input.hide(); - - $('select[name="' + name + '"]').on('change', function(evt) { - var name = evt.target.name; - var facetValue = evt.target.value; - var category_id = registery.get('emico_attributelanding_page_form.emico_attributelanding_page_form.general.category_id').value(); - var select = $('select[name="' + name + '"]'); - var input = $('input[name="' + name.replace('[value-tmp]', '[value]') + '"]'); - - if (select.val() == 'tw_other') { - input.show(); - } else { - input.hide(); - input.val(select.val()); - input.change(); - } - }); - } - }); - - return $.tweakwise.filter_attribute_values; -}); diff --git a/src/view/adminhtml/web/js/filter-attributes.js b/src/view/adminhtml/web/js/filter-attributes.js deleted file mode 100644 index 8223132..0000000 --- a/src/view/adminhtml/web/js/filter-attributes.js +++ /dev/null @@ -1,128 +0,0 @@ -define([ - 'jquery', - 'uiRegistry', - 'mage/url', -], function($, registery, url) { - 'use strict'; - - $.widget('tweakwise.filter_attributes', { - /** - * Bind handlers to events - */ - _create: function(config, element) { - this._bindEvents(this.options.name); - this._initAttributes(this.options.name); - }, - - _initAttributes: function (name) { - //bind to event. So it's not triggered by the user, but can be triggered in the code. - $('select[name="' + name + '"]').on('initAttributes', function(evt) { - var name = evt.target.name; - var category_id = registery.get('emico_attributelanding_page_form.emico_attributelanding_page_form.general.category_id').value(); - var selectAttribute = $('select[name="' + name + '"]'); - var inputAttribute = $('input[name="' + name.replace('[attribute-tmp]', '[attribute]') + '"]'); - var inputValue = $('input[name="' + name.replace('[attribute-tmp]', '[value]') + '"]'); - var selectedValue = inputAttribute.val(); - var templateValue = $('select[name="tweakwise_filter_template"]'); - var facetUrl = url.build('/tweakwise/ajax/facets/category/' + category_id); - var foundSelectedValue = false; - - if (templateValue.val() !== '') { - facetUrl += '/filtertemplate/' + templateValue.val(); - } - - inputAttribute.hide(); - inputValue.hide(); - - $.getJSON(facetUrl, function( data ) { - selectAttribute.empty(); - data.data.forEach(value => { - if(value.value != selectedValue) { - selectAttribute.append($("").attr("value", value.value).text(value.label)); - } else { - foundSelectedValue = true; - selectAttribute.append($("").attr("value", value.value).text(value.label).attr("selected", "selected")); - } - }); - - if (foundSelectedValue === false && selectedValue) { - selectAttribute.val('tw_other'); - } else { - //no default value, set value to first option - inputAttribute.val(selectAttribute.val()); - } - selectAttribute.trigger('change'); - }); - }); - - $('select[name="' + name + '"]').trigger('initAttributes'); - }, - - _bindEvents(name) { - $('select[name="' + name + '"]').on('change', function(evt) { - var name = evt.target.name; - var facetValue = evt.target.value; - var category_id = registery.get('emico_attributelanding_page_form.emico_attributelanding_page_form.general.category_id').value(); - var inputValue = $('input[name="' + name.replace('[attribute-tmp]', '[value]') + '"]'); - var inputAttribute = $('input[name="' + name.replace('[attribute-tmp]', '[attribute]') + '"]'); - var selectValue = $('select[name="' + name.replace('[attribute-tmp]', '[value-tmp]') + '"]'); - var templateValue = $('select[name="tweakwise_filter_template"]'); - var facetUrl = url.build('/tweakwise/ajax/facetattributes/category/' + category_id + '/facetkey/' + facetValue); - var foundSelectedValue = false; - - if (templateValue.val() !== '') { - facetUrl += '/filtertemplate/' + templateValue.val(); - } - - //no value selected, load initial value - if (!facetValue) { - facetValue = inputAttribute.val(); - } - - if (facetValue == 'tw_other') { - inputAttribute.show(); - selectValue.val('tw_other'); - inputValue.show(); - } else { - inputAttribute.hide(); - selectValue.val(facetValue); - if (selectValue.val() != 'tw_other') { - inputValue.hide(); - inputAttribute.val(facetValue); - } else { - inputValue.show(); - } - } - - inputAttribute.change(); - - $.getJSON(facetUrl, function (data) { - selectValue.empty(); - data.data.forEach(value => { - if (value.value != inputValue.val()) { - selectValue.append($("").attr("value", value.value).text(value.label)); - } else { - foundSelectedValue = true; - selectValue.append($("").attr("value", value.value).text(value.label).attr("selected", "selected")); - } - }); - - if (foundSelectedValue === false && inputValue.val()) { - selectValue.val('tw_other'); - } else { - //no default value, set value to first option - inputValue.val(selectValue.val()); - inputValue.change(); - } - }); - }); - - //select different filter template - $('select[name="tweakwise_filter_template"]').on('change', function(evt) { - $('select[name*="[attribute-tmp]"]').trigger('initAttributes'); - }); - } - }); - - return $.tweakwise.filter_attributes; -}); diff --git a/src/view/adminhtml/web/js/form/element/filter-attribute-value.js b/src/view/adminhtml/web/js/form/element/filter-attribute-value.js new file mode 100644 index 0000000..6ee974f --- /dev/null +++ b/src/view/adminhtml/web/js/form/element/filter-attribute-value.js @@ -0,0 +1,82 @@ +define([ + 'Magento_Ui/js/form/element/select', + 'jquery', + 'mage/url', + 'uiRegistry' +], function (Select, $, urlBuilder, registry) { + return Select.extend({ + attributeFieldName: 'attribute', + otherFieldName: 'attribute_value_other', + otherValue: 'tw_other', + initialize: function () { + this._super(); + this.savedValue = this.value(); + this.subscribeAttributeValue(); + + return this; + }, + + subscribeAttributeValue: function () { + this.value.subscribe(function (newAttributeValue) { + this.setOtherFieldVisibility(newAttributeValue); + }.bind(this)); + }, + + setInitialValue: function () { + return this; + }, + + initFromAttribute: function (attribute) { + const currentValue = this.value() ? this.value() : this.savedValue; + this.fetchOptions(attribute).then(() => { + this.restoreValue(currentValue); + this.setOtherFieldVisibility(this.value()) + }); + }, + + setOtherFieldVisibility: function (selectedAttributeValue) { + registry.get(`${this.parentName}.${this.otherFieldName}`, function (otherField) { + const otherFieldVisible = selectedAttributeValue === this.otherValue + otherField.disabled(!otherFieldVisible); + if (selectedAttributeValue && !otherFieldVisible) { + otherField.value(''); + } + }.bind(this)); + }, + + restoreValue: function (valueToRestore) { + const optionExists = this.options().some(function (option) { + return option.value === valueToRestore; + }.bind(this)); + + if (optionExists) { + this.value(valueToRestore); + } else { + const firstOption = this.options()[0]; + if(firstOption) { + this.value(firstOption.value); + } + } + }, + + fetchOptions: function (attribute) { + const url = `${this.source.get('admin_url')}tweakwise/ajax/facetattributes`; + const formKey = $('[name="form_key"]').val(); + const filterTemplate = this.source.get('data.tweakwise_filter_template'); + const categoryId = this.source.get('data.category_id'); + + return $.ajax({ + url: url, + type: 'POST', + data: { + form_key: formKey, + category_id: categoryId, + filter_template: filterTemplate, + facet_key: attribute + } + }).done(function (response) { + this.options(response); + }.bind(this)); + } + }); +}); diff --git a/src/view/adminhtml/web/js/form/element/filter-attribute.js b/src/view/adminhtml/web/js/form/element/filter-attribute.js new file mode 100644 index 0000000..56bf1bf --- /dev/null +++ b/src/view/adminhtml/web/js/form/element/filter-attribute.js @@ -0,0 +1,139 @@ +define([ + 'Magento_Ui/js/form/element/select', + 'jquery', + 'mage/url', + 'uiRegistry' +], function (Select, $, urlBuilder, registry) { + return Select.extend({ + attributeValueFieldName: 'value', + otherFieldName: 'attribute_other', + otherValue: 'tw_other', + initialize: function () { + this._super(); + + this.savedValue = this.value(); + this.value(''); + + this.setOtherFieldVisibility(this.savedValue); + + this.fetchOptions().then(function () { + this.setRestoredValue(); + this.subscribeAttribute(); + this.subscribeFilterTemplate(); + this.subscribeCategoryId(); + this.initAttributeValueField(this.value()); + }.bind(this)); + + return this; + }, + + setInitialValue: function () { + return this; + }, + + initObservable: function () { + this._super().observe(['value']); + return this; + }, + + subscribeAttribute: function () { + this.value.subscribe(function (newAttribute) { + this.setOtherFieldVisibility(newAttribute); + this.initAttributeValueField(newAttribute); + }.bind(this)); + }, + + subscribeCategoryId: function () { + const categoryIdPath = 'emico_attributelanding_page_form.emico_attributelanding_page_form.general.category_id'; + registry.get(categoryIdPath, (categoryField) => { + categoryField.value.subscribe((newCategoryId) => { + const currentAttributeValue = this.value(); + this.fetchOptions().then(() => { + this.restoreValue(currentAttributeValue); + }); + }); + }); + }, + + subscribeFilterTemplate: function () { + const filterTemplatePath = 'emico_attributelanding_page_form.emico_attributelanding_page_form.general.tweakwise_filter_template'; + registry.get(filterTemplatePath, (filterTemplateField) => { + filterTemplateField.value.subscribe((newFilterTemplate) => { + const currentAttributeValue = this.value(); + this.fetchOptions().then(() => { + this.restoreValue(currentAttributeValue); + }); + }); + }); + }, + + setOtherFieldVisibility: function (selectedAttribute) { + registry.get(`${this.parentName}.${this.otherFieldName}`, function (otherField) { + const otherFieldVisible = selectedAttribute === this.otherValue + otherField.disabled(!otherFieldVisible); + if (!otherFieldVisible) { + otherField.value(''); + } + }.bind(this)); + }, + + setRestoredValue: function () { + if (!this.savedValue) { + return; + } + + const optionExists = this.options().some(function (option) { + return option.value === this.savedValue; + }.bind(this)); + + if (optionExists) { + this.value(this.savedValue); + } else { + const firstOption = this.options()[0]; + if (firstOption) { + this.value(firstOption.value); + } + } + }, + + restoreValue: function (valueToRestore) { + const optionExists = this.options().some(function (option) { + return option.value === valueToRestore; + }.bind(this)); + + if (optionExists) { + this.value(valueToRestore); + } else { + const firstOption = this.options()[0]; + if (firstOption) { + this.value(firstOption.value); + } + } + }, + + fetchOptions: function () { + const url = `${this.source.get('admin_url')}tweakwise/ajax/facets`; + const formKey = $('[name="form_key"]').val(); + const filterTemplate = this.source.get('data.tweakwise_filter_template'); + const categoryId = this.source.get('data.category_id'); + + return $.ajax({ + url: url, + type: 'POST', + data: { + form_key: formKey, + category_id: categoryId, + filter_template: filterTemplate + } + }).done(function (response) { + this.options(response); + }.bind(this)); + }, + + initAttributeValueField: function (attribute) { + registry.get(`${this.parentName}.${this.attributeValueFieldName}`, (attributeValueField) => { + attributeValueField.initFromAttribute(attribute); + }); + } + }); +}); diff --git a/src/view/adminhtml/web/template/attribute_values.html b/src/view/adminhtml/web/template/attribute_values.html deleted file mode 100644 index 374e045..0000000 --- a/src/view/adminhtml/web/template/attribute_values.html +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/src/view/adminhtml/web/template/attributes.html b/src/view/adminhtml/web/template/attributes.html deleted file mode 100644 index bdbff82..0000000 --- a/src/view/adminhtml/web/template/attributes.html +++ /dev/null @@ -1,16 +0,0 @@ - From 782b65cb3cfe2c7227f978606c51502facfd7815 Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 27 Mar 2026 10:01:02 +0100 Subject: [PATCH 02/13] Made start on backend API controller functionality --- .../Ajax/AbstractFacetController.php | 44 +++++++++ .../Adminhtml/Ajax/FacetAttributes.php | 10 +- src/Controller/Adminhtml/Ajax/Facets.php | 93 +++++++++++++------ src/Model/Config.php | 24 +++++ src/etc/adminhtml/system.xml | 20 ++++ src/etc/config.xml | 11 +++ 6 files changed, 171 insertions(+), 31 deletions(-) create mode 100644 src/Controller/Adminhtml/Ajax/AbstractFacetController.php create mode 100644 src/Model/Config.php create mode 100644 src/etc/adminhtml/system.xml create mode 100644 src/etc/config.xml diff --git a/src/Controller/Adminhtml/Ajax/AbstractFacetController.php b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php new file mode 100644 index 0000000..a376d30 --- /dev/null +++ b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php @@ -0,0 +1,44 @@ +config->getBackendApiToken($store); + } + + /** + * @param Store|null $store + * @return bool + * @throws LocalizedException + */ + protected function isBackendApiEnabled(Store $store = null): bool + { + return (bool)$this->getBackendApiToken($store); + } +} diff --git a/src/Controller/Adminhtml/Ajax/FacetAttributes.php b/src/Controller/Adminhtml/Ajax/FacetAttributes.php index e87fec0..4bc83bd 100644 --- a/src/Controller/Adminhtml/Ajax/FacetAttributes.php +++ b/src/Controller/Adminhtml/Ajax/FacetAttributes.php @@ -5,21 +5,24 @@ namespace Tweakwise\AttributeLandingTweakwise\Controller\Adminhtml\Ajax; use Exception; -use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\Controller\ResultInterface; +use Magento\Store\Model\StoreManagerInterface; +use Tweakwise\AttributeLandingTweakwise\Model\Config; use Tweakwise\Magento2Tweakwise\Model\Client; use Tweakwise\Magento2Tweakwise\Model\Client\Request\FacetAttributeRequest; use Tweakwise\Magento2Tweakwise\Model\Client\RequestFactory; use Tweakwise\Magento2Tweakwise\Model\Client\Response\FacetAttributesResponse; use Tweakwise\Magento2TweakwiseExport\Model\Helper; -class FacetAttributes implements HttpPostActionInterface +class FacetAttributes extends AbstractFacetController { /** + * @param Config $config + * @param StoreManagerInterface $storeManager * @param RequestInterface $request * @param JsonFactory $resultJsonFactory * @param Client $client @@ -27,12 +30,15 @@ class FacetAttributes implements HttpPostActionInterface * @param Helper $helper */ public function __construct( + Config $config, + StoreManagerInterface $storeManager, private readonly RequestInterface $request, private readonly JsonFactory $resultJsonFactory, private readonly Client $client, private readonly RequestFactory $requestFactory, private readonly Helper $helper, ) { + parent::__construct($config, $storeManager); } /** diff --git a/src/Controller/Adminhtml/Ajax/Facets.php b/src/Controller/Adminhtml/Ajax/Facets.php index 2897db3..6a5ac6b 100644 --- a/src/Controller/Adminhtml/Ajax/Facets.php +++ b/src/Controller/Adminhtml/Ajax/Facets.php @@ -4,23 +4,26 @@ namespace Tweakwise\AttributeLandingTweakwise\Controller\Adminhtml\Ajax; -use Magento\Framework\App\Action\HttpPostActionInterface; +use Exception; use Magento\Framework\App\RequestInterface; -use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; -use Magento\Framework\Controller\ResultInterface; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Tweakwise\AttributeLandingTweakwise\Model\Config; use Tweakwise\Magento2Tweakwise\Model\Client; use Tweakwise\Magento2Tweakwise\Model\Client\RequestFactory; use Tweakwise\Magento2Tweakwise\Model\Client\Response\FacetResponse; use Tweakwise\Magento2TweakwiseExport\Model\Helper; -class Facets implements HttpPostActionInterface +class Facets extends AbstractFacetController { public const OTHER_ATTRIBUTE_VALUE = 'tw_other'; /** + * @param Config $config + * @param StoreManagerInterface $storeManager * @param RequestInterface $request * @param JsonFactory $resultJsonFactory * @param Client $client @@ -28,21 +31,50 @@ class Facets implements HttpPostActionInterface * @param Helper $helper */ public function __construct( + Config $config, + StoreManagerInterface $storeManager, private readonly RequestInterface $request, private readonly JsonFactory $resultJsonFactory, private readonly Client $client, private readonly RequestFactory $requestFactory, private readonly Helper $helper, ) { + parent::__construct($config, $storeManager); } /** - * @return ResponseInterface|Json|ResultInterface - * @throws NoSuchEntityException + * @return Json + * @throws LocalizedException + * phpcs:disable Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge */ public function execute() { $result = $this->resultJsonFactory->create(); + + $facets = []; + /** @var Store $store */ + foreach ($this->storeManager->getStores() as $store) { + if ($this->isBackendApiEnabled($store)) { + $facets = array_merge($this->executeBackendApiRequest((int)$store->getId()), $facets); + } + + $facets = array_merge($this->executeDefaultRequest((int)$store->getId()), $facets); + } + + $facets[] = ['value' => self::OTHER_ATTRIBUTE_VALUE, 'label' => 'Other (text field)']; + + $facets = array_values(array_unique($facets, SORT_REGULAR)); + + return $result->setData($facets); + } + + /** + * @param int $storeId + * @return array + * @throws Exception + */ + private function executeDefaultRequest(int $storeId): array + { $facetRequest = $this->requestFactory->create(); $filterTemplate = $this->request->getParam('filter_template'); @@ -50,32 +82,35 @@ public function execute() $facetRequest->setParameter('tn_ft', $filterTemplate); } - $allStores = $facetRequest->getStores(); - $facets = []; - foreach ($allStores as $store) { - $categoryId = $this->helper->getTweakwiseId( - (int)$store->getId(), - (int) $this->request->getParam('category_id') - ); - if ($categoryId) { - $facetRequest->setParameter('tn_cid', $categoryId); - } + $categoryId = $this->helper->getTweakwiseId( + $storeId, + (int) $this->request->getParam('category_id') + ); + if ($categoryId) { + $facetRequest->setParameter('tn_cid', $categoryId); + } - /** @var FacetResponse $response */ - $response = $this->client->request($facetRequest); + /** @var FacetResponse $response */ + $response = $this->client->request($facetRequest); - foreach ($response->getFacets() as $facet) { - $facets[] = [ - 'value' => $facet->getFacetSettings()->getUrlKey(), - 'label' => $facet->getFacetSettings()->getTitle() - ]; - } + $facets = []; + foreach ($response->getFacets() as $facet) { + $facets[] = [ + 'value' => $facet->getFacetSettings()->getUrlKey(), + 'label' => $facet->getFacetSettings()->getTitle() + ]; } - $facets[] = ['value' => self::OTHER_ATTRIBUTE_VALUE, 'label' => 'Other (text field)']; - - $facets = array_values(array_unique($facets, SORT_REGULAR)); + return $facets; + } - return $result->setData($facets); + /** + * TO DO CREATE FUNCTIONALITY + * @param int $storeId + * @return array + */ + private function executeBackendApiRequest(int $storeId): array + { + return [$storeId]; } } diff --git a/src/Model/Config.php b/src/Model/Config.php new file mode 100644 index 0000000..38ee13a --- /dev/null +++ b/src/Model/Config.php @@ -0,0 +1,24 @@ +getStoreConfig(self::BACKEND_API_TOKEN_PATH, $store); + } +} diff --git a/src/etc/adminhtml/system.xml b/src/etc/adminhtml/system.xml new file mode 100644 index 0000000..bc0682c --- /dev/null +++ b/src/etc/adminhtml/system.xml @@ -0,0 +1,20 @@ + + + +
+ + + + + OPTIONAL: Enter the backend API token only if you want to use the Tweakwise backend API to + retrieve attributes when creating attribute landing pages. + + Magento\Config\Model\Config\Backend\Encrypted + + +
+
+
diff --git a/src/etc/config.xml b/src/etc/config.xml new file mode 100644 index 0000000..3ff8eca --- /dev/null +++ b/src/etc/config.xml @@ -0,0 +1,11 @@ + + + + + + + + + + From 6324a5874cbc0a845cea3f88236f05bd75a63c54 Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 27 Mar 2026 11:25:53 +0100 Subject: [PATCH 03/13] Implemented facet backend API functionality --- src/ApiClient/BackendApiClient.php | 124 ++++++++++++++++++ .../Ajax/AbstractFacetController.php | 17 +-- .../Adminhtml/Ajax/FacetAttributes.php | 5 +- src/Controller/Adminhtml/Ajax/Facets.php | 17 +-- src/Model/Config.php | 4 +- 5 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 src/ApiClient/BackendApiClient.php diff --git a/src/ApiClient/BackendApiClient.php b/src/ApiClient/BackendApiClient.php new file mode 100644 index 0000000..0a59776 --- /dev/null +++ b/src/ApiClient/BackendApiClient.php @@ -0,0 +1,124 @@ +httpClient === null) { + $this->httpClient = new Client( + [ + 'base_uri' => self::TWEAKWISE_BACKEND_API_BASE_URL + ] + ); + } + + return $this->httpClient; + } + + /** + * @param string $path + * @param Store $store + * @return ResponseInterface + * @throws GuzzleException + * @throws LocalizedException + */ + public function doRequest( + string $path, + Store $store + ): ResponseInterface { + try { + return $this->getHttpClient() + ->request( + 'GET', + $path, + [ + 'headers' => [ + 'TWN-InstanceKey' => $this->config->getGeneralAuthenticationKey($store), + 'TWN-Authentication' => $this->config->getBackendApiToken($store) + ] + ] + ); + } catch (Exception $e) { + $this->logger->critical( + 'Tweakwise Backend API request failed', + [ + 'path' => $path, + 'exception' => $e->getMessage() + ] + ); + throw $e; + } + } + + /** + * @param Store $store + * @return array + */ + public function getAttributes(Store $store): array + { + $attributes = []; + try { + $response = $this->doRequest('attribute', $store); + $contents = $response->getBody()->getContents(); + $result = $this->jsonSerializer->unserialize($contents); + + if (!isset($result['Records'])) { + return []; + } + + foreach ($result['Records'] as $record) { + $attributes[] = [ + 'value' => $record['UrlName'], + 'label' => $record['Name'] + ]; + } + + return $attributes; + } catch (GuzzleException | Exception $e) { + $this->logger->critical( + 'Retrieving attributes from Tweakiwse Backend API failed', + [ + 'exception' => $e->getMessage() + ] + ); + return []; + } + } +} diff --git a/src/Controller/Adminhtml/Ajax/AbstractFacetController.php b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php index a376d30..abe5322 100644 --- a/src/Controller/Adminhtml/Ajax/AbstractFacetController.php +++ b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php @@ -8,30 +8,25 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Tweakwise\AttributeLandingTweakwise\ApiClient\BackendApiClient; use Tweakwise\AttributeLandingTweakwise\Model\Config; abstract class AbstractFacetController implements HttpPostActionInterface { + protected const OTHER_ATTRIBUTE_VALUE = 'tw_other'; + /** * @param Config $config * @param StoreManagerInterface $storeManager + * @param BackendApiClient $backendApiClient */ public function __construct( private readonly Config $config, protected readonly StoreManagerInterface $storeManager, + protected readonly BackendApiClient $backendApiClient, ) { } - /** - * @param Store|null $store - * @return string|null - * @throws LocalizedException - */ - protected function getBackendApiToken(?Store $store = null): ?string - { - return $this->config->getBackendApiToken($store); - } - /** * @param Store|null $store * @return bool @@ -39,6 +34,6 @@ protected function getBackendApiToken(?Store $store = null): ?string */ protected function isBackendApiEnabled(Store $store = null): bool { - return (bool)$this->getBackendApiToken($store); + return (bool)$this->config->getBackendApiToken($store); } } diff --git a/src/Controller/Adminhtml/Ajax/FacetAttributes.php b/src/Controller/Adminhtml/Ajax/FacetAttributes.php index 4bc83bd..2e766ac 100644 --- a/src/Controller/Adminhtml/Ajax/FacetAttributes.php +++ b/src/Controller/Adminhtml/Ajax/FacetAttributes.php @@ -11,6 +11,7 @@ use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\Controller\ResultInterface; use Magento\Store\Model\StoreManagerInterface; +use Tweakwise\AttributeLandingTweakwise\ApiClient\BackendApiClient; use Tweakwise\AttributeLandingTweakwise\Model\Config; use Tweakwise\Magento2Tweakwise\Model\Client; use Tweakwise\Magento2Tweakwise\Model\Client\Request\FacetAttributeRequest; @@ -23,6 +24,7 @@ class FacetAttributes extends AbstractFacetController /** * @param Config $config * @param StoreManagerInterface $storeManager + * @param BackendApiClient $backendApiClient * @param RequestInterface $request * @param JsonFactory $resultJsonFactory * @param Client $client @@ -32,13 +34,14 @@ class FacetAttributes extends AbstractFacetController public function __construct( Config $config, StoreManagerInterface $storeManager, + BackendApiClient $backendApiClient, private readonly RequestInterface $request, private readonly JsonFactory $resultJsonFactory, private readonly Client $client, private readonly RequestFactory $requestFactory, private readonly Helper $helper, ) { - parent::__construct($config, $storeManager); + parent::__construct($config, $storeManager, $backendApiClient); } /** diff --git a/src/Controller/Adminhtml/Ajax/Facets.php b/src/Controller/Adminhtml/Ajax/Facets.php index 6a5ac6b..09b61b9 100644 --- a/src/Controller/Adminhtml/Ajax/Facets.php +++ b/src/Controller/Adminhtml/Ajax/Facets.php @@ -11,6 +11,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Tweakwise\AttributeLandingTweakwise\ApiClient\BackendApiClient; use Tweakwise\AttributeLandingTweakwise\Model\Config; use Tweakwise\Magento2Tweakwise\Model\Client; use Tweakwise\Magento2Tweakwise\Model\Client\RequestFactory; @@ -19,11 +20,10 @@ class Facets extends AbstractFacetController { - public const OTHER_ATTRIBUTE_VALUE = 'tw_other'; - /** * @param Config $config * @param StoreManagerInterface $storeManager + * @param BackendApiClient $backendApiClient * @param RequestInterface $request * @param JsonFactory $resultJsonFactory * @param Client $client @@ -33,13 +33,14 @@ class Facets extends AbstractFacetController public function __construct( Config $config, StoreManagerInterface $storeManager, + BackendApiClient $backendApiClient, private readonly RequestInterface $request, private readonly JsonFactory $resultJsonFactory, private readonly Client $client, private readonly RequestFactory $requestFactory, private readonly Helper $helper, ) { - parent::__construct($config, $storeManager); + parent::__construct($config, $storeManager, $backendApiClient); } /** @@ -55,7 +56,8 @@ public function execute() /** @var Store $store */ foreach ($this->storeManager->getStores() as $store) { if ($this->isBackendApiEnabled($store)) { - $facets = array_merge($this->executeBackendApiRequest((int)$store->getId()), $facets); + $facets = array_merge($this->executeBackendApiRequest($store), $facets); + continue; } $facets = array_merge($this->executeDefaultRequest((int)$store->getId()), $facets); @@ -105,12 +107,11 @@ private function executeDefaultRequest(int $storeId): array } /** - * TO DO CREATE FUNCTIONALITY - * @param int $storeId + * @param Store $store * @return array */ - private function executeBackendApiRequest(int $storeId): array + private function executeBackendApiRequest(Store $store): array { - return [$storeId]; + return $this->backendApiClient->getAttributes($store); } } diff --git a/src/Model/Config.php b/src/Model/Config.php index 38ee13a..0773053 100644 --- a/src/Model/Config.php +++ b/src/Model/Config.php @@ -6,9 +6,9 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Store\Model\Store; -use Tweakwise\Magento2Tweakwise\Model\Config as TweakwiseConfig; +use Tweakwise\Magento2Tweakwise\Model\Config as BaseConfig; -class Config extends TweakwiseConfig +class Config extends BaseConfig { private const BACKEND_API_TOKEN_PATH = 'tweakwise/attribute_landing/backend_api_token'; From a939d746ac8917c85d049c9ac9d0a6f80942d4d9 Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 27 Mar 2026 11:58:27 +0100 Subject: [PATCH 04/13] Added caching for backend API requests --- src/ApiClient/BackendApiClient.php | 43 +++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/ApiClient/BackendApiClient.php b/src/ApiClient/BackendApiClient.php index 0a59776..2ec9635 100644 --- a/src/ApiClient/BackendApiClient.php +++ b/src/ApiClient/BackendApiClient.php @@ -8,6 +8,7 @@ use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\GuzzleException; +use Magento\Framework\App\CacheInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Serialize\Serializer\Json; use Magento\Store\Model\Store; @@ -18,6 +19,7 @@ class BackendApiClient { private const TWEAKWISE_BACKEND_API_BASE_URL = 'https://navigator-api.tweakwise.com'; + private const CACHE_LIFETIME = 600; /** * @var ClientInterface|null @@ -28,11 +30,13 @@ class BackendApiClient * @param Config $config * @param LoggerInterface $logger * @param Json $jsonSerializer + * @param CacheInterface $cache */ public function __construct( private readonly Config $config, private readonly LoggerInterface $logger, private readonly Json $jsonSerializer, + private readonly CacheInterface $cache ) { } @@ -93,6 +97,11 @@ public function doRequest( */ public function getAttributes(Store $store): array { + $cacheKey = $this->getCacheKey('attributes', (int)$store->getId()); + if ($this->cacheExists($cacheKey)) { + return $this->getFromCache($cacheKey); + } + $attributes = []; try { $response = $this->doRequest('attribute', $store); @@ -106,10 +115,13 @@ public function getAttributes(Store $store): array foreach ($result['Records'] as $record) { $attributes[] = [ 'value' => $record['UrlName'], - 'label' => $record['Name'] + 'label' => $record['Name'], + 'id' => $record['Id'] ]; } + $this->cache->save($this->jsonSerializer->serialize($attributes), $cacheKey, [], self::CACHE_LIFETIME); + return $attributes; } catch (GuzzleException | Exception $e) { $this->logger->critical( @@ -121,4 +133,33 @@ public function getAttributes(Store $store): array return []; } } + + /** + * @param string $type + * @param int $storeId + * @return string + */ + private function getCacheKey(string $type, int $storeId): string + { + return sprintf('tweakwise_backend_api_result_%s_%s', $type, $storeId); + } + + /** + * @param string $cacheKey + * @return bool + */ + private function cacheExists(string $cacheKey): bool + { + /** @phpstan-ignore-next-line */ + return $this->cache->load($cacheKey) !== false; + } + + /** + * @param string $cacheKey + * @return array + */ + private function getFromCache(string $cacheKey): array + { + return (array)$this->jsonSerializer->unserialize($this->cache->load($cacheKey)); + } } From c83ac5510f6217897601b98f7483fb607559b52d Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 27 Mar 2026 13:10:41 +0100 Subject: [PATCH 05/13] Made start on FacetAttributes backend API functionality --- .../Adminhtml/Ajax/FacetAttributes.php | 88 +++++++++++++------ 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/src/Controller/Adminhtml/Ajax/FacetAttributes.php b/src/Controller/Adminhtml/Ajax/FacetAttributes.php index 2e766ac..e2f504d 100644 --- a/src/Controller/Adminhtml/Ajax/FacetAttributes.php +++ b/src/Controller/Adminhtml/Ajax/FacetAttributes.php @@ -10,6 +10,8 @@ use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\Result\JsonFactory; use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Tweakwise\AttributeLandingTweakwise\ApiClient\BackendApiClient; use Tweakwise\AttributeLandingTweakwise\Model\Config; @@ -46,18 +48,45 @@ public function __construct( /** * @return ResponseInterface|Json|ResultInterface - * @throws Exception + * @throws LocalizedException + * phpcs:disable Magento2.Performance.ForeachArrayMerge.ForeachArrayMerge */ public function execute() { $result = $this->resultJsonFactory->create(); $facetKey = $this->request->getParam('facet_key'); - $otherAttributeOption = ['value' => Facets::OTHER_ATTRIBUTE_VALUE, 'label' => 'Other (text field)']; + $otherAttributeOption = ['value' => self::OTHER_ATTRIBUTE_VALUE, 'label' => 'Other (text field)']; - if ($facetKey === Facets::OTHER_ATTRIBUTE_VALUE) { + if ($facetKey === self::OTHER_ATTRIBUTE_VALUE) { return $result->setData([$otherAttributeOption]); } + $attributeValues = []; + /** @var Store $store */ + foreach ($this->storeManager->getStores() as $store) { + if ($this->isBackendApiEnabled($store)) { + $attributeValues = array_merge($this->executeBackendApiRequest($store), $attributeValues); + continue; + } + + $attributeValues = array_merge($this->executeDefaultRequest((int)$store->getId(), $facetKey), $attributeValues); + } + + $attributeValues[] = $otherAttributeOption; + + $attributeValues = array_values(array_unique($attributeValues, SORT_REGULAR)); + + return $result->setData($attributeValues); + } + + /** + * @param int $storeId + * @param string|null $facetKey + * @return array + * @throws Exception + */ + private function executeDefaultRequest(int $storeId, ?string $facetKey): array + { $facetAttributeRequest = $this->requestFactory->create(); $filterTemplate = $this->request->getParam('filter_template'); @@ -69,37 +98,38 @@ public function execute() $facetAttributeRequest->addFacetKey($facetKey); } - $allStores = $facetAttributeRequest->getStores(); - $attributes = []; - foreach ($allStores as $store) { - $categoryId = $this->helper->getTweakwiseId( - (int)$store->getId(), - (int) $this->request->getParam('category_id') - ); - if ($categoryId) { - $facetAttributeRequest->setParameter('tn_cid', $categoryId); - } - - /** @var FacetAttributesResponse $response */ - $response = $this->client->request($facetAttributeRequest); + $categoryId = $this->helper->getTweakwiseId( + $storeId, + (int) $this->request->getParam('category_id') + ); + if ($categoryId) { + $facetAttributeRequest->setParameter('tn_cid', $categoryId); + } - // @phpstan-ignore-next-line - if (!$response) { - return $result->setData([$otherAttributeOption]); - } + /** @var FacetAttributesResponse $response */ + $response = $this->client->request($facetAttributeRequest); - foreach ($response->getAttributes() as $attribute) { - $attributes[] = [ - 'value' => $attribute['title'], - 'label' => $attribute['title'] - ]; - } + if (!$response->getAttributes()) { + return []; } - $attributes[] = $otherAttributeOption; + $attributes = []; + foreach ($response->getAttributes() as $attribute) { + $attributes[] = [ + 'value' => $attribute['title'], + 'label' => $attribute['title'] + ]; + } - $attributes = array_values(array_unique($attributes, SORT_REGULAR)); + return $attributes; + } - return $result->setData($attributes); + /** + * @param Store $store + * @return array + */ + private function executeBackendApiRequest(Store $store): array + { + return [$store->getId()]; } } From 5097332fc4d270c02aee3acf6324942580fd739a Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 27 Mar 2026 13:31:57 +0100 Subject: [PATCH 06/13] Added FacetAttributes backend API functionality --- src/ApiClient/BackendApiClient.php | 73 ++++++++++++++++++- .../Adminhtml/Ajax/FacetAttributes.php | 16 +++- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/ApiClient/BackendApiClient.php b/src/ApiClient/BackendApiClient.php index 2ec9635..5fdaa6c 100644 --- a/src/ApiClient/BackendApiClient.php +++ b/src/ApiClient/BackendApiClient.php @@ -120,7 +120,7 @@ public function getAttributes(Store $store): array ]; } - $this->cache->save($this->jsonSerializer->serialize($attributes), $cacheKey, [], self::CACHE_LIFETIME); + $this->cache->save($this->jsonSerializer->serialize($attributes), $cacheKey, [], $this->getCacheLifetime()); return $attributes; } catch (GuzzleException | Exception $e) { @@ -134,13 +134,73 @@ public function getAttributes(Store $store): array } } + /** + * @param string $attributeCode + * @param Store $store + * @return int|null + */ + public function getAttributeIdByCode(string $attributeCode, Store $store): ?int + { + $attributes = $this->getAttributes($store); + $index = array_search($attributeCode, array_column($attributes, 'value'), true); + + return $index !== false ? (int)$attributes[$index]['id'] : null; + } + + /** + * @param int $attributeId + * @param Store $store + * @return array + */ + public function getAttributeValues(int $attributeId, Store $store): array + { + $cacheKey = $this->getCacheKey('attribute_values', (int)$store->getId(), $attributeId); + if ($this->cacheExists($cacheKey)) { + return $this->getFromCache($cacheKey); + } + + $attributeValues = []; + try { + $response = $this->doRequest(sprintf('attribute/%s/values', $attributeId), $store); + $contents = $response->getBody()->getContents(); + $result = $this->jsonSerializer->unserialize($contents); + + if (!isset($result['Records'])) { + return []; + } + + foreach ($result['Records'] as $record) { + $attributeValues[] = [ + 'value' => $record['Value'], + 'label' => $record['Value'], + ]; + } + + $this->cache->save($this->jsonSerializer->serialize($attributeValues), $cacheKey, [], $this->getCacheLifetime()); + + return $attributeValues; + } catch (GuzzleException | Exception $e) { + $this->logger->critical( + 'Retrieving attribute valus from Tweakiwse Backend API failed', + [ + 'exception' => $e->getMessage() + ] + ); + return []; + } + } + /** * @param string $type * @param int $storeId + * @param int|null $attributeId * @return string */ - private function getCacheKey(string $type, int $storeId): string + private function getCacheKey(string $type, int $storeId, ?int $attributeId = null): string { + if ($attributeId) { + return sprintf('tweakwise_backend_api_result_%s_%s_%s', $type, $attributeId, $storeId); + } return sprintf('tweakwise_backend_api_result_%s_%s', $type, $storeId); } @@ -162,4 +222,13 @@ private function getFromCache(string $cacheKey): array { return (array)$this->jsonSerializer->unserialize($this->cache->load($cacheKey)); } + + /** + * Protected function added so that cache lifetime is overwritable + * @return int + */ + protected function getCacheLifetime(): int + { + return self::CACHE_LIFETIME; + } } diff --git a/src/Controller/Adminhtml/Ajax/FacetAttributes.php b/src/Controller/Adminhtml/Ajax/FacetAttributes.php index e2f504d..d0688b2 100644 --- a/src/Controller/Adminhtml/Ajax/FacetAttributes.php +++ b/src/Controller/Adminhtml/Ajax/FacetAttributes.php @@ -65,7 +65,7 @@ public function execute() /** @var Store $store */ foreach ($this->storeManager->getStores() as $store) { if ($this->isBackendApiEnabled($store)) { - $attributeValues = array_merge($this->executeBackendApiRequest($store), $attributeValues); + $attributeValues = array_merge($this->executeBackendApiRequest($store, $facetKey), $attributeValues); continue; } @@ -126,10 +126,20 @@ private function executeDefaultRequest(int $storeId, ?string $facetKey): array /** * @param Store $store + * @param string|null $facetKey * @return array */ - private function executeBackendApiRequest(Store $store): array + private function executeBackendApiRequest(Store $store, ?string $facetKey): array { - return [$store->getId()]; + if (!$facetKey) { + return []; + } + + $attributeId = $this->backendApiClient->getAttributeIdByCode($facetKey, $store); + if (!$attributeId) { + return []; + } + + return $this->backendApiClient->getAttributeValues($attributeId, $store); } } From 41e5385a75554986911788299307e49a9d99507a Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 27 Mar 2026 15:57:22 +0100 Subject: [PATCH 07/13] Added facet request classes --- .../Adminhtml/Ajax/FacetAttributes.php | 2 +- src/Controller/Adminhtml/Ajax/Facets.php | 2 +- .../Client/Request/FacetAttributeRequest.php | 33 ++++++++++ src/Model/Client/Request/FacetRequest.php | 24 +++++++ .../Response/FacetAttributesResponse.php | 62 ++++++++++++++++++ src/Model/Client/Response/FacetResponse.php | 63 +++++++++++++++++++ src/etc/adminhtml/di.xml | 21 ++++++- 7 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 src/Model/Client/Request/FacetAttributeRequest.php create mode 100644 src/Model/Client/Request/FacetRequest.php create mode 100644 src/Model/Client/Response/FacetAttributesResponse.php create mode 100644 src/Model/Client/Response/FacetResponse.php diff --git a/src/Controller/Adminhtml/Ajax/FacetAttributes.php b/src/Controller/Adminhtml/Ajax/FacetAttributes.php index d0688b2..903b178 100644 --- a/src/Controller/Adminhtml/Ajax/FacetAttributes.php +++ b/src/Controller/Adminhtml/Ajax/FacetAttributes.php @@ -14,9 +14,9 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Tweakwise\AttributeLandingTweakwise\ApiClient\BackendApiClient; +use Tweakwise\AttributeLandingTweakwise\Model\Client\Request\FacetAttributeRequest; use Tweakwise\AttributeLandingTweakwise\Model\Config; use Tweakwise\Magento2Tweakwise\Model\Client; -use Tweakwise\Magento2Tweakwise\Model\Client\Request\FacetAttributeRequest; use Tweakwise\Magento2Tweakwise\Model\Client\RequestFactory; use Tweakwise\Magento2Tweakwise\Model\Client\Response\FacetAttributesResponse; use Tweakwise\Magento2TweakwiseExport\Model\Helper; diff --git a/src/Controller/Adminhtml/Ajax/Facets.php b/src/Controller/Adminhtml/Ajax/Facets.php index 09b61b9..45feecf 100644 --- a/src/Controller/Adminhtml/Ajax/Facets.php +++ b/src/Controller/Adminhtml/Ajax/Facets.php @@ -12,10 +12,10 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Tweakwise\AttributeLandingTweakwise\ApiClient\BackendApiClient; +use Tweakwise\AttributeLandingTweakwise\Model\Client\Response\FacetResponse; use Tweakwise\AttributeLandingTweakwise\Model\Config; use Tweakwise\Magento2Tweakwise\Model\Client; use Tweakwise\Magento2Tweakwise\Model\Client\RequestFactory; -use Tweakwise\Magento2Tweakwise\Model\Client\Response\FacetResponse; use Tweakwise\Magento2TweakwiseExport\Model\Helper; class Facets extends AbstractFacetController diff --git a/src/Model/Client/Request/FacetAttributeRequest.php b/src/Model/Client/Request/FacetAttributeRequest.php new file mode 100644 index 0000000..7b05be0 --- /dev/null +++ b/src/Model/Client/Request/FacetAttributeRequest.php @@ -0,0 +1,33 @@ +setPath(sprintf('%s/%s/attributes', $this->path, $facetKey)); + } +} diff --git a/src/Model/Client/Request/FacetRequest.php b/src/Model/Client/Request/FacetRequest.php new file mode 100644 index 0000000..4b3e49d --- /dev/null +++ b/src/Model/Client/Request/FacetRequest.php @@ -0,0 +1,24 @@ +data['attributes'] ?? []; + } + + /** + * @param array $attributesData + * @return $this + */ + public function setAttributes(array $attributesData): FacetAttributesResponse + { + $attributesData = $this->normalizeArray($attributesData, 'attributes'); + + foreach ($attributesData as $attributeData) { + $attribute = $this->attributeTypeFactory->create()->setData($attributeData); + + $attributes = $attribute->getValue('attribute'); + + if (isset($attributes[0])) { + $this->data['attributes'] = $attribute->getValue('attribute'); + } else { + // Only one result + $this->data['attributes'][] = $attribute->getValue('attribute'); + } + + return $this; + } + + return $this; + } +} diff --git a/src/Model/Client/Response/FacetResponse.php b/src/Model/Client/Response/FacetResponse.php new file mode 100644 index 0000000..680069b --- /dev/null +++ b/src/Model/Client/Response/FacetResponse.php @@ -0,0 +1,63 @@ +data['facets'] ?? []; + } + + /** + * @param array $facetsData + * @return $this + */ + public function setFacets(array $facetsData): FacetResponse + { + $facetsData = $this->normalizeArray($facetsData, 'facet'); + + $facets = []; + foreach ($facetsData as $facetData) { + $facet = $this->facetTypeFactory->create()->setData($facetData); + + // Remove tree, link and slider facets + if ( + $facet->getFacetSettings()->getSelectionType() !== 'checkbox' && + $facet->getFacetSettings()->getSelectionType() !== 'color' + ) { + continue; + } + + $facets[] = $facet; + } + + $this->data['facets'] = $facets; + return $this; + } +} diff --git a/src/etc/adminhtml/di.xml b/src/etc/adminhtml/di.xml index f012830..c90cbb2 100644 --- a/src/etc/adminhtml/di.xml +++ b/src/etc/adminhtml/di.xml @@ -1,17 +1,34 @@ + + + + Tweakwise\AttributeLandingTweakwise\Model\Client\Request\FacetRequest + + + + + + + Tweakwise\AttributeLandingTweakwise\Model\Client\Request\FacetAttributeRequest + + + + - Tweakwise\Magento2Tweakwise\Model\Client\RequestFactory\FacetRequest + Tweakwise\AttributeLandingTweakwise\Model\Client\RequestFactory\FacetRequest - Tweakwise\Magento2Tweakwise\Model\Client\RequestFactory\FacetAttributeRequest + Tweakwise\AttributeLandingTweakwise\Model\Client\RequestFactory\FacetAttributeRequest From 2380c9dc54a93f15b6d0b8da6386eeb60d328f3e Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Sun, 29 Mar 2026 11:23:41 +0200 Subject: [PATCH 08/13] Updated versions of required packages --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 7f3d09c..cda0aab 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "license": "OSL-3.0", "require": { "php": "^8.1", - "emico/m2-attributelanding": ">=5.1.0", - "tweakwise/magento2-tweakwise": ">=8.4.0" + "emico/m2-attributelanding": ">=6.0.5", + "tweakwise/magento2-tweakwise": ">=9.0.0" }, "require-dev": { "emico/code-quality": "~10.6.0", From b515929fe584f50c9dd3587bc392203ca825af82 Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 3 Apr 2026 11:57:20 +0200 Subject: [PATCH 09/13] Fixed deprecated functionality --- src/Controller/Adminhtml/Ajax/AbstractFacetController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/Adminhtml/Ajax/AbstractFacetController.php b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php index abe5322..45e75f1 100644 --- a/src/Controller/Adminhtml/Ajax/AbstractFacetController.php +++ b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php @@ -32,7 +32,7 @@ public function __construct( * @return bool * @throws LocalizedException */ - protected function isBackendApiEnabled(Store $store = null): bool + protected function isBackendApiEnabled(?Store $store = null): bool { return (bool)$this->config->getBackendApiToken($store); } From d96391c8dcfdd5c143822638776a0dda6cc21031 Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 3 Apr 2026 12:55:19 +0200 Subject: [PATCH 10/13] Set other attribute and other attribute value as attribute and value --- .../Ajax/AbstractFacetController.php | 2 +- src/Plugin/Model/LandingPagePlugin.php | 31 +++++++++++++++++++ src/etc/di.xml | 5 +++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/Plugin/Model/LandingPagePlugin.php diff --git a/src/Controller/Adminhtml/Ajax/AbstractFacetController.php b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php index 45e75f1..79f6020 100644 --- a/src/Controller/Adminhtml/Ajax/AbstractFacetController.php +++ b/src/Controller/Adminhtml/Ajax/AbstractFacetController.php @@ -13,7 +13,7 @@ abstract class AbstractFacetController implements HttpPostActionInterface { - protected const OTHER_ATTRIBUTE_VALUE = 'tw_other'; + public const OTHER_ATTRIBUTE_VALUE = 'tw_other'; /** * @param Config $config diff --git a/src/Plugin/Model/LandingPagePlugin.php b/src/Plugin/Model/LandingPagePlugin.php new file mode 100644 index 0000000..c34ceb9 --- /dev/null +++ b/src/Plugin/Model/LandingPagePlugin.php @@ -0,0 +1,31 @@ + $filterAttribute) { + if ($filterAttribute['attribute'] !== AbstractFacetController::OTHER_ATTRIBUTE_VALUE) { + continue; + } + + $result[$key]['attribute'] = $filterAttribute['attribute_other']; + $result[$key]['value'] = $filterAttribute['attribute_value_other']; + } + + return $result; + } +} diff --git a/src/etc/di.xml b/src/etc/di.xml index ac7fa49..ea5d727 100644 --- a/src/etc/di.xml +++ b/src/etc/di.xml @@ -68,4 +68,9 @@ + + + + From af1284803bb2b595012aaabd830cc9d0260fb8a3 Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 3 Apr 2026 14:17:54 +0200 Subject: [PATCH 11/13] Other field instead of random first option when option not exists anymore --- src/Plugin/Model/LandingPagePlugin.php | 2 +- .../web/js/form/element/filter-attribute-value.js | 10 +++++----- .../adminhtml/web/js/form/element/filter-attribute.js | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Plugin/Model/LandingPagePlugin.php b/src/Plugin/Model/LandingPagePlugin.php index c34ceb9..995183a 100644 --- a/src/Plugin/Model/LandingPagePlugin.php +++ b/src/Plugin/Model/LandingPagePlugin.php @@ -15,7 +15,7 @@ class LandingPagePlugin * @param array $result * @return array */ - public function afterGetUnserializedFilterAttributes(LandingPage $subject, array $result): array + public function afterGetFrontendFilterAttributes(LandingPage $subject, array $result): array { foreach ($result as $key => $filterAttribute) { if ($filterAttribute['attribute'] !== AbstractFacetController::OTHER_ATTRIBUTE_VALUE) { diff --git a/src/view/adminhtml/web/js/form/element/filter-attribute-value.js b/src/view/adminhtml/web/js/form/element/filter-attribute-value.js index 6ee974f..f685c2d 100644 --- a/src/view/adminhtml/web/js/form/element/filter-attribute-value.js +++ b/src/view/adminhtml/web/js/form/element/filter-attribute-value.js @@ -34,12 +34,14 @@ define([ }); }, - setOtherFieldVisibility: function (selectedAttributeValue) { + setOtherFieldVisibility: function (selectedAttributeValue, otherFieldValue = null) { registry.get(`${this.parentName}.${this.otherFieldName}`, function (otherField) { const otherFieldVisible = selectedAttributeValue === this.otherValue otherField.disabled(!otherFieldVisible); if (selectedAttributeValue && !otherFieldVisible) { otherField.value(''); + } else if (otherFieldValue) { + otherField.value(otherFieldValue); } }.bind(this)); }, @@ -52,10 +54,8 @@ define([ if (optionExists) { this.value(valueToRestore); } else { - const firstOption = this.options()[0]; - if(firstOption) { - this.value(firstOption.value); - } + this.value(this.otherValue); + this.setOtherFieldVisibility(this.otherValue, valueToRestore); } }, diff --git a/src/view/adminhtml/web/js/form/element/filter-attribute.js b/src/view/adminhtml/web/js/form/element/filter-attribute.js index 56bf1bf..830a7d1 100644 --- a/src/view/adminhtml/web/js/form/element/filter-attribute.js +++ b/src/view/adminhtml/web/js/form/element/filter-attribute.js @@ -67,12 +67,14 @@ define([ }); }, - setOtherFieldVisibility: function (selectedAttribute) { + setOtherFieldVisibility: function (selectedAttribute, otherFieldValue = null) { registry.get(`${this.parentName}.${this.otherFieldName}`, function (otherField) { const otherFieldVisible = selectedAttribute === this.otherValue otherField.disabled(!otherFieldVisible); if (!otherFieldVisible) { otherField.value(''); + } else if (otherFieldValue) { + otherField.value(otherFieldValue); } }.bind(this)); }, @@ -89,10 +91,8 @@ define([ if (optionExists) { this.value(this.savedValue); } else { - const firstOption = this.options()[0]; - if (firstOption) { - this.value(firstOption.value); - } + this.value(this.otherValue); + this.setOtherFieldVisibility(this.otherValue, this.savedValue); } }, From 556787ec5a5a0b55e4c93a84a9b0d2e286f547b1 Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 10 Apr 2026 08:41:43 +0200 Subject: [PATCH 12/13] Use break instead of return $this for clarity --- src/Model/Client/Response/FacetAttributesResponse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Client/Response/FacetAttributesResponse.php b/src/Model/Client/Response/FacetAttributesResponse.php index bd1c7e4..a89159b 100644 --- a/src/Model/Client/Response/FacetAttributesResponse.php +++ b/src/Model/Client/Response/FacetAttributesResponse.php @@ -54,7 +54,7 @@ public function setAttributes(array $attributesData): FacetAttributesResponse $this->data['attributes'][] = $attribute->getValue('attribute'); } - return $this; + break; } return $this; From 701b303b68706a05f7a1968eb79f7e57fbc9384e Mon Sep 17 00:00:00 2001 From: tjeujansen Date: Fri, 10 Apr 2026 08:54:29 +0200 Subject: [PATCH 13/13] Revert required version changes --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index cda0aab..7f3d09c 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "license": "OSL-3.0", "require": { "php": "^8.1", - "emico/m2-attributelanding": ">=6.0.5", - "tweakwise/magento2-tweakwise": ">=9.0.0" + "emico/m2-attributelanding": ">=5.1.0", + "tweakwise/magento2-tweakwise": ">=8.4.0" }, "require-dev": { "emico/code-quality": "~10.6.0",