ngx-dynamic-entity-builder 1.0.0 → 1.1.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';
@@ -32,7 +32,8 @@ import * as i3 from '@angular/material/divider';
32
32
  import { MatDividerModule } from '@angular/material/divider';
33
33
  import * as i9 from '@angular/material/core';
34
34
  import { CommonModule } from '@angular/common';
35
- import { ConfigSourceService } from 'ngx-dynamic-entity';
35
+ import { ConfigSourceService, SYSTEM_DEFAULT_CAN_EDIT } from 'ngx-dynamic-entity';
36
+ export { SYSTEM_DEFAULT_CAN_EDIT } 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 ||
@@ -396,7 +454,7 @@ class BuilderStore {
396
454
  }
397
455
  /** `derived`, or `derived_2`, `derived_3`… if another field already holds it. */
398
456
  availableId(derived, currentId) {
399
- const taken = new Set(this.fields().map(f => f.id).filter(fid => fid !== currentId));
457
+ const taken = new Set([...this.allFieldIds(this._config().tabs)].filter(fid => fid !== currentId));
400
458
  if (!taken.has(derived))
401
459
  return derived;
402
460
  let n = 2;
@@ -426,9 +484,15 @@ class BuilderStore {
426
484
  delete field.validators.required;
427
485
  }
428
486
  else if (validator === 'email') {
487
+ // Writes its own flag, not `pattern`. Sharing `pattern` meant a field could not have
488
+ // both an email check and a custom regex, setting a custom pattern made the Email box
489
+ // appear ticked, and un-ticking Email deleted whatever pattern the author had written.
429
490
  if (on)
430
- field.validators.pattern = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$';
491
+ field.validators.email = true;
431
492
  else
493
+ delete field.validators.email;
494
+ // Clear the legacy encoding either way, so a config touched here stops carrying both.
495
+ if (field.validators.pattern === LEGACY_EMAIL_PATTERN)
432
496
  delete field.validators.pattern;
433
497
  }
434
498
  });
