├── 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 | 
60 |
61 | ### Product Form - Multiselect
62 |
63 | 
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 |
25 |
--------------------------------------------------------------------------------
/view/adminhtml/ui_component/category_form.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
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 |
--------------------------------------------------------------------------------