image001

© Syntellect 2019


1. Общие сведения

Веб-приложение Tessa – это Single-Page Application (SPA). В папке с web-сервисом "web" находятся файлы и папки, относящиеся к веб-серверу Tessa, который написан на ASP.NET Core. В подпапке wwwroot находится клиентская часть приложения:

image1

В данной папке содержатся:

  • js-файлы с хешами в названиях. Хеши зависят от содержимого файлов, что позволяет браузеру кешировать старые скрипты и загружать новые версии при следующем запросе к серверу;

  • в папке extensions лежат пользовательские расширения – это должны быть обычные js-скрипты стандарта es5 с самозагрузкой.

Загрузка приложения происходит следующим образом:

  1. Пользователь обращается к веб-серверу Тессы.

  2. Веб-сервер отдает пользователю html-файл со ссылкой на основные скрипты приложения и набор скриптов расширений.

  3. Браузер пользователя загружает скрипты с приложением, которое начинает автоматически выполняться.

  4. Приложение строит ui-интерфейс из react – компонентов и показывает его пользователю.

2. React.js

Для создания пользовательского интерфейса мы используем библиотеку React.js, разработанную в facebook.

Особенности:

  • Декларативное описание интерфейса.

  • Перестроение интерфейса при каждом обновлении данных (это работает быстро из-за концепции shadow DOM и оптимизационных техник, типа shoudComponentUpdate и Immutable.js – подробнее в документации и интернете).

  • Приоритетным является односторонний поток данных (односторонние биндинги).

Рассмотрим пример:

class Content extends React.Component {
  render() {
    return (
      <div className="dashboard">
        <TessaTree treeData={this.props.treeData} selected={selectedView} />
        <TessaView viewData={this.props.viewData} />
      </div>
    );
  }
}

Здесь приведено определение компонента Content.

Метод render возвращает то, как должен выглядеть компонент.

Как видно, компонент состоит из div-элемента, а также из других компонентов: TessaTree, TessaView. При построении компоненты TessaTree, TessaView также будут опрошены через метод render, как они должны выглядеть. Таким образом создание новых компонент может быть разбито на части, со своими свойствами и отображением.

Компонент может получать данные и ссылки на функции через свойства. В этом случае свойства - это treeData, selected для компонента TessaTree, и viewData для TessaView.

К переданным данным компонент может получить доступ через свойство props. В этом случае компоненту Content были переданы свойства viewData и treeData (за рамками примера). Компонент может работать с переданными данными как ему надо. В данном случае Content передает полученные данные дальше, вниз по иерархии.

После опроса всех компонентов мы получаем метаструктуру описания html-страницы, которую react переводит в html-теги и разметку показывает пользователю.

Подробнее о работе React.js в документации.

3. Ключевые моменты и словарь

3.1. Action, action creator, dispatch

Для отправки экшена из компонентов используется системная функция dispatch:

function dispatch(_arg: Action | ActionCreator);

На вход подается или Action, или ActionCreator.

  • Action – это обычный простой js-объект:

    {
       type: TYPE,
       payload: PAYLOAD
    }

    где type – это обозначение типа экшена (например, нажата кнопка меню), payload – нагрузка.

  • ActionCreator – это функция с сигнатурой:

    function (dispatch, getState)

    где dispatch – это та самая системная функция обновления стейта, getState – функция, возвращающая текущий стейт. Т.е. внутри actionCreator есть доступ ко всем данным приложения и возможна сколь угодно сложная логика вызова обновления состояния.

3.2. Reducer

Функция обновления состояния приложения с сигнатурой:

function (action, previousState): newstate

3.3. Однонаправленный поток данных

Все обновления стейта происходят через вызов экшенов.

4. Состояние приложения

Это простой объект js с полями:

  • routing - системная часть, описывает работу с url (react-router);

  • loginInfo - аутентификационная информация;

  • systemInfo - системная информация;

  • ui - внешний вид приложения, immutable;

  • data - данные приложения, immutable.

Состояние приложения документировано, поставляется вместе со сборкой.

