ngx-dynamic-entity-builder 1.0.0 → 1.2.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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, inject, Component, EventEmitter, Output, Input, InjectionToken, effect } from '@angular/core';
2
+ import { signal, computed, Injectable, inject, Component, EventEmitter, Output, Input, effect } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { FormsModule } from '@angular/forms';
5
5
  import * as i1$1 from '@angular/cdk/drag-drop';
@@ -28,11 +28,12 @@ import * as i4$2 from '@angular/material/tooltip';
28
28
  import { MatTooltipModule } from '@angular/material/tooltip';
29
29
  import { normalizeConfigOptions, getFieldTypeMeta, createFieldConfig, labelToId, createFieldSnapshot, computeFieldDrift, humanizeId, findTab, resolveLabel, resolveOptionLabel, FIELD_TYPE_CATALOG, COMMON_MODULES } from '@dynamic-entity/core';
30
30
  export { FIELD_TYPE_CATALOG, createFieldConfig, getFieldTypeMeta, humanizeId } from '@dynamic-entity/core';
31
+ import { ConfigSourceService, SYSTEM_DEFAULT_CAN_EDIT } from 'ngx-dynamic-entity';
32
+ export { SYSTEM_DEFAULT_CAN_EDIT } from 'ngx-dynamic-entity';
31
33
  import * as i3 from '@angular/material/divider';
32
34
  import { MatDividerModule } from '@angular/material/divider';
33
35
  import * as i9 from '@angular/material/core';
34
36
  import { CommonModule } from '@angular/common';
35
- import { ConfigSourceService } from 'ngx-dynamic-entity';
36
37
 
