Как стать автором
Обновить
44.43
lsFusion
Не очередной язык программирования

Релиз lsFusion 5.0 — новой версии самой декларативной платформы разработки в мире

Уровень сложностиСредний
Время на прочтение39 мин
Количество просмотров2.6K

Исторически платформа lsFusion долгое время разрабатывалась как платформа разработки бизнес-приложений. В современном же мире грань между бизнес-приложениями и веб-приложениями постепенно стирается, соответственно одной из основных целей последних версий lsFusion стало превращение ее в том числе в платформу разработки веб-приложений.

Для достижения этой цели в 5-й версии (как и в 4-й) гораздо больше внимания было уделено UI/UX, а не бизнес-логике. Так, существенно расширились возможности кастомизации пользовательского интерфейса, осовременился дизайн, асинхронность большинства процессов вышла на новый уровень и вообще произошло значительное улучшение многих метрик, критически важных при разработке любого современного веб-приложению. Впрочем, обо всем по порядку.

Справедливости ради стоит отметить, что версия вышла уже давно, и вот-вот выйдет 6-я. Но новые возможности и изменения 5 версии пока не были описаны в блоге. Исправляем ситуацию.

Оглавление

Кастомизация компонент

По умолчанию, в lsFusion каждое свойство на форме отображается при помощи специального предопределенного компонента, зависящего от типа свойства (для логических типов — галочка, для картинок — картинка, для строк, чисел — текстовое поле и т.п.). Тоже самое касается и контейнеров на форме: каждый контейнер в зависимости от некоторых его атрибутов отображается либо в виде flex-контейнера (с соответствующим значением display), либо в виде tabbed-контейнера (где отображается ровно один вложенный компонент в зависимости от выбора пользователя).

Для всех этих предопределенных отображений существует большое количество различных настроек, однако, часто для отображения данных необходимо все же использовать что-то более сложное и гибкое, как с точки зрения дизайна, так и с точки зрения интерактивности. В четвертой версии существовала всего одна возможность такой кастомизации — подмена отображения списка объектов целиком. Впрочем, такой механизм кастомизации часто слишком громоздок и неудобен, например, если надо подменить отображение только одного свойства. Также этот механизм не позволяет подменять отображения свойств, отображаемых для текущего объекта (в "панели"), а не в списке объектов.

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

Бесшовное подключение сторонних ресурсов

В предыдущих версиях платформы "бесшовно" (то есть на сервере приложений) подключались только самые "базовые" сторонние ресурсы — java-библиотеки (на сервере и десктоп-клиенте, причем не включая использование этих библиотек в отчетах). Все остальные сторонние ресурсы (sql-скрипты, java-библиотеки при использовании в отчетах, веб-ресурсы: js-скрипты, css, картинки и другие файлы) необходимо было подключать вручную (в том числе на клиентах, веб-серверах, серверах БД), что существенно усложняло администрирование/сопровождение систем, использующих такие ресурсы. В пятой версии все эти неудобства были устранены.

  • Подключение веб-ресурсов (js, css, картинок и других файлов)

    Практически все сторонние ресурсы в пятой версии подключаются при помощи соответствующих опций в операторе INTERNAL. Для веб-ресурсов (а точнее клиентских ресурсов) эта опция называется CLIENT и позволяет:

    1. Загрузить и выполнить javascript-код. Код может быть задаваться как:

    • путь к файлу на сервере приложений, содержащий этот код (должен иметь расширение js). В этом случае в момент вызова действия заданный файл добавляется в head с использованием тега script (то есть "выполняется"). PS. Здесь и далее предполагается, что путь к файлу задается относительно classpath'а JVM, в которой работает сервер приложений.

    • имя-javascript функции (предполагается, что она инициализирована в другом вызове INTERNAL CLIENT, или каким-нибудь другим образом). В этом случае в момент вызова действия вызывается заданная функция:

    
    onWebClientStarted() + {
        INTERNAL CLIENT 'my.js'
    }
    
    myAction() {
        INTERNAL CLIENT 'myFunc' PARAMS f() TO g();
    }
    
    1. Загрузить css. Задается как путь к файлу на сервере приложений с расширением css. В момент вызова действия заданный файл добавляется в head с использованием тега link.

    onWebClientStarted() + {
        INTERNAL CLIENT 'my.css';
    }
    
    1. Загрузить шрифт. Задается как путь к файлу на сервере с расширением ttf.

    2. Подключить для дальнейшего использования любой файл (например картинку). Также как и при загрузке css и js-файлов задается как путь к файлу на сервере (с расширением отличным от css и js). В момент вызова действия сгенерированный url доступа к заданному файлу записывается в объект lsfResources с ключом, равным пути к этому заданному файлу на сервере.

Пример
onWebClientStarted() + {
    INTERNAL CLIENT 'apply.png';
}

Использование в renderer'е:

var applyImage = document.createElement("img");
applyImage.src = window.lsfFiles["apply.png"];
element.appendChild(applyImage);

Как видно в примерах выше, основным вариантом использования оператора INTERNAL CLIENT является загрузка различных веб-ресурсов на страницу именно в момент старта клиента (для дальнейшего использования в кастомизированных отображениях). Соответственно, чтобы упростить этот вариант использования, в платформе поддерживается предопределенная папка onStarted. Содержимое этой папки автоматически подключается в момент старта клиента (здесь можно посмотреть код системного модуля, делающего это), поэтому для загрузки файла на клиенте достаточно просто положить его в подпапку onStarted (так, чтобы она была доступна в classpath). Впрочем, такой подход не очень модулен (так как файлы загружаются вне зависимости от подключенных модулей), поэтому его рекомендуется использовать только в монолитных "custom-made" проектах.

  • Подключение sql-скриптов

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

    Отметим, что в четвертой версии такая функция была в операторе EXTERNAL SQL (если в качестве строки подключения указать значение LOCAL), но в операторе EXTERNAL в качестве строки подключения нельзя было обращаться к файлам проекта. К тому же, архитектурно оператор EXTERNAL — это все-таки оператор взаимодействия с внешними, а не внутренними, системами, поэтому функционал работы с внутренней БД и "переехал" в опцию INTERNAL.

// Если в строке команды имя файла, то платформа 
//считает команду, содержащуюся в этом файле, и выполнит ее на сервере БД
onStarted() + {
      INTERNAL DB 'myscript.sql'; 
}

Так же, как и для веб-ресурсов, основным вариантом использования INTERNAL DB является загрузка различных sql-функций в БД в момент старта сервера приложений (для дальнейшего использования в операторе FORMULA). Поэтому, так же как для веб-ресурсов, в платформе поддерживается автоматическая загрузка всех файлов с расширением .sql из папки onStarted.

Впрочем, кроме загрузки sql-функций, оператор INTERNAL DB можно использовать для других действий, например, построения своих индексов, чтения данных из БД в обход платформы и т.п.

Оператор JSON

В 4-й версии платформы уже существовал оператор работы с форматом JSON — оператор EXPORT. Однако этот оператор является оператором создания действия, а не свойства, что делает невозможным его использование при работе с формами (например, в блоке PROPERTIES). При этом именно при работе с формами создание свойств со значением типа JSON очень важно, этот тип крайне удобно использовать для передачи данных функциям "императивной" отрисовки кастомизированных компонент в веб-клиенте, и, соответственно, построения более красивых и эргономичных интерфейсов (о новых возможностях кастомизированной отрисовки чуть позже).

