Complete Example — Customer entity
A complete entity using all the covered annotations.
This example intentionally combines many features in one place. In a real project, split handlers, enums, and form models into separate files.
Tabs
enum class CustomerTab(
override val label: String,
override val icon: String,
override val order: Int
) : PortalTab {
BASIC("Basic Info", "user", 0),
CONTACT("Contact", "phone", 1),
FINANCIAL("Financial","dollar-sign", 2)
}Enums
enum class CustomerType(val label: String) {
NEW("New"), REGULAR("Regular"), PREMIUM("Premium"), VIP("VIP");
override fun toString() = label
}
enum class CustomerTag { VIP, NEW, REGULAR, PREMIUM }Action form model
data class SendEmailForm(
@field:PortalFormField(label = "Subject", renderer = RendererType.TEXT, required = true, order = 1)
val subject: String = "",
@field:PortalFormField(label = "Body", renderer = RendererType.TEXTAREA, required = true, order = 2)
val body: String = ""
)Action handlers
@ApplicationScoped @io.quarkus.arc.Unremovable
class ActivateCustomerHandler {
val actionName = "activate"
suspend fun validate(entity: EntityData, formData: EntityData?) =
if (entity["isActive"] as? Boolean == true) "Customer is already active" else null
suspend fun execute(entity: EntityData, formData: EntityData?) =
ActionResult.Success("Customer activated.", refreshTable = true)
}
@ApplicationScoped @io.quarkus.arc.Unremovable
class SendEmailHandler {
val actionName = "sendEmail"
suspend fun validate(entity: EntityData, formData: EntityData?) = null
suspend fun execute(entity: EntityData, formData: EntityData?): ActionResult {
val subject = formData?.get("subject") as? String ?: ""
val email = entity["email"] as? String ?: ""
return ActionResult.Success("Email '$subject' sent to $email.")
}
suspend fun executeBulk(entities: List<EntityData>, formData: EntityData?) =
ActionResult.Success("Email sent to ${entities.size} customers.")
}Customer entity
@Entity
@Table(name = "customer")
@PortalEntity(
label = "Customer",
module = "CRM",
group = "Customers",
icon = "users",
order = 1,
description = "Company customers — main CRM dictionary",
tabs = CustomerTab::class,
pageSize = 25,
softDelete = true,
auditLog = true
)
@PortalSecurity(
viewRoles = ["user", "admin"],
editRoles = ["editor", "admin"],
deleteRoles = ["admin"],
actionRoles = ["editor", "admin"]
)
@PortalAction(
name = "activate",
label = "Activate",
icon = "check-circle",
handler = ActivateCustomerHandler::class,
confirmMessage = "Activate this customer?",
order = 1
)
@PortalAction(
name = "sendEmail",
label = "Send Email",
icon = "mail",
handler = SendEmailHandler::class,
formModel = SendEmailForm::class,
bulkAllowed = true,
order = 2,
variant = "outline"
)
class Customer {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@PortalField(label = "ID", tab = "BASIC", order = 0, readonly = true, showInFilter = false)
var id: Long = 0
@Column(length = 100, nullable = false)
@PortalField(
label = "Full Name",
tab = "BASIC", order = 1,
required = true,
renderer = RendererType.TEXT,
filterType = FilterType.CONTAINS
)
var name: String = ""
@Column(length = 20)
@Enumerated(EnumType.STRING)
@PortalField(
label = "Customer Type",
tab = "BASIC", order = 2,
renderer = RendererType.SELECT,
filterType = FilterType.IN,
selectEnum = CustomerType::class
)
var customerType: CustomerType? = null
@Column
@PortalField(
label = "Active",
tab = "BASIC", order = 3,
renderer = RendererType.BOOLEAN,
filterType = FilterType.BOOLEAN
)
var isActive: Boolean = true
@Column(unique = true)
@PortalField(
label = "Email",
tab = "CONTACT", order = 1,
renderer = RendererType.EMAIL,
filterType = FilterType.EXACT
)
var email: String = ""
@Column(length = 20)
@Regex(pattern = """^\+?[\d\s\-]{7,20}$""", message = "Invalid phone number format")
@PortalField(
label = "Phone",
tab = "CONTACT", order = 2,
renderer = RendererType.TEXT,
filterType = FilterType.STARTS_WITH,
placeholder = "+1 …"
)
var phone: String = ""
@Column
@PortalField(
label = "Country",
tab = "CONTACT", order = 3,
renderer = RendererType.RELATION,
filterType = FilterType.EXACT,
showInTable = false
)
@PortalRelation(
targetEntity = Country::class,
displayFields = ["name", "code"],
searchFields = ["name", "code"]
)
@PortalLookup(labelField = "name", valueField = "id")
var countryId: Long? = null
@Column
@PortalField(
label = "Credit Limit",
tab = "FINANCIAL", order = 1,
renderer = RendererType.DECIMAL,
filterType = FilterType.RANGE,
placeholder = "0.00"
)
@PortalDependency(
field = "customerType", operator = DependencyOperator.EQ, value = "New",
max = "5000", message = "New customer — max 5000"
)
@PortalDependency(
field = "customerType", operator = DependencyOperator.EQ, value = "VIP",
min = "5000", max = "500000"
)
var creditLimit: Double = 0.0
@Column
@PortalField(
label = "Tags",
tab = "FINANCIAL", order = 2,
renderer = RendererType.MULTI_SELECT,
filterType = FilterType.IN,
selectEnum = CustomerTag::class,
showInTable = false
)
@PortalDependency(
field = "customerType", operator = DependencyOperator.EQ, value = "New",
allowedValues = ["NEW"], message = "New customer can only have the NEW tag"
)
var tags: String = ""
// Soft-delete — required when softDelete = true in @PortalEntity
@Column
@PortalField(label = "Deleted", hidden = true)
var deleted: Boolean = false
}Last updated on