@meshmakers/octo-ui 3.4.1450 → 3.4.1490
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/entity-forms/README.md +460 -0
- package/fesm2022/meshmakers-octo-ui-branding-settings.mjs +2 -2
- package/fesm2022/meshmakers-octo-ui-branding-settings.mjs.map +1 -1
- package/fesm2022/meshmakers-octo-ui-entity-forms.mjs +7880 -0
- package/fesm2022/meshmakers-octo-ui-entity-forms.mjs.map +1 -0
- package/fesm2022/meshmakers-octo-ui-tree-navigation-settings.mjs +2 -2
- package/fesm2022/meshmakers-octo-ui-tree-navigation-settings.mjs.map +1 -1
- package/fesm2022/meshmakers-octo-ui.mjs +985 -402
- package/fesm2022/meshmakers-octo-ui.mjs.map +1 -1
- package/lib/runtime-browser/styles/_button.scss +87 -0
- package/lib/runtime-browser/styles/_field-input.scss +16 -0
- package/lib/runtime-browser/styles/_flat-button.scss +14 -0
- package/lib/runtime-browser/styles/_index.scss +1 -1
- package/lib/runtime-browser/styles/_styles.scss +760 -1407
- package/lib/runtime-browser/styles/_theme.scss +248 -288
- package/lib/runtime-browser/styles/_variables.scss +26 -41
- package/package.json +5 -1
- package/types/meshmakers-octo-ui-entity-forms.d.ts +2594 -0
- package/types/meshmakers-octo-ui.d.ts +197 -4
- package/lib/runtime-browser/styles/_lcars-button.scss +0 -57
- package/lib/runtime-browser/styles/_lcars-flat-btn.scss +0 -11
- package/lib/runtime-browser/styles/_lcars-input.scss +0 -13
package/README.md
CHANGED
|
@@ -137,6 +137,7 @@ export class CustomerDataSourceDirective extends OctoGraphQlDataSource<CustomerD
|
|
|
137
137
|
| `FieldFilterEditorComponent` | Visual filter editor for queries |
|
|
138
138
|
| `EntityIdInfoComponent` | Entity ID display with copy-to-clipboard dropdown |
|
|
139
139
|
| `OctoLoaderComponent` | Animated OctoMesh logo loading indicator |
|
|
140
|
+
| `PageComponent` (`MM_PAGE`) | `<mm-page>` page layout: optional header (title, subtitle, actions) + scrolling content, no footer |
|
|
140
141
|
|
|
141
142
|
## Available Services
|
|
142
143
|
|
|
@@ -207,6 +208,113 @@ See [`src/lib/branding/BRANDING_USAGE.md`](./src/lib/branding/BRANDING_USAGE.md)
|
|
|
207
208
|
for the full list of CSS variables the library updates and the host-app
|
|
208
209
|
contract for the surface ladder.
|
|
209
210
|
|
|
211
|
+
## Page layout (`mm-page`)
|
|
212
|
+
|
|
213
|
+
`<mm-page>` (`PageComponent`) is the token-based page frame that replaces the
|
|
214
|
+
LCARS header / content panel / footer triple: an optional header (title,
|
|
215
|
+
subtitle, `[mmPageActions]`) and a scrolling content area, no footer. Import
|
|
216
|
+
`MM_PAGE` (component + slot directives); the title defaults to heading level 2
|
|
217
|
+
and the header is a plain `<div>`. See
|
|
218
|
+
[`src/lib/page/README.md`](./src/lib/page/README.md).
|
|
219
|
+
|
|
220
|
+
```html
|
|
221
|
+
<mm-page pageTitle="Adapters" pageSubtitle="12 registered">
|
|
222
|
+
<div mmPageActions><button kendoButton themeColor="primary">New adapter</button></div>
|
|
223
|
+
<mm-list-view …></mm-list-view>
|
|
224
|
+
</mm-page>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Theme tokens (overridable CSS variables)
|
|
228
|
+
|
|
229
|
+
`@include octo.theme()` emits the "Deep Sea" token set on `:root` (dark
|
|
230
|
+
default, light via `prefers-color-scheme` or `data-theme="light"`); source:
|
|
231
|
+
`src/lib/runtime-browser/styles/_theme.scss`. Hosts may override any of these
|
|
232
|
+
custom properties after the include. All `--theme-*` names of the former
|
|
233
|
+
theme are kept; tokens marked *new* were added in AB#5519.
|
|
234
|
+
|
|
235
|
+
| Group | Tokens |
|
|
236
|
+
|---|---|
|
|
237
|
+
| Surfaces | `--theme-bg-app`, `--theme-bg-app-end`, `--theme-bg-surface`, `--theme-bg-elevated`, `--theme-bg-overlay`, `--theme-bg-input`, *new:* `--theme-bg-sunken`, `--theme-bg-selected`, `--theme-bg-hover` |
|
|
238
|
+
| Text | `--theme-text-primary`, `--theme-text-secondary`, `--theme-text-muted`, `--theme-text-on-accent`, `--theme-text-accent`, *new:* `--theme-text-code` |
|
|
239
|
+
| Borders | `--theme-border-subtle`, `--theme-border-strong`, `--theme-border-divider` (= subtle), *new:* `--theme-border-default`, `--theme-focus-ring` (a `box-shadow` value) |
|
|
240
|
+
| Accent / AI (*new*) | `--theme-accent`, `--theme-accent-hover`, `--theme-accent-subtle`, `--theme-accent-2`, `--theme-ai`, `--theme-ai-subtle` |
|
|
241
|
+
| Status | `--theme-status-success`, `-warning`, `-error`, `-info`, *new:* `--theme-status-neutral`, `--theme-status-{success,warning,error,info,neutral}-subtle` |
|
|
242
|
+
| Effects | `--theme-shadow-panel`, `--theme-shadow-popup`; `--theme-glow-primary`, `--theme-glow-accent` (both `none`); `--theme-gradient-page`, `--theme-gradient-header`, `--theme-accent-bar`, `--theme-accent-line`, `--theme-panel-rule` (now flat colours) |
|
|
243
|
+
| Charts | `--theme-chart-1` … `--theme-chart-8` |
|
|
244
|
+
| Ink overlays | `--theme-ink-02` … `--theme-ink-70` (text colour at n % — flips with the theme) |
|
|
245
|
+
| Typography (*new*, theme-invariant) | `--theme-font-display` (Montserrat), `--theme-font-ui` (Roboto), `--theme-font-mono` (Roboto Mono) |
|
|
246
|
+
| Spacing (*new*) | `--theme-space-1` … `--theme-space-8` (4 px grid: 4, 8, 12, 16, 20, 24, 28, 32 px) |
|
|
247
|
+
| Radius (*new*) | `--theme-radius-xs` 2 px (chips), `-sm` 4 px (buttons, inputs), `-md` 6 px (cards, panels), `-lg` 8 px (dialogs, popovers) |
|
|
248
|
+
| Motion (*new*) | `--theme-motion-fast` 120 ms, `--theme-motion-base` 200 ms, `--theme-motion-easing`; both durations become `0ms` under `prefers-reduced-motion: reduce` |
|
|
249
|
+
|
|
250
|
+
Legacy aliases (AB#5526 phase 2): only `--lcars-font-primary` →
|
|
251
|
+
`--theme-font-display`, `--lcars-font-mono` → `--theme-font-mono` and
|
|
252
|
+
`--lcars-transition-fast` → `--theme-motion-fast` + easing remain, kept for the
|
|
253
|
+
Meshmakers App until it migrates. All other `--lcars-*` variables (glows,
|
|
254
|
+
button bases, input focus, radii, panel tokens, `--lcars-transition-normal`)
|
|
255
|
+
were deleted — use the `--theme-*` tokens.
|
|
256
|
+
|
|
257
|
+
Body text: `octo.styles()` sets `body { font-family: var(--theme-font-ui) }`
|
|
258
|
+
(Roboto) since AB#5526; `h1`–`h3`, page and dialog titles, `mm-page` titles and
|
|
259
|
+
KPI numbers use `--theme-font-display` (Montserrat 600).
|
|
260
|
+
|
|
261
|
+
### Component styling in `octo.styles()` (Deep Sea, AB#5526)
|
|
262
|
+
|
|
263
|
+
- **Buttons are flat**: primary = `--theme-accent` with `--theme-text-on-accent`
|
|
264
|
+
(hover `--theme-accent-hover`), base = `--theme-bg-elevated` with a
|
|
265
|
+
`--theme-border-default` hairline (hover `--theme-bg-hover` +
|
|
266
|
+
`--theme-border-strong`), outline/flat variants transparent; error =
|
|
267
|
+
`--theme-status-error`. Sentence case, `--theme-font-ui`, `--theme-radius-sm`,
|
|
268
|
+
keyboard focus `--theme-focus-ring`. Both themes use the same rules, so the
|
|
269
|
+
former light-only button overrides are no-ops.
|
|
270
|
+
- **No decoration**: no gradients (sliders included), glows, pulses, scanlines
|
|
271
|
+
or text-shadows; grid headers, tabs (Kendo and dockview), process designer
|
|
272
|
+
palette/inspector headers, dialog titles and context menus are sentence case
|
|
273
|
+
in `--theme-font-ui` (the only uppercase left is the avatar initials); popups,
|
|
274
|
+
dialogs and menus use `--theme-bg-overlay` + `--theme-border-default` +
|
|
275
|
+
`--theme-shadow-popup`; the drawer is flat `--theme-bg-elevated`.
|
|
276
|
+
- **`theme-overrides()`**: `light-theme-surface-overrides` was reduced to what
|
|
277
|
+
the tokens cannot express (the old light card, `#d7ebe5` table band, drawer
|
|
278
|
+
and app-bar treatments are gone — both themes agree now). It also emits a
|
|
279
|
+
sentence-case rule (`html:root .k-label, kendo-label, .section-title`) that
|
|
280
|
+
outranks host component styles in both themes.
|
|
281
|
+
- **Legacy LCARS page classes — kept only for the Meshmakers App** (AB#5526
|
|
282
|
+
phase 2; the Refinery Studio no longer uses them): `.lcars-page-header`,
|
|
283
|
+
`.page-title` / `.title-prefix` / `.title-main`, `.header-content` and
|
|
284
|
+
`.lcars-content-panel` (surface card, with its `.k-grid` rule) keep a neutral
|
|
285
|
+
Deep Sea definition; `.lcars-header-accent`, `.lcars-header-line`,
|
|
286
|
+
`.panel-accent-top/-bottom`, `.lcars-footer` and `.footer-*` render nothing
|
|
287
|
+
(`display: none`). Everything else was deleted: `.header-stats`,
|
|
288
|
+
`.stat-badge`, `.lcars-panel`, `.lcars-panel-asymmetric`, `.lcars-header-bar`,
|
|
289
|
+
`.lcars-divider`, `.lcars-text-*`, `.lcars-bg-*`, `.lcars-border-mint`, the
|
|
290
|
+
glow/scanline/pulse utilities, and the `_lcars-button.scss`,
|
|
291
|
+
`_lcars-flat-btn.scss` and `_lcars-input.scss` mixin files (now the internal
|
|
292
|
+
`_button.scss` / `octo-button`, `_flat-button.scss` / `flat-button` and
|
|
293
|
+
`_field-input.scss` / `field-input`). New pages use `<mm-page>`.
|
|
294
|
+
|
|
295
|
+
Deviations from concept §6.2 (contrast-driven): light `--theme-text-accent` /
|
|
296
|
+
`--theme-accent` `#2c7d6d` (concept `#2e8473`, 4.16:1 on the light canvas →
|
|
297
|
+
4.55:1), light `--theme-accent-hover` `#24685a` (concept `#266f61`),
|
|
298
|
+
`--theme-text-muted` dark `#77869c` (concept `#687890`) and light `#657189`
|
|
299
|
+
(concept `#75829a`), both ≥ 4.5:1 on `--theme-bg-input`. The Kendo bridge points `--kendo-color-primary`,
|
|
300
|
+
`--kendo-color-surface(-alt)`, `--kendo-color-border`, `--kendo-color-base*`,
|
|
301
|
+
`--kendo-border-radius-*` and `--kendo-font-family` at these tokens.
|
|
302
|
+
|
|
303
|
+
Fonts are not bundled: the host loads Montserrat, Roboto and Roboto Mono
|
|
304
|
+
(the Refinery Studio does so via Google Fonts in `index.html`).
|
|
305
|
+
|
|
306
|
+
## Secondary Entry Points
|
|
307
|
+
|
|
308
|
+
Heavy admin editors live in their own entry points so apps that do not use them keep the
|
|
309
|
+
primary bundle small. Each one imports only the public API of `@meshmakers/octo-ui`.
|
|
310
|
+
|
|
311
|
+
| Entry point | Contents |
|
|
312
|
+
|-------------|----------|
|
|
313
|
+
| `@meshmakers/octo-ui/branding` | Branding services, `provideOctoBranding`, theme switcher |
|
|
314
|
+
| `@meshmakers/octo-ui/branding-settings` | Branding settings page (`BRANDING_ROUTES`) |
|
|
315
|
+
| `@meshmakers/octo-ui/tree-navigation-settings` | Editor for `System.UI/TreeNavigationConfiguration` |
|
|
316
|
+
| `@meshmakers/octo-ui/entity-forms` | Form-driven entity list / create / edit pages (`<mm-entity-page>`, `<mm-entity-list>`, `<mm-entity-form>`, `entityFormRoutes()`), driven by `System.UI/EntityForm` — see [`entity-forms/README.md`](./entity-forms/README.md) |
|
|
317
|
+
|
|
210
318
|
## Build
|
|
211
319
|
|
|
212
320
|
```bash
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# @meshmakers/octo-ui/entity-forms
|
|
2
|
+
|
|
3
|
+
Form-driven list, create and edit pages for OctoMesh runtime entities (AB#5522).
|
|
4
|
+
|
|
5
|
+
The layout of a page — sections, field order, labels, help texts, editors, visibility rules,
|
|
6
|
+
list columns, capabilities — comes from the tenant's `System.UI/EntityForm` entities
|
|
7
|
+
(System.UI ≥ 2.8.0, seeded by the `System.UI.EntityForms` blueprint). Types without a form, and
|
|
8
|
+
tenants without System.UI, fall back to a built-in copy of the seeded `form-default`, so every
|
|
9
|
+
CK type gets a usable page.
|
|
10
|
+
|
|
11
|
+
This is a **secondary entry point**: it imports only the public API of `@meshmakers/octo-ui`
|
|
12
|
+
and other packages, and hosts that do not use it keep the primary bundle unchanged.
|
|
13
|
+
|
|
14
|
+
## Building blocks
|
|
15
|
+
|
|
16
|
+
| Export | Purpose |
|
|
17
|
+
|--------|---------|
|
|
18
|
+
| `<mm-entity-page>` (`EntityPageComponent`) | Route component: list, create, edit and singleton flows, save, delete, unsaved-changes guard, breadcrumbs |
|
|
19
|
+
| `entityFormRoutes(opts?)` | The three child routes `''` / `new` / `:rtId` of a page |
|
|
20
|
+
| `<mm-entity-list>` (`EntityListComponent`) | `mm-list-view` of a resolved form (columns, Copy ID, delete, "New" incl. subtype picker) |
|
|
21
|
+
| `<mm-entity-form>` (`EntityFormComponent`) | The form itself (sections, editors, validation, change set) |
|
|
22
|
+
| `EntityFormService` | Loads forms + CK metadata (cached per tenant) and resolves the form for a type or form key |
|
|
23
|
+
| `EntityFormDataService` | Reads values / secret state / associations; create, update (incl. `clearSecretAttributes`), delete |
|
|
24
|
+
| `parseEntityForms`, `pickEntityForm`, `resolveEntityForm`, … | Pure functions behind the service, e.g. for a forms editor |
|
|
25
|
+
| `entityFormCatalog(forms)`, `entityFormKey(form)` | Settings overview: one entry per target type whose effective form has a `Category`, with the URL key |
|
|
26
|
+
| `EntityFormsMessages`, `DEFAULT_ENTITY_FORMS_MESSAGES` | All UI strings (English defaults; pass `Partial<…>` via `messages`) |
|
|
27
|
+
| `ENTITY_FORM_FALLBACK_FORMS`, `provideEntityFormFallbacks`, `selectFallbackForms` | Host-provided built-in forms that apply per type where the resolution would end at `form-default` (see below) |
|
|
28
|
+
| `ENTITY_FORM_ACTION_CONFIRMATION` | Optional host hook asked before a delete (e.g. a production-mode check) |
|
|
29
|
+
| `ENTITY_FORM_DANGER_CONFIRMATION`, `entityDeleteConfirmation` | Optional host replacement of the delete danger confirmation (AB#5579) |
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Routes (recommended)
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { entityFormRoutes } from '@meshmakers/octo-ui/entity-forms';
|
|
37
|
+
|
|
38
|
+
export const routes: Routes = [
|
|
39
|
+
{
|
|
40
|
+
path: 'sftp',
|
|
41
|
+
children: entityFormRoutes({
|
|
42
|
+
formKey: 'sftp-configuration', // or ckTypeId: 'System.Communication/SftpConfiguration'
|
|
43
|
+
breadcrumbUrl: 'communication/sftp', // optional; adds {{entityFormTitle}} / {{entityName}} crumbs
|
|
44
|
+
canWrite: true, // optional; default true
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The host must provide what the library services expect:
|
|
51
|
+
|
|
52
|
+
- `provideOctoUi()` — includes `provideMmSharedUi()` (`ConfirmationService`,
|
|
53
|
+
`NotificationDisplayService`, `EntitySelectDialogService` for the reference picker) and
|
|
54
|
+
`CkTypeSelectorDialogService` (subtype picker). Hosts that do not use `provideOctoUi()` must
|
|
55
|
+
call `provideMmSharedUi()` themselves.
|
|
56
|
+
- A `<div kendoDialogContainer></div>` (record row dialog, confirmations) and
|
|
57
|
+
`<div kendoWindowContainer></div>` (CK type selector) in the app shell.
|
|
58
|
+
- Apollo for the tenant; optionally `TENANT_ID_PROVIDER` (cache scope per tenant) and
|
|
59
|
+
`BreadCrumbService` (breadcrumb labels; skipped when absent).
|
|
60
|
+
|
|
61
|
+
### Route-parameter contract (stable; used by the Refinery Studio, AB#5523)
|
|
62
|
+
|
|
63
|
+
| URL (relative to the mount point) | Page state |
|
|
64
|
+
|-----------------------------------|------------|
|
|
65
|
+
| `''` | List. A **singleton** form skips the list and opens its entity (see below). |
|
|
66
|
+
| `new` | Create form. Route data `rtId: 'new'`. |
|
|
67
|
+
| `new?type=<rtCkTypeId>` | Create form for a concrete subtype (required for abstract types; set by the list's subtype picker). |
|
|
68
|
+
| `:rtId` | Edit form; read-only view when `canWrite` is false or the form has `CanEdit: false`. |
|
|
69
|
+
| `:rtId?type=<rtCkTypeId>` | Edit form for an entity of a derived type (set when the list opens such a row). Without it the page detects the type after loading and re-resolves. |
|
|
70
|
+
|
|
71
|
+
Optional `entityFormRoutes` options for the page heading and list: `title` (route data
|
|
72
|
+
`entityListTitle`) replaces the resolved form title as the list heading and breadcrumb label
|
|
73
|
+
(e.g. "All configurations" over `System/Configuration`); `showTypeColumn` (route data
|
|
74
|
+
`entityListTypeColumn`) adds a "Type" column with the display name of each row's CK type: the
|
|
75
|
+
`Name` of the type's entity form (`entityFormTypeTitles` / `ckTypeDisplayName`), else
|
|
76
|
+
`humanizeCkTypeName` — sentence case with known acronyms and brands (`EMailReceiverConfiguration`
|
|
77
|
+
→ "E-mail receiver configuration", `FinApiConfiguration` → "finAPI configuration",
|
|
78
|
+
`SftpConfiguration` → "SFTP configuration"; AB#5524). A
|
|
79
|
+
**singleton** form is titled with its form name (e.g. "Tenant mode"), never "Edit <entity name>".
|
|
80
|
+
|
|
81
|
+
Page inputs (bound by `withComponentInputBinding()` from route params / data, otherwise read from
|
|
82
|
+
`ActivatedRoute` — params, route data of the route and its ancestors, query params):
|
|
83
|
+
|
|
84
|
+
| Input | Source fallback | Meaning |
|
|
85
|
+
|-------|-----------------|---------|
|
|
86
|
+
| `formKey` | `data.formKey`, then `params.formKey` | `form-sftp-configuration` (rtWellKnownName) or the kebab type key `sftp-configuration`. Wins over `ckTypeId`. |
|
|
87
|
+
| `ckTypeId` | `data.ckTypeId`, then `params.ckTypeId` | Runtime CK type id of the target type. |
|
|
88
|
+
| `rtId` | `params.rtId`, `data.rtId` | Absent = list; `'new'` = create; otherwise edit. |
|
|
89
|
+
| `canWrite` | `data.canWrite`, then `true` | Write permission; the host maps roles to it (the library knows no role names). |
|
|
90
|
+
| `messages` | `data.messages` | `Partial<EntityFormsMessages>`. |
|
|
91
|
+
| `routerNavigation` | — | Default `true`. Set `false` and handle the `navigate` output to drive navigation yourself. |
|
|
92
|
+
|
|
93
|
+
The query parameter is deliberately named `type`, not `ckTypeId`, so input binding never
|
|
94
|
+
overwrites the page's `ckTypeId` (the form's target type).
|
|
95
|
+
|
|
96
|
+
Navigation is always **relative** (`..`, `new`, `:rtId`, with `replaceUrl` after a create), so
|
|
97
|
+
the routes work under any mount point. The `navigate` output (`{ kind: 'list'|'create'|'edit', rtId?, ckTypeId? }`)
|
|
98
|
+
is emitted for every transition.
|
|
99
|
+
|
|
100
|
+
Two more outputs let a host that embeds the page (e.g. the Studio's Data Explorer peek) keep its
|
|
101
|
+
own lists current:
|
|
102
|
+
|
|
103
|
+
| Output | Payload | When |
|
|
104
|
+
|--------|---------|------|
|
|
105
|
+
| `saved` | `{ kind: 'create' \| 'update', rtId, ckTypeId }` | After a successful create or update (not for an empty change set) |
|
|
106
|
+
| `deleted` | `{ rtId, ckTypeId }` | After the open entity was deleted from the form, before the navigation to the list |
|
|
107
|
+
|
|
108
|
+
Breadcrumb labels set via `BreadCrumbService.updateBreadcrumbLabels`: `entityFormTitle` (the
|
|
109
|
+
resolved form title) and `entityName` (entity `name`, else `rtWellKnownName`, else `rtId`; the
|
|
110
|
+
create title on `new`).
|
|
111
|
+
|
|
112
|
+
### Singleton forms
|
|
113
|
+
|
|
114
|
+
`Singleton: true` skips the list:
|
|
115
|
+
|
|
116
|
+
1. With `SingletonWellKnownName` the entity is loaded by that well-known name.
|
|
117
|
+
2. Without it, the first entity of the type is used.
|
|
118
|
+
3. If none exists, the create form opens (with write permission) and the well-known name is
|
|
119
|
+
written on first save; afterwards the page stays on the same URL in edit mode.
|
|
120
|
+
|
|
121
|
+
### Saving
|
|
122
|
+
|
|
123
|
+
`saveChanges(): Promise<boolean>` (also called by `UnsavedChangesGuard` on "Yes"):
|
|
124
|
+
|
|
125
|
+
- invalid form → all fields touched, warning, `false`;
|
|
126
|
+
- create → `EntityFormDataService.create`, then navigation to `:rtId` (replacing `new`);
|
|
127
|
+
- edit → only the changed attributes are sent (empty change set = "no changes"), then the
|
|
128
|
+
values are read again.
|
|
129
|
+
- a host `beforeSave` hook (see below) runs after validation and before the server call; a
|
|
130
|
+
veto shows a warning (the hook's message or `saveVetoed`), an error shows `saveError`, and
|
|
131
|
+
nothing is saved in either case.
|
|
132
|
+
|
|
133
|
+
### Before-save hook (`beforeSave`, AB#5623)
|
|
134
|
+
|
|
135
|
+
Normalise or derive values, or stop the save, right before it happens. Sync or async
|
|
136
|
+
(`Promise` / `Observable`, first value counts):
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const normaliseContact: EntityFormBeforeSaveHook = (changeSet) => {
|
|
140
|
+
for (const a of changeSet.attributes) {
|
|
141
|
+
if (typeof a.value === 'string') a.value = a.value.trim();
|
|
142
|
+
if (a.attributeName === 'iban' && typeof a.value === 'string') a.value = a.value.replace(/\s+/g, '').toUpperCase();
|
|
143
|
+
}
|
|
144
|
+
const email = changeSet.attributes.find((a) => a.attributeName === 'email');
|
|
145
|
+
if (email) {
|
|
146
|
+
if (typeof email.value === 'string' && email.value && !email.value.includes('@')) {
|
|
147
|
+
throw new EntityFormSaveVeto(translate('CONTACT.INVALID_EMAIL'));
|
|
148
|
+
}
|
|
149
|
+
changeSet.attributes.push({ attributeName: 'normalizedEmail', value: String(email.value ?? '').toLowerCase() });
|
|
150
|
+
}
|
|
151
|
+
// returning nothing saves the (mutated) change set; return a new change set to replace it,
|
|
152
|
+
// or null / false to veto silently
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- Where: `mm-entity-page [beforeSave]` > route option `beforeSave` (data `entityFormBeforeSave`)
|
|
157
|
+
> `ENTITY_FORM_BEFORE_SAVE`. `mm-entity-form [beforeSave]` (or the token) applies through
|
|
158
|
+
`getChangeSetForSave({ rtId })` for hosts that save themselves; the form never notifies.
|
|
159
|
+
- The hook gets a copy of the change set plus `{ mode, ckTypeId, rtId?, form }`. Attribute names
|
|
160
|
+
are camelCase; edit mode holds only the changed attributes, so derive from a source attribute
|
|
161
|
+
only when it is in the change set. An edit without changes skips the hook.
|
|
162
|
+
- **Secrets:** nothing beyond the change set reaches the hook. A SECRET attribute appears only as
|
|
163
|
+
the write value the user just typed (or in `clearSecretAttributes`), never as a stored value —
|
|
164
|
+
the form never reads stored secrets. Do not log the change set.
|
|
165
|
+
|
|
166
|
+
### Embedding without routes
|
|
167
|
+
|
|
168
|
+
```html
|
|
169
|
+
<mm-entity-list [model]="model" [canWrite]="canWrite" (openRequested)="open($event)" (createRequested)="create($event)" />
|
|
170
|
+
<mm-entity-form [model]="model" mode="edit" [state]="state" (dirtyChange)="dirty = $event" />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Resolve `model` with `EntityFormService.resolve(rtCkTypeId)` / `resolveByFormKey(key)` and the
|
|
174
|
+
state with `EntityFormDataService.load(model, { rtId })`.
|
|
175
|
+
|
|
176
|
+
## Settings overview (catalog)
|
|
177
|
+
|
|
178
|
+
`entityFormCatalog(await formService.getForms())` returns one entry per target type whose
|
|
179
|
+
**effective** form (tenant form before seeded, then higher `Priority`) has a `Category`:
|
|
180
|
+
`key`, `category` (lower case), `title` (form `Name`, else the humanized type name),
|
|
181
|
+
`description`, `icon`, `targetCkTypeId`, `includeDerivedTypes`, `singleton`.
|
|
182
|
+
`form-default` has no category and never appears. A tenant form replaces the delivered entry
|
|
183
|
+
(and can move it to another category, or hide it by leaving `Category` empty).
|
|
184
|
+
|
|
185
|
+
`entityFormKey(form)` is the URL key: the well-known name without `form-`
|
|
186
|
+
(`form-email-sender-configuration` → `email-sender-configuration`), otherwise the kebab type
|
|
187
|
+
name. The catalog prefers a delivered `form-*` name of the type, so URLs survive a tenant
|
|
188
|
+
override. `resolveByFormKey` accepts both forms.
|
|
189
|
+
|
|
190
|
+
`EntityFormDataService.count(ckTypeId, includeDerivedTypes?)` counts entities without reading
|
|
191
|
+
attributes (`attributeNames: []`); without `includeDerivedTypes` only the exact type is counted.
|
|
192
|
+
|
|
193
|
+
## Fallback forms (`ENTITY_FORM_FALLBACK_FORMS` / `provideEntityFormFallbacks`)
|
|
194
|
+
|
|
195
|
+
A host can ship built-in copies of delivered forms, for example while a new version of the
|
|
196
|
+
seeding blueprint has not been rolled out yet (AB#5524). Two ways to provide them:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
// root (pulls the entry point into the initial bundle):
|
|
200
|
+
providers: [{ provide: ENTITY_FORM_FALLBACK_FORMS, useValue: MY_FALLBACK_FORMS }]
|
|
201
|
+
// or on every lazy route that renders entity forms (registers them in the root service, one cache):
|
|
202
|
+
{ path: 'settings', providers: [provideEntityFormFallbacks(MY_FALLBACK_FORMS)], loadChildren: … }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- `EntityFormService.getForms()` appends a fallback **only where the normal resolution would end
|
|
206
|
+
at the chain end** — no form at all, or `form-default` (a form on `System/Entity`). A tenant or
|
|
207
|
+
seeded form for the exact type, or for an ancestor with `IncludeDerivedTypes`, always wins
|
|
208
|
+
(`selectFallbackForms`). Fallbacks for types the tenant does not have (no CK metadata) are
|
|
209
|
+
dropped. The first fallback per type wins. The result feeds `resolve`, `resolveByFormKey` and
|
|
210
|
+
`entityFormCatalog` unchanged.
|
|
211
|
+
- Fallbacks are also used when the forms cannot be loaded (no System.UI ≥ 2.8.0, query error).
|
|
212
|
+
- `registerFallbackForms(forms)` (behind `provideEntityFormFallbacks`) is idempotent per array and
|
|
213
|
+
drops the cached forms and resolutions once.
|
|
214
|
+
- They count as delivered forms (`isTenantForm` is forced to `false`, `source: 'seeded'`). Use the
|
|
215
|
+
delivered `rtWellKnownName` (`form-<kebab-type>`) so URL keys do not change when the seeded form
|
|
216
|
+
arrives, and leave `rtId` empty.
|
|
217
|
+
- A tenant form for the type without `Category` hides the entry, exactly like it hides a seeded one.
|
|
218
|
+
|
|
219
|
+
This is the per-type counterpart of the built-in `form-default` safety net; the host is
|
|
220
|
+
responsible for keeping its copies in step with the seed.
|
|
221
|
+
|
|
222
|
+
## Action confirmation (`ENTITY_FORM_ACTION_CONFIRMATION`)
|
|
223
|
+
|
|
224
|
+
Optional `(request: { action: 'delete', ckTypeId, count, description }) => Promise<boolean>`,
|
|
225
|
+
asked by `mm-entity-list` and `mm-entity-page` **before** their own danger confirmation; `false` (or a
|
|
226
|
+
throwing hook) cancels. The Refinery Studio maps it to its production-mode confirmation.
|
|
227
|
+
|
|
228
|
+
## Delete confirmation (`ENTITY_FORM_DANGER_CONFIRMATION`, AB#5579)
|
|
229
|
+
|
|
230
|
+
Deletes ask `ConfirmationService.showDangerConfirm` (shared-ui action guideline): the title names the
|
|
231
|
+
entity ("Delete Primary?") or the count ("Delete 3 entities?"), the confirming button says
|
|
232
|
+
"Delete entity" / "Delete entities", Cancel keeps the initial focus. Texts: the optional
|
|
233
|
+
`confirmDelete*` keys of `EntityFormsMessages` (the former `confirmDeleteTitle` / `confirmDeleteMessage` /
|
|
234
|
+
`confirmDeleteManyMessage` are no longer shown). A host that adds environment knowledge provides
|
|
235
|
+
`ENTITY_FORM_DANGER_CONFIRMATION: (options: DangerConfirmationOptions, request) => Promise<boolean>`
|
|
236
|
+
instead of the built-in dialog — e.g. the Studio's `DangerConfirmService.confirm` (production = type the
|
|
237
|
+
name); it then no longer needs the production check in `ENTITY_FORM_ACTION_CONFIRMATION`.
|
|
238
|
+
|
|
239
|
+
## `<mm-entity-list>`
|
|
240
|
+
|
|
241
|
+
- Inputs: `model` (required), `canWrite = true`, `messages`, `listStateKey`.
|
|
242
|
+
- Outputs: `createRequested { ckTypeId }`, `openRequested { rtId, ckTypeId }`, `deleted { rtId, ckTypeId }[]`.
|
|
243
|
+
- Rows are flattened (`rtId`, `ckTypeId`, `rtWellKnownName`, `rtDisplayName`,
|
|
244
|
+
`rtCreationDateTime`, `rtChangedDateTime`, `<attributeName>: value`), so a column `field`
|
|
245
|
+
equals the GraphQL attribute path and server-side sort / filter / search work.
|
|
246
|
+
- Derived types are listed when the form has `IncludeDerivedTypes` or the type is abstract;
|
|
247
|
+
otherwise a `ckTypeId EQUALS` field filter restricts the list to the exact type
|
|
248
|
+
(`runtimeEntities(ckId)` returns derived types by default).
|
|
249
|
+
- Column display: `chip` → badge, `date` → localized date, `mono` → monospace cell, else text.
|
|
250
|
+
Cells are formatted by the CK value type of the attribute like the reference display (AB#5547):
|
|
251
|
+
ENUM → the enum value's name (the API returns the key, e.g. `0`), BOOLEAN → yes / no
|
|
252
|
+
(`toggleOn` / `toggleOff` messages); a `chip` column maps the keys via `badgeMapping`.
|
|
253
|
+
- Context menu: **Copy ID** (RtId / CkTypeId / RtCkTypeId / RtEntityId), then — only with
|
|
254
|
+
`canWrite && CanDelete` — Delete with a confirmation. Toolbar "New" only with
|
|
255
|
+
`canWrite && CanCreate`.
|
|
256
|
+
- **Abstract types** (`createRequiresSubtype`): "New" opens `CkTypeSelectorDialogService`
|
|
257
|
+
restricted to concrete subtypes (`derivedFromRtCkTypeId`, `allowAbstract: false`);
|
|
258
|
+
cancelling emits nothing.
|
|
259
|
+
|
|
260
|
+
## Host extensions (AB#5623)
|
|
261
|
+
|
|
262
|
+
All optional; without them the components behave as before.
|
|
263
|
+
|
|
264
|
+
| Need | API |
|
|
265
|
+
|---|---|
|
|
266
|
+
| Save / Cancel below the form | `mm-entity-page [actionBarPosition]="'bottom'"` (route option `actionBarPosition`, data `entityPageActionBarPosition`) |
|
|
267
|
+
| Extra page actions (Export, Prefill, ...) | `<ng-template mmEntityPageActions let-ctx>` inside `mm-entity-page` — rendered in the list header and in the form's action bar; `ctx` = `EntityPageActionsContext` (`view`, `mode`, `rtId`, `ckTypeId`, `model`, `saving`, `page`) |
|
|
268
|
+
| Extra list actions | `mm-entity-list` `toolbarActions` / `rowActions` (icon buttons) / `rowMenuActions` (context menu); on the page `listToolbarActions` / `listRowActions` / `listRowMenuActions`. Plain `CommandItem`s; `onClick` gets the row as `e.data` |
|
|
269
|
+
| Default list order | `listDefaultSort` in a host fallback form, or `defaultSort` on list / page (route option `defaultSort`). A user sort (also a remembered one) wins; the header shows no marker for the default order |
|
|
270
|
+
| Translated labels / enum texts | `ENTITY_FORM_LABEL_RESOLVER` or the `labelResolver` input: `(request) => string \| null`; `request.kind` = `field`, `help`, `placeholder`, `section`, `sectionDescription`, `formTitle`, `formDescription`, `listColumn`, `recordColumn`, `enumOption`. Read a signal (language) inside to re-render on change; controls are not rebuilt |
|
|
271
|
+
| Fixed texts incl. Copy ID | `messages` (`copyId`, `copyIdTooltip`, `copiedId`, `copyFailed`, ...); `mm-entity-id-info` has `buttonText` / `tooltip` / `copiedMessage` / `copyFailedMessage` |
|
|
272
|
+
| Prefill a create form | `mm-entity-form [initialValues]` / `mm-entity-page [initialValues]` (object or `({ ckTypeId }) => values`; route option `initialValues`), or `entityFormPrefillState(values)` as `state` |
|
|
273
|
+
| Normalise / veto before saving | `beforeSave` on `mm-entity-page` / `mm-entity-form`, route option `beforeSave`, or `ENTITY_FORM_BEFORE_SAVE` — see [Before-save hook](#before-save-hook-beforesave-ab5623) |
|
|
274
|
+
| Set values from a host action | `EntityFormComponent.patchValues(values)` / `EntityPageComponent.patchFormValues(values)` — values count as edits (dirty, saved); secrets and read-only fields are skipped |
|
|
275
|
+
| Placeholder values of legacy STRING secrets | `ENTITY_FORM_SECRET_PLACEHOLDER_VALUES` (exact values that read "Not set"). SECRET-typed attributes (AB#5528) need nothing: their state comes from the server |
|
|
276
|
+
| Create / edit in a dialog over the list | `mm-entity-page [editMode]="'dialog'"` (route option `editMode`, data `entityPageEditMode`) — see [Dialog mode](#dialog-mode-editmode-dialog-ab5623) |
|
|
277
|
+
| CSS classes per list row (e.g. disabled rows) | `mm-entity-list [rowClass]` / `mm-entity-page [listRowClass]` (route option `rowClass`, data `entityListRowClass`) or `ENTITY_LIST_ROW_CLASS`: `(row) => string \| string[] \| Record<string, boolean> \| null`. Classes land on the grid's `<tr>`: style them globally (or `::ng-deep`) |
|
|
278
|
+
| Placeholder values of NON-secret config fields | `ENTITY_FORM_UNSET_PLACEHOLDER_VALUES` — see [Unset placeholders](#unset-placeholders-entity_form_unset_placeholder_values-ab5623) |
|
|
279
|
+
|
|
280
|
+
### Dialog mode (`editMode: 'dialog'`, AB#5623)
|
|
281
|
+
|
|
282
|
+
"New", row click and Edit / View open the form in a Kendo dialog over the list; the URL does not
|
|
283
|
+
change and no `navigate` events are emitted.
|
|
284
|
+
|
|
285
|
+
- Same form behaviour as the page: `beforeSave`, `initialValues`, `labelResolver`, secrets,
|
|
286
|
+
`patchFormValues`, the record row dialog. Host page actions (`mmEntityPageActions`) render in the
|
|
287
|
+
dialog with `ctx.view === 'form'`.
|
|
288
|
+
- Bottom bar (`kendo-dialog-actions`, right-aligned like the record row dialog): page actions,
|
|
289
|
+
Cancel (Close when read-only), Delete (edit, with permission), Save (primary, right-most).
|
|
290
|
+
- Cancel, the title-bar close button and Escape ask before discarding unsaved changes
|
|
291
|
+
(`unsavedChangesTitle` / `unsavedChangesMessage` / `discardChanges` / `keepEditing`). A route
|
|
292
|
+
change while the dialog has changes goes through `UnsavedChangesGuard` (save / discard / stay).
|
|
293
|
+
- After a successful save (also "no changes") or delete the dialog closes; after a write the list
|
|
294
|
+
reloads (`saved` / `deleted` are emitted as on the page). Focus returns to the element that opened
|
|
295
|
+
the dialog (the list when that element is gone).
|
|
296
|
+
- A load error or a missing entity shows an error notification; the list stays.
|
|
297
|
+
- The `new` / `:rtId` routes keep working as pages (deep links); singleton forms always use the page.
|
|
298
|
+
|
|
299
|
+
### Unset placeholders (`ENTITY_FORM_UNSET_PLACEHOLDER_VALUES`, AB#5623)
|
|
300
|
+
|
|
301
|
+
Seeded placeholder values of non-secret configuration attributes (e.g. `TODO_SET_AZURE_TENANT_ID`)
|
|
302
|
+
read as "not configured":
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
providers: [{
|
|
306
|
+
provide: ENTITY_FORM_UNSET_PLACEHOLDER_VALUES,
|
|
307
|
+
// a plain list applies to every attribute; or per attribute (any casing) and/or global:
|
|
308
|
+
useValue: { attributes: { azureTenantId: ['TODO_SET_AZURE_TENANT_ID'], clientId: ['TODO_SET_CLIENT_ID'] } },
|
|
309
|
+
}]
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
- Exact, case-sensitive match on string values.
|
|
313
|
+
- List: the cell shows `messages.notConfigured` ("Not configured"; text, mono and chip columns).
|
|
314
|
+
- Form: the field starts empty with `notConfigured` as placeholder hint; the value counts as unset
|
|
315
|
+
(a required field is invalid, `VisibleWhen` sees no value).
|
|
316
|
+
- Save: the form never writes a placeholder back. Untouched = not in the change set (the stored
|
|
317
|
+
placeholder stays and keeps reading "not configured"); a typed value replaces it; emptying the
|
|
318
|
+
field again is "no change". To remove the placeholder from the store, write `null` yourself
|
|
319
|
+
(e.g. in `beforeSave`). In create mode a placeholder default / prefill is left out of the create.
|
|
320
|
+
- Never applies to secrets (SECRET decision AB#5528: secret placeholders are dropped; secrets use
|
|
321
|
+
`secretIsSet`): fields with `secret` (value type `SECRET`, CK metadata, credential-name rule, form
|
|
322
|
+
decision) and attributes in `secretFields` are skipped even when listed. Legacy STRING secrets
|
|
323
|
+
keep using `ENTITY_FORM_SECRET_PLACEHOLDER_VALUES`.
|
|
324
|
+
|
|
325
|
+
## Secrets (write-only)
|
|
326
|
+
|
|
327
|
+
Secret values never reach the browser (AB#5522 D5, AB#5542, AB#5544 item 4, decisions 2026-10-06).
|
|
328
|
+
|
|
329
|
+
**Which fields are secret**
|
|
330
|
+
|
|
331
|
+
- The **SECRET value type** (AB#5528) is always secret and maps automatically to a write-only
|
|
332
|
+
field — no form definition needed; `Secret: false` is ignored with a warning. Compatible
|
|
333
|
+
editors: `password` (default) and `multiline` (PEM keys, `EntityFormField.Editor: multiline`);
|
|
334
|
+
any other editor falls back to `password` with a warning.
|
|
335
|
+
- Fallback for attributes that are not SECRET yet (older models): the form says `Secret: true`,
|
|
336
|
+
**or its editor is `password`** (always write-only), or the CK attribute carries the metadata
|
|
337
|
+
`secret=true`, or a textual attribute has a credential-like name (shared rule
|
|
338
|
+
`isSecretAttributeCandidate` of `@meshmakers/octo-services`).
|
|
339
|
+
|
|
340
|
+
**Reading**
|
|
341
|
+
|
|
342
|
+
- Value reads, the list and the reference picker use documents whose `$attributeNames` is
|
|
343
|
+
declared `[String]!` and always pass explicit names. Never add a document that selects
|
|
344
|
+
`attributes` without that argument — **omitting it makes the server return every attribute,
|
|
345
|
+
secrets included.**
|
|
346
|
+
- SECRET fields (`ResolvedEntityForm.secretStateFields`) are part of that list: the server
|
|
347
|
+
returns `value: null` plus `secretIsSet`, giving `EntityFormValueState.secretStates`
|
|
348
|
+
(`isSet`, `keyMissing`, `setAt`) from `secretIsSet` / `secretKeyMissing` / `secretSetAt`
|
|
349
|
+
(selected since the SECRET round-2 schema).
|
|
350
|
+
- Fallback secrets are never listed; whether they are set is read with an `IS_NOT_NULL` field
|
|
351
|
+
filter — plus `NOT_EQUALS ""` for STRING secrets — and `totalCount`. (Never use that probe on a
|
|
352
|
+
SECRET: the server refuses every filter but `IS_NULL` / `IS_NOT_NULL` there.)
|
|
353
|
+
|
|
354
|
+
**UI** (field shell badge + `mm-entity-form-secret-editor`)
|
|
355
|
+
|
|
356
|
+
- Badge next to the label, visible to read-only users too (Q15): **Set · set at …** (or **Set**
|
|
357
|
+
for legacy values without a timestamp), **Not set**, **Key missing — re-enter** (a value is
|
|
358
|
+
stored but its key is not in this environment's key ring; it reads as not set for consumers but
|
|
359
|
+
counts as present for "required"), **Will be cleared** (staged clear). Create mode shows
|
|
360
|
+
"Secret".
|
|
361
|
+
- The input is never prefilled; placeholder "Leave empty to keep" when a value is stored.
|
|
362
|
+
Read-only users get no input, no "Show" and no "Clear".
|
|
363
|
+
- **Show** reveals only the value typed in this session, never a stored one; it is disabled while
|
|
364
|
+
the input is empty (Q10).
|
|
365
|
+
- **Multiline** (PEM keys): the text area renders its text transparent until **Show** (caret,
|
|
366
|
+
selection and placeholder stay visible) and a status line reports only the number of lines
|
|
367
|
+
entered. This works in every browser — Firefox has no reliable `-webkit-text-security`, and a
|
|
368
|
+
password input would drop the PEM line breaks.
|
|
369
|
+
- **Clear** (optional SECRET fields with a stored value, edit mode, write access) is staged and
|
|
370
|
+
sent on Save as `clearSecretAttributes` (Q8). Clear and a new value are mutually exclusive:
|
|
371
|
+
Clear is disabled while a value is typed; a staged clear replaces the input by a note with
|
|
372
|
+
**Undo**. Required secrets cannot be cleared; fallback secrets cannot be cleared either (the
|
|
373
|
+
server accepts `clearSecretAttributes` only for SECRET attributes).
|
|
374
|
+
- **No key ring** (Q17): when the host provides `ENTITY_FORM_SECRET_KEY_RING_CONFIGURED`
|
|
375
|
+
(a `Signal<boolean | null | undefined>`) and it is `false`, secret inputs are disabled with a
|
|
376
|
+
hint. Only **SECRET-typed** fields (and SECRET record members) are gated — fallback secrets
|
|
377
|
+
(name rule / metadata) are plain values server-side and stay writable. Without a provider (or
|
|
378
|
+
while the signal is `null` / `undefined`) secrets are writable.
|
|
379
|
+
The signal may change after the form was built (status loaded asynchronously): the controls
|
|
380
|
+
follow it. In **create** mode a visible, required secret that cannot be entered blocks the form:
|
|
381
|
+
the field stays required (marker + hint), `isValid()` is `false` and the public signal
|
|
382
|
+
`saveBlockedReason()` names the fields — `mm-entity-page` disables Save with that text as
|
|
383
|
+
tooltip and the form shows it as a notice; other hosts should do the same. Edit mode is not
|
|
384
|
+
blocked (the server enforces required secrets only on create). The Studio provides the token
|
|
385
|
+
app-wide from the bot status endpoint `GET {tenantId}/v1/secrets/status`
|
|
386
|
+
(`SecretEnvironmentStatusService`, fail-open).
|
|
387
|
+
|
|
388
|
+
**Writing**
|
|
389
|
+
|
|
390
|
+
- An empty secret is left out of the change set (unchanged). A required secret is required
|
|
391
|
+
only on create or while it is not present (set or key missing).
|
|
392
|
+
- Secret list columns are dropped. The update mutation selects no attributes (no echo).
|
|
393
|
+
- Record members of value type SECRET: the generic read returns `value: null` + `secretIsSet` per
|
|
394
|
+
member; the form keeps that state (never a value). The records grid and the row editor show the
|
|
395
|
+
shared status badge (**Set** / **Not set** / **Key missing — re-enter**); the row editor offers
|
|
396
|
+
an empty password input (read-only users: badge only). A typed value replaces the member
|
|
397
|
+
(grid: "New value (unsaved)"); an empty input keeps it — on save the member is **omitted** and
|
|
398
|
+
the server carries the stored value over from the element with the same record key (handover
|
|
399
|
+
§2). Changing an element's record key therefore drops its stored secret. A state object is
|
|
400
|
+
never sent back. Without a key ring the member input is disabled with the hint (badge stays).
|
|
401
|
+
- The CK description of `EntityFormField.Secret` says "masked … revealed on demand"; the concept
|
|
402
|
+
(§5.8, write-only) wins.
|
|
403
|
+
|
|
404
|
+
## Editors (MVP)
|
|
405
|
+
|
|
406
|
+
| Editor | Implementation |
|
|
407
|
+
|--------|----------------|
|
|
408
|
+
| text, multiline, email, url, password, number, toggle, enum, datetime | Kendo inputs; email / url validators by editor; `Min`/`Max` on numbers, `Pattern` on text |
|
|
409
|
+
| chips | Array editor for `STRING_ARRAY` / `INT_ARRAY` |
|
|
410
|
+
| cron | shared-ui `mm-cron-builder` |
|
|
411
|
+
| **json / yaml** | Monospace `kendo-textarea` (no Monaco / YAML library in the workspace). `json` validates with `JSON.parse`, `yaml` is not validated. Both are stored as STRING. The Studio can swap in Monaco later via `CustomComponent`. |
|
|
412
|
+
| **reference** | shared-ui `mm-entity-select-input` (typeahead plus its **grid dialog**, multi-select for `N` roles) on a secret-safe data source that selects only the target's non-secret `name` attribute (literal `attributeNames: ["name"]`). Labels (picker and current value): a real `rtDisplayName` > `name` > `rtWellKnownName` > the synthetic `<type>@<rtId>` (so a pool shows "Default Cloud", not its well-known name `CommunicationPool`). Deviates from concept §5.4, which names `mm-entity-selector-dialog` — that one is a perspective tree picker without type filter or multi-select, and configuration types are not in a tree. A form field's `referenceDisplayAttributes` (CK `EntityFormField.ReferenceDisplayAttributes`, System.UI ≥ 2.9.0, read through the generic EntityForm query — no typed schema field, so older models simply lack it; host fallback forms may set it too; e.g. `['repositoryUrl', 'channel']`): the picker then reads exactly those non-secret target attributes with `entityFormGetReferenceOptionsWithAttributes` (explicit `[String]!` `attributeNames`) and shows `name · value · value` — in the picker rows and for the current value (looked up by rtId; the field value keeps its plain name). Values are formatted by the target's CK value type (`EntityFormService.resolve` attaches `reference.displayAttributeInfo`): ENUM key → enum name (e.g. channel `0` → "Release"), BOOLEAN → the `toggleOn`/`toggleOff` labels, DATE_TIME → localized date and time, arrays comma-separated, records skipped (AB#5547; never list a secret attribute — attributes marked secret on the target are dropped, and a SECRET value is never returned anyway). |
|
|
413
|
+
| records | Table with add / remove / move / edit; rows are edited in a dialog generated from the record's CK attributes. Nested records are read-only. |
|
|
414
|
+
| unsupported (BINARY, GEOSPATIAL_POINT, TIME_SPAN, …) | Read-only display |
|
|
415
|
+
|
|
416
|
+
**Runtime state is never generated as an editable field.** Generated fields (`form-default`,
|
|
417
|
+
"Further attributes") whose attribute is in `RUNTIME_STATE_ATTRIBUTES` (`core/attribute-path.ts`:
|
|
418
|
+
deployment / communication / configuration state, last errors and their timestamps, status
|
|
419
|
+
message, last synced sequence number, lifecycle state, last activity, on-demand flags) are
|
|
420
|
+
read-only, like the engine-stamped `rtBlueprint*` attributes. It is a documented list because the
|
|
421
|
+
CK `ownership: RuntimeState` marker is not in the schema the library is generated from; a field a
|
|
422
|
+
form defines explicitly keeps its own `ReadOnly`.
|
|
423
|
+
|
|
424
|
+
## Form resolution (summary)
|
|
425
|
+
|
|
426
|
+
1. Forms targeting the exact type win; otherwise the nearest ancestor with forms that set
|
|
427
|
+
`IncludeDerivedTypes` supplies the candidates (`form-default` targets `System/Entity`).
|
|
428
|
+
2. Tenant forms beat seeded forms (empty `RtBlueprintSource`), then higher `Priority`, then
|
|
429
|
+
`rtWellKnownName` / `rtId` ascending (deterministic tie-break).
|
|
430
|
+
3. No match → built-in default form with a warning. Forms are never merged.
|
|
431
|
+
4. Attribute paths match case-insensitively (forms write `Host`, CK names are `host`); unknown
|
|
432
|
+
paths are skipped, dotted paths are skipped with a warning; unmentioned attributes go to a
|
|
433
|
+
generated "Further attributes" section unless `GenerateRemainingFields: false`.
|
|
434
|
+
|
|
435
|
+
## Known backend limits
|
|
436
|
+
|
|
437
|
+
- **CK record attributes always report `isOptional: false`.** Record sub-fields are therefore
|
|
438
|
+
treated as optional in the UI; the server still enforces mandatory sub-attributes and answers
|
|
439
|
+
`ASSET1004`.
|
|
440
|
+
- **The `attributeNames` filter is applied inside records too.** Reading a record attribute
|
|
441
|
+
returns its rows with empty `attributes` unless the record's sub-attribute names are listed as
|
|
442
|
+
well, so `readAttributeNames` contains them. When a sub-attribute name equals a fallback
|
|
443
|
+
(non-SECRET) secret top-level attribute name it is dropped and the record field becomes
|
|
444
|
+
read-only (warning), so a save cannot erase that sub-value. A top-level SECRET name does not block
|
|
445
|
+
only when every record member of that name is a SECRET too (the server redacts both). If a
|
|
446
|
+
NON-SECRET member shares the name, listing it would return that member in clear text: the name is
|
|
447
|
+
not listed, the record field becomes read-only and the SECRET falls back to the presence probe.
|
|
448
|
+
- The edit flow relies on a partial `RtEntityUpdate` keeping the attributes that are not sent
|
|
449
|
+
(unchanged values, secrets). That is what makes the write-only secret handling safe.
|
|
450
|
+
|
|
451
|
+
## Tests
|
|
452
|
+
|
|
453
|
+
Specs live next to the sources and run with the octo-ui test target
|
|
454
|
+
(`../**/*.spec.ts`). `@meshmakers/octo-ui` resolves to `dist`, so build first:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
npm run build:octo-ui && npm run test:octo-ui
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Demo: `demo-app` → `demos/entity-forms` (SFTP configuration form).
|