К счастью, современные СУБД вполне неплохо поддерживают работу с JSON (это важно, так как для работы со свойствами используются сервера БД, то есть SQL, в то время как для работы с действиями — сервера приложений, то есть Java/JVM), а значит и реализовать аналог оператора EXPORT для свойств оказалось не так сложно. Новый оператор называется JSON и имеет как синтаксис, так и результат, аналогичный оператору EXPORT:

export(A a) {
    EXPORT f OBJECTS o = a FILTERS;
}
export(A a)  = JSON (f OBJECTS o = a FILTERS);

Так же, как и для оператора EXPORT, поддерживается "плоский" вариант оператора:

export() {
    EXPORT FROM f(a), g(a) WHERE h(a);
}
export() = JSON FROM f(a), g(a) WHERE h(a);

Более того, так как оператор JSON можно использовать в выражениях, это во многих случаях позволяет сложные JSON без создания формы (как это приходится делать в операторе EXPORT):

export() = JSON FROM date(Invoice i), number(i), 
                     lines = (JSON FROM sku(InvoiceDetail id), quantity(id), 
             price(id) WHERE invoice(id) = i) 
             WHERE date(i) > subtractDays(currentDate(),3)

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

Интерполяция строк

В 4-й версии платформы единственной возможностью подстановки в строковом литерале (обычно это называется термином "строковая интерполяция" (string interpolation)) была локализация строковых данных, например:

company '{Company}' = DATA Company (Invoice);

В этом примере заголовок свойства company задан идентификатором, а реальный отображаемый заголовок зависит от локали пользователя. Синтаксис такой подстановки — идентификатор, заключенный в фигурные скобки: {id}.

В 5-й версии платформы возможности строковой интерполяции были значительно расширены.

  1. Подстановка выражений (${expression}) позволяет подставить в строку значение некоторого выражения на языке lsFusion. Например, рассмотрим свойство, возвращающее строку с описанием количества голов в игре. Вместо

goals(Game game) = 'Number of goals = ' + (hostGoals(game) (+) guestGoals(game)) + '.';

теперь мы можем написать так:

goals(Game game) = 'Number of goals = ${hostGoals(game) (+) guestGoals(game)}.';

В одном литерале может быть сколько угодно таких подстановок, и они также могут использоваться одновременно с локализацией:

descr(Game g) = '{description.prefix} ${hostTeam(g)} ${hostGoals(g)} : ${guestGoals(g)} ${guestTeam(g)}';
  1. Подстановка файла ($I{filename}) позволяет подставить в строку содержимое указанного файла ресурсов.

  2. Подстановка web-ссылки на ресурс ($R{resourceName})

Декларативная кастомизация компонент (через HTML + интерполяцию строк)

Итак, вернемся наконец непосредственно к кастомизации компонент. Самым простым способом сделать это в 5-й версии является задание отображения компоненты в виде некоторого HTML-кода. Естественно, внутри этого кода часто необходимо использовать данные из системы, для чего, в свою очередь, очень удобно использовать описанную выше интерполяцию строк.

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

Кастомизация свойств (тип HTMLTEXT)

Для того, чтобы изменить отображение свойств на кастомизированное, достаточно обеспечить, чтобы его тип был равен HTMLTEXT (например, преобразовав к нему строку). В этом случае платформа при отображении вставляет строку текста в элемент (в innerHTML) как есть, без каких-либо экранирований (как в случае обычных типов STRING/TEXT). Отметим, что для того, чтобы не заниматься экранированием в строковых литералах в lsFusion, самый удобный метод использовать в строке HTML-код — это использование конструкции $I{} строковой интерполяции. Так HTML-код можно поместить в отдельный файл (например my.html), а, соответственно, при добавлении свойства на форму использовать строковый литерал, содержащий "подстановку" этого файла:

PROPERTIES 'Order'=HTMLTEXT('$I{my.html}')

Отметим, что внутри самого файла html для "подстановки" данных можно (и нужно) использовать конструкцию ${}, где можно указывать любые выражения.

Пример

LSF-код:

colorText(STRING green, STRING blue, BOOLEAN bold) = HTML('$I{color-text.html}');
colorText() = colorText('greenText', 'blueText', TRUE); 

FORM colorText
    PROPERTIES() colorText
;

run() {
    SHOW colorText DOCKED;
}

color-text.html:

<div style="font-weight: ${IF bold THEN 'bold' ELSE 'normal'}">
    <span style="color: green">${green}</span>
    <span style="color: blue">${blue}</span>
</div>

Может показаться, что такую кастомизацию можно использовать только для создания статичных отображений (то есть без какой-либо интерактивности). Но это не так. Сам HTML позволяет в элементах задавать обработчики различных событий. В то же время в клиентской среде существует глобальный предопределенный объект $controller, который, в частности, предоставляет интерфейс для вызова различных "серверных" событий (например, события изменения CHANGE). Таким образом можно легко создавать практически любые сценарии взаимодействия с пользователем.

Пример

LSF-код:

FORM myForm1
    OBJECTS o = CustomUser
    PROPERTIES(o) name
;

FORM myForm2
    OBJECTS o = CustomUser
    PROPERTIES(o) login
;

FORM myForm
    OBJECTS myo = CustomUser
    PROPERTIES 'Order'=HTMLTEXT('$I{my.html}') ON CHANGE {
        LOCAL type = INTEGER ();
        INPUT j = JSON DO { // "считывает" параметр указанный при вызове события
            IMPORT JSON FROM j FIELDS() INTEGER type DO { // "разбирает" json
                IF type = 1 THEN {
                    SHOW myForm1 OBJECTS o = myo;
                }

                IF type = 2 THEN {
                    SHOW myForm2 OBJECTS o = myo;
                }
            }
        }
    }
;

run() {
    SHOW myForm;
}

HTML - код (my.html)

<button onClick="lsfController(this).change({type : 1})"> TYPE 1 </button>
<button onClick="lsfController(this).change({type : 2})"> TYPE 2 </button>

Кастомизация контейнеров (атрибут custom)

По аналогии с кастомизацией отображений свойств, в пятой версии можно кастомизировать отображение контейнеров. Для этого надо в атрибуте контейнера custom указать необходимый HTML-код отображения. Этот код задается как некоторое выражение, значение которого должно быть строкового типа. Как правило, таким выражением является строковый литерал, в котором, также как и при кастомизации отображений свойств, можно использовать интерполяцию строк. При этом, также как и для некоторых других атрибутов компонент, в выражении можно обращаться к объектам формы, и, естественно, любым свойствам логики.

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

<div> 
    <div>
            [PROPERTY(f(a))]
            [PROPERTY(g(a))]
    </div>
</div>

Отметим, что при использовании кастомизированных отображений контейнеров существует две стратегии:

  • Кастомизировать всю форму целиком - задав custom для одного корневого контейнера (то есть контейнера всей формы), а внутри использовать только базовые компоненты (PROPERTY, FILTER и т.п.). Такой подход хорош тем, что можно использовать готовые snippet-ы, просто подставляя туда нужные данные (при этом эти подставленные данные будут "живыми" — с настройками политики безопасности, "серверными" событиями, механизмами ввода, переходом по ссылке и т.п.).

  • Разбивать дизайн формы на lsFusion контейнеры, внутри каждого из которых или опять-таки использовать атрибуты custom/class или использовать готовые контейнеры из дизайна по умолчанию. Такой подход хорош тем, что:

    • может быть менее трудоемким, так как используются автоматически созданные контейнеры (то есть их не надо создавать вручную);

    • как правило, лучше оптимизируется, например:

      • если в атрибутах custom/class используются изменяемые на форме свойства (тогда обновляется только один небольшой контейнер),

      • при использовании стандартных lsFusion tabbed или collapsible контейнеров, а также контейнеров с условной видимостью (так как все эти контейнеры следят за видимостью свойств и передают эту информацию на сервер, что позволяет платформе не читать эти свойства на сервере, если они не видимы);

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

