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. MobX.js

Для управления состоянием приложения используется библиотека MobX.js (https://mobx.js.org/)

MobX добавляет возможность наблюдать за существующими структурами данных, такими как объекты, массивы и экземпляры классов. Т.е. используется концепция observer/observable.

Базовые элементы библиотеки это @observable, @computed, @action.

Пример класса с observable свойством:

class Example {
	@observable
	public id: string;
	@computed
	public get name(): string {
		return this.id + '_example';
	}
}

При использовании React, можно сделать компоненты реактивными, добавив декоратор @observer из пакета mobx-react:

@observer
class ExampleContent extends React.Component {
  render() {
	const { example } = this.props;
    return (
      <div>
        {example.id}
      </div>
    );
  }
}

@observer говорит React-компоненту реагировать на изменение данных. MobX гарантирует, что компоненты будут перерисовываться, когда это необходимо, но так же не больше, чем нужно. Для правильного реагирования компонента-наблюдателя, ему нужно передавать не внутренние свойства наблюдаемых объектов, а весь объект целиком, т.к. в первом случае будет передано лишь значения, изменение которого ни на что не повлияет.

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

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

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

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

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

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

npm install

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

4.1. VS Code

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

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

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

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

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

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

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

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

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

  • src/default – дефолтные расширения;

  • src/default/examples – примеры расширений;

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

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

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

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

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

npm run build

4.2.1. Добавление расширений

Для того чтобы добавить свои расширения необходимо создать новую папку рядом с default, где будет лежать код всех новых расширений. Добавить registrator.ts по аналогии с регистраторами из default. И включить его в bundle добавив путь в default/index.ts

import '../myExtensions/registrator';

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

Расширения для web-клиента очень похожи на расширения основного клиента.

  1. ApplicationExtension – расширения инициализации приложения.

  2. TileExtension – расширения для тайлов.

  3. CardUIExtension – расширения ui карточки.

  4. CardDeleteExtension, CardGetExtension, CardNewExtension, CardNewTemplateExtension, CardRequestExtension, CardStoreExtension – расширения сохранения, удаления, создания карточки.

  5. CardMetadataExtension – расширения метаданных.

  6. CardGetFileContentExtension – расширения получения контента файла.

  7. CardGetFileVersionsExtension – расширения получения списка версий файла.

  8. TreeItemExtension – расширения дерева представлений РМ.

  9. WorkplaceViewComponentExtension – расширения РМ.

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

По умолчанию примеры расширений отключены и не попадают в сборку. Для их включения необходимо открыть src/default/index.ts и убрать комментарий со строчки:

// import './examples/registrator';

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

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

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

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

npm run build

Собранный билд будет лежать в папке wwwroot/extensions.

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

1_hideBlockByRefValueUIExtension.ts

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

  • Скрываем\показываем элементы управления (блок, контрол)

  • Делаем рид-онли\редактируемыми элементы управления (контрол)

  1. Если поле Валюта не пустое, то блок с файлами становится видимым, контрол Автор скрывается, а контрол Контрагент становится только для чтения.

  2. Если поле Валюта пустое, то блок с файлами скрывается, контрол Автор становится видимым, а контрол Контрагент становится редактируемым.

2_changeFieldOrRowUIExtension.ts

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

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

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

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

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

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

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

3_tableSectionChangedUIExtension.ts

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

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

4_hideFormUIExtension.ts

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

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

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

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

5_hideTaskBlockUIExtension.ts

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

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

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

6_simpleCardTileExtension.ts

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

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

7_simpleViewTileExtension.ts

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

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

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

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

8_requestTileExtension.ts

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

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

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

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

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

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

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

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

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

9_additionalTableButtonUIExtension.ts

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

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

10_dialogModeAutocompleteUIExtension.ts

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

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

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

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

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

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

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

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

image4

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

image5

11_customBPTileExtension.ts

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

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

12_fileControlUIExtension.ts

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

13_hideTileExtension.ts

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

14_tableControlDoubleClickUIExtension.ts

Подписываемся на двойной клик в контроле таблицы в карточке "Автомобиль".

15_showFormDialogUIExtension.ts, 16_showCustomDialogUIExtension.tsx

По клику вызываем диалог с кастомной формой.

17_closeCardOnCompleteTaskExtension.ts

Закрывает карточку после завершения задания.

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

18_additionalMetaExtension.ts

На серверной стороне добавляем необходимые данные в мету через ServerInitializationExtension. На клиенте достаем эти данные по ключу из меты и добавляем в MetadataStorage.info. После инициализации приложения эти данные из MetadataStorage.info можно получить в любом расширении и использовать.

19_customThemeProp.ts

Добавляем дополнительное свойство в темы в расширении ApplicationExtension. В карточке автомобиля меняем фон для тестового контрола и ставим значения с учетом этого свойства из темы.

20_tabPanelButtonUIExtension.ts

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

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

Темы

Легкий клиент поддерживает файлы с настройками тем толстого клиента. Темы можно найти в папке themes в корневой папке. При необходимости в эту папку можно добавить кастомные темы. Они автоматически подхватятся сервером после перезапуска.

Фон

Легкий клиент также поддерживает дополнительные фоны, которые может выбирать пользователь. Для этого нужно добавить соответствующие файлы в папку ./wwwroot/images/tessa-wallpapers, и после добавить иконки для выбора в папку ./wwwroot/images/tessa-wallpapers/icons. Также, чтобы у пользователя появилась возможность выбирать дополнительный фон, необходимо добавить название файла с фоном в подходящие темы (свойство TessaWallpapers).

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

Чтобы открыть консоль разработчика - нажмите 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))
    ;

