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

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

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

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

my onboarding

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

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

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

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

create screen 1

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

create screen 2

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

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

  • Descriptor name: my-onboarding-view

  • Controller name: MyOnboardingView

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

create screen 3

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

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

create screen 4

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

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

create screen 5

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

create screen 6

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

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

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

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

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

Во-первых, добавьте контейнер данных, который предоставит набор сущностей UserStep для UI-таблицы. Нажмите на кнопку Add Component на панели действий, выберите раздел Data components и дважды щелкните на элементе Collection. В окне Data Container Properties Editor в поле Entity выберите UserStep и нажмите кнопку OK:

data container 1

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

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

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

Прежде всего, удалите атрибут readOnly="true" у сгенерированного загрузчика, потому что объекты, отображаемые в этом экране, нужно будет изменять и сохранять. Вы можете сделать это в инспекторе компонентов или прямо в XML:

<loader id="userStepsDl">
    <query>
        <![CDATA[select e from UserStep e]]>
    </query>
</loader>

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

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

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

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

data container 3

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

@Route(value = "MyOnboardingView", layout = MainView.class)
@ViewController("MyOnboardingView")
@ViewDescriptor("my-onboarding-view.xml")
public class MyOnboardingView extends StandardView {
    @Subscribe
    public void onBeforeShow(final BeforeShowEvent event) {

    }
}

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

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

data container 4

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

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

@Autowired
private CurrentAuthentication currentAuthentication;

@ViewComponent
private CollectionLoader<UserStep> userStepsDl;

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

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

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

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

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

На панели структуры Jmix UI нажмите правой кнопкой мыши на элементе layout и выберите пункт Add Component в контекстном меню. Найдите и дважды щелкните на компоненте DataGrid. Выберите контейнер данных userStepsDc в диалоге DataGrid Properties Editor:

table 1

Нажмите OK.

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

<dataGrid id="userStepsDataGrid" dataContainer="userStepsDc" width="100%">
    <columns>
        <column property="dueDate"/>
        <column property="completedDate"/>
        <column property="sortValue"/>
    </columns>
</dataGrid>

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

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

table 2

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

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<view xmlns="http://jmix.io/schema/flowui/view"
      title="msg://myOnboardingView.title">
    <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>
        <dataGrid id="userStepsDataGrid" dataContainer="userStepsDc" width="100%">
            <columns>
                <column property="step.name"/>
                <column property="dueDate"/>
                <column property="completedDate"/>
            </columns>
        </dataGrid>
    </layout>
</view>

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

table 3

Добавление колонки с компонентом

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

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

<columns>
    <column key="completed" sortable="false" width="4em" flexGrow="0"/>

В контроллере инжектируйте фабрику UiComponents. Сгенерируйте для колонки completed обработчик renderer и реализуйте его следующим образом:

@Autowired
private UiComponents uiComponents;

@Supply(to = "userStepsDataGrid.completed", subject = "renderer")
private Renderer<UserStep> userStepsDataGridCompletedRenderer() {
    return new ComponentRenderer<>(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

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

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

Нажмите на кнопку Add Component на панели действий и перетащите LayoutsVBox (контейнер с вертикальным размещением) в элемент layout на панель структуры Jmix UI перед userStepsDataGrid. Затем добавьте три компонента HTMLSpan в vbox.

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

<layout>
    <vbox>
        <span id="totalStepsLabel"/>
        <span id="completedStepsLabel"/>
        <span id="overdueStepsLabel"/>
    </vbox>

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

@ViewComponent
private Span completedStepsLabel;

@ViewComponent
private Span overdueStepsLabel;

@ViewComponent
private Span totalStepsLabel;

@ViewComponent
private CollectionContainer<UserStep> userStepsDc;

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

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

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

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

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

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

  1. Вызовите updateLabels() из существующего обработчика BeforeShowEvent:

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

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

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

    label 2
  3. Вызовите метод updateLabels() из обработчика, который вы только что сгенерировали:

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

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

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

label 3

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

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

Сначала нажмите Add Component, выберите LayoutsHBox (контейнер с горизонтальным размещением) и поместите его после userStepsDataGrid. Затем добавьте в него две кнопки:

Задайте идентификаторы кнопок и подписи к ним. Для кнопки Save добавьте primary в атрибуте themeNames:

<hbox>
    <button id="saveButton" text="Save" themeNames="primary"/>
    <button id="discardButton" text="Discard"/>
</hbox>

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

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

@ViewComponent
private DataContext dataContext;

@Subscribe(id = "saveButton", subject = "clickListener")
public void onSaveButtonClick(final ClickEvent<JmixButton> event) {
    dataContext.save(); (1)
    close(StandardOutcome.SAVE); (2)
}

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

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

buttons 3

Стилизация таблицы (dataGrid)

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

Сначала назначьте класс onboarding-steps компоненту dataGrid добавив его в свойство classNames:

theme 1

Выберите колонку dueDate, переключитесь на вкладку Handlers инспектора и создайте обработчик partNameGenerator. Реализуйте его следующим образом:

@Install(to = "userStepsDataGrid.dueDate", subject = "partNameGenerator")
private String userStepsDataGridDueDatePartNameGenerator(final UserStep userStep) {
    return isOverdue(userStep) ? "overdue-step" : null;
}

Обработчик принимает экземпляр UserStep отображаемой строки и возвращает имя для использования в специальном CSS-селекторе для этой колонки.

Наконец, откройте файл onboarding.css из раздела User InterfaceThemes и добавьте следующий код CSS:

vaadin-grid.onboarding-steps::part(overdue-step) {
    color: red;
}

В данном селекторе vaadin-grid.onboarding-steps указывает на конкретный экземпляр компонента dataGrid, а ::part(overdue-step) указывает на ячейки, которые необходимо подсветить.

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

theme 4

Резюме

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

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

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

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

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

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

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

  • CSS-файл, находящийся в проекте может определять стили визуальных компонентов.

  • The partNameGenerator handler should be used to change the style of a table cell.

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