4.1. Immutable.js

Для оптимизации перестройки компонентов, некоторые части стейта сделаны через Immutable-структуры. Immutable буквально - "неизменяемый". При попытке изменения таких объектов создаются новые объекты, а старые остаются, как были – из этого получается полезный для нас эффект: для сравнения двух объектов будет достаточно сравнить "ссылки" на них.

Например: допустим, часть приложения у нас завязана на сложную часть стейта с глубокой иерархией. Поскольку, react.js, для понимания, надо ли обновлять html-страницу (что является самой затратной операцией), делает в памяти виртуальное представление страницы, то он должен виртуально перестроить те компоненты, которые зависят на эту часть стейта. Это перестроение в памяти довольно быстрая операция, но и этого можно было бы избежать, если бы понять, что данные для компонентов не изменились.

Для этого нужно написать сравнитель старых и новых данных: если данные у нас изменяемые, то чтобы сравнить два объекта, нужно сравнить все его поля, а чтобы сравнить данные, которые неизменяемые, достаточно сравнить внутренние ссылки на них – отсюда выигрыш в производительности.

Выигрыш в производительности выливается в менее удобное использование. Например, для получения данных из поля надо использовать метод get, а для установки - set. Но для большинства immutable-данных мы написали вспомогательные структуры, что позволяет обращаться к полям через точку, к тому же, для написания большинства расширений установка напрямую значений в стейт будет не нужна.

5. Процесс разработки

Для разработки будет выдан шаблонный проект с набором конфигурационных файлов.

Для работы потребуется установить Node.js.

Для разработки используется язык typescript – это надстройка на javascript, добавляющая возможность строгой типизации.

Нужно установить все зависимости из package.json:

npm install

В качестве редактора мы рекомендуем использовать vs code или sublime.

5.1. VS Code

Полезные расширения:

  • Eslint – "полиция моды" для es6 js-файлов (указывает на неправильное форматирование и стиль кода);

  • Tslint – "полиция моды" для typescript файлов;

  • Oceanic_next theme – приятная цветовая схема;

  • Prettify JSON – преобразует json-строку в json-структуру с отступами.

Расширения ставятся стандартным для VS Code способом: в панели расширения в поисковой строке пишете названия расширений.

5.2. Шаблонный проект

В состав сборки входит папка WebClient SDK со следующей структурой:

image3
  • dts – папка с описанием API для работы intellisense в typescript;

  • examples - примеры расширений;

  • wwwroot/extensions – тут будут размещаться "скомпилированные" js-расширения;

  • .babelrc, .eslintrc, .tslint – конфигурационные файлы для указания стиля кода;

  • package.json – описание внешних зависимостей;

  • webpack.config – конфигурация процесса компиляции.

Для сборки расширений нужно в корне папки выполнить команду:

npm run build

5.2.1. Webpack.config

Для сборки своих расширений нужно модифицировать webpack.config.js: в поле module.exports.entry указать "ключ: значение", где:

  • ключ – название файла с расширением после сборки,

  • значение – путь к исходному коду (например, ./examples/1_hideBlockbyReferenceValue - путь от корня):

module.exports = {
...
  entry: {
    hideBlockbyReferenceValue: './examples/1_hideBlockbyReferenceValue',
  },
...
}

В названии файлов будет использован хеш от содержимого файлов.

6. Расширения

Расширения для web-клиента делятся на три вида:

  1. Расширение экшена – для кастомизации потоков информации приложения: отменять/изменять/добавлять/проверять события приложения.

  2. Расширение компонента – для кастомизации ui компонентов приложения. Например, если нужно изменить внешний вид стандартных компонентов или написать свои.

  3. Расширение редьюсера – для обработки кастомных событий (для изменения данных в стейте нестандартным образом).

6.1. Расширение экшена

Для расширений экшена можно использовать стандартные классы экшенов, или написать свои.

Экшен – событие в системе, такое как: открытие вкладки, загрузка карточки, переключение представлений.

Например, при загрузке карточки может произойти четыре типа событий:

type описание

CARD_GET

При запросе на обращение к серверу

CARD_GET_REQUEST

Перед вызовом сервера (информационное)

CARD_GET_SUCCESS

При успешном ответе от сервера

CARD_GET_FAILURE

При ошибке

Расширение экшена может отменить события, изменить их данные, выполнить действия до или после.

Существующие типы экшенов можно импортировать из actions/actionTypes.

6.1.1. Базовые классы расширений экшена

  • Класс SimpleActionLogic из actions/types – для расширений, не взаимодействующих с сервером.

  • Класс AsyncActionLogic из actions/types – для расширений, которые могут взаимодействовать с сервером.

Существует несколько точек обработки экшенов в расширении:

  • onValidate – метод вызывается перед началом любых действий с экшеном.

  • onTransform – метод вызывается после валидации. В методе можно изменить данные экшена, которые попадут остальным методам в цепочке.

  • onBefore – метод вызывается перед выполнением основной логики приложения.

  • onAfter – метод вызывается после выполнения основной логики приложения.

Для AsyncActionLogic есть дополнительные точки расширения:

  • onProcess – метод вызывается после onBefore и в нем выполняется основная логика обработки экшена (например, отправляется запрос на сервер).

  • onPostTransform – метод выполняется после выполнения onProcess. В методе можно изменить данные, которые попадут остальным методам в цепочке.

  • onPostProcess – метод выполняется в конце цепочки. В методе можно обработать результаты, полученные с сервера. После, в цепочке, вызовется onAfter.

  • onErrorHandler – метод вызывается, если в процессе работы произошла ошибка. Цепочка выполнения прерывается.

6.1.2. Стандартные классы расширений экшена

Стандартные классы можно импортировать из модуля actions/types.

Для примера рассмотрим класс экшена createdOrGotCardAction. Этот экшен подписан на два события:

  • получение карточки с сервера (CARD_GET_SUCCESS);

  • создание новой карточки (W_CARD_SUCCESS).

Допустим, мы хотим показать пользователю тему в карточке в стандартном информационном сообщении. Мы подпишемся на действие после события:

import { createdOrGotCardAction } from 'actions/types';

createdOrGotCardAction.onAfter(({action, dispatch}) => {
  const { cardId, data } = action.payload;
  const section: ICardSection = <any>data.Card.Sections['DocumentCommonInfo'];
  const field = section.Fields['Subject']

  alert(field.$value);
});

6.1.3. Кастомные классы расширений экшена

Для создания класса расширения необходимо отнаследоваться от одного из базовых классов.

В расширении необходимо указать типы экшенов, которые будут расширены (например, UI_INIT).

6.2. Расширение компонента

Расширение компонента позволяет изменить/преобразовать/доработать поведение стандартных компонентов. Ниже показан пример регистрации компонента на расширение.

businessLogic.addComponent({
  desc: {
    /**
     * Тип компонента для расширения
     */
    type: 'ТИП_КОМПОНЕНТА',
    /**
     * Идентификатор для расширения
     */
    id: 'ИДЕНТИФИКАТОР_РАСШИРЕНИЯ'
  },

  /**
   * Опционально. Надо ли рендерить?
   */
  shouldComponentUpdate: (nextProps, nextState) => {
    // ...
  },

  /**
   * Опционально. Как рендерить компонент
   */
  render: (props) => {
    // ...
  },

  /**
   * Опционально. Взять дополнительные данные из стейта
   */
  mapStateToProps: (state, ownProps) => {
    // ...
  },

  /**
   * Опционально. Преобразовать свойства передающиеся в компонент
   */
  modifyProps: (props) => {
    // ...
  },

  /**
   * Опционально. Передать экшены в компонент
   */
  mapDispatchToProps: (dispatch) => {
    // ...
  }
});

В параметре desc указываются обязательные параметры, остальные параметры - опциональные. Пример расширения на компонент можно посмотреть в разделе Примеры расширений, 8-ое расширение.

6.3. API

