├── README.md ├── Scope └── Config.php ├── Ui └── DataProvider │ └── Product │ └── Form │ └── Modifier │ └── SelectToUiSelect.php ├── composer.json ├── etc ├── adminhtml │ ├── di.xml │ └── system.xml ├── config.xml └── module.xml ├── registration.php └── view ├── adminhtml └── ui_component │ ├── catalog_staging_category_update_form.xml │ └── category_form.xml └── base └── web └── js └── form └── element └── ui-select.js /README.md: -------------------------------------------------------------------------------- 1 | # Aimes_ImprovedAdminUi 2 | !["Supported Magento Version"][magento-badge] !["Supported Adobe Commerce Version"][commerce-badge] !["Latest Release"][release-badge] 3 | 4 | * Compatible with _Magento Open Source_ and _Adobe Commerce_ `2.4.x` 5 | 6 | ## Features 7 | * Use a slightly modified [UI-select][ui-select-docs] component to replace standard `select` and `multiselect` components 8 | * Provides a search field for option models that have a lot of options (E.g. CMS Blocks) 9 | * Dynamically use ui-select components, replacing `select` and `multiselect` components, in the product edit form 10 | * Configured to perform this action only when a certain number of options are shown 11 | * Support for the default category attribute `landing_page` on the category form 12 | * Any custom form can be modified, see [Usage](#statically-declared-ui-components) 13 | 14 | ## Requirements 15 | * Magento Open Source or Adobe Commerce version `2.4.x` 16 | 17 | ## Installation 18 | Please install this module via Composer. This module is hosted on [Packagist][packagist]. 19 | 20 | * `composer require aimes/magento2-improved-admin-ui` 21 | * `bin/magento module:enable Aimes_ImprovedAdminUi` 22 | * `bin/magento setup:upgrade` 23 | 24 | ## Usage 25 | 26 | ### Dynamic Replacement 27 | System configuration is provided to set the minimum amount of options required before the component is rendered as a ui-select. By default, this value is set to `20`. 28 | 29 | `Stores -> Configuration -> Catalog -> Catalog -> Admin UI` 30 | 31 | ### Statically Declared UI Components 32 | 33 | Not every form has a pool of modifiers, most are statically declared. Since modifying attributes within these forms generally requires a new ui_component file, customisation to additional attributes can be done there. For example: 34 | 35 | `view/adminhtml/category_form.xml` 36 | ```xml 37 | 38 | 41 | 42 | 43 | false 44 | 45 | 46 | 47 | ui/grid/filters/elements/ui-select 48 | 49 | 50 | 51 | ``` 52 | 53 | This should be merged with any other desired/required settings. Settings can be found on the default [ui-select component documentation][ui-select-docs]. 54 | 55 | ## Preview 56 | 57 | ### Product Form - Select 58 | 59 | ![image](https://github.com/user-attachments/assets/96a6070c-0267-4d9d-93ea-09912f529b2c) 60 | 61 | ### Product Form - Multiselect 62 | 63 | ![image](https://github.com/user-attachments/assets/46128ea7-e966-45bf-9948-1f61b698e41a) 64 | 65 | 66 | ## Licence 67 | [GPLv3][gpl] © [Rob Aimes][author] 68 | 69 | [magento-badge]:https://img.shields.io/badge/Magento-2.4.x-orange.svg?logo=Magento&style=for-the-badge 70 | [commerce-badge]:https://img.shields.io/badge/Adobe%20Commerce-2.4.x-red.svg?logo=Adobe&style=for-the-badge 71 | [release-badge]:https://img.shields.io/github/v/release/robaimes/magento2-improved-admin-ui?style=for-the-badge 72 | [packagist]:https://packagist.org/packages/aimes/magento2-improved-admin-ui 73 | [gpl]:https://www.gnu.org/licenses/gpl-3.0.en.html 74 | [author]:https://aimes.dev/ 75 | [ui-select-docs]:https://developer.adobe.com/commerce/frontend-core/ui-components/components/secondary-ui-select/ 76 | -------------------------------------------------------------------------------- /Scope/Config.php: -------------------------------------------------------------------------------- 1 | scopeConfig->getValue(self::XML_PATH_UI_SELECT_MIN_OPTIONS_AMOUNT); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Ui/DataProvider/Product/Form/Modifier/SelectToUiSelect.php: -------------------------------------------------------------------------------- 1 | arrayManager->findPaths('config', $meta); 46 | 47 | foreach ($componentConfigPaths as $componentConfigPath) { 48 | $componentConfig = $this->arrayManager->get($componentConfigPath, $meta); 49 | 50 | if (!$componentConfig || !$this->shouldProcessComponent($componentConfig)) { 51 | continue; 52 | } 53 | 54 | $isMultiple = $componentConfig['formElement'] === MultiSelect::NAME; 55 | 56 | if ($isMultiple) { 57 | $this->recordMultiSelectAttribute($componentConfigPath); 58 | } 59 | 60 | $meta = $this->arrayManager->merge( 61 | $componentConfigPath, 62 | $meta, 63 | $this->getUiSelectConfig($isMultiple) 64 | ); 65 | } 66 | 67 | return $meta; 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public function modifyData(array $data): array 74 | { 75 | $productId = $this->locator->getProduct()->getId(); 76 | 77 | if (!$productId) { 78 | return $data; 79 | } 80 | 81 | foreach ($this->multiSelectAttributes as $multiSelectAttribute) { 82 | if (!isset($data[$productId]['product'][$multiSelectAttribute])) { 83 | continue; 84 | } 85 | 86 | $initialValue = $data[$productId]['product'][$multiSelectAttribute]; 87 | $data[$productId]['product'][$multiSelectAttribute] = explode(',', $initialValue); 88 | } 89 | 90 | return $data; 91 | } 92 | 93 | /** 94 | * @param array $componentConfig 95 | * @return bool 96 | */ 97 | private function shouldProcessComponent(array $componentConfig): bool { 98 | $formElement = $componentConfig['formElement'] ?? null; 99 | 100 | if ($formElement !== Select::NAME && $formElement !== MultiSelect::NAME) { 101 | return false; 102 | } 103 | 104 | if (isset($componentConfig['component'])) { 105 | return false; 106 | } 107 | 108 | if (count($componentConfig['options'] ?? []) < $this->uiConfig->getUISelectMinOptionsAmount()) { 109 | return false; 110 | } 111 | 112 | return true; 113 | } 114 | 115 | /** 116 | * @return array[] 117 | */ 118 | private function getUiSelectConfig(bool $isMultiple): array { 119 | return [ 120 | 'component' => 'Aimes_ImprovedAdminUi/js/form/element/ui-select', 121 | 'componentType' => Field::NAME, 122 | 'dataType' => 'text', 123 | 'filterPlaceholder' => __('Search...'), 124 | 'missingValuePlaceholder' => __('Selected value/identifier "%s" doesn\'t exist'), 125 | 'multiple' => $isMultiple, 126 | 'elementTmpl' => 'ui/grid/filters/elements/ui-select', 127 | ]; 128 | } 129 | 130 | /** 131 | * @param string $componentConfigPath 132 | * @return void 133 | */ 134 | private function recordMultiSelectAttribute(string $componentConfigPath): void 135 | { 136 | $componentPath = $this->arrayManager->slicePath($componentConfigPath, 0, -3); 137 | $finalDelimiterPosition = strrpos($componentPath, '/'); 138 | $attributeCode = substr($componentPath, $finalDelimiterPosition + 1); 139 | 140 | $this->multiSelectAttributes[] = $attributeCode; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aimes/magento2-improved-admin-ui", 3 | "type": "magento2-module", 4 | "description": "Improved UI for certain aspects of the admin area", 5 | "license": [ 6 | "GPL-3.0-or-later" 7 | ], 8 | "authors": [ 9 | { 10 | "name": "Rob Aimes", 11 | "email": "rob@aimes.dev", 12 | "homepage": "https://aimes.dev" 13 | } 14 | ], 15 | "require": { 16 | "magento/module-ui": "^101.2", 17 | "php": "~8.2 | ~8.3" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Aimes\\ImprovedAdminUi\\": "" 22 | }, 23 | "files": [ 24 | "registration.php" 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /etc/adminhtml/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 11 | 12 | 13 | 14 | Aimes\ImprovedAdminUi\Ui\DataProvider\Product\Form\Modifier\SelectToUiSelect 15 | 139 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 11 |
12 | 18 | 19 | 27 | 28 | When this amount of options or greater are available, replace 'select' components with 'ui-select'. 29 | required-entry validate-digits validate-zero-or-greater 30 | 31 | 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 11 | 12 | 13 | 20 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 8 |
10 |
11 | 14 | 15 | 16 | false 17 | 18 | 19 | 20 | ui/grid/filters/elements/ui-select 21 | 22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /view/adminhtml/ui_component/category_form.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 |
10 |
11 | 14 | 15 | 16 | false 17 | 18 | 19 | 20 | ui/grid/filters/elements/ui-select 21 | 22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /view/base/web/js/form/element/ui-select.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © Rob Aimes - https://aimes.dev/ 3 | * https://github.com/robaimes 4 | */ 5 | define([ 6 | 'Magento_Ui/js/form/element/ui-select', 7 | 'underscore', 8 | ], function (UiSelect, _) { 9 | 'use strict'; 10 | 11 | /** 12 | * A slightly modified version of the default ui-select component 13 | * 14 | * Ensures an empty string is set as the value, where necessary, so that it posts in the form data. 15 | * 16 | * The default component uses an array for storing data, which is fine. However, when a field is not required, when 17 | * no options are selected this does not get put in the save form's POST request data. In cases where values were 18 | * set and were attempted to be cleared, the absence of the data in the form would cause them to not be updated. 19 | * 20 | * This is relatively hacky by causing the value observable to swap between a string and array type. 21 | * 22 | * Saying that, we probably wouldn't need all this if there weren't numerous methods within this component that 23 | * arbitrarily mutate the value observable. 24 | */ 25 | return UiSelect.extend({ 26 | defaults: { 27 | presets: { 28 | single: { 29 | chipsEnabled: true, 30 | closeBtn: true, 31 | }, 32 | }, 33 | disableLabel: true, 34 | filterOptions: true, 35 | }, 36 | 37 | /** 38 | * Toggle activity list element 39 | * 40 | * @note Modification allows for de-selecting a value if in single mode. 41 | * @note Ensure empty string is set as the value, where necessary, so that it posts in the form data. 42 | * 43 | * @param {Object} data - selected option data 44 | * @returns {Object} Chainable 45 | */ 46 | toggleOptionSelected: function (data) { 47 | var isSelected = this.isSelected(data.value); 48 | 49 | if (this.lastSelectable && data.hasOwnProperty(this.separator)) { 50 | return this; 51 | } 52 | 53 | if (!this.multiple) { 54 | if (!isSelected) { 55 | this.value(data.value); 56 | } else { 57 | this.clear(); 58 | } 59 | this.listVisible(false); 60 | } else { 61 | if (!isSelected) { /*eslint no-lonely-if: 0*/ 62 | if (this.value() === '') { 63 | this.value([]); 64 | } 65 | 66 | this.value.push(data.value); 67 | } else { 68 | this.value(_.without(this.value(), data.value)); 69 | } 70 | } 71 | 72 | return this; 73 | }, 74 | 75 | setCaption: function () { 76 | var length, caption = ''; 77 | 78 | if (!_.isArray(this.value()) && this.value()) { 79 | length = 1; 80 | } else if (this.value()) { 81 | length = this.value().length; 82 | } else { 83 | this.multiple ? this.value([]) : this.clear(); 84 | length = 0; 85 | } 86 | this.warn(caption); 87 | 88 | //check if option was removed 89 | if (this.isDisplayMissingValuePlaceholder && length && !this.getSelected().length) { 90 | caption = this.missingValuePlaceholder.replace('%s', this.value()); 91 | this.placeholder(caption); 92 | this.warn(caption); 93 | 94 | return this.placeholder(); 95 | } 96 | 97 | if (length > 1) { 98 | this.placeholder(length + ' ' + this.selectedPlaceholders.lotPlaceholders); 99 | } else if (length && this.getSelected().length) { 100 | this.placeholder(this.getSelected()[0].label); 101 | } else { 102 | this.placeholder(this.selectedPlaceholders.defaultPlaceholder); 103 | } 104 | 105 | return this.placeholder(); 106 | }, 107 | 108 | removeSelected: function (value, data, event) { 109 | event ? event.stopPropagation() : false; 110 | 111 | if (this.multiple) { 112 | this.value().length > 1 ? this.value.remove(value) : this.clear(); 113 | } else { 114 | this.clear(); 115 | } 116 | }, 117 | 118 | hasData: function () { 119 | if (!this.value()) { 120 | this.clear(); 121 | } 122 | 123 | return this.value() ? !!this.value().length : false; 124 | }, 125 | 126 | getSelected: function () { 127 | if (!this.value()) { 128 | return []; 129 | } 130 | 131 | return this._super(); 132 | }, 133 | }); 134 | }); 135 | --------------------------------------------------------------------------------