@@ -454,8 +518,12 @@ class BuilderStore {
454
518
  hasFlagValidator(field, validator) {
455
519
  if (validator === 'required')
456
520
  return !!field.validators?.required;
457
- if (validator === 'email')
458
- return !!field.validators?.pattern;
521
+ if (validator === 'email') {
522
+ // This used to report *any* pattern as email, so writing a custom regex silently
523
+ // ticked the box. The legacy encoding is still recognised so a config authored by the
524
+ // old builder keeps showing Email as ticked.
525
+ return !!field.validators?.email || field.validators?.pattern === LEGACY_EMAIL_PATTERN;
526
+ }
459
527
  return false;
460
528
  }
461
529
  // ─── Options (dropdown / multiSelect / radio) ────────────────────────────────
@@ -761,7 +829,7 @@ class BuilderStore {
761
829
  this._rules.set(clone(rules));
762
830
  }
763
831
  addRule(rule) {
764
- const id = rule.id?.trim() || this.uniqueId('rule', this._rules().map(r => ({ id: r.id ?? '' })));
832
+ const id = rule.id?.trim() || this.uniqueId('rule', new Set(this._rules().map(r => r.id ?? '')));
765
833
  this._rules.update(rules => [...rules, { ...clone(rule), id }]);
766
834
  return id;
767
835
  }
@@ -797,7 +865,7 @@ class BuilderStore {
797
865
  // Must be unique across the whole tree, not just the top level: a tab id is the record
798
866
  // storage path, so a new top-level tab colliding with an existing sub-tab would make two
799
867
  // tabs write to the same key and break findTab/getTabPath.
800
- const id = this.uniqueId('tab', this.allTabIds().map(tid => ({ id: tid })));
868
+ const id = this.uniqueId('tab', new Set(this.allTabIds()));
801
869
  this.mutate(draft => {
802
870
  const tabs = draft.tabs ?? (draft.tabs = []);
803
871
  const lang = draft.defaultLanguage ?? 'en';
@@ -818,7 +886,7 @@ class BuilderStore {
818
886
  return ids;
819
887
  }
820
888
  addSubTab(parentId) {
821
- const id = this.uniqueId('tab', this.allTabIds().map(tid => ({ id: tid })));
889
+ const id = this.uniqueId('tab', new Set(this.allTabIds()));
822
890
  this.mutate(draft => {
823
891
  const parent = findTab(draft.tabs, parentId);
824
892
  if (parent) {
@@ -893,8 +961,7 @@ class BuilderStore {
893
961
  this._config.set(draft);
894
962
  this._isDirty.set(true);
895
963
  }
896
- uniqueId(prefix, existing) {
897
- const taken = new Set(existing.map(e => e.id));
964
+ uniqueId(prefix, taken) {
898
965
  let n = 1;
899
966
  let candidate = `${prefix}_${n}`;
900
967
  while (taken.has(candidate)) {
@@ -918,9 +985,7 @@ class BuilderStore {
918
985
  if (allFields.length === 0) {
919
986
  problems.push({ level: 'warning', message: 'This entity has no fields yet.' });
920
987
  }
921
- const seen = new Map();
922
988
  for (const field of allFields) {
923
- seen.set(field.id, (seen.get(field.id) ?? 0) + 1);
924
989
  if (!field.id || !ID_PATTERN.test(field.id)) {
925
990
  problems.push({
926
991
  level: 'error',
@@ -957,7 +1022,22 @@ class BuilderStore {
957
1022
  });
958
1023
  }
959
1024
  }
960
- for (const [id, count] of seen) {
1025
+ // Id uniqueness is a whole-config invariant, and it is counted over the entire field
1026
+ // tree rather than `allFields`, which stops at a tab's own fields. Two ids colliding
1027
+ // inside a group or array went unreported even though rules, showWhen conditions and
1028
+ // autoPatch mappings all address fields by bare id and would resolve to whichever one
1029
+ // the lookup happened to reach first.
1030
+ const idCounts = new Map();
1031
+ const countIds = (fields) => {
1032
+ for (const f of fields ?? []) {
1033
+ idCounts.set(f.id, (idCounts.get(f.id) ?? 0) + 1);
1034
+ if (f.children?.length)
1035
+ countIds(f.children);
1036
+ }
1037
+ };
1038
+ for (const tab of this.flattenTabs(config.tabs))
1039
+ countIds(tab.fields);
1040
+ for (const [id, count] of idCounts) {
961
1041
  if (count > 1) {
962
1042
  problems.push({ level: 'error', message: `Duplicate field id "${id}" (${count}×).`, fieldId: id });
963
1043
  }
@@ -1037,156 +1117,156 @@ class EntityReferenceConfigComponent {
1037
1117
  }
1038
1118
  }
1039
1119
  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
- }
1120
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: EntityReferenceConfigComponent, isStandalone: true, selector: "ngx-entity-reference-config", ngImport: i0, template: `
1121
+ @if (field(); as f) {
1122
+ <div class="deb-entity-ref">
1123
+ <span class="deb-section-title">Entity reference</span>
1124
+
1125
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1126
+ <mat-label>Registry key</mat-label>
1127
+ <input
1128
+ matInput
1129
+ data-testid="entity-ref-key"
1130
+ [ngModel]="ref(f).linkedEntityKey ?? ''"
1131
+ (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1132
+ />
1133
+ <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
1134
+ </mat-form-field>
1135
+
1136
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1137
+ <mat-label>Display fields</mat-label>
1138
+ <input
1139
+ matInput
1140
+ data-testid="entity-ref-display-fields"
1141
+ [ngModel]="displayFieldsText(f)"
1142
+ (ngModelChange)="setDisplayFields(f, $event)"
1143
+ />
1144
+ <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
1145
+ </mat-form-field>
1146
+
1147
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1148
+ <mat-label>Static filters (JSON)</mat-label>
1149
+ <input
1150
+ matInput
1151
+ data-testid="entity-ref-filters"
1152
+ [ngModel]="filtersText(f)"
1153
+ (ngModelChange)="setFilters(f, $event)"
1154
+ />
1155
+ @if (filtersError()) {
1156
+ <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1157
+ } @else {
1158
+ <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
1159
+ }
1160
+ </mat-form-field>
1161
+
1162
+ <mat-divider></mat-divider>
1163
+
1164
+ <!-- Cascade -->
1165
+ <span class="deb-section-title">Cascade</span>
1166
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1167
+ <mat-label>Parent field</mat-label>
1168
+ <mat-select
1169
+ data-testid="entity-ref-parent"
1170
+ [ngModel]="ref(f).parentField ?? ''"
1171
+ (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1172
+ >
1173
+ <mat-option value="">None — load all options</mat-option>
1174
+ @for (candidate of parentCandidates(); track candidate.id) {
1175
+ <mat-option [value]="candidate.id">{{ fieldLabel(candidate) }} ({{ candidate.id }})</mat-option>
1176
+ }
1177
+ </mat-select>
1178
+ <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
1179
+ </mat-form-field>
1180
+
1181
+ @if (ref(f).parentField) {
1182
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1183
+ <mat-label>Lookup filter path</mat-label>
1184
+ <input
1185
+ matInput
1186
+ data-testid="entity-ref-lookup-filter"
1187
+ [ngModel]="ref(f).lookupFilter ?? ''"
1188
+ (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1189
+ />
1190
+ <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
1191
+ </mat-form-field>
1192
+
1193
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1194
+ <mat-label>Lookup path (nested options)</mat-label>
1195
+ <input
1196
+ matInput
1197
+ data-testid="entity-ref-lookup-path"
1198
+ [ngModel]="ref(f).lookupPath ?? ''"
1199
+ (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1200
+ />
1201
+ <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
1202
+ </mat-form-field>
1203
+ }
1204
+
1205
+ <mat-divider></mat-divider>
1206
+
1207
+ <!-- autoPatch -->
1208
+ <div class="deb-row deb-row--split">
1209
+ <span class="deb-section-title">Auto-patch on select</span>
1210
+ <button
1211
+ mat-stroked-button
1212
+ type="button"
1213
+ data-testid="add-auto-patch"
1214
+ (click)="store.addAutoPatchMapping(f.id)"
1215
+ >
1216
+ <mat-icon>add</mat-icon> Mapping
1217
+ </button>
1218
+ </div>
1219
+
1220
+ @if (f.autoPatch) {
1221
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1222
+ <mat-label>Target tab</mat-label>
1223
+ <mat-select
1224
+ data-testid="auto-patch-tab"
1225
+ [ngModel]="f.autoPatch.targetTab"
1226
+ (ngModelChange)="store.setAutoPatchTargetTab(f.id, $event)"
1227
+ >
1228
+ @for (tab of store.tabs(); track tab.id) {
1229
+ <mat-option [value]="tab.id">{{ tabLabel(tab.id) }}</mat-option>
1230
+ }
1231
+ </mat-select>
1232
+ </mat-form-field>
1233
+
1234
+ @for (mapping of f.autoPatch.mappings; track $index) {
1235
+ <div class="deb-option-row">
1236
+ <mat-form-field appearance="outline" subscriptSizing="dynamic">
1237
+ <mat-label>Source (linked record)</mat-label>
1238
+ <input
1239
+ matInput
1240
+ [ngModel]="mapping.source"
1241
+ (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { source: $event })"
1242
+ />
1243
+ </mat-form-field>
1244
+ <mat-form-field appearance="outline" subscriptSizing="dynamic">
1245
+ <mat-label>Target field</mat-label>
1246
+ <input
1247
+ matInput
1248
+ [ngModel]="mapping.target"
1249
+ (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { target: $event })"
1250
+ />
1251
+ </mat-form-field>
1252
+ <button
1253
+ mat-icon-button
1254
+ type="button"
1255
+ color="warn"
1256
+ matTooltip="Remove mapping"
1257
+ (click)="store.removeAutoPatchMapping(f.id, $index)"
1258
+ >
1259
+ <mat-icon>delete</mat-icon>
1260
+ </button>
1261
+ </div>
1262
+ }
1263
+ } @else {
1264
+ <p class="deb-hint">
1265
+ No auto-patch configured — selecting a reference will not copy any values.
1266
+ </p>
1267
+ }
1268
+ </div>
1269
+ }
1190
1270
  `, 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
1271
  }
1192
1272
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityReferenceConfigComponent, decorators: [{
@@ -1201,156 +1281,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
1201
1281
  MatSelectModule,
1202
1282
  MatSlideToggleModule,
1203
1283
  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
- }
1284
+ ], template: `
1285
+ @if (field(); as f) {
1286
+ <div class="deb-entity-ref">
1287
+ <span class="deb-section-title">Entity reference</span>
1288
+
1289
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1290
+ <mat-label>Registry key</mat-label>
1291
+ <input
1292
+ matInput
1293
+ data-testid="entity-ref-key"
1294
+ [ngModel]="ref(f).linkedEntityKey ?? ''"
1295
+ (ngModelChange)="patchRef(f, { linkedEntityKey: $event || undefined })"
1296
+ />
1297
+ <mat-hint>Key the consumer registered a loader under. Defaults to the field id.</mat-hint>
1298
+ </mat-form-field>
1299
+
1300
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1301
+ <mat-label>Display fields</mat-label>
1302
+ <input
1303
+ matInput
1304
+ data-testid="entity-ref-display-fields"
1305
+ [ngModel]="displayFieldsText(f)"
1306
+ (ngModelChange)="setDisplayFields(f, $event)"
1307
+ />
1308
+ <mat-hint>Comma-separated record paths used to build each option label.</mat-hint>
1309
+ </mat-form-field>
1310
+
1311
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1312
+ <mat-label>Static filters (JSON)</mat-label>
1313
+ <input
1314
+ matInput
1315
+ data-testid="entity-ref-filters"
1316
+ [ngModel]="filtersText(f)"
1317
+ (ngModelChange)="setFilters(f, $event)"
1318
+ />
1319
+ @if (filtersError()) {
1320
+ <mat-hint class="deb-error-hint">{{ filtersError() }}</mat-hint>
1321
+ } @else {
1322
+ <mat-hint>Passed to the loader, e.g. &#123;"isEmployee": false&#125;.</mat-hint>
1323
+ }
1324
+ </mat-form-field>
1325
+
1326
+ <mat-divider></mat-divider>
1327
+
1328
+ <!-- Cascade -->
1329
+ <span class="deb-section-title">Cascade</span>
1330
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1331
+ <mat-label>Parent field</mat-label>
1332
+ <mat-select
1333
+ data-testid="entity-ref-parent"
1334
+ [ngModel]="ref(f).parentField ?? ''"
1335
+ (ngModelChange)="patchRef(f, { parentField: $event || undefined })"
1336
+ >
1337
+ <mat-option value="">None — load all options</mat-option>
1338
+ @for (candidate of parentCandidates(); track candidate.id) {
1339
+ <mat-option [value]="candidate.id">{{ fieldLabel(candidate) }} ({{ candidate.id }})</mat-option>
1340
+ }
1341
+ </mat-select>
1342
+ <mat-hint>This field's options reload whenever the parent changes.</mat-hint>
1343
+ </mat-form-field>
1344
+
1345
+ @if (ref(f).parentField) {
1346
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1347
+ <mat-label>Lookup filter path</mat-label>
1348
+ <input
1349
+ matInput
1350
+ data-testid="entity-ref-lookup-filter"
1351
+ [ngModel]="ref(f).lookupFilter ?? ''"
1352
+ (ngModelChange)="patchRef(f, { lookupFilter: $event || undefined })"
1353
+ />
1354
+ <mat-hint>Keep options whose record matches the parent value at this path.</mat-hint>
1355
+ </mat-form-field>
1356
+
1357
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1358
+ <mat-label>Lookup path (nested options)</mat-label>
1359
+ <input
1360
+ matInput
1361
+ data-testid="entity-ref-lookup-path"
1362
+ [ngModel]="ref(f).lookupPath ?? ''"
1363
+ (ngModelChange)="patchRef(f, { lookupPath: $event || undefined })"
1364
+ />
1365
+ <mat-hint>Take options from this array on the selected parent's record instead.</mat-hint>
1366
+ </mat-form-field>
1367
+ }
1368
+
1369
+ <mat-divider></mat-divider>
1370
+
1371
+ <!-- autoPatch -->
1372
+ <div class="deb-row deb-row--split">
1373
+ <span class="deb-section-title">Auto-patch on select</span>
1374
+ <button
1375
+ mat-stroked-button
1376
+ type="button"
1377
+ data-testid="add-auto-patch"
1378
+ (click)="store.addAutoPatchMapping(f.id)"
1379
+ >
1380
+ <mat-icon>add</mat-icon> Mapping
1381
+ </button>
1382
+ </div>
1383
+
1384
+ @if (f.autoPatch) {
1385
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-full">
1386
+ <mat-label>Target tab</mat-label>
1387
+ <mat-select
1388
+ data-testid="auto-patch-tab"
1389
+ [ngModel]="f.autoPatch.targetTab"
1390
+ (ngModelChange)="store.setAutoPatchTargetTab(f.id, $event)"
1391
+ >
1392
+ @for (tab of store.tabs(); track tab.id) {
1393
+ <mat-option [value]="tab.id">{{ tabLabel(tab.id) }}</mat-option>
1394
+ }
1395
+ </mat-select>
1396
+ </mat-form-field>
1397
+
1398
+ @for (mapping of f.autoPatch.mappings; track $index) {
1399
+ <div class="deb-option-row">
1400
+ <mat-form-field appearance="outline" subscriptSizing="dynamic">
1401
+ <mat-label>Source (linked record)</mat-label>
1402
+ <input
1403
+ matInput
1404
+ [ngModel]="mapping.source"
1405
+ (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { source: $event })"
1406
+ />
1407
+ </mat-form-field>
1408
+ <mat-form-field appearance="outline" subscriptSizing="dynamic">
1409
+ <mat-label>Target field</mat-label>
1410
+ <input
1411
+ matInput
1412
+ [ngModel]="mapping.target"
1413
+ (ngModelChange)="store.updateAutoPatchMapping(f.id, $index, { target: $event })"
1414
+ />
1415
+ </mat-form-field>
1416
+ <button
1417
+ mat-icon-button
1418
+ type="button"
1419
+ color="warn"
1420
+ matTooltip="Remove mapping"
1421
+ (click)="store.removeAutoPatchMapping(f.id, $index)"
1422
+ >
1423
+ <mat-icon>delete</mat-icon>
1424
+ </button>
1425
+ </div>
1426
+ }
1427
+ } @else {
1428
+ <p class="deb-hint">
1429
+ No auto-patch configured — selecting a reference will not copy any values.
1430
+ </p>
1431
+ }
1432
+ </div>
1433
+ }
1354
1434
  `, 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
1435
  }] });
1356
1436
 
@@ -1587,166 +1667,166 @@ class FieldRulesListComponent {
1587
1667
  return `${rule.fieldId} ${cond} → ${action}`;
1588
1668
  }
1589
1669
  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>
1670
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: FieldRulesListComponent, isStandalone: true, selector: "ngx-field-rules-list", ngImport: i0, template: `
1671
+ <div class="deb-rules">
1672
+ <div class="deb-rules__head">
1673
+ <span class="deb-section-title">Rules</span>
1674
+ <button
1675
+ mat-stroked-button
1676
+ type="button"
1677
+ data-testid="add-rule"
1678
+ [disabled]="!store.selectedFieldId()"
1679
+ (click)="startCreate()"
1680
+ >
1681
+ <mat-icon>add</mat-icon> Rule
1682
+ </button>
1683
+ </div>
1684
+
1685
+ @if (editing(); as draft) {
1686
+ <ngx-rule-form [rule]="draft" (save)="commit($event)" (cancel)="editing.set(null)" />
1687
+ }
1688
+
1689
+ @if (store.rulesForSelectedField().length === 0 && !editing()) {
1690
+ <p class="deb-hint" data-testid="rules-empty">No rules on this field.</p>
1691
+ }
1692
+
1693
+ @for (rule of store.rulesForSelectedField(); track rule.id) {
1694
+ <div class="deb-rule-item" [class.deb-rule-item--off]="!rule.enabled" data-testid="rule-item">
1695
+ <div class="deb-rule-item__body">
1696
+ <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
1697
+ <span class="deb-rule-item__meta">
1698
+ priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
1699
+ </span>
1700
+ </div>
1701
+ <div class="deb-rule-item__actions">
1702
+ <mat-slide-toggle
1703
+ [checked]="rule.enabled"
1704
+ matTooltip="Enable / disable"
1705
+ (change)="store.toggleRule(rule.id!, $event.checked)"
1706
+ />
1707
+ <button
1708
+ mat-icon-button
1709
+ type="button"
1710
+ matTooltip="Move up"
1711
+ [attr.data-testid]="'rule-up-' + rule.id"
1712
+ (click)="store.moveRule(rule.id!, -1)"
1713
+ >
1714
+ <mat-icon>arrow_upward</mat-icon>
1715
+ </button>
1716
+ <button
1717
+ mat-icon-button
1718
+ type="button"
1719
+ matTooltip="Move down"
1720
+ [attr.data-testid]="'rule-down-' + rule.id"
1721
+ (click)="store.moveRule(rule.id!, 1)"
1722
+ >
1723
+ <mat-icon>arrow_downward</mat-icon>
1724
+ </button>
1725
+ <button
1726
+ mat-icon-button
1727
+ type="button"
1728
+ matTooltip="Edit rule"
1729
+ [attr.data-testid]="'rule-edit-' + rule.id"
1730
+ (click)="startEdit(rule)"
1731
+ >
1732
+ <mat-icon>edit</mat-icon>
1733
+ </button>
1734
+ <button
1735
+ mat-icon-button
1736
+ type="button"
1737
+ color="warn"
1738
+ matTooltip="Delete rule"
1739
+ [attr.data-testid]="'rule-delete-' + rule.id"
1740
+ (click)="store.removeRule(rule.id!)"
1741
+ >
1742
+ <mat-icon>delete</mat-icon>
1743
+ </button>
1744
+ </div>
1745
+ </div>
1746
+ }
1747
+ </div>
1668
1748
  `, 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
1749
  }
1670
1750
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FieldRulesListComponent, decorators: [{
1671
1751
  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>
1752
+ args: [{ selector: 'ngx-field-rules-list', standalone: true, imports: [MatButtonModule, MatIconModule, MatSlideToggleModule, MatTooltipModule, RuleFormComponent], template: `
1753
+ <div class="deb-rules">
1754
+ <div class="deb-rules__head">
1755
+ <span class="deb-section-title">Rules</span>
1756
+ <button
1757
+ mat-stroked-button
1758
+ type="button"
1759
+ data-testid="add-rule"
1760
+ [disabled]="!store.selectedFieldId()"
1761
+ (click)="startCreate()"
1762
+ >
1763
+ <mat-icon>add</mat-icon> Rule
1764
+ </button>
1765
+ </div>
1766
+
1767
+ @if (editing(); as draft) {
1768
+ <ngx-rule-form [rule]="draft" (save)="commit($event)" (cancel)="editing.set(null)" />
1769
+ }
1770
+
1771
+ @if (store.rulesForSelectedField().length === 0 && !editing()) {
1772
+ <p class="deb-hint" data-testid="rules-empty">No rules on this field.</p>
1773
+ }
1774
+
1775
+ @for (rule of store.rulesForSelectedField(); track rule.id) {
1776
+ <div class="deb-rule-item" [class.deb-rule-item--off]="!rule.enabled" data-testid="rule-item">
1777
+ <div class="deb-rule-item__body">
1778
+ <span class="deb-rule-item__summary">{{ summarize(rule) }}</span>
1779
+ <span class="deb-rule-item__meta">
1780
+ priority {{ rule.priority }} · {{ rule.targets.length }} target(s)
1781
+ </span>
1782
+ </div>
1783
+ <div class="deb-rule-item__actions">
1784
+ <mat-slide-toggle
1785
+ [checked]="rule.enabled"
1786
+ matTooltip="Enable / disable"
1787
+ (change)="store.toggleRule(rule.id!, $event.checked)"
1788
+ />
1789
+ <button
1790
+ mat-icon-button
1791
+ type="button"
1792
+ matTooltip="Move up"
1793
+ [attr.data-testid]="'rule-up-' + rule.id"
1794
+ (click)="store.moveRule(rule.id!, -1)"
1795
+ >
1796
+ <mat-icon>arrow_upward</mat-icon>
1797
+ </button>
1798
+ <button
1799
+ mat-icon-button
1800
+ type="button"
1801
+ matTooltip="Move down"
1802
+ [attr.data-testid]="'rule-down-' + rule.id"
1803
+ (click)="store.moveRule(rule.id!, 1)"
1804
+ >
1805
+ <mat-icon>arrow_downward</mat-icon>
1806
+ </button>
1807
+ <button
1808
+ mat-icon-button
1809
+ type="button"
1810
+ matTooltip="Edit rule"
1811
+ [attr.data-testid]="'rule-edit-' + rule.id"
1812
+ (click)="startEdit(rule)"
1813
+ >
1814
+ <mat-icon>edit</mat-icon>
1815
+ </button>
1816
+ <button
1817
+ mat-icon-button
1818
+ type="button"
1819
+ color="warn"
1820
+ matTooltip="Delete rule"
1821
+ [attr.data-testid]="'rule-delete-' + rule.id"
1822
+ (click)="store.removeRule(rule.id!)"
1823
+ >
1824
+ <mat-icon>delete</mat-icon>
1825
+ </button>
1826
+ </div>
1827
+ </div>
1828
+ }
1829
+ </div>
1750
1830
  `, 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
1831
  }] });
1752
1832
 
@@ -1783,8 +1863,14 @@ class ReferencedFieldConfigComponent {
1783
1863
  this.store.syncReferencedField(field.id, sourceField);
1784
1864
  }
1785
1865
  }
1866
+ /**
1867
+ * Both callers pass the id of the field they just edited. This used to ignore that
1868
+ * argument and read `store.selectedField()` instead, so whenever the edited field was not
1869
+ * the selected one it checked drift against the wrong field's source entity — or bailed
1870
+ * out entirely if nothing was selected.
1871
+ */
1786
1872
  async checkDriftForField(fieldId) {
1787
- const f = this.store.selectedField();
1873
+ const f = this.store.fields().find(field => field.id === fieldId);
1788
1874
  if (!f || !f.isReferenced || !f.referencedEntityKey || !f.referencedFieldId || !this.configSource)
1789
1875
  return;
1790
1876
  const sourceConfig = await this.configSource.getConfig(f.referencedEntityKey);
@@ -2049,7 +2135,7 @@ class FieldInspectorComponent {
2049
2135
  return String(value);
2050
2136
  }
2051
2137
  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" }] });
2138
+ 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
2139
  }
2054
2140
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FieldInspectorComponent, decorators: [{
2055
2141
  type: Component,
@@ -2067,7 +2153,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2067
2153
  EntityReferenceConfigComponent,
2068
2154
  FieldRulesListComponent,
2069
2155
  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"] }]
2156
+ ], 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
2157
  }] });
2072
2158
 
2073
2159
  /**
@@ -2077,48 +2163,47 @@ class FieldPaletteComponent {
2077
2163
  catalog = FIELD_TYPE_CATALOG;
2078
2164
  pick = new EventEmitter();
2079
2165
  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>
2166
+ 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: `
2167
+ <div class="deb-palette">
2168
+ @for (meta of catalog; track meta.type) {
2169
+ <button
2170
+ mat-stroked-button
2171
+ type="button"
2172
+ class="deb-palette__item"
2173
+ [attr.data-testid]="'palette-' + meta.type"
2174
+ [matTooltip]="meta.description"
2175
+ (click)="pick.emit(meta.type)"
2176
+ >
2177
+ <mat-icon>{{ meta.icon }}</mat-icon>
2178
+ <span>{{ meta.label }}</span>
2179
+ </button>
2180
+ }
2181
+ </div>
2096
2182
  `, 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
2183
  }
2098
2184
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: FieldPaletteComponent, decorators: [{
2099
2185
  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>
2186
+ args: [{ selector: 'ngx-field-palette', standalone: true, imports: [MatButtonModule, MatIconModule, MatTooltipModule], template: `
2187
+ <div class="deb-palette">
2188
+ @for (meta of catalog; track meta.type) {
2189
+ <button
2190
+ mat-stroked-button
2191
+ type="button"
2192
+ class="deb-palette__item"
2193
+ [attr.data-testid]="'palette-' + meta.type"
2194
+ [matTooltip]="meta.description"
2195
+ (click)="pick.emit(meta.type)"
2196
+ >
2197
+ <mat-icon>{{ meta.icon }}</mat-icon>
2198
+ <span>{{ meta.label }}</span>
2199
+ </button>
2200
+ }
2201
+ </div>
2116
2202
  `, 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
2203
  }], propDecorators: { pick: [{
2118
2204
  type: Output
2119
2205
  }] } });
2120
2206
 
2121
- const SYSTEM_DEFAULT_CAN_EDIT = new InjectionToken('SYSTEM_DEFAULT_CAN_EDIT');
2122
2207
  /**
2123
2208
  * TabManagerComponent — add / rename / reorder / remove tabs & sub-tabs with systemDefault protection.
2124
2209
  */
@@ -2135,153 +2220,156 @@ class TabManagerComponent {
2135
2220
  if (!tab.systemDefault)
2136
2221
  return true;
2137
2222
  if (this.canEditSystemDefaultsFn) {
2138
- return this.canEditSystemDefaultsFn([]);
2223
+ // The predicate's entire contract is (roles) => boolean. This passed a hardcoded []
2224
+ // regardless of who was using the builder, so any predicate that actually inspected
2225
+ // roles answered false for everyone and locked every system-default tab.
2226
+ return this.canEditSystemDefaultsFn(this.store.userRoles());
2139
2227
  }
2140
2228
  return true;
2141
2229
  }
2142
2230
  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>
2231
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
2232
+ <div class="deb-tabs">
2233
+ <div class="deb-tabs__head">
2234
+ <span class="deb-section-title">Tabs Manager</span>
2235
+ <button mat-stroked-button type="button" (click)="store.addTab()">
2236
+ <mat-icon>add</mat-icon> Add Tab
2237
+ </button>
2238
+ </div>
2239
+
2240
+ @if (store.tabs().length === 0) {
2241
+ <p class="deb-hint">No tabs — all fields render in a single section.</p>
2242
+ }
2243
+
2244
+ @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
2245
+ <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
2246
+ <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
2247
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2248
+ <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
2249
+ <input
2250
+ matInput
2251
+ [disabled]="!canEditTab(tab)"
2252
+ [ngModel]="tabLabel(tab)"
2253
+ (ngModelChange)="store.setTabLabel(tab.id, lang(), $event)"
2254
+ />
2255
+ </mat-form-field>
2256
+ <button
2257
+ mat-icon-button
2258
+ type="button"
2259
+ [disabled]="i === 0 || !canEditTab(tab)"
2260
+ (click)="store.moveTab(tab.id, -1)"
2261
+ matTooltip="Move up"
2262
+ >
2263
+ <mat-icon>arrow_upward</mat-icon>
2264
+ </button>
2265
+ <button
2266
+ mat-icon-button
2267
+ type="button"
2268
+ [disabled]="i === count - 1 || !canEditTab(tab)"
2269
+ (click)="store.moveTab(tab.id, 1)"
2270
+ matTooltip="Move down"
2271
+ >
2272
+ <mat-icon>arrow_downward</mat-icon>
2273
+ </button>
2274
+ <button
2275
+ mat-icon-button
2276
+ type="button"
2277
+ color="warn"
2278
+ [disabled]="!canEditTab(tab)"
2279
+ (click)="store.removeTab(tab.id)"
2280
+ matTooltip="Remove tab"
2281
+ >
2282
+ <mat-icon>delete</mat-icon>
2283
+ </button>
2284
+ </div>
2285
+
2286
+ <!-- Tab settings row -->
2287
+ <div class="deb-tab-settings">
2288
+ <mat-checkbox
2289
+ [disabled]="!canEditTab(tab)"
2290
+ [ngModel]="tab.flatData"
2291
+ (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
2292
+ >
2293
+ Flat Data
2294
+ </mat-checkbox>
2295
+ <mat-checkbox
2296
+ [disabled]="!canEditTab(tab)"
2297
+ [ngModel]="tab.isPrimaryTab"
2298
+ (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
2299
+ >
2300
+ Primary Tab
2301
+ </mat-checkbox>
2302
+ <mat-checkbox
2303
+ [disabled]="!canEditTab(tab)"
2304
+ [ngModel]="tab.maskData"
2305
+ (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
2306
+ >
2307
+ Mask Tab Data
2308
+ </mat-checkbox>
2309
+ <mat-checkbox
2310
+ [disabled]="!canEditTab(tab)"
2311
+ [ngModel]="tab.systemDefault"
2312
+ (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
2313
+ >
2314
+ System Default
2315
+ </mat-checkbox>
2316
+ <button
2317
+ mat-button
2318
+ type="button"
2319
+ color="primary"
2320
+ [disabled]="!canEditTab(tab)"
2321
+ (click)="store.addSubTab(tab.id)"
2322
+ >
2323
+ + Sub-tab
2324
+ </button>
2325
+ </div>
2326
+
2327
+ <!-- Module tab configuration -->
2328
+ <div class="deb-tab-module">
2329
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2330
+ <mat-label>Consumer Module Name (Optional)</mat-label>
2331
+ <input
2332
+ matInput
2333
+ placeholder="e.g. documents-view"
2334
+ [disabled]="!canEditTab(tab)"
2335
+ [ngModel]="tab.moduleName"
2336
+ (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
2337
+ />
2338
+ </mat-form-field>
2339
+ </div>
2340
+
2341
+ <!-- Sub-tabs recursive render (Level 2) -->
2342
+ @if (tab.children && tab.children.length > 0) {
2343
+ <div class="deb-subtabs">
2344
+ <span class="deb-subtabs__title">Sub-tabs:</span>
2345
+ @for (sub of tab.children; track sub.id) {
2346
+ <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
2347
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2348
+ <mat-label>{{ sub.id }}</mat-label>
2349
+ <input
2350
+ matInput
2351
+ [disabled]="!canEditTab(sub)"
2352
+ [ngModel]="tabLabel(sub)"
2353
+ (ngModelChange)="store.setTabLabel(sub.id, lang(), $event)"
2354
+ />
2355
+ </mat-form-field>
2356
+ <button
2357
+ mat-icon-button
2358
+ type="button"
2359
+ color="warn"
2360
+ [disabled]="!canEditTab(sub)"
2361
+ (click)="store.removeTab(sub.id)"
2362
+ matTooltip="Remove sub-tab"
2363
+ >
2364
+ <mat-icon>delete</mat-icon>
2365
+ </button>
2366
+ </div>
2367
+ }
2368
+ </div>
2369
+ }
2370
+ </div>
2371
+ }
2372
+ </div>
2285
2373
  `, 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
2374
  }
2287
2375
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: TabManagerComponent, decorators: [{
@@ -2295,148 +2383,148 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2295
2383
  MatInputModule,
2296
2384
  MatSlideToggleModule,
2297
2385
  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>
2386
+ ], template: `
2387
+ <div class="deb-tabs">
2388
+ <div class="deb-tabs__head">
2389
+ <span class="deb-section-title">Tabs Manager</span>
2390
+ <button mat-stroked-button type="button" (click)="store.addTab()">
2391
+ <mat-icon>add</mat-icon> Add Tab
2392
+ </button>
2393
+ </div>
2394
+
2395
+ @if (store.tabs().length === 0) {
2396
+ <p class="deb-hint">No tabs — all fields render in a single section.</p>
2397
+ }
2398
+
2399
+ @for (tab of store.tabs(); track tab.id; let i = $index, count = $count) {
2400
+ <div class="deb-tab-card" [class.deb-tab-card--system]="tab.systemDefault">
2401
+ <div class="deb-tabs__row" [attr.data-testid]="'tab-row-' + tab.id">
2402
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2403
+ <mat-label>{{ tab.id }} {{ tab.systemDefault ? '(System)' : '' }}</mat-label>
2404
+ <input
2405
+ matInput
2406
+ [disabled]="!canEditTab(tab)"
2407
+ [ngModel]="tabLabel(tab)"
2408
+ (ngModelChange)="store.setTabLabel(tab.id, lang(), $event)"
2409
+ />
2410
+ </mat-form-field>
2411
+ <button
2412
+ mat-icon-button
2413
+ type="button"
2414
+ [disabled]="i === 0 || !canEditTab(tab)"
2415
+ (click)="store.moveTab(tab.id, -1)"
2416
+ matTooltip="Move up"
2417
+ >
2418
+ <mat-icon>arrow_upward</mat-icon>
2419
+ </button>
2420
+ <button
2421
+ mat-icon-button
2422
+ type="button"
2423
+ [disabled]="i === count - 1 || !canEditTab(tab)"
2424
+ (click)="store.moveTab(tab.id, 1)"
2425
+ matTooltip="Move down"
2426
+ >
2427
+ <mat-icon>arrow_downward</mat-icon>
2428
+ </button>
2429
+ <button
2430
+ mat-icon-button
2431
+ type="button"
2432
+ color="warn"
2433
+ [disabled]="!canEditTab(tab)"
2434
+ (click)="store.removeTab(tab.id)"
2435
+ matTooltip="Remove tab"
2436
+ >
2437
+ <mat-icon>delete</mat-icon>
2438
+ </button>
2439
+ </div>
2440
+
2441
+ <!-- Tab settings row -->
2442
+ <div class="deb-tab-settings">
2443
+ <mat-checkbox
2444
+ [disabled]="!canEditTab(tab)"
2445
+ [ngModel]="tab.flatData"
2446
+ (ngModelChange)="store.updateTab(tab.id, { flatData: $event })"
2447
+ >
2448
+ Flat Data
2449
+ </mat-checkbox>
2450
+ <mat-checkbox
2451
+ [disabled]="!canEditTab(tab)"
2452
+ [ngModel]="tab.isPrimaryTab"
2453
+ (ngModelChange)="$event ? store.setPrimaryTab(tab.id) : store.updateTab(tab.id, { isPrimaryTab: false })"
2454
+ >
2455
+ Primary Tab
2456
+ </mat-checkbox>
2457
+ <mat-checkbox
2458
+ [disabled]="!canEditTab(tab)"
2459
+ [ngModel]="tab.maskData"
2460
+ (ngModelChange)="store.updateTab(tab.id, { maskData: $event })"
2461
+ >
2462
+ Mask Tab Data
2463
+ </mat-checkbox>
2464
+ <mat-checkbox
2465
+ [disabled]="!canEditTab(tab)"
2466
+ [ngModel]="tab.systemDefault"
2467
+ (ngModelChange)="store.updateTab(tab.id, { systemDefault: $event })"
2468
+ >
2469
+ System Default
2470
+ </mat-checkbox>
2471
+ <button
2472
+ mat-button
2473
+ type="button"
2474
+ color="primary"
2475
+ [disabled]="!canEditTab(tab)"
2476
+ (click)="store.addSubTab(tab.id)"
2477
+ >
2478
+ + Sub-tab
2479
+ </button>
2480
+ </div>
2481
+
2482
+ <!-- Module tab configuration -->
2483
+ <div class="deb-tab-module">
2484
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2485
+ <mat-label>Consumer Module Name (Optional)</mat-label>
2486
+ <input
2487
+ matInput
2488
+ placeholder="e.g. documents-view"
2489
+ [disabled]="!canEditTab(tab)"
2490
+ [ngModel]="tab.moduleName"
2491
+ (ngModelChange)="store.updateTab(tab.id, { moduleName: $event || undefined })"
2492
+ />
2493
+ </mat-form-field>
2494
+ </div>
2495
+
2496
+ <!-- Sub-tabs recursive render (Level 2) -->
2497
+ @if (tab.children && tab.children.length > 0) {
2498
+ <div class="deb-subtabs">
2499
+ <span class="deb-subtabs__title">Sub-tabs:</span>
2500
+ @for (sub of tab.children; track sub.id) {
2501
+ <div class="deb-tabs__row deb-subtab-row" [attr.data-testid]="'subtab-row-' + sub.id">
2502
+ <mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-grow">
2503
+ <mat-label>{{ sub.id }}</mat-label>
2504
+ <input
2505
+ matInput
2506
+ [disabled]="!canEditTab(sub)"
2507
+ [ngModel]="tabLabel(sub)"
2508
+ (ngModelChange)="store.setTabLabel(sub.id, lang(), $event)"
2509
+ />
2510
+ </mat-form-field>
2511
+ <button
2512
+ mat-icon-button
2513
+ type="button"
2514
+ color="warn"
2515
+ [disabled]="!canEditTab(sub)"
2516
+ (click)="store.removeTab(sub.id)"
2517
+ matTooltip="Remove sub-tab"
2518
+ >
2519
+ <mat-icon>delete</mat-icon>
2520
+ </button>
2521
+ </div>
2522
+ }
2523
+ </div>
2524
+ }
2525
+ </div>
2526
+ }
2527
+ </div>
2440
2528
  `, 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
2529
  }] });
2442
2530
 
@@ -2686,6 +2774,12 @@ class EntityBuilderComponent {
2686
2774
  languages = ['en'];
2687
2775
  /** Optional role list — enables multi-select role pickers for RBAC instead of free text. */
2688
2776
  availableRoles = [];
2777
+ /**
2778
+ * Roles of the person using the builder. Distinct from `availableRoles`, which is the
2779
+ * vocabulary a schema may reference. Passed to the `SYSTEM_DEFAULT_CAN_EDIT` predicate so
2780
+ * it can decide whether this user may edit system-default tabs.
2781
+ */
2782
+ userRoles = [];
2689
2783
  /** Optional common-module definitions for module tabs; defaults to the built-in list from core. */
2690
2784
  commonModules = COMMON_MODULES;
2691
2785
  /** Emitted on every change to the working config. */
@@ -2708,6 +2802,9 @@ class EntityBuilderComponent {
2708
2802
  if (!this.languages.includes(active))
2709
2803
  this.store.setActiveLanguage(this.languages[0]);
2710
2804
  }
2805
+ if (changes['userRoles']) {
2806
+ this.store.setUserRoles(this.userRoles);
2807
+ }
2711
2808
  }
2712
2809
  // ─── Field canvas ─────────────────────────────────────────────────────────
2713
2810
  onDrop(event) {
@@ -2753,7 +2850,7 @@ class EntityBuilderComponent {
2753
2850
  this.store.setPermission(action, roles);
2754
2851
  }
2755
2852
  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" }] });
2853
+ 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 <button mat-flat-button color=\"accent\" 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:#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" }] });
2757
2854
  }
2758
2855
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: EntityBuilderComponent, decorators: [{
2759
2856
  type: Component,
@@ -2775,13 +2872,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2775
2872
  TabManagerComponent,
2776
2873
  FieldInspectorComponent,
2777
2874
  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"] }]
2875
+ ], 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 <button mat-flat-button color=\"accent\" 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:#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"] }]
2779
2876
  }], ctorParameters: () => [], propDecorators: { config: [{
2780
2877
  type: Input
2781
2878
  }], languages: [{
2782
2879
  type: Input
2783
2880
  }], availableRoles: [{
2784
2881
  type: Input
2882
+ }], userRoles: [{
2883
+ type: Input
2785
2884
  }], commonModules: [{
2786
2885
  type: Input
2787
2886
  }], configChange: [{
@@ -2790,121 +2889,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
2790
2889
  type: Output
2791
2890
  }] } });
2792
2891
 
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
2892
  // Main component
2904
2893
 
2905
2894
  /**
2906
2895
  * Generated bundle index. Do not edit.
2907
2896
  */
2908
2897
 
2909
- export { BuilderStore, ConnectionSourceConfigComponent, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleFormComponent, SYSTEM_DEFAULT_CAN_EDIT, TabManagerComponent };
2898
+ export { BuilderStore, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleFormComponent, TabManagerComponent };
2910
2899
  //# sourceMappingURL=ngx-dynamic-entity-builder.mjs.map