Императивная кастомизация компонент (через Javascript + JSON)

Описанные выше кастомизированные компоненты (в частности отображения свойств) удобно задавать, но, в текущей реализации, DOM внутри элементов их отображения перестраивается полностью (старый DOM удаляется и рендерится новый). Это имеет сразу несколько недостатков:

  • Пониженная производительность, как в плане необходимости парсинга HTML-кода браузером, так и в плане затрат на пересоздание DOM.

  • Потенциальные проблемы с фокусом и ссылками на элементы (опять-таки из-за пересоздания DOM).

  • Недостаточная гибкость как в плане интерактивности (которая, по сути, сводится только к возможности добавить обработчик события элемента), так и в плане подключения сторонних библиотек (которые создают DOM программно).

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

Для того, чтобы избавиться от описанных выше недостатков, в 5-й версии поддерживается и другой способ задания кастомизированных компонент — императивно при помощи javascript/JSON.

Кастомизация свойств (опция CUSTOM)

В пятой версии для свойства на форме появилась возможность задать опцию CUSTOM, подменяющую отображение этого свойства на форме. В этой опции нужно задать имя функции отображения (которая может быть загружена на страницу, например, при помощи описанного выше оператора INTERNAL CLIENT), которая в свою очередь, должна/может иметь три поля-функции:

  • render (element) — вызывается, когда в элементе (element) необходимо отобразить заданное свойство на форме (далее будем называть его элементом отображения). Предполагается, что функция render должна подготовить DOM в элементе отображения для последующей записи туда значения;

  • update (element, value, controller) — вызывается, когда в элементе отображения (element) надо отобразить заданное значение (value);

  • clear (element) — вызывается, когда элемент отображения необходимо "очистить". По умолчанию все созданные дочерние элементы этого элемента удаляются автоматически, поэтому достаточно очищать только те изменения, которые делались непосредственно в самом элементе отображения (например, добавлялись какие-нибудь классы и т.п.).

Пример

LSF-код

PROPERTIES f(x) CUSTOM 'threeStateCheckbox'

Javascript-код

function threeStateCheckbox() {
    var inp;
    return {
        render: function( element ) {
            element.setAttribute( 'customType', 'threeStateCheckbox' );
            inp = document.createElement( 'input' );
            inp.setAttribute( 'type', 'checkbox' );
            element.append(inp);
        },
        update: function( element, controller, value ) {
            if ( !inp.onchange ) {
                inp.onchange = () => controller.changeValue((inp.checked ? inp.checked : null));
            }
            if ( value || value == null ) {
                inp.checked = value;
                inp.indeterminate = false;
            } else {
                inp.indeterminate = true;
            }
        },
         clearRender : function(element) {
	         //do something to clear
         }
    }
}

PS. В принципе, никто не мешает всю работу по обновлению DOM/заполнению данных делать исключительно в методе update (то есть вручную проверять текущее состояние DOM, или всегда пересоздавать DOM), но с разбиением на render и update код получается красивее и читабельнее (и часто производительнее).

Отметим, что часто для отображения свойств нужно не одно значение, а сразу несколько (или даже массив объектов). Для этого, как уже упоминалось выше, очень удобно использовать тип и оператор JSON.

Пример

LSF-код

onWebClientLoad() + {
    INTERNAL CLIENT 'chat.js';
    INTERNAL CLIENT 'chat.css';
}

json (Message m) = JSON FROM
        author = nameAuthor(m),
        time = dateTime(m),
        text = text(m),
        own = IF own(m) THEN 1 ELSE 0,
        replyAuthor = nameAuthorReplyTo(m),
        replyText = textReplyTo(m),
        replyMessage = replyTo(m),
        id = m,
        attachment = attachmentName(m),
        status = status(m);


EXTEND FORM fullChat
    PROPERTIES(m) '' = json CUSTOM 'chatMessageRender' SHOWIF isWeb()

Javascript-код

function chatMessageRender() {
    return {
        render: function (element) {
            var message = document.createElement("div")
            message.classList.add("chat-message");
            message.classList.add("ql-bubble");

            var header = document.createElement("div");
            header.classList.add("chat-header");

            var author = document.createElement("div");
            author.classList.add("chat-author");

            element.author = author;
            header.appendChild(author);

            var replyAction = document.createElement("a");
            replyAction.classList.add("chat-reply-action");

            var replyCaption = document.createTextNode("Reply");
            replyAction.appendChild(replyCaption);

            element.replyAction = replyAction;
            header.appendChild(replyAction);

            message.appendChild(header);

            var replyContent = document.createElement("div");
            replyContent.classList.add("chat-reply-content");

            var replyAuthor = document.createElement("div");
            replyAuthor.classList.add("chat-reply-author");

            element.replyAuthor = replyAuthor;
            replyContent.appendChild(replyAuthor);

            var replyText = document.createElement("div");
            replyText.classList.add("chat-reply-text");

            element.replyText = replyText;
            replyContent.appendChild(replyText);

            element.replyContent = replyContent;
            message.appendChild(replyContent);

            var text = document.createElement("div");
            text.classList.add("chat-text");
            text.classList.add("ql-editor");

            element.text = text;
            message.appendChild(text);

            var attachments = document.createElement("div");
            attachments.classList.add("chat-attachments");

            element.attachments = attachments;
            message.appendChild(attachments);

            var footer = document.createElement("div");
            footer.classList.add("chat-footer");

            var time = document.createElement("div");
            time.classList.add("chat-time");

            element.time = time;
            footer.appendChild(time);

            var status = document.createElement("div");
            status.classList.add("chat-status");

            element.status = status;
            footer.appendChild(status);

            message.appendChild(footer);

            element.message = message;
            element.appendChild(message);
        },
        update: function (element, controller, value) {
            element.author.innerHTML = value.author || '';

            element.replyAction.onclick = function(event) {
                controller.change({ action : 'reply' });
 $(this).closest("div[lsfusion-container='chat']").find(".ql-editor").focus(); // works only in firefox
            }

            element.replyAuthor.innerHTML = value.replyAuthor || '';
            element.replyText.innerHTML = value.replyText || '';
            element.replyContent.onmousedown = function(event) {
                controller.change({ action : 'goToReply' });
            }

            element.text.innerHTML = value.text || '';
            element.time.innerHTML = value.time || '';
            element.status.innerHTML = value.status || '';

            while (element.attachments.lastElementChild) {
     element.attachments.removeChild(element.attachments.lastElementChild);
            }
            if (value.attachment) {
                var attachmentA = document.createElement("a");
                attachmentA.classList.add("chat-message-attachment");

                attachmentA.onclick = function(event) {
                    controller.change({ action : 'open', id : value.id });
                }

                var attachmentCaption = document.createTextNode(value.attachment);
                attachmentA.appendChild(attachmentCaption);

                element.attachments.appendChild(attachmentA);
            }

            if (value.own) {
                element.message.classList.add('chat-message-own');
            } else
                element.message.classList.remove('chat-message-own');
        }
    }
}

