Перейти к основному содержимому
Версия: 7.0

Rules: custom views

Правила: выбор пользовательского представления​

  1. Для отображения данных ассистент ОБЯЗАН сначала рассматривать стандартные виды представления группы объектов: таблицу, сводную таблицу с её диаграммами (PIVOT), календарь (CALENDAR), карту (MAP). Пользовательское представление применяется, когда требуется что-то сверх простой таблицы, простого календаря, простой диаграммы или сводной таблицы: канбан-доска, расписание, лента карточек, схема рассадки, перетаскивание, которого нет в стандартных видах, нестандартная раскладка или интерактивность.

  2. Пользовательское представление — одно из трёх, и ассистент ОБЯЗАН выбирать по тому, что оно рисует:

    • опция CUSTOM свойства формы — функция отрисовки ОДНОГО значения ОДНОГО объекта (свой редактор, виджет для поля);
    • CUSTOM-представление группы объектов — пара JS-функций render / update над строками ОДНОЙ группы, с diff, фильтрами представления и размером страницы у контроллера;
    • контейнер в DESIGN с атрибутом custom — React-компонент (имя вида [A-Z][A-Za-z0-9_$]*), рисующий всё поддерево контейнера из проекции props.data, либо HTML-шаблон, который только размещает потомков, нарисованных платформой; таблица, тулбар, управляющие элементы фильтров или группа фильтров принимают React-компонент так же, и он рисует на их месте то, что этот компонент показывает из своей группы.

    Все три — только веб-клиент: десктоп-клиент отрисовывает стандартные виды и обычное поддерево контейнера. Перед созданием React-представления ассистент ОБЯЗАН получить документацию How-to_Custom_React_views; перед классическим — How-to_Custom_components_objects либо How-to_Custom_components_properties. Эти правила не описывают API компонента и НЕ ДОЛЖНЫ использоваться вместо тех статей.

Правила: путь данных​

  1. Пользовательское представление свойства — опция CUSTOM свойства формы или .value, которое компонент читает из props.data, — рисует ОДНО значение ОДНОГО объекта. Набор объектов ОБЯЗАН приходить на клиент группой объектов — таблицей (GRID), CUSTOM-представлением группы объектов или боксом группы, вложенным в React-контейнер (props.data.<g>.list), — и НЕ ДОЛЖЕН упаковываться в одно свойство JSON / JSONTEXT, построенное JSON FROM по классу. Такое свойство — одно значение: оно перечитывается и пересылается целиком при любом изменении данных, из которых построено, его размер ничем не ограничен, и платформа не может ни листать его (PAGESIZE, useSeekOnScroll), ни фильтровать на сервере (FILTERS и FILTERGROUP формы, фильтры setBooleanViewFilter / setDateIntervalViewFilter CUSTOM-представления), ни применить к его полям политику безопасности свойств — так что каждая колонка каждой строки попадает к каждому пользователю, которому доступно это одно свойство, какой бы ни была политика на свойствах, из которых оно построено.

    Экран из нескольких списков делится на несколько групп объектов, а там, где стандартные виды не подходят, — на несколько контейнеров custom, по одному на область экрана (правило 3), а не один компонент над одной выгрузкой всей базы. Группа, все строки которой доска или календарь показывают разом, получает PAGESIZE 0; список с неограниченным числом строк сохраняет страницу и следует за прокруткой через useSeekOnScroll.

    JSON FROM по объектам МОЖЕТ передаваться представлению только небольшой ограниченной выгрузкой — ряд диаграммы, сводка, последние N записей, ось доски — и тогда ОБЯЗАН быть ограничен явно: явное WHERE, ORDER ..., <объект> с TOP n, только те поля, которые представление рисует, длинный текст приведён к STRING[n], никакой почты, телефонов, свободного текста и других персональных данных, которых представление не показывает. Он НЕ ДОЛЖЕН быть основным путём данных списка, который пользователь просматривает.

  2. Значения ОБЯЗАНЫ попадать в представление отдельными свойствами формы, а изменения — возвращаться через контроллер свойство за свойством: строка группы несёт каждое свойство под его интеграционным именем, представление свойства работает со своим единственным значением (update(..., value) / controller.change(value)), а действие, выведенное на форму, запускается через его член, controller.<группа>.<действие>.exec(row) (React-компонент), либо через controller.form.exec('<действие>'), когда действие перечислено в CUSTOMS формы (представление свойства, у контроллера формы которого членов нет). Ассистент НЕ ДОЛЖЕН использовать JSON как транспорт ни в одну сторону — ни JSON FROM для сбора нескольких значений в одну колонку, ни JSON, собранный в компоненте и разбираемый на сервере через INPUT f = JSON DO IMPORT JSON FROM f для диспетчеризации событий. Когда представлению свойства нужно показать несколько значений, выбран не тот вид: ассистент переходит на CUSTOM-представление группы объектов или React-контейнер.

  3. Страница делится на контейнеры custom по областям — список, панель деталей, панель фильтров, корзина, — а раскладывают их обычные контейнеры DESIGN. Ассистент НЕ ДОЛЖЕН заворачивать форму целиком в один контейнер custom (MOVE PANEL; MOVE OBJECTS): компонент контейнера перерисовывается при любом изменении своей проекции, а композицию React другой модуль из DESIGN расширить не может. Зависимые друг от друга группы (категория → элемент → деталь) всё равно разделяются: фильтры нижестоящих групп и так следуют за текущим объектом вышестоящей. Один корневой компонент над всей страницей оправдан только для небольшой формы, которая меняется как единое целое, либо когда сама раскладка вычисляется из данных или её диктует библиотека интерфейса, и причина указана в коде. Контейнеру даётся только то, что он рисует, а часть группы проецируется туда, где стоит её компонент, — строки туда, где грид группы, панельное свойство туда, где само свойство: то, что другой контейнер показывает из текущей строки, добавляется панельным свойством под алиасом и переносится к нему (PROPERTIES(i) nameItem = name PANEL, MOVE PROPERTY(nameItem) — how-to, Страница из нескольких контейнеров), а свойство уровня формы, нужное двум контейнерам, добавляется дважды, под двумя алиасами, по одному в каждый.

  4. Отбор, порядок и итоги принадлежат форме, а не компоненту. Любое состояние интерфейса, от которого зависит, какие строки показаны, — выбранный раздел или закладка, категория, текст поиска, открытая запись, — ОБЯЗАНО принадлежать форме: текущим объектом группы (<group>.change(row)), когда запись или категория выбирается из списка, а иначе свойством формы (обычно DATA LOCAL), изменяемым через контроллер, а строки ОБЯЗАНЫ отбираться FILTERS / FILTERGROUP формы и упорядочиваться ORDERS по нему. Компонент НЕ ДОЛЖЕН фильтровать, искать, сортировать, считать или суммировать спроецированные строки и вычислять значение по двум строкам из них: количество или итог по строкам, прошедшим фильтр формы, — свойство (GROUP SUM 1 IF [ FILTER orders.o](Order o), how-to), заголовок — свойство, раздел строки в плоском списке — свойство строки. Только число уже прочитанных строк — это list.length. Раскладывать отобранные сервером строки по визуальным блокам по такому вычисленному сервером значению — отрисовка (bucketOf из how-to), и она допустима. В состоянии React живёт только состояние представления — раскрытая карточка, открытая панель, черновик ввода.

