.. _spec_work-center_navhub_history-hooks: Наполнение секций истории ========================= .. versionadded:: 1.28.0 Секции "Часто используемое" и "Недавно закрытые" :ref:`Навигационного хаба ` наполняет прикладной слой: он перехватывает действия пользователя, накапливает историю и уведомляет сервер о её изменении. Подключение самих секций через разметку описано на странице :doc:`Подключение Хаба к приложению `. Модель ответственности ---------------------- Платформа и прикладной слой делят обязанности так: - **Платформа** отображает секции, хранит пользовательские настройки секции "Закрепленное", читает данные секций истории из прикладных выборок (``selectionName`` контролов ```` с ролями ``popular`` и ``latest``) и обновляет клиент сессии, в которой опубликовано событие истории. - **Прикладной слой** перехватывает действия пользователя (выполнение операций, закрытие выборок), накапливает и агрегирует историю (например, в таблицах решения), поддерживает актуальность данных выборок истории и публикует событие в топик ``NavHubHistoryEventsTopic`` после каждого изменения. Выбор способа накопления и агрегации истории — зона ответственности прикладного департамента; ориентиром служит существующая прикладная библиотека телеметрии ``ru.bitec.app.btk.telemetry.Btk_TelemetryLib``. Перехват закрытия выборок ------------------------- Факт использования выборки надёжнее регистрировать по событию закрытия (``afterClose``): к этому моменту выборка гарантированно открывалась без ошибок и пользователь взаимодействовал с ней. Перехват по событию открытия (``beforeOpen``) реализуется идентично. Закрытие перехватывается переопределением метода ``afterClose`` (``ru.bitec.app.gtk.gl.DefaultRep#afterClose``) в выборке. Чтобы не дублировать переопределение в каждой выборке, создаётся промежуточный класс: .. code-block:: scala abstract class NavigationHubEntityAvi extends EntityAvi { trait NavigationHubEntityAviDefault extends EntityAviDefault { override protected def afterClose(): Unit = { super.afterClose() // Зарегистрировать закрытие выборки в истории текущего пользователя. // ... } } } AVI-классы выборок, участвующих в истории, наследуются от промежуточного класса: .. code-block:: scala @AvmFile(name = "Module_Document.avm.xml") class Module_DocumentAvi extends Module_DocumentDvi with NavigationHubEntityAvi { trait Default extends DviDefault with NavigationHubEntityAviDefault trait List extends Default with DviList trait Card extends Default with DviCard } Такой подход заодно определяет область охвата: выборки, не подмешавшие ``NavigationHubEntityAviDefault``, в истории не регистрируются. Параметры открытия в истории ---------------------------- Чтобы история различала объекты одного класса — например, карточки разных договоров, — вместе с именем выборки при закрытии сохраняются параметры, с которыми открыта форма. В выборке тайлов истории они отдаются колонкой параметров открытия (атрибут ``actionParamsAttr`` фрейма ````, см. :doc:`Подключение Хаба к приложению `), и клик по тайлу воспроизводит открытие. - **Источник.** ``selection.form.params`` (``ru.bitec.app.gtk.gl.Form#params``) — параметры, переданные форме при открытии: из URI-ссылки на выборку, из ``openFormEx`` прикладного кода или из тайла Хаба. Значения в карте не обязательно строки; в JSON они записываются строкой, числом или ``true``/``false``. Ключи со значением ``null`` при сериализации отбрасываются, вложенные объекты и массивы не записываются: тайл с такими значениями не выводится. - **Что в карту не попадает.** Фильтры, выставленные пользователем в открытой форме, и переход к другой записи внутри карточки. Если для истории важна запись, на которой пользователь закончил работу, её ключ берётся из текущей строки выборки и записывается под именем параметра ключа карточки (в прикладных библиотеках обычно ``idItem#``). Для списков, открытых без параметров, карта пуста — колонка параметров остаётся пустой. - **Служебные ключи.** Ключи, не влияющие на то, какой объект откроется (``CREATINGMODE``, ``DEFAULTEDITOR``, размеры окна), в историю не записываются, иначе одна карточка даст несколько записей. ``EDITINGTYPE`` со значением ``Edit`` сохраняется. Карточки, открытые на создание или копирование, в историю не записываются: повторное открытие создало бы ещё один объект. - **Подпись тайла.** Для тайла записи подпись формируется из объекта (номер, наименование), а не из заголовка выборки, и пересчитывается при чтении выборки истории. - **Ключ агрегации.** Сервер сравнивает параметры тайлов без учёта порядка ключей. Чтобы одна карточка не расходилась в истории на несколько записей, ключ агрегации строится из имени выборки и параметров одним методом для обеих секций — например, с сортировкой ключей перед сериализацией. - **Размер колонки.** Колонка параметров должна вмещать сериализованную карту целиком: обрезанный JSON не разбирается, и тайл не выводится. Если в параметрах бывают фильтры или макросы, колонку заводят текстового типа без ограничения длины. Перехват выполнения операций ---------------------------- Выполнение операций перехватывается обработчиком событий уровня приложения. В классе главной выборки приложения (какой класс является главным — определяется по ``src/main/resources/META-INF/applications.xml`` прикладного решения) переопределяется метод ``onRestoreState`` (``ru.bitec.app.btk.gl.ApplicationAvi.ApplicationAviDefault#onRestoreState``) и регистрируется обработчик на все операции: .. code-block:: scala override def onRestoreState(bundle: KeyBundle): Unit = { super.onRestoreState(bundle) val thisRep = selection.baseRep.unwrap(classOf[InternalRep]) val idUser = Btk_UserApi().getCurrentUserID() val sSelectionName = selection.name + "#" + selection.representation val isOperProcessing = (sOperName: String) => { (sOperName.toUpperCase === "closeFormOk".toUpperCase).isFalse && (sOperName.toUpperCase === "closeFormCancel".toUpperCase).isFalse && !session.isClosed && selection.coreRep != null // Допустимы дополнительные проверки. } thisRep.handlers.addBeforeEvent(RepEvent.allOperations, spOperName => { val sOperName = spOperName.toLowerCase if (isOperProcessing(sOperName)) { // Сохранить параметры выполненной операции в историю. // ... } }) } Служебные операции (в примере — операции закрытия форм) исключаются из регистрации фильтром ``isOperProcessing``. Событие изменения истории ------------------------- После внесения изменений в историю (или пересчёта агрегатов) прикладной код публикует событие в шину сообщений. Контракт события задаёт интерфейс ``ru.bitec.gtk.core.gl.workcenter.navhub.NavHubHistoryEventsTopic`` (модуль ``gtk-core`` — доступен и прикладному слою): .. list-table:: :header-rows: 1 :width: 100% :widths: 34 33 33 * - Константа - Значение - Назначение * - ``TOPIC_NAME`` - ``WorkCenter.NavHub.History.AppEvents`` - Имя выделенного топика событий истории * - ``LASTCLOSED_EVENT_NAME`` - ``EVENT_LASTCLOSED`` - Изменился список недавно закрытых — обновить секцию "Недавно закрытые" * - ``POPULAR_EVENT_NAME`` - ``EVENT_POPULAR`` - Изменилась статистика использования — обновить секцию "Часто используемое" Публикация выполняется асинхронно, ответ не предусмотрен: .. code-block:: scala import ru.bitec.gtk.core.gl.workcenter.navhub.NavHubHistoryEventsTopic application.eventBus.post( NavHubHistoryEventsTopic.TOPIC_NAME, NavHubHistoryEventsTopic.LASTCLOSED_EVENT_NAME, Array.empty ) Событие — только сигнал: получив его, сервер заново читает прикладные выборки истории соответствующей секции (см. :doc:`Подключение Хаба к приложению `) и передаёт обновлённое состояние Хаба на открытые клиенты. Аргументы события сервер не разбирает, поэтому к моменту публикации данные выборок истории уже должны отражать новое состояние. События с другими именами в этом топике сервер игнорирует. .. seealso:: :doc:`Подключение Хаба к приложению `