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