Imports:

  • actions – все типы экшенов и сами экшены;

  • actions/types – базовые классы расширений на конкретные экшены;

  • common – хелперные классы (API, card, tile);

  • common/cards – интерфейсы и мета, связанные с карточками;

  • common/utility – интерфесы хелперных методов и классов;

  • common/views и common/workplaces – интерфейсы и мета, связанные с представлениями и рабочими местами;

  • models – интерфесы стейта приложения;

  • ui – базовые компоненты ui (Autocomplete, AutocompleteContextMenu, AutocompleteDropdown, AutocompleteInput, FlatButton, IconButton, RaisedButton, Checkbox, DatePicker, DatePickerCalendar, DatePickerDay, DatePickerMonth, DatePickerWeek, Dialog, DialogContainer, DialogContent, DialogFooter, DialogHeader, Dropdown, DropdownItem, FontIcon, Popover, Portal, SlideCloser, TextField, EnhancedTextarea, ViewGrid).

7. Примеры расширений

Работа расширений демонстрируется на основе тестового типа карточки WebTestType.

Загрузите из папки Configuration рабочее место, представление и карточку.

Добавьте тип карточки в типовое решение, разрешите создание заданий.

Расширения собираются командой:

npm run build

Собранные версии будут лежать в папке wwwroot/extensions. Копируйте их в папку по одному и перегружайте web клиент нажатием кнопки f5 для того, чтобы они применились.

Расширения можно копировать все вместе, но для демонстрации работы каждого рекомендуем пробовать по одному.

Для текстовых контролов и автокомплита учитывайте, что данные изменяются при снятии фокуса с контрола (клик в другом месте).

1_hideBlockbyReferenceValue_newApi.ts

В зависимости от значения ссылочного поля карточки скрывается/показывается блок:

Если валюта не Рубль, то не показываем блок с Темой.

С новым API (более простым).

1_hideBlockbyReferenceValue.ts

В зависимости от значения ссылочного поля карточки скрывается/показывается блок:

Если валюта не Рубль, то не показываем блок с Темой.

Старый API (для информации).

1_hideControlbyReferenceValue.ts

В зависимости от значения ссылочного поля карточки скрывается/показывается контрол:

Если валюта не Рубль, то не показываем контрол Автор.

1_makeControlReadonlykbyReferenceValue.ts

В зависимости от ссылочного поля элемент управления становится доступен только на чтение:

Если валюта не Рубль, то делаем контрол Контрагент только на чтение.

2_changeFieldInCard.ts

В этом расширении, в зависимости от значения флага:

  • Меняется содержимое текстового поля.

  • Меняется содержимое табличной секции (в нее добавляется элемент).

Когда меняют значение флага Базовый цвет, то:

  1. Если флаг установлен, то значение поля Цвет меняется на Это базовый цвет.

  2. Если флаг установлен, то в секцию Исполнители добавляется новый сотрудник Admin.

  3. Если флаг не установлен, то значение поля Цвет меняется на Это другой, не базовый цвет.

3_onTableSectionChanged.ts

В этом расширении демонстрируется подписка на изменение элементов табличной секции:

Когда изменяется секция Получатели, то в поле Тема добавляется строка Card recipients section has changed.

4_hideForm.ts

Скрывать/показывать вкладку карточки в зависимости:

  • От наличия какого-то признака в info, пришедшем с сервера.

  • От значения какого-то справочника, загруженного в init-стриме.

  • От данных карточки.

Расширение всегда показывает вкладку, но в нем, в закомментаренном виде, приведены примеры того, как достучаться до данных в info справочника init-стрима.

5_hideTaskBlock.ts

Скрывать/показывать контрол с информацией о задании (в задании) в зависимости от наличия/отсутствия текста в комментарии. По клику показывает лингвистический анекдот.

Как тестировать

Отправьте себе стандартную задачу через тайл на левой панели - "Поставить задачу". Затем возьмите её в работу, нажмите "Завершить" и напишите любой текст в поле "Комментарий".

6_changeCardTile.ts

На левую панель добавляет тайл с названием "Тайл карточки".

