@PortalField — UI fields
Field or function-level annotation — declares an entity property as a UI field visible in the table, form, or filter panel.
@Target(AnnotationTarget.FIELD, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class PortalField(
val label: String,
val labelKey: String = "",
val tab: String = "",
val renderer: RendererType = RendererType.AUTO,
val order: Int = 0,
val readonly: Boolean = false,
val hidden: Boolean = false,
val showInTable: Boolean = true,
val showInFilter: Boolean = true,
val required: Boolean = false,
val placeholder: String = "",
val tooltip: String = "",
val tooltipKey: String = "",
val width: Int = 0,
val group: String = "",
val displayExpression: String = "",
val filterType: FilterType = FilterType.AUTO,
val selectOptions: Array<String> = [],
val selectEnum: KClass<*> = Unit::class,
val min: Double = Double.NaN,
val max: Double = Double.NaN,
val defaultValue: String = ""
)selectEnum — when set, SELECT/MULTI_SELECT options are built by calling .toString() on each enum constant.
If the enum does not override toString(), the constant’s name is used (e.g. "VIP").
The value in @PortalDependency must match the same toString() result.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
label | String | — | Column / form field label |
labelKey | String | "" | i18n key for label |
tab | String | "" | PortalTab enum constant name (e.g. "BASIC") |
renderer | RendererType | AUTO | UI component type |
order | Int | 0 | Sort position within tab / group |
readonly | Boolean | false | Always read-only, even in edit mode |
hidden | Boolean | false | Excluded from table and form |
showInTable | Boolean | true | Whether to show as a table column |
showInFilter | Boolean | true | Whether to show in the filter panel |
required | Boolean | false | Must be non-empty before saving |
placeholder | String | "" | Placeholder text for input |
tooltip | String | "" | Short help text near the field |
tooltipKey | String | "" | i18n key for tooltip |
width | Int | 0 | Preferred column width in px (0 = auto) |
group | String | "" | Visually groups related fields within a tab |
displayExpression | String | "" | Template ${fieldName} for computed display values |
filterType | FilterType | AUTO | Filter strategy |
selectOptions | Array<String> | [] | Explicit options for SELECT/MULTI_SELECT |
selectEnum | KClass<*> | Unit::class | Enum whose constants define options |
min | Double | NaN | Minimum value for NUMBER/DECIMAL |
max | Double | NaN | Maximum value for NUMBER/DECIMAL |
defaultValue | String | "" | Default value when creating a new record |
RendererType — renderer types
| Value | Description | Notes |
|---|---|---|
AUTO | Framework infers from JPA/Kotlin type | Default |
TEXT | Single-line text input | — |
TEXTAREA | Multi-line text area | Use @Column(columnDefinition = "TEXT") |
NUMBER | Integer numeric input | Int, Long |
DECIMAL | Floating-point input | Double, BigDecimal |
DATE | Date picker (ISO-8601 YYYY-MM-DD) | — |
DATETIME | Date and time picker (YYYY-MM-DDTHH:mm) | — |
BOOLEAN | Checkbox / toggle | Boolean |
SELECT | Single-value dropdown | Requires selectOptions or selectEnum |
MULTI_SELECT | Multi-value dropdown | Comma-separated in the database |
RELATION | ManyToOne / OneToOne picker | Requires @PortalRelation + @PortalLookup |
RELATION_LIST | OneToMany / ManyToMany inline list | Requires @PortalRelation + @PortalLookup |
PASSWORD | Password input (masked) | Not shown in table |
EMAIL | Email with format validation | — |
URL | URL with format validation | — |
COLOR | Color picker (hex #FF5733) | length = 7 |
FILE | File / image upload | Path or base64 |
JSON | Raw JSON editor | @Column(columnDefinition = "TEXT") |
CUSTOM | Custom renderer from frontend | — |
FilterType — filter strategies
| Value | Description | Example SQL |
|---|---|---|
AUTO | Inferred from field type | — |
EXACT | Equality match | field = :value |
CONTAINS | Case-insensitive substring | LOWER(field) LIKE %value% |
STARTS_WITH | Prefix search | LOWER(field) LIKE value% |
RANGE | Numeric or date range | field BETWEEN :from AND :to |
IN | Value-set membership | field IN (:values) |
BOOLEAN | Boolean equality | field = true/false |
NONE | Not filterable | — |
Examples
Text field with validation:
@Column(length = 100, nullable = false)
@PortalField(
label = "Full Name",
tab = "BASIC", order = 1,
required = true,
renderer = RendererType.TEXT,
filterType = FilterType.CONTAINS,
placeholder = "Enter full name"
)
var name: String = ""SELECT field with enum:
enum class Status { ACTIVE, INACTIVE, PENDING }
@Column(length = 20)
@Enumerated(EnumType.STRING)
@PortalField(
label = "Status", order = 2,
renderer = RendererType.SELECT,
filterType = FilterType.IN,
selectEnum = Status::class
)
var status: Status? = nullSELECT field with options list:
@Column(length = 20)
@PortalField(
label = "Priority", order = 3,
renderer = RendererType.SELECT,
filterType = FilterType.IN,
selectOptions = ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
)
var priority: String = ""DECIMAL field with range:
@Column
@PortalField(
label = "Price", order = 5,
renderer = RendererType.DECIMAL,
filterType = FilterType.RANGE,
min = 0.0, max = 99999.99,
placeholder = "0.00"
)
var price: Double = 0.0DATE field:
@Column
@PortalField(
label = "Birth Date", order = 6,
renderer = RendererType.DATE,
filterType = FilterType.RANGE,
showInTable = false,
tooltip = "Format: YYYY-MM-DD"
)
var birthDate: String = ""Read-only ID field:
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@PortalField(label = "ID", order = 0, readonly = true, showInFilter = false)
var id: Long = 0Field with displayExpression:
@PortalField(
label = "Full Name", order = 7,
displayExpression = "\${firstName} \${lastName}",
showInTable = true, readonly = true
)
var fullName: String = ""Field with default value:
@Column
@PortalField(
label = "Active", order = 8,
renderer = RendererType.BOOLEAN,
defaultValue = "true"
)
var isActive: Boolean = true@Regex — pattern validation
Field-level annotation that attaches a regex propagated to the frontend as client-side validation.
annotation class Regex(
val pattern: String,
val message: String = "The value does not match the required format"
)@Column(length = 20)
@Regex(
pattern = """^\+?[\d\s\-]{7,20}$""",
message = "Phone number may contain digits, spaces, hyphens, and an optional +"
)
@PortalField(label = "Phone", order = 2, renderer = RendererType.TEXT)
var phone: String = ""@Regex is client-side only. Add backend validation separately (e.g. Bean Validation @Pattern).