В случае, если необходимо не только как-то хитро отобразить свойство, но и обеспечить интерактивность этого отображением, третьим параметром в функции update передается объект controller. У этого объекта есть много различных вспомогательных функций изменения состояния, но основной и наиболее часто используемой из них является функция change. Эта функция вызывает обработку события CHANGE отображаемого свойства на форме, при этом, если внутри этой обработки есть оператор INPUT, то в качестве его результата используется переданный в функцию change параметр.

Пример
changeInputMessage () {
    INPUT f = JSON DO {
        IMPORT JSON FROM f FIELDS() STRING action, TEXT value, STRING name, STRING data DO {
            IF action = 'replyRemove' THEN {
                replyTo() <- NULL;
            }
    
            IF action = 'change' THEN {
                message() <- value;
            }
    
            IF action = 'open' THEN {
                open(attachment(), attachmentName());
            }
    
            IF action = 'remove' THEN {
                attachment() <- NULL;
                attachmentName() <- NULL;
            }
        }
    }
}


EXTEND FORM fullChat
    PROPERTIES(m) '' = json CUSTOM 'chatMessageRender' SHOWIF isWeb() ON CHANGE
            changeInputMessage()

Кастомизация редактирования свойств (опция CUSTOM в INPUT)

Для редактирования свойств в lsFusion, как правило, используется оператор INPUT. Также как и при отображении свойств, для ввода значения в этом операторе используется специальная предопределенная компонента, зависящая от заданного в этом операторе типа. И, также как и при отображении свойств, иногда бывает необходимо кастомизировать эту компоненту.

Синтаксис кастомизации компоненты редактирования, как и требования к ее javascript-функции, во многом схожи с синтаксисом и требованиями к javascript-функции в кастомизированных отображениях свойств:

  • опция называется CUSTOM и тоже требует задание javascript-функции

  • javascript-функция должна возвращать объект со следующими полями-функциями:

    • render (element, controller, value) — вызывается, когда в элементе (element) необходимо начать редактирование (далее будем называть его элементом редактирования). Предполагается, что функция render должна подготовить DOM в элементе отображения и, при необходимости, отобразить заданное значение (value). Для завершения ввода и ряда других действий, можно/нужно использовать контроллер (controller).

    • clear (element, cancel) — вызывается, когда редактирование завершается. Параметр cancel позволяет определить, завершилось ли редактирование применением изменений (например, пользователь кликнул по другому элементу) или их отменой (например, пользователь нажал Escape).

Пример

Файл custom-input.js:

function customInput() {
    return {
        render: function (element, controller) {
            let randomButton = document.createElement("button")
            randomButton.innerText = 'Generate random'
            randomButton.onclick = function() {
                let random = Math.floor(Math.random() * (99999999 - 11111111 + 1)) + 11111111;
                controller.commit(random.toString())
            }
            element.appendChild(randomButton);
        }
    }
}

LSF-код:

onWebClientInit() + {
    onWebClientInit('custom-input.js') <- 1;
}

input() {
    INPUT random = STRING CUSTOM 'customInput' DO {
        MESSAGE(random);
    }
}

FORM customInput 
    PROPERTIES() input
;

run() {
    SHOW customInput DOCKED;
}

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

Пример
function googleAutocomplete() {
    return {
        renderInput: (element, controller) => {
            if (lsfParams.googleMapsAPILoaded) {
                let autocompleteOptions = {
                    types: ['address'],
                    componentRestrictions: {
                        country: lsfParams.googleMapAutocompleteCountry
                    }
                };

                // it's tricky here, we don't use onSelectedEvent, but use side effect that autocomplete is rendered outside element
                // so blur happens on that selection event, which eventually leads to commit. But blur happens before value is set, so we defer onBlur
                controller.setDeferredCommitOnBlur(true);

                new google.maps.places.Autocomplete(element, autocompleteOptions);
            } else {
                let tooltipElement = document.createElement('tooltip')
                tooltipElement.style.setProperty("position", "fixed");
                tooltipElement.style.setProperty("color", "red");
                tooltipElement.style.setProperty("font-weight", "bold");

                let firstLineText = document.createTextNode("Google API key does not set");
                let secondLineText = document.createTextNode("Autocomplete is not available");
                let thirdLineText = document.createTextNode("Contact your administrator");
                tooltipElement.appendChild(firstLineText);
                tooltipElement.appendChild(document.createElement("br"));
                tooltipElement.appendChild(secondLineText);
                tooltipElement.appendChild(document.createElement("br"));
                tooltipElement.appendChild(thirdLineText);

                element.onmouseover = function (event) {
                    removeTooltipElement(tooltipElement);

                    tooltipElement.style.top = (event.pageY + 10) + 'px';
                    tooltipElement.style.left = (event.pageX + 10) + 'px';
                    document.body.appendChild(tooltipElement);
                }

                element.onmouseout = function () {
                    removeTooltipElement(tooltipElement);
                };

                element.onkeypress = function () {
                    removeTooltipElement(tooltipElement);
                };
            }
        },
        clear: (element, cancel) => {
            // remove autocomplete elements from <body>. https://stackoverflow.com/questions/33049322/no-way-to-remove-google-places-autocomplete
            $(".pac-container").remove();
        }
    };
}

LSF-код

CLASS AutocompleteTest;
address = DATA LOCAL STRING(AutocompleteTest);

FORM autocomplete
    OBJECTS o = AutocompleteTest
    PROPERTIES(o) address ON CHANGE { INPUT sl = STRING[150] CUSTOM 'customGoogleAutocomplete' DO address(o) <- sl; } PANEL
;

DESIGN autocomplete {
    NEW test {
        MOVE PROPERTY (address(o)) {
            charWidth = 60;
        }
    }
}

Для кастомизации редактирования свойств также можно использовать новую возможность платформы — отображение диалоговых форм в поле редактирования свойства (но об этом позже).

Предопределенные кастомизации свойств и списков объектов

Очевидно, что вариантов возможных кастомизированных компонент существует очень много, и встраивать их все в платформу на уровне языка не очень эффективно. В то же время существует набор довольно часто используемых компонент, искать "на стороне" и подключать которые при разработке каждого нового проекта также не очень удобно. Поэтому, начиная с пятой версии, в платформе появился некоторый набор "предопределенных" кастомизируемых отображений (встроенных), для их использования достаточно:

  • указать функцию в опции CUSTOM

  • обеспечить, чтобы имена свойств на форме (для отображений списков), или тип/структура JSON свойства (для отображения свойств) совпадали с требуемыми.

address = DATA STRING[500];
FORM route 'Маршрут'
    PROPERTIES () address ON CHANGE { INPUT sl = STRING[500] CUSTOM 'googleAutocomplete' DO address() <- sl; }
;

Пока список таких предопределенных кастомизируемых компонент не сильно большой, например:

  • googleAutocomplete - ввод адреса с автоподстановкой из google-карт

  • и так далее

Но с каждой версией этот набор, естественно, будет пополняться (правда, не до конца понятно, будет ли он встроен в платформу, или оформлен в отдельный репозиторий).

