Попытка сделать "самопополняющийся" listbox-multiple с migx-записями

Для начала опишу задачу:
0. MODX Revo 2.8.8, Migx 3.0.2-beta1
1. Есть определённый набор сущностей: name+description (migx-конфигурация symptoms_config)
2. Хочу создать список множественного выбора с возможностью его пополнения через кнопку и всплывающую форму

Предпринятые шаги:

1. Создал TV symptoms_selector — список, множественный выбор
Возможные значения:
@EVAL return $modx->runSnippet('getSymptomsList');

2. Создал сниппет getSymptomsList для связи «Название-Id»
$query = $modx->newQuery('migxConfigElement');
$query->where(array('config_id' => 16)); // ID конфигурации symptoms_config
$symptoms = $modx->getCollection('migxConfigElement', $query);

$output = array();
foreach ($symptoms as $symptom) {
    $fields = $modx->fromJSON($symptom->get('fields'));
    $output[] = $fields['name'] . '==' . $symptom->get('id');
}

return implode('||', $output);

3. Создал дополнительный плагин на onDocFormRender

Он добавляет кнопку рядом с этим мультиселектом и вызывает форму заполнения новой сущности
Всё в порядке — форма открывается, поля на месте.

Но сохраняться эта штука никуда не хочет:)))

Ниже полный код этого плагина (на всякий случай). Загвоздка именно в том, как через Migx-коннектор/Migx-процессор сохранить новое значение и автоматически подставить его в выбранные… Ну и чтобы для следующих страниц была возможность выбрать его.

<?php
if ($modx->event->name == 'OnDocFormRender') {
    $resourceId = isset($_REQUEST['id']) ? (int)$_REQUEST['id'] : 0;
    
    if ($resourceId > 0) {
    	
        $resource = $modx->getObject('modResource', $resourceId);
        $resourceTmplId = $resource->get('template');
                
        if ($resource && $resourceTmplId > 0) {
        	$template = $modx->getObject('modTemplate', $resourceTmplId);
            
            $symptomTV = $modx->getObject('modTemplateVar', ['name' => 'symptoms_selector']);
            
            if ($symptomTV) {
            	$tvId = $symptomTV->get('id');
            	
                $tvTemplate = $modx->getObject('modTemplateVarTemplate', [
                    'tmplvarid' => $tvId,
                    'templateid' => $resourceTmplId
                ]);
                
                if ($tvTemplate) {
    				
                	$customScript = "<script>Ext.onReady(function() {
    // Ждём полной загрузки формы
    setTimeout(function() {
        
        const tvInput = document.querySelector('input[id=\"tvdef{$tvId}\"]');
        
        if (tvInput) {
            var tvContainer = tvInput.closest('.x-form-item');
            if (tvContainer) {
                // Добавляем кнопку после контейнера TV
                var buttonHtml = '<div style=\"margin-top: 10px; margin-bottom: 15px;\"><button type=\"button\" onclick=\"addNewSymptomTest()\" class=\"x-btn x-btn-default-small\">+ Добавить новый симптом</button></div>';
                tvContainer.insertAdjacentHTML('afterend', buttonHtml);
            }
        }
    }, 500);
    
    // Функция для добавления нового симптома
    window.addNewSymptom = function() {
    		MODx.load({
	        xtype: 'modx-window',
	        title: 'Добавить новый симптом',
	        width: 600,
	        height: 400,
	        closeAction: 'close',
	        layout: 'fit', // Добавляем layout
	        items: [{
	            xtype: 'form',
	            id: 'symptom-add-form',
	            layout: 'form', // Меняем на 'form' layout
	            labelWidth: 120,
	            bodyStyle: 'padding: 15px;',
	            defaults: {
	                anchor: '100%',
	                msgTarget: 'under'
	            },
	            items: [{
	                xtype: 'textfield',
	                fieldLabel: 'Название',
	                name: 'name',
	                allowBlank: false,
	                width: '100%'
	            }, {
	                xtype: 'textarea',
	                fieldLabel: 'Описание',
	                name: 'description',
	                height: 200,
	                width: '100%',
	                grow: true
	            }]
	        }],
	        buttons: [{
	            text: 'Отмена',
	            handler: function() {
	                this.ownerCt.ownerCt.close();
	            }
	        }, {
	            text: 'Сохранить',
	            cls: 'primary-button',
	            handler: function() {
				    const form = Ext.getCmp('symptom-add-form');
				    
				    if (form && form.getForm().isValid()) {
				        const values = form.getForm().getValues();
				        
				        Ext.Ajax.request({
				            url: MODx.config.connector_url,
				            method: 'POST',
				            params: {
				                action: 'mgr/migx/create',
				                configs: 'symptoms_config',
				                data: Ext.encode([values])
				            },
				            success: function(response) {
				                var result = Ext.decode(response.responseText);
				                if (result.success) {
				                    MODx.msg.alert('Успех', 'Симптом добавлен!');
				                    this.ownerCt.ownerCt.close();
				                    setTimeout(function() {
				                        window.location.reload();
				                    }, 1000);
				                } else {
				                    MODx.msg.alert('Ошибка', result.message || 'Ошибка при сохранении');
				                }
				            },
				            failure: function(response) {
				                MODx.msg.alert('Ошибка', 'Ошибка при сохранении симптома');
				            },
				            scope: this
				        });
				    }
				}
	        }]
	    }).show();
    };
                    $modx->regClientStartupScript($customScript);
                }
            }
        }
    }
}
Плагин пока что работает только для уже созданных ресурсов, к шаблонам которых уже прикреплено данное ТВ.

