[ExtJS] Расширяем компонент Collections




Часто ли вам приходится расширять какие-то стандартные штуки в MODX или в компонентах для него? Мне вот часто! Поэтому, давно хотелось поделиться чем-нибудь интересным на этот счёт, что я собственно и сделал в статье Дополнительные поля профиля юзера, где описал, как можно расширить профиль юзера, чтобы всё выглядело натурально. На этот раз расскажу, как можно расширять компонент Collections, не прибегая к крайним мерам, вроде правки исходников приложения.

  1. Устанавливаем MODX, пакет Collections.

  2. Создаём ТВ поле, например status, с типом Список (одиночный выбор).
    Привязываем к шаблону, который будем назначать записям, находящимся внутри ресурса-коллекции.
    В поле Возможные значения пишем:
    Не оптимизирован==1||Оптимизирован==2||Не требует оптимизации==3

  3. Заходим на страницу пакета Collections в бек-энде по пути Приложения > Виды коллекции. Здесь кликаем на имеющемся по-умолчанию пункте правой мышью и жмём Копировать.

  4. Сейчас, пожалуй, начинается самое интересное!
    Качаем заготовку для создания пакетов — modExtra, из GitHub репозитория Василия Наумкина, заливаем в корень сайта, распаковываем. Сразу переименуем название компонента с modExtra на, например, extCollections — заходим по адресу _http://вашсайт/modExtra-master/rename_it.php?name=extCollections

  5. Теперь нам нужно настроить заготовку под наши нужды:

    1. Удаляем папки и файлы:
      /extCollections/assets/components/extcollections/js/mgr/sections/
      /extCollections/assets/components/extcollections/js/mgr/widgets/
      /extCollections/assets/components/extcollections/js/mgr/misc/
      /extCollections/assets/components/extcollections/css/
      /extCollections/core/components/extcollections/controllers/
      /extCollections/core/components/extcollections/elements/chunks/
      /extCollections/core/components/extcollections/elements/snippets/
      /extCollections/core/components/extcollections/elements/templates/
      /extCollections/core/components/extcollections/model/schema/
      /extCollections/core/components/extcollections/model/extcollections/mysql/
      /extCollections/core/components/extcollections/model/extcollections/extcollectionsitem.class.php
      /extCollections/core/components/extcollections/model/extcollections/metadata.mysql.php
      /extCollections/core/components/extcollections/processors/mgr/item/
      /extCollections/_build/properties/
      /extCollections/_build/resolvers/

    2. Заменяем всё содержимое файла /extCollections/_build/data/transport.chunks.php на:
      <?php
      $BUILD_CHUNKS = array();
      return array();

    3. Заменяем всё содержимое в файлах:
      /extCollections/_build/data/transport.menu.php
      /extCollections/_build/data/transport.settings.php
      /extCollections/_build/data/transport.snippets.php
      на:
      <?php
      return array();

    4. В файле /extCollections/_build/build.config.php меняем версию на 1.0.0 (не обязательно), а также в самом низу файла опустошаем массив $BUILD_RESOLVERS:
      $BUILD_RESOLVERS = array();

    5. В файле /extCollections/_build/build.transport.php на строках 13-15 комментируем подключение файла build.model.php:
      // if (file_exists('build.model.php')) {
      //     require_once 'build.model.php';
      // }

    6. В файле /extCollections/_build/data/transport.plugins.php добавляем плагин на событие OnDocFormPrerender:
      $tmp = array(
          'extCollections' => array(
              'file' => 'extcollections',
              'description' => '',
              'events' => array(
                  'OnDocFormPrerender' => array(),
              )
          ),
      );

    7. Создаём файл плагина в /extCollections/core/components/extcollections/elements/plugins/plugin.extcollections.php с содержимым:
      <?php
      if ($modx->event->name != 'OnDocFormPrerender' || $modx->context->key != 'mgr' || !is_object($resource)) {
          return;
      }
      
      $core_path = $modx->getOption('core_path') . 'components/extcollections/';
      $assets_url = $modx->getOption('assets_url') . 'components/extcollections/';
      $connector_url = $assets_url . 'connector.php';
      
      $modx->controller->addHtml("<script type=\"text/javascript\">
          extCollectionsConfig = {
              connectorUrl: \"{$connector_url}\",
          };
      </script>");
      
      $modx->controller->addJavascript($assets_url .'js/mgr/extcollections.js');
      $modx->controller->addLastJavascript($assets_url .'js/mgr/collections/ext.js');
      Как видно из этого кода, мы вначале подключаем конфиг со свойством connectorUrl равным /assets/components/extcollections/connector.php. После чего подключаем основной файл /assets/components/extcollections/js/mgr/extcollections.js, в котором создаётся и объявляется наш класс extCollections. Далее, подключаем файл /assets/components/extcollections/js/mgr/collections/ext.js, в котором у нас и будет происходить вся основная логика по расширению функционала Collections.

    8. Создаём файл /extCollections/assets/components/extcollections/js/mgr/collections/ext.js и добавляем в него такое содержимое:
      // Добавляем свои элементы в меню "Массовые действия"
      Ext.ComponentMgr.onAvailable('collections-grid-container-collections', function () {
          this.on('beforerender', function () {
              // Распечатываем в консоль объект this, чтобы найти в дереве свойств расположение меню
              // console.log('this', this);
              
              // Благодаря коду выше мы узнали, что кнопка "Массовые действия"
              // находится по адресу this.topToolbar.items.items[0].items.items[1]
              // Присваиваем её переменной, чтобы было проще с работать
              var btn = this.topToolbar.items.items[0].items.items[1];
              
              // Добавляем разделитель под оригинальными пунктами пакета Collections
              btn.menu.addMenuItem({
                  xtype: 'menuseparator',
              });
              
              // Добавляем наши кастомные пункты меню
              btn.menu.addMenuItem({
                  text: 'Изменить статус на "Не оптимизирован"',
                  handler: extCollections.utils.setTVValue,
                  scope: this,
                  tvName: 'status',
                  tvValue: 1,
              });
              btn.menu.addMenuItem({
                  text: 'Изменить статус на "Оптимизирован"',
                  handler: extCollections.utils.setTVValue,
                  scope: this,
                  tvName: 'status',
                  tvValue: 2,
              });
              btn.menu.addMenuItem({
                  text: 'Изменить статус на "Не требует оптимизации"',
                  handler: extCollections.utils.setTVValue,
                  scope: this,
                  tvName: 'status',
                  tvValue: 3,
              });
          });
      });
      В этом куске кода мы, на событие beforerender гриды #collections-grid-container-collections, повесили добавление своих пунктов меню в выпадающую кнопку «Массовые действия».

      В этом же файле пишем обработчик действия по кнопке:
      extCollections.utils.setTVValue = function (btn, e) {
          // В объекте btn хранятся свойства, которые мы прописали при добавлении элемента в меню.
          // Отсюда, мы получаем название ТВ поля и значение, которое необходимо установить.
          var tv_name = btn['tvName'];
          var tv_value = btn['tvValue'];
          if (typeof(tv_value) == 'undefined') {
              return;
          }
          
          // Объект this хранит в себе информацию о гриде, т.к. при добавлении элемента в меню, мы прописали свойство scope: this.
          // Отсюда, мы получаем список id у выделенных элементов гриды методом getSelectedAsList().
          var ids = this.getSelectedAsList();
          if (ids === false) {
              return false;
          }
      
          // Отправляем Ajax запрос по адресу /assets/components/extcollections/connector.php, в этом файле произойдёт вызов процессора /core/components/extcollections/processors/mgr/resource/settvvalue.class.php
          MODx.Ajax.request({
              url: extCollectionsConfig['connectorUrl'],
              params: {
                  action: 'mgr/resource/settvvalue',
                  ids: ids,
                  name: tv_name,
                  value: tv_value,
              },
              listeners: {
                  'success': {fn:
                      function() {
                          this.getSelectionModel().clearSelections(true);
                          this.refresh();
                      },
                      scope:this
                  }
              }
          });
          
          return true;
      };
      Думаю, тут дополнительные комментарии будут излишни — хватает комментариев в коде.

      И, наконец, добавим функцию рендера, которую позже пропишем в настройки вида коллекции:
      extCollections.utils.renderTVStatus = function(value) {
          var color = 'white';
          if (value == 1) {
              color = 'red';
          } else if (value == 2) {
              color = 'green';
          } else if (value == 3) {
              color = 'grey';
          }
          
          return '<div style="width: 12px; height: 12px; line-height: 0; background: ' + color + '; border-radius: 50%;"> </div>';
      };

    9. Создаём файл процессора /core/components/extcollections/processors/mgr/resource/settvvalue.class.php:
      <?php
      class extCollectionsResourceSetTvValueProcessor extends modObjectProcessor
      {
          public $objectType = 'modResource';
          public $classKey = 'modResource';
          public $languageTopics = array('extcollections');
          
          /**
           * @return array|string
           */
          public function process()
          {
              if (!$this->checkPermissions()) {
                  return $this->failure($this->modx->lexicon('access_denied'));
              }
              
              $name = $this->getProperty('name');
              if (empty($name)) {
                  return $this->failure('error');
              }
      
              $ids = array_map('trim', explode(',', $this->getProperty('ids')));
              if (empty($ids)) {
                  return $this->failure('error');
              }
      
              foreach ($ids as $id) {
                  /** @var modResource $obj */
                  if (!$obj = $this->modx->getObject($this->classKey, $id)) {
                      return $this->failure('error');
                  }
      
                  $obj->setTVValue($name, $this->getProperty('value'));
              }
      
              return $this->success();
          }
      }
      return 'extCollectionsResourceSetTvValueProcessor';

  6. Идём в Приложения > Виды коллекции, кликаем правой мышью на недавно созданный нами вид коллекции > Изменить, крутим в самый низ > Добавить столбец:
    Заголовок: Статус
    Название: tv_status
    Ширина: 40
    Рендерер: extCollections.utils.renderTVStatus

  7. Назначаем ресурсу-коллекции этот вид, чтобы увидеть новый столбец.

  8. Билдим пакет — _http://вашсайт/extCollections/_build/build.transport.php
28 september 2016, 00:34    Павел Гвоздь   
20    915 +19

Comments (19)

  1. Кирилл 28 september 2016, 06:27 # +2
    Спасибо за подробный мануал. На заметку:

    $obj->setTVValue($name, $this->getProperty('value'));
    $obj->save();
    
    save() нет необходимости делать, будет работать и так:

    $obj->setTVValue($name, $this->getProperty('value'));
    1. Павел Карелин 29 september 2016, 01:27 # 0
      Ой не соглашусь, совсем не соглашусь. $obj->save(); обязательно к использованию, да бывает конечно все сохраняется, но для подстраховки все таки лучше использовать, потому что было у меня пару раз ситуация когда тв просто отказывались сохранятся, особенно если идут прямо после
      $obj->set();
      1. Кирилл 29 september 2016, 05:10 # 0
        Можно глянуть в документацию.

        Use setTVValue to save a new value to a TV. Unlike some other xPDO API methods, this method stores values to the database immediately, so you do not need to invoke a separate call to a save() method. This method does not clear the resource cache.

        > было у меня пару раз ситуация когда тв просто отказывались сохранятся
        Наверное, потому что ресурс еще не создан. Т.е. у него еще нету ID, поэтому и setTVValue не может в БД записать значение. В исходном примере такая ситуация не возможна.
        1. Павел Карелин 29 september 2016, 15:55 # 0
          Так и есть. Но это не отменяет того факта что save лучше использовать.
          1. Воеводский Михаил 29 september 2016, 16:03 # +1
            Отменяет.
            1. Павел Карелин 29 september 2016, 16:42 # 0
              Вот вы зануды.
            2. Роман Садоян 29 september 2016, 16:19 # 0
              Лучше глянуть в код, там всё понятно. ТВ это другой объект.
      2. Илья Уткин 28 september 2016, 08:40 # 0
        Каеф
        1. Сергей Шлоков 28 september 2016, 09:52 # +3
          Позволю себе побрюзжать немного :)
          1. Людям не знакомым с Collections (как я например) из описания совершенно не понятно, что и для чего. Нужно лезть в инструкцию, чтобы понять, что добавляются пункты контекстного меню.
          2. Ну и зачем всё это писать, если можно выложить на гитхаб уже готовое решение с очень маленькой инструкцией по кастомизации — где и что изменить в заготовке под себя. А в такой простыне кода даже мне страшно разбираться.
          Это два, как мне кажется, упущения.
          Ну а по мелочи… Думаю, такие вещи как
          this.topToolbar.items.items[0].items.items[1];
          и
          var color = 'white';
          if (value == 1) {
              color = 'red';
          } else if (value == 2) {
              color = 'green';
          } else if (value == 3) {
              color = 'grey';
          }
          
          и
          if (!$this->checkPermissions()) {
               return $this->failure($this->modx->lexicon('access_denied'));
          }
          со временем трансформируются во что-то более совершенное. :)
          Согласен, коллега?
          1. Павел Гвоздь 28 september 2016, 10:16 # +3
            Странно, вопросы как будто к автору поста, а направлены Илье. Сергей, кому вопросы то и кто этот загадочный «коллега»?)
            1. Сергей Шлоков 28 september 2016, 10:33 # 0
              Каждый человек — загадка, мир, вселенная. В философском смысле. А в профессиональном Илья на высокой ступени иерархии. Тут нет никакой загадки. Для меня по крайней мере.
              кому вопросы то
              Какие вопросы? Согласен, коллега? Это риторический вопрос, не требующий ответа. ;)
              А всё остальное — просто брюзжание немолодого человека юных лет про то, что мне не хватает для кайфа. )
              1. Павел Гвоздь 28 september 2016, 13:48 # +3
                Ясно…

                1. Людям не знакомым с Collections (как я например) из описания совершенно не понятно, что и для чего. Нужно лезть в инструкцию, чтобы понять, что добавляются пункты контекстного меню.
                Людям, не знакомым с Collections, стоит ознакомиться с ним, раз уж заинтересовались данной темой.

                2. Ну и зачем всё это писать, если можно выложить на гитхаб уже готовое решение с очень маленькой инструкцией по кастомизации — где и что изменить в заготовке под себя.
                Так это не готовое решение, а пример того, как можно расширить компонент Collections, не затрагивая его исходников.

                А в такой простыне кода даже мне страшно разбираться.
                Сергей, если страшно, то не надо. :)

                такие вещи как this.topToolbar.items.items[0].items.items[1]; со временем трансформируются во что-то более совершенное.
                Представь, как твой комментарий приобрёл бы ценность, добавив ты туда пример совершенного кода?
                1. Сергей Шлоков 28 september 2016, 14:52 # +3
                  Признаюсь, я ошибался. Видишь как люди плюсуют твои комментарии. Значит им удобнее твой вариант — пройти все 8 шагов инструкции. А я то по старинке всё готовые дополнения делаю. )
                  Представь, как твой комментарий приобрёл бы ценность, добавив ты туда пример совершенного кода?
                  Представь, как мне фиолетово. Судя по комментарию это мне у тебя ещё учится и учится.
                  П.С. А если серьёзно, то код-то как раз пользователя не волнует. Единицы смотрят под капот. А вот удобство настройки, мне кажется, дело первоочередное. UX forever!
                  П.П.С. Collections у меня нет, скажу по теме пользователей. Вот такая конструкция
                  var leftCol = this.items.items[0].items.items[0].items.items[0];
                  легко заменяется на такую
                  Ext.getCmp('modx-user-active').ownerCt
                  И в итоге чекбокс добавить можно так:
                  $modx->controller->addHtml("
                  <script type='text/javascript'>
                       Ext.ComponentMgr.onAvailable('modx-user-tabs', function() {
                            this.on('beforerender', function() {
                                 var cb = Ext.create({
                                       xtype: 'xcheckbox',
                                       boxLabel: 'Тестовый чекбокс',
                                       description: 'Тестовый чекбокс',
                                       name: 'testCheckbox',
                                       checked: true
                                 });
                            Ext.getCmp('modx-user-active').ownerCt.insert(0, cb);
                           });
                       });
                  </script>
                  ");
                  1. Павел Гвоздь 28 september 2016, 15:04 # 0
                    Collections у меня нет, скажу по теме пользователей. Вот такая конструкция
                    var leftCol = this.items.items[0].items.items[0].items.items[0]; легко превращается в такую
                    Ext.getCmp('modx-user-active').ownerCt
                    Сергей, исключительно ради интереса, я тебе создам тестовый сайт и загружу туда Collections, а ты мне покажешь, как имея объект гриды и выходя вверх, можно получить объект кнопки, которая находится в верхнем тулбаре этой гриды, фактически на несколько уровней ниже. Согласен?
                    1. Руслан Кундиус 28 september 2016, 17:11 # 0
                      getCmp не прокатит, т.к. кнопки анонимные там
                    2. Руслан Кундиус 28 september 2016, 16:59 # +1
                      extCollections.utils.renderTVStatus = function(value = 0) {    
                          return '<div style="width: 12px; height: 12px; line-height: 0; background: ' + ['white', 'red', 'green', 'grey'][value] + '; border-radius: 50%;"> </div>';
                      };
                      :)
            2. Владимир 28 september 2016, 09:00 # 0
              Круто! И нужная вещь, спасибо!
              1. Abu 28 september 2016, 14:30 # 0
                Оффтоп. Еще не разобрался, но плюсанул заранее, пока время голосования не истекло. Все таки жалко, нахожу постоянно старые полезные посты, а автора даже уже не лайкнуть. Кому жаловаться?
                1. Роман Садоян 28 september 2016, 17:16 # 0
                  Сам знаешь.
                You need to login to create comments.