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