Больше адаптивности, интерактивности и красоты в дизайне

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

  • B2B приложения гораздо более сложно функциональны, постоянно изменяются и зачастую требуют модульности (когда разным заказчикам нужны отдельные блоки функционала). Это все приводит к тому, что дизайн системы крайне "нестабилен" и должен быстро адаптироваться под изменения, причем желательно автоматически (без разработчика).

  • Так как в B2B круг пользователей ограничен "своими" людьми (мнение которых менее важно, чем, скажем, бюджеты и сроки разработки), то на первый план выходят утилитарные, а не эстетические характеристики интерфейса. Другими словами, заказчик гораздо меньше готов платить за дизайн, чем за надежность, скорость разработки, порог вхождения и т.п.

  • Основные устройства для B2B — это десктоп компьютеры, а не мобильные устройства, причем число таких компьютеров ограничено и, как правило, имеет дополнительное оборудование. Плюс у этих десктоп компьютеров основным механизмом ввода является клавиатура, что также сильно влияет на требования к интерфейсу.

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

  • построение компонент (как контейнеров, так и базовых компонент) на основе бизнес-логики,

  • расположение этих компонент,

  • адаптивность этого расположения под разрешение экрана.

"Автоматичность" последних двух механизмов сводится к тому, что расположение компонент (layouting) должно быть content-first (то есть определяться содержимым компонент), а не layout-first (определяться заранее дизайнером, как, например, это предполагается в Bootstrap). Помимо "автоматичности" вышеобозначенных процессов в B2B приложениях также важную роль играет возможность их "пользовательской настройки" (например, если "автоматичность" сработала неправильно и/или реальный размер конкретных данных не соответствует размеру отображающего их компонента, пользователь должен иметь возможность расширить такую компоненту).

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

Поддержка атрибутов контейнера wrap, shrink

Ключевыми способами обеспечить адаптивность интерфейса в браузере (и css, в частности) являются использование css-атрибутов flex-wrap и flex-shrink. Первый атрибут позволяет переносить элементы на новую строку, если в старую строку они не помещаются. Второй атрибут позволяет уменьшать элемент если он не помещается в контейнер (в противовес flex-grow, который увеличивает элемент). Соответственно, в lsFusion появилась поддержка обоих этих атрибутов.

prop1 = DATA STRING[500]();
prop2 = DATA STRING[500]();

FORM wrapAndShrink
    PROPERTIES() prop1, prop2

DESIGN wrapAndShrink {
    NEW container {
        horizontal = TRUE;
        wrap = FALSE; // по умолчанию TRUE
        MOVE PROPERTY(prop1());
        MOVE PROPERTY(prop2());
    }
}

PS. Отметим, что для обеспечения адаптивности интерфейса существует другой подход — так называемые breakpoint'ы (используется в Bootstrap). В этом подходе для каждого контейнера при помощи css-классов можно задавать разное число колонок в зависимости от разрешения всего экрана. Такой способ позволяет строить более "красивые" интерфейсы для каждого разрешения, но этот подход по своей сути является layout-first подходом, а значит, требует дополнительной работы дизайнера, что для бизнес-приложений не всегда оправдано. Впрочем, никто не мешает использовать и этот подход в lsFusion путем задания дополнительных css-классов компонентам формы и/или использовать кастомизированные представления контейнеров, описанные выше.

Изменение размеров компонент пользователем

В 4-й версии, для того чтобы пользователю дать возможность изменять размеры компонент в контейнере, необходимо было задавать этому контейнеру специальный тип - SPLIT. В 5-й версии в этом нет необходимости — пользователь может по умолчанию расширять/сужать любые компоненты. Также все компоненты в 5-й версии lsFusion автоматически стали скроллируемыми (соответственно, и тип контейнера SCROLL также стал не нужным).

Сворачивание содержимого контейнера пользователем

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

Также, как и с tabbed контейнерами, важной особенностью сворачивания является то, что, если контейнер свернут, его данные не только перестают пересылаться с сервера на клиент, а даже перестают читаться на самом сервере (то есть по сути "вырезаются" из соответствующих SQL-запросов).

Поддержка атрибутов контейнера grid, alignCaptions

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

Для того, чтобы выровнять компоненты и по второму направлению, в 5-й версии появился специальный атрибут grid, задав который равным TRUE, компоненты будут выравнены в обоих направлениях:

property1 = DATA STRING[50]();
pro2 = DATA STRING[40]();
p3 = DATA STRING[30]();
prop4 = DATA STRING[20]();
prop5 = DATA STRING[10]();

FORM grid
    PROPERTIES() property1, pro2, p3, prop4, prop5;

DESIGN grid {
    NEW container {
        horizontal = TRUE;
        lines = 2;
        grid = TRUE;
        
        MOVE PROPERTY(property1());
        MOVE PROPERTY(pro2());
        MOVE PROPERTY(p3());
        MOVE PROPERTY(prop4());
        MOVE PROPERTY(prop5());
    }
    MOVE TOOLBARBOX;
}

Впрочем, выравнивать компоненты по обоим направлениям не такая уж и частая необходимость, во всяком случае, по сравнению с необходимостью выравнивать заголовки свойств, когда эти свойства находятся в вертикальном контейнере (и у них разная ширина). Соответственно, для поддержки такой возможности в 5-й версии появился специальный атрибут alignCaptions. Если задать этот атрибут равным TRUE, заголовки всех свойств в контейнере выделяются в отдельную колонку таблицы (при этом, если этот контейнер не таблица, то есть grid = FALSE, то он автоматически предварительно "преобразуется" к таблице, состоящей из одной колонки).
alignCaptions = FALSE

property1 = DATA STRING[50]();
pro2 = DATA STRING[40]();
p3 = DATA STRING[30]();
prop4 = DATA STRING[20]();
prop5 = DATA STRING[10]();

FORM grid
    PROPERTIES() property1, pro2, p3, prop4, prop5;

DESIGN grid {
    NEW container {
        lines = 2;  
        alignCaptions = FALSE;     
        MOVE PROPERTY(property1());
        MOVE PROPERTY(pro2());
        MOVE PROPERTY(p3());
        MOVE PROPERTY(prop4());
        MOVE PROPERTY(prop5());
    }
    MOVE TOOLBARBOX;
}

alignCaptions = TRUE (по умолчанию)

property1 = DATA STRING[50]();
pro2 = DATA STRING[40]();
p3 = DATA STRING[30]();
prop4 = DATA STRING[20]();
prop5 = DATA STRING[10]();

FORM grid
    PROPERTIES() property1, pro2, p3, prop4, prop5;

DESIGN grid {
    NEW container {
        lines = 2;
        MOVE PROPERTY(property1());
        MOVE PROPERTY(pro2());
        MOVE PROPERTY(p3());
        MOVE PROPERTY(prop4());
        MOVE PROPERTY(prop5());
    }
    MOVE TOOLBARBOX;
}

Поддержка авто-расширения и одиночных границ

В lsFusion, для того чтобы отделить на форме одни данные от других, используются именованные контейнеры. В 4-й версии все такие контейнеры рисуются как прямоугольники (с заголовком), что приводило к появлению множественных (двойных, тройных) рамок, что с точки зрения дизайна выглядело весьма громоздко:

В 5-й версии рисование границ перешло с уровня компоненты (когда компонента оборачивает сама себе), на уровень ее родительского контейнера (то есть контейнер разделяет компоненты между собой). Кроме того, в 5-й версии платформа автоматически пытается делать именованные контейнеры расширяемыми (если это возможно), чтобы избегать "оборванных" линий (когда линия не дотягивается до края верхнего контейнера):

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

Для реализации сложных сценариев редактирования свойств на форме в lsFusion существует две возможности:

  1. использовать кастомизированные отображение и/или редактирование свойств описанные выше;

  2. создать новую форму и вызвать ее через DIALOG.