Помогите, пожалуйста, «добить» уже этот функционал (Полтора дня сижу и ломаю голову и всё вокруг)… ну или предложить достойную альтернативу подобному самопополняющемуся справочнику.
Бонусный вопрос: как это дело выводить? По идее просто с помощью getImageList можно?

*UPD
url: MODx.config.connector_url,
— сравнил в исходном коде с другими migx tv и там указан путь '/assets/components/migx/connector.php'.
Я попробовал и его, но всё равно не работает. Предполагаю, что причина в том, что нет такого процессора 'create'.
но я не понимаю, какой процессор использовать, чтобы создать сущность
Евгений
14 октября 2025, 17:05
modx.pro
226
0

Комментарии: 4

ВитОс
15 октября 2025, 02:38
0
Зачем вы опять усложняете. Где вы хотите брать список (где и как он должен обновляться)?
    Евгений
    15 октября 2025, 22:19
    0
    Уважаемый @ВитОс я как раз не хочу усложнять.
    Задача казалась простой: сделать аналог мультивыбора: туда можно вбить значение, которого нет в предустановленном списке, и далее оно будет показываться.
    Здесь, вместо простого строкового значения я хочу сохранять пару (название+текст) и в дальнейшем иметь возможность выбрать только что добавленный вариант. Путём некоторых размышлений пришёл к такому варианту. Храниться этот список думал в modx_migx_configelements

    Если есть вариант проще и нативнее, то я Вас с удовольствием почитаю! подскажете?
      ВитОс
      16 октября 2025, 00:17
      0
      мне кажется если не много значений будет, то можно использовать обычный migx без кастомных баз
      вот пример
      самый простой вариант
      создать migx color
      {
        "formtabs":[
          {
            "MIGX_id":1,
            "caption":"",
            "print_before_tabs":"0",
            "fields":[
              {
                "MIGX_id":1,
                "field":"name",
                "caption":"\u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435",
                "description":"",
                "description_is_code":"0",
                "inputTV":"",
                "inputTVtype":"",
                "validation":"",
                "configs":"",
                "restrictive_condition":"",
                "display":"",
                "sourceFrom":"config",
                "sources":"",
                "inputOptionValues":"",
                "default":"",
                "useDefaultIfEmpty":"0",
                "pos":1
              },
              {
                "MIGX_id":2,
                "field":"number",
                "caption":"\u041d\u043e\u043c\u0435\u0440",
                "description":"",
                "description_is_code":"0",
                "inputTV":"",
                "inputTVtype":"number",
                "validation":"",
                "configs":"",
                "restrictive_condition":"",
                "display":"",
                "sourceFrom":"config",
                "sources":"",
                "inputOptionValues":"",
                "default":"",
                "useDefaultIfEmpty":"0",
                "pos":2
              }
            ],
            "pos":1
          }
        ],
        "contextmenus":"",
        "actionbuttons":"",
        "columnbuttons":"",
        "filters":"",
        "extended":{
          "migx_add":"",
          "disable_add_item":"",
          "add_items_directly":"",
          "formcaption":"",
          "update_win_title":"",
          "win_id":"",
          "maxRecords":"",
          "addNewItemAt":"bottom",
          "media_source_id":"",
          "multiple_formtabs":"",
          "multiple_formtabs_label":"",
          "multiple_formtabs_field":"",
          "multiple_formtabs_optionstext":"",
          "multiple_formtabs_optionsvalue":"",
          "actionbuttonsperrow":4,
          "winbuttonslist":"",
          "extrahandlers":"",
          "filtersperrow":4,
          "packageName":"",
          "classname":"",
          "task":"",
          "getlistsort":"",
          "getlistsortdir":"",
          "sortconfig":"",
          "gridpagesize":"",
          "use_custom_prefix":"0",
          "prefix":"",
          "grid":"",
          "gridload_mode":1,
          "check_resid":1,
          "check_resid_TV":"",
          "join_alias":"",
          "has_jointable":"yes",
          "getlistwhere":"",
          "joins":"",
          "hooksnippets":"",
          "cmpmaincaption":"",
          "cmptabcaption":"",
          "cmptabdescription":"",
          "cmptabcontroller":"",
          "winbuttons":"",
          "onsubmitsuccess":"",
          "submitparams":""
        },
        "permissions":{
          "apiaccess":"",
          "view":"",
          "list":"",
          "save":"",
          "create":"",
          "remove":"",
          "delete":"",
          "publish":"",
          "unpublish":"",
          "viewdeleted":"",
          "viewunpublished":""
        },
        "fieldpermissions":"",
        "columns":[
          {
            "MIGX_id":1,
            "header":"\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0446\u0432\u0435\u0442\u0430",
            "dataIndex":"name",
            "width":"",
            "sortable":"false",
            "show_in_grid":1,
            "customrenderer":"",
            "renderer":"",
            "clickaction":"",
            "selectorconfig":"",
            "renderchunktpl":"",
            "renderoptions":"",
            "editor":""
          }
        ],
        "category":""
      }
      его подключаем например на tv_color
      Затем создаем migx new
      {
        "formtabs":[
          {
            "MIGX_id":3,
            "caption":"",
            "print_before_tabs":"0",
            "fields":[
              {
                "MIGX_id":4,
                "field":"selected_item",
                "caption":"\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442",
                "description":"\u0412\u044b\u0431\u043e\u0440 \u0438\u0437 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439",
                "description_is_code":"0",
                "inputTV":"",
                "inputTVtype":"listbox",
                "validation":"",
                "configs":"",
                "restrictive_condition":"",
                "display":"",
                "sourceFrom":"config",
                "sources":"",
                "inputOptionValues":"@SNIPPET getTestItems",
                "default":"",
                "useDefaultIfEmpty":"0",
                "pos":1
              },
              {
                "MIGX_id":5,
                "field":"custom_note",
                "caption":"\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0437\u0430\u043c\u0435\u0442\u043a\u0430",
                "description":"\u041b\u044e\u0431\u043e\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u043e\u043b\u0435",
                "description_is_code":"0",
                "inputTV":"",
                "inputTVtype":"",
                "validation":"",
                "configs":"",
                "restrictive_condition":"",
                "display":"",
                "sourceFrom":"config",
                "sources":"",
                "inputOptionValues":"",
                "default":"",
                "useDefaultIfEmpty":"0",
                "pos":2
              }
            ],
            "pos":1
          }
        ],
        "contextmenus":"",
        "actionbuttons":"",
        "columnbuttons":"",
        "filters":"",
        "extended":{
          "migx_add":"",
          "disable_add_item":"",
          "add_items_directly":"",
          "formcaption":"",
          "update_win_title":"",
          "win_id":"",
          "maxRecords":"",
          "addNewItemAt":"bottom",
          "media_source_id":"",
          "multiple_formtabs":"",
          "multiple_formtabs_label":"",
          "multiple_formtabs_field":"",
          "multiple_formtabs_optionstext":"",
          "multiple_formtabs_optionsvalue":"",
          "actionbuttonsperrow":4,
          "winbuttonslist":"",
          "extrahandlers":"",
          "filtersperrow":4,
          "packageName":"",
          "classname":"",
          "task":"",
          "getlistsort":"",
          "getlistsortdir":"",
          "sortconfig":"",
          "gridpagesize":"",
          "use_custom_prefix":"0",
          "prefix":"",
          "grid":"",
          "gridload_mode":1,
          "check_resid":1,
          "check_resid_TV":"",
          "join_alias":"",
          "has_jointable":"yes",
          "getlistwhere":"",
          "joins":"",
          "hooksnippets":"",
          "cmpmaincaption":"",
          "cmptabcaption":"",
          "cmptabdescription":"",
          "cmptabcontroller":"",
          "winbuttons":"",
          "onsubmitsuccess":"",
          "submitparams":""
        },
        "permissions":{
          "apiaccess":"",
          "view":"",
          "list":"",
          "save":"",
          "create":"",
          "remove":"",
          "delete":"",
          "publish":"",
          "unpublish":"",
          "viewdeleted":"",
          "viewunpublished":""
        },
        "fieldpermissions":"",
        "columns":[
          {
            "MIGX_id":1,
            "header":"\u0437\u0430\u043c\u0435\u0442\u043a\u0430",
            "dataIndex":"custom_note",
            "width":"",
            "sortable":"false",
            "show_in_grid":1,
            "customrenderer":"",
            "renderer":"",
            "clickaction":"",
            "selectorconfig":"",
            "renderchunktpl":"",
            "renderoptions":"",
            "editor":""
          },
          {
            "MIGX_id":2,
            "header":"\u0426\u0432\u0435\u0442",
            "dataIndex":"selected_item",
            "width":"",
            "sortable":"false",
            "show_in_grid":1,
            "customrenderer":"",
            "renderer":"",
            "clickaction":"",
            "selectorconfig":"",
            "renderchunktpl":"",
            "renderoptions":"",
            "editor":""
          }
        ],
        "category":""
      }
      тут в поле selected_item в Input Option Values указан спиппет
      создаем его getTestItems
      $output = [];
      
      // Получаем TV с первым MIGX где цвет
      $tv = $modx->getObject('modTemplateVar', array('name' => 'tv_color'));
      if ($tv) {
          $value = $tv->getValue($modx->resource->get('id'));
          
          if (!empty($value)) {
              $items = $modx->fromJSON($value);
              
              if (is_array($items)) {
                  foreach ($items as $item) {
                      if (!empty($item['name'])) {
                          $output[] = $item['name'] . '==' . $item['number'];
                      }
                  }
              }
          }
      }
      
      return implode('||', $output);
      вот что получается




      если их будет очень много можно вместо сниппета указать запрос к bd например
      @SELECT `name`,`number` FROM `[[+PREFIX]]migx_color`
      Если правильно вас понял, то это то что вам нужно
        Евгений
        16 октября 2025, 10:42
        0
        @ВитОс то есть, Ваше предложение — создать одно tv для хранения, а второе для выбора из первого?
        Если так, то да, согласен. Я сейчас как раз «откатился» к тому, что создал отдельную техническую страницу, завел там tv для хранения списка значений и «подсасываю» значения в другую тв-шку.
        Но я чот прям хочу заморочиться на красоте решения:) И, как будто бы, всё получилось, кроме последнего, самого важного шага — сохранения migx значения путём обращения к migx через js =)

        *не могу с уверенностью сказать, сколько значений там будет, так как это полностью будет зависеть от фантазии заказчика, а она у него богатая:)
    Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
    4