Отслеживание изменений свойств контролов и изменения массивов

Многие свойства контролов являются observable. Изменения этих свойств можно отслеживать через вызовы функции MobX. Например свойство selectedRow у контрола таблицы:

import { reaction } from 'mobx';
...
const disposer = reaction(() => grid.selectedRow, row => console.log(row));
...
disposer(); // необходимо вызывать когда реакция больше не нужна (например в finalized в CardUIExtension)

Похожий пример можно найти в расширении 9_additionalTableButtonUIExtension.ts в папке examples.

Свойство rows у грида тоже является observable и тут тоже можно применить reaction, но реакция может вызываться даже когда значения в массиве не поменялись, т.к. reaction по умолчанию сравнивает значения по ссылке. Чтобы этого избежать нужно переопределить функцию сравнения. Пример:

import { reaction } from 'mobx';
...
const disposer = reaction(
  () => grid.rows,
  rows => console.log(rows),
  {
    equals: (a: ReadonlyArray<GridRowViewModel>, b: ReadonlyArray<GridRowViewModel>): boolean => {
      // ... сравниваем два массива
    }
  }
);
...
disposer();

Есть другой способ: если есть возможность работать с табличной секцией карточки, то лучше использовать её. Для этого необходимо использовать CardRowsListener. Пример его использования есть в 3_tableSectionChangedUIExtension.ts в папке examples.

import { CardRowsListener } from 'tessa/cards';
...
const card = grid.cardModel.card;
const cardSection = card.sections.get('MyTableSection')!;
const rows = cardSection.rows;
const listener = new CardRowsListener();
listener.rowInserted.add(() => console.log(grid.rows));
listener.rowDeleted.add(() => console.log(grid.rows));
listener.start(rows);
...
listener.stop(); // необходимо вызывать когда listener больше не нужен (например в finalized в CardUIExtension)

Focus контролов при открытии карточки или формы

Метод focus() контролов карточки вызывает нативный метод focus() элемента. Поэтому для правильной работы метода, необходимо чтобы контрол уже был отрендерен в момент вызова. Поэтому если необходимо вызвать focus() при открытии карточки, то нужно вызывать его в CardUIExtension.contextInitialized. В этом методе расширения карточка уже будет отрисована и все контролы будут иметь ссылки на свои html элементы.

public contextInitialized(context: ICardUIExtensionContext) {
  const textBox = context.model.controls.get('MyTextBox') as TextBoxViewModel;
  textBox.focus();
}

Если необходимо вызвать при открытии формы контрола таблицы, то нужно вызывать его в GridViewModel.rowInitialized.

table.rowInitialized.add(e => {
  const textBox = e.cardModel.controls.get('MyTextBox') as GridViewModel;
  textBox.focus();
});