Skill for programming WebJET CMS datatable backend - JPA entity, Spring DATA repository, REST controller, frontend HTML page, modinfo.properties and automated tests. Use when creating new admin CRUD applications, adding datatable entities, REST endpoints, or modifying existing datatable-based admin pages.
Skill for programming WebJET CMS datatable backend - JPA entity, Spring DATA repository, REST controller, frontend HTML page, modinfo.properties and automated tests. Use when creating new admin CRUD applications, adding datatable entities, REST endpoints, or modifying existing datatable-based admin pages.
WebJET CMS DataTable Backend Programming Skill
This skill provides complete instructions for creating a datatable-based admin application in WebJET CMS. The architecture uses JPA entities with @DataTableColumn annotations, Spring DATA repositories, DatatableRestControllerV2 REST controllers, and Thymeleaf/HTML frontends.
Architecture Overview
A complete datatable application consists of:
JPA Entity - annotated with @DataTableColumn for automatic UI generation
Spring DATA Repository - extends JpaRepository + JpaSpecificationExecutor
NEVER use primitive types (int, long, boolean) - always use wrapper objects (Integer, Long, Boolean). Primitive types break search/filtering because ExampleMatcher cannot use NULL for primitives.
Primary key must be Long id - if the DB column has a different name, use @Column(name = "actual_column_name").
Always use @EntityListeners for automatic audit logging with appropriate Adminlog.TYPE_* constant.
Use @Getter and @Setter from Lombok - do not write getters/setters manually.
Use Jakarta imports (jakarta.persistence.*, jakarta.validation.*), NOT javax versions.
DataTableColumnType Reference
Field types for @DataTableColumn(inputType = ...):
Type
Description
ID
Primary key column
OPEN_EDITOR
Text field that opens editor on click - use for main/name field
TEXT
Standard single-line text input
TEXTAREA
Multi-line text input
NUMBER
Numeric input
SELECT
Dropdown selection
MULTISELECT
Multi-value dropdown
BOOLEAN
Checkbox for true/false
CHECKBOX
Checkbox with custom values
DATE
Date picker
DATETIME
Date and time picker
TIME_HM
Time picker (hours:minutes)
HIDDEN
Hidden field (not shown in editor)
DISABLED
Read-only field
QUILL
Simple HTML editor
WYSIWYG
Full HTML editor
JSON
Directory/page tree selector
DATATABLE
Nested datatable
ELFINDER
File/link browser
UPLOAD
File upload
PASSWORD
Password field
RADIO
Radio button selection
COLOR
Color picker
ICON
Icon selector (Tabler Icons)
ROW_REORDER
Drag & drop row ordering
STATIC_TEXT
Static text display
@DataTableColumn Key Attributes
title - Translation key for column header. If omitted, auto-generated as components.entity_name.field_name
tab - Editor tab ID where this field appears
hidden - Hide column from table (user cannot show it)
visible - Hide column but user can show it via column selector
hiddenEditor - Don't show field in editor
filter - Set false to hide filter in table header
sortAfter - Field name after which to position this field
@DataTableColumn(inputType = DataTableColumnType.TEXT, title="my.field", editor = {
@DataTableColumnEditor(
type = "text",
tab = "basic",
attr = {
@DataTableColumnEditorAttr(key = "disabled", value = "disabled"),
@DataTableColumnEditorAttr(key = "data-dt-field-hr", value = "after"),
@DataTableColumnEditorAttr(key = "data-dt-field-headline", value = "translation.key.for.headline")
},
options = {
@DataTableColumnEditorAttr(key = "Label text", value = "option_value")
}
)
})
Validation Annotations
@NotBlank// Non-empty, no whitespace only@NotEmpty// Non-empty, allows whitespace@Size(min=5, max=255)// Length constraint@Email// Email validation@Pattern(regexp = "^.+@.+\\.")// Regex validation@DecimalMax(value="200")// Max numeric value@DecimalMin(value="100")// Min numeric value@Future// Date must be in the future@Past// Date must be in the past@NotNull// Must not be null
Nested/Editor Fields
For additional non-database fields that appear in the editor, use @Transient with @DataTableColumnNested:
And use DomainIdRepository instead of JpaRepository (see Repository section).
Upgrading Database Schema
When you create a new table or modify an existing table (add/alter columns), you must add a new UpdateDBBean XML element at the bottom of the file src/main/webapp/WEB-INF/sql/autoupdate-webjet9.xml (before the closing </object></java> tags). Each entry must contain SQL for all four supported databases: MySQL, MSSQL (Microsoft SQL Server), Oracle and PostgreSQL.
XML Structure
<voidmethod="add"><objectclass="sk.iway.iwcm.system.UpdateDBBean"><voidproperty="author"><string>your_name</string></void><voidproperty="date"><string>DD.MM.YYYY</string></void><voidproperty="desc"><string>Short description of the change in English</string></void><voidproperty="mysql"><string>SQL FOR MYSQL</string></void><voidproperty="mssql"><string>SQL FOR MSSQL</string></void><voidproperty="oracle"><string>SQL FOR ORACLE</string></void><voidproperty="pgsql"><string>SQL FOR POSTGRESQL</string></void></object></void>
Oracle (requires separate SEQUENCE + TRIGGER for auto-increment):
CREATE TABLE my_table (
id INTEGERNOT NULLPRIMARY KEY,
name NVARCHAR2(255) NOT NULL,
description CLOB,
domain_id INTEGERDEFAULT1NOT NULL
);
CREATE SEQUENCE S_my_table STARTWITH1;
CREATETRIGGER T_my_table BEFORE INSERTON my_table
FOREACHROWDECLARE
val INTEGER|BEGIN
IF :new.id ISNULLTHENSELECT S_my_table.nextval into val FROM dual|
:new.id:= val|END IF|END|
;
PostgreSQL (requires separate SEQUENCE):
CREATE SEQUENCE S_my_table STARTWITH1;
CREATE TABLE my_table (
id INTDEFAULT NEXTVAL('S_my_table') PRIMARY KEYNOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
domain_id INTDEFAULT1NOT NULL
);
Important Rules
Oracle trigger syntax: Use | (pipe) instead of ; (semicolon) as statement delimiter inside trigger bodies. The semicolon at the very end terminates the whole block.
MSSQL uses NVARCHAR/NTEXT for Unicode string support (not plain VARCHAR/TEXT).
Oracle uses NVARCHAR2/CLOB for Unicode strings.
Always include all four <void property="..."> elements (mysql, mssql, oracle, pgsql), even if the SQL is identical — use an empty <string></string> only if the change genuinely does not apply to that database.
Multiple SQL statements can be placed in a single <string> block, separated by semicolons.
Date format in <void property="date"> is DD.MM.YYYY.
When creating a new table, also consider inserting a row into pkey_generator if the WebJET primary key generator is used (not needed when JPA @TableGenerator handles ID generation).
Always use @Datatable annotation - ensures correct error response handling.
Always use @PreAuthorize - controls access permissions.
URL pattern: /admin/rest/apps/{appname}/ for custom apps, /admin/rest/components/{name} for components, /admin/rest/settings/{name} for settings.
Constructor must call super(repository) or super(repository, EntityClass.class). Passing the entity class enables fetchOnCreate (default values for new records from server).
Never override REST endpoint methods with @Override - always override the xxxItem methods instead.
Override Methods (Life Cycle Hooks)
All of these are optional. Override only what you need:
// Called BEFORE saving (insert or edit)publicvoidbeforeSave(T entity) { }
// Called AFTER saving entitypublicvoidafterSave(T entity, T saved) { }
// Called BEFORE deletingpublicbooleanbeforeDelete(T entity) { returntrue; }
// Called AFTER deletingpublicvoidafterDelete(T entity, long id) { }
// Called BEFORE duplicatingpublicvoidbeforeDuplicate(T entity) { }
// Called AFTER duplicatingpublicvoidafterDuplicate(T entity, Long originalId) { }
// Validation before save/deletepublicvoidvalidateEditor(HttpServletRequest request, DatatableRequest<Long, T> target,
Identity user, Errors errors, Long id, T entity) { }
// Permission check per itempublicbooleancheckItemPerms(T entity, Long id) { returntrue; }
// Process action button clickspublicbooleanprocessAction(T entity, String action) { returnfalse; }
// Set select box options (recommended way)publicvoidgetOptions(DatatablePageImpl<T> page) { }
// Convert entity before returning via RESTpublic T processFromEntity(T entity, ProcessItemAction action) { return entity; }
// Convert entity before saving via RESTpublic T processToEntity(T entity, ProcessItemAction action) { return entity; }
// Custom search conditions (JPA Specification)publicvoidaddSpecSearch(Map<String, String> params, List<Predicate> predicates,
Root<T> root, CriteriaBuilder builder) { }
// Custom sortingpublic Pageable addSpecSort(Map<String, String> params, Pageable pageable) { }
// Data manipulation without repositorypublic T insertItem(T entity) { }
public T editItem(T entity, long id) { }
publicbooleandeleteItem(T entity, long id) { }
public T getOneItem(long id) { }
public Page<T> getAllItems(Pageable pageable) { }
public Page<T> searchItem(Map<String, String> params, Pageable pageable, T search) { }
Place in src/main/webapp/apps/{appname}/modinfo.properties:
# Translation key for menu item name
leftMenuNameKey=components.contact.title
# Permission key
itemKey=cmp_contact
# Allow permission assignment to users
userItem=true
# Menu link URL
leftMenuLink=/apps/contact/admin/
# FontAwesome icon (https://tabler.io/icons/)
icon=address-book
# If true, disabled by default for all users
defaultDisabled=true
# If true, shown as customer app at the beginning
custom=true
# Optional submenu
#leftSubmenu1NameKey=components.contact.subpage.title
#leftSubmenu1Link=/apps/contact/admin/subpage/
Add translations to src/main/webapp/WEB-INF/classes/text-webjet9.properties in Slovak language, text_cz-webjet9.properties for Czech and text_en-webjet9.properties for English:
After adding, reload translations by visiting /admin/?userlngr=true or restart the server.
8. Common Patterns
Row Styling
In BaseEditorFields subclass:
addRowClass("is-disabled"); // Red text - inactive
addRowClass("is-not-public"); // Red text - not public
addRowClass("is-default-page"); // Bold - default page
Status Icons
addStatusIcon("ti ti-map-pin"); // Show pinned icon
addStatusIcon("ti ti-lock-filled"); // Show lock icon