[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
Павел Гвоздь
27 сентября 2016, 21:34
modx.pro
31
5 400
+19
Поблагодарить автора Отправить деньги

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

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

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

$obj->setTVValue($name, $this->getProperty('value'));
    Павел Карелин
    29 сентября 2016, 01:27
    0
    Ой не соглашусь, совсем не соглашусь. $obj->save(); обязательно к использованию, да бывает конечно все сохраняется, но для подстраховки все таки лучше использовать, потому что было у меня пару раз ситуация когда тв просто отказывались сохранятся, особенно если идут прямо после
    $obj->set();
      Кирилл
      29 сентября 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 не может в БД записать значение. В исходном примере такая ситуация не возможна.
Илья Уткин
28 сентября 2016, 08:40
0
Каеф
    Сергей Шлоков
    28 сентября 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'));
    }
    со временем трансформируются во что-то более совершенное. :)
    Согласен, коллега?
      Павел Гвоздь
      28 сентября 2016, 10:16
      +3
      Странно, вопросы как будто к автору поста, а направлены Илье. Сергей, кому вопросы то и кто этот загадочный «коллега»?)
        Сергей Шлоков
        28 сентября 2016, 10:33
        0
        Каждый человек — загадка, мир, вселенная. В философском смысле. А в профессиональном Илья на высокой ступени иерархии. Тут нет никакой загадки. Для меня по крайней мере.
        кому вопросы то
        Какие вопросы? Согласен, коллега? Это риторический вопрос, не требующий ответа. ;)
        А всё остальное — просто брюзжание немолодого человека юных лет про то, что мне не хватает для кайфа. )
          Павел Гвоздь
          28 сентября 2016, 13:48
          +3
          Ясно…

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

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

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

          такие вещи как this.topToolbar.items.items[0].items.items[1]; со временем трансформируются во что-то более совершенное.
          Представь, как твой комментарий приобрёл бы ценность, добавив ты туда пример совершенного кода?
            Сергей Шлоков
            28 сентября 2016, 14:52
            1
            +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>
            ");
              Павел Гвоздь
              28 сентября 2016, 15:04
              0
              Collections у меня нет, скажу по теме пользователей. Вот такая конструкция
              var leftCol = this.items.items[0].items.items[0].items.items[0]; легко превращается в такую
              Ext.getCmp('modx-user-active').ownerCt
              Сергей, исключительно ради интереса, я тебе создам тестовый сайт и загружу туда Collections, а ты мне покажешь, как имея объект гриды и выходя вверх, можно получить объект кнопки, которая находится в верхнем тулбаре этой гриды, фактически на несколько уровней ниже. Согласен?
                Руслан Кундиус
                28 сентября 2016, 17:11
                0
                getCmp не прокатит, т.к. кнопки анонимные там
                Руслан Кундиус
                28 сентября 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>';
                };
                :)
        Владимир
        28 сентября 2016, 09:00
        0
        Круто! И нужная вещь, спасибо!
          Abu
          Abu
          28 сентября 2016, 14:30
          0
          Оффтоп. Еще не разобрался, но плюсанул заранее, пока время голосования не истекло. Все таки жалко, нахожу постоянно старые полезные посты, а автора даже уже не лайкнуть. Кому жаловаться?
          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
          19