Показывается только в карточке нашего типа, и при условии, что тема карточки будет "Карточка с тайлом".

7_changeViewTile.ts

В этом расширении демонстрируется:

  • Добавление тайла, связанного с представлением.

  • Доступ к данным выделенной строки в представлении.

Добавляет на левую панель тайл с названием "Данные строки". Показывается только в представлении, если выделен узел "Мои документы". При выделенной строке в представлении, по клику на тайл показываются необработанные данные строки.

8_requestTile.ts, 8_requestTileModalForm.jsx

В этом расширении демонстрируется:

  • Тайл-группа на левой панели.

  • Запрос карточки с сервера.

  • Запрос данных представления с сервера.

  • Заполнение параметров поиска в запросе к представлению.

  • Доступ к результату запроса представления.

  • Кастомный диалог со списком контрагентов.

  • По двойному клику открывается карточка контрагента.

На левую панель добавляет групповой тайл - "Запросы". По клику показываются дочерние тайлы - "Запрос карточки" и "Запрос вью".

При клике на "Запрос карточки" происходит вызов сервера, получается карточка прав, и необработанное содержимое показывается в модальном окне. Аналогично, с помощью CardService можно делать и другие реквесты (request, get, store и т.д.).

При клике на "Запрос вью" происходит вызов сервера, получаются данные списка контрагентов, показывается модальное окно со списком контрагентов.

9_additionalTableButton.jsx

Пример расширения работы кнопки в карточке. Расширение определяет логику при нажатии на кнопку на форме с карточкой.

При нажатии на кнопку определяется текущая выделенная строка в контроле таблицы. Получаются данные этой строки и, после, добавляется новая строка в секцию с данными из выделенной строки.

10_dialogModeAutocomplete.jsx

В web-клиенте есть два вида автокомплитов:

  • Первый используется для десктопного режима - текст набирается прямо в элементе управления и окно с подсказками появляется рядом с курсором. Также и выпадающий список комбобокса.

  • Второй используется для мобильных устройств - это отдельный диалог.

Достоинством второго является возможность показать в диалоге длинный список подсказок, т.к. в нем есть прокрутка. Вариант с диалогом удобно использовать в десктопе для некоторых автокомплитов, где заранее известно, что будет много вариантов выбора. Т.е. диалог по умолчанию есть только в мобильной версии, но, если нужно, его можно включить и на десктопной версии для выбранных контролов.

В этом расширении демонстрируется открытие определённых автокомплитов в режиме диалога.

Для карточки: проверяем, что значения типа карточки и алиаса контрола те, что нам нужны и если так, то добавляем флаг, который указывает автокомплиту работать в режиме диалога.

Для фильтров: проверяем, что фильтруется нужное представление и нужный параметр, добавляем флаг, который указывает автокомплиту работать в режиме диалога.

Пример работы (без диалога):

image4

Пример работы (с диалогом):

image5

11_customBPTile.ts

Пример расширения, которое добавляет тайл, доступный в карточках. Тайл запускает бизнес процесс.

При нажатии на тайл в "Info" карточки добавляется ключ .startProcess с значением WfResolution. После, карточка сохраняется и обновляется.

12_additionalApprovalCardUIExtension.ts

Пример расширения, используемого в типовом решении (только для изучения - не нужно собирать и запускать в приложении). Расширение добавляет возможность редактировать доп. исполнителей для согласующих. В расширениях есть пример скрытия/вывода блока, получения настроек контролов, изменения полей и секций карточки. При выборе согласующего из табличного автокомплита открывается блок с контролами для доп. исполнителей и загружаются доп. исполнители для выбранного сотрудника.

Пример работы:

image6

13_fileControlExtension.ts

Пример расширения файлового контрола. Расширение фильтрует файлы по имени, которые выводятся в файловом контроле. Расширение запрещает добавление файла с определенным именем.

14_intermediateState.ts

Пример расширения, в котором показывается, каким образом можно получить промежуточный стейт в цепочке экшенов.

15_hideTileExample.ts

