7. Создание пользовательского интерфейса с нуля

На данном этапе в приложении есть все необходимое для администраторов и HR-менеджеров: они могут настраивать отделы и шаги онбординга, управлять пользователями, генерировать и отслеживать шаги онбординга для каждого пользователя.

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

Ниже приведен макет экрана My Onboarding:

my onboarding

Ранее вы создавали пользовательский интерфейс, генерируя и изменяя CRUD-экраны для сущностей. В этой главе вы создадите экран My Onboarding с нуля.

Создание пустого экрана

Если ваше приложение запущено, остановите его с помощью кнопки Stop (suspend) на главной панели инструментов.

В окне инструментов Jmix нажмите New (add) → Screen:

create screen 1

В окне Create Jmix Screen выберите шаблон Blank screen:

create screen 2

Нажмите кнопку Next.

На следующем шаге мастера введите:

  • Package name: com.company.onboarding.screen.myonboarding

  • Descriptor name: my-onboarding-screen

  • Controller name: MyOnboardingScreen

create screen 3

Нажмите кнопку Next.

На следующем шаге мастера измените заголовок экрана на My onboarding:

create screen 4

Нажмите кнопку Create.

Студия создаст пустой экран и откроет его в дизайнере:

create screen 5

Новый экран также будет добавлен в главное меню. Дважды щелкните по пункту User InterfaceMain Menu в окне инструментов Jmix и перейдите на вкладку Structure. Перетащите экран MyOnboardingScreen наверх:

create screen 6

Запустите приложение, нажав кнопку Debug (start debugger) на главной панели инструментов. Откройте http://localhost:8080 в вашем веб-браузере и войдите в приложение.

Раскройте меню Application, нажмите на подпункт My onboarding и убедитесь, что ваш пустой экран открывается.

Добавление таблицы

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

Определение контейнера данных

Во-первых, добавьте контейнер данных, который предоставит набор сущностей UserStep для UI-таблицы. Перетащите элемент Data componentsCollection из палитры компонентов в элемент window иерархии компонентов, выберите UserStep в диалоговом окне и нажмите кнопку OK:

data container 1

Студия создаст контейнер коллекции:

<data>
    <collection id="userStepsDc"
                class="com.company.onboarding.entity.UserStep">
        <fetchPlan extends="_base"/>
        <loader id="userStepsDl">
            <query>
                <![CDATA[select e from UserStep e]]>
            </query>
        </loader>
    </collection>
</data>

Загрузка данных

Запрос по умолчанию загрузит все экземпляры UserStep, но вам нужно выбрать только шаги текущего пользователя и в определенном порядке. Давайте изменим запрос с помощью конструктора JPQL. Выберите контейнер userStepsDc в иерархии компонентов и щелкните на значение атрибута query. Затем добавьте раздел where с параметром :user и раздел order by:

data container 2

Результирующий запрос должен быть таким, как показано ниже:

<query>
    <![CDATA[select e from UserStep e
    where e.user = :user
    order by e.sortValue asc]]>
</query>

Следующая задача - указать значение для параметра :user. Вы можете сделать это в обработчике BeforeShowEvent. Переключитесь на класс контроллера MyOnboardingScreen, нажмите кнопку Generate Handler на верхней панели действий и выберите Controller handlersBeforeShowEvent:

data container 3

Нажмите на кнопку OK. Студия сгенерирует заглушку метода обработчика:

@UiController("MyOnboardingScreen")
@UiDescriptor("my-onboarding-screen.xml")
public class MyOnboardingScreen extends Screen {

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {

    }
}

Теперь вам нужно получить текущего пользователя, вошедшего в систему, и установить его в качестве параметра запроса загрузчика.

Используйте окно инструментов Code Snippets для генерации кода и получения текущего пользователя:

data container 4

Затем инжектируйте загрузчик, установите параметр :user для текущего пользователя и вызовите его метод load() для выполнения запроса и загрузки данных в контейнер коллекции:

data container 5

Результирующий код для загрузки данных в контейнер коллекции:

@Autowired
private CurrentAuthentication currentAuthentication;

@Autowired
private CollectionLoader<UserStep> userStepsDl;