37
38
  /**
38
39
  * field-catalog.ts — re-exports shared field catalog from @dynamic-entity/core.
@@ -55,6 +56,14 @@ function deepClone(value) {
55
56
 
56
57
  const clone = deepClone;
57
58
  const ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
59
+ /**
60
+ * The regex the builder used to write into `pattern` to mean "email".
61
+ *
62
+ * Retained only to recognise configs authored before `FieldValidators.email` existed, so
63
+ * they still show Email as ticked and do not leave a stray pattern behind when it is
64
+ * un-ticked. Nothing writes it any more.
65
+ */
66
+ const LEGACY_EMAIL_PATTERN = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$';
58
67
  class BuilderStore {
59
68
  _config = signal(this.emptyConfig());
60
69
  _selectedFieldId = signal(null);
@@ -68,11 +77,19 @@ class BuilderStore {
68
77
  */
69
78
  manualIds = new Set();
70
79
  _isDirty = signal(false);
80
+ /**
81
+ * Roles of the person using the builder — distinct from `availableRoles`, which is the
82
+ * vocabulary a schema may reference. Needed so the SYSTEM_DEFAULT_CAN_EDIT predicate can
83
+ * be asked a real question; it was previously called with a hardcoded empty array, which
84
+ * made any role-checking predicate answer false for everyone.
85
+ */
86
+ _userRoles = signal([]);
71
87
  config = this._config.asReadonly();
72
88
  selectedFieldId = this._selectedFieldId.asReadonly();
73
89
  activeLanguage = this._activeLanguage.asReadonly();
74
90
  rules = this._rules.asReadonly();
75
91
  isDirty = this._isDirty.asReadonly();
92
+ userRoles = this._userRoles.asReadonly();
76
93
  tabs = computed(() => this._config().tabs ?? []);
77
94
  fields = computed(() => {
78
95
  return (this._config().tabs ?? []).flatMap(t => t.fields ?? []);
@@ -157,6 +174,10 @@ class BuilderStore {
157
174
  setActiveLanguage(language) {
158
175
  this._activeLanguage.set(language);
159
176
  }
177
+ /** Roles of the person using the builder, for SYSTEM_DEFAULT_CAN_EDIT. */
178
+ setUserRoles(roles) {
179
+ this._userRoles.set(roles ?? []);
180
+ }
160
181
  // ─── Helper Traversal ───────────────────────────────────────────────────────
161
182
  getAllFields(tabs = []) {
162
183
  const list = [];
@@ -168,6 +189,44 @@ class BuilderStore {
168
189
  }
169
190
  return list;
170
191
  }
192
+ /**
193
+ * Every tab in the tree, flattened.
194
+ *
195
+ * The structural operations below used to iterate `draft.tabs` directly, which is only
196
+ * the top level. A field on a sub-tab could be selected and edited but never removed,
197
+ * duplicated, moved or reordered — the builder could author nested configs it was then
198
+ * unable to restructure, which is the worst possible state for an authoring tool.
199
+ */
200
+ flattenTabs(tabs = []) {
201
+ const list = [];
202
+ for (const t of tabs) {
203
+ list.push(t);
204
+ if (t.children)
205
+ list.push(...this.flattenTabs(t.children));
206
+ }
207
+ return list;
208
+ }
209
+ /**
210
+ * Every field id in the config, including `group`/`array` children.
211
+ *
212
+ * Id uniqueness is a whole-config invariant, not a per-tab one: rules, `showWhen`
213
+ * conditions and `autoPatch` mappings all address fields by bare id, so two fields
214
+ * sharing an id anywhere in the tree interfere with each other. Uniqueness checks must
215
+ * therefore see deeper than `fields()`, which stops at a tab's own fields.
216
+ */
217
+ allFieldIds(tabs = []) {
218
+ const ids = new Set();
219
+ const walkFields = (fields) => {
220
+ for (const f of fields ?? []) {
221
+ ids.add(f.id);
222
+ if (f.children?.length)
223
+ walkFields(f.children);
224
+ }
225
+ };
226
+ for (const tab of this.flattenTabs(tabs))
227
+ walkFields(tab.fields);
228
+ return ids;
229
+ }
171
230
  findFieldInTabs(tabs = [], id) {
172
231
  for (const t of tabs) {
173
232
  if (t.fields) {
@@ -187,12 +246,11 @@ class BuilderStore {
187
246
  addField(type, targetTabId) {
188
247
  const meta = getFieldTypeMeta(type);
189
248
  const prefix = meta?.idPrefix ?? 'field';
190
- const allFields = this.fields();
191
- const id = this.uniqueId(prefix, allFields);
249
+ const id = this.uniqueId(prefix, this.allFieldIds(this._config().tabs));
192
250
  this.mutate(draft => {
193
251
  const field = createFieldConfig(type, id, draft.defaultLanguage ?? 'en');
194
252
  const targetTab = targetTabId
195
- ? draft.tabs.find(t => t.id === targetTabId)
253
+ ? this.flattenTabs(draft.tabs).find(t => t.id === targetTabId)
196
254
  : draft.tabs[0];
197
255
  if (targetTab) {
198
256
  targetTab.fields = targetTab.fields ?? [];
@@ -222,7 +280,7 @@ class BuilderStore {
222
280
  const trimmed = newId.trim();
223
281
  if (!trimmed || trimmed === oldId)
224
282
  return;
225
- if (this.fields().some(f => f.id === trimmed))
283
+ if (this.allFieldIds(this._config().tabs).has(trimmed))
226
284
  return;
227
285
  if (!this.findFieldInTabs(this._config().tabs, oldId))
228
286
  return;
@@ -296,7 +354,7 @@ class BuilderStore {
296
354
  }
297
355
  removeField(id) {
298
356
  this.mutate(draft => {
299
- for (const tab of draft.tabs) {
357
+ for (const tab of this.flattenTabs(draft.tabs)) {
300
358
  if (tab.fields) {
301
359
  tab.fields = tab.fields.filter(f => f.id !== id);
302
360
  }
@@ -315,9 +373,9 @@ class BuilderStore {
315
373
  return null;
316
374
  const meta = getFieldTypeMeta(source.type);
317
375
  const prefix = meta?.idPrefix ?? 'field';
318
- const newId = this.uniqueId(prefix, this.fields());
376
+ const newId = this.uniqueId(prefix, this.allFieldIds(this._config().tabs));
319
377
  this.mutate(draft => {
320
- for (const tab of draft.tabs) {
378
+ for (const tab of this.flattenTabs(draft.tabs)) {
321
379
  const index = (tab.fields ?? []).findIndex(f => f.id === id);
322
380
  if (index !== -1) {
323
381
  const copy = clone(source);
@@ -332,7 +390,7 @@ class BuilderStore {
332
390
  }
333
391
  moveField(id, direction) {
334
392
  this.mutate(draft => {
335
- for (const tab of draft.tabs) {
393
+ for (const tab of this.flattenTabs(draft.tabs)) {
336
394
  const fields = tab.fields ?? [];
337
395
  const from = fields.findIndex(f => f.id === id);
338
396
  if (from !== -1) {
@@ -350,7 +408,7 @@ class BuilderStore {
350
408
  reorderField(fromIndex, toIndex, tabId) {
351
409
  this.mutate(draft => {
352
410
  const targetTab = tabId
353
- ? draft.tabs.find(t => t.id === tabId)
411
+ ? this.flattenTabs(draft.tabs).find(t => t.id === tabId)
354
412
  : draft.tabs[0];
355
413
  const fields = targetTab?.fields ?? [];
356
414
  if (fromIndex < 0 ||
@@ -380,11 +438,8 @@ class BuilderStore {
380
438
  * anything.
381
439
  */
382
440
  setFieldLabel(id, language, value) {
383
- this.mutate(draft => {
384
- const field = this.findFieldInTabs(draft.tabs, id);
385
- if (field)
386
- field.label = { ...field.label, [language]: value };
387
- });
441
+ // Bound to a keystroke event, so this uses the structural-sharing path.
442
+ this.mutateField(id, field => ({ ...field, label: { ...field.label, [language]: value } }));
388
443
  if (language !== (this._config().defaultLanguage ?? 'en'))
389
444
  return;
390
445
  if (this.manualIds.has(id))
@@ -396,7 +451,7 @@ class BuilderStore {
396
451
  }
397
452
  /** `derived`, or `derived_2`, `derived_3`… if another field already holds it. */
398
453
  availableId(derived, currentId) {
399
- const taken = new Set(this.fields().map(f => f.id).filter(fid => fid !== currentId));
454
+ const taken = new Set([...this.allFieldIds(this._config().tabs)].filter(fid => fid !== currentId));
400
455
  if (!taken.has(derived))
401
456
  return derived;
402
457
  let n = 2;
@@ -426,9 +481,15 @@ class BuilderStore {
426
481
  delete field.validators.required;
427
482
  }
428
483
  else if (validator === 'email') {
484
+ // Writes its own flag, not `pattern`. Sharing `pattern` meant a field could not have
485
+ // both an email check and a custom regex, setting a custom pattern made the Email box
486
+ // appear ticked, and un-ticking Email deleted whatever pattern the author had written.
429
487
  if (on)
430
- field.validators.pattern = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$';
488
+ field.validators.email = true;
431
489
  else
490
+ delete field.validators.email;
491
+ // Clear the legacy encoding either way, so a config touched here stops carrying both.
492
+ if (field.validators.pattern === LEGACY_EMAIL_PATTERN)
432
493
  delete field.validators.pattern;
433
494
  }
434
495
  });
@@ -454,8 +515,12 @@ class BuilderStore {
454
515
  hasFlagValidator(field, validator) {
455
516
  if (validator === 'required')
456
517
  return !!field.validators?.required;
457
- if (validator === 'email')
458
- return !!field.validators?.pattern;
518
+ if (validator === 'email') {
519
+ // This used to report *any* pattern as email, so writing a custom regex silently
520
+ // ticked the box. The legacy encoding is still recognised so a config authored by the
521
+ // old builder keeps showing Email as ticked.
522
+ return !!field.validators?.email || field.validators?.pattern === LEGACY_EMAIL_PATTERN;
523
+ }
459
524
  return false;
460
525
  }
461
526
  // ─── Options (dropdown / multiSelect / radio) ────────────────────────────────
@@ -628,12 +693,13 @@ class BuilderStore {
628
693
  * so this is the only way to edit one — there is no separate value to set.
629
694
  */
630
695
  setOptionLabel(fieldId, index, language, value) {
631
- this.mutate(draft => {
632
- const field = this.findFieldInTabs(draft.tabs, fieldId);
633
- const option = field?.options?.[index];
696
+ // Also keystroke-bound.
697
+ this.mutateField(fieldId, field => {
698
+ const option = field.options?.[index];
634
699
  if (!option)
635
- return;
636
- field.options[index] = { ...option, [language]: value };
700
+ return field;
701
+ const options = field.options.map((o, i) => (i === index ? { ...o, [language]: value } : o));
702
+ return { ...field, options };
637
703
  });
638
704
  }
639
705
  removeOption(fieldId, index) {
@@ -761,7 +827,7 @@ class BuilderStore {
761
827
  this._rules.set(clone(rules));
762
828
  }
763
829
  addRule(rule) {
764
- const id = rule.id?.trim() || this.uniqueId('rule', this._rules().map(r => ({ id: r.id ?? '' })));
830
+ const id = rule.id?.trim() || this.uniqueId('rule', new Set(this._rules().map(r => r.id ?? '')));
765
831
  this._rules.update(rules => [...rules, { ...clone(rule), id }]);
766
832
  return id;
767
833
  }
@@ -797,7 +863,7 @@ class BuilderStore {
797
863
  // Must be unique across the whole tree, not just the top level: a tab id is the record
798
864
  // storage path, so a new top-level tab colliding with an existing sub-tab would make two
799
865
  // tabs write to the same key and break findTab/getTabPath.
800
- const id = this.uniqueId('tab', this.allTabIds().map(tid => ({ id: tid })));
866
+ const id = this.uniqueId('tab', new Set(this.allTabIds()));
801
867
  this.mutate(draft => {
802
868
  const tabs = draft.tabs ?? (draft.tabs = []);
803
869
  const lang = draft.defaultLanguage ?? 'en';
@@ -818,7 +884,7 @@ class BuilderStore {
818
884
  return ids;
819
885
  }
820
886
  addSubTab(parentId) {
821
- const id = this.uniqueId('tab', this.allTabIds().map(tid => ({ id: tid })));
887
+ const id = this.uniqueId('tab', new Set(this.allTabIds()));
822
888
  this.mutate(draft => {
823
889
  const parent = findTab(draft.tabs, parentId);
824
890
  if (parent) {
@@ -893,8 +959,64 @@ class BuilderStore {
893
959
  this._config.set(draft);
894
960
  this._isDirty.set(true);
895
961
  }
896
- uniqueId(prefix, existing) {
897
- const taken = new Set(existing.map(e => e.id));
962
+ /**
963
+ * Replace one field, copying only the objects on the path to it.
964
+ *
965
+ * `mutate` deep-clones the entire config, which is right for a structural edit but wrong
966
+ * for the two setters bound to keystroke events: typing a label allocated a full copy of
967
+ * the config per character. Here every tab and field outside the target's chain is reused
968
+ * by reference, so the cost is proportional to nesting depth rather than config size —
969
+ * while the parts that did change are still fresh objects, so change detection and the
970
+ * signal both see a new identity.
971
+ *
972
+ * Returns without touching anything when the field is not found.
973
+ */
974
+ mutateField(id, update) {
975
+ let found = false;
976
+ const visitFields = (fields) => {
977
+ if (!fields)
978
+ return fields;
979
+ let changed = false;
980
+ const next = fields.map(field => {
981
+ if (field.id === id) {
982
+ found = true;
983
+ changed = true;
984
+ return update(field);
985
+ }
986
+ const children = visitFields(field.children);
987
+ if (children !== field.children) {
988
+ changed = true;
989
+ return { ...field, children };
990
+ }
991
+ return field;
992
+ });
993
+ return changed ? next : fields;
994
+ };
995
+ const visitTabs = (tabs) => {
996
+ if (!tabs)
997
+ return tabs;
998
+ let changed = false;
999
+ const next = tabs.map(tab => {
1000
+ const fields = visitFields(tab.fields);
1001
+ const children = visitTabs(tab.children);
1002
+ if (fields !== tab.fields || children !== tab.children) {
1003
+ changed = true;
1004
+ return { ...tab, fields, children };
1005
+ }
1006
+ return tab;
1007
+ });
1008
+ return changed ? next : tabs;
1009
+ };
1010
+ const current = this._config();
1011
+ const tabs = visitTabs(current.tabs);
1012
+ // `found` implies current.tabs existed and was walked, so tabs is defined here — the
1013
+ // fallback below is for the type checker, not a reachable state.
1014
+ if (!found || tabs === current.tabs)
1015
+ return;
1016
+ this._config.set({ ...current, tabs: tabs ?? [] });
1017
+ this._isDirty.set(true);
1018
+ }
1019
+ uniqueId(prefix, taken) {
898
1020
  let n = 1;
899
1021
  let candidate = `${prefix}_${n}`;
900
1022
  while (taken.has(candidate)) {
@@ -918,9 +1040,7 @@ class BuilderStore {
918
1040
  if (allFields.length === 0) {
919
1041
  problems.push({ level: 'warning', message: 'This entity has no fields yet.' });
920
1042
  }
921
- const seen = new Map();
922
1043
  for (const field of allFields) {
923
- seen.set(field.id, (seen.get(field.id) ?? 0) + 1);
924
1044
  if (!field.id || !ID_PATTERN.test(field.id)) {
925
1045
  problems.push({
926
1046
  level: 'error',
@@ -957,7 +1077,22 @@ class BuilderStore {
957
1077
  });
958
1078
  }
959
1079
  }
960
- for (const [id, count] of seen) {
1080
+ // Id uniqueness is a whole-config invariant, and it is counted over the entire field
1081
+ // tree rather than `allFields`, which stops at a tab's own fields. Two ids colliding
1082
+ // inside a group or array went unreported even though rules, showWhen conditions and
1083
+ // autoPatch mappings all address fields by bare id and would resolve to whichever one
1084
+ // the lookup happened to reach first.
1085
+ const idCounts = new Map();
1086
+ const countIds = (fields) => {
1087
+ for (const f of fields ?? []) {
1088
+ idCounts.set(f.id, (idCounts.get(f.id) ?? 0) + 1);
1089
+ if (f.children?.length)
1090
+ countIds(f.children);
1091
+ }
1092
+ };
1093
+ for (const tab of this.flattenTabs(config.tabs))
1094
+ countIds(tab.fields);
1095
+ for (const [id, count] of idCounts) {
961
1096
  if (count > 1) {
962
1097
  problems.push({ level: 'error', message: `Duplicate field id "${id}" (${count}×).`, fieldId: id });
963
1098
  }
@@ -1037,156 +1172,156 @@ class EntityReferenceConfigComponent {
1037
1172
  }
1038
1173
  }
1039
1174
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityReferenceConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1040
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EntityReferenceConfigComponent, isStandalone: true, selector: "ngx-entity-reference-config", ngImport: i0, template: `
1041
- @if (field(); as f) {
1042
- <div class="deb-entity-ref">
1043
- <span class="deb-section-title">Entity reference</span>
1044
-
1045
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1046
- <mat-label>Registry key</mat-label>
1047
- <input
1048
- matInput
1049
- data-testid="entity-ref-key"
1050
- [ngModel]="ref(f).linkedEntityKey ?? ''"
1051
- (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1052
- />
1053
- <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
1054
- </mat-form-field>
1055
-
1056
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1057
- <mat-label>Display fields</mat-label>
1058
- <input
1059
- matInput
1060
- data-testid="entity-ref-display-fields"
1061
- [ngModel]="displayFieldsText(f)"
1062
- (ngModelChange)="setDisplayFields(f, $event)"
1063
- />
1064
- <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
1065
- </mat-form-field>
1066
-
1067
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1068
- <mat-label>Static filters (JSON)</mat-label>
1069
- <input
1070
- matInput
1071
- data-testid="entity-ref-filters"
1072
- [ngModel]="filtersText(f)"
1073
- (ngModelChange)="setFilters(f, $event)"
1074
- />
1075
- @if (filtersError()) {
1076
- <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1077
- } @else {
1078
- <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
1079
- }
1080
- </mat-form-field>
1081
-
1082
- <mat-divider></mat-divider>
1083
-
1084
- <!-- Cascade -->
1085
- <span class="deb-section-title">Cascade</span>
1086
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1087
- <mat-label>Parent field</mat-label>
1088
- <mat-select
1089
- data-testid="entity-ref-parent"
1090
- [ngModel]="ref(f).parentField ?? ''"
1091
- (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1092
- >
1093
- <mat-option value="">None — load all options</mat-option>
1094
- @for (candidate of parentCandidates(); track candidate.id) {
1095
- <mat-option [value]="candidate.id">{{ fieldLabel(candidate) }} ({{ candidate.id }})</mat-option>
1096
- }
1097
- </mat-select>
1098
- <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
1099
- </mat-form-field>
1100
-
1101
- @if (ref(f).parentField) {
1102
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1103
- <mat-label>Lookup filter path</mat-label>
1104
- <input
1105
- matInput
1106
- data-testid="entity-ref-lookup-filter"
1107
- [ngModel]="ref(f).lookupFilter ?? ''"
1108
- (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1109
- />
1110
- <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
1111
- </mat-form-field>
1112
-
1113
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1114
- <mat-label>Lookup path (nested options)</mat-label>
1115
- <input
1116
- matInput
1117
- data-testid="entity-ref-lookup-path"
1118
- [ngModel]="ref(f).lookupPath ?? ''"
1119
- (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1120
- />
1121
- <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
1122
- </mat-form-field>
1123
- }
1124
-
1125
- <mat-divider></mat-divider>
1126
-
1127
- <!-- autoPatch -->
1128
- <div class="deb-row deb-row--split">
1129
- <span class="deb-section-title">Auto-patch on select</span>
1130
- <button
1131
- mat-stroked-button
1132
- type="button"
1133
- data-testid="add-auto-patch"
1134
- (click)="store.addAutoPatchMapping(f.id)"
1135
- >
1136
- <mat-icon>add</mat-icon> Mapping
1137
- </button>
1138
- </div>
1139
-
1140
- @if (f.autoPatch) {
1141
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1142
- <mat-label>Target tab</mat-label>
1143
- <mat-select
1144
- data-testid="auto-patch-tab"
1145
- [ngModel]="f.autoPatch.targetTab"
1146
- (ngModelChange)="store.setAutoPatchTargetTab(f.id, $event)"
1147
- >
1148
- @for (tab of store.tabs(); track tab.id) {
1149
- <mat-option [value]="tab.id">{{ tabLabel(tab.id) }}</mat-option>
1150
- }
1151
- </mat-select>
1152
- </mat-form-field>
1153
-
1154
- @for (mapping of f.autoPatch.mappings; track $index) {
1155
- <div class="deb-option-row">
1156
- <mat-form-field appearance="outline" subscriptSizing="dynamic">
1157
- <mat-label>Source (linked record)</mat-label>
1158
- <input
1159
- matInput
1160
- [ngModel]="mapping.source"
1161
- (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { source: $event })"
1162
- />
1163
- </mat-form-field>
1164
- <mat-form-field appearance="outline" subscriptSizing="dynamic">
1165
- <mat-label>Target field</mat-label>
1166
- <input
1167
- matInput
1168
- [ngModel]="mapping.target"
1169
- (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { target: $event })"
1170
- />
1171
- </mat-form-field>
1172
- <button
1173
- mat-icon-button
1174
- type="button"
1175
- color="warn"
1176
- matTooltip="Remove mapping"
1177
- (click)="store.removeAutoPatchMapping(f.id, $index)"
1178
- >
1179
- <mat-icon>delete</mat-icon>
1180
- </button>
1181
- </div>
1182
- }
1183
- } @else {
1184
- <p class="deb-hint">
1185
- No auto-patch configured — selecting a reference will not copy any values.
1186
- </p>
1187
- }
1188
- </div>
1189
- }
1175
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EntityReferenceConfigComponent, isStandalone: true, selector: "ngx-entity-reference-config", ngImport: i0, template: `
1176
+ @if (field(); as f) {
1177
+ <div class="deb-entity-ref">
1178
+ <span class="deb-section-title">Entity reference</span>
1179
+
1180
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1181
+ <mat-label>Registry key</mat-label>
1182
+ <input
1183
+ matInput
1184
+ data-testid="entity-ref-key"
1185
+ [ngModel]="ref(f).linkedEntityKey ?? ''"
1186
+ (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1187
+ />
1188
+ <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
1189
+ </mat-form-field>
1190
+
1191
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1192
+ <mat-label>Display fields</mat-label>
1193
+ <input
1194
+ matInput
1195
+ data-testid="entity-ref-display-fields"
1196
+ [ngModel]="displayFieldsText(f)"
1197
+ (ngModelChange)="setDisplayFields(f, $event)"
1198
+ />
1199
+ <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
1200
+ </mat-form-field>
1201
+
1202
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1203
+ <mat-label>Static filters (JSON)</mat-label>
1204
+ <input
1205
+ matInput
1206
+ data-testid="entity-ref-filters"
1207
+ [ngModel]="filtersText(f)"
1208
+ (ngModelChange)="setFilters(f, $event)"
1209
+ />
1210
+ @if (filtersError()) {
1211
+ <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1212
+ } @else {
1213
+ <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
1214
+ }
1215
+ </mat-form-field>
1216
+
1217
+ <mat-divider></mat-divider>
1218
+
1219
+ <!-- Cascade -->
1220
+ <span class="deb-section-title">Cascade</span>
1221
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1222
+ <mat-label>Parent field</mat-label>
1223
+ <mat-select
1224
+ data-testid="entity-ref-parent"
1225
+ [ngModel]="ref(f).parentField ?? ''"
1226
+ (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1227
+ >
1228
+ <mat-option value="">None — load all options</mat-option>
1229
+ @for (candidate of parentCandidates(); track candidate.id) {
1230
+ <mat-option [value]="candidate.id">{{ fieldLabel(candidate) }} ({{ candidate.id }})</mat-option>
1231
+ }
1232
+ </mat-select>
1233
+ <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
1234
+ </mat-form-field>
1235
+
1236
+ @if (ref(f).parentField) {
1237
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1238
+ <mat-label>Lookup filter path</mat-label>
1239
+ <input
1240
+ matInput
1241
+ data-testid="entity-ref-lookup-filter"
1242
+ [ngModel]="ref(f).lookupFilter ?? ''"
1243
+ (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1244
+ />
1245
+ <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
1246
+ </mat-form-field>
1247
+
1248
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1249
+ <mat-label>Lookup path (nested options)</mat-label>
1250
+ <input
1251
+ matInput
1252
+ data-testid="entity-ref-lookup-path"
1253
+ [ngModel]="ref(f).lookupPath ?? ''"
1254
+ (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1255
+ />
1256
+ <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
1257
+ </mat-form-field>
1258
+ }
1259
+
1260
+ <mat-divider></mat-divider>
1261
+
1262
+ <!-- autoPatch -->
1263
+ <div class="deb-row deb-row--split">
1264
+ <span class="deb-section-title">Auto-patch on select</span>
1265
+ <button
1266
+ mat-stroked-button
1267
+ type="button"
1268
+ data-testid="add-auto-patch"
1269
+ (click)="store.addAutoPatchMapping(f.id)"
1270
+ >
1271
+ <mat-icon>add</mat-icon> Mapping
1272
+ </button>
1273
+ </div>
1274
+
1275
+ @if (f.autoPatch) {
1276
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1277
+ <mat-label>Target tab</mat-label>
1278
+ <mat-select
1279
+ data-testid="auto-patch-tab"
1280
+ [ngModel]="f.autoPatch.targetTab"
1281
+ (ngModelChange)="store.setAutoPatchTargetTab(f.id, $event)"
1282
+ >
1283
+ @for (tab of store.tabs(); track tab.id) {
1284
+ <mat-option [value]="tab.id">{{ tabLabel(tab.id) }}</mat-option>
1285
+ }
1286
+ </mat-select>
1287
+ </mat-form-field>
1288
+
1289
+ @for (mapping of f.autoPatch.mappings; track $index) {
1290
+ <div class="deb-option-row">
1291
+ <mat-form-field appearance="outline" subscriptSizing="dynamic">
1292
+ <mat-label>Source (linked record)</mat-label>
1293
+ <input
1294
+ matInput
1295
+ [ngModel]="mapping.source"
1296
+ (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { source: $event })"
1297
+ />
1298
+ </mat-form-field>
1299
+ <mat-form-field appearance="outline" subscriptSizing="dynamic">
1300
+ <mat-label>Target field</mat-label>
1301
+ <input
1302
+ matInput
1303
+ [ngModel]="mapping.target"
1304
+ (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { target: $event })"
1305
+ />
1306
+ </mat-form-field>
1307
+ <button
1308
+ mat-icon-button
1309
+ type="button"
1310
+ color="warn"
1311
+ matTooltip="Remove mapping"
1312
+ (click)="store.removeAutoPatchMapping(f.id, $index)"
1313
+ >
1314
+ <mat-icon>delete</mat-icon>
1315
+ </button>
1316
+ </div>
1317
+ }
1318
+ } @else {
1319
+ <p class="deb-hint">
1320
+ No auto-patch configured — selecting a reference will not copy any values.
1321
+ </p>
1322
+ }
1323
+ </div>
1324
+ }
1190
1325
  `, isInline: true, styles: [".deb-entity-ref{display:flex;flex-direction:column;gap:12px}.deb-full{width:100%}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-option-row>mat-form-field{flex:1}.deb-error-hint{color:#b91c1c}\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.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[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { 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"], 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"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
1191
1326
  }
1192
1327
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityReferenceConfigComponent, decorators: [{
@@ -1201,156 +1336,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1201
1336
  MatSelectModule,
1202
1337
  MatSlideToggleModule,
1203
1338
  MatTooltipModule,
1204
- ], template: `
1205
- @if (field(); as f) {
1206
- <div class="deb-entity-ref">
1207
- <span class="deb-section-title">Entity reference</span>
1208
-
1209
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1210
- <mat-label>Registry key</mat-label>
1211
- <input
1212
- matInput
1213
- data-testid="entity-ref-key"
1214
- [ngModel]="ref(f).linkedEntityKey ?? ''"
1215
- (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1216
- />
1217
- <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
1218
- </mat-form-field>
1219
-
1220
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1221
- <mat-label>Display fields</mat-label>
1222
- <input
1223
- matInput
1224
- data-testid="entity-ref-display-fields"
1225
- [ngModel]="displayFieldsText(f)"
1226
- (ngModelChange)="setDisplayFields(f, $event)"
1227
- />
1228
- <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
1229
- </mat-form-field>
1230
-
1231
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1232
- <mat-label>Static filters (JSON)</mat-label>
1233
- <input
1234
- matInput
1235
- data-testid="entity-ref-filters"
1236
- [ngModel]="filtersText(f)"
1237
- (ngModelChange)="setFilters(f, $event)"
1238
- />
1239
- @if (filtersError()) {
1240
- <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1241
- } @else {
1242
- <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
1243
- }
1244
- </mat-form-field>
1245
-
1246
- <mat-divider></mat-divider>
1247
-
1248
- <!-- Cascade -->
1249
- <span class="deb-section-title">Cascade</span>
1250
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1251
- <mat-label>Parent field</mat-label>
1252
- <mat-select
1253
- data-testid="entity-ref-parent"
1254
- [ngModel]="ref(f).parentField ?? ''"
1255
- (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1256
- >
1257
- <mat-option value="">None — load all options</mat-option>
1258
- @for (candidate of parentCandidates(); track candidate.id) {
1259
- <mat-option [value]="candidate.id">{{ fieldLabel(candidate) }} ({{ candidate.id }})</mat-option>
1260
- }
1261
- </mat-select>
1262
- <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
1263
- </mat-form-field>
1264
-
1265
- @if (ref(f).parentField) {
1266
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1267
- <mat-label>Lookup filter path</mat-label>
1268
- <input
1269
- matInput
1270
- data-testid="entity-ref-lookup-filter"
1271
- [ngModel]="ref(f).lookupFilter ?? ''"
1272
- (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1273
- />
1274
- <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
1275
- </mat-form-field>
1276
-
1277
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1278
- <mat-label>Lookup path (nested options)</mat-label>
1279
- <input
1280
- matInput
1281
- data-testid="entity-ref-lookup-path"
1282
- [ngModel]="ref(f).lookupPath ?? ''"
1283
- (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1284
- />
1285
- <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
1286
- </mat-form-field>
1287
- }
1288
-
1289
- <mat-divider></mat-divider>
1290
-
1291
- <!-- autoPatch -->
1292
- <div class="deb-row deb-row--split">
1293
- <span class="deb-section-title">Auto-patch on select</span>
1294
- <button
1295
- mat-stroked-button
1296
- type="button"
1297
- data-testid="add-auto-patch"
1298
- (click)="store.addAutoPatchMapping(f.id)"
1299
- >
1300
- <mat-icon>add</mat-icon> Mapping
1301
- </button>
1302
- </div>
1303
-
1304
- @if (f.autoPatch) {
1305
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1306
- <mat-label>Target tab</mat-label>
1307
- <mat-select
1308
- data-testid="auto-patch-tab"
1309
- [ngModel]="f.autoPatch.targetTab"
1310
- (ngModelChange)="store.setAutoPatchTargetTab(f.id, $event)"
1311
- >
1312
- @for (tab of store.tabs(); track tab.id) {
1313
- <mat-option [value]="tab.id">{{ tabLabel(tab.id) }}</mat-option>
1314
- }
1315
- </mat-select>
1316
- </mat-form-field>
1317
-
1318
- @for (mapping of f.autoPatch.mappings; track $index) {
1319
- <div class="deb-option-row">
1320
- <mat-form-field appearance="outline" subscriptSizing="dynamic">
1321
- <mat-label>Source (linked record)</mat-label>
1322
- <input
1323
- matInput
1324
- [ngModel]="mapping.source"
1325
- (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { source: $event })"
1326
- />
1327
- </mat-form-field>
1328
- <mat-form-field appearance="outline" subscriptSizing="dynamic">
1329
- <mat-label>Target field</mat-label>
1330
- <input
1331
- matInput
1332
- [ngModel]="mapping.target"
1333
- (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { target: $event })"
1334
- />
1335
- </mat-form-field>
1336
- <button
1337
- mat-icon-button
1338
- type="button"
1339
- color="warn"
1340
- matTooltip="Remove mapping"
1341
- (click)="store.removeAutoPatchMapping(f.id, $index)"
1342
- >
1343
- <mat-icon>delete</mat-icon>
1344
- </button>
1345
- </div>
1346
- }
1347
- } @else {
1348
- <p class="deb-hint">
1349
- No auto-patch configured — selecting a reference will not copy any values.
1350
- </p>
1351
- }
1352
- </div>
1353
- }
1339
+ ], template: `
1340
+ @if (field(); as f) {
1341
+ <div class="deb-entity-ref">
1342
+ <span class="deb-section-title">Entity reference</span>
1343
+
1344
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1345
+ <mat-label>Registry key</mat-label>
1346
+ <input
1347
+ matInput
1348
+ data-testid="entity-ref-key"
1349
+ [ngModel]="ref(f).linkedEntityKey ?? ''"
1350
+ (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1351
+ />
1352
+ <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
1353
+ </mat-form-field>
1354
+
1355
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1356
+ <mat-label>Display fields</mat-label>
1357
+ <input
1358
+ matInput
1359
+ data-testid="entity-ref-display-fields"
1360
+ [ngModel]="displayFieldsText(f)"
1361
+ (ngModelChange)="setDisplayFields(f, $event)"
1362
+ />
1363
+ <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
1364
+ </mat-form-field>
1365
+
1366
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1367
+ <mat-label>Static filters (JSON)</mat-label>
1368
+ <input
1369
+ matInput
1370
+ data-testid="entity-ref-filters"
1371
+ [ngModel]="filtersText(f)"
1372
+ (ngModelChange)="setFilters(f, $event)"
1373
+ />
1374
+ @if (filtersError()) {
1375
+ <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1376
+ } @else {
1377
+ <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
1378
+ }
1379
+ </mat-form-field>
1380
+
1381
+ <mat-divider></mat-divider>
1382
+
1383
+ <!-- Cascade -->
1384
+ <span class="deb-section-title">Cascade</span>
1385
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1386
+ <mat-label>Parent field</mat-label>
1387
+ <mat-select
1388
+ data-testid="entity-ref-parent"
1389
+ [ngModel]="ref(f).parentField ?? ''"
1390
+ (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1391
+ >
1392
+ <mat-option value="">None — load all options</mat-option>
1393
+ @for (candidate of parentCandidates(); track candidate.id) {
1394
+ <mat-option [value]="candidate.id">{{ fieldLabel(candidate) }} ({{ candidate.id }})</mat-option>
1395
+ }
1396
+ </mat-select>
1397
+ <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
1398
+ </mat-form-field>
1399
+
1400
+ @if (ref(f).parentField) {
1401
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1402
+ <mat-label>Lookup filter path</mat-label>
1403
+ <input
1404
+ matInput
1405
+ data-testid="entity-ref-lookup-filter"
1406
+ [ngModel]="ref(f).lookupFilter ?? ''"
1407
+ (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1408
+ />
1409
+ <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
1410
+ </mat-form-field>
1411
+
1412
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1413
+ <mat-label>Lookup path (nested options)</mat-label>
1414
+ <input
1415
+ matInput
1416
+ data-testid="entity-ref-lookup-path"
1417
+ [ngModel]="ref(f).lookupPath ?? ''"
1418
+ (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1419
+ />
1420
+ <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
1421
+ </mat-form-field>
1422
+ }
1423
+
1424
+ <mat-divider></mat-divider>
1425
+
1426
+ <!-- autoPatch -->
1427
+ <div class="deb-row deb-row--split">
1428
+ <span class="deb-section-title">Auto-patch on select</span>
1429
+ <button
1430
+ mat-stroked-button
1431
+ type="button"
1432
+ data-testid="add-auto-patch"
1433
+ (click)="store.addAutoPatchMapping(f.id)"
1434
+ >
1435
+ <mat-icon>add</mat-icon> Mapping
1436
+ </button>
1437
+ </div>
1438
+
1439
+ @if (f.autoPatch) {
1440
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1441
+ <mat-label>Target tab</mat-label>
1442
+ <mat-select
1443
+ data-testid="auto-patch-tab"
1444
+ [ngModel]="f.autoPatch.targetTab"
1445
+ (ngModelChange)="store.setAutoPatchTargetTab(f.id, $event)"
1446
+ >
1447
+ @for (tab of store.tabs(); track tab.id) {
1448
+ <mat-option [value]="tab.id">{{ tabLabel(tab.id) }}</mat-option>
1449
+ }
1450
+ </mat-select>
1451
+ </mat-form-field>
1452
+
1453
+ @for (mapping of f.autoPatch.mappings; track $index) {
1454
+ <div class="deb-option-row">
1455
+ <mat-form-field appearance="outline" subscriptSizing="dynamic">
1456
+ <mat-label>Source (linked record)</mat-label>
1457
+ <input
1458
+ matInput
1459
+ [ngModel]="mapping.source"
1460
+ (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { source: $event })"
1461
+ />
1462
+ </mat-form-field>
1463
+ <mat-form-field appearance="outline" subscriptSizing="dynamic">
1464
+ <mat-label>Target field</mat-label>
1465
+ <input
1466
+ matInput
1467
+ [ngModel]="mapping.target"
1468
+ (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { target: $event })"
1469
+ />
1470
+ </mat-form-field>
1471
+ <button
1472
+ mat-icon-button
1473
+ type="button"
1474
+ color="warn"
1475
+ matTooltip="Remove mapping"
1476
+ (click)="store.removeAutoPatchMapping(f.id, $index)"
1477
+ >
1478
+ <mat-icon>delete</mat-icon>
1479
+ </button>
1480
+ </div>
1481
+ }
1482
+ } @else {
1483
+ <p class="deb-hint">
1484
+ No auto-patch configured — selecting a reference will not copy any values.
1485
+ </p>
1486
+ }
1487
+ </div>
1488
+ }
1354
1489
  `, styles: [".deb-entity-ref{display:flex;flex-direction:column;gap:12px}.deb-full{width:100%}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-option-row{display:flex;gap:6px;align-items:center}.deb-option-row>mat-form-field{flex:1}.deb-error-hint{color:#b91c1c}\n"] }]
1355
1490
  }] });
1356
1491
 
@@ -1587,166 +1722,166 @@ class FieldRulesListComponent {
1587
1722
  return `${rule.fieldId} ${cond} → ${action}`;
1588
1723
  }
1589
1724
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FieldRulesListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1590
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FieldRulesListComponent, isStandalone: true, selector: "ngx-field-rules-list", ngImport: i0, template: `
1591
- <div class="deb-rules">
1592
- <div class="deb-rules__head">
1593
- <span class="deb-section-title">Rules</span>
1594
- <button
1595
- mat-stroked-button
1596
- type="button"
1597
- data-testid="add-rule"
1598
- [disabled]="!store.selectedFieldId()"
1599
- (click)="startCreate()"
1600
- >
1601
- <mat-icon>add</mat-icon> Rule
1602
- </button>
1603
- </div>
1604
-
1605
- @if (editing(); as draft) {
1606
- <ngx-rule-form [rule]="draft" (save)="commit($event)" (cancel)="editing.set(null)" />
1607
- }
1608
-
1609
- @if (store.rulesForSelectedField().length === 0 && !editing()) {
1610
- <p class="deb-hint" data-testid="rules-empty">No rules on this field.</p>
1611
- }
1612
-
1613
- @for (rule of store.rulesForSelectedField(); track rule.id) {
1614
- <div class="deb-rule-item" [class.deb-rule-item--off]="!rule.enabled" data-testid="rule-item">
1615
- <div class="deb-rule-item__body">
1616
- <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
1617
- <span class="deb-rule-item__meta">
1618
- priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
1619
- </span>
1620
- </div>
1621
- <div class="deb-rule-item__actions">
1622
- <mat-slide-toggle
1623
- [checked]="rule.enabled"
1624
- matTooltip="Enable / disable"
1625
- (change)="store.toggleRule(rule.id!, $event.checked)"
1626
- />
1627
- <button
1628
- mat-icon-button
1629
- type="button"
1630
- matTooltip="Move up"
1631
- [attr.data-testid]="'rule-up-' + rule.id"
1632
- (click)="store.moveRule(rule.id!, -1)"
1633
- >
1634
- <mat-icon>arrow_upward</mat-icon>
1635
- </button>
1636
- <button
1637
- mat-icon-button
1638
- type="button"
1639
- matTooltip="Move down"
1640
- [attr.data-testid]="'rule-down-' + rule.id"
1641
- (click)="store.moveRule(rule.id!, 1)"
1642
- >
1643
- <mat-icon>arrow_downward</mat-icon>
1644
- </button>
1645
- <button
1646
- mat-icon-button
1647
- type="button"
1648
- matTooltip="Edit rule"
1649
- [attr.data-testid]="'rule-edit-' + rule.id"
1650
- (click)="startEdit(rule)"
1651
- >
1652
- <mat-icon>edit</mat-icon>
1653
- </button>
1654
- <button
1655
- mat-icon-button
1656
- type="button"
1657
- color="warn"
1658
- matTooltip="Delete rule"
1659
- [attr.data-testid]="'rule-delete-' + rule.id"
1660
- (click)="store.removeRule(rule.id!)"
1661
- >
1662
- <mat-icon>delete</mat-icon>
1663
- </button>
1664
- </div>
1665
- </div>
1666
- }
1667
- </div>
1725
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FieldRulesListComponent, isStandalone: true, selector: "ngx-field-rules-list", ngImport: i0, template: `
1726
+ <div class="deb-rules">
1727
+ <div class="deb-rules__head">
1728
+ <span class="deb-section-title">Rules</span>
1729
+ <button
1730
+ mat-stroked-button
1731
+ type="button"
1732
+ data-testid="add-rule"
1733
+ [disabled]="!store.selectedFieldId()"
1734
+ (click)="startCreate()"
1735
+ >
1736
+ <mat-icon>add</mat-icon> Rule
1737
+ </button>
1738
+ </div>
1739
+
1740
+ @if (editing(); as draft) {
1741
+ <ngx-rule-form [rule]="draft" (save)="commit($event)" (cancel)="editing.set(null)" />
1742
+ }
1743
+
1744
+ @if (store.rulesForSelectedField().length === 0 && !editing()) {
1745
+ <p class="deb-hint" data-testid="rules-empty">No rules on this field.</p>
1746
+ }
1747
+
1748
+ @for (rule of store.rulesForSelectedField(); track rule.id) {
1749
+ <div class="deb-rule-item" [class.deb-rule-item--off]="!rule.enabled" data-testid="rule-item">
1750
+ <div class="deb-rule-item__body">
1751
+ <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
1752
+ <span class="deb-rule-item__meta">
1753
+ priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
1754
+ </span>
1755
+ </div>
1756
+ <div class="deb-rule-item__actions">
1757
+ <mat-slide-toggle
1758
+ [checked]="rule.enabled"
1759
+ matTooltip="Enable / disable"
1760
+ (change)="store.toggleRule(rule.id!, $event.checked)"
1761
+ />
1762
+ <button
1763
+ mat-icon-button
1764
+ type="button"
1765
+ matTooltip="Move up" aria-label="Move rule up"
1766
+ [attr.data-testid]="'rule-up-' + rule.id"
1767
+ (click)="store.moveRule(rule.id!, -1)"
1768
+ >
1769
+ <mat-icon>arrow_upward</mat-icon>
1770
+ </button>
1771
+ <button
1772
+ mat-icon-button
1773
+ type="button"
1774
+ matTooltip="Move down" aria-label="Move rule down"
1775
+ [attr.data-testid]="'rule-down-' + rule.id"
1776
+ (click)="store.moveRule(rule.id!, 1)"
1777
+ >
1778
+ <mat-icon>arrow_downward</mat-icon>
1779
+ </button>
1780
+ <button
1781
+ mat-icon-button
1782
+ type="button"
1783
+ matTooltip="Edit rule"
1784
+ [attr.data-testid]="'rule-edit-' + rule.id"
1785
+ (click)="startEdit(rule)"
1786
+ >
1787
+ <mat-icon>edit</mat-icon>
1788
+ </button>
1789
+ <button
1790
+ mat-icon-button
1791
+ type="button"
1792
+ color="warn"
1793
+ matTooltip="Delete rule"
1794
+ [attr.data-testid]="'rule-delete-' + rule.id"
1795
+ (click)="store.removeRule(rule.id!)"
1796
+ >
1797
+ <mat-icon>delete</mat-icon>
1798
+ </button>
1799
+ </div>
1800
+ </div>
1801
+ }
1802
+ </div>
1668
1803
  `, isInline: true, styles: [".deb-rules{display:flex;flex-direction:column;gap:8px}.deb-rules__head{display:flex;align-items:center;justify-content:space-between}.deb-rule-item{display:flex;align-items:center;justify-content:space-between;gap:8px;border:1px solid #e5e7eb;border-radius:8px;padding:8px 10px}.deb-rule-item--off{opacity:.55}.deb-rule-item__body{display:flex;flex-direction:column;min-width:0}.deb-rule-item__summary{font-size:13px;font-weight:600;overflow-wrap:anywhere}.deb-rule-item__meta{font-size:11px;color:#6b7280}.deb-rule-item__actions{display:flex;align-items:center;flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i10.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon"], 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: RuleFormComponent, selector: "ngx-rule-form", inputs: ["rule"], outputs: ["save", "cancel"] }] });
1669
1804
  }
1670
1805
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FieldRulesListComponent, decorators: [{
1671
1806
  type: Component,
1672
- args: [{ selector: 'ngx-field-rules-list', standalone: true, imports: [MatButtonModule, MatIconModule, MatSlideToggleModule, MatTooltipModule, RuleFormComponent], template: `
1673
- <div class="deb-rules">
1674
- <div class="deb-rules__head">
1675
- <span class="deb-section-title">Rules</span>
1676
- <button
1677
- mat-stroked-button
1678
- type="button"
1679
- data-testid="add-rule"
1680
- [disabled]="!store.selectedFieldId()"
1681
- (click)="startCreate()"
1682
- >
1683
- <mat-icon>add</mat-icon> Rule
1684
- </button>
1685
- </div>
1686
-
1687
- @if (editing(); as draft) {
1688
- <ngx-rule-form [rule]="draft" (save)="commit($event)" (cancel)="editing.set(null)" />
1689
- }
1690
-
1691
- @if (store.rulesForSelectedField().length === 0 && !editing()) {
1692
- <p class="deb-hint" data-testid="rules-empty">No rules on this field.</p>
1693
- }
1694
-
1695
- @for (rule of store.rulesForSelectedField(); track rule.id) {
1696
- <div class="deb-rule-item" [class.deb-rule-item--off]="!rule.enabled" data-testid="rule-item">
1697
- <div class="deb-rule-item__body">
1698
- <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
1699
- <span class="deb-rule-item__meta">
1700
- priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
1701
- </span>
1702
- </div>
1703
- <div class="deb-rule-item__actions">
1704
- <mat-slide-toggle
1705
- [checked]="rule.enabled"
1706
- matTooltip="Enable / disable"
1707
- (change)="store.toggleRule(rule.id!, $event.checked)"
1708
- />
1709
- <button
1710
- mat-icon-button
1711
- type="button"
1712
- matTooltip="Move up"
1713
- [attr.data-testid]="'rule-up-' + rule.id"
1714
- (click)="store.moveRule(rule.id!, -1)"
1715
- >
1716
- <mat-icon>arrow_upward</mat-icon>
1717
- </button>
1718
- <button
1719
- mat-icon-button
1720
- type="button"
1721
- matTooltip="Move down"
1722
- [attr.data-testid]="'rule-down-' + rule.id"
1723
- (click)="store.moveRule(rule.id!, 1)"
1724
- >
1725
- <mat-icon>arrow_downward</mat-icon>
1726
- </button>
1727
- <button
1728
- mat-icon-button
1729
- type="button"
1730
- matTooltip="Edit rule"
1731
- [attr.data-testid]="'rule-edit-' + rule.id"
1732
- (click)="startEdit(rule)"
1733
- >
1734
- <mat-icon>edit</mat-icon>
1735
- </button>
1736
- <button
1737
- mat-icon-button
1738
- type="button"
1739
- color="warn"
1740
- matTooltip="Delete rule"
1741
- [attr.data-testid]="'rule-delete-' + rule.id"
1742
- (click)="store.removeRule(rule.id!)"
1743
- >
1744
- <mat-icon>delete</mat-icon>
1745
- </button>
1746
- </div>
1747
- </div>
1748
- }
1749
- </div>
1807
+ args: [{ selector: 'ngx-field-rules-list', standalone: true, imports: [MatButtonModule, MatIconModule, MatSlideToggleModule, MatTooltipModule, RuleFormComponent], template: `
1808
+ <div class="deb-rules">
1809
+ <div class="deb-rules__head">
1810
+ <span class="deb-section-title">Rules</span>
1811
+ <button
1812
+ mat-stroked-button
1813
+ type="button"
1814
+ data-testid="add-rule"
1815
+ [disabled]="!store.selectedFieldId()"
1816
+ (click)="startCreate()"
1817
+ >
1818
+ <mat-icon>add</mat-icon> Rule
1819
+ </button>
1820
+ </div>
1821
+
1822
+ @if (editing(); as draft) {
1823
+ <ngx-rule-form [rule]="draft" (save)="commit($event)" (cancel)="editing.set(null)" />
1824
+ }
1825
+
1826
+ @if (store.rulesForSelectedField().length === 0 && !editing()) {
1827
+ <p class="deb-hint" data-testid="rules-empty">No rules on this field.</p>
1828
+ }
1829
+
1830
+ @for (rule of store.rulesForSelectedField(); track rule.id) {
1831
+ <div class="deb-rule-item" [class.deb-rule-item--off]="!rule.enabled" data-testid="rule-item">
1832
+ <div class="deb-rule-item__body">
1833
+ <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
1834
+ <span class="deb-rule-item__meta">
1835
+ priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
1836
+ </span>
1837
+ </div>
1838
+ <div class="deb-rule-item__actions">
1839
+ <mat-slide-toggle
1840
+ [checked]="rule.enabled"
1841
+ matTooltip="Enable / disable"
1842
+ (change)="store.toggleRule(rule.id!, $event.checked)"
1843
+ />
1844
+ <button
1845
+ mat-icon-button
1846
+ type="button"
1847
+ matTooltip="Move up" aria-label="Move rule up"
1848
+ [attr.data-testid]="'rule-up-' + rule.id"
1849
+ (click)="store.moveRule(rule.id!, -1)"
1850
+ >
1851
+ <mat-icon>arrow_upward</mat-icon>
1852
+ </button>
1853
+ <button
1854
+ mat-icon-button
1855
+ type="button"
1856
+ matTooltip="Move down" aria-label="Move rule down"
1857
+ [attr.data-testid]="'rule-down-' + rule.id"
1858
+ (click)="store.moveRule(rule.id!, 1)"
1859
+ >
1860
+ <mat-icon>arrow_downward</mat-icon>
1861
+ </button>
1862
+ <button
1863
+ mat-icon-button
1864
+ type="button"
1865
+ matTooltip="Edit rule"
1866
+ [attr.data-testid]="'rule-edit-' + rule.id"
1867
+ (click)="startEdit(rule)"
1868
+ >
1869
+ <mat-icon>edit</mat-icon>
1870
+ </button>
1871
+ <button
1872
+ mat-icon-button
1873
+ type="button"
1874
+ color="warn"
1875
+ matTooltip="Delete rule"
1876
+ [attr.data-testid]="'rule-delete-' + rule.id"
1877
+ (click)="store.removeRule(rule.id!)"
1878
+ >
1879
+ <mat-icon>delete</mat-icon>
1880
+ </button>
1881
+ </div>
1882
+ </div>
1883
+ }
1884
+ </div>
1750
1885
  `, styles: [".deb-rules{display:flex;flex-direction:column;gap:8px}.deb-rules__head{display:flex;align-items:center;justify-content:space-between}.deb-rule-item{display:flex;align-items:center;justify-content:space-between;gap:8px;border:1px solid #e5e7eb;border-radius:8px;padding:8px 10px}.deb-rule-item--off{opacity:.55}.deb-rule-item__body{display:flex;flex-direction:column;min-width:0}.deb-rule-item__summary{font-size:13px;font-weight:600;overflow-wrap:anywhere}.deb-rule-item__meta{font-size:11px;color:#6b7280}.deb-rule-item__actions{display:flex;align-items:center;flex:0 0 auto}\n"] }]
1751
1886
  }] });
1752
1887
 
@@ -1783,8 +1918,14 @@ class ReferencedFieldConfigComponent {
1783
1918
  this.store.syncReferencedField(field.id, sourceField);
1784
1919
  }
1785
1920
  }
1921
+ /**
1922
+ * Both callers pass the id of the field they just edited. This used to ignore that
1923
+ * argument and read `store.selectedField()` instead, so whenever the edited field was not
1924
+ * the selected one it checked drift against the wrong field's source entity — or bailed
1925
+ * out entirely if nothing was selected.
1926
+ */
1786
1927
  async checkDriftForField(fieldId) {
1787
- const f = this.store.selectedField();
1928
+ const f = this.store.fields().find(field => field.id === fieldId);
1788
1929
  if (!f || !f.isReferenced || !f.referencedEntityKey || !f.referencedFieldId || !this.configSource)
1789
1930
  return;
1790
1931
  const sourceConfig = await this.configSource.getConfig(f.referencedEntityKey);
@@ -2049,7 +2190,7 @@ class FieldInspectorComponent {
2049
2190
  return String(value);
2050
2191
  }
2051
2192
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2052
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FieldInspectorComponent, isStandalone: true, selector: "ngx-field-inspector", ngImport: i0, template: "@if (field(); as f) {\r\n <div class=\"deb-inspector\">\r\n <!-- Identity -->\r\n <div class=\"deb-row\">\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>Field id</mat-label>\r\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\r\n @if (store.hasManualId(f.id)) {\r\n <mat-hint>The stored data key for this saved field \u2014 fixed.</mat-hint>\r\n } @else {\r\n <mat-hint>Derived from the label. Used as the data key.</mat-hint>\r\n }\r\n </mat-form-field>\r\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\r\n </div>\r\n\r\n <!-- Localised text (active language) -->\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Label ({{ lang() }})</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"labelValue(f)\"\r\n (ngModelChange)=\"store.setFieldLabel(f.id, lang(), $event)\"\r\n />\r\n </mat-form-field>\r\n\r\n @if (meta()?.supportsPlaceholder) {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Placeholder ({{ lang() }})</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"placeholderValue(f)\"\r\n (ngModelChange)=\"store.setFieldPlaceholder(f.id, lang(), $event)\"\r\n />\r\n </mat-form-field>\r\n }\r\n\r\n @if (meta()?.supportsDefaultValue) {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Default value</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"f.defaultValue ?? ''\"\r\n (ngModelChange)=\"store.updateField(f.id, { defaultValue: $event })\"\r\n />\r\n </mat-form-field>\r\n }\r\n\r\n <!-- Entity reference, cascade, and autoPatch -->\r\n @if (meta()?.isEntityRef) {\r\n <mat-divider></mat-divider>\r\n <ngx-entity-reference-config />\r\n }\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Validators -->\r\n @if (meta(); as m) {\r\n @if (m.flagValidators.length || m.paramValidators.length) {\r\n <span class=\"deb-section-title\">Validation</span>\r\n <div class=\"deb-toggles\">\r\n @for (v of m.flagValidators; track v) {\r\n <mat-checkbox\r\n [ngModel]=\"store.hasFlagValidator(f, v)\"\r\n (ngModelChange)=\"store.toggleFlagValidator(f.id, v, $event)\"\r\n >{{ v }}</mat-checkbox>\r\n }\r\n </div>\r\n @for (v of m.paramValidators; track v) {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>{{ v }}</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [ngModel]=\"store.getParamValidator(f, v)\"\r\n (ngModelChange)=\"store.setParamValidator(f.id, v, toNum($event))\"\r\n />\r\n </mat-form-field>\r\n }\r\n }\r\n }\r\n\r\n <!-- Options (dropdown / radio / multiSelect) -->\r\n @if (meta()?.hasOptions) {\r\n <mat-divider></mat-divider>\r\n <span class=\"deb-section-title\">Data source</span>\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Options come from</mat-label>\r\n <mat-select\r\n data-testid=\"data-source\"\r\n [ngModel]=\"store.fieldDataSource(f)\"\r\n (ngModelChange)=\"store.setFieldDataSource(f.id, $event)\"\r\n >\r\n <mat-option value=\"none\">Nothing yet</mat-option>\r\n <mat-option value=\"manual\">Options authored here</mat-option>\r\n <mat-option value=\"lookup\">A named list</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n }\r\n\r\n <!-- Named lookup list -->\r\n @if (meta()?.hasOptions && store.fieldDataSource(f) === 'lookup') {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>List name</mat-label>\r\n <input\r\n matInput\r\n data-testid=\"list-name\"\r\n [ngModel]=\"f.listName ?? ''\"\r\n (ngModelChange)=\"store.setListName(f.id, $event)\"\r\n />\r\n </mat-form-field>\r\n <p class=\"deb-hint\">\r\n The consuming app resolves this name through its lookup registry. Options are not\r\n authored here.\r\n </p>\r\n }\r\n\r\n <!-- Inline options -->\r\n @if (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\r\n <div class=\"deb-row\" style=\"justify-content: space-between;\">\r\n <span class=\"deb-section-title\">Options</span>\r\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(f.id)\">\r\n <mat-icon>add</mat-icon> Option\r\n </button>\r\n </div>\r\n @for (opt of f.options ?? []; track $index) {\r\n <div class=\"deb-option-row\" data-testid=\"option-row\">\r\n <!-- One input: the displayed text is the stored value. -->\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Option ({{ lang() }})</mat-label>\r\n <input\r\n matInput\r\n [attr.data-testid]=\"'option-' + $index\"\r\n [ngModel]=\"optionLabel(opt)\"\r\n (ngModelChange)=\"store.setOptionLabel(f.id, $index, lang(), $event)\"\r\n />\r\n </mat-form-field>\r\n <button mat-icon-button type=\"button\" color=\"warn\"\r\n (click)=\"store.removeOption(f.id, $index)\" matTooltip=\"Remove option\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n @if (!(f.options ?? []).length) {\r\n <p class=\"deb-hint\">No options yet \u2014 add at least one.</p>\r\n }\r\n }\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Flags -->\r\n <span class=\"deb-section-title\">Display</span>\r\n <div class=\"deb-toggles\">\r\n <mat-slide-toggle\r\n [ngModel]=\"f.visibility ?? true\"\r\n (ngModelChange)=\"store.updateField(f.id, { visibility: $event })\"\r\n >Visible</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.table?.visible ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { table: { visible: $event } })\"\r\n >Table column</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.readonly ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { readonly: $event })\"\r\n >Read-only</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.disabled ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { disabled: $event })\"\r\n >Disabled</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.maskData ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { maskData: $event })\"\r\n >Mask data (RBAC)</mat-slide-toggle>\r\n <mat-slide-toggle\r\n data-testid=\"toggle-critical\"\r\n [ngModel]=\"f.criticalField ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { criticalField: $event })\"\r\n matTooltip=\"Renders locked; editing it announces a change against the session baseline.\"\r\n >Critical field</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.showOnMinimize ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { showOnMinimize: $event })\"\r\n matTooltip=\"Shown in the record editor's summary panel.\"\r\n >Show in summary</mat-slide-toggle>\r\n </div>\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Conditional visibility (static showWhen) -->\r\n <div class=\"deb-row deb-row--split\">\r\n <span class=\"deb-section-title\">Show when</span>\r\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\r\n <mat-icon>add</mat-icon> Condition\r\n </button>\r\n </div>\r\n @for (entry of showWhenEntries(f); track entry.key) {\r\n <div class=\"deb-option-row\" data-testid=\"option-row\">\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>Field id</mat-label>\r\n <input matInput [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\" />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>Equals</mat-label>\r\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\r\n </mat-form-field>\r\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove condition\"\r\n (click)=\"removeShowWhen(f, entry.key)\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n @if (!showWhenEntries(f).length) {\r\n <p class=\"deb-hint\">Always visible. Add a condition to show this field only for certain values.</p>\r\n }\r\n\r\n <!-- patchOnTrue (boolean/checkbox only) -->\r\n @if (f.type === 'boolean' || f.type === 'checkbox') {\r\n <mat-divider></mat-divider>\r\n <div class=\"deb-row deb-row--split\">\r\n <span class=\"deb-section-title\">Copy on true</span>\r\n <button mat-stroked-button type=\"button\" data-testid=\"add-patch-on-true\"\r\n (click)=\"store.addPatchOnTrueMapping(f.id)\">\r\n <mat-icon>add</mat-icon> Mapping\r\n </button>\r\n </div>\r\n @for (mapping of f.patchOnTrue ?? []; track $index) {\r\n <div class=\"deb-option-row\" data-testid=\"option-row\">\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>From field</mat-label>\r\n <input matInput [ngModel]=\"mapping.from\"\r\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { from: $event })\" />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>To field</mat-label>\r\n <input matInput [ngModel]=\"mapping.to\"\r\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { to: $event })\" />\r\n </mat-form-field>\r\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove mapping\"\r\n (click)=\"store.removePatchOnTrueMapping(f.id, $index)\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n @if (!(f.patchOnTrue ?? []).length) {\r\n <p class=\"deb-hint\">When this flips to true, copy one field's value into another.</p>\r\n }\r\n }\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Rules attached to this field -->\r\n <ngx-field-rules-list />\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Cross-entity field reference & drift management (Phase 8) -->\r\n <ngx-referenced-field-config />\r\n </div>\r\n} @else {\r\n <div class=\"deb-inspector__empty\">\r\n <mat-icon>touch_app</mat-icon>\r\n <p>Select a field to edit its properties.</p>\r\n </div>\r\n}\r\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[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "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"], 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"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i10.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon"], 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" }] });
2193
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", 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 <!-- 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 [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>Field id</mat-label>\n <input matInput [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\" />\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 <input matInput [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { from: $event })\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>To field</mat-label>\n <input matInput [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { to: $event })\" />\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[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "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"], 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"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i10.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon"], 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" }] });
2053
2194
  }
2054
2195
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FieldInspectorComponent, decorators: [{
2055
2196
  type: Component,
@@ -2067,7 +2208,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2067
2208
  EntityReferenceConfigComponent,
2068
2209
  FieldRulesListComponent,
2069
2210
  ReferencedFieldConfigComponent,
2070
- ], template: "@if (field(); as f) {\r\n <div class=\"deb-inspector\">\r\n <!-- Identity -->\r\n <div class=\"deb-row\">\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>Field id</mat-label>\r\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\r\n @if (store.hasManualId(f.id)) {\r\n <mat-hint>The stored data key for this saved field \u2014 fixed.</mat-hint>\r\n } @else {\r\n <mat-hint>Derived from the label. Used as the data key.</mat-hint>\r\n }\r\n </mat-form-field>\r\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\r\n </div>\r\n\r\n <!-- Localised text (active language) -->\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Label ({{ lang() }})</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"labelValue(f)\"\r\n (ngModelChange)=\"store.setFieldLabel(f.id, lang(), $event)\"\r\n />\r\n </mat-form-field>\r\n\r\n @if (meta()?.supportsPlaceholder) {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Placeholder ({{ lang() }})</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"placeholderValue(f)\"\r\n (ngModelChange)=\"store.setFieldPlaceholder(f.id, lang(), $event)\"\r\n />\r\n </mat-form-field>\r\n }\r\n\r\n @if (meta()?.supportsDefaultValue) {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Default value</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"f.defaultValue ?? ''\"\r\n (ngModelChange)=\"store.updateField(f.id, { defaultValue: $event })\"\r\n />\r\n </mat-form-field>\r\n }\r\n\r\n <!-- Entity reference, cascade, and autoPatch -->\r\n @if (meta()?.isEntityRef) {\r\n <mat-divider></mat-divider>\r\n <ngx-entity-reference-config />\r\n }\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Validators -->\r\n @if (meta(); as m) {\r\n @if (m.flagValidators.length || m.paramValidators.length) {\r\n <span class=\"deb-section-title\">Validation</span>\r\n <div class=\"deb-toggles\">\r\n @for (v of m.flagValidators; track v) {\r\n <mat-checkbox\r\n [ngModel]=\"store.hasFlagValidator(f, v)\"\r\n (ngModelChange)=\"store.toggleFlagValidator(f.id, v, $event)\"\r\n >{{ v }}</mat-checkbox>\r\n }\r\n </div>\r\n @for (v of m.paramValidators; track v) {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>{{ v }}</mat-label>\r\n <input\r\n matInput\r\n type=\"number\"\r\n [ngModel]=\"store.getParamValidator(f, v)\"\r\n (ngModelChange)=\"store.setParamValidator(f.id, v, toNum($event))\"\r\n />\r\n </mat-form-field>\r\n }\r\n }\r\n }\r\n\r\n <!-- Options (dropdown / radio / multiSelect) -->\r\n @if (meta()?.hasOptions) {\r\n <mat-divider></mat-divider>\r\n <span class=\"deb-section-title\">Data source</span>\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Options come from</mat-label>\r\n <mat-select\r\n data-testid=\"data-source\"\r\n [ngModel]=\"store.fieldDataSource(f)\"\r\n (ngModelChange)=\"store.setFieldDataSource(f.id, $event)\"\r\n >\r\n <mat-option value=\"none\">Nothing yet</mat-option>\r\n <mat-option value=\"manual\">Options authored here</mat-option>\r\n <mat-option value=\"lookup\">A named list</mat-option>\r\n </mat-select>\r\n </mat-form-field>\r\n }\r\n\r\n <!-- Named lookup list -->\r\n @if (meta()?.hasOptions && store.fieldDataSource(f) === 'lookup') {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>List name</mat-label>\r\n <input\r\n matInput\r\n data-testid=\"list-name\"\r\n [ngModel]=\"f.listName ?? ''\"\r\n (ngModelChange)=\"store.setListName(f.id, $event)\"\r\n />\r\n </mat-form-field>\r\n <p class=\"deb-hint\">\r\n The consuming app resolves this name through its lookup registry. Options are not\r\n authored here.\r\n </p>\r\n }\r\n\r\n <!-- Inline options -->\r\n @if (meta()?.hasOptions && store.fieldDataSource(f) !== 'lookup') {\r\n <div class=\"deb-row\" style=\"justify-content: space-between;\">\r\n <span class=\"deb-section-title\">Options</span>\r\n <button mat-stroked-button type=\"button\" (click)=\"store.addOption(f.id)\">\r\n <mat-icon>add</mat-icon> Option\r\n </button>\r\n </div>\r\n @for (opt of f.options ?? []; track $index) {\r\n <div class=\"deb-option-row\" data-testid=\"option-row\">\r\n <!-- One input: the displayed text is the stored value. -->\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Option ({{ lang() }})</mat-label>\r\n <input\r\n matInput\r\n [attr.data-testid]=\"'option-' + $index\"\r\n [ngModel]=\"optionLabel(opt)\"\r\n (ngModelChange)=\"store.setOptionLabel(f.id, $index, lang(), $event)\"\r\n />\r\n </mat-form-field>\r\n <button mat-icon-button type=\"button\" color=\"warn\"\r\n (click)=\"store.removeOption(f.id, $index)\" matTooltip=\"Remove option\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n @if (!(f.options ?? []).length) {\r\n <p class=\"deb-hint\">No options yet \u2014 add at least one.</p>\r\n }\r\n }\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Flags -->\r\n <span class=\"deb-section-title\">Display</span>\r\n <div class=\"deb-toggles\">\r\n <mat-slide-toggle\r\n [ngModel]=\"f.visibility ?? true\"\r\n (ngModelChange)=\"store.updateField(f.id, { visibility: $event })\"\r\n >Visible</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.table?.visible ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { table: { visible: $event } })\"\r\n >Table column</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.readonly ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { readonly: $event })\"\r\n >Read-only</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.disabled ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { disabled: $event })\"\r\n >Disabled</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.maskData ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { maskData: $event })\"\r\n >Mask data (RBAC)</mat-slide-toggle>\r\n <mat-slide-toggle\r\n data-testid=\"toggle-critical\"\r\n [ngModel]=\"f.criticalField ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { criticalField: $event })\"\r\n matTooltip=\"Renders locked; editing it announces a change against the session baseline.\"\r\n >Critical field</mat-slide-toggle>\r\n <mat-slide-toggle\r\n [ngModel]=\"f.showOnMinimize ?? false\"\r\n (ngModelChange)=\"store.updateField(f.id, { showOnMinimize: $event })\"\r\n matTooltip=\"Shown in the record editor's summary panel.\"\r\n >Show in summary</mat-slide-toggle>\r\n </div>\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Conditional visibility (static showWhen) -->\r\n <div class=\"deb-row deb-row--split\">\r\n <span class=\"deb-section-title\">Show when</span>\r\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\r\n <mat-icon>add</mat-icon> Condition\r\n </button>\r\n </div>\r\n @for (entry of showWhenEntries(f); track entry.key) {\r\n <div class=\"deb-option-row\" data-testid=\"option-row\">\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>Field id</mat-label>\r\n <input matInput [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\" />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>Equals</mat-label>\r\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\r\n </mat-form-field>\r\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove condition\"\r\n (click)=\"removeShowWhen(f, entry.key)\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n @if (!showWhenEntries(f).length) {\r\n <p class=\"deb-hint\">Always visible. Add a condition to show this field only for certain values.</p>\r\n }\r\n\r\n <!-- patchOnTrue (boolean/checkbox only) -->\r\n @if (f.type === 'boolean' || f.type === 'checkbox') {\r\n <mat-divider></mat-divider>\r\n <div class=\"deb-row deb-row--split\">\r\n <span class=\"deb-section-title\">Copy on true</span>\r\n <button mat-stroked-button type=\"button\" data-testid=\"add-patch-on-true\"\r\n (click)=\"store.addPatchOnTrueMapping(f.id)\">\r\n <mat-icon>add</mat-icon> Mapping\r\n </button>\r\n </div>\r\n @for (mapping of f.patchOnTrue ?? []; track $index) {\r\n <div class=\"deb-option-row\" data-testid=\"option-row\">\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>From field</mat-label>\r\n <input matInput [ngModel]=\"mapping.from\"\r\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { from: $event })\" />\r\n </mat-form-field>\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\r\n <mat-label>To field</mat-label>\r\n <input matInput [ngModel]=\"mapping.to\"\r\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { to: $event })\" />\r\n </mat-form-field>\r\n <button mat-icon-button type=\"button\" color=\"warn\" matTooltip=\"Remove mapping\"\r\n (click)=\"store.removePatchOnTrueMapping(f.id, $index)\">\r\n <mat-icon>delete</mat-icon>\r\n </button>\r\n </div>\r\n }\r\n @if (!(f.patchOnTrue ?? []).length) {\r\n <p class=\"deb-hint\">When this flips to true, copy one field's value into another.</p>\r\n }\r\n }\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Rules attached to this field -->\r\n <ngx-field-rules-list />\r\n\r\n <mat-divider></mat-divider>\r\n\r\n <!-- Cross-entity field reference & drift management (Phase 8) -->\r\n <ngx-referenced-field-config />\r\n </div>\r\n} @else {\r\n <div class=\"deb-inspector__empty\">\r\n <mat-icon>touch_app</mat-icon>\r\n <p>Select a field to edit its properties.</p>\r\n </div>\r\n}\r\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"] }]
2211
+ ], 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 <!-- 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 [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>Field id</mat-label>\n <input matInput [ngModel]=\"entry.key\" (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\" />\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 <input matInput [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { from: $event })\" />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>To field</mat-label>\n <input matInput [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(f.id, $index, { to: $event })\" />\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"] }]
2071
2212
  }] });
2072
2213
 
2073
2214
  /**
@@ -2077,48 +2218,47 @@ class FieldPaletteComponent {
2077
2218
  catalog = FIELD_TYPE_CATALOG;
2078
2219
  pick = new EventEmitter();
2079
2220
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FieldPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2080
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FieldPaletteComponent, isStandalone: true, selector: "ngx-field-palette", outputs: { pick: "pick" }, ngImport: i0, template: `
2081
- <div class="deb-palette">
2082
- @for (meta of catalog; track meta.type) {
2083
- <button
2084
- mat-stroked-button
2085
- type="button"
2086
- class="deb-palette__item"
2087
- [attr.data-testid]="'palette-' + meta.type"
2088
- [matTooltip]="meta.description"
2089
- (click)="pick.emit(meta.type)"
2090
- >
2091
- <mat-icon>{{ meta.icon }}</mat-icon>
2092
- <span>{{ meta.label }}</span>
2093
- </button>
2094
- }
2095
- </div>
2221
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FieldPaletteComponent, isStandalone: true, selector: "ngx-field-palette", outputs: { pick: "pick" }, ngImport: i0, template: `
2222
+ <div class="deb-palette">
2223
+ @for (meta of catalog; track meta.type) {
2224
+ <button
2225
+ mat-stroked-button
2226
+ type="button"
2227
+ class="deb-palette__item"
2228
+ [attr.data-testid]="'palette-' + meta.type"
2229
+ [matTooltip]="meta.description"
2230
+ (click)="pick.emit(meta.type)"
2231
+ >
2232
+ <mat-icon>{{ meta.icon }}</mat-icon>
2233
+ <span>{{ meta.label }}</span>
2234
+ </button>
2235
+ }
2236
+ </div>
2096
2237
  `, isInline: true, styles: [".deb-palette{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-palette__item{justify-content:flex-start;overflow:hidden;border-radius:8px!important;border:1px solid var(--border, #e2e8f0)!important;padding:8px 12px!important;font-family:var(--font-heading, inherit)!important;font-weight:600!important;font-size:13px!important;transition:all .18s cubic-bezier(.16,1,.3,1)!important;background:#fff!important}.deb-palette__item:hover{border-color:#6366f1!important;background:#f5f3ff!important;color:#4f46e5!important;transform:translateY(-1px);box-shadow:0 4px 12px #6366f126!important}.deb-palette__item mat-icon{color:#6366f1;margin-right:6px;font-size:18px;width:18px;height:18px}.deb-palette__item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
2097
2238
  }
2098
2239
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FieldPaletteComponent, decorators: [{
2099
2240
  type: Component,
2100
- args: [{ selector: 'ngx-field-palette', standalone: true, imports: [MatButtonModule, MatIconModule, MatTooltipModule], template: `
2101
- <div class="deb-palette">
2102
- @for (meta of catalog; track meta.type) {
2103
- <button
2104
- mat-stroked-button
2105
- type="button"
2106
- class="deb-palette__item"
2107
- [attr.data-testid]="'palette-' + meta.type"
2108
- [matTooltip]="meta.description"
2109
- (click)="pick.emit(meta.type)"
2110
- >
2111
- <mat-icon>{{ meta.icon }}</mat-icon>
2112
- <span>{{ meta.label }}</span>
2113
- </button>
2114
- }
2115
- </div>
2241
+ args: [{ selector: 'ngx-field-palette', standalone: true, imports: [MatButtonModule, MatIconModule, MatTooltipModule], template: `
2242
+ <div class="deb-palette">
2243
+ @for (meta of catalog; track meta.type) {
2244
+ <button
2245
+ mat-stroked-button
2246
+ type="button"
2247
+ class="deb-palette__item"
2248
+ [attr.data-testid]="'palette-' + meta.type"
2249
+ [matTooltip]="meta.description"
2250
+ (click)="pick.emit(meta.type)"
2251
+ >
2252
+ <mat-icon>{{ meta.icon }}</mat-icon>
2253
+ <span>{{ meta.label }}</span>
2254
+ </button>
2255
+ }
2256
+ </div>
2116
2257
  `, styles: [".deb-palette{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-palette__item{justify-content:flex-start;overflow:hidden;border-radius:8px!important;border:1px solid var(--border, #e2e8f0)!important;padding:8px 12px!important;font-family:var(--font-heading, inherit)!important;font-weight:600!important;font-size:13px!important;transition:all .18s cubic-bezier(.16,1,.3,1)!important;background:#fff!important}.deb-palette__item:hover{border-color:#6366f1!important;background:#f5f3ff!important;color:#4f46e5!important;transform:translateY(-1px);box-shadow:0 4px 12px #6366f126!important}.deb-palette__item mat-icon{color:#6366f1;margin-right:6px;font-size:18px;width:18px;height:18px}.deb-palette__item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
2117
2258
  }], propDecorators: { pick: [{
2118
2259
  type: Output
2119
2260
  }] } });
2120
2261
 
2121
- const SYSTEM_DEFAULT_CAN_EDIT = new InjectionToken('SYSTEM_DEFAULT_CAN_EDIT');
2122
2262
  /**
2123
2263
  * TabManagerComponent — add / rename / reorder / remove tabs & sub-tabs with systemDefault protection.
2124
2264
  */
@@ -2135,153 +2275,156 @@ class TabManagerComponent {
2135
2275
  if (!tab.systemDefault)
2136
2276
  return true;
2137
2277
  if (this.canEditSystemDefaultsFn) {
2138
- return this.canEditSystemDefaultsFn([]);
2278
+ // The predicate's entire contract is (roles) => boolean. This passed a hardcoded []
2279
+ // regardless of who was using the builder, so any predicate that actually inspected
2280
+ // roles answered false for everyone and locked every system-default tab.
2281
+ return this.canEditSystemDefaultsFn(this.store.userRoles());
2139
2282
  }
2140
2283
  return true;
2141
2284
  }
2142
2285
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TabManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2143
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
2144
- <div class="deb-tabs">
2145
- <div class="deb-tabs__head">
2146
- <span class="deb-section-title">Tabs Manager</span>
2147
- <button mat-stroked-button type="button" (click)="store.addTab()">
2148
- <mat-icon>add</mat-icon> Add Tab
2149
- </button>
2150
- </div>
2151
-
2152
- @if (store.tabs().length === 0) {
2153
- <p class="deb-hint">No tabs — all fields render in a single section.</p>
2154
- }
2155
-
2156
- @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
2157
- <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
2158
- <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
2159
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2160
- <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
2161
- <input
2162
- matInput
2163
- [disabled]="!canEditTab(tab)"
2164
- [ngModel]="tabLabel(tab)"
2165
- (ngModelChange)="store.setTabLabel(tab.id, lang(), $event)"
2166
- />
2167
- </mat-form-field>
2168
- <button
2169
- mat-icon-button
2170
- type="button"
2171
- [disabled]="i === 0 || !canEditTab(tab)"
2172
- (click)="store.moveTab(tab.id, -1)"
2173
- matTooltip="Move up"
2174
- >
2175
- <mat-icon>arrow_upward</mat-icon>
2176
- </button>
2177
- <button
2178
- mat-icon-button
2179
- type="button"
2180
- [disabled]="i === count - 1 || !canEditTab(tab)"
2181
- (click)="store.moveTab(tab.id, 1)"
2182
- matTooltip="Move down"
2183
- >
2184
- <mat-icon>arrow_downward</mat-icon>
2185
- </button>
2186
- <button
2187
- mat-icon-button
2188
- type="button"
2189
- color="warn"
2190
- [disabled]="!canEditTab(tab)"
2191
- (click)="store.removeTab(tab.id)"
2192
- matTooltip="Remove tab"
2193
- >
2194
- <mat-icon>delete</mat-icon>
2195
- </button>
2196
- </div>
2197
-
2198
- <!-- Tab settings row -->
2199
- <div class="deb-tab-settings">
2200
- <mat-checkbox
2201
- [disabled]="!canEditTab(tab)"
2202
- [ngModel]="tab.flatData"
2203
- (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
2204
- >
2205
- Flat Data
2206
- </mat-checkbox>
2207
- <mat-checkbox
2208
- [disabled]="!canEditTab(tab)"
2209
- [ngModel]="tab.isPrimaryTab"
2210
- (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
2211
- >
2212
- Primary Tab
2213
- </mat-checkbox>
2214
- <mat-checkbox
2215
- [disabled]="!canEditTab(tab)"
2216
- [ngModel]="tab.maskData"
2217
- (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
2218
- >
2219
- Mask Tab Data
2220
- </mat-checkbox>
2221
- <mat-checkbox
2222
- [disabled]="!canEditTab(tab)"
2223
- [ngModel]="tab.systemDefault"
2224
- (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
2225
- >
2226
- System Default
2227
- </mat-checkbox>
2228
- <button
2229
- mat-button
2230
- type="button"
2231
- color="primary"
2232
- [disabled]="!canEditTab(tab)"
2233
- (click)="store.addSubTab(tab.id)"
2234
- >
2235
- + Sub-tab
2236
- </button>
2237
- </div>
2238
-
2239
- <!-- Module tab configuration -->
2240
- <div class="deb-tab-module">
2241
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2242
- <mat-label>Consumer Module Name (Optional)</mat-label>
2243
- <input
2244
- matInput
2245
- placeholder="e.g. documents-view"
2246
- [disabled]="!canEditTab(tab)"
2247
- [ngModel]="tab.moduleName"
2248
- (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
2249
- />
2250
- </mat-form-field>
2251
- </div>
2252
-
2253
- <!-- Sub-tabs recursive render (Level 2) -->
2254
- @if (tab.children && tab.children.length > 0) {
2255
- <div class="deb-subtabs">
2256
- <span class="deb-subtabs__title">Sub-tabs:</span>
2257
- @for (sub of tab.children; track sub.id) {
2258
- <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
2259
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2260
- <mat-label>{{ sub.id }}</mat-label>
2261
- <input
2262
- matInput
2263
- [disabled]="!canEditTab(sub)"
2264
- [ngModel]="tabLabel(sub)"
2265
- (ngModelChange)="store.setTabLabel(sub.id, lang(), $event)"
2266
- />
2267
- </mat-form-field>
2268
- <button
2269
- mat-icon-button
2270
- type="button"
2271
- color="warn"
2272
- [disabled]="!canEditTab(sub)"
2273
- (click)="store.removeTab(sub.id)"
2274
- matTooltip="Remove sub-tab"
2275
- >
2276
- <mat-icon>delete</mat-icon>
2277
- </button>
2278
- </div>
2279
- }
2280
- </div>
2281
- }
2282
- </div>
2283
- }
2284
- </div>
2286
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
2287
+ <div class="deb-tabs">
2288
+ <div class="deb-tabs__head">
2289
+ <span class="deb-section-title">Tabs Manager</span>
2290
+ <button mat-stroked-button type="button" (click)="store.addTab()">
2291
+ <mat-icon>add</mat-icon> Add Tab
2292
+ </button>
2293
+ </div>
2294
+
2295
+ @if (store.tabs().length === 0) {
2296
+ <p class="deb-hint">No tabs — all fields render in a single section.</p>
2297
+ }
2298
+
2299
+ @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
2300
+ <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
2301
+ <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
2302
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2303
+ <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
2304
+ <input
2305
+ matInput
2306
+ [disabled]="!canEditTab(tab)"
2307
+ [ngModel]="tabLabel(tab)"
2308
+ (ngModelChange)="store.setTabLabel(tab.id, lang(), $event)"
2309
+ />
2310
+ </mat-form-field>
2311
+ <button
2312
+ mat-icon-button
2313
+ type="button"
2314
+ [disabled]="i === 0 || !canEditTab(tab)"
2315
+ (click)="store.moveTab(tab.id, -1)"
2316
+ matTooltip="Move up" aria-label="Move tab up"
2317
+ >
2318
+ <mat-icon>arrow_upward</mat-icon>
2319
+ </button>
2320
+ <button
2321
+ mat-icon-button
2322
+ type="button"
2323
+ [disabled]="i === count - 1 || !canEditTab(tab)"
2324
+ (click)="store.moveTab(tab.id, 1)"
2325
+ matTooltip="Move down" aria-label="Move tab down"
2326
+ >
2327
+ <mat-icon>arrow_downward</mat-icon>
2328
+ </button>
2329
+ <button
2330
+ mat-icon-button
2331
+ type="button"
2332
+ color="warn"
2333
+ [disabled]="!canEditTab(tab)"
2334
+ (click)="store.removeTab(tab.id)"
2335
+ matTooltip="Remove tab" aria-label="Remove tab"
2336
+ >
2337
+ <mat-icon>delete</mat-icon>
2338
+ </button>
2339
+ </div>
2340
+
2341
+ <!-- Tab settings row -->
2342
+ <div class="deb-tab-settings">
2343
+ <mat-checkbox
2344
+ [disabled]="!canEditTab(tab)"
2345
+ [ngModel]="tab.flatData"
2346
+ (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
2347
+ >
2348
+ Flat Data
2349
+ </mat-checkbox>
2350
+ <mat-checkbox
2351
+ [disabled]="!canEditTab(tab)"
2352
+ [ngModel]="tab.isPrimaryTab"
2353
+ (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
2354
+ >
2355
+ Primary Tab
2356
+ </mat-checkbox>
2357
+ <mat-checkbox
2358
+ [disabled]="!canEditTab(tab)"
2359
+ [ngModel]="tab.maskData"
2360
+ (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
2361
+ >
2362
+ Mask Tab Data
2363
+ </mat-checkbox>
2364
+ <mat-checkbox
2365
+ [disabled]="!canEditTab(tab)"
2366
+ [ngModel]="tab.systemDefault"
2367
+ (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
2368
+ >
2369
+ System Default
2370
+ </mat-checkbox>
2371
+ <button
2372
+ mat-button
2373
+ type="button"
2374
+ color="primary"
2375
+ [disabled]="!canEditTab(tab)"
2376
+ (click)="store.addSubTab(tab.id)"
2377
+ >
2378
+ + Sub-tab
2379
+ </button>
2380
+ </div>
2381
+
2382
+ <!-- Module tab configuration -->
2383
+ <div class="deb-tab-module">
2384
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2385
+ <mat-label>Consumer Module Name (Optional)</mat-label>
2386
+ <input
2387
+ matInput
2388
+ placeholder="e.g. documents-view"
2389
+ [disabled]="!canEditTab(tab)"
2390
+ [ngModel]="tab.moduleName"
2391
+ (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
2392
+ />
2393
+ </mat-form-field>
2394
+ </div>
2395
+
2396
+ <!-- Sub-tabs recursive render (Level 2) -->
2397
+ @if (tab.children && tab.children.length > 0) {
2398
+ <div class="deb-subtabs">
2399
+ <span class="deb-subtabs__title">Sub-tabs:</span>
2400
+ @for (sub of tab.children; track sub.id) {
2401
+ <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
2402
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2403
+ <mat-label>{{ sub.id }}</mat-label>
2404
+ <input
2405
+ matInput
2406
+ [disabled]="!canEditTab(sub)"
2407
+ [ngModel]="tabLabel(sub)"
2408
+ (ngModelChange)="store.setTabLabel(sub.id, lang(), $event)"
2409
+ />
2410
+ </mat-form-field>
2411
+ <button
2412
+ mat-icon-button
2413
+ type="button"
2414
+ color="warn"
2415
+ [disabled]="!canEditTab(sub)"
2416
+ (click)="store.removeTab(sub.id)"
2417
+ matTooltip="Remove sub-tab" aria-label="Remove sub-tab"
2418
+ >
2419
+ <mat-icon>delete</mat-icon>
2420
+ </button>
2421
+ </div>
2422
+ }
2423
+ </div>
2424
+ }
2425
+ </div>
2426
+ }
2427
+ </div>
2285
2428
  `, isInline: true, styles: [".deb-tabs__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.deb-tab-card{border:1px solid var(--deb-border, #e5e7eb);border-radius:8px;padding:10px;margin-bottom:10px;background:var(--deb-bg-surface, #ffffff)}.deb-tab-card--system{border-left:4px solid var(--deb-accent, #6366f1)}.deb-tabs__row{display:flex;align-items:center;gap:4px}.deb-tab-settings{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;margin-top:8px;font-size:13px}.deb-tab-module{margin-top:8px}.deb-subtabs{margin-top:8px;padding-left:16px;border-left:2px dashed var(--deb-border, #e5e7eb)}.deb-subtabs__title{font-size:12px;font-weight:600;color:var(--deb-muted, #6b7280)}.deb-subtab-row{margin-top:6px}.deb-grow{flex:1}\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.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[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { 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: "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"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
2286
2429
  }
2287
2430
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TabManagerComponent, decorators: [{
@@ -2295,148 +2438,148 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2295
2438
  MatInputModule,
2296
2439
  MatSlideToggleModule,
2297
2440
  MatTooltipModule,
2298
- ], template: `
2299
- <div class="deb-tabs">
2300
- <div class="deb-tabs__head">
2301
- <span class="deb-section-title">Tabs Manager</span>
2302
- <button mat-stroked-button type="button" (click)="store.addTab()">
2303
- <mat-icon>add</mat-icon> Add Tab
2304
- </button>
2305
- </div>
2306
-
2307
- @if (store.tabs().length === 0) {
2308
- <p class="deb-hint">No tabs — all fields render in a single section.</p>
2309
- }
2310
-
2311
- @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
2312
- <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
2313
- <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
2314
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2315
- <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
2316
- <input
2317
- matInput
2318
- [disabled]="!canEditTab(tab)"
2319
- [ngModel]="tabLabel(tab)"
2320
- (ngModelChange)="store.setTabLabel(tab.id, lang(), $event)"
2321
- />
2322
- </mat-form-field>
2323
- <button
2324
- mat-icon-button
2325
- type="button"
2326
- [disabled]="i === 0 || !canEditTab(tab)"
2327
- (click)="store.moveTab(tab.id, -1)"
2328
- matTooltip="Move up"
2329
- >
2330
- <mat-icon>arrow_upward</mat-icon>
2331
- </button>
2332
- <button
2333
- mat-icon-button
2334
- type="button"
2335
- [disabled]="i === count - 1 || !canEditTab(tab)"
2336
- (click)="store.moveTab(tab.id, 1)"
2337
- matTooltip="Move down"
2338
- >
2339
- <mat-icon>arrow_downward</mat-icon>
2340
- </button>
2341
- <button
2342
- mat-icon-button
2343
- type="button"
2344
- color="warn"
2345
- [disabled]="!canEditTab(tab)"
2346
- (click)="store.removeTab(tab.id)"
2347
- matTooltip="Remove tab"
2348
- >
2349
- <mat-icon>delete</mat-icon>
2350
- </button>
2351
- </div>
2352
-
2353
- <!-- Tab settings row -->
2354
- <div class="deb-tab-settings">
2355
- <mat-checkbox
2356
- [disabled]="!canEditTab(tab)"
2357
- [ngModel]="tab.flatData"
2358
- (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
2359
- >
2360
- Flat Data
2361
- </mat-checkbox>
2362
- <mat-checkbox
2363
- [disabled]="!canEditTab(tab)"
2364
- [ngModel]="tab.isPrimaryTab"
2365
- (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
2366
- >
2367
- Primary Tab
2368
- </mat-checkbox>
2369
- <mat-checkbox
2370
- [disabled]="!canEditTab(tab)"
2371
- [ngModel]="tab.maskData"
2372
- (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
2373
- >
2374
- Mask Tab Data
2375
- </mat-checkbox>
2376
- <mat-checkbox
2377
- [disabled]="!canEditTab(tab)"
2378
- [ngModel]="tab.systemDefault"
2379
- (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
2380
- >
2381
- System Default
2382
- </mat-checkbox>
2383
- <button
2384
- mat-button
2385
- type="button"
2386
- color="primary"
2387
- [disabled]="!canEditTab(tab)"
2388
- (click)="store.addSubTab(tab.id)"
2389
- >
2390
- + Sub-tab
2391
- </button>
2392
- </div>
2393
-
2394
- <!-- Module tab configuration -->
2395
- <div class="deb-tab-module">
2396
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2397
- <mat-label>Consumer Module Name (Optional)</mat-label>
2398
- <input
2399
- matInput
2400
- placeholder="e.g. documents-view"
2401
- [disabled]="!canEditTab(tab)"
2402
- [ngModel]="tab.moduleName"
2403
- (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
2404
- />
2405
- </mat-form-field>
2406
- </div>
2407
-
2408
- <!-- Sub-tabs recursive render (Level 2) -->
2409
- @if (tab.children && tab.children.length > 0) {
2410
- <div class="deb-subtabs">
2411
- <span class="deb-subtabs__title">Sub-tabs:</span>
2412
- @for (sub of tab.children; track sub.id) {
2413
- <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
2414
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2415
- <mat-label>{{ sub.id }}</mat-label>
2416
- <input
2417
- matInput
2418
- [disabled]="!canEditTab(sub)"
2419
- [ngModel]="tabLabel(sub)"
2420
- (ngModelChange)="store.setTabLabel(sub.id, lang(), $event)"
2421
- />
2422
- </mat-form-field>
2423
- <button
2424
- mat-icon-button
2425
- type="button"
2426
- color="warn"
2427
- [disabled]="!canEditTab(sub)"
2428
- (click)="store.removeTab(sub.id)"
2429
- matTooltip="Remove sub-tab"
2430
- >
2431
- <mat-icon>delete</mat-icon>
2432
- </button>
2433
- </div>
2434
- }
2435
- </div>
2436
- }
2437
- </div>
2438
- }
2439
- </div>
2441
+ ], template: `
2442
+ <div class="deb-tabs">
2443
+ <div class="deb-tabs__head">
2444
+ <span class="deb-section-title">Tabs Manager</span>
2445
+ <button mat-stroked-button type="button" (click)="store.addTab()">
2446
+ <mat-icon>add</mat-icon> Add Tab
2447
+ </button>
2448
+ </div>
2449
+
2450
+ @if (store.tabs().length === 0) {
2451
+ <p class="deb-hint">No tabs — all fields render in a single section.</p>
2452
+ }
2453
+
2454
+ @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
2455
+ <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
2456
+ <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
2457
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2458
+ <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
2459
+ <input
2460
+ matInput
2461
+ [disabled]="!canEditTab(tab)"
2462
+ [ngModel]="tabLabel(tab)"
2463
+ (ngModelChange)="store.setTabLabel(tab.id, lang(), $event)"
2464
+ />
2465
+ </mat-form-field>
2466
+ <button
2467
+ mat-icon-button
2468
+ type="button"
2469
+ [disabled]="i === 0 || !canEditTab(tab)"
2470
+ (click)="store.moveTab(tab.id, -1)"
2471
+ matTooltip="Move up" aria-label="Move tab up"
2472
+ >
2473
+ <mat-icon>arrow_upward</mat-icon>
2474
+ </button>
2475
+ <button
2476
+ mat-icon-button
2477
+ type="button"
2478
+ [disabled]="i === count - 1 || !canEditTab(tab)"
2479
+ (click)="store.moveTab(tab.id, 1)"
2480
+ matTooltip="Move down" aria-label="Move tab down"
2481
+ >
2482
+ <mat-icon>arrow_downward</mat-icon>
2483
+ </button>
2484
+ <button
2485
+ mat-icon-button
2486
+ type="button"
2487
+ color="warn"
2488
+ [disabled]="!canEditTab(tab)"
2489
+ (click)="store.removeTab(tab.id)"
2490
+ matTooltip="Remove tab" aria-label="Remove tab"
2491
+ >
2492
+ <mat-icon>delete</mat-icon>
2493
+ </button>
2494
+ </div>
2495
+
2496
+ <!-- Tab settings row -->
2497
+ <div class="deb-tab-settings">
2498
+ <mat-checkbox
2499
+ [disabled]="!canEditTab(tab)"
2500
+ [ngModel]="tab.flatData"
2501
+ (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
2502
+ >
2503
+ Flat Data
2504
+ </mat-checkbox>
2505
+ <mat-checkbox
2506
+ [disabled]="!canEditTab(tab)"
2507
+ [ngModel]="tab.isPrimaryTab"
2508
+ (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
2509
+ >
2510
+ Primary Tab
2511
+ </mat-checkbox>
2512
+ <mat-checkbox
2513
+ [disabled]="!canEditTab(tab)"
2514
+ [ngModel]="tab.maskData"
2515
+ (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
2516
+ >
2517
+ Mask Tab Data
2518
+ </mat-checkbox>
2519
+ <mat-checkbox
2520
+ [disabled]="!canEditTab(tab)"
2521
+ [ngModel]="tab.systemDefault"
2522
+ (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
2523
+ >
2524
+ System Default
2525
+ </mat-checkbox>
2526
+ <button
2527
+ mat-button
2528
+ type="button"
2529
+ color="primary"
2530
+ [disabled]="!canEditTab(tab)"
2531
+ (click)="store.addSubTab(tab.id)"
2532
+ >
2533
+ + Sub-tab
2534
+ </button>
2535
+ </div>
2536
+
2537
+ <!-- Module tab configuration -->
2538
+ <div class="deb-tab-module">
2539
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2540
+ <mat-label>Consumer Module Name (Optional)</mat-label>
2541
+ <input
2542
+ matInput
2543
+ placeholder="e.g. documents-view"
2544
+ [disabled]="!canEditTab(tab)"
2545
+ [ngModel]="tab.moduleName"
2546
+ (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
2547
+ />
2548
+ </mat-form-field>
2549
+ </div>
2550
+
2551
+ <!-- Sub-tabs recursive render (Level 2) -->
2552
+ @if (tab.children && tab.children.length > 0) {
2553
+ <div class="deb-subtabs">
2554
+ <span class="deb-subtabs__title">Sub-tabs:</span>
2555
+ @for (sub of tab.children; track sub.id) {
2556
+ <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
2557
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2558
+ <mat-label>{{ sub.id }}</mat-label>
2559
+ <input
2560
+ matInput
2561
+ [disabled]="!canEditTab(sub)"
2562
+ [ngModel]="tabLabel(sub)"
2563
+ (ngModelChange)="store.setTabLabel(sub.id, lang(), $event)"
2564
+ />
2565
+ </mat-form-field>
2566
+ <button
2567
+ mat-icon-button
2568
+ type="button"
2569
+ color="warn"
2570
+ [disabled]="!canEditTab(sub)"
2571
+ (click)="store.removeTab(sub.id)"
2572
+ matTooltip="Remove sub-tab" aria-label="Remove sub-tab"
2573
+ >
2574
+ <mat-icon>delete</mat-icon>
2575
+ </button>
2576
+ </div>
2577
+ }
2578
+ </div>
2579
+ }
2580
+ </div>
2581
+ }
2582
+ </div>
2440
2583
  `, styles: [".deb-tabs__head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.deb-tab-card{border:1px solid var(--deb-border, #e5e7eb);border-radius:8px;padding:10px;margin-bottom:10px;background:var(--deb-bg-surface, #ffffff)}.deb-tab-card--system{border-left:4px solid var(--deb-accent, #6366f1)}.deb-tabs__row{display:flex;align-items:center;gap:4px}.deb-tab-settings{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;margin-top:8px;font-size:13px}.deb-tab-module{margin-top:8px}.deb-subtabs{margin-top:8px;padding-left:16px;border-left:2px dashed var(--deb-border, #e5e7eb)}.deb-subtabs__title{font-size:12px;font-weight:600;color:var(--deb-muted, #6b7280)}.deb-subtab-row{margin-top:6px}.deb-grow{flex:1}\n"] }]
2441
2584
  }] });
2442
2585
 
@@ -2463,8 +2606,14 @@ class EntityBuilderTreeNodeComponent {
2463
2606
  data-testid="builder-field-row"
2464
2607
  [class.deb-field-row--active]="store.selectedFieldId() === field.id"
2465
2608
  (click)="store.selectField(field.id)"
2609
+ (keydown.enter)="store.selectField(field.id)"
2610
+ (keydown.space)="store.selectField(field.id); $event.preventDefault()"
2611
+ role="button"
2612
+ tabindex="0"
2613
+ [attr.aria-pressed]="store.selectedFieldId() === field.id"
2614
+ [attr.aria-label]="'Select field ' + fieldLabel(field)"
2466
2615
  >
2467
- <mat-icon class="deb-drag-handle" cdkDragHandle title="Drag to reorder">drag_indicator</mat-icon>
2616
+ <mat-icon class="deb-drag-handle" cdkDragHandle aria-hidden="true" title="Drag to reorder (or use the move buttons)">drag_indicator</mat-icon>
2468
2617
  <mat-icon class="deb-field-type" [matTooltip]="fieldTypeLabel(field.type)">{{ fieldTypeIcon(field.type) }}</mat-icon>
2469
2618
  <div class="deb-field-meta">
2470
2619
  <div class="deb-row" style="gap:6px">
@@ -2483,16 +2632,19 @@ class EntityBuilderTreeNodeComponent {
2483
2632
 
2484
2633
  <button mat-icon-button type="button" [disabled]="index === 0"
2485
2634
  [attr.data-testid]="'row-up-' + field.id"
2635
+ [attr.aria-label]="'Move ' + fieldLabel(field) + ' up'"
2486
2636
  (click)="store.moveField(field.id, -1); $event.stopPropagation()" matTooltip="Move up">
2487
2637
  <mat-icon>arrow_upward</mat-icon>
2488
2638
  </button>
2489
2639
  <button mat-icon-button type="button" [disabled]="index === totalCount - 1"
2490
2640
  [attr.data-testid]="'row-down-' + field.id"
2641
+ [attr.aria-label]="'Move ' + fieldLabel(field) + ' down'"
2491
2642
  (click)="store.moveField(field.id, 1); $event.stopPropagation()" matTooltip="Move down">
2492
2643
  <mat-icon>arrow_downward</mat-icon>
2493
2644
  </button>
2494
2645
  <button mat-icon-button type="button"
2495
2646
  [attr.data-testid]="'row-duplicate-' + field.id"
2647
+ [attr.aria-label]="'Duplicate ' + fieldLabel(field)"
2496
2648
  (click)="store.duplicateField(field.id); $event.stopPropagation()" matTooltip="Duplicate">
2497
2649
  <mat-icon>content_copy</mat-icon>
2498
2650
  </button>
@@ -2535,8 +2687,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2535
2687
  data-testid="builder-field-row"
2536
2688
  [class.deb-field-row--active]="store.selectedFieldId() === field.id"
2537
2689
  (click)="store.selectField(field.id)"
2690
+ (keydown.enter)="store.selectField(field.id)"
2691
+ (keydown.space)="store.selectField(field.id); $event.preventDefault()"
2692
+ role="button"
2693
+ tabindex="0"
2694
+ [attr.aria-pressed]="store.selectedFieldId() === field.id"
2695
+ [attr.aria-label]="'Select field ' + fieldLabel(field)"
2538
2696
  >
2539
- <mat-icon class="deb-drag-handle" cdkDragHandle title="Drag to reorder">drag_indicator</mat-icon>
2697
+ <mat-icon class="deb-drag-handle" cdkDragHandle aria-hidden="true" title="Drag to reorder (or use the move buttons)">drag_indicator</mat-icon>
2540
2698
  <mat-icon class="deb-field-type" [matTooltip]="fieldTypeLabel(field.type)">{{ fieldTypeIcon(field.type) }}</mat-icon>
2541
2699
  <div class="deb-field-meta">
2542
2700
  <div class="deb-row" style="gap:6px">
@@ -2555,16 +2713,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2555
2713
 
2556
2714
  <button mat-icon-button type="button" [disabled]="index === 0"
2557
2715
  [attr.data-testid]="'row-up-' + field.id"
2716
+ [attr.aria-label]="'Move ' + fieldLabel(field) + ' up'"
2558
2717
  (click)="store.moveField(field.id, -1); $event.stopPropagation()" matTooltip="Move up">
2559
2718
  <mat-icon>arrow_upward</mat-icon>
2560
2719
  </button>
2561
2720
  <button mat-icon-button type="button" [disabled]="index === totalCount - 1"
2562
2721
  [attr.data-testid]="'row-down-' + field.id"
2722
+ [attr.aria-label]="'Move ' + fieldLabel(field) + ' down'"
2563
2723
  (click)="store.moveField(field.id, 1); $event.stopPropagation()" matTooltip="Move down">
2564
2724
  <mat-icon>arrow_downward</mat-icon>
2565
2725
  </button>
2566
2726
  <button mat-icon-button type="button"
2567
2727
  [attr.data-testid]="'row-duplicate-' + field.id"
2728
+ [attr.aria-label]="'Duplicate ' + fieldLabel(field)"
2568
2729
  (click)="store.duplicateField(field.id); $event.stopPropagation()" matTooltip="Duplicate">
2569
2730
  <mat-icon>content_copy</mat-icon>
2570
2731
  </button>
@@ -2680,12 +2841,23 @@ const EMPTY_ROLES = Object.freeze([]);
2680
2841
  */
2681
2842
  class EntityBuilderComponent {
2682
2843
  store = inject(BuilderStore);
2844
+ /**
2845
+ * Optional: only apps that registered a CONFIG_SOURCE have one. Used to drop the cached
2846
+ * copy of a config the moment it is edited — see `doSave`.
2847
+ */
2848
+ configSource = inject(ConfigSourceService, { optional: true });
2683
2849
  /** Existing config to edit. When omitted, the builder starts blank. */
2684
2850
  config;
2685
2851
  /** Languages available for label/placeholder editing. First entry is the default. */
2686
2852
  languages = ['en'];
2687
2853
  /** Optional role list — enables multi-select role pickers for RBAC instead of free text. */
2688
2854
  availableRoles = [];
2855
+ /**
2856
+ * Roles of the person using the builder. Distinct from `availableRoles`, which is the
2857
+ * vocabulary a schema may reference. Passed to the `SYSTEM_DEFAULT_CAN_EDIT` predicate so
2858
+ * it can decide whether this user may edit system-default tabs.
2859
+ */
2860
+ userRoles = [];
2689
2861
  /** Optional common-module definitions for module tabs; defaults to the built-in list from core. */
2690
2862
  commonModules = COMMON_MODULES;
2691
2863
  /** Emitted on every change to the working config. */
@@ -2708,6 +2880,9 @@ class EntityBuilderComponent {
2708
2880
  if (!this.languages.includes(active))
2709
2881
  this.store.setActiveLanguage(this.languages[0]);
2710
2882
  }
2883
+ if (changes['userRoles']) {
2884
+ this.store.setUserRoles(this.userRoles);
2885
+ }
2711
2886
  }
2712
2887
  // ─── Field canvas ─────────────────────────────────────────────────────────
2713
2888
  onDrop(event) {
@@ -2725,7 +2900,13 @@ class EntityBuilderComponent {
2725
2900
  }
2726
2901
  // ─── Toolbar actions ──────────────────────────────────────────────────────
2727
2902
  doSave() {
2728
- this.save.emit(this.store.exportConfig());
2903
+ const config = this.store.exportConfig();
2904
+ // Saving is the moment this entity's config changes, so anything holding a cached copy is
2905
+ // now stale — most visibly a referenced field elsewhere resolving against the old schema.
2906
+ // ConfigSourceService caches per entity and exposed clearCache from the start, but nothing
2907
+ // ever called it, so a cache entry lived for the lifetime of the page.
2908
+ this.configSource?.clearCache(config.entity);
2909
+ this.save.emit(config);
2729
2910
  }
2730
2911
  get json() {
2731
2912
  return JSON.stringify(this.store.exportConfig(), null, 2);
@@ -2753,7 +2934,7 @@ class EntityBuilderComponent {
2753
2934
  this.store.setPermission(action, roles);
2754
2935
  }
2755
2936
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2756
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EntityBuilderComponent, isStandalone: true, selector: "ngx-entity-builder", inputs: { config: "config", languages: "languages", availableRoles: "availableRoles", commonModules: "commonModules" }, outputs: { configChange: "configChange", save: "save" }, providers: [BuilderStore], usesOnChanges: true, ngImport: i0, template: "<mat-toolbar class=\"deb-toolbar\" color=\"primary\">\r\n <mat-icon>dashboard_customize</mat-icon>\r\n <span class=\"deb-toolbar__title\">Entity Builder</span>\r\n <span class=\"deb-spacer\"></span>\r\n\r\n @if (store.problems().length) {\r\n <button\r\n mat-button\r\n type=\"button\"\r\n class=\"deb-problems\"\r\n [class.deb-problems--error]=\"!store.isValid()\"\r\n [matTooltip]=\"store.problems().length + ' issue(s) \u2014 hover items in the list'\"\r\n >\r\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\r\n {{ store.errors().length }} / {{ store.problems().length }}\r\n </button>\r\n }\r\n\r\n <button mat-button type=\"button\" (click)=\"copyJson()\">\r\n <mat-icon>content_copy</mat-icon> Copy JSON\r\n </button>\r\n <button mat-flat-button color=\"accent\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\r\n <mat-icon>save</mat-icon> Save\r\n </button>\r\n</mat-toolbar>\r\n\r\n<div class=\"deb-body\">\r\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\r\n <div class=\"deb-col deb-col--left\">\r\n <mat-card>\r\n <mat-card-content>\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Entity name</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"store.config().entity\"\r\n (ngModelChange)=\"store.setEntityName($event)\"\r\n placeholder=\"e.g. clients\"\r\n />\r\n </mat-form-field>\r\n\r\n <div class=\"deb-row\">\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-grow\">\r\n <mat-label>Editing language</mat-label>\r\n <mat-select\r\n [ngModel]=\"store.activeLanguage()\"\r\n (ngModelChange)=\"store.setActiveLanguage($event)\"\r\n >\r\n @for (l of languages; track l) {\r\n <mat-option [value]=\"l\">{{ l }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <mat-slide-toggle\r\n [ngModel]=\"store.config().maskData ?? false\"\r\n (ngModelChange)=\"store.setMaskData($event)\"\r\n >Mask this entity (RBAC)</mat-slide-toggle>\r\n\r\n <mat-expansion-panel class=\"deb-rbac\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>Permissions</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n @for (action of rbacActions; track action) {\r\n @if (availableRoles.length) {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>{{ action }} roles</mat-label>\r\n <mat-select multiple [ngModel]=\"rolesFor(action)\" (ngModelChange)=\"setRoles(action, $event)\">\r\n @for (role of availableRoles; track role) {\r\n <mat-option [value]=\"role\">{{ role }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n } @else {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>{{ action }} roles (comma-separated)</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"rolesFor(action).join(', ')\"\r\n (ngModelChange)=\"setRolesFromText(action, $event)\"\r\n />\r\n </mat-form-field>\r\n }\r\n }\r\n <p class=\"deb-hint\">Empty = no restriction for that action.</p>\r\n </mat-expansion-panel>\r\n </mat-card-content>\r\n </mat-card>\r\n\r\n <mat-card>\r\n <mat-card-header><mat-card-title>Add field</mat-card-title></mat-card-header>\r\n <mat-card-content>\r\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\r\n </mat-card-content>\r\n </mat-card>\r\n\r\n <mat-card>\r\n <mat-card-content>\r\n <ngx-tab-manager></ngx-tab-manager>\r\n </mat-card-content>\r\n </mat-card>\r\n </div>\r\n\r\n <!-- \u2500\u2500\u2500 Center: field canvas \u2500\u2500\u2500 -->\r\n <div class=\"deb-col deb-col--center\">\r\n <ngx-entity-builder-canvas />\r\n </div>\r\n\r\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\r\n <div class=\"deb-col deb-col--right\">\r\n <mat-card>\r\n <mat-card-header><mat-card-title>Field properties</mat-card-title></mat-card-header>\r\n <mat-card-content>\r\n <ngx-field-inspector></ngx-field-inspector>\r\n </mat-card-content>\r\n </mat-card>\r\n </div>\r\n</div>\r\n\r\n<!-- \u2500\u2500\u2500 Footer: JSON + optional live preview \u2500\u2500\u2500 -->\r\n<div class=\"deb-footer\">\r\n <mat-expansion-panel>\r\n <mat-expansion-panel-header>\r\n <mat-panel-title><mat-icon>data_object</mat-icon>&nbsp;Config JSON</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <pre class=\"deb-json\">{{ json }}</pre>\r\n </mat-expansion-panel>\r\n\r\n <div class=\"deb-preview-slot\">\r\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\r\n </div>\r\n</div>\r\n", styles: [":host{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-soft: #eeef2;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;display:block}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:16px;font-weight:700;color:#0f172a}.deb-spacer{flex:1 1 auto}.deb-problems--error{color:#ef4444;font-weight:600}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted)}.deb-hint{margin:4px 0 0;font-size:12px;color:var(--deb-muted)}.deb-chip{display:inline-block;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.deb-chip--muted{background:#f1f5f9;color:var(--deb-muted)}.deb-body{display:grid;grid-template-columns:320px minmax(0,1fr) 380px;gap:20px;padding:20px;align-items:start}.deb-col{display:flex;flex-direction:column;gap:16px;min-width:0}.deb-col--right{position:sticky;top:20px}@media (max-width: 1100px){.deb-body{grid-template-columns:1fr}.deb-col--right{position:static}}.deb-field-list{display:flex;flex-direction:column;gap:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--deb-border);border-radius:10px;background:var(--deb-surface);cursor:pointer;box-shadow:0 1px 3px #00000008;transition:all .18s cubic-bezier(.16,1,.3,1)}.deb-field-row:hover{border-color:#a5b4fc;background:#fcfaff;transform:translateY(-1px);box-shadow:0 4px 12px #6366f114}.deb-field-row--active{border-color:var(--deb-accent);border-left:4px solid var(--deb-accent);background:#f5f3ff;box-shadow:0 4px 16px #6366f12e}.deb-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca}.deb-drag-handle{cursor:grab;color:#94a3b8;transition:color .15s}.deb-drag-handle:hover,.deb-field-type{color:var(--deb-accent)}.deb-field-meta{display:flex;flex-direction:column;min-width:0;flex:1}.deb-field-label{font-family:var(--font-heading, sans-serif);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#0f172a;font-size:14px}.deb-field-id{font-size:11px;color:var(--deb-muted);font-family:ui-monospace,SFMono-Regular,monospace}.deb-req{color:#ef4444;font-weight:700}.deb-empty,.deb-inspector__empty{text-align:center;color:var(--deb-muted);padding:40px 16px}.deb-empty mat-icon{transform:scale(1.8);margin-bottom:16px;color:#cbd5e1}.cdk-drag-preview{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent);background:#fff}.cdk-drag-placeholder{opacity:.35}.cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-footer{padding:0 20px 20px;display:flex;flex-direction:column;gap:16px}.deb-json{margin:0;max-height:320px;overflow:auto;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.5;padding:14px;background:#0f172a;color:#e2e8f0;border-radius:10px}\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.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: DragDropModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i2$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i2$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i2$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i2$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4$3.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$3.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$3.MatExpansionPanelTitle, selector: "mat-panel-title" }, { 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: "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"], 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"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i10.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatToolbarModule }, { kind: "component", type: i11.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { 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: FieldPaletteComponent, selector: "ngx-field-palette", outputs: ["pick"] }, { kind: "component", type: TabManagerComponent, selector: "ngx-tab-manager" }, { kind: "component", type: FieldInspectorComponent, selector: "ngx-field-inspector" }, { kind: "component", type: EntityBuilderCanvasComponent, selector: "ngx-entity-builder-canvas" }] });
2937
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EntityBuilderComponent, isStandalone: true, selector: "ngx-entity-builder", inputs: { config: "config", languages: "languages", availableRoles: "availableRoles", userRoles: "userRoles", commonModules: "commonModules" }, outputs: { configChange: "configChange", save: "save" }, providers: [BuilderStore], usesOnChanges: true, ngImport: i0, template: "<mat-toolbar class=\"deb-toolbar\" color=\"primary\">\n <mat-icon>dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">Entity Builder</span>\n <span class=\"deb-spacer\"></span>\n\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matTooltip]=\"store.problems().length + ' issue(s) \u2014 hover items in the list'\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ store.errors().length }} / {{ store.problems().length }}\n </button>\n }\n\n <button mat-button type=\"button\" (click)=\"copyJson()\">\n <mat-icon>content_copy</mat-icon> Copy JSON\n </button>\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> Save\n </button>\n</mat-toolbar>\n\n<div class=\"deb-body\">\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--left\">\n <mat-card>\n <mat-card-content>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Entity name</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n placeholder=\"e.g. clients\"\n />\n </mat-form-field>\n\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-grow\">\n <mat-label>Editing language</mat-label>\n <mat-select\n [ngModel]=\"store.activeLanguage()\"\n (ngModelChange)=\"store.setActiveLanguage($event)\"\n >\n @for (l of languages; track l) {\n <mat-option [value]=\"l\">{{ l }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <mat-slide-toggle\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >Mask this entity (RBAC)</mat-slide-toggle>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>Permissions</mat-panel-title>\n </mat-expansion-panel-header>\n @for (action of rbacActions; track action) {\n @if (availableRoles.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ action }} roles</mat-label>\n <mat-select multiple [ngModel]=\"rolesFor(action)\" (ngModelChange)=\"setRoles(action, $event)\">\n @for (role of availableRoles; track role) {\n <mat-option [value]=\"role\">{{ role }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ action }} roles (comma-separated)</mat-label>\n <input\n matInput\n [ngModel]=\"rolesFor(action).join(', ')\"\n (ngModelChange)=\"setRolesFromText(action, $event)\"\n />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">Empty = no restriction for that action.</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card>\n <mat-card-header><mat-card-title>Add field</mat-card-title></mat-card-header>\n <mat-card-content>\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\n </mat-card-content>\n </mat-card>\n\n <mat-card>\n <mat-card-content>\n <ngx-tab-manager></ngx-tab-manager>\n </mat-card-content>\n </mat-card>\n </div>\n\n <!-- \u2500\u2500\u2500 Center: field canvas \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\">\n <ngx-entity-builder-canvas />\n </div>\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--right\">\n <mat-card>\n <mat-card-header><mat-card-title>Field properties</mat-card-title></mat-card-header>\n <mat-card-content>\n <ngx-field-inspector></ngx-field-inspector>\n </mat-card-content>\n </mat-card>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Footer: JSON + optional live preview \u2500\u2500\u2500 -->\n<div class=\"deb-footer\">\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title><mat-icon>data_object</mat-icon>&nbsp;Config JSON</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n\n <div class=\"deb-preview-slot\">\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </div>\n</div>\n", styles: [":host{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-soft: #eeef2;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;display:block}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:16px;font-weight:700;color:#fff}.deb-spacer{flex:1 1 auto}.deb-problems--error{color:#ef4444;font-weight:600}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted)}.deb-hint{margin:4px 0 0;font-size:12px;color:var(--deb-muted)}.deb-chip{display:inline-block;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.deb-chip--muted{background:#f1f5f9;color:var(--deb-muted)}.deb-body{display:grid;grid-template-columns:320px minmax(0,1fr) 380px;gap:20px;padding:20px;align-items:start}.deb-col{display:flex;flex-direction:column;gap:16px;min-width:0}.deb-col--right{position:sticky;top:20px}@media (max-width: 1100px){.deb-body{grid-template-columns:1fr}.deb-col--right{position:static}}.deb-field-list{display:flex;flex-direction:column;gap:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--deb-border);border-radius:10px;background:var(--deb-surface);cursor:pointer;box-shadow:0 1px 3px #00000008;transition:all .18s cubic-bezier(.16,1,.3,1)}.deb-field-row:hover{border-color:#a5b4fc;background:#fcfaff;transform:translateY(-1px);box-shadow:0 4px 12px #6366f114}.deb-field-row--active{border-color:var(--deb-accent);border-left:4px solid var(--deb-accent);background:#f5f3ff;box-shadow:0 4px 16px #6366f12e}.deb-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca}.deb-drag-handle{cursor:grab;color:#94a3b8;transition:color .15s}.deb-drag-handle:hover,.deb-field-type{color:var(--deb-accent)}.deb-field-meta{display:flex;flex-direction:column;min-width:0;flex:1}.deb-field-label{font-family:var(--font-heading, sans-serif);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#0f172a;font-size:14px}.deb-field-id{font-size:11px;color:var(--deb-muted);font-family:ui-monospace,SFMono-Regular,monospace}.deb-req{color:#ef4444;font-weight:700}.deb-empty,.deb-inspector__empty{text-align:center;color:var(--deb-muted);padding:40px 16px}.deb-empty mat-icon{transform:scale(1.8);margin-bottom:16px;color:#cbd5e1}.cdk-drag-preview{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent);background:#fff}.cdk-drag-placeholder{opacity:.35}.cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-footer{padding:0 20px 20px;display:flex;flex-direction:column;gap:16px}.deb-json{margin:0;max-height:320px;overflow:auto;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.5;padding:14px;background:#0f172a;color:#e2e8f0;border-radius:10px}\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.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: DragDropModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i2$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i2$1.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i2$1.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i2$1.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4$3.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$3.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$3.MatExpansionPanelTitle, selector: "mat-panel-title" }, { 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: "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"], 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"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i10.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatToolbarModule }, { kind: "component", type: i11.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { 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: FieldPaletteComponent, selector: "ngx-field-palette", outputs: ["pick"] }, { kind: "component", type: TabManagerComponent, selector: "ngx-tab-manager" }, { kind: "component", type: FieldInspectorComponent, selector: "ngx-field-inspector" }, { kind: "component", type: EntityBuilderCanvasComponent, selector: "ngx-entity-builder-canvas" }] });
2757
2938
  }
2758
2939
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityBuilderComponent, decorators: [{
2759
2940
  type: Component,
@@ -2775,13 +2956,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2775
2956
  TabManagerComponent,
2776
2957
  FieldInspectorComponent,
2777
2958
  EntityBuilderCanvasComponent,
2778
- ], template: "<mat-toolbar class=\"deb-toolbar\" color=\"primary\">\r\n <mat-icon>dashboard_customize</mat-icon>\r\n <span class=\"deb-toolbar__title\">Entity Builder</span>\r\n <span class=\"deb-spacer\"></span>\r\n\r\n @if (store.problems().length) {\r\n <button\r\n mat-button\r\n type=\"button\"\r\n class=\"deb-problems\"\r\n [class.deb-problems--error]=\"!store.isValid()\"\r\n [matTooltip]=\"store.problems().length + ' issue(s) \u2014 hover items in the list'\"\r\n >\r\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\r\n {{ store.errors().length }} / {{ store.problems().length }}\r\n </button>\r\n }\r\n\r\n <button mat-button type=\"button\" (click)=\"copyJson()\">\r\n <mat-icon>content_copy</mat-icon> Copy JSON\r\n </button>\r\n <button mat-flat-button color=\"accent\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\r\n <mat-icon>save</mat-icon> Save\r\n </button>\r\n</mat-toolbar>\r\n\r\n<div class=\"deb-body\">\r\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\r\n <div class=\"deb-col deb-col--left\">\r\n <mat-card>\r\n <mat-card-content>\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>Entity name</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"store.config().entity\"\r\n (ngModelChange)=\"store.setEntityName($event)\"\r\n placeholder=\"e.g. clients\"\r\n />\r\n </mat-form-field>\r\n\r\n <div class=\"deb-row\">\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-grow\">\r\n <mat-label>Editing language</mat-label>\r\n <mat-select\r\n [ngModel]=\"store.activeLanguage()\"\r\n (ngModelChange)=\"store.setActiveLanguage($event)\"\r\n >\r\n @for (l of languages; track l) {\r\n <mat-option [value]=\"l\">{{ l }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n </div>\r\n\r\n <mat-slide-toggle\r\n [ngModel]=\"store.config().maskData ?? false\"\r\n (ngModelChange)=\"store.setMaskData($event)\"\r\n >Mask this entity (RBAC)</mat-slide-toggle>\r\n\r\n <mat-expansion-panel class=\"deb-rbac\">\r\n <mat-expansion-panel-header>\r\n <mat-panel-title>Permissions</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n @for (action of rbacActions; track action) {\r\n @if (availableRoles.length) {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>{{ action }} roles</mat-label>\r\n <mat-select multiple [ngModel]=\"rolesFor(action)\" (ngModelChange)=\"setRoles(action, $event)\">\r\n @for (role of availableRoles; track role) {\r\n <mat-option [value]=\"role\">{{ role }}</mat-option>\r\n }\r\n </mat-select>\r\n </mat-form-field>\r\n } @else {\r\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\r\n <mat-label>{{ action }} roles (comma-separated)</mat-label>\r\n <input\r\n matInput\r\n [ngModel]=\"rolesFor(action).join(', ')\"\r\n (ngModelChange)=\"setRolesFromText(action, $event)\"\r\n />\r\n </mat-form-field>\r\n }\r\n }\r\n <p class=\"deb-hint\">Empty = no restriction for that action.</p>\r\n </mat-expansion-panel>\r\n </mat-card-content>\r\n </mat-card>\r\n\r\n <mat-card>\r\n <mat-card-header><mat-card-title>Add field</mat-card-title></mat-card-header>\r\n <mat-card-content>\r\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\r\n </mat-card-content>\r\n </mat-card>\r\n\r\n <mat-card>\r\n <mat-card-content>\r\n <ngx-tab-manager></ngx-tab-manager>\r\n </mat-card-content>\r\n </mat-card>\r\n </div>\r\n\r\n <!-- \u2500\u2500\u2500 Center: field canvas \u2500\u2500\u2500 -->\r\n <div class=\"deb-col deb-col--center\">\r\n <ngx-entity-builder-canvas />\r\n </div>\r\n\r\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\r\n <div class=\"deb-col deb-col--right\">\r\n <mat-card>\r\n <mat-card-header><mat-card-title>Field properties</mat-card-title></mat-card-header>\r\n <mat-card-content>\r\n <ngx-field-inspector></ngx-field-inspector>\r\n </mat-card-content>\r\n </mat-card>\r\n </div>\r\n</div>\r\n\r\n<!-- \u2500\u2500\u2500 Footer: JSON + optional live preview \u2500\u2500\u2500 -->\r\n<div class=\"deb-footer\">\r\n <mat-expansion-panel>\r\n <mat-expansion-panel-header>\r\n <mat-panel-title><mat-icon>data_object</mat-icon>&nbsp;Config JSON</mat-panel-title>\r\n </mat-expansion-panel-header>\r\n <pre class=\"deb-json\">{{ json }}</pre>\r\n </mat-expansion-panel>\r\n\r\n <div class=\"deb-preview-slot\">\r\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\r\n </div>\r\n</div>\r\n", styles: [":host{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-soft: #eeef2;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;display:block}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:16px;font-weight:700;color:#0f172a}.deb-spacer{flex:1 1 auto}.deb-problems--error{color:#ef4444;font-weight:600}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted)}.deb-hint{margin:4px 0 0;font-size:12px;color:var(--deb-muted)}.deb-chip{display:inline-block;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.deb-chip--muted{background:#f1f5f9;color:var(--deb-muted)}.deb-body{display:grid;grid-template-columns:320px minmax(0,1fr) 380px;gap:20px;padding:20px;align-items:start}.deb-col{display:flex;flex-direction:column;gap:16px;min-width:0}.deb-col--right{position:sticky;top:20px}@media (max-width: 1100px){.deb-body{grid-template-columns:1fr}.deb-col--right{position:static}}.deb-field-list{display:flex;flex-direction:column;gap:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--deb-border);border-radius:10px;background:var(--deb-surface);cursor:pointer;box-shadow:0 1px 3px #00000008;transition:all .18s cubic-bezier(.16,1,.3,1)}.deb-field-row:hover{border-color:#a5b4fc;background:#fcfaff;transform:translateY(-1px);box-shadow:0 4px 12px #6366f114}.deb-field-row--active{border-color:var(--deb-accent);border-left:4px solid var(--deb-accent);background:#f5f3ff;box-shadow:0 4px 16px #6366f12e}.deb-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca}.deb-drag-handle{cursor:grab;color:#94a3b8;transition:color .15s}.deb-drag-handle:hover,.deb-field-type{color:var(--deb-accent)}.deb-field-meta{display:flex;flex-direction:column;min-width:0;flex:1}.deb-field-label{font-family:var(--font-heading, sans-serif);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#0f172a;font-size:14px}.deb-field-id{font-size:11px;color:var(--deb-muted);font-family:ui-monospace,SFMono-Regular,monospace}.deb-req{color:#ef4444;font-weight:700}.deb-empty,.deb-inspector__empty{text-align:center;color:var(--deb-muted);padding:40px 16px}.deb-empty mat-icon{transform:scale(1.8);margin-bottom:16px;color:#cbd5e1}.cdk-drag-preview{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent);background:#fff}.cdk-drag-placeholder{opacity:.35}.cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-footer{padding:0 20px 20px;display:flex;flex-direction:column;gap:16px}.deb-json{margin:0;max-height:320px;overflow:auto;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.5;padding:14px;background:#0f172a;color:#e2e8f0;border-radius:10px}\n"] }]
2959
+ ], template: "<mat-toolbar class=\"deb-toolbar\" color=\"primary\">\n <mat-icon>dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">Entity Builder</span>\n <span class=\"deb-spacer\"></span>\n\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matTooltip]=\"store.problems().length + ' issue(s) \u2014 hover items in the list'\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ store.errors().length }} / {{ store.problems().length }}\n </button>\n }\n\n <button mat-button type=\"button\" (click)=\"copyJson()\">\n <mat-icon>content_copy</mat-icon> Copy JSON\n </button>\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> Save\n </button>\n</mat-toolbar>\n\n<div class=\"deb-body\">\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--left\">\n <mat-card>\n <mat-card-content>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>Entity name</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n placeholder=\"e.g. clients\"\n />\n </mat-form-field>\n\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-grow\">\n <mat-label>Editing language</mat-label>\n <mat-select\n [ngModel]=\"store.activeLanguage()\"\n (ngModelChange)=\"store.setActiveLanguage($event)\"\n >\n @for (l of languages; track l) {\n <mat-option [value]=\"l\">{{ l }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n </div>\n\n <mat-slide-toggle\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >Mask this entity (RBAC)</mat-slide-toggle>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>Permissions</mat-panel-title>\n </mat-expansion-panel-header>\n @for (action of rbacActions; track action) {\n @if (availableRoles.length) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ action }} roles</mat-label>\n <mat-select multiple [ngModel]=\"rolesFor(action)\" (ngModelChange)=\"setRoles(action, $event)\">\n @for (role of availableRoles; track role) {\n <mat-option [value]=\"role\">{{ role }}</mat-option>\n }\n </mat-select>\n </mat-form-field>\n } @else {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ action }} roles (comma-separated)</mat-label>\n <input\n matInput\n [ngModel]=\"rolesFor(action).join(', ')\"\n (ngModelChange)=\"setRolesFromText(action, $event)\"\n />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">Empty = no restriction for that action.</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card>\n <mat-card-header><mat-card-title>Add field</mat-card-title></mat-card-header>\n <mat-card-content>\n <ngx-field-palette (pick)=\"store.addField($event)\"></ngx-field-palette>\n </mat-card-content>\n </mat-card>\n\n <mat-card>\n <mat-card-content>\n <ngx-tab-manager></ngx-tab-manager>\n </mat-card-content>\n </mat-card>\n </div>\n\n <!-- \u2500\u2500\u2500 Center: field canvas \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\">\n <ngx-entity-builder-canvas />\n </div>\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--right\">\n <mat-card>\n <mat-card-header><mat-card-title>Field properties</mat-card-title></mat-card-header>\n <mat-card-content>\n <ngx-field-inspector></ngx-field-inspector>\n </mat-card-content>\n </mat-card>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Footer: JSON + optional live preview \u2500\u2500\u2500 -->\n<div class=\"deb-footer\">\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title><mat-icon>data_object</mat-icon>&nbsp;Config JSON</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n\n <div class=\"deb-preview-slot\">\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </div>\n</div>\n", styles: [":host{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-soft: #eeef2;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;display:block}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:16px;font-weight:700;color:#fff}.deb-spacer{flex:1 1 auto}.deb-problems--error{color:#ef4444;font-weight:600}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted)}.deb-hint{margin:4px 0 0;font-size:12px;color:var(--deb-muted)}.deb-chip{display:inline-block;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}.deb-chip--muted{background:#f1f5f9;color:var(--deb-muted)}.deb-body{display:grid;grid-template-columns:320px minmax(0,1fr) 380px;gap:20px;padding:20px;align-items:start}.deb-col{display:flex;flex-direction:column;gap:16px;min-width:0}.deb-col--right{position:sticky;top:20px}@media (max-width: 1100px){.deb-body{grid-template-columns:1fr}.deb-col--right{position:static}}.deb-field-list{display:flex;flex-direction:column;gap:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--deb-border);border-radius:10px;background:var(--deb-surface);cursor:pointer;box-shadow:0 1px 3px #00000008;transition:all .18s cubic-bezier(.16,1,.3,1)}.deb-field-row:hover{border-color:#a5b4fc;background:#fcfaff;transform:translateY(-1px);box-shadow:0 4px 12px #6366f114}.deb-field-row--active{border-color:var(--deb-accent);border-left:4px solid var(--deb-accent);background:#f5f3ff;box-shadow:0 4px 16px #6366f12e}.deb-type-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:2px 7px;border-radius:6px;background:#e0e7ff;color:#4338ca}.deb-drag-handle{cursor:grab;color:#94a3b8;transition:color .15s}.deb-drag-handle:hover,.deb-field-type{color:var(--deb-accent)}.deb-field-meta{display:flex;flex-direction:column;min-width:0;flex:1}.deb-field-label{font-family:var(--font-heading, sans-serif);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#0f172a;font-size:14px}.deb-field-id{font-size:11px;color:var(--deb-muted);font-family:ui-monospace,SFMono-Regular,monospace}.deb-req{color:#ef4444;font-weight:700}.deb-empty,.deb-inspector__empty{text-align:center;color:var(--deb-muted);padding:40px 16px}.deb-empty mat-icon{transform:scale(1.8);margin-bottom:16px;color:#cbd5e1}.cdk-drag-preview{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent);background:#fff}.cdk-drag-placeholder{opacity:.35}.cdk-drag-animating{transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:transform .2s cubic-bezier(0,0,.2,1)}.deb-footer{padding:0 20px 20px;display:flex;flex-direction:column;gap:16px}.deb-json{margin:0;max-height:320px;overflow:auto;font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;line-height:1.5;padding:14px;background:#0f172a;color:#e2e8f0;border-radius:10px}\n"] }]
2779
2960
  }], ctorParameters: () => [], propDecorators: { config: [{
2780
2961
  type: Input
2781
2962
  }], languages: [{
2782
2963
  type: Input
2783
2964
  }], availableRoles: [{
2784
2965
  type: Input
2966
+ }], userRoles: [{
2967
+ type: Input
2785
2968
  }], commonModules: [{
2786
2969
  type: Input
2787
2970
  }], configChange: [{
@@ -2790,121 +2973,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2790
2973
  type: Output
2791
2974
  }] } });
2792
2975
 
2793
- /**
2794
- * ConnectionSourceConfigComponent — allows configuring connection source settings
2795
- * for an entity or field (Phase 7.2).
2796
- */
2797
- class ConnectionSourceConfigComponent {
2798
- store = inject(BuilderStore);
2799
- field = computed(() => this.store.selectedField());
2800
- connectionSourceEntity(field) {
2801
- return field.connectionSource?.entity ?? '';
2802
- }
2803
- setConnectionSourceEntity(field, entity) {
2804
- const trimmed = entity.trim();
2805
- const existing = field.connectionSource ?? {};
2806
- if (!trimmed && !existing.targetField) {
2807
- this.store.updateField(field.id, { connectionSource: undefined });
2808
- }
2809
- else {
2810
- this.store.updateField(field.id, {
2811
- connectionSource: { ...existing, entity: trimmed },
2812
- });
2813
- }
2814
- }
2815
- connectionTargetField(field) {
2816
- return field.connectionSource?.targetField ?? '';
2817
- }
2818
- setConnectionTargetField(field, targetField) {
2819
- const trimmed = targetField.trim();
2820
- const existing = field.connectionSource ?? {};
2821
- if (!trimmed && !existing.entity) {
2822
- this.store.updateField(field.id, { connectionSource: undefined });
2823
- }
2824
- else {
2825
- this.store.updateField(field.id, {
2826
- connectionSource: { ...existing, targetField: trimmed },
2827
- });
2828
- }
2829
- }
2830
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConnectionSourceConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2831
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: ConnectionSourceConfigComponent, isStandalone: true, selector: "ngx-connection-source-config", ngImport: i0, template: `
2832
- @if (field(); as f) {
2833
- <div class="deb-connection-source">
2834
- <span class="deb-section-title">Connection Source</span>
2835
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2836
- <mat-label>Source Entity</mat-label>
2837
- <input
2838
- matInput
2839
- data-testid="connection-source-entity"
2840
- [ngModel]="connectionSourceEntity(f)"
2841
- (ngModelChange)="setConnectionSourceEntity(f, $event)"
2842
- placeholder="e.g. organizations"
2843
- />
2844
- </mat-form-field>
2845
-
2846
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2847
- <mat-label>Target Field ID</mat-label>
2848
- <input
2849
- matInput
2850
- data-testid="connection-target-field"
2851
- [ngModel]="connectionTargetField(f)"
2852
- (ngModelChange)="setConnectionTargetField(f, $event)"
2853
- placeholder="e.g. companyId"
2854
- />
2855
- </mat-form-field>
2856
- </div>
2857
- }
2858
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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.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: 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: "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"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }] });
2859
- }
2860
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConnectionSourceConfigComponent, decorators: [{
2861
- type: Component,
2862
- args: [{
2863
- selector: 'ngx-connection-source-config',
2864
- standalone: true,
2865
- imports: [
2866
- CommonModule,
2867
- FormsModule,
2868
- MatFormFieldModule,
2869
- MatInputModule,
2870
- MatSelectModule,
2871
- ],
2872
- template: `
2873
- @if (field(); as f) {
2874
- <div class="deb-connection-source">
2875
- <span class="deb-section-title">Connection Source</span>
2876
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2877
- <mat-label>Source Entity</mat-label>
2878
- <input
2879
- matInput
2880
- data-testid="connection-source-entity"
2881
- [ngModel]="connectionSourceEntity(f)"
2882
- (ngModelChange)="setConnectionSourceEntity(f, $event)"
2883
- placeholder="e.g. organizations"
2884
- />
2885
- </mat-form-field>
2886
-
2887
- <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
2888
- <mat-label>Target Field ID</mat-label>
2889
- <input
2890
- matInput
2891
- data-testid="connection-target-field"
2892
- [ngModel]="connectionTargetField(f)"
2893
- (ngModelChange)="setConnectionTargetField(f, $event)"
2894
- placeholder="e.g. companyId"
2895
- />
2896
- </mat-form-field>
2897
- </div>
2898
- }
2899
- `,
2900
- }]
2901
- }] });
2902
-
2903
2976
  // Main component
2904
2977
 
2905
2978
  /**
2906
2979
  * Generated bundle index. Do not edit.
2907
2980
  */
2908
2981
 
2909
- export { BuilderStore, ConnectionSourceConfigComponent, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleFormComponent, SYSTEM_DEFAULT_CAN_EDIT, TabManagerComponent };
2982
+ export { BuilderStore, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleFormComponent, TabManagerComponent };
2910
2983
  //# sourceMappingURL=ngx-dynamic-entity-builder.mjs.map