Перейти к основному содержимому

Как создать форму (List/Edit Template)

Обзор

Для каждого типа сущности или документа создаётся пара шаблонов:

  • ListTemplate — описание списка (колонки, фильтры, действия)
  • EditTemplate — описание формы редактирования (поля, таблицы, действия, файлы)

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

ListTemplate

Базовый пример

@Service
public class MyEntityListTemplate extends BaseEntityListTemplate<MyEntityListTemplate, MyEntity> {

public MyEntityListTemplate(MyEntityEntityService entityService) {
super(entityService, READ_PRIVILEGE, CREATE_PRIVILEGE, UPDATE_PRIVILEGE, DELETE_PRIVILEGE);

// Колонки (ключ = порядковый номер)
columns.put(1, ListColumn.<MyEntity>builder()
.fieldName("name")
.linkToEdit(true) // ячейка — ссылка на форму
.build());

columns.put(2, ListColumn.<MyEntity>builder()
.fieldName("status")
.build());

columns.put(3, ListColumn.<MyEntity>builder()
.fieldName("warehouse.title") // вложенное поле через точку
.build());

columns.put(4, ListColumn.<MyEntity>builder()
.fieldName("this") // вся сущность
.title("Количество")
.type(ParamType.INTEGER)
.valueGetter(e -> e.getItems().size()) // кастомный getter
.build());

// Фильтры
filters.put(1, ListFilterParam.<MyEntity>builder()
.fieldName("status")
.build()); // enum → автоматически SELECT с IN-фильтром

filters.put(2, ListFilterParam.<MyEntity>builder()
.fieldName("name")
.build()); // string → автоматически LIKE

// Кастомный фильтр
filters.put(3, ListFilterParam.<MyEntity>builder()
.fieldName("warehouse")
.specificationsSupplier(value -> {
// кастомная JPA Specification
var ids = parseIds(value);
return (root, query, cb) -> root.get("warehouse").get("id").in(ids);
})
.build());

// Поиск по строке
searchSpecificationsSupplier = search -> List.of(
(root, q, cb) -> cb.like(cb.lower(root.get("name")), "%" + search.toLowerCase() + "%")
);

// Действия
actions.put(10, ListAction.builder()
.title("Экспорт в Excel")
.requireEntitySelection(true)
.action((params, ids) -> {
// логика действия
return ActionResponseDto.success();
})
.build());
}
}

Автоматические фильтры

Тип Java-поляТип фильтраГенерируемый SQL
EnumSELECT (множественный выбор)WHERE status IN (?, ?)
StringTEXTWHERE name LIKE '%...%'
BooleanBOOLEANWHERE active = true
BaseEntityENTITY (выбор по ID)WHERE entity_id IN (?, ?)
LocalDateDATE (диапазон)WHERE date >= ? AND date <= ?
LocalDateTimeDATE_TIME (диапазон)WHERE created_at >= ? AND created_at <= ?

EditTemplate

Базовый пример

@Service
public class MyEntityEditTemplate extends BaseEntityEditTemplate<MyEntityEditTemplate, MyEntity> {

public MyEntityEditTemplate(MyEntityEntityService entityService) {
super(entityService, READ_PRIVILEGE, CREATE_PRIVILEGE, UPDATE_PRIVILEGE, DELETE_PRIVILEGE);

// Поля
fields.put(1, EditFormField.<MyEntity>builder()
.fieldName("name")
.required(e -> true)
.build());

fields.put(2, EditFormField.<MyEntity>builder()
.fieldName("status")
.readOnly(e -> true) // всегда read-only
.build());

fields.put(3, EditFormField.<MyEntity>builder()
.fieldName("warehouse")
.readOnly(e -> e.getId() != null) // read-only для существующих
.build());

// Группа полей
fields.put(4, EditFormField.<MyEntity>builder()
.fieldName("dimensions")
.type(ParamType.GROUP)
.fields(Map.of(
1, EditFormField.<MyEntity>builder().fieldName("width").build(),
2, EditFormField.<MyEntity>builder().fieldName("height").build(),
3, EditFormField.<MyEntity>builder().fieldName("depth").build()
))
.build());

// Связанные поля (каскадная фильтрация)
fields.put(5, EditFormField.<MyEntity>builder()
.fieldName("merchant")
.build());
fields.put(6, EditFormField.<MyEntity>builder()
.fieldName("sku")
.relatedFieldId(5) // зависит от поля merchant
.availableValues(e -> getSkusByMerchant(e.getMerchant()))
.build());

// Таблица позиций
tables.put(1, EditFormTable.<MyEntity, MyEntityItem>builder()
.fieldName("items")
.visible(e -> e.getId() != null)
.creationAvailable(e -> canEdit(e))
.deletionAvailable(e -> canEdit(e))
.columns(Map.of(
1, EditFormTableColumn.<MyEntity, MyEntityItem>builder()
.fieldName("skuPackaging")
.type(ParamType.SKU_PACKAGING)
.build(),
2, EditFormTableColumn.<MyEntity, MyEntityItem>builder()
.fieldName("quantity")
.totalValueGetter(items -> items.stream()
.map(MyEntityItem::getQuantity)
.reduce(BigDecimal.ZERO, BigDecimal::add))
.build()
))
.build());

// Действия
actions.put(1, EditFormAction.<MyEntity>builder()
.title("Провести")
.available(e -> e.getStatus() == MyEntityStatus.NEW)
.action((entity, params, selectedItems) -> {
entity.setStatus(MyEntityStatus.DONE);
return ActionResponseDto.success();
})
.build());

// Файлы
fileLists.put(1, EditFormFileList.<MyEntity, MyEntityFile>builder()
.fieldName("files")
.visible(e -> e.getId() != null)
.uploadAvailable(e -> canEdit(e))
.build());
}
}

Жизненный цикл отображения

Список

GET /list/template → ListTemplate.getFilters() + getColumns() + getActions()
POST /list → ListTemplate.getListDto(filters, sort, pagination)
POST /list/action → ListTemplate.performAction(actionId, params, entityIds)

Форма

GET /edit/template  → EditTemplate.getForm(entityId)
→ Единый ответ: шаблон полей + данные + таблицы + данные таблиц
POST /edit → EditTemplate.saveEntity(entityId, params, tableRows)
POST /edit/action → EditTemplate.performAction(entityId, actionId, params, selectedItems)

Готовые группы полей

// В конструкторе EditTemplate:
addDimensionsFieldGroup(10); // width/height/depth
addPersonFieldGroup(20); // surname/name/patronymic/phone/email
addLegalEntityFieldGroup(30); // title/inn/kpp/ogrn
addAddressFieldGroup(40); // country/region/city/.../fullAddress

Для документов

Используйте BaseDocListTemplate и BaseDocEditTemplate — они автоматически:

  • Добавляют колонки createTs, humanId (ссылка), status
  • Раскрашивают статусы (первый=серый, DONE=зелёный, CANCELED=серый, ERROR=красный)
  • Делегируют сохранение в docManager.save() (через полный конвейер)
  • Добавляют фильтр по статусу
  • Поиск по humanId