@Subscribe
public void onBeforeShow(BeforeShowEvent event) {
    User user = (User) currentAuthentication.getUser();
    userStepsDl.setParameter("user", user);
    userStepsDl.load();
}

На экране браузера сущностей или редактора сущности, созданном Studio, загрузка данных по умолчанию инициируется фасетом DataLoadCoordinator:

<facets>
    <dataLoadCoordinator auto="true"/>
</facets>

Вот почему вы не вызывали метод load() загрузчиков данных на CRUD-экранах, созданных в предыдущих главах.

Настройка таблицы

Перетащите компонент Table из палитры компонентов на элемент layout иерархии компонентов. Выберите контейнер данных userStepsDc в диалоге Table Properties Editor, затем установите ширину таблицы на 100% и высоту на 400px в инспекторе компонентов:

table 1

Как вы можете видеть, в таблице нет колонки для отображения названия шага:

<table id="userStepsTable" height="400px" width="100%"
       dataContainer="userStepsDc">
    <columns>
        <column id="dueDate"/>
        <column id="completedDate"/>
        <column id="sortValue"/>
    </columns>
</table>

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

Добавьте атрибут step в фетч-план, затем добавьте колонку для него в таблицу и удалите ненужную колонку sortValue:

table 2

На этом этапе XML-файл экрана должен быть таким, как показано ниже:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://jmix.io/schema/ui/window"
        caption="msg://myOnboardingScreen.caption">
    <data>
        <collection id="userStepsDc" class="com.company.onboarding.entity.UserStep">
            <fetchPlan extends="_base">
                <property name="step" fetchPlan="_base"/>
            </fetchPlan>
            <loader id="userStepsDl">
                <query>
                    <![CDATA[select e from UserStep e
                    where e.user = :user
                    order by e.sortValue asc]]>
                </query>
            </loader>
        </collection>
    </data>
    <layout>
        <table id="userStepsTable" height="400px" width="100%"
               dataContainer="userStepsDc">
            <columns>
                <column id="step.name"/>
                <column id="dueDate"/>
                <column id="completedDate"/>
            </columns>
        </table>
    </layout>
</window>

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Убедитесь, что у вашего текущего пользователя (возможно, это admin) есть несколько пользовательских шагов, сгенерированных на экране редактирования пользователя. Снова откройте экран My onboarding и посмотрите ваши онбординг-шаги:

table 3

Добавление генерируемой колонки

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

В XML-дескриптор добавьте объявление колонки completed:

<table id="userStepsTable" height="400px" width="100%"
       dataContainer="userStepsDc">
    <columns>
        <column id="completed" caption="" width="50px"/>
        <column id="step.name"/>
        <column id="dueDate"/>
        <column id="completedDate"/>
    </columns>
</table>

В контроллере инжектируйте фабрику UiComponents и реализуйте обработчик генератора колонки columnGenerator:

@Autowired
private UiComponents uiComponents;

@Install(to = "userStepsTable.completed", subject = "columnGenerator")
private Component userStepsTableCompletedColumnGenerator(UserStep userStep) {
    CheckBox checkBox = uiComponents.create(CheckBox.class);
    checkBox.setValue(userStep.getCompletedDate() != null);
    checkBox.addValueChangeListener(e -> {
        if (userStep.getCompletedDate() == null) {
            userStep.setCompletedDate(LocalDate.now());
        } else {
            userStep.setCompletedDate(null);
        }
    });
    return checkBox;
}

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте My onboarding и протестируйте свои последние изменения:

gen column 1

Добавление надписей

Таблица почти готова. Теперь давайте добавим надписи, отображающие счетчики общего количества, выполненных и просроченных шагов.

Перетащите ContainersVBox (контейнер с вертикальным размещением) из палитры компонентов в элемент layout иерархии компонентов перед userStepsTable. Затем добавьте три компонента Label в vbox:

label 1

Установите идентификаторы надписей, как показано ниже:

<layout>
    <vbox spacing="true">
        <label id="totalStepsLabel"/>
        <label id="completedStepsLabel"/>
        <label id="overdueStepsLabel"/>
    </vbox>

Теперь вам нужно вычислить и установить их значения программно в контроллере. Переключитесь на контроллер MyOnboardingScreen, инжектируйте надписи и контейнер коллекции userStepsDc:

