ngx-dynamic-entity-builder 1.6.0 → 1.7.0
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.
|
@@ -264,21 +264,62 @@ class BuilderStore {
|
|
|
264
264
|
walkFields(tab.fields);
|
|
265
265
|
return ids;
|
|
266
266
|
}
|
|
267
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Resolve a field by key, where a key is either its `refererField` path or a bare id.
|
|
269
|
+
*
|
|
270
|
+
* Every resolution in this store funnels through here, which is why a bare id was enough
|
|
271
|
+
* to edit the wrong field: with `personal.address` and `work.address` both present, the
|
|
272
|
+
* first match won, so selecting or editing the second one silently addressed the first.
|
|
273
|
+
*
|
|
274
|
+
* The path is tried across the whole tree before any bare id is, so an exact key never
|
|
275
|
+
* loses to a nearer-but-wrong match. Bare ids still resolve — every existing caller and
|
|
276
|
+
* spec passes one, and for an unambiguous id it is the same field either way.
|
|
277
|
+
*/
|
|
278
|
+
findFieldInTabs(tabs = [], key) {
|
|
279
|
+
return this.findFieldBy(tabs, f => f.refererField === key) ?? this.findFieldBy(tabs, f => f.id === key);
|
|
280
|
+
}
|
|
281
|
+
findFieldBy(tabs = [], match) {
|
|
268
282
|
for (const t of tabs) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return found;
|
|
273
|
-
}
|
|
283
|
+
const found = t.fields?.find(match);
|
|
284
|
+
if (found)
|
|
285
|
+
return found;
|
|
274
286
|
if (t.children) {
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
return
|
|
287
|
+
const nested = this.findFieldBy(t.children, match);
|
|
288
|
+
if (nested)
|
|
289
|
+
return nested;
|
|
278
290
|
}
|
|
279
291
|
}
|
|
280
292
|
return null;
|
|
281
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* The key a caller should hand back to address this exact field. Templates use it so a
|
|
296
|
+
* click on the second of two same-named fields selects that one.
|
|
297
|
+
*/
|
|
298
|
+
keyOf(field) {
|
|
299
|
+
return field.refererField || field.id;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Whether this exact field is the selected one.
|
|
303
|
+
*
|
|
304
|
+
* Not `selectedFieldId() === field.id`: selection may hold a bare id or a path depending
|
|
305
|
+
* on who set it, and with two fields sharing an id a bare comparison lights up both rows
|
|
306
|
+
* — or neither, once the click starts storing a path. Resolving both sides answers for
|
|
307
|
+
* the field itself.
|
|
308
|
+
*/
|
|
309
|
+
isSelected(field) {
|
|
310
|
+
const key = this._selectedFieldId();
|
|
311
|
+
if (!key)
|
|
312
|
+
return false;
|
|
313
|
+
// Same key, same field — and this is the path a row click takes, so it is the common
|
|
314
|
+
// case. It also covers a field handed in as an @Input that the store's own config has
|
|
315
|
+
// no object for, which identity alone cannot.
|
|
316
|
+
if (key === this.keyOf(field))
|
|
317
|
+
return true;
|
|
318
|
+
// Otherwise the selection is in the other form (a bare id against a field that carries a
|
|
319
|
+
// path, or vice versa), so let it resolve and compare the field it names.
|
|
320
|
+
const resolved = this.findFieldInTabs(this._config().tabs, key);
|
|
321
|
+
return !!resolved && resolved === field;
|
|
322
|
+
}
|
|
282
323
|
// ─── Field CRUD ─────────────────────────────────────────────────────────────
|
|
283
324
|
addField(type, targetTabId) {
|
|
284
325
|
const meta = getFieldTypeMeta(type);
|
|
@@ -321,7 +362,7 @@ class BuilderStore {
|
|
|
321
362
|
return;
|
|
322
363
|
if (!this.findFieldInTabs(this._config().tabs, oldId))
|
|
323
364
|
return;
|
|
324
|
-
this.applyRename(oldId, trimmed);
|
|
365
|
+
this.applyRename(oldId, oldId, trimmed);
|
|
325
366
|
this.manualIds.delete(oldId);
|
|
326
367
|
this.manualIds.add(trimmed);
|
|
327
368
|
}
|
|
@@ -337,9 +378,23 @@ class BuilderStore {
|
|
|
337
378
|
* That was survivable while renaming was a rare manual act; label-derived ids make it
|
|
338
379
|
* routine, so the references move with it.
|
|
339
380
|
*/
|
|
340
|
-
|
|
381
|
+
/**
|
|
382
|
+
* `key` addresses the field to rename; `oldId` is the bare id every *reference* to it
|
|
383
|
+
* carries. They were one parameter, which worked only while an id was unique across the
|
|
384
|
+
* config: resolving needs the path, while `showWhen`, `parentField`, `autoPatch` targets
|
|
385
|
+
* and rules are all keyed by bare id and never match a path.
|
|
386
|
+
*/
|
|
387
|
+
applyRename(key, oldId, newId) {
|
|
388
|
+
// Selection is held as whatever the caller passed — `addField` stores a bare id while a
|
|
389
|
+
// canvas click stores a path — so comparing the two strings said "not selected" for a
|
|
390
|
+
// field that plainly was, and the inspector emptied itself mid-rename. Compare the
|
|
391
|
+
// fields the two keys resolve to instead.
|
|
392
|
+
const selectedKey = this._selectedFieldId();
|
|
393
|
+
const tabsNow = this._config().tabs;
|
|
394
|
+
const wasSelected = !!selectedKey &&
|
|
395
|
+
this.findFieldInTabs(tabsNow, selectedKey) === this.findFieldInTabs(tabsNow, key);
|
|
341
396
|
this.mutate(draft => {
|
|
342
|
-
const field = this.findFieldInTabs(draft.tabs,
|
|
397
|
+
const field = this.findFieldInTabs(draft.tabs, key);
|
|
343
398
|
if (!field)
|
|
344
399
|
return;
|
|
345
400
|
field.id = newId;
|
|
@@ -386,19 +441,40 @@ class BuilderStore {
|
|
|
386
441
|
fieldId: rule.fieldId === oldId ? newId : rule.fieldId,
|
|
387
442
|
targets: rule.targets.map(t => t.type === 'field' && t.id === oldId ? { ...t, id: newId } : t),
|
|
388
443
|
})));
|
|
389
|
-
|
|
390
|
-
|
|
444
|
+
// `mutate` restamps `refererField`, so the key's last segment moves with the id. Without
|
|
445
|
+
// this the selection points at a path nothing resolves to any more, and the inspector
|
|
446
|
+
// empties itself mid-keystroke as soon as a typed label derives a new id.
|
|
447
|
+
if (wasSelected) {
|
|
448
|
+
// Keep the shape the selection already had: a bare id stays bare, a path keeps its
|
|
449
|
+
// scope and moves only its last segment.
|
|
450
|
+
const cut = selectedKey.lastIndexOf('.');
|
|
451
|
+
this._selectedFieldId.set(cut === -1 ? newId : `${selectedKey.slice(0, cut + 1)}${newId}`);
|
|
452
|
+
}
|
|
391
453
|
}
|
|
392
|
-
removeField(
|
|
454
|
+
removeField(key) {
|
|
455
|
+
let removedId = null;
|
|
393
456
|
this.mutate(draft => {
|
|
457
|
+
// Resolve inside the draft, then remove that one by identity. Filtering on
|
|
458
|
+
// `f.id !== id` deleted every field sharing the id, so removing `work.address` took
|
|
459
|
+
// `personal.address` with it.
|
|
460
|
+
const target = this.findFieldInTabs(draft.tabs, key);
|
|
461
|
+
if (!target)
|
|
462
|
+
return;
|
|
463
|
+
removedId = target.id;
|
|
394
464
|
for (const tab of this.flattenTabs(draft.tabs)) {
|
|
395
465
|
if (tab.fields) {
|
|
396
|
-
tab.fields = tab.fields.filter(f => f
|
|
466
|
+
tab.fields = tab.fields.filter(f => f !== target);
|
|
397
467
|
}
|
|
398
468
|
}
|
|
399
469
|
});
|
|
400
|
-
|
|
401
|
-
|
|
470
|
+
if (removedId === null)
|
|
471
|
+
return;
|
|
472
|
+
// `manualIds` is keyed by bare id and shared by every field carrying it, so it is only
|
|
473
|
+
// safe to forget once no field uses that id any more.
|
|
474
|
+
if (!this.getAllFields(this._config().tabs ?? []).some(f => f.id === removedId)) {
|
|
475
|
+
this.manualIds.delete(removedId);
|
|
476
|
+
}
|
|
477
|
+
if (this._selectedFieldId() === key)
|
|
402
478
|
this._selectedFieldId.set(null);
|
|
403
479
|
}
|
|
404
480
|
duplicateField(id) {
|
|
@@ -425,11 +501,17 @@ class BuilderStore {
|
|
|
425
501
|
this._selectedFieldId.set(newId);
|
|
426
502
|
return newId;
|
|
427
503
|
}
|
|
428
|
-
moveField(
|
|
504
|
+
moveField(key, direction) {
|
|
429
505
|
this.mutate(draft => {
|
|
506
|
+
// Resolve by key, then locate that object — `f.id === id` moved whichever same-named
|
|
507
|
+
// field the walk reached first, which for two `address` fields was never the one the
|
|
508
|
+
// arrow button belonged to.
|
|
509
|
+
const wanted = this.findFieldInTabs(draft.tabs, key);
|
|
510
|
+
if (!wanted)
|
|
511
|
+
return;
|
|
430
512
|
for (const tab of this.flattenTabs(draft.tabs)) {
|
|
431
513
|
const fields = tab.fields ?? [];
|
|
432
|
-
const from = fields.findIndex(f => f
|
|
514
|
+
const from = fields.findIndex(f => f === wanted);
|
|
433
515
|
if (from !== -1) {
|
|
434
516
|
const to = from + direction;
|
|
435
517
|
if (to < 0 || to >= fields.length)
|
|
@@ -461,8 +543,13 @@ class BuilderStore {
|
|
|
461
543
|
const targetTab = this.flattenTabs(draft.tabs).find(t => t.id === targetTabId);
|
|
462
544
|
if (!targetTab)
|
|
463
545
|
return;
|
|
546
|
+
// Resolve by key first so a path names one field, then locate that object. Matching
|
|
547
|
+
// `f.id === id` moved whichever same-named field came first in the walk.
|
|
548
|
+
const wanted = this.findFieldInTabs(draft.tabs, id);
|
|
549
|
+
if (!wanted)
|
|
550
|
+
return;
|
|
464
551
|
for (const tab of this.flattenTabs(draft.tabs)) {
|
|
465
|
-
const index = (tab.fields ?? []).findIndex(f => f
|
|
552
|
+
const index = (tab.fields ?? []).findIndex(f => f === wanted);
|
|
466
553
|
if (index === -1)
|
|
467
554
|
continue;
|
|
468
555
|
if (tab.id === targetTabId)
|
|
@@ -509,17 +596,25 @@ class BuilderStore {
|
|
|
509
596
|
* Only the config's default language drives the id; translating a label must not rename
|
|
510
597
|
* anything.
|
|
511
598
|
*/
|
|
512
|
-
setFieldLabel(
|
|
599
|
+
setFieldLabel(key, language, value) {
|
|
513
600
|
// Bound to a keystroke event, so this uses the structural-sharing path.
|
|
514
|
-
this.mutateField(
|
|
601
|
+
this.mutateField(key, field => ({ ...field, label: { ...field.label, [language]: value } }));
|
|
515
602
|
if (language !== (this._config().defaultLanguage ?? 'en'))
|
|
516
603
|
return;
|
|
517
|
-
|
|
604
|
+
// The mutation is addressed by key, but everything below is about the *id*: `manualIds`
|
|
605
|
+
// records bare ids, `availableId` compares bare ids, and a rename replaces one. Reading
|
|
606
|
+
// the field's own id keeps the two straight — passing the path into `manualIds.has`
|
|
607
|
+
// would never match, so a pinned id would silently start following the label again.
|
|
608
|
+
const field = this.findFieldInTabs(this._config().tabs, key);
|
|
609
|
+
if (!field)
|
|
610
|
+
return;
|
|
611
|
+
const currentId = field.id;
|
|
612
|
+
if (this.manualIds.has(currentId))
|
|
518
613
|
return;
|
|
519
614
|
const derived = labelToId(value);
|
|
520
|
-
if (!derived || derived ===
|
|
615
|
+
if (!derived || derived === currentId)
|
|
521
616
|
return;
|
|
522
|
-
this.applyRename(
|
|
617
|
+
this.applyRename(this.keyOf(field), currentId, this.availableId(derived, currentId));
|
|
523
618
|
}
|
|
524
619
|
/** `derived`, or `derived_2`, `derived_3`… if another field already holds it. */
|
|
525
620
|
availableId(derived, currentId) {
|
|
@@ -1129,14 +1224,21 @@ class BuilderStore {
|
|
|
1129
1224
|
*
|
|
1130
1225
|
* Returns without touching anything when the field is not found.
|
|
1131
1226
|
*/
|
|
1132
|
-
mutateField(
|
|
1227
|
+
mutateField(key, update) {
|
|
1133
1228
|
let found = false;
|
|
1229
|
+
// Resolve once, then match on object identity. Matching on `field.id === id` inside the
|
|
1230
|
+
// walk updated *every* field carrying that id, so renaming `work.address` also rewrote
|
|
1231
|
+
// `personal.address` — both rows changed, and the second write was silent. Identity
|
|
1232
|
+
// cannot be ambiguous the way a bare id can.
|
|
1233
|
+
const target = this.findFieldInTabs(this._config().tabs, key);
|
|
1234
|
+
if (!target)
|
|
1235
|
+
return;
|
|
1134
1236
|
const visitFields = (fields) => {
|
|
1135
1237
|
if (!fields)
|
|
1136
1238
|
return fields;
|
|
1137
1239
|
let changed = false;
|
|
1138
1240
|
const next = fields.map(field => {
|
|
1139
|
-
if (field
|
|
1241
|
+
if (field === target) {
|
|
1140
1242
|
found = true;
|
|
1141
1243
|
changed = true;
|
|
1142
1244
|
return update(field);
|
|
@@ -1235,24 +1337,37 @@ class BuilderStore {
|
|
|
1235
1337
|
});
|
|
1236
1338
|
}
|
|
1237
1339
|
}
|
|
1238
|
-
// Id uniqueness is a
|
|
1239
|
-
//
|
|
1240
|
-
//
|
|
1241
|
-
//
|
|
1242
|
-
//
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1340
|
+
// Id uniqueness is a *per-scope* invariant, not a whole-config one — this counted into
|
|
1341
|
+
// one flat map keyed by bare id, so it rejected `personal.address` alongside
|
|
1342
|
+
// `work.address` and made every such config unopenable: Save is disabled while an error
|
|
1343
|
+
// stands, so unrelated edits could not be saved either. The runtime, the record shape
|
|
1344
|
+
// and `validateConfig` have all accepted per-scope ids since 1.4.0; this was the last
|
|
1345
|
+
// component still enforcing the old rule, and it enforced it against configs the rest
|
|
1346
|
+
// of the stack calls valid.
|
|
1347
|
+
//
|
|
1348
|
+
// The scope rule itself comes from core rather than being reimplemented here, so the
|
|
1349
|
+
// builder and the validator cannot drift apart on what "same scope" means.
|
|
1350
|
+
const scopeCounts = new Map();
|
|
1351
|
+
for (const entry of collectFieldScopes(config)) {
|
|
1352
|
+
if (!entry.field.id)
|
|
1353
|
+
continue;
|
|
1354
|
+
const key = `${entry.scope}::${entry.field.id}`;
|
|
1355
|
+
const seen = scopeCounts.get(key);
|
|
1356
|
+
if (seen)
|
|
1357
|
+
seen.count += 1;
|
|
1358
|
+
else
|
|
1359
|
+
scopeCounts.set(key, { id: entry.field.id, count: 1 });
|
|
1360
|
+
}
|
|
1361
|
+
for (const [key, { id, count }] of scopeCounts) {
|
|
1254
1362
|
if (count > 1) {
|
|
1255
|
-
|
|
1363
|
+
// Still an error, and it must stay one: two fields in the same scope share a single
|
|
1364
|
+
// control and a single record key, so the second silently overwrites the first.
|
|
1365
|
+
const scope = key.slice(0, key.lastIndexOf('::'));
|
|
1366
|
+
problems.push({
|
|
1367
|
+
level: 'error',
|
|
1368
|
+
message: `Duplicate field id "${id}" (${count}×) in ${scope}. Two fields in one scope share a control and a record key.`,
|
|
1369
|
+
fieldId: id,
|
|
1370
|
+
});
|
|
1256
1371
|
}
|
|
1257
1372
|
}
|
|
1258
1373
|
return problems;
|
|
@@ -1341,7 +1456,7 @@ class EntityReferenceConfigComponent {
|
|
|
1341
1456
|
return field.entityReference ?? { enabled: true };
|
|
1342
1457
|
}
|
|
1343
1458
|
patchRef(field, patch) {
|
|
1344
|
-
this.store.updateEntityReference(field
|
|
1459
|
+
this.store.updateEntityReference(this.store.keyOf(field), patch);
|
|
1345
1460
|
}
|
|
1346
1461
|
fieldLabel(field) {
|
|
1347
1462
|
return resolveLabel(field.label, this.store.activeLanguage());
|
|
@@ -1482,7 +1597,7 @@ class EntityReferenceConfigComponent {
|
|
|
1482
1597
|
mat-stroked-button
|
|
1483
1598
|
type="button"
|
|
1484
1599
|
data-testid="add-auto-patch"
|
|
1485
|
-
(click)="store.addAutoPatchMapping(f
|
|
1600
|
+
(click)="store.addAutoPatchMapping(store.keyOf(f))"
|
|
1486
1601
|
>
|
|
1487
1602
|
<mat-icon>add</mat-icon> Mapping
|
|
1488
1603
|
</button>
|
|
@@ -1494,7 +1609,7 @@ class EntityReferenceConfigComponent {
|
|
|
1494
1609
|
<mat-select
|
|
1495
1610
|
data-testid="auto-patch-tab"
|
|
1496
1611
|
[ngModel]="f.autoPatch.targetTab"
|
|
1497
|
-
(ngModelChange)="store.setAutoPatchTargetTab(f
|
|
1612
|
+
(ngModelChange)="store.setAutoPatchTargetTab(store.keyOf(f), $event)"
|
|
1498
1613
|
>
|
|
1499
1614
|
@for (tab of store.tabs(); track tab.id) {
|
|
1500
1615
|
<mat-option [value]="tab.id">{{ tabLabel(tab.id) }}</mat-option>
|
|
@@ -1509,7 +1624,7 @@ class EntityReferenceConfigComponent {
|
|
|
1509
1624
|
<input
|
|
1510
1625
|
matInput
|
|
1511
1626
|
[ngModel]="mapping.source"
|
|
1512
|
-
(ngModelChange)="store.updateAutoPatchMapping(f
|
|
1627
|
+
(ngModelChange)="store.updateAutoPatchMapping(store.keyOf(f), $index, { source: $event })"
|
|
1513
1628
|
/>
|
|
1514
1629
|
</mat-form-field>
|
|
1515
1630
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
|
@@ -1517,7 +1632,7 @@ class EntityReferenceConfigComponent {
|
|
|
1517
1632
|
<mat-select
|
|
1518
1633
|
data-testid="auto-patch-target"
|
|
1519
1634
|
[ngModel]="mapping.target"
|
|
1520
|
-
(ngModelChange)="store.updateAutoPatchMapping(f
|
|
1635
|
+
(ngModelChange)="store.updateAutoPatchMapping(store.keyOf(f), $index, { target: $event })"
|
|
1521
1636
|
>
|
|
1522
1637
|
@for (option of optionsWith(mapping.target); track option.value) {
|
|
1523
1638
|
<mat-option [value]="option.value">
|
|
@@ -1531,7 +1646,7 @@ class EntityReferenceConfigComponent {
|
|
|
1531
1646
|
type="button"
|
|
1532
1647
|
color="warn"
|
|
1533
1648
|
matTooltip="Remove mapping"
|
|
1534
|
-
(click)="store.removeAutoPatchMapping(f
|
|
1649
|
+
(click)="store.removeAutoPatchMapping(store.keyOf(f), $index)"
|
|
1535
1650
|
>
|
|
1536
1651
|
<mat-icon>delete</mat-icon>
|
|
1537
1652
|
</button>
|
|
@@ -1654,7 +1769,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1654
1769
|
mat-stroked-button
|
|
1655
1770
|
type="button"
|
|
1656
1771
|
data-testid="add-auto-patch"
|
|
1657
|
-
(click)="store.addAutoPatchMapping(f
|
|
1772
|
+
(click)="store.addAutoPatchMapping(store.keyOf(f))"
|
|
1658
1773
|
>
|
|
1659
1774
|
<mat-icon>add</mat-icon> Mapping
|
|
1660
1775
|
</button>
|
|
@@ -1666,7 +1781,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1666
1781
|
<mat-select
|
|
1667
1782
|
data-testid="auto-patch-tab"
|
|
1668
1783
|
[ngModel]="f.autoPatch.targetTab"
|
|
1669
|
-
(ngModelChange)="store.setAutoPatchTargetTab(f
|
|
1784
|
+
(ngModelChange)="store.setAutoPatchTargetTab(store.keyOf(f), $event)"
|
|
1670
1785
|
>
|
|
1671
1786
|
@for (tab of store.tabs(); track tab.id) {
|
|
1672
1787
|
<mat-option [value]="tab.id">{{ tabLabel(tab.id) }}</mat-option>
|
|
@@ -1681,7 +1796,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1681
1796
|
<input
|
|
1682
1797
|
matInput
|
|
1683
1798
|
[ngModel]="mapping.source"
|
|
1684
|
-
(ngModelChange)="store.updateAutoPatchMapping(f
|
|
1799
|
+
(ngModelChange)="store.updateAutoPatchMapping(store.keyOf(f), $index, { source: $event })"
|
|
1685
1800
|
/>
|
|
1686
1801
|
</mat-form-field>
|
|
1687
1802
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
|
@@ -1689,7 +1804,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1689
1804
|
<mat-select
|
|
1690
1805
|
data-testid="auto-patch-target"
|
|
1691
1806
|
[ngModel]="mapping.target"
|
|
1692
|
-
(ngModelChange)="store.updateAutoPatchMapping(f
|
|
1807
|
+
(ngModelChange)="store.updateAutoPatchMapping(store.keyOf(f), $index, { target: $event })"
|
|
1693
1808
|
>
|
|
1694
1809
|
@for (option of optionsWith(mapping.target); track option.value) {
|
|
1695
1810
|
<mat-option [value]="option.value">
|
|
@@ -1703,7 +1818,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
1703
1818
|
type="button"
|
|
1704
1819
|
color="warn"
|
|
1705
1820
|
matTooltip="Remove mapping"
|
|
1706
|
-
(click)="store.removeAutoPatchMapping(f
|
|
1821
|
+
(click)="store.removeAutoPatchMapping(store.keyOf(f), $index)"
|
|
1707
1822
|
>
|
|
1708
1823
|
<mat-icon>delete</mat-icon>
|
|
1709
1824
|
</button>
|
|
@@ -2212,20 +2327,20 @@ class ReferencedFieldConfigComponent {
|
|
|
2212
2327
|
field = computed(() => this.store.selectedField(), ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
2213
2328
|
toggleReferenced(field, enabled) {
|
|
2214
2329
|
if (enabled) {
|
|
2215
|
-
this.store.updateField(field
|
|
2330
|
+
this.store.updateField(this.store.keyOf(field), { isReferenced: true });
|
|
2216
2331
|
}
|
|
2217
2332
|
else {
|
|
2218
|
-
this.store.unlinkReferencedField(field
|
|
2333
|
+
this.store.unlinkReferencedField(this.store.keyOf(field));
|
|
2219
2334
|
}
|
|
2220
2335
|
}
|
|
2221
2336
|
updateEntityKey(field, key) {
|
|
2222
2337
|
const trimmed = key.trim();
|
|
2223
|
-
this.store.updateField(field
|
|
2338
|
+
this.store.updateField(this.store.keyOf(field), { referencedEntityKey: trimmed || undefined });
|
|
2224
2339
|
this.checkDriftForField(field.id);
|
|
2225
2340
|
}
|
|
2226
2341
|
updateFieldId(field, fieldId) {
|
|
2227
2342
|
const trimmed = fieldId.trim();
|
|
2228
|
-
this.store.updateField(field
|
|
2343
|
+
this.store.updateField(this.store.keyOf(field), { referencedFieldId: trimmed || undefined });
|
|
2229
2344
|
this.checkDriftForField(field.id);
|
|
2230
2345
|
}
|
|
2231
2346
|
async syncWithSource(field) {
|
|
@@ -2236,7 +2351,7 @@ class ReferencedFieldConfigComponent {
|
|
|
2236
2351
|
return;
|
|
2237
2352
|
const sourceField = this.findField(sourceConfig.tabs ?? [], field.referencedFieldId);
|
|
2238
2353
|
if (sourceField) {
|
|
2239
|
-
this.store.syncReferencedField(field
|
|
2354
|
+
this.store.syncReferencedField(this.store.keyOf(field), sourceField);
|
|
2240
2355
|
}
|
|
2241
2356
|
}
|
|
2242
2357
|
/**
|
|
@@ -2479,7 +2594,7 @@ class FieldInspectorComponent {
|
|
|
2479
2594
|
}
|
|
2480
2595
|
moveToTab(field, tabId) {
|
|
2481
2596
|
if (tabId && tabId !== this.currentTabId(field))
|
|
2482
|
-
this.store.moveFieldToTab(field
|
|
2597
|
+
this.store.moveFieldToTab(this.store.keyOf(field), tabId);
|
|
2483
2598
|
}
|
|
2484
2599
|
/**
|
|
2485
2600
|
* Fields offered wherever this inspector names one — the `showWhen` key and both ends of a
|
|
@@ -2516,7 +2631,7 @@ class FieldInspectorComponent {
|
|
|
2516
2631
|
while (key in next)
|
|
2517
2632
|
key = `field_${++n}`;
|
|
2518
2633
|
next[key] = true;
|
|
2519
|
-
this.store.setShowWhen(field
|
|
2634
|
+
this.store.setShowWhen(this.store.keyOf(field), next);
|
|
2520
2635
|
}
|
|
2521
2636
|
renameShowWhen(field, oldKey, newKey) {
|
|
2522
2637
|
const trimmed = newKey.trim();
|
|
@@ -2529,15 +2644,15 @@ class FieldInspectorComponent {
|
|
|
2529
2644
|
// Rebuild in order so the row does not jump while the user is typing.
|
|
2530
2645
|
for (const [k, v] of Object.entries(current))
|
|
2531
2646
|
next[k === oldKey ? trimmed : k] = v;
|
|
2532
|
-
this.store.setShowWhen(field
|
|
2647
|
+
this.store.setShowWhen(this.store.keyOf(field), next);
|
|
2533
2648
|
}
|
|
2534
2649
|
setShowWhenValue(field, key, raw) {
|
|
2535
|
-
this.store.setShowWhen(field
|
|
2650
|
+
this.store.setShowWhen(this.store.keyOf(field), { ...(field.showWhen ?? {}), [key]: this.parseShowWhen(raw) });
|
|
2536
2651
|
}
|
|
2537
2652
|
removeShowWhen(field, key) {
|
|
2538
2653
|
const next = { ...(field.showWhen ?? {}) };
|
|
2539
2654
|
delete next[key];
|
|
2540
|
-
this.store.setShowWhen(field
|
|
2655
|
+
this.store.setShowWhen(this.store.keyOf(field), next);
|
|
2541
2656
|
}
|
|
2542
2657
|
/**
|
|
2543
2658
|
* `showWhen` compares with `!==`, so the authored type has to survive the round trip:
|
|
@@ -2563,7 +2678,7 @@ class FieldInspectorComponent {
|
|
|
2563
2678
|
return String(value);
|
|
2564
2679
|
}
|
|
2565
2680
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2566
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FieldInspectorComponent, isStandalone: true, selector: "ngx-field-inspector", ngImport: i0, template: "@if (field(); as f) {\n <div class=\"deb-inspector\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Field id</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>The stored data key for this saved field \u2014 fixed.</mat-hint>\n } @else {\n <mat-hint>Derived from the label. Used as the data key.</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Tab</mat-label>\n <mat-select data-testid=\"field-tab\"\n [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>Moving the field rewrites its path and repoints the rules that named it.</mat-hint>\n </mat-form-field>\n }\n\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Label ({{ lang() }})</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(f.id, lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Placeholder ({{ lang() }})</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(f.id, lang(), $event)\"\n />\n </mat-form-field>\n }\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Default value</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(f.id, { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n\n <!-- Entity reference, cascade, and autoPatch -->\n @if (meta()?.isEntityRef) {\n <mat-divider></mat-divider>\n <ngx-entity-reference-config />\n }\n\n <mat-divider></mat-divider>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <span class=\"deb-section-title\">Validation</span>\n <div class=\"deb-toggles\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(f.id, v, $event)\"\n >{{ v }}</mat-checkbox>\n }\n </div>\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(f.id, v, toNum($event))\"\n />\n </mat-form-field>\n }\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <mat-divider></mat-divider>\n <span class=\"deb-section-title\">Data source</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Options come from</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(f.id, $event)\"\n >\n <mat-option value=\"none\">Nothing yet</mat-option>\n <mat-option value=\"manual\">Options authored here</mat-option>\n <mat-option value=\"lookup\">A named list</mat-option>\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Named lookup list -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>List name</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(f.id, $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n The consuming app resolves this name through its lookup registry. Options are not\n authored here.\n </p>\n }\n\n <!-- Inline options -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row\" style=\"justify-content: space-between;\">\n <span class=\"deb-section-title\">Options</span>\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(f.id)\">\n <mat-icon>add</mat-icon> Option\n </button>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Option ({{ lang() }})</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(f.id, $index, lang(), $event)\"\n />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\"\n (click)=\"store.removeOption(f.id, $index)\" matTooltip=\"Remove option\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">No options yet \u2014 add at least one.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Flags -->\n <span class=\"deb-section-title\">Display</span>\n <div class=\"deb-toggles\">\n <mat-slide-toggle\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(f.id, { visibility: $event })\"\n >Visible</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { table: { visible: $event } })\"\n >Table column</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { readonly: $event })\"\n >Read-only</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { disabled: $event })\"\n >Disabled</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { maskData: $event })\"\n >Mask data (RBAC)</mat-slide-toggle>\n <mat-slide-toggle\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { criticalField: $event })\"\n matTooltip=\"Renders locked; editing it announces a change against the session baseline.\"\n >Critical field</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { showOnMinimize: $event })\"\n matTooltip=\"Shown in the record editor's summary panel.\"\n >Show in summary</mat-slide-toggle>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Conditional visibility (static showWhen) -->\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Show when</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> Condition\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Watched field</mat-label>\n <mat-select data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\">\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Equals</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove condition\"\n (click)=\"removeShowWhen(f, entry.key)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">Always visible. Add a condition to show this field only for certain values.</p>\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <mat-divider></mat-divider>\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Copy on true</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(f.id)\">\n <mat-icon>add</mat-icon> Mapping\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>From field</mat-label>\n <mat-select data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { from: $event })\">\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>To field</mat-label>\n <mat-select data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { to: $event })\">\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove mapping\"\n (click)=\"store.removePatchOnTrueMapping(f.id, $index)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">When this flips to true, copy one field's value into another.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Rules attached to this field -->\n <ngx-field-rules-list />\n\n <mat-divider></mat-divider>\n\n <!-- Cross-entity field reference & drift management (Phase 8) -->\n <ngx-referenced-field-config />\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>Select a field to edit its properties.</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row{display:flex;gap:8px;align-items:center}.deb-row>*{flex:1}.deb-row--split{justify-content:space-between}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-chip{display:inline-block;padding:2px 8px;border-radius:12px;background:var(--deb-accent-soft, #eef2ff);color:var(--deb-accent, #6366f1);font-size:12px;font-weight:600}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i9.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: EntityReferenceConfigComponent, selector: "ngx-entity-reference-config" }, { kind: "component", type: FieldRulesListComponent, selector: "ngx-field-rules-list" }, { kind: "component", type: ReferencedFieldConfigComponent, selector: "ngx-referenced-field-config" }] });
|
|
2681
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FieldInspectorComponent, isStandalone: true, selector: "ngx-field-inspector", ngImport: i0, template: "@if (field(); as f) {\n <div class=\"deb-inspector\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Field id</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>The stored data key for this saved field \u2014 fixed.</mat-hint>\n } @else {\n <mat-hint>Derived from the label. Used as the data key.</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Tab</mat-label>\n <mat-select data-testid=\"field-tab\"\n [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>Moving the field rewrites its path and repoints the rules that named it.</mat-hint>\n </mat-form-field>\n }\n\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Label ({{ lang() }})</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Placeholder ({{ lang() }})</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Default value</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n\n <!-- Entity reference, cascade, and autoPatch -->\n @if (meta()?.isEntityRef) {\n <mat-divider></mat-divider>\n <ngx-entity-reference-config />\n }\n\n <mat-divider></mat-divider>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <span class=\"deb-section-title\">Validation</span>\n <div class=\"deb-toggles\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox>\n }\n </div>\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <mat-divider></mat-divider>\n <span class=\"deb-section-title\">Data source</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Options come from</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">Nothing yet</mat-option>\n <mat-option value=\"manual\">Options authored here</mat-option>\n <mat-option value=\"lookup\">A named list</mat-option>\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Named lookup list -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>List name</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n The consuming app resolves this name through its lookup registry. Options are not\n authored here.\n </p>\n }\n\n <!-- Inline options -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row\" style=\"justify-content: space-between;\">\n <span class=\"deb-section-title\">Options</span>\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(store.keyOf(f))\">\n <mat-icon>add</mat-icon> Option\n </button>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Option ({{ lang() }})</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\" matTooltip=\"Remove option\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">No options yet \u2014 add at least one.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Flags -->\n <span class=\"deb-section-title\">Display</span>\n <div class=\"deb-toggles\">\n <mat-slide-toggle\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >Visible</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >Table column</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >Read-only</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >Disabled</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >Mask data (RBAC)</mat-slide-toggle>\n <mat-slide-toggle\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n matTooltip=\"Renders locked; editing it announces a change against the session baseline.\"\n >Critical field</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n matTooltip=\"Shown in the record editor's summary panel.\"\n >Show in summary</mat-slide-toggle>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Conditional visibility (static showWhen) -->\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Show when</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> Condition\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Watched field</mat-label>\n <mat-select data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\">\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Equals</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove condition\"\n (click)=\"removeShowWhen(f, entry.key)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">Always visible. Add a condition to show this field only for certain values.</p>\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <mat-divider></mat-divider>\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Copy on true</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\">\n <mat-icon>add</mat-icon> Mapping\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>From field</mat-label>\n <mat-select data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\">\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>To field</mat-label>\n <mat-select data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\">\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove mapping\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">When this flips to true, copy one field's value into another.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Rules attached to this field -->\n <ngx-field-rules-list />\n\n <mat-divider></mat-divider>\n\n <!-- Cross-entity field reference & drift management (Phase 8) -->\n <ngx-referenced-field-config />\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>Select a field to edit its properties.</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row{display:flex;gap:8px;align-items:center}.deb-row>*{flex:1}.deb-row--split{justify-content:space-between}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-chip{display:inline-block;padding:2px 8px;border-radius:12px;background:var(--deb-accent-soft, #eef2ff);color:var(--deb-accent, #6366f1);font-size:12px;font-weight:600}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i9.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: EntityReferenceConfigComponent, selector: "ngx-entity-reference-config" }, { kind: "component", type: FieldRulesListComponent, selector: "ngx-field-rules-list" }, { kind: "component", type: ReferencedFieldConfigComponent, selector: "ngx-referenced-field-config" }] });
|
|
2567
2682
|
}
|
|
2568
2683
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FieldInspectorComponent, decorators: [{
|
|
2569
2684
|
type: Component,
|
|
@@ -2581,7 +2696,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
2581
2696
|
EntityReferenceConfigComponent,
|
|
2582
2697
|
FieldRulesListComponent,
|
|
2583
2698
|
ReferencedFieldConfigComponent,
|
|
2584
|
-
], template: "@if (field(); as f) {\n <div class=\"deb-inspector\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Field id</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>The stored data key for this saved field \u2014 fixed.</mat-hint>\n } @else {\n <mat-hint>Derived from the label. Used as the data key.</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Tab</mat-label>\n <mat-select data-testid=\"field-tab\"\n [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>Moving the field rewrites its path and repoints the rules that named it.</mat-hint>\n </mat-form-field>\n }\n\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Label ({{ lang() }})</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(f.id, lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Placeholder ({{ lang() }})</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(f.id, lang(), $event)\"\n />\n </mat-form-field>\n }\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Default value</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(f.id, { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n\n <!-- Entity reference, cascade, and autoPatch -->\n @if (meta()?.isEntityRef) {\n <mat-divider></mat-divider>\n <ngx-entity-reference-config />\n }\n\n <mat-divider></mat-divider>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <span class=\"deb-section-title\">Validation</span>\n <div class=\"deb-toggles\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(f.id, v, $event)\"\n >{{ v }}</mat-checkbox>\n }\n </div>\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(f.id, v, toNum($event))\"\n />\n </mat-form-field>\n }\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <mat-divider></mat-divider>\n <span class=\"deb-section-title\">Data source</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Options come from</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(f.id, $event)\"\n >\n <mat-option value=\"none\">Nothing yet</mat-option>\n <mat-option value=\"manual\">Options authored here</mat-option>\n <mat-option value=\"lookup\">A named list</mat-option>\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Named lookup list -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>List name</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(f.id, $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n The consuming app resolves this name through its lookup registry. Options are not\n authored here.\n </p>\n }\n\n <!-- Inline options -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row\" style=\"justify-content: space-between;\">\n <span class=\"deb-section-title\">Options</span>\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(f.id)\">\n <mat-icon>add</mat-icon> Option\n </button>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Option ({{ lang() }})</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(f.id, $index, lang(), $event)\"\n />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\"\n (click)=\"store.removeOption(f.id, $index)\" matTooltip=\"Remove option\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">No options yet \u2014 add at least one.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Flags -->\n <span class=\"deb-section-title\">Display</span>\n <div class=\"deb-toggles\">\n <mat-slide-toggle\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(f.id, { visibility: $event })\"\n >Visible</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { table: { visible: $event } })\"\n >Table column</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { readonly: $event })\"\n >Read-only</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { disabled: $event })\"\n >Disabled</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { maskData: $event })\"\n >Mask data (RBAC)</mat-slide-toggle>\n <mat-slide-toggle\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { criticalField: $event })\"\n matTooltip=\"Renders locked; editing it announces a change against the session baseline.\"\n >Critical field</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(f.id, { showOnMinimize: $event })\"\n matTooltip=\"Shown in the record editor's summary panel.\"\n >Show in summary</mat-slide-toggle>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Conditional visibility (static showWhen) -->\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Show when</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> Condition\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Watched field</mat-label>\n <mat-select data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\">\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Equals</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove condition\"\n (click)=\"removeShowWhen(f, entry.key)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">Always visible. Add a condition to show this field only for certain values.</p>\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <mat-divider></mat-divider>\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Copy on true</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(f.id)\">\n <mat-icon>add</mat-icon> Mapping\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>From field</mat-label>\n <mat-select data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { from: $event })\">\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>To field</mat-label>\n <mat-select data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { to: $event })\">\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove mapping\"\n (click)=\"store.removePatchOnTrueMapping(f.id, $index)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">When this flips to true, copy one field's value into another.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Rules attached to this field -->\n <ngx-field-rules-list />\n\n <mat-divider></mat-divider>\n\n <!-- Cross-entity field reference & drift management (Phase 8) -->\n <ngx-referenced-field-config />\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>Select a field to edit its properties.</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row{display:flex;gap:8px;align-items:center}.deb-row>*{flex:1}.deb-row--split{justify-content:space-between}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-chip{display:inline-block;padding:2px 8px;border-radius:12px;background:var(--deb-accent-soft, #eef2ff);color:var(--deb-accent, #6366f1);font-size:12px;font-weight:600}\n"] }]
|
|
2699
|
+
], template: "@if (field(); as f) {\n <div class=\"deb-inspector\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Field id</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>The stored data key for this saved field \u2014 fixed.</mat-hint>\n } @else {\n <mat-hint>Derived from the label. Used as the data key.</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Tab</mat-label>\n <mat-select data-testid=\"field-tab\"\n [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>Moving the field rewrites its path and repoints the rules that named it.</mat-hint>\n </mat-form-field>\n }\n\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Label ({{ lang() }})</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Placeholder ({{ lang() }})</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Default value</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n\n <!-- Entity reference, cascade, and autoPatch -->\n @if (meta()?.isEntityRef) {\n <mat-divider></mat-divider>\n <ngx-entity-reference-config />\n }\n\n <mat-divider></mat-divider>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <span class=\"deb-section-title\">Validation</span>\n <div class=\"deb-toggles\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox>\n }\n </div>\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <mat-divider></mat-divider>\n <span class=\"deb-section-title\">Data source</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Options come from</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">Nothing yet</mat-option>\n <mat-option value=\"manual\">Options authored here</mat-option>\n <mat-option value=\"lookup\">A named list</mat-option>\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Named lookup list -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>List name</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n The consuming app resolves this name through its lookup registry. Options are not\n authored here.\n </p>\n }\n\n <!-- Inline options -->\n @if (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row\" style=\"justify-content: space-between;\">\n <span class=\"deb-section-title\">Options</span>\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(store.keyOf(f))\">\n <mat-icon>add</mat-icon> Option\n </button>\n </div>\n @for (opt of f.options ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <!-- One input: the displayed text is the stored value. -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Option ({{ lang() }})</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\" matTooltip=\"Remove option\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">No options yet \u2014 add at least one.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Flags -->\n <span class=\"deb-section-title\">Display</span>\n <div class=\"deb-toggles\">\n <mat-slide-toggle\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >Visible</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >Table column</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >Read-only</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >Disabled</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >Mask data (RBAC)</mat-slide-toggle>\n <mat-slide-toggle\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n matTooltip=\"Renders locked; editing it announces a change against the session baseline.\"\n >Critical field</mat-slide-toggle>\n <mat-slide-toggle\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n matTooltip=\"Shown in the record editor's summary panel.\"\n >Show in summary</mat-slide-toggle>\n </div>\n\n <mat-divider></mat-divider>\n\n <!-- Conditional visibility (static showWhen) -->\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Show when</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> Condition\n </button>\n </div>\n @for (entry of showWhenEntries(f); track entry.key) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Watched field</mat-label>\n <mat-select data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\">\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>Equals</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove condition\"\n (click)=\"removeShowWhen(f, entry.key)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">Always visible. Add a condition to show this field only for certain values.</p>\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <mat-divider></mat-divider>\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">Copy on true</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\">\n <mat-icon>add</mat-icon> Mapping\n </button>\n </div>\n @for (mapping of f.patchOnTrue ?? []; track $index) {\n <div class=\"deb-option-row\" data-testid=\"option-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>From field</mat-label>\n <mat-select data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\">\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>To field</mat-label>\n <mat-select data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\">\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove mapping\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">When this flips to true, copy one field's value into another.</p>\n }\n }\n\n <mat-divider></mat-divider>\n\n <!-- Rules attached to this field -->\n <ngx-field-rules-list />\n\n <mat-divider></mat-divider>\n\n <!-- Cross-entity field reference & drift management (Phase 8) -->\n <ngx-referenced-field-config />\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>Select a field to edit its properties.</p>\n </div>\n}\n", styles: [".deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__empty{color:var(--deb-muted, #6b7280);text-align:center;padding:24px 8px}.deb-full{width:100%}.deb-row{display:flex;gap:8px;align-items:center}.deb-row>*{flex:1}.deb-row--split{justify-content:space-between}.deb-row--split>*{flex:0 0 auto}.deb-toggles{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-chip{display:inline-block;padding:2px 8px;border-radius:12px;background:var(--deb-accent-soft, #eef2ff);color:var(--deb-accent, #6366f1);font-size:12px;font-weight:600}\n"] }]
|
|
2585
2700
|
}] });
|
|
2586
2701
|
|
|
2587
2702
|
/**
|
|
@@ -2977,13 +3092,13 @@ class EntityBuilderTreeNodeComponent {
|
|
|
2977
3092
|
cdkDrag
|
|
2978
3093
|
class="deb-field-row"
|
|
2979
3094
|
data-testid="builder-field-row"
|
|
2980
|
-
[class.deb-field-row--active]="store.
|
|
2981
|
-
(click)="store.selectField(field
|
|
2982
|
-
(keydown.enter)="store.selectField(field
|
|
2983
|
-
(keydown.space)="store.selectField(field
|
|
3095
|
+
[class.deb-field-row--active]="store.isSelected(field)"
|
|
3096
|
+
(click)="store.selectField(store.keyOf(field))"
|
|
3097
|
+
(keydown.enter)="store.selectField(store.keyOf(field))"
|
|
3098
|
+
(keydown.space)="store.selectField(store.keyOf(field)); $event.preventDefault()"
|
|
2984
3099
|
role="button"
|
|
2985
3100
|
tabindex="0"
|
|
2986
|
-
[attr.aria-pressed]="store.
|
|
3101
|
+
[attr.aria-pressed]="store.isSelected(field)"
|
|
2987
3102
|
[attr.aria-label]="'Select field ' + fieldLabel(field)"
|
|
2988
3103
|
>
|
|
2989
3104
|
<mat-icon class="deb-drag-handle" cdkDragHandle aria-hidden="true" title="Drag to reorder (or use the move buttons)">drag_indicator</mat-icon>
|
|
@@ -3006,24 +3121,24 @@ class EntityBuilderTreeNodeComponent {
|
|
|
3006
3121
|
<button mat-icon-button type="button" [disabled]="index === 0"
|
|
3007
3122
|
[attr.data-testid]="'row-up-' + field.id"
|
|
3008
3123
|
[attr.aria-label]="'Move ' + fieldLabel(field) + ' up'"
|
|
3009
|
-
(click)="store.moveField(field
|
|
3124
|
+
(click)="store.moveField(store.keyOf(field), -1); $event.stopPropagation()" matTooltip="Move up">
|
|
3010
3125
|
<mat-icon>arrow_upward</mat-icon>
|
|
3011
3126
|
</button>
|
|
3012
3127
|
<button mat-icon-button type="button" [disabled]="index === totalCount - 1"
|
|
3013
3128
|
[attr.data-testid]="'row-down-' + field.id"
|
|
3014
3129
|
[attr.aria-label]="'Move ' + fieldLabel(field) + ' down'"
|
|
3015
|
-
(click)="store.moveField(field
|
|
3130
|
+
(click)="store.moveField(store.keyOf(field), 1); $event.stopPropagation()" matTooltip="Move down">
|
|
3016
3131
|
<mat-icon>arrow_downward</mat-icon>
|
|
3017
3132
|
</button>
|
|
3018
3133
|
<button mat-icon-button type="button"
|
|
3019
3134
|
[attr.data-testid]="'row-duplicate-' + field.id"
|
|
3020
3135
|
[attr.aria-label]="'Duplicate ' + fieldLabel(field)"
|
|
3021
|
-
(click)="store.duplicateField(field
|
|
3136
|
+
(click)="store.duplicateField(store.keyOf(field)); $event.stopPropagation()" matTooltip="Duplicate">
|
|
3022
3137
|
<mat-icon>content_copy</mat-icon>
|
|
3023
3138
|
</button>
|
|
3024
3139
|
<button mat-icon-button type="button" color="warn"
|
|
3025
3140
|
[attr.data-testid]="'row-delete-' + field.id"
|
|
3026
|
-
(click)="store.removeField(field
|
|
3141
|
+
(click)="store.removeField(store.keyOf(field)); $event.stopPropagation()" matTooltip="Delete">
|
|
3027
3142
|
<mat-icon>delete</mat-icon>
|
|
3028
3143
|
</button>
|
|
3029
3144
|
</div>
|
|
@@ -3059,13 +3174,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3059
3174
|
cdkDrag
|
|
3060
3175
|
class="deb-field-row"
|
|
3061
3176
|
data-testid="builder-field-row"
|
|
3062
|
-
[class.deb-field-row--active]="store.
|
|
3063
|
-
(click)="store.selectField(field
|
|
3064
|
-
(keydown.enter)="store.selectField(field
|
|
3065
|
-
(keydown.space)="store.selectField(field
|
|
3177
|
+
[class.deb-field-row--active]="store.isSelected(field)"
|
|
3178
|
+
(click)="store.selectField(store.keyOf(field))"
|
|
3179
|
+
(keydown.enter)="store.selectField(store.keyOf(field))"
|
|
3180
|
+
(keydown.space)="store.selectField(store.keyOf(field)); $event.preventDefault()"
|
|
3066
3181
|
role="button"
|
|
3067
3182
|
tabindex="0"
|
|
3068
|
-
[attr.aria-pressed]="store.
|
|
3183
|
+
[attr.aria-pressed]="store.isSelected(field)"
|
|
3069
3184
|
[attr.aria-label]="'Select field ' + fieldLabel(field)"
|
|
3070
3185
|
>
|
|
3071
3186
|
<mat-icon class="deb-drag-handle" cdkDragHandle aria-hidden="true" title="Drag to reorder (or use the move buttons)">drag_indicator</mat-icon>
|
|
@@ -3088,24 +3203,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3088
3203
|
<button mat-icon-button type="button" [disabled]="index === 0"
|
|
3089
3204
|
[attr.data-testid]="'row-up-' + field.id"
|
|
3090
3205
|
[attr.aria-label]="'Move ' + fieldLabel(field) + ' up'"
|
|
3091
|
-
(click)="store.moveField(field
|
|
3206
|
+
(click)="store.moveField(store.keyOf(field), -1); $event.stopPropagation()" matTooltip="Move up">
|
|
3092
3207
|
<mat-icon>arrow_upward</mat-icon>
|
|
3093
3208
|
</button>
|
|
3094
3209
|
<button mat-icon-button type="button" [disabled]="index === totalCount - 1"
|
|
3095
3210
|
[attr.data-testid]="'row-down-' + field.id"
|
|
3096
3211
|
[attr.aria-label]="'Move ' + fieldLabel(field) + ' down'"
|
|
3097
|
-
(click)="store.moveField(field
|
|
3212
|
+
(click)="store.moveField(store.keyOf(field), 1); $event.stopPropagation()" matTooltip="Move down">
|
|
3098
3213
|
<mat-icon>arrow_downward</mat-icon>
|
|
3099
3214
|
</button>
|
|
3100
3215
|
<button mat-icon-button type="button"
|
|
3101
3216
|
[attr.data-testid]="'row-duplicate-' + field.id"
|
|
3102
3217
|
[attr.aria-label]="'Duplicate ' + fieldLabel(field)"
|
|
3103
|
-
(click)="store.duplicateField(field
|
|
3218
|
+
(click)="store.duplicateField(store.keyOf(field)); $event.stopPropagation()" matTooltip="Duplicate">
|
|
3104
3219
|
<mat-icon>content_copy</mat-icon>
|
|
3105
3220
|
</button>
|
|
3106
3221
|
<button mat-icon-button type="button" color="warn"
|
|
3107
3222
|
[attr.data-testid]="'row-delete-' + field.id"
|
|
3108
|
-
(click)="store.removeField(field
|
|
3223
|
+
(click)="store.removeField(store.keyOf(field)); $event.stopPropagation()" matTooltip="Delete">
|
|
3109
3224
|
<mat-icon>delete</mat-icon>
|
|
3110
3225
|
</button>
|
|
3111
3226
|
</div>
|