Правила: проекция и редактирование​

  1. В контейнере с атрибутом custom значения свойств платформа не показывает, а передаёт компоненту в проекции props.data, где значение-объект — числовой идентификатор. Объектные связи (assignedTo(s), customer(o)) там нужны именно в таком виде: по идентификатору представление раскладывает строки по ячейкам, сопоставляет строку со строкой другой группы — у группы с одним объектом пользовательского класса row.key строки численно равен идентификатору этого объекта — и записывает связь обратно через член свойства (.change(id, row)). Поэтому в таком контейнере ассистент МОЖЕТ добавлять на форму объектное свойство как есть — для логики компонента, а не для показа. Если связь показывается пользователю, её заголовок ДОЛЖЕН добавляться отдельной записью — композицией заголовка из правил view (captionStatus 'Статус' = caption(status(p))). Свойство с пометкой NOCUSTOM, с функцией отображения CUSTOM или с SELECT, заданными в FORM, рисует платформа, и запрет показывать идентификаторы действует для него как обычно.

  2. В контейнере с атрибутом custom значения рисует компонент, и он же решает, что редактируется: правка идёт через контроллер (член свойства, .change(value[, row])). Статическая пометка READONLY в проекцию props.data не попадает — туда приходит только зависящий от данных readOnly из READONLYIF, — но изменение помеченного свойства сервер отклоняет, и правка через контроллер молча не выполняется. Поэтому там READONLY помечаются свойства, которые представление не меняет, а свойство, которое оно меняет через контроллер, READONLY нести НЕ ДОЛЖНО. Свойство с пометкой NOCUSTOM, с функцией отображения CUSTOM или с SELECT, заданными в FORM, рисует платформа своим редактором, и правило о READONLY из правил view действует для него как обычно.

  3. Изменение через контроллер делается в сессии изменений формы, как правка в стандартной таблице: представление видит новое значение в следующей проекции сразу, а в базу оно попадает при применении формы. Результат, который пользователь должен видеть и дальше, ОБЯЗАН быть положен в свойство формы, а не храниться только в узлах DOM или в состоянии компонента: классический update, который перестраивает элементы по новому list, а не применяет изменение пошагово через controller.diff, теряет его при следующем изменении списка.