@Autowired
private Label totalStepsLabel;

@Autowired
private Label completedStepsLabel;

@Autowired
private Label overdueStepsLabel;

@Autowired
private CollectionContainer<UserStep> userStepsDc;

Затем добавьте пару методов для вычисления и определения счетчиков:

private void updateLabels() {
    totalStepsLabel.setValue("Total steps: " + userStepsDc.getItems().size());

    long completedCount = userStepsDc.getItems().stream()
            .filter(us -> us.getCompletedDate() != null)
            .count();
    completedStepsLabel.setValue("Completed steps: " + completedCount);

    long overdueCount = userStepsDc.getItems().stream()
            .filter(us -> isOverdue(us))
            .count();
    overdueStepsLabel.setValue("Overdue steps: " + overdueCount);
}

private boolean isOverdue(UserStep us) {
    return us.getCompletedDate() == null
            && us.getDueDate() != null
            && us.getDueDate().isBefore(LocalDate.now());
}

Наконец, вызовите метод updateLabels() из двух обработчиков событий:

  1. Существующего обработчика BeforeShowEvent:

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {
        // ...
        updateLabels();
    }

    Таким образом, надписи будут обновлены при открытии экрана.

  2. Нажмите Generate Handler и выберите Data container handlersuserStepsDcItemPropertyChangeEvent:

    label 2
    @Subscribe(id = "userStepsDc", target = Target.DATA_CONTAINER)
    public void onUserStepsDcItemPropertyChange(InstanceContainer.ItemPropertyChangeEvent<UserStep> event) {
        updateLabels();
    }

    С помощью обработчика ItemPropertyChangeEvent надписи будут обновлены, когда вы измените их атрибут completedDate, используя флажки в таблице.

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте экран My onboarding и проверьте значения надписей:

label 3

Разворачивание элементов в контейнерах

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

Сейчас вертикальное пространство, доступное для корневого элемента layout, разделено на две равные части между его вложенными компонентами: vbox и table. Таким образом, table начинается с середины экрана.

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

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

Перетащите Label на элемент layout, задайте идентификатор надписи и используйте его в атрибуте expand элемента layout:

expand 1

Результирующий XML-файл должен быть следующим:

    <layout expand="spacer" spacing="true">
        <vbox spacing="true">
            ...
        </vbox>
        <table id="userStepsTable" ...>
            ...
        </table>
        <label id="spacer"/>
    </layout>

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

Атрибут spacing="true" указывает контейнеру добавить небольшое смещение между компонентами.

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте экран My onboarding и посмотрите исправленную компоновку экрана:

expand 2

Сохранение изменений и закрытие экрана

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

Сначала выберите ContainersHBox (контейнер с горизонтальным размещением) из палитры компонентов и поместите его между userStepstable и spacer. Затем добавьте в него две кнопки:

buttons 1

Задайте названия кнопок и подписи к ним. Для кнопки Save добавьте атрибут primary="true":

<hbox spacing="true">
    <button id="saveButton" caption="Save" primary="true"/>
    <button id="discardButton" caption="Discard"/>
</hbox>

Сгенерируйте обработчики нажатия кнопок с помощью вкладки Component InspectorHandlers:

buttons 2

Инжектируйте DataContext в класс контроллера и реализуйте обработчики нажатия кнопок:

@Autowired
private DataContext dataContext;

@Subscribe("saveButton")
public void onSaveButtonClick(Button.ClickEvent event) {
    dataContext.commit(); (1)
    close(StandardOutcome.COMMIT); (2)
}

@Subscribe("discardButton")
public void onDiscardButtonClick(Button.ClickEvent event) {
    close(StandardOutcome.DISCARD); (2)
}
1 DataContext отслеживает изменения в сущностях, загруженных в контейнеры данных. Когда вы вызываете его метод commit(), все измененные экземпляры сохраняются в базе данных.
2 Метод close() закрывает экран. Он принимает объект "outcome", который может быть проанализирован вызывающим кодом.

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте экран My onboarding и посмотрите на кнопки в действии:

buttons 3

Работа со стилями

Последнее требование к экрану My onboarding - выделить просроченные шаги, изменив цвет шрифта в ячейках с Due date. Вы сделаете это, создав класс CSS и используя его в таблице.

