Skip to main content
Version: 2.4

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

  1. Open the fluent-forms designer for your form.
  2. In the component palette, open the Advanced category → Secondary group.
  3. Drag the Create Entity item (icon: plus / button) onto the canvas.
  4. 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:

WhatRequired byFails as
Entity type with the given entityType code, its microservice and config.api.entityEndpoint registeredall scenariosempty LOV / empty dialog
Microservice backendUrl + apiVersion reachable from the browserall scenariosnetwork 404 from the LOV
At least one {entityType}-type row (e.g. one CustomerType, OrderType, TicketType)generic entity-type modeempty LOV, Save disabled
EntitySpecification pointed to by entitySpecIdProperty on the selected typeall scenariosempty form body
Form specification matching formSelector (JEXL) or fallbackFormCodeall scenariosform area blank
inventoryType row with instantiable: true in the catalogEntityInstanceConfigurationempty catalog-spec LOV
Catalog row with the given catalogCode; category row with the given categoryIdEntityCatalogSpecification / EntityCatalogCategoryform body blank
Register row with the given registerCodeRegisterValueform body blank
Script with the given preScriptCode / scriptCodescenarios that use scriptsclick errors out (preScript) or Save fails (saveScript)
createPrivilege from newRecordConfig granted to the current userscenarios with privilege gatebutton 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):

  1. 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, selectMicroserviceByCode would crash with TypeError: Cannot read properties of undefined (reading 'microservice') and the dialog body stayed blank.
  2. Selected-type wiringselectedType is now derived from typeControl.valueChanges (via toSignal). 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 and formResource never fired.
  3. formSelector auto-lookup — resolved through FrameworkPluginService matching on registered tsm-controls-configuration.ts entries (see runtime flow step 4).
  4. entitySpecIdProperty convention — Order/Ticket/Lead/Person/Account default to {entity}SpecId without 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

PropertyTypeDefaultDescription
titlestringButton label. Lives at the schema node root (sibling of widget), not inside config. Localizable via the form's localization data.
buttonAppearancestring"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".
buttonSeveritystring"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.
customIconCssClassstringCSS class for the icon (e.g. "pi pi-plus" or "tsm-icon-customer"). Lives under widget.
customCssClassstringExtra CSS classes appended to the button element. Lives under widget.
tooltipstringHover tooltip. Lives under widget.
hiddenbooleanfalseHides the button at runtime (still visible in the designer). Lives under widget.
disabledbooleanfalseDisables the button (unless alwaysEnabled is set). Lives under widget.

Entity configuration

PropertyTypeDefaultDescription
entityTypestring— (required)Entity type code — e.g. "Customer", "Order", "Ticket", "EntityInstanceConfiguration", "EntityCatalogSpecification", "EntityCatalogCategory", "RegisterValue". Drives which LOV and special sections apply.
entitySpecIdPropertystring"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.
formSelectorstringSelector 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.
fallbackFormCodestringForm code used when the EntitySpecification has no form matching formSelector (e.g. "Customer.Forms.Default.New").
dialogTitlestringCustom dialog title. If omitted, a default localized title is used.
dialogWidthstring"900px"Dialog width including unit ("900px", "50vw", "80%", ...).
alwaysEnabledbooleanfalseIf 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.

PropertyTypeDescription
labelstringLegacy dialog-title override (transloco key). The editor UI hides this field — use dialogTitle instead. Still read at runtime for backward compatibility with older schemas.
typesstring[]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.
hiddenTypesstring[]Type codes to hide from the LOV. Same semantics as types.
defaultTypestringPre-selected type code.
createPrivilegestringPrivilege code required to use the button.

Inventory — only when entityType = "EntityInstanceConfiguration"

PropertyTypeDescription
inventoryTypestringOne of: "PRODUCT", "SERVICE", "RESOURCE", "PRICELIST".

Catalog — only when entityType is "EntityCatalogSpecification" or "EntityCatalogCategory"

PropertyTypeDescription
catalogCodestringCode of the parent catalog.
categoryIdstringCategory within the catalog. Only shown for EntityCatalogSpecification.

Register — only when entityType = "RegisterValue"

PropertyTypeDescription
registerCodestringCode of the register.

Context data

PropertyTypeDescription
contextDataanyJSON / 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.

PropertyTypeDescription
preScriptCodestringScript code to execute.
preScriptDataanyJSON input payload for the pre-script.

Save script

Runs after the user confirms the dialog. Receives the form data as input.

PropertyTypeDescription
scriptCodestringScript code to execute on save.
scriptDataanyAdditional 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).