Правила: размер контейнера​

  1. Форме с контейнером custom, открываемой окном (FLOAT; у DIALOG — расположение по умолчанию), ассистент ОБЯЗАН задать контейнеру базовый размер — size = (w, h) либо отдельные атрибуты width и height: размер окна вычисляется по содержимому в момент открытия, когда компонент ещё ничего не нарисовал, поэтому без него окно с одним таким контейнером схлопывается до заголовка и системных кнопок, а нарисованное позже содержимое вытесняет кнопки OK / Закрыть за край окна. Для формы без таблиц и с умеренной высотой содержимого ассистент МОЖЕТ вместо этого не задавать базовый размер контейнеру, а задать size = (-1, -1) главному контейнеру самой формы: окно тогда не фиксируется и следует за содержимым (подробности в Form_design).

  2. Закладке (WINDOW) размер задаёт окно форм, но высоту самого контейнера базовый размер ограничивает и там: контейнеру, чей компонент рисует больше, чем помещается на форме, — ленте карточек, представлению с useSeekOnScroll, — ассистенту СЛЕДУЕТ задать базовую высоту (height), помещающуюся на форме: без неё контейнер растягивает форму, с ней расширяется в свободное место по коэффициенту расширения (fill) и прокручивает содержимое внутри себя.

Правила: живые данные​

  1. Данные, которые форма должна поддерживать актуальными, — котировки, очередь, монитор — обновляет событие SCHEDULE формы (EVENTS ON SCHEDULE PERIOD n formRefresh(), правила view), и платформа доставляет изменившееся через props.data, как любое другое изменение. Ассистент НЕ ДОЛЖЕН опрашивать сервер из пользовательского компонента React собственным таймером через член действия (controller.<действие>.exec()): действие, выведенное без NOWAIT, идёт синхронным запросом, блокирующим ввод во всём веб-клиенте на каждом тике, а таймер компонента продолжает опрос на скрытой форме — в фоновой закладке или на форме, которую компонент окна форм никуда не поместил. Событие по расписанию идёт асинхронным запросом и выполняется, только пока форма показана на экране. formRefresh[] перечитывает всю форму на каждом запуске, поэтому такую форму держат небольшой: обновления одной группы объектов нет — forceUpdate[STRING] лишь применяет накопленное обновление группы в режиме ручного обновления. Форма, обновляемая так, читает только то, что показано: строки вне текущего раздела, поиска или выбора исключают фильтры формы (правило 4 пути данных), а не прячет компонент, и представлению СЛЕДУЕТ читать данные через List, мемоизированные строки и useData, чтобы изменившееся значение не перерисовывало все строки представления.

Правила: вызовы сервера​

  1. Действие, нужное представлению, СЛЕДУЕТ выводить на форму и запускать по каналу редактирования формы, который не ограничен шлюзом: в React-компоненте — через член действия, controller.<группа>.<действие>.exec(row), который сверх строки ничего не передаёт, в классическом представлении группы объектов — через changeProperty с целевой строкой, который сверх неё передаёт только значение, запрашиваемое действием с запросом значения (changeProperty(action, row, value)). Как только нужно больше — значение из React-компонента, несколько значений, массив, — вызов делается через exec(...) / change(...) контроллера формы (props.controller в React-компоненте, controller.form в классическом представлении), и цель ОБЯЗАНА быть перечислена в блоке CUSTOMS формы.

  2. Ассистент ОБЯЗАН предпочитать CUSTOMS, а не @@api, для вызова, нужного пользовательскому представлению: CUSTOMS ограничивает доступ этой формой («пользователь может открыть форму» плюс явное перечисление), тогда как @@api заодно открывает действие или свойство во внешний HTTP API. @@api ставится только тогда, когда это действительно часть того API. CUSTOMS не ограничивает значения аргументов, которые передаёт компонент, поэтому каждая перечисленная запись ОБЯЗАНА оставаться безопасной при произвольном идентификаторе и произвольных значениях; eval / evalAction выполняют произвольный скрипт и остаются под шлюзом.

Правила: клиентский код​

  1. В проекте со сборкой браузерный код кладётся в src/main/web модуля логики (.js, .jsx, .ts, .tsx), каждый файл вне подпапки lib собирается в свой автоматически загружаемый бандл — lib держит общие вспомогательные модули, доступные только через импорт, — а компонент экспортируется именованным экспортом — тем именем, на которое ссылаются custom = '...' в DESIGN / CUSTOM '...'. Имя на window — лишь запасной вариант для существующих скриптов. Ассистент НЕ ДОЛЖЕН собирать свою копию React или ReactDOM: их предоставляет платформа, и импорты react / react-dom разрешаются в её сборку.

  2. Без сборки файл кладётся в src/main/resources/web: в web/init для автозагрузки — тогда он ОБЯЗАН быть независим от порядка загрузки, регистрироваться при загрузке и обращаться к другим библиотекам только при отрисовке или по событию, — либо вне его, с перечислением в onWebClientInit с целочисленным порядком, когда порядок важен (библиотека, которая должна загрузиться раньше использующего её компонента) или загрузка условна. Такой файл не может import-ировать локальные модули. .jsx преобразуется при выдаче, а файл .js пишет React.createElement через window.React.

  3. Стили: CSS-модули (Component.module.css) для собственных стилей компонента, встроенный style={{ ... }} для значений, вычисляемых из данных, и обычная глобальная таблица стилей — с префиксом пространства имён у классов — только для сторонних или намеренно глобальных стилей. Скомпилированный .css бандла загружается автоматически и НЕ ДОЛЖЕН регистрироваться в onWebClientInit второй раз.