Create Entity Button
A fluent-forms layout widget that renders a button which opens a modal dialog for creating a new entity (Customer, Order, Ticket, Inventory item, Catalog specification, Register value, ...). The dialog shows a type selector (where relevant) and the form defined for that entity's specification. Optional pre-script, save-script, store actions, and effect hooks can be attached around the dialog lifecycle.
Designer selector: create-entity-button · runtime widget type: dtl-create-entity-button · schema node type: layout
Adding the widget to a form
- Open the fluent-forms designer for your form.
- In the component palette, open the Advanced category → Secondary group.
- Drag the Create Entity item (icon: plus / button) onto the canvas.
- In the right-hand editor, fill in at least Entity Type and a Title.
The widget is a layout node — it does not produce a form value, so it can sit anywhere (toolbars, headers, card footers) without affecting the parent form's data.
Runtime prerequisites — what must exist in the backend
The button renders from the schema alone (so designer / preview always shows it), but the dialog's contents load from the backend. If the referenced data is missing, the dialog opens but stays empty (header + footer only — no type LOV, no form). Before you ship a create-entity-button, make sure the following data is seeded in the environment you're targeting:
| What | Required by | Fails as |
|---|---|---|
Entity type with the given entityType code, its microservice and config.api.entityEndpoint registered | all scenarios | empty LOV / empty dialog |
Microservice backendUrl + apiVersion reachable from the browser | all scenarios | network 404 from the LOV |
At least one {entityType}-type row (e.g. one CustomerType, OrderType, TicketType) | generic entity-type mode | empty LOV, Save disabled |
EntitySpecification pointed to by entitySpecIdProperty on the selected type | all scenarios | empty form body |
Form specification matching formSelector (JEXL) or fallbackFormCode | all scenarios | form area blank |
inventoryType row with instantiable: true in the catalog | EntityInstanceConfiguration | empty catalog-spec LOV |
Catalog row with the given catalogCode; category row with the given categoryId | EntityCatalogSpecification / EntityCatalogCategory | form body blank |
Register row with the given registerCode | RegisterValue | form body blank |
Script with the given preScriptCode / scriptCode | scenarios that use scripts | click errors out (preScript) or Save fails (saveScript) |
createPrivilege from newRecordConfig granted to the current user | scenarios with privilege gate | button disabled / dialog rejects save |
The JSON examples below use real codes probed from the reference dev backend (tsm.datalite.cloud) — they should work if the same codes exist in your environment. If you're deploying elsewhere, swap the codes ('Ukazkovy', 'Mpe.Cfg', 'Zdroj', '4061fa84-eaac-420d-a3c0-788f3589b00b', 'Crm.Customer.Get', ...) for codes that exist in your backend.
Implementation notes (hardened by a recent round of fixes):
- NgRx feature state registration — the dialog is a standalone component using selectors from
@tsm/config-form/service,@tsm/entity-type/service, and@tsm/catalog/service. Those modules are now explicitly imported in generic-new-entity-dialog.component.ts, so the dialog no longer depends on whichever host route happened to lazy-load them. Before the fix,selectMicroserviceByCodewould crash withTypeError: Cannot read properties of undefined (reading 'microservice')and the dialog body stayed blank. - Selected-type wiring —
selectedTypeis now derived fromtypeControl.valueChanges(viatoSignal). Previously the template bound(valueChanged)="changeType($event)"to<tsm-entity-type-generic-lov>, but that component declares no such output — so the signal never updated after the LOV auto-selected a type andformResourcenever fired. formSelectorauto-lookup — resolved throughFrameworkPluginServicematching on registeredtsm-controls-configuration.tsentries (see runtime flow step 4).entitySpecIdPropertyconvention — Order/Ticket/Lead/Person/Account default to{entity}SpecIdwithout user configuration.
Verified by the e2e suite in apps/datalite-e2e/src/tests/config-form/ — render, editor-contract click-through scenarios, and two end-to-end save-flow scenarios. The remaining save-flow scenarios are marked test.fixme because the generic fillVisibleInputs helper can't satisfy per-entity form validation rules (fragility of the test harness, not the widget).
Configuration reference
The editor is divided into accordion sections. The tables below follow the same order. All keys live inside the schema node's config object unless noted otherwise (see Schema shape below).
Appearance
| Property | Type | Default | Description |
|---|---|---|---|
title | string | — | Button label. Lives at the schema node root (sibling of widget), not inside config. Localizable via the form's localization data. |
buttonAppearance | string | "p-button p-button-text" | PrimeNG appearance class. Supported: "p-button p-button-raised", "p-button p-button-text", "p-button p-button-raised p-button-text", "p-button p-button-outlined". |
buttonSeverity | string | "p-button-primary" | PrimeNG severity class. Supported: p-button-primary, p-button-secondary, p-button-success, p-button-warning, p-button-danger, p-button-help, p-button-info, p-button-plain. |
customIconCssClass | string | — | CSS class for the icon (e.g. "pi pi-plus" or "tsm-icon-customer"). Lives under widget. |
customCssClass | string | — | Extra CSS classes appended to the button element. Lives under widget. |
tooltip | string | — | Hover tooltip. Lives under widget. |
hidden | boolean | false | Hides the button at runtime (still visible in the designer). Lives under widget. |
disabled | boolean | false | Disables the button (unless alwaysEnabled is set). Lives under widget. |
Entity configuration
| Property | Type | Default | Description |
|---|---|---|---|
entityType | string | — (required) | Entity type code — e.g. "Customer", "Order", "Ticket", "EntityInstanceConfiguration", "EntityCatalogSpecification", "EntityCatalogCategory", "RegisterValue". Drives which LOV and special sections apply. |
entitySpecIdProperty | string | "entitySpecId" | Property on the selected type that holds the EntitySpecification ID. For Order/Ticket/Lead/Person/Account the widget auto-uses the convention {entityType}SpecId (so orderSpecId, ticketSpecId, ...) — you don't need to set this. Override only for entities that break the convention. |
formSelector | string | — | Selector for looking up the form specification. Left empty the widget auto-derives it via FrameworkPluginService — it looks up registered tsm-controls-configuration.ts entries with useType: 'FORM_NEW' matching the entityType (Customer→tsm-customer-new, Order→tsm-order-new, Ticket→tsm-ticket-new, RegisterValue→tsm-register-value-new, ...). For EntityInstanceConfiguration derived from inventoryType (e.g. PRODUCT→tsm-inventory-product-new). Set explicitly only to override the auto-lookup with a custom selector. |
fallbackFormCode | string | — | Form code used when the EntitySpecification has no form matching formSelector (e.g. "Customer.Forms.Default.New"). |
dialogTitle | string | — | Custom dialog title. If omitted, a default localized title is used. |
dialogWidth | string | "900px" | Dialog width including unit ("900px", "50vw", "80%", ...). |
alwaysEnabled | boolean | false | If true, the button stays clickable even when the surrounding form is readonly. Useful for "Add" actions on detail pages. |
New record config
Passed as a newRecordConfig object — controls the type picker inside the dialog.
| Property | Type | Description |
|---|---|---|
label | string | Legacy dialog-title override (transloco key). The editor UI hides this field — use dialogTitle instead. Still read at runtime for backward compatibility with older schemas. |
types | string[] | Whitelist of allowed type codes. If only one is provided, the LOV becomes readonly. Semantics differ by entityType: for entity-type mode (Customer/Order/Ticket/...) filters on the type-entity code (CustomerType.code, OrderType.code, ...). For EntityInstanceConfiguration forwarded as productTypeCodes → filter on entitySpecificationType.code. |
hiddenTypes | string[] | Type codes to hide from the LOV. Same semantics as types. |
defaultType | string | Pre-selected type code. |
createPrivilege | string | Privilege code required to use the button. |
Inventory — only when entityType = "EntityInstanceConfiguration"
| Property | Type | Description |
|---|---|---|
inventoryType | string | One of: "PRODUCT", "SERVICE", "RESOURCE", "PRICELIST". |
Catalog — only when entityType is "EntityCatalogSpecification" or "EntityCatalogCategory"
| Property | Type | Description |
|---|---|---|
catalogCode | string | Code of the parent catalog. |
categoryId | string | Category within the catalog. Only shown for EntityCatalogSpecification. |
Register — only when entityType = "RegisterValue"
| Property | Type | Description |
|---|---|---|
registerCode | string | Code of the register. |
Context data
| Property | Type | Description |
|---|---|---|
contextData | any | JSON / JEXL object used to pre-fill the new entity form. Keys matching the form structure are filled in automatically. |
Pre-script
Runs before the dialog opens. Its output is merged with contextData and used to pre-fill the form.
| Property | Type | Description |
|---|---|---|
preScriptCode | string | Script code to execute. |
preScriptData | any | JSON input payload for the pre-script. |
Save script
Runs after the user confirms the dialog. Receives the form data as input.
| Property | Type | Description |
|---|---|---|
scriptCode | string | Script code to execute on save. |
scriptData | any | Additional JSON input merged with the form data. |
Success / error actions
Dispatched synchronously after the save completes (or the dialog is cancelled / the save script fails).
| Property | Type | Description |
|---|---|---|
successActions | StoreAction[] | NgRx-style actions dispatched on success. Each: {action, actionData?, passScriptDataToAction?}. |
errorActions | StoreAction[] | NgRx-style actions dispatched on error / cancel. |
successDo | EffectAction[] | Fluent-forms effect actions run on success (set, clear, copy, script). |
errorDo | EffectAction[] | Fluent-forms effect actions run on error / cancel. |
Schema shape
Every example below conforms to this shape:
{
"type": "layout", // always "layout" — this widget produces no value
"title": "...", // button label
"widget": {
"type": "dtl-create-entity-button", // runtime type (not the designer alias)
"tooltip": "...", // optional
"customIconCssClass": "...", // optional
"customCssClass": "...", // optional
"disabled": false, // optional
"hidden": false, // optional
},
"config": {
// everything else: entityType, dialogTitle, contextData, scripts, actions, ...
},
}
Only the six keys in widget above are remapped by the widgetMapper — anything else placed inside widget will be silently ignored. All behavioral configuration belongs in config.
Runtime flow
- User clicks the button.
- If
preScriptCodeis set, it runs first; its result is merged withcontextDatainto the form's initial values. - The dialog opens. Depending on
entityTypeit shows:- an entity-type LOV (Customer, Order, Ticket, ...), or
- a catalog-specification LOV (EntityInstanceConfiguration with
inventoryType), or - no LOV at all — the context (catalog, category, register) determines the spec directly.
- The form for the chosen EntitySpecification is rendered. Resolution order for
formSelector: explicitconfig.formSelector→FrameworkPluginServicelookup (registeredtsm-controls-configuration.tsentries withuseType: 'FORM_NEW'matching the entityType;EntityInstanceConfigurationderives frominventoryType) → hardcoded entity-type defaults (RegisterValue/EntityCatalogSpecification/EntityCatalogCategory). If a spec-specific form isn't found,fallbackFormCodeis tried; for the 3 hardcoded entity types there are also hardcoded fallbacks (e.g.UserRegisterValue.New.Default). - User confirms. If
scriptCodeis set, it runs with{...formData, scriptData}as input. successActions+successDorun on success;errorActions+errorDorun on cancel or failure.
Tips and pitfalls
- Layout, not a value widget. The node's
typeis"layout"because the button itself has no form value. Do not wrap it in"type": "widget". - Keep configuration in
config. Only the six widget-mapper keys (customIconCssClass,customCssClass,disabled,hidden,tooltip, plus thetypediscriminator) belong underwidget. Everything else (entityType,scriptCode,contextData,successActions, ...) goes underconfig. formSelectorvsfallbackFormCode. Most of the time leaveformSelectorempty — the widget auto-resolves it fromFrameworkPluginService(matchesuseType: 'FORM_NEW' + entityTypeagainst registeredtsm-controls-configuration.tsentries). ExplicitformSelectoroverrides the lookup.fallbackFormCodeis consulted when the resolvedformSelectordoesn't match any form on the chosen EntitySpecification.alwaysEnabledfor readonly parents. Set it totruewhen the surrounding form is in readonly / view mode but the "Add" action should still be available.- Effects need a parent form.
successDo/errorDooperate on the parentTsmFormGroup. This wiring is automatic inside the fluent-forms runtime — no manual setup is needed; just make sure the button lives inside a fluent-forms-rendered form. - Conditional sections. The Inventory / Catalog / Register sections of the editor appear only when
entityTypematches. SwitchingentityTypeactively resets the dependent fields (inventoryType,catalogCode,categoryId,registerCode) — so leftover values from a previous selection aren't silently persisted in the schema. newRecordConfig.typessemantics differ by entityType. For entity-type mode (Customer/Order/Ticket/...) thetypesarray filters the type-entity LOV oncode. ForEntityInstanceConfigurationit's forwarded totsm-catalog-specification-lovasproductTypeCodesand filters onentitySpecificationType.code— i.e. on the spec-type's code, not on the catalog-specification's owncode. See scenario 3 below for details.
Examples
Each example below is a complete layout node. Drop it into your schema's items array, or paste it as the entire schema ({ "items": [ ...example... ] }) for a quick smoke test in the designer.
1. Minimal — Customer button
The shortest useful form: one entityType, a title, and a severity. newRecordConfig.types narrows the type LOV to a single existing customer type (verified to exist in the reference dev backend — Ukazkovy).
{
"type": "layout",
"title": "New Customer",
"widget": {
"type": "dtl-create-entity-button"
},
"config": {
"entityType": "Customer",
"buttonSeverity": "p-button-primary",
"newRecordConfig": {
"types": ["Ukazkovy"],
"defaultType": "Ukazkovy"
}
}
}
2. Styled Order button with custom dialog
A raised success-green button with a pi pi-plus icon, a custom dialog title, and a half-viewport-wide dialog. Restricts the type LOV to two real order types in the reference backend (Standardni, TestGenerovani) via newRecordConfig.types. Since this is entity-type mode, types filters on OrderType.code (see scenario 3 for how the same field behaves differently in catalog-spec mode).
{
"type": "layout",
"title": "Create Order",
"widget": {
"type": "dtl-create-entity-button",
"customIconCssClass": "pi pi-plus",
"tooltip": "Opens a dialog for creating a new order"
},
"config": {
"entityType": "Order",
"buttonAppearance": "p-button p-button-raised",
"buttonSeverity": "p-button-success",
"dialogTitle": "New sales order",
"dialogWidth": "50vw",
"newRecordConfig": {
"types": ["Standardni", "TestGenerovani"],
"defaultType": "Standardni"
}
}
}
3. Create Inventory Product
Uses EntityInstanceConfiguration with inventoryType: "PRODUCT" so the dialog shows a tsm-catalog-specification-lov populated with all instantiable PRODUCT-type catalog specifications from the backend. Without any filter the user can pick from the full set.
{
"type": "layout",
"title": "Add Product",
"widget": {
"type": "dtl-create-entity-button"
},
"config": {
"entityType": "EntityInstanceConfiguration",
"inventoryType": "PRODUCT",
"buttonAppearance": "p-button p-button-raised",
"buttonSeverity": "p-button-primary"
}
}
newRecordConfig.typeshas a different meaning in catalog-spec mode. For entity-type mode (Customer/Order/Ticket/...)typesfilters on the type-entitycode(CustomerType.code, OrderType.code, ...). ForEntityInstanceConfigurationit's forwarded asproductTypeCodesto the catalog-specification LOV, which filters onentitySpecificationType.code— i.e. on the spec-type's code, not on the catalog-specification's own code. If you want to narrow the LOV, settypesto values that matchentitySpecificationType.codeon your target specs (not the specs' own codes). In the reference dev backend none of the PRODUCT specs currently haveentitySpecificationTypepopulated, so anytypesfilter would empty the LOV — that's why this example omits it.
4. Create Catalog Specification
Catalog-scoped: the dialog opens directly with the form for the given catalog + category, no type LOV. Uses catalog Zdroj + category Zdroj.1 (UUID 4061fa84-eaac-420d-a3c0-788f3589b00b) — both confirmed in the reference backend.
{
"type": "layout",
"title": "New Catalog Item",
"widget": {
"type": "dtl-create-entity-button"
},
"config": {
"entityType": "EntityCatalogSpecification",
"catalogCode": "Zdroj",
"categoryId": "4061fa84-eaac-420d-a3c0-788f3589b00b",
"dialogWidth": "900px",
"buttonSeverity": "p-button-primary"
}
}
5. Create Register Value
Register-scoped. alwaysEnabled: true makes the button usable even on readonly detail pages (typical for a "+" action next to a lookup field). Uses register Mpe.Cfg — confirmed in the reference backend.
{
"type": "layout",
"title": "Add register value",
"widget": {
"type": "dtl-create-entity-button",
"customIconCssClass": "pi pi-plus"
},
"config": {
"entityType": "RegisterValue",
"registerCode": "Mpe.Cfg",
"alwaysEnabled": true,
"buttonAppearance": "p-button p-button-text",
"buttonSeverity": "p-button-primary"
}
}
6. Scripted Customer with context
The most complete scenario: pre-fill from the parent record via contextData + preScriptCode, run a save-script on confirm, dispatch a reload action on success, show a toast on failure, and update a timestamp field in the parent form with a SET effect. Uses Customer entity (Ukazkovy type) and the real SPEL script Crm.Customer.Get as an illustrative pre/save script — swap for your own script codes.
{
"type": "layout",
"title": "Create Follow-up Customer",
"widget": {
"type": "dtl-create-entity-button",
"tooltip": "Pre-fills context from the parent record"
},
"config": {
"entityType": "Customer",
"buttonAppearance": "p-button p-button-raised",
"buttonSeverity": "p-button-warning",
"dialogTitle": "New follow-up customer",
"dialogWidth": "60vw",
"newRecordConfig": {
"types": ["Ukazkovy"],
"defaultType": "Ukazkovy"
},
"contextData": "${ { parentId: $value.id, priority: $value.priority } }",
"preScriptCode": "Crm.Customer.Get",
"preScriptData": {
"customerKey": "${ $value.id }"
},
"scriptCode": "Crm.Customer.Get",
"scriptData": {
"channel": "web"
},
"successActions": [
{
"action": "[Customer] Reload",
"actionData": {"id": "${ $value.id }"},
"passScriptDataToAction": true
}
],
"errorActions": [
{
"action": "[Toast] Show Error",
"actionData": {"message": "Failed to create follow-up customer"}
}
],
"successDo": [
{
"type": "set",
"field": "lastFollowUpCreatedAt",
"value": "${ now() }"
}
],
"errorDo": [
{
"type": "clear",
"field": "followUpInProgress",
"mode": "empty"
}
]
}
}
Full input reference
Every input the widget component accepts. Use this table to spot-check any key you put into config (or widget for the widget-mapped ones marked with †).
| Input | Type | Default | Section |
|---|---|---|---|
title † | string | — | Appearance (root) |
tooltip † | string | — | Appearance (widget) |
hidden † | boolean | false | Appearance (widget) |
disabled † | boolean | false | Appearance (widget) |
customIconCssClass † | string | — | Appearance (widget) |
customCssClass † | string | — | Appearance (widget) |
buttonAppearance | string | "p-button p-button-text" | Appearance |
buttonSeverity | string | "p-button-primary" | Appearance |
buttonPriority | string | — | Appearance |
readonly | boolean | false | Appearance |
alwaysEnabled | boolean | false | Entity configuration |
entityType | string | — | Entity configuration |
entitySpecIdProperty | string | "entitySpecId" | Entity configuration |
formSelector | string | — | Entity configuration |
fallbackFormCode | string | — | Entity configuration |
newRecordConfig | NewRecordConfig | — | New record config |
dialogTitle | string | — | Entity configuration |
dialogWidth | string | "900px" | Entity configuration |
inventoryType | string | — | Inventory |
catalogCode | string | — | Catalog |
categoryId | string | — | Catalog |
registerCode | string | — | Register |
contextData | any | — | Context data |
preScriptCode | string | — | Pre-script |
preScriptData | any | — | Pre-script |
scriptCode | string | — | Save script |
scriptData | any | — | Save script |
successActions | StoreAction[] | [] | Success actions |
errorActions | StoreAction[] | [] | Error actions |
successDo | EffectAction[] | [] | Success effects |
errorDo | EffectAction[] | [] | Error effects |
localizationData | LocalizationVersionData | — | (wired by runtime) |
rootControl | TsmFormGroup | — | (wired by runtime) |
rootFormId | string | — | (wired by runtime) |
† = also exposed as a widget.* key via the widgetMapper.