PropertyTypeDescription
successActionsStoreAction[]NgRx-style actions dispatched on success. Each: {action, actionData?, passScriptDataToAction?}.
errorActionsStoreAction[]NgRx-style actions dispatched on error / cancel.
successDoEffectAction[]Fluent-forms effect actions run on success (set, clear, copy, script).
errorDoEffectAction[]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

  1. User clicks the button.
  2. If preScriptCode is set, it runs first; its result is merged with contextData into the form's initial values.
  3. The dialog opens. Depending on entityType it 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.
  4. The form for the chosen EntitySpecification is rendered. Resolution order for formSelector: explicit config.formSelectorFrameworkPluginService lookup (registered tsm-controls-configuration.ts entries with useType: 'FORM_NEW' matching the entityType; EntityInstanceConfiguration derives from inventoryType) → hardcoded entity-type defaults (RegisterValue/EntityCatalogSpecification/EntityCatalogCategory). If a spec-specific form isn't found, fallbackFormCode is tried; for the 3 hardcoded entity types there are also hardcoded fallbacks (e.g. UserRegisterValue.New.Default).
  5. User confirms. If scriptCode is set, it runs with {...formData, scriptData} as input.
  6. successActions + successDo run on success; errorActions + errorDo run on cancel or failure.

Tips and pitfalls

  • Layout, not a value widget. The node's type is "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 the type discriminator) belong under widget. Everything else (entityType, scriptCode, contextData, successActions, ...) goes under config.
  • formSelector vs fallbackFormCode. Most of the time leave formSelector empty — the widget auto-resolves it from FrameworkPluginService (matches useType: 'FORM_NEW' + entityType against registered tsm-controls-configuration.ts entries). Explicit formSelector overrides the lookup. fallbackFormCode is consulted when the resolved formSelector doesn't match any form on the chosen EntitySpecification.
  • alwaysEnabled for readonly parents. Set it to true when the surrounding form is in readonly / view mode but the "Add" action should still be available.
  • Effects need a parent form. successDo / errorDo operate on the parent TsmFormGroup. 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 entityType matches. Switching entityType actively resets the dependent fields (inventoryType, catalogCode, categoryId, registerCode) — so leftover values from a previous selection aren't silently persisted in the schema.
  • newRecordConfig.types semantics differ by entityType. For entity-type mode (Customer/Order/Ticket/...) the types array filters the type-entity LOV on code. For EntityInstanceConfiguration it's forwarded to tsm-catalog-specification-lov as productTypeCodes and filters on entitySpecificationType.code — i.e. on the spec-type's code, not on the catalog-specification's own code. 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.types has a different meaning in catalog-spec mode. For entity-type mode (Customer/Order/Ticket/...) types filters on the type-entity code (CustomerType.code, OrderType.code, ...). For EntityInstanceConfiguration it's forwarded as productTypeCodes to the catalog-specification LOV, which filters on entitySpecificationType.code — i.e. on the spec-type's code, not on the catalog-specification's own code. If you want to narrow the LOV, set types to values that match entitySpecificationType.code on your target specs (not the specs' own codes). In the reference dev backend none of the PRODUCT specs currently have entitySpecificationType populated, so any types filter 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 †).

InputTypeDefaultSection
titlestringAppearance (root)
tooltipstringAppearance (widget)
hiddenbooleanfalseAppearance (widget)
disabledbooleanfalseAppearance (widget)
customIconCssClassstringAppearance (widget)
customCssClassstringAppearance (widget)
buttonAppearancestring"p-button p-button-text"Appearance
buttonSeveritystring"p-button-primary"Appearance
buttonPrioritystringAppearance
readonlybooleanfalseAppearance
alwaysEnabledbooleanfalseEntity configuration
entityTypestringEntity configuration
entitySpecIdPropertystring"entitySpecId"Entity configuration
formSelectorstringEntity configuration
fallbackFormCodestringEntity configuration
newRecordConfigNewRecordConfigNew record config
dialogTitlestringEntity configuration
dialogWidthstring"900px"Entity configuration
inventoryTypestringInventory
catalogCodestringCatalog
categoryIdstringCatalog
registerCodestringRegister
contextDataanyContext data
preScriptCodestringPre-script
preScriptDataanyPre-script
scriptCodestringSave script
scriptDataanySave script
successActionsStoreAction[][]Success actions
errorActionsStoreAction[][]Error actions
successDoEffectAction[][]Success effects
errorDoEffectAction[][]Error effects
localizationDataLocalizationVersionData(wired by runtime)
rootControlTsmFormGroup(wired by runtime)
rootFormIdstring(wired by runtime)

† = also exposed as a widget.* key via the widgetMapper.