Пример расширения, которое скрывает дефолтный тайл.

Дополнительно

Консоль разработчика

Чтобы открыть консоль разработчика - нажмите F12 На вкладке "Console" выводятся все ошибки во время выполнения js кода

image7

На вкладке "Network" выводятся все запросы к серверу. При нажатии на запрос будут выводиться данные ответа.

image8

Настройка сотрудников, создаваемых при входе для ADFS

Web-клиент поддерживает аутентификацию через ADFS, основанную на стандарте SAML 2.0. Настройка ADFS описана в руководстве по установке.

В настройке AddNewUserToRoles указывается список идентификаторов ролей, в которые сотрудник, создаваемый при логине при наличии флага CreateUserAfterAuthenticationIfNotExists:true, будет сразу же добавлен до загрузки рабочего места. По умолчанию прописан идентификатор роли "Все сотрудники".

Вы также можете написать серверное расширение, которое добавляет сотрудника в другие роли или выполняет другие настройки. Приведённое расширение есть в сборке Tessa.Extensions.Default.Server.

/// <summary>
/// Расширение добавляет сотрудников, которые были созданы при adfs аутентификации в ЛК, в роль "Все сотрудники".
/// </summary>
public sealed class ADFSAuthenticationPersonalRoleStoreExtension:
    CardStoreExtension
{
    private static readonly Guid AllUsersRoleID = Guid.Parse("7FF52DC0-FF6A-4C9D-BA25-B562C370004D"); // Все сотрудники

    private readonly IRoleRepository roleRepository;

    public ADFSAuthenticationPersonalRoleStoreExtension(IRoleRepository roleRepository)
    {
        this.roleRepository = roleRepository;
    }

    public override void BeforeCommitTransaction(ICardStoreExtensionContext context)
    {
        if (!context.Request.IsADFSAuthenticationResponseExists())
        {
            return;
        }

        var card = context.Request.Card;
        var section = card.Sections["PersonalRoles"];
        var userID = card.ID;
        var userName = section.Fields.Get<string>("FullName");

        var roleUserRecord = new RoleUserRecord
        {
            ID = AllUsersRoleID,
            RowID = Guid.NewGuid(),
            IsDeputy = false,
            RoleType = RoleType.Dynamic,
            UserID = userID,
            UserName = userName
        };

        this.roleRepository.Insert(roleUserRecord);
    }
}
// регистрация в классе Registrator, метод RegisterExtensions

extensionContainer
    .RegisterExtension<ICardStoreExtension, ADFSAuthenticationPersonalRoleStoreExtension>(x => x
        .WithOrder(ExtensionStage.AfterPlatform, 1)
        .WithSingleton()
        .WhenCardTypes(RoleHelper.PersonalRoleTypeID))
    ;

Добавление иконок-тегов к файлам

Для этого можно переопределить props компонента FilesControlFileItem.

У props есть свойство tags - это массив тегов. тег - это параметр с двумя полями className и style. style - хеш css параметров в стиле React. className - это строка с названием иконки в формате: ta icon-thin-XXX - где XXX - три цифры номера иконки в шрифте тессы, например, 015 (иконка "кнопка").

Для выравнивания иконки по высоте можно использовать параметр стиля lineHeight (line-height); значение 1.6 даст примерную середину по вертикали

import { businessLogic } from 'common/utility';

const ContractTypeId = '335f86a1-d009-012c-8b45-1f43c2382c2d';

businessLogic.addComponent({
  desc: {
    type: 'FilesControlFileItem'
  },

  modifyProps: (props) => {
    // нужный тип карточки
    if (props.UIContext.cardTypeId !== ContractTypeId) {
      return;
    }

    const tags = props.tags || [];
    const newTag = {
      className: 'ta icon-thin-015', // нужная иконка из набора тессы
      style: {
        lineHeight: 1.6, // выравниваем по высоте
        backgroundColor: 'antiquewhite' // цеет фона
      }
    };

    return {
      tags: [
        ...tags,
        newTag
      ]
    };
  }
});