Первый подход максимально гибкий, но при этом весьма трудоемкий (так как требует написания javascript кода). Проблема же второго подхода в том, что в четвертой версии форма всегда показывалось в новом окне (встроенном или диалоговом), что не всегда удобно с точки зрения дизайна/UX.

Соответственно, в пятой версии появилась возможно показывать форму в элементе отображения свойства, а также в всплывающем окне. Это сделало возможным реализацию, например, следующего сценария:

selected = DATA LOCAL BOOLEAN(CustomUser);

FORM cuForm
    OBJECTS user = CustomUser CUSTOM 'selectMultiButton'
    PROPERTIES(user) name = login, selected 
;

dialogAction ()  {
    DIALOG cuForm OBJECTS user INPUT EMBEDDED DO {
        MESSAGE 'Selected logins : ' + (GROUP CONCAT login(CustomUser c) IF selected(c), ', ' ORDER login(c));
    }
}

FORM testForm
    PROPERTIES() dialogAction
;

run() {
    SHOW testForm DOCKED;
}

Мобильный навигатор

Базовый навигатор в lsFusion заточен под работу на больших мониторах с высоким разрешением. Использование его на мобильных устройствах требует большого количества лишних действий (скроллирований и приближений), что делает это использование неудобным. Поэтому в пятой версии на мобильных устройствах используется другое представление навигатора — мобильное. В этом представлении окна и их расположение игнорируется, а навигатор показывается как простое дерево, выезжающее при нажатии на соответствующий "гамбургер" внизу:

Больше эргономичности и возможностей настройки пользовательских фильтров

В веб-клиенте 4-й версии пользовательские фильтры показывались в отдельном диалоговом окне и скрывались сразу после их применения. В чем была проблема такого подхода:

  • не было видно какие фильтры применены в данный момент;

  • невозможность программной настройки (как задание предопределенных пользовательских фильтров, так и расположение пользовательских);

  • неудобная работа с фильтрами с клавиатуры (получить доступ к ним без мышки было очень тяжело).

Да и вообще, все современные интерфейсы постепенно уходят от диалоговых окон (там, где это возможно), поэтому в 5-й версии интерфейс работы с фильтрами был переработан:

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

Программные пользовательские фильтры

Одним из достаточно частых требований к функционалу форм является создание на ней предопределенных фильтров по некоторым свойствам объектов. В 4-й версии это можно было делать вручную:

filterStock = DATA LOCAL Stock ();
nameFilterStock 'Warehouse' = name(filterStock());
  
FORM onStockLocal 'Balances'
    PROPERTIES() nameFilterStock
  
    OBJECTS sb = (s = Stock, b = Book)
    PROPERTIES READONLY name(s), name(b), balance(b, s)
    ORDERS name(s), name(b)
  
    FILTERS s == filterStock() OR NOT filterStock()
;

Но это, все же, было весьма неудобно. Плюс, это полностью пересекалось с функционалом пользовательских фильтров, поэтому в пятой версии появился функционал так называемых программных (предопределенных) пользовательских фильтров. Работает он следующим образом: для свойства на форме можно задать специальную опцию FILTER, в этом случае платформа автоматически создает базовую компоненту пользовательского фильтра, как если бы ее создал сам пользователь:

FORM testForm 
    OBJECTS cu=CustomUser
    PROPERTIES(cu) login FILTER
;

run() {
    SHOW testForm DOCKED;
}

Создаваемая базовая компонента пользовательского фильтра имеет идентификатор FILTER(<ид свойства>), и при необходимости, разработчик может ее дополнительно настраивать (перемещать, изменять атрибуты и так далее).

Нечеткий поиск

Большинство объектов в информационных системах, как правило, идентифицируются своим именем. Это имя может генерироваться, может задаваться явно, но независимо от этого, очень часто пользователь при работе с системой не в состоянии вспомнить, как именно называется тот или иной объект. Соответственно, очень часто ему бывает необходимо найти объекты, зная лишь часть слов имени этих объектов (или даже часть частей слов их имени). К счастью, многие СУБД обладают поддержкой таких механизмов "нечеткого" поиска из коробки, и, как следствие, в 5-й версии платформы поддержка такого поиска также появилась (собственно, на основе существующих в СУБД механизмов).

При использовании "нечеткого" поиска пользователю достаточно задать через пробел только отдельные слова (или даже части этих слов), в результате чего платформа найдет все объекты с именами, содержащими эти слова/части слов, причем в любом порядке и, в том числе, например, с учетом склонений – то есть при использовании фильтра "красный" будут найдены и объекты с именем содержащим "красным", "красные" и т.п.:

Отметим, что для эффективной работы нечетких поисков на больших объемах, как правило, необходимы специальные индексы, поддержка которых также появилась в 5-й версии:

name = DATA STRING (Store) INDEXED MATCH;

INDEX ‘optional-index-name’ MATCH customer(InvoiceDetail d), supplier(d), sku(d);

Больше асинхронности

Одним из важных признаков хорошего интерфейса в плане UX является максимальная асинхронность всех пользовательских действий. Соответственно, в 5-й версии lsFusion асинхронность всех действий стала если не абсолютной, то очень близкой к ней.

Асинхронный ввод объектов и автодополнение

В 4-й версии ввод (выбор) большинства объектов осуществлялся при помощи диалогового окна. Само по себе это, конечно, не является проблемой (и естественно, поддерживается и довольно часто используется пользователями даже в 5-й версии), но из-за того, что открытие диалогового окна требовало как некоторые лишние действия со стороны пользователя, так и ожидание "ответа" от сервера, прежде чем начинать делать эти действия. Последнее, мало того, что занимало некоторое время, но, что куда важнее, приводило к синхронности всего процесса, что создавало определенные неудобства для пользователя.

В 5-й версии описанная выше проблема была решена следующим образом. В операторе ввода примитивных данных (то есть чисел, строк), который всегда был асинхронным, появилась возможность ввода объектов. В этом случае пользователь прямо в поле отображения вводит строку, идентифицирующую объект (как обычную строку), после чего платформа находит объект с этим идентификатором (при помощи описанного выше нечеткого поиска) и дальше платформа действует точно также, как если бы этот объект был выбран при помощи диалогового окна. Естественно, вводить имя объекта без подсказок было бы очень неудобно для пользователя, поэтому в 5-й версии также появилось автодополнение такого ввода. Как это все поддерживается с точки зрения разработчика.

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

  1. Самым базовым механизмом является автодополнение при вводе обычных строк.
    Например:

    PROPERTIES ''='textfield' ON CHANGE { INPUT sl = STRING[300] LIST login(CustomUser u) DO MESSAGE sl; } PANEL
    

    Тут в опции LIST (точно также, как и в операторе FOR) можно расширять контекст новыми параметрами, соответственно, автодополнение будет осуществляться из списка значений указанного свойства (выражения).

    Также в INPUT появилась опция ACTIONS, где можно указывать дополнительные кнопки (действия) в тулбаре внизу выпадающего списка (или выезжающего при наведении на поле отображения свойства, если этот INPUT используется в обработке события изменения этого свойства):

    PROPERTIES ''='textfield' ON CHANGE { INPUT sl = STRING[300] LIST login(CustomUser u) ACTIONS 'myaction' {MESSAGE 'myaction';} DO {}} PANEL
    
  2. Над этим надстраивается синтаксический сахар в виде того, что в INPUT вместо примитивного класса можно указывать пользовательский:

    INPUT s=Sku LIST name(s) DO
        MESSAGE id(s);
    

    Здесь уже контекст расширять нельзя, он всегда соответствуют вводимому объекту, но "на выходе" (в действии DO) приходит уже "готовый" объект (а не строка как в примерах выше).

  3. Над всем этим, в свою очередь, надстраивается синтаксический сахар в виде опции LIST в DIALOG:

    DIALOG customerStocks OBJECTS l = customer(o), s = customerStock(o) INPUT LIST name(s) DO
         MESSAGE 's : ' + id(s);
    

    который при запуске преобразуется в:

    INPUT s=Stock LIST name(s) WHERE <автоматически определяется из формы> 
        ACTIONS 'dialog' { DIALOG customerStocks OBJECTS  ... } DO 
        MESSAGE 's : ' + id(s);
    

    то есть INPUT с автодополнением (с учетом фильтра формы) и кнопкой "…" в тулбаре, при нажатии на которую вызывается заданный диалог (там, на самом деле, еще добавление объекта (+) генерируется, а в будущем еще и переход по ссылке будет генерироваться, но это уже тонкости).

  4. Ну и, наконец, обработка события CHANGE по умолчанию (напомню, что INPUT может только в этом событии использоваться) любого свойства генерируется как DIALOG ... LIST ... (с учетом того, что это за свойство, его статистики и т.п.). Соответственно, в большинстве случаев все работает само из коробки, но если нужно что-то более гибкое, то просто разработчик может создать свою обработку ON CHANGE, а дальше, по стеку от DIALOG .. .LIST ... до INPUT s=STRING ... ACTIONS.