Расширение темы по умолчанию

По умолчанию ваше приложение использует тему Helium, которая определяет стили всех UI компонентов. Чтобы добавить свои собственные стили, вам необходимо создать пользовательскую тему на основе темы по умолчанию.

Если ваше приложение запущено, остановите его с помощью кнопки Stop (suspend) на главной панели инструментов.

В окне инструментов Jmix нажмите New (add) → AdvancedCustom Theme:

theme 1

В диалоговом окне Create Custom Theme введите helium-ext в поле Theme name и выберите helium в раскрывающемся списке Base theme:

theme 2

Нажмите кнопку OK.

Студия создаст файловую структуру для новой темы:

theme 3

Она также перенастроит зависимости в build.gradle и добавит пару свойств в файл application.properties:

jmix.ui.theme.name=helium-ext
jmix.ui.theme-config=com/company/onboarding/theme/helium-ext-theme.properties

Откройте файл styles.css и добавьте класс overdue-step, как показано ниже:

@import "helium-ext-defaults";
@import "addons";
@import "helium-ext";

.helium-ext {
    @include addons;
    @include helium-ext;

    .overdue-step {
      color: red;
    }
}

Теперь вы можете использовать overdue-step в атрибутах stylename UI компонентов.

Добавление провайдера стилей таблицы

Чтобы применить пользовательский стиль к ячейкам таблицы, вам необходимо определить провайдера стилей (Style Provider) для компонента таблицы.

Откройте класс контроллера MyOnboardingScreen и нажмите кнопку Generate Handler на верхней панели действий. Выберите элемент Component handlersuserStepsTablestyleProvider:

style 1

Нажмите на кнопку OK.

Вы также можете сгенерировать обработчик на вкладке Handlers окна инструмента Component Inspector.

Реализуйте обработчик styleProvider, как показано ниже:

@Install(to = "userStepsTable", subject = "styleProvider") (1)
private String userStepsTableStyleProvider(
        UserStep entity, String property) { (2)
    if ("dueDate".equals(property) && isOverdue(entity)) {
        return "overdue-step"; (3)
    }
    return null; (4)
}
1 Аннотация @Install указывает, что метод является делегатом: UI компонент (в данном случае таблица) вызывает его на каком-то этапе своего жизненного цикла.
2 Этот конкретный делегат (провайдер стиля) получает экземпляр сущности и имя свойства, которое отображается в ячейке таблицы в качестве аргументов.
3 Если обработчик вызывается для свойства dueDate, и этот шаг просрочен, обработчик возвращает имя пользовательского стиля.
4 В противном случае ячейка будет отрисована с использованием стиля по умолчанию.

Нажмите Ctrl/Cmd+S и переключитесь на запущенное приложение. Снова откройте экран My onboarding и проверьте работу стиля для просроченных шагов:

theme 4

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

./gradlew compileThemes

Затем переключитесь на приложение и принудительно перезагрузите страницу (в Google Chrome вы можете сделать это, нажав Shift+Ctrl/Cmd+R).

Резюме

В этом разделе вы с нуля разработали целый экран для работы с данными.

Вы узнали, что:

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

  • Чтобы запустить загрузку данных, вы должны либо вызвать метод load() загрузчика данных в обработчике событий, либо добавить на экран фасет DataLoadCoordinator.

  • Контейнеры VBox и HBox используются для размещения компонентов пользовательского интерфейса вертикально или горизонтально. Корневой контейнер layout сам по себе представляет собой контейнер с вертикальным размещением.

  • Атрибут expand UI контейнеров указывает вложенный компонент, который должен занимать все доступное пространство внутри контейнера. Если он не используется, контейнеры разделяют пространство поровну между вложенными компонентами.

  • Метод commit() DataContext сохраняет все измененные объекты в базе данных.

  • Экран может быть закрыт программно с помощью метода close(), предоставляемого базовым классом Screen.

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

  • Для изменения стиля ячейки таблицы следует использовать обработчик провайдера стилей (style provider).

  • Окно инструмента Code Snippets можно использовать для быстрого поиска и генерации кода, работающего с API фреймворка.

Смотрите подробную информацию о расположении UI компонентов и контейнеров в разделе Правила компоновки экрана.