Также стоит отметить, что в 5-й версии автодополнение используется по умолчанию, в том числе для ввода любых строковых данных (выбор из предыдущих введенных значений) и для ввода фильтров (как нечеткого, так и обычного поиска).

Асинхронные действия, формы и деревья, а также отображение этой асинхронности

В 5-й версии для любого действия на форме можно явно задать его асинхронность при помощи соответствующей опции NOWAIT (в противовес WAIT). В этом случае форма не блокируется, а пользователь может продолжать работу с формой (например, вводить другие данные и т.п.):

longIterate() {
    LOCAL int = INTEGER();
    FOR iterate(INTEGER i, 1, 2222) DO {
        FOR iterate(INTEGER j, 0, i) DO {
            int() <- divideInteger(i, j);   
        }
    }
    MESSAGE 'Готово';
}
FORM testForm
    PROPERTIES longIterate()
    PROPERTIES NOWAIT nowaitLongIterate 'nowaitLongIterate' = longIterate()
;

run() {
    SHOW testForm DOCKED;
}

Кроме того, в 5-й версии стало больше случаев, когда платформа может заранее определить "клиентский эффект" выполняемого действия, и соответственно, отобразить этот эффект заранее (до запроса к серверу), и тем самым сделать это действие асинхронным по умолчанию. Так, если в 4-й версии к таким действиям (эффектам) относились только добавление/удаление объектов и ввод примитивных данных, то в 5-й версии к ним добавились:

  • открытие формы

  • закрытие формы (в том числе с подтверждением)

  • изменение свойства на фиксированное значение (в том числе NULL)

  • разворачивание элементов в дереве

Визуально это выглядит следующим образом:

Также, в 5-й версии появилось расширенное отображение всей описанной выше асинхронности. Если в 4-й версии это отображение сводилось к "крутелке" на кнопке обновить, то в 5-й версии соответствующая "крутелка" появляется возле каждого действия/свойства по мере обработки (!) его вызова/изменения:

Улучшения процессов поддержки и разработки

Перейти в редакторе

Одной из основных потребностей при сопровождении (а иногда и при разработке) информационных систем является потребность в сопоставлении элемента интерфейса, который администратор (разработчик) видит на экране, и строки кода, где этот элемент интерфейса задается. Причем эта потребность порой возникает настолько часто, что просто видеть имя файла и строки, как это было в 4-й версии, уже недостаточно. Поэтому в 5-й версии имя файла и строки стали отображаться в виде гиперссылки, при нажатии на которую автоматически открывается среда разработки IDEA, где, в свою очередь, осуществляется автоматический переход на необходимые файл/строку:

Синтаксическая подсветка lsFusion кода в веб-клиенте

В lsFusion очень важным инструментом работы при сопровождении любой системы является так называемый Интерпретатор — форма, где администратор может выполнять любые скрипты на языке lsFusion. В 4-й версии для ввода этих скриптов использовалось обычное текстовое поле, что было не очень удобно (так как приводило к частым ошибкам). В 5-й версии для отображения и ввода этих скриптов используется библиотека Ace Editor, которая предоставляет достаточно широкие возможности работы с кодом, такие как проверка синтаксиса, автодополнение, раскраска кода и т.п. (впрочем, до возможностей IDEA этой библиотеке все же весьма далеко):

Соответственно, работа с lsFusion кодом в интерпретаторе в 5-й версии стала значительно удобнее. Кстати, эта же библиотека (ace editor) также используется на сайте lsFusion.org в разделе Попробовать онлайн.

Быстрая документация по синтаксису

Использование любого языка программирования подразумевает необходимость знать его синтаксис. И если в низкоуровневых императивных языках это не такая большая проблема, то в настолько высокодекларативном языке, которым является lsFusion, помнить наизусть все синтаксические конструкции может быть не так просто. Автодополнение, подсветки ошибок решают эту проблему, однако все же частично, и поэтому разработчику (во всяком случае начинающему) часто приходится обращаться к документации, чтобы уточнить синтаксис того или иного оператора. Чтобы упростить этот процесс, в плагине к IDEA появилась поддержка функционала быстрой документации (по умолчанию присутствующая в IDEA, но не поддерживаемая для языка lsFusion). Для использования этого функционала достаточно в коде поместить курсор на место с оператором (или частью этого оператора), для которого необходимо посмотреть синтаксис, нажать CTRL+Q, после чего IDEA покажет во всплывающем окне выдержку из документации с искомым синтаксисом:

"Живой" предпросмотр форм

Наверное, одной из самых любимых "фич" любого разработчика является возможность видеть результат выполнения написанного им кода "сразу", а не "потом", после компиляции/сборки/запуска. Такая функциональность поддерживается в lsFusion при разработке форм: если открыть закладку Design, разработчик сразу видит, как будет выглядеть форма, на которой находится курсор (в текущем модуле). Впрочем, этот механизм имеет ряд недостатков:

  • формы в этом механизме "мертвые", то есть не показывают данные, не отрабатывают события (нажатия кнопок) и т.п.;

  • поддерживаются только базовые возможности дизайна (поддерживаемые в десктоп-клиенте), все остальное (то есть то, что поддерживается в веб-клиенте), как, например, кастомизированные представления, css-классы и т.п. не поддерживается;

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

Чтобы избавится от этих ограничений/недостатков в 5-й версии подход был изменен. В IDE появился режим "живого" предпросмотра. В этом режиме для формы, на которой находится курсор, плагин автоматически "собирает" код ее создания из всех модулей (точно также, как при нажатии правой кнопки на форме и выборе пункта Aggregate Form), после чего посылает этот код на сервер, который в свою очередь автоматически показывает/обновляет форму во всех подключениях (обычно это одно подключение локально запущенного сервера с которым работает разработчик). Выглядит это следующим образом:

Правда, у этого механизма тоже есть определенные недостатки:

  • он требует, чтобы сервер приложений был запущен;

  • не учитывает изменения в логике до следующего перезапуска сервера. Частично, это, наверное, решится в будущем, когда, скажем, для еще не "обновленных" (а по сути, всех отсутствующих) свойств будут, например, генерироваться заглушки, но опять-таки, это все равно будет лишь частичное решение проблемы.

Но несмотря на эти недостатки, все же механизм "живого" предпросмотра в дальнейшем будет считаться основным инструментом, используемым для разработки форм в lsFusion.

Что еще?

Липкие колонки

Если таблица в плоском представлении или в представлении сводной таблицы содержит слишком много колонок, то обычно возникает проблема, что при горизонтальном скроллировании исчезают колонки, содержащие информацию, идентифицирующую объекты в этой таблице. В результате этого пользователь видит какие-то цифры, но не в состоянии понять к каким именно объектам эти цифры относятся (и ему приходится выбирать нужный ряд и скроллировать таблицу назад). Чтобы решить эту проблему в 5-й версии lsFusion появилась возможность помечать некоторые колонки как липкие. В этом случае при горизонтальном скроллировании такие колонки залипают слева (отсюда и названия) и тем самым остаются видны.

Также стоит отметить, что некоторые колонки, которые стоит сделать липкими, платформа определяет самостоятельно на основе статистики и имен колонок (а точнее свойств в этих колонках). Впрочем, так как такой механизм определения липких колонок весьма эвристичен, платформа следит, чтобы ширина этих колонок в сумме была не больше 30% ширины всей таблицы, так как в противном случае горизонтальное скроллирование становится невозможным.

Интервальные типы и операторы/свойства работы с ними

В B2B приложениях (как, впрочем, и B2C приложениях) очень часто возникают задачи, связанные с аналитикой. Эти задачи, в свою очередь, обычно требуют работы с теми или иными интервалами (как правило, связанными со временем), которые необходимо вводить, хранить и так далее. В 4-й версии для этого отдельно создавались, например, дата от и дата до, которые в свою очередь вводились/обрабатывались также отдельно. В 5-й версии появился специальный тип (а точнее семейство типов) - INTERVAL[<тип>]:

myInterval = INTERVAL[DATE] (MyObject);

Для этого типа в lsFusion поддерживается:

  • ввод на клиенте (при помощи специальных интервальных компонент)

    Пример
  • всевозможные свойства преобразования этих интервалов

  • а также специальный оператор создания интервального свойства на форме (по аналогии с VALUE, NEW и т.п.)

    OBJECTS dFrom = DATE, dTo = DATE
    PROPERTIES INTERVAL(dFrom, dTo) // создает свойство на форме, которое сразу вводит и dFrom, и dTo (по аналогии с вводом на клиенте значений интервальных типов, как на видео выше)
    

Новые сайт и документация

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

Кроме того, была осуществлена миграция документации с Confluence на Docusaurus. Как следствие, благодаря "Documentation-as-a-Code", а также определенными возможностями Docusaurus из коробки, эта миграция позволила значительно улучшить/упростить следующие процессы работы с документацией:

  • интернационализацию документации

  • версионирование документации

  • актуальность документации (то есть в одном коммите можно одновременно делать и изменения кода, и изменение документации)

  • обновление документации сообществом (на каждой странице появилась кнопка Edit this page, которая позволяет любому пользователю Github в одной действие создавать Pull Request-ы на изменение документации: исправление ошибок, например).

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

И многое другое

Все, что описано выше, — это далеко не полный список нововведений 5-й версии (полный можно посмотреть здесь), но это наиболее важные новые возможности. Среди менее важных можно, например, отметить поддержку:

  • низкоуровневых протоколов TCP и UDP в операторе EXTERNAL

  • google, yandex и других слоев в представлении Карта

  • планировщика на форме (выполнение серверного действия в заданные интервалы времени)

  • типов:

    • NAMEDFILE - файл, хранящий не только расширение, но и имя файла

    • TBOOLEAN - 3-значный логический тип, с тремя значениями true, false и null

Что дальше?

Даже с появлением в 5-й версии всех описанных выше возможностей, превращающих lsFusion в том числе в платформу разработки веб-приложения, lsFusion еще есть куда расти в этом плане (разработки веб-приложений). И в 6-й версии работа в этом направлении будет продолжена.

Поддержка material дизайна.

Этот вид дизайна де-факто стал практически стандартом в области разработки веб-приложений, и поддержка этого вида дизайна критически важна для любых платформ в этой области. Безусловно, поддерживать такой дизайн совсем с нуля неудобно, как с точки зрения трудоемкости разработки самой платформы, так и с точки зрения удобства использования/кастомизации этого дизайна разработчиками на lsFusion. Поэтому в качестве основы поддержки этого дизайна был взят один из самых популярных css-фреймворков в мире Bootstrap (по разным оценкам вплоть до 30 процентов рынка, когда остальные css-фреймворки помимо animate не дотягивают и до пары процентов). Тут, конечно, надо понимать, что Bootstrap это не спецификация, не платформа и даже не библиотека — это скорее просто набор шаблонов кода. Однако даже в таком виде использование Bootstrap одновременно помогает и получить классический дизайн из коробки, и хоть как-то стандартизировать процесс разработки/кастомизации дизайна форм.
Отметим, что в 6-й версии поддержка Bootstrap уже находится в достаточно высокой степени готовности, и то, как эта поддержка будет выглядеть, можно уже увидеть на примере одного из существующих демо-приложений.
Также стоит сказать, что поддержка Bootstrap (а точнее его классической темы) никоим образом не отменяет поддержку "excel" дизайна, используемого lsFusion сейчас. Дело в том, что в сложных бизнес-приложениях пользователи очень часто хотят работать в режиме single-screen, то есть видеть как можно больше информации сразу, не прибегая к дополнительным скроллам (и "excel" дизайн для этого идеально подходит). Таким образом, если Bootstrap значительно лучше подходит для B2C и простых приложений (условного MyCompany), "excel" — наоборот, для сложных приложений (условно, класса ERP).

Канбан представление, а также контейнеры в колонках в гриде.

Сейчас канбан представление можно реализовать при помощи кастомизированных представлений списков. Однако делать это на javascript, скажем прямо, не очень удобно. Поэтому в следующей версии, с большой вероятностью, появится возможность отображать любой список в виде канбан-доски. Предполагается, что для этого представления должен будет задаваться контейнер отображения записи, который, по сути, будет "размножаться" для каждой записи.
Также в следующей версии, скорее всего, появится своего рода микс между канбаном и гридом, а именно возможность отображать в колонке не просто свойства, а контейнеры, отображающие сразу несколько свойств (с некоторым кастомизированным дизайном).

Больше анимации.

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

Кроме того

И конечно, все не ограничивается только веб-разработкой. Безусловно, работа в развитии платформы в сторону разработки бизнес-приложений также будет вестись. Например, в части реализации заявленных в предыдущей статье возможностей:

  • пользовательская настройка форм,

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

  • агрегация (наследование) форм.

Конечно, весь описанный выше план (впрочем, как и предыдущий) весьма условен, и поэтому может (и, вероятно, будет) изменяться, в зависимости от полученного фидбека по результатам выпуска этой версии, а также текущих потребностей сообщества (разработчиков и пользователей lsFusion).

Теги:
Хабы:
Всего голосов 16: ↑16 и ↓0+19
Комментарии4
5

Публикации

Информация

Сайт
lsfusion.org
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Беларусь
Представитель
NitroJunkie

Истории