ngx-dynamic-entity-builder 2.0.2 → 2.1.1
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.
|
@@ -8,11 +8,11 @@ import * as i3$3 from '@angular/material/card';
|
|
|
8
8
|
import { MatCardModule } from '@angular/material/card';
|
|
9
9
|
import * as i3$2 from '@angular/material/checkbox';
|
|
10
10
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
11
|
-
import * as i4$
|
|
11
|
+
import * as i4$3 from '@angular/material/expansion';
|
|
12
12
|
import { MatExpansionModule } from '@angular/material/expansion';
|
|
13
13
|
import * as i4 from '@angular/material/form-field';
|
|
14
14
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
15
|
-
import * as
|
|
15
|
+
import * as i4$1 from '@angular/material/icon';
|
|
16
16
|
import { MatIconModule } from '@angular/material/icon';
|
|
17
17
|
import * as i6 from '@angular/material/input';
|
|
18
18
|
import { MatInputModule } from '@angular/material/input';
|
|
@@ -24,7 +24,7 @@ import * as i3$1 from '@angular/material/slide-toggle';
|
|
|
24
24
|
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
|
25
25
|
import * as i11 from '@angular/material/toolbar';
|
|
26
26
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
|
27
|
-
import * as i4$
|
|
27
|
+
import * as i4$2 from '@angular/material/tooltip';
|
|
28
28
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
29
29
|
import { parseFieldRef, toRefToken, collectFieldScopes, assignFieldRefs, normalizeConfigOptions, getFieldTypeMeta, createFieldConfig, labelToId, createFieldSnapshot, computeFieldDrift, OPTION_KEY, optionKeyOf, resolveLabel, humanizeId, findTab, fieldRefFor, refOf, resolveOptionLabel, FIELD_TYPE_CATALOG, COMMON_MODULES } from '@dynamic-entity/core';
|
|
30
30
|
export { FIELD_TYPE_CATALOG, createFieldConfig, getFieldTypeMeta, humanizeId } from '@dynamic-entity/core';
|
|
@@ -35,6 +35,7 @@ import { MatDividerModule } from '@angular/material/divider';
|
|
|
35
35
|
import { CommonModule } from '@angular/common';
|
|
36
36
|
import * as i1$1 from '@angular/cdk/drag-drop';
|
|
37
37
|
import { DragDropModule } from '@angular/cdk/drag-drop';
|
|
38
|
+
import { MatChipsModule } from '@angular/material/chips';
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* field-catalog.ts — re-exports shared field catalog from @dynamic-entity/core.
|
|
@@ -55,6 +56,95 @@ function deepClone(value) {
|
|
|
55
56
|
return JSON.parse(JSON.stringify(value));
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Bounded undo/redo over immutable states, with keystroke coalescing.
|
|
61
|
+
*
|
|
62
|
+
* Entries hold **references, not clones**. The store builds a new state on every edit and
|
|
63
|
+
* shares unchanged subtrees, so every entry already refers to immutable objects. Undo puts
|
|
64
|
+
* the stored object back, so a `record` that follows sees the reference already sitting at
|
|
65
|
+
* the cursor and skips it — no suppression flag to get out of step.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately free of Angular DI: signals need no injection context, so `new BuilderStore()`
|
|
68
|
+
* keeps working and a test can assert straight after an edit without flushing effects.
|
|
69
|
+
*/
|
|
70
|
+
class BuilderHistory {
|
|
71
|
+
shapeOf;
|
|
72
|
+
same;
|
|
73
|
+
maxEntries;
|
|
74
|
+
coalesceMs;
|
|
75
|
+
entries = [];
|
|
76
|
+
cursor = signal(-1, ...(ngDevMode ? [{ debugName: "cursor" }] : /* istanbul ignore next */ []));
|
|
77
|
+
length = signal(0, ...(ngDevMode ? [{ debugName: "length" }] : /* istanbul ignore next */ []));
|
|
78
|
+
lastEditAt = 0;
|
|
79
|
+
canUndo = computed(() => this.cursor() > 0, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
|
|
80
|
+
canRedo = computed(() => this.cursor() < this.length() - 1, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
|
|
81
|
+
/**
|
|
82
|
+
* @param shapeOf Counts that distinguish a structural edit from a value edit. Two
|
|
83
|
+
* consecutive edits merge only when their shapes match.
|
|
84
|
+
* @param same Whether two states are the same state (reference identity, in practice).
|
|
85
|
+
*/
|
|
86
|
+
constructor(shapeOf, same, maxEntries = 200, coalesceMs = 400) {
|
|
87
|
+
this.shapeOf = shapeOf;
|
|
88
|
+
this.same = same;
|
|
89
|
+
this.maxEntries = maxEntries;
|
|
90
|
+
this.coalesceMs = coalesceMs;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Fold a state into history.
|
|
94
|
+
*
|
|
95
|
+
* Recording the same state twice is harmless. A label bound to a keystroke would otherwise
|
|
96
|
+
* make undo walk back one character at a time, so two consecutive edits merge when they
|
|
97
|
+
* land inside `coalesceMs` *and* the shape is unchanged — a rename coalesces, while adding,
|
|
98
|
+
* removing or moving anything always earns its own step however fast it is clicked.
|
|
99
|
+
*/
|
|
100
|
+
record(state) {
|
|
101
|
+
const at = this.cursor();
|
|
102
|
+
const top = at >= 0 ? this.entries[at] : undefined;
|
|
103
|
+
if (top && this.same(top.state, state))
|
|
104
|
+
return;
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const entry = { state, shape: this.shapeOf(state) };
|
|
107
|
+
if (top && now - this.lastEditAt < this.coalesceMs && top.shape === entry.shape && at === this.entries.length - 1) {
|
|
108
|
+
this.entries[at] = entry;
|
|
109
|
+
this.lastEditAt = now;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// A new edit after an undo discards the redo branch, which is what every editor does.
|
|
113
|
+
this.entries.length = at + 1;
|
|
114
|
+
this.entries.push(entry);
|
|
115
|
+
// Bounded: a builder session is long, and the oldest steps are the ones nobody walks back to.
|
|
116
|
+
if (this.entries.length > this.maxEntries) {
|
|
117
|
+
this.entries.splice(0, this.entries.length - this.maxEntries);
|
|
118
|
+
}
|
|
119
|
+
this.cursor.set(this.entries.length - 1);
|
|
120
|
+
this.length.set(this.entries.length);
|
|
121
|
+
this.lastEditAt = now;
|
|
122
|
+
}
|
|
123
|
+
/** Start again from `state`. Nothing before it is undoable. */
|
|
124
|
+
reset(state) {
|
|
125
|
+
this.entries.length = 0;
|
|
126
|
+
this.entries.push({ state, shape: this.shapeOf(state) });
|
|
127
|
+
this.cursor.set(0);
|
|
128
|
+
this.length.set(1);
|
|
129
|
+
this.lastEditAt = 0;
|
|
130
|
+
}
|
|
131
|
+
/** Step back, returning the state to restore, or `null` when there is none. */
|
|
132
|
+
undo() {
|
|
133
|
+
return this.canUndo() ? this.moveTo(this.cursor() - 1) : null;
|
|
134
|
+
}
|
|
135
|
+
/** Step forward, returning the state to restore, or `null` when there is none. */
|
|
136
|
+
redo() {
|
|
137
|
+
return this.canRedo() ? this.moveTo(this.cursor() + 1) : null;
|
|
138
|
+
}
|
|
139
|
+
moveTo(index) {
|
|
140
|
+
const entry = this.entries[index];
|
|
141
|
+
if (!entry)
|
|
142
|
+
return null;
|
|
143
|
+
this.cursor.set(index);
|
|
144
|
+
return entry.state;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
58
148
|
const clone = deepClone;
|
|
59
149
|
const ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
60
150
|
/**
|
|
@@ -65,6 +155,26 @@ const ID_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
65
155
|
* un-ticked. Nothing writes it any more.
|
|
66
156
|
*/
|
|
67
157
|
const LEGACY_EMAIL_PATTERN = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$';
|
|
158
|
+
/** Counts that distinguish a structural edit from a value edit. */
|
|
159
|
+
function shapeOf({ config, rules }) {
|
|
160
|
+
let fields = 0;
|
|
161
|
+
let tabs = 0;
|
|
162
|
+
const walkFields = (list) => {
|
|
163
|
+
for (const f of list ?? []) {
|
|
164
|
+
fields += 1;
|
|
165
|
+
walkFields(f.children);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const walkTabs = (list) => {
|
|
169
|
+
for (const t of list ?? []) {
|
|
170
|
+
tabs += 1;
|
|
171
|
+
walkFields(t.fields);
|
|
172
|
+
walkTabs(t.children);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
walkTabs(config.tabs);
|
|
176
|
+
return `${tabs}:${fields}:${rules.length}`;
|
|
177
|
+
}
|
|
68
178
|
/**
|
|
69
179
|
* Follow a field rename through a rule reference, in either spelling.
|
|
70
180
|
*
|
|
@@ -112,129 +222,34 @@ class BuilderStore {
|
|
|
112
222
|
/**
|
|
113
223
|
* Roles of the person using the builder — distinct from `availableRoles`, which is the
|
|
114
224
|
* vocabulary a schema may reference. Needed so the SYSTEM_DEFAULT_CAN_EDIT predicate can
|
|
115
|
-
* be asked a real question
|
|
116
|
-
* made any role-checking predicate answer false for everyone.
|
|
225
|
+
* be asked a real question rather than answering for an empty role list.
|
|
117
226
|
*/
|
|
118
227
|
_userRoles = signal([], ...(ngDevMode ? [{ debugName: "_userRoles" }] : /* istanbul ignore next */ []));
|
|
119
228
|
// ─── Undo / redo ────────────────────────────────────────────────────────────
|
|
120
229
|
//
|
|
121
|
-
// History
|
|
122
|
-
//
|
|
123
|
-
// exists.
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// re-entrancy problem for free: undo puts the stored object back, so the recording effect
|
|
129
|
-
// sees the exact reference already sitting at the cursor and skips it. No suppression flag
|
|
130
|
-
// to get out of step.
|
|
131
|
-
history = [];
|
|
132
|
-
cursor = signal(-1, ...(ngDevMode ? [{ debugName: "cursor" }] : /* istanbul ignore next */ []));
|
|
133
|
-
historyLength = signal(0, ...(ngDevMode ? [{ debugName: "historyLength" }] : /* istanbul ignore next */ []));
|
|
134
|
-
lastEditAt = 0;
|
|
135
|
-
/** Consecutive edits closer together than this fold into one undo step. */
|
|
136
|
-
static COALESCE_MS = 400;
|
|
137
|
-
canUndo = computed(() => this.cursor() > 0, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
|
|
138
|
-
canRedo = computed(() => this.cursor() < this.historyLength() - 1, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
|
|
139
|
-
/**
|
|
140
|
-
* Fold the current state into history.
|
|
141
|
-
*
|
|
142
|
-
* Called explicitly by the write paths rather than from an `effect`. An effect would have
|
|
143
|
-
* been fewer call sites, but it requires an injection context: `BuilderStore` had no
|
|
144
|
-
* constructor, so `new BuilderStore()` was legal, and adding one broke every caller that
|
|
145
|
-
* did it with NG0203. Explicit calls also make the timing deterministic — a test can
|
|
146
|
-
* assert straight after an edit instead of flushing effects first.
|
|
147
|
-
*
|
|
148
|
-
* Calling it twice for one operation is harmless: the second call sees the same signal
|
|
149
|
-
* references already at the cursor and returns.
|
|
150
|
-
*
|
|
151
|
-
* `setFieldLabel` is bound to a keystroke, so recording every emission would make undo
|
|
152
|
-
* walk back one character at a time. Two consecutive edits merge when they land inside
|
|
153
|
-
* `COALESCE_MS` *and* the structure is unchanged — a rename coalesces, while adding,
|
|
154
|
-
* removing or moving anything always earns its own step however fast it is clicked.
|
|
155
|
-
*/
|
|
230
|
+
// History stores {config, rules} pairs, because the builder's state is two signals and
|
|
231
|
+
// undoing one without the other would leave a rule pointing at a field that no longer
|
|
232
|
+
// exists. Write paths call `record()` explicitly rather than from an `effect`, which would
|
|
233
|
+
// need an injection context and break `new BuilderStore()`.
|
|
234
|
+
history = new BuilderHistory(shapeOf, (a, b) => a.config === b.config && a.rules === b.rules);
|
|
235
|
+
canUndo = this.history.canUndo;
|
|
236
|
+
canRedo = this.history.canRedo;
|
|
156
237
|
record() {
|
|
157
|
-
|
|
158
|
-
const rules = this._rules();
|
|
159
|
-
const at = this.cursor();
|
|
160
|
-
const top = at >= 0 ? this.history[at] : undefined;
|
|
161
|
-
if (top && top.config === config && top.rules === rules)
|
|
162
|
-
return;
|
|
163
|
-
const now = Date.now();
|
|
164
|
-
const snapshot = { config, rules, shape: this.shapeOf(config, rules) };
|
|
165
|
-
if (top &&
|
|
166
|
-
now - this.lastEditAt < BuilderStore.COALESCE_MS &&
|
|
167
|
-
top.shape === snapshot.shape &&
|
|
168
|
-
at === this.history.length - 1) {
|
|
169
|
-
this.history[at] = snapshot;
|
|
170
|
-
this.lastEditAt = now;
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
// A new edit after an undo discards the redo branch, which is what every editor does.
|
|
174
|
-
this.history.length = at + 1;
|
|
175
|
-
this.history.push(snapshot);
|
|
176
|
-
// Bounded, because a builder session is long and every keystroke that does not coalesce
|
|
177
|
-
// earns an entry. Snapshots hold references and share unchanged subtrees, so an entry is
|
|
178
|
-
// cheap — but unbounded is unbounded, and the oldest steps are the ones nobody walks
|
|
179
|
-
// back to. Dropping from the front keeps the most recent 200.
|
|
180
|
-
if (this.history.length > BuilderStore.MAX_HISTORY) {
|
|
181
|
-
this.history.splice(0, this.history.length - BuilderStore.MAX_HISTORY);
|
|
182
|
-
}
|
|
183
|
-
this.cursor.set(this.history.length - 1);
|
|
184
|
-
this.historyLength.set(this.history.length);
|
|
185
|
-
this.lastEditAt = now;
|
|
186
|
-
}
|
|
187
|
-
/** How many undo steps are kept. */
|
|
188
|
-
static MAX_HISTORY = 200;
|
|
189
|
-
/** Counts that distinguish a structural edit from a value edit. */
|
|
190
|
-
shapeOf(config, rules) {
|
|
191
|
-
let fields = 0;
|
|
192
|
-
let tabs = 0;
|
|
193
|
-
const walkFields = (list) => {
|
|
194
|
-
for (const f of list ?? []) {
|
|
195
|
-
fields += 1;
|
|
196
|
-
walkFields(f.children);
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
const walkTabs = (list) => {
|
|
200
|
-
for (const t of list ?? []) {
|
|
201
|
-
tabs += 1;
|
|
202
|
-
walkFields(t.fields);
|
|
203
|
-
walkTabs(t.children);
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
walkTabs(config.tabs);
|
|
207
|
-
return `${tabs}:${fields}:${rules.length}`;
|
|
238
|
+
this.history.record({ config: this._config(), rules: this._rules() });
|
|
208
239
|
}
|
|
209
240
|
/** Start history again from the state just loaded. Nothing before it is undoable. */
|
|
210
241
|
resetHistory() {
|
|
211
|
-
this.history.
|
|
212
|
-
this.history.push({
|
|
213
|
-
config: this._config(),
|
|
214
|
-
rules: this._rules(),
|
|
215
|
-
shape: this.shapeOf(this._config(), this._rules()),
|
|
216
|
-
});
|
|
217
|
-
this.cursor.set(0);
|
|
218
|
-
this.historyLength.set(1);
|
|
219
|
-
this.lastEditAt = 0;
|
|
242
|
+
this.history.reset({ config: this._config(), rules: this._rules() });
|
|
220
243
|
}
|
|
221
244
|
undo() {
|
|
222
|
-
|
|
223
|
-
return;
|
|
224
|
-
this.applyHistory(this.cursor() - 1);
|
|
245
|
+
this.restore(this.history.undo());
|
|
225
246
|
}
|
|
226
247
|
redo() {
|
|
227
|
-
|
|
228
|
-
return;
|
|
229
|
-
this.applyHistory(this.cursor() + 1);
|
|
248
|
+
this.restore(this.history.redo());
|
|
230
249
|
}
|
|
231
|
-
|
|
232
|
-
const snapshot = this.history[index];
|
|
250
|
+
restore(snapshot) {
|
|
233
251
|
if (!snapshot)
|
|
234
252
|
return;
|
|
235
|
-
this.cursor.set(index);
|
|
236
|
-
// Order matters only in that both land before the effect runs; it then sees references
|
|
237
|
-
// identical to this entry and records nothing.
|
|
238
253
|
this._config.set(snapshot.config);
|
|
239
254
|
this._rules.set(snapshot.rules);
|
|
240
255
|
this._isDirty.set(true);
|
|
@@ -254,11 +269,8 @@ class BuilderStore {
|
|
|
254
269
|
/**
|
|
255
270
|
* Every field the config declares, sub-tabs included.
|
|
256
271
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* picker could not offer a nested field as a source, and the drift check looked a nested
|
|
260
|
-
* field up here, got nothing, and returned without checking. The store's structural
|
|
261
|
-
* operations already walked the whole tree — only this view did not.
|
|
272
|
+
* The canvas, the entity-reference source picker and the drift check all read this, so it
|
|
273
|
+
* has to reach nested tabs just as the store's structural operations do.
|
|
262
274
|
*
|
|
263
275
|
* It deliberately does not descend into a field's own `children`: `group` and `array`
|
|
264
276
|
* children are rendered by the row that owns them, not as rows of their own.
|
|
@@ -388,10 +400,9 @@ class BuilderStore {
|
|
|
388
400
|
/**
|
|
389
401
|
* Every tab in the tree, flattened.
|
|
390
402
|
*
|
|
391
|
-
* The structural operations below
|
|
392
|
-
* the top level
|
|
393
|
-
* duplicated, moved or reordered
|
|
394
|
-
* unable to restructure, which is the worst possible state for an authoring tool.
|
|
403
|
+
* The structural operations below go through this rather than `draft.tabs`, which is only
|
|
404
|
+
* the top level — otherwise a field on a sub-tab could be edited but never removed,
|
|
405
|
+
* duplicated, moved or reordered.
|
|
395
406
|
*/
|
|
396
407
|
flattenTabs(tabs = []) {
|
|
397
408
|
const list = [];
|
|
@@ -532,20 +543,16 @@ class BuilderStore {
|
|
|
532
543
|
*
|
|
533
544
|
* Ids are the wiring between fields — rules, cascades, patches and `showWhen` all address
|
|
534
545
|
* fields by id — so a rename that only touches `field.id` silently breaks that wiring.
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*/
|
|
538
|
-
/**
|
|
546
|
+
* Label-derived ids make renaming routine, so the references move with it.
|
|
547
|
+
*
|
|
539
548
|
* `key` addresses the field to rename; `oldId` is the bare id every *reference* to it
|
|
540
|
-
* carries. They
|
|
541
|
-
*
|
|
542
|
-
* and rules are all keyed by bare id and never match a path.
|
|
549
|
+
* carries. They are separate because resolving needs the path, while `showWhen`,
|
|
550
|
+
* `parentField`, `autoPatch` targets and rules are all keyed by bare id.
|
|
543
551
|
*/
|
|
544
552
|
applyRename(key, oldId, newId) {
|
|
545
553
|
// Selection is held as whatever the caller passed — `addField` stores a bare id while a
|
|
546
|
-
// canvas click stores a path — so
|
|
547
|
-
//
|
|
548
|
-
// fields the two keys resolve to instead.
|
|
554
|
+
// canvas click stores a path — so compare the fields the two keys resolve to, not the
|
|
555
|
+
// strings.
|
|
549
556
|
const selectedKey = this._selectedFieldId();
|
|
550
557
|
const tabsNow = this._config().tabs;
|
|
551
558
|
const wasSelected = !!selectedKey && this.findFieldInTabs(tabsNow, selectedKey) === this.findFieldInTabs(tabsNow, key);
|
|
@@ -846,6 +853,21 @@ class BuilderStore {
|
|
|
846
853
|
}
|
|
847
854
|
});
|
|
848
855
|
}
|
|
856
|
+
setPatternValidator(id, pattern) {
|
|
857
|
+
this.mutate(draft => {
|
|
858
|
+
const field = this.findFieldInTabs(draft.tabs, id);
|
|
859
|
+
if (!field)
|
|
860
|
+
return;
|
|
861
|
+
field.validators = field.validators ?? {};
|
|
862
|
+
const trimmed = pattern?.trim();
|
|
863
|
+
if (!trimmed) {
|
|
864
|
+
delete field.validators.pattern;
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
field.validators.pattern = trimmed;
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
849
871
|
getParamValidator(field, validator) {
|
|
850
872
|
const val = field.validators?.[validator];
|
|
851
873
|
return typeof val === 'number' ? val : null;
|
|
@@ -854,9 +876,8 @@ class BuilderStore {
|
|
|
854
876
|
if (validator === 'required')
|
|
855
877
|
return !!field.validators?.required;
|
|
856
878
|
if (validator === 'email') {
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
// old builder keeps showing Email as ticked.
|
|
879
|
+
// Only the legacy email regex counts, never any pattern: a custom regex must not tick
|
|
880
|
+
// the box. Recognising it keeps older configs showing Email as ticked.
|
|
860
881
|
return !!field.validators?.email || field.validators?.pattern === LEGACY_EMAIL_PATTERN;
|
|
861
882
|
}
|
|
862
883
|
return false;
|
|
@@ -925,7 +946,7 @@ class BuilderStore {
|
|
|
925
946
|
}
|
|
926
947
|
});
|
|
927
948
|
}
|
|
928
|
-
/** Link a field to a field defined in another entity configuration
|
|
949
|
+
/** Link a field to a field defined in another entity configuration */
|
|
929
950
|
linkReferencedField(fieldId, sourceEntityKey, sourceField) {
|
|
930
951
|
this.mutate(draft => {
|
|
931
952
|
const field = this.findFieldInTabs(draft.tabs, fieldId);
|
|
@@ -1654,10 +1675,10 @@ class BuilderStore {
|
|
|
1654
1675
|
}
|
|
1655
1676
|
return problems;
|
|
1656
1677
|
}
|
|
1657
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1658
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1678
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1679
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderStore });
|
|
1659
1680
|
}
|
|
1660
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1681
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderStore, decorators: [{
|
|
1661
1682
|
type: Injectable
|
|
1662
1683
|
}] });
|
|
1663
1684
|
|
|
@@ -1699,6 +1720,177 @@ function withExistingOptions(options, current) {
|
|
|
1699
1720
|
return [...options, ...extra];
|
|
1700
1721
|
}
|
|
1701
1722
|
|
|
1723
|
+
/**
|
|
1724
|
+
* Computes all dependency edges (rules, showWhen, cascades, patchOnTrue, autoPatch)
|
|
1725
|
+
* across an EntityFormConfig and its rules.
|
|
1726
|
+
*/
|
|
1727
|
+
function computeRuleDependencies(config, rules = [], language = 'en') {
|
|
1728
|
+
const edges = [];
|
|
1729
|
+
let edgeIdSeq = 0;
|
|
1730
|
+
const scopes = collectFieldScopes(config);
|
|
1731
|
+
const byKey = new Map();
|
|
1732
|
+
const byId = new Map();
|
|
1733
|
+
const byToken = new Map();
|
|
1734
|
+
for (const entry of scopes) {
|
|
1735
|
+
const key = refOf(entry.field, entry.scope);
|
|
1736
|
+
const token = toRefToken(key);
|
|
1737
|
+
const label = resolveLabel(entry.field.label, language) || entry.field.id;
|
|
1738
|
+
const meta = { key, id: entry.field.id, label, field: entry.field };
|
|
1739
|
+
byKey.set(key, meta);
|
|
1740
|
+
byId.set(entry.field.id, meta);
|
|
1741
|
+
byToken.set(token, meta);
|
|
1742
|
+
}
|
|
1743
|
+
function resolveRef(refStr) {
|
|
1744
|
+
if (!refStr)
|
|
1745
|
+
return { key: '', label: '—', valid: false };
|
|
1746
|
+
const parsed = parseFieldRef(refStr);
|
|
1747
|
+
const val = parsed.value;
|
|
1748
|
+
const meta = byKey.get(refStr) ??
|
|
1749
|
+
byKey.get(val) ??
|
|
1750
|
+
byId.get(val) ??
|
|
1751
|
+
byToken.get(refStr) ??
|
|
1752
|
+
byToken.get(toRefToken(val));
|
|
1753
|
+
if (meta) {
|
|
1754
|
+
return { key: meta.key, label: meta.label, valid: true };
|
|
1755
|
+
}
|
|
1756
|
+
return { key: val || refStr, label: val || refStr, valid: false };
|
|
1757
|
+
}
|
|
1758
|
+
let totalRules = 0;
|
|
1759
|
+
let totalShowWhen = 0;
|
|
1760
|
+
let totalCascades = 0;
|
|
1761
|
+
let totalPatches = 0;
|
|
1762
|
+
// 1. Form Rules
|
|
1763
|
+
for (const rule of rules) {
|
|
1764
|
+
if (!rule.fieldId)
|
|
1765
|
+
continue;
|
|
1766
|
+
totalRules++;
|
|
1767
|
+
const source = resolveRef(rule.fieldId);
|
|
1768
|
+
const targets = rule.targets ?? [];
|
|
1769
|
+
const actionType = rule.action?.type ?? 'action';
|
|
1770
|
+
let actionSummary = actionType;
|
|
1771
|
+
if (actionType === 'visibility') {
|
|
1772
|
+
actionSummary = rule.action?.value === false || rule.action?.value === 'false' ? 'hide' : 'show';
|
|
1773
|
+
}
|
|
1774
|
+
else if (actionType === 'validation' || actionType === 'validationMessage') {
|
|
1775
|
+
actionSummary = 'validate';
|
|
1776
|
+
}
|
|
1777
|
+
else if (actionType === 'info' || actionType === 'infoBanner') {
|
|
1778
|
+
actionSummary = 'info';
|
|
1779
|
+
}
|
|
1780
|
+
const conditionsText = rule.conditions && rule.conditions.length > 0
|
|
1781
|
+
? rule.conditions.map(c => `${c.operator ?? 'EQUAL'} ${c.value ?? ''}`.trim()).join(' & ')
|
|
1782
|
+
: 'always';
|
|
1783
|
+
for (const target of targets) {
|
|
1784
|
+
const targetResolved = resolveRef(target.id);
|
|
1785
|
+
edges.push({
|
|
1786
|
+
id: `rule-${++edgeIdSeq}`,
|
|
1787
|
+
sourceKey: source.key,
|
|
1788
|
+
sourceLabel: source.label,
|
|
1789
|
+
targetKey: targetResolved.key,
|
|
1790
|
+
targetLabel: targetResolved.label,
|
|
1791
|
+
type: 'rule',
|
|
1792
|
+
actionSummary,
|
|
1793
|
+
description: `${actionSummary.toUpperCase()} when ${conditionsText}`,
|
|
1794
|
+
sourceValid: source.valid,
|
|
1795
|
+
targetValid: targetResolved.valid,
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
// 2. Field-level features: showWhen, cascade, patchOnTrue, autoPatch
|
|
1800
|
+
for (const entry of scopes) {
|
|
1801
|
+
const field = entry.field;
|
|
1802
|
+
const fieldKey = refOf(field, entry.scope);
|
|
1803
|
+
const fieldResolved = resolveRef(fieldKey);
|
|
1804
|
+
// showWhen
|
|
1805
|
+
if (field.showWhen && typeof field.showWhen === 'object') {
|
|
1806
|
+
for (const [watchedRef, expectedVal] of Object.entries(field.showWhen)) {
|
|
1807
|
+
totalShowWhen++;
|
|
1808
|
+
const source = resolveRef(watchedRef);
|
|
1809
|
+
const valStr = typeof expectedVal === 'object' ? JSON.stringify(expectedVal) : String(expectedVal);
|
|
1810
|
+
edges.push({
|
|
1811
|
+
id: `showWhen-${++edgeIdSeq}`,
|
|
1812
|
+
sourceKey: source.key,
|
|
1813
|
+
sourceLabel: source.label,
|
|
1814
|
+
targetKey: fieldResolved.key,
|
|
1815
|
+
targetLabel: fieldResolved.label,
|
|
1816
|
+
type: 'showWhen',
|
|
1817
|
+
actionSummary: `= ${valStr}`,
|
|
1818
|
+
description: `Visible when ${source.label} = ${valStr}`,
|
|
1819
|
+
sourceValid: source.valid,
|
|
1820
|
+
targetValid: fieldResolved.valid,
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
// cascade parentField
|
|
1825
|
+
if (field.entityReference?.parentField) {
|
|
1826
|
+
totalCascades++;
|
|
1827
|
+
const source = resolveRef(field.entityReference.parentField);
|
|
1828
|
+
edges.push({
|
|
1829
|
+
id: `cascade-${++edgeIdSeq}`,
|
|
1830
|
+
sourceKey: source.key,
|
|
1831
|
+
sourceLabel: source.label,
|
|
1832
|
+
targetKey: fieldResolved.key,
|
|
1833
|
+
targetLabel: fieldResolved.label,
|
|
1834
|
+
type: 'cascade',
|
|
1835
|
+
actionSummary: 'filters options',
|
|
1836
|
+
description: `Reloads options when ${source.label} changes`,
|
|
1837
|
+
sourceValid: source.valid,
|
|
1838
|
+
targetValid: fieldResolved.valid,
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
// patchOnTrue
|
|
1842
|
+
if (Array.isArray(field.patchOnTrue)) {
|
|
1843
|
+
for (const mapping of field.patchOnTrue) {
|
|
1844
|
+
if (!mapping.to)
|
|
1845
|
+
continue;
|
|
1846
|
+
totalPatches++;
|
|
1847
|
+
const toResolved = resolveRef(mapping.to);
|
|
1848
|
+
const fromResolved = resolveRef(mapping.from);
|
|
1849
|
+
edges.push({
|
|
1850
|
+
id: `patch-${++edgeIdSeq}`,
|
|
1851
|
+
sourceKey: fieldResolved.key,
|
|
1852
|
+
sourceLabel: fieldResolved.label,
|
|
1853
|
+
targetKey: toResolved.key,
|
|
1854
|
+
targetLabel: toResolved.label,
|
|
1855
|
+
type: 'patchOnTrue',
|
|
1856
|
+
actionSummary: `copy ${fromResolved.label} → ${toResolved.label}`,
|
|
1857
|
+
description: `When true, copies ${fromResolved.label} to ${toResolved.label}`,
|
|
1858
|
+
sourceValid: fieldResolved.valid,
|
|
1859
|
+
targetValid: toResolved.valid,
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
// autoPatch
|
|
1864
|
+
if (field.autoPatch && Array.isArray(field.autoPatch.mappings)) {
|
|
1865
|
+
for (const mapping of field.autoPatch.mappings) {
|
|
1866
|
+
if (!mapping.target)
|
|
1867
|
+
continue;
|
|
1868
|
+
totalPatches++;
|
|
1869
|
+
const targetResolved = resolveRef(mapping.target);
|
|
1870
|
+
edges.push({
|
|
1871
|
+
id: `autopatch-${++edgeIdSeq}`,
|
|
1872
|
+
sourceKey: fieldResolved.key,
|
|
1873
|
+
sourceLabel: fieldResolved.label,
|
|
1874
|
+
targetKey: targetResolved.key,
|
|
1875
|
+
targetLabel: targetResolved.label,
|
|
1876
|
+
type: 'autoPatch',
|
|
1877
|
+
actionSummary: `patch → ${targetResolved.label}`,
|
|
1878
|
+
description: `Selection copies ${mapping.source} to ${targetResolved.label}`,
|
|
1879
|
+
sourceValid: fieldResolved.valid,
|
|
1880
|
+
targetValid: targetResolved.valid,
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return {
|
|
1886
|
+
edges,
|
|
1887
|
+
totalRules,
|
|
1888
|
+
totalShowWhen,
|
|
1889
|
+
totalCascades,
|
|
1890
|
+
totalPatches,
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1702
1894
|
/**
|
|
1703
1895
|
* Every word the builder puts on screen that is not part of the config being authored.
|
|
1704
1896
|
*
|
|
@@ -1790,8 +1982,6 @@ const DEFAULT_BUILDER_TEXT = {
|
|
|
1790
1982
|
hintHint: 'An info icon beside the label, shown on hover and while the field has focus. Read out with the field.',
|
|
1791
1983
|
defaultValue: 'Default value',
|
|
1792
1984
|
// Field inspector — section headings and the layout control.
|
|
1793
|
-
// `validation` used to be the heading above the validator checkboxes; the inspector's
|
|
1794
|
-
// sections now own their headings, so `sectionValidation` replaces it.
|
|
1795
1985
|
sectionBasics: 'Basics',
|
|
1796
1986
|
sectionLayout: 'Layout',
|
|
1797
1987
|
sectionValidation: 'Validation',
|
|
@@ -1801,6 +1991,7 @@ const DEFAULT_BUILDER_TEXT = {
|
|
|
1801
1991
|
sectionAutomation: 'Automation',
|
|
1802
1992
|
sectionRules: 'Rules',
|
|
1803
1993
|
sectionReference: 'Reference',
|
|
1994
|
+
sectionDependencies: 'Dependencies',
|
|
1804
1995
|
width: 'Width',
|
|
1805
1996
|
widthQuarter: '¼',
|
|
1806
1997
|
widthThird: '⅓',
|
|
@@ -1927,6 +2118,33 @@ const DEFAULT_BUILDER_TEXT = {
|
|
|
1927
2118
|
driftHeading: 'Source field definition has drifted!',
|
|
1928
2119
|
driftBody: 'The upstream field configuration in "{entity}" has evolved.',
|
|
1929
2120
|
syncWithSource: 'Sync with Source',
|
|
2121
|
+
// Validation pattern playground
|
|
2122
|
+
pattern: 'Validation Pattern (Regex)',
|
|
2123
|
+
patternHint: 'Regular expression for format validation.',
|
|
2124
|
+
testPattern: 'Test Pattern',
|
|
2125
|
+
testPatternPlaceholder: 'Type sample input to test…',
|
|
2126
|
+
patternMatch: 'Pattern matches',
|
|
2127
|
+
patternNoMatch: 'Pattern does not match',
|
|
2128
|
+
patternInvalid: 'Invalid regex syntax',
|
|
2129
|
+
patternEmptyPrompt: 'Enter regex above to test sample input',
|
|
2130
|
+
quickPresets: 'Presets',
|
|
2131
|
+
// Field dependencies
|
|
2132
|
+
dependsOn: 'Depends on',
|
|
2133
|
+
affects: 'Affects',
|
|
2134
|
+
noDependencies: 'No active rule or field dependencies for this field.',
|
|
2135
|
+
// Rule graph
|
|
2136
|
+
ruleGraph: 'Rule Graph',
|
|
2137
|
+
ruleGraphTitle: 'Rule & Dependency Graph',
|
|
2138
|
+
ruleGraphSubtitle: 'Schema-wide rule triggers, conditional visibility, and cascades',
|
|
2139
|
+
filterDependencies: 'Filter dependencies…',
|
|
2140
|
+
noDependenciesFound: 'No dependencies matching current filter.',
|
|
2141
|
+
jumpToField: 'Select {field}',
|
|
2142
|
+
closeDialog: 'Close',
|
|
2143
|
+
dependencyTypeRule: 'Rule',
|
|
2144
|
+
dependencyTypeShowWhen: 'Show When',
|
|
2145
|
+
dependencyTypeCascade: 'Cascade',
|
|
2146
|
+
dependencyTypePatch: 'Copy on True',
|
|
2147
|
+
totalDependencies: '{count} dependencies',
|
|
1930
2148
|
};
|
|
1931
2149
|
/**
|
|
1932
2150
|
* Overrides for the builder's own chrome.
|
|
@@ -1960,10 +2178,10 @@ class BuilderTextService {
|
|
|
1960
2178
|
text(key, params) {
|
|
1961
2179
|
return resolveUiText(this.overrides, DEFAULT_BUILDER_TEXT, key, this.language(), params);
|
|
1962
2180
|
}
|
|
1963
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1964
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
2181
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderTextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2182
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderTextService, providedIn: 'root' });
|
|
1965
2183
|
}
|
|
1966
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2184
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: BuilderTextService, decorators: [{
|
|
1967
2185
|
type: Injectable,
|
|
1968
2186
|
args: [{ providedIn: 'root' }]
|
|
1969
2187
|
}] });
|
|
@@ -2053,8 +2271,8 @@ class EntityReferenceConfigComponent {
|
|
|
2053
2271
|
this.filtersParseError.set('Invalid JSON.');
|
|
2054
2272
|
}
|
|
2055
2273
|
}
|
|
2056
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2057
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2274
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2275
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityReferenceConfigComponent, isStandalone: true, selector: "ngx-entity-reference-config", ngImport: i0, template: `
|
|
2058
2276
|
@if (field(); as f) {
|
|
2059
2277
|
<div class="deb-entity-ref">
|
|
2060
2278
|
<span class="deb-section-title">{{ ui.text('entityReference') }}</span>
|
|
@@ -2212,9 +2430,9 @@ class EntityReferenceConfigComponent {
|
|
|
2212
2430
|
}
|
|
2213
2431
|
</div>
|
|
2214
2432
|
}
|
|
2215
|
-
`, isInline: true, styles: [".deb-entity-ref{display:flex;flex-direction:column;gap:12px}.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[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: 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:
|
|
2433
|
+
`, isInline: true, styles: [".deb-entity-ref{display:flex;flex-direction:column;gap:12px}.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[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: 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: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2216
2434
|
}
|
|
2217
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2435
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityReferenceConfigComponent, decorators: [{
|
|
2218
2436
|
type: Component,
|
|
2219
2437
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-entity-reference-config', standalone: true, imports: [
|
|
2220
2438
|
FormsModule,
|
|
@@ -2404,6 +2622,8 @@ class RuleFormComponent {
|
|
|
2404
2622
|
priority: 1,
|
|
2405
2623
|
};
|
|
2406
2624
|
save = new EventEmitter();
|
|
2625
|
+
// Published API since 1.x: renaming it would break every host bound to `(cancel)`.
|
|
2626
|
+
// eslint-disable-next-line @angular-eslint/no-output-native
|
|
2407
2627
|
cancel = new EventEmitter();
|
|
2408
2628
|
operators = [
|
|
2409
2629
|
'EQUAL',
|
|
@@ -2469,8 +2689,8 @@ class RuleFormComponent {
|
|
|
2469
2689
|
removeCondition(index) {
|
|
2470
2690
|
this.rule.conditions.splice(index, 1);
|
|
2471
2691
|
}
|
|
2472
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2473
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2692
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2693
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RuleFormComponent, isStandalone: true, selector: "ngx-rule-form", inputs: { rule: "rule" }, outputs: { save: "save", cancel: "cancel" }, ngImport: i0, template: `
|
|
2474
2694
|
<div class="deb-rule-form">
|
|
2475
2695
|
<h3 class="deb-rule-form__title">{{ ui.text(rule.id ? 'editRuleTitle' : 'newRuleTitle') }}</h3>
|
|
2476
2696
|
|
|
@@ -2549,9 +2769,9 @@ class RuleFormComponent {
|
|
|
2549
2769
|
<button mat-raised-button color="primary" type="button" (click)="save.emit(rule)">{{ ui.text('saveRule') }}</button>
|
|
2550
2770
|
</div>
|
|
2551
2771
|
</div>
|
|
2552
|
-
`, isInline: true, styles: [".deb-rule-form{display:flex;flex-direction:column;gap:12px;background:#fafafa;border:1px solid #e0e0e0;border-radius:8px;padding:16px}.deb-rule-form__title{margin:0 0 8px;font-size:16px;font-weight:600}.deb-full{width:100%}.deb-rule-path{color:#888;font-size:11px;margin-left:6px}.deb-rule-section{display:flex;flex-direction:column;gap:8px}.deb-rule-section h4{margin:0;font-size:13px;font-weight:600;color:#555}.deb-rule-row{display:flex;gap:8px;align-items:center}.deb-rule-row>*{flex:1}.deb-rule-form__actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}\n"], 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: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: 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:
|
|
2772
|
+
`, isInline: true, styles: [".deb-rule-form{display:flex;flex-direction:column;gap:12px;background:#fafafa;border:1px solid #e0e0e0;border-radius:8px;padding:16px}.deb-rule-form__title{margin:0 0 8px;font-size:16px;font-weight:600}.deb-full{width:100%}.deb-rule-path{color:#888;font-size:11px;margin-left:6px}.deb-rule-section{display:flex;flex-direction:column;gap:8px}.deb-rule-section h4{margin:0;font-size:13px;font-weight:600;color:#555}.deb-rule-row{display:flex;gap:8px;align-items:center}.deb-rule-row>*{flex:1}.deb-rule-form__actions{display:flex;justify-content:flex-end;gap:8px;margin-top:8px}\n"], 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: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: 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: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2553
2773
|
}
|
|
2554
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2774
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleFormComponent, decorators: [{
|
|
2555
2775
|
type: Component,
|
|
2556
2776
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-form', standalone: true, imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule], template: `
|
|
2557
2777
|
<div class="deb-rule-form">
|
|
@@ -2695,8 +2915,8 @@ class FieldRulesListComponent {
|
|
|
2695
2915
|
: `${rule.action.type}: ${rule.action.value}`;
|
|
2696
2916
|
return `${rule.fieldId} ${cond} → ${action}`;
|
|
2697
2917
|
}
|
|
2698
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2699
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2918
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRulesListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2919
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FieldRulesListComponent, isStandalone: true, selector: "ngx-field-rules-list", ngImport: i0, template: `
|
|
2700
2920
|
<div class="deb-rules">
|
|
2701
2921
|
<div class="deb-rules__head">
|
|
2702
2922
|
<span class="deb-section-title">{{ ui.text('rules') }}</span>
|
|
@@ -2777,9 +2997,9 @@ class FieldRulesListComponent {
|
|
|
2777
2997
|
</div>
|
|
2778
2998
|
}
|
|
2779
2999
|
</div>
|
|
2780
|
-
`, 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[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type:
|
|
3000
|
+
`, 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[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: 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: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: RuleFormComponent, selector: "ngx-rule-form", inputs: ["rule"], outputs: ["save", "cancel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2781
3001
|
}
|
|
2782
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3002
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldRulesListComponent, decorators: [{
|
|
2783
3003
|
type: Component,
|
|
2784
3004
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-field-rules-list', standalone: true, imports: [MatButtonModule, MatIconModule, MatSlideToggleModule, MatTooltipModule, RuleFormComponent], template: `
|
|
2785
3005
|
<div class="deb-rules">
|
|
@@ -2905,10 +3125,8 @@ class ReferencedFieldConfigComponent {
|
|
|
2905
3125
|
}
|
|
2906
3126
|
}
|
|
2907
3127
|
/**
|
|
2908
|
-
*
|
|
2909
|
-
*
|
|
2910
|
-
* the selected one it checked drift against the wrong field's source entity — or bailed
|
|
2911
|
-
* out entirely if nothing was selected.
|
|
3128
|
+
* Takes the id of the field just edited rather than reading `store.selectedField()`, which
|
|
3129
|
+
* need not be the same field — or any field at all.
|
|
2912
3130
|
*/
|
|
2913
3131
|
async checkDriftForField(fieldId) {
|
|
2914
3132
|
const f = this.store.fields().find(field => field.id === fieldId);
|
|
@@ -2957,8 +3175,8 @@ class ReferencedFieldConfigComponent {
|
|
|
2957
3175
|
}
|
|
2958
3176
|
return undefined;
|
|
2959
3177
|
}
|
|
2960
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
2961
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
3178
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ReferencedFieldConfigComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3179
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: ReferencedFieldConfigComponent, isStandalone: true, selector: "ngx-referenced-field-config", ngImport: i0, template: `
|
|
2962
3180
|
@if (field(); as f) {
|
|
2963
3181
|
<div class="deb-referenced-field" style="margin-top: 16px;">
|
|
2964
3182
|
<div class="deb-row" style="justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
@@ -3017,9 +3235,9 @@ class ReferencedFieldConfigComponent {
|
|
|
3017
3235
|
}
|
|
3018
3236
|
</div>
|
|
3019
3237
|
}
|
|
3020
|
-
`, 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: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatDividerModule }, { 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:
|
|
3238
|
+
`, 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: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatDividerModule }, { 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: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3021
3239
|
}
|
|
3022
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3240
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: ReferencedFieldConfigComponent, decorators: [{
|
|
3023
3241
|
type: Component,
|
|
3024
3242
|
args: [{
|
|
3025
3243
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -3228,9 +3446,7 @@ class FieldInspectorComponent {
|
|
|
3228
3446
|
}
|
|
3229
3447
|
/**
|
|
3230
3448
|
* Seeds a new condition with the first field not already watched.
|
|
3231
|
-
*
|
|
3232
|
-
* It used to seed the literal string `field`, which is not a field id at all — so a new
|
|
3233
|
-
* condition started out referencing nothing and silently hid the field until it was edited.
|
|
3449
|
+
* A seed that names no real field would silently hide this one until edited.
|
|
3234
3450
|
*/
|
|
3235
3451
|
addShowWhen(field) {
|
|
3236
3452
|
const next = { ...(field.showWhen ?? {}) };
|
|
@@ -3290,10 +3506,65 @@ class FieldInspectorComponent {
|
|
|
3290
3506
|
return JSON.stringify(value);
|
|
3291
3507
|
return String(value);
|
|
3292
3508
|
}
|
|
3293
|
-
|
|
3294
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FieldInspectorComponent, isStandalone: true, selector: "ngx-field-inspector", ngImport: i0, template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n What is open by default is what an author sets when they add a field \u2014 name it, size it,\n mark it required, give a choice field its options. The advanced half starts closed, but\n opens itself as soon as *this* field uses it: a field with two rules is not a field whose\n rules are advanced, and hiding what is already configured is how a setting gets forgotten\n about. A closed section still reports itself through the count in its heading.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\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>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" [attr.open]=\"displayFlagCount(f) ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" [attr.open]=\"showWhenEntries(f).length ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('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>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" [attr.open]=\"meta()?.isEntityRef || (f.patchOnTrue ?? []).length ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('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>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" [attr.open]=\"store.rulesForSelectedField().length ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" [attr.open]=\"f.isReferenced || f.hasDrift ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</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>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.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}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$2.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "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: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$1.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" }] });
|
|
3509
|
+
// ─── Pattern Playground ────────────────────────────────────────────────────
|
|
3510
|
+
testSample = signal('', ...(ngDevMode ? [{ debugName: "testSample" }] : /* istanbul ignore next */ []));
|
|
3511
|
+
regexEvaluation = computed(() => {
|
|
3512
|
+
const f = this.field();
|
|
3513
|
+
const pattern = f?.validators?.pattern?.trim();
|
|
3514
|
+
if (!pattern)
|
|
3515
|
+
return { status: 'empty' };
|
|
3516
|
+
let regex;
|
|
3517
|
+
try {
|
|
3518
|
+
regex = new RegExp(pattern);
|
|
3519
|
+
}
|
|
3520
|
+
catch (e) {
|
|
3521
|
+
return { status: 'invalid', error: e.message };
|
|
3522
|
+
}
|
|
3523
|
+
const sample = this.testSample();
|
|
3524
|
+
if (!sample)
|
|
3525
|
+
return { status: 'idle' };
|
|
3526
|
+
return { status: regex.test(sample) ? 'match' : 'no-match' };
|
|
3527
|
+
}, ...(ngDevMode ? [{ debugName: "regexEvaluation" }] : /* istanbul ignore next */ []));
|
|
3528
|
+
patternPresets = [
|
|
3529
|
+
{ label: 'Alpha', pattern: '^[a-zA-Z]+$' },
|
|
3530
|
+
{ label: 'Alphanumeric', pattern: '^[a-zA-Z0-9]+$' },
|
|
3531
|
+
{ label: 'Digits', pattern: '^\\d+$' },
|
|
3532
|
+
{ label: 'Postal Code', pattern: '^\\d{5}(-\\d{4})?$' },
|
|
3533
|
+
{ label: 'Phone', pattern: '^\\+?[1-9]\\d{1,14}$' },
|
|
3534
|
+
{ label: 'Slug', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' },
|
|
3535
|
+
];
|
|
3536
|
+
supportsPattern(type) {
|
|
3537
|
+
return ['text', 'textarea', 'markdown', 'email', 'password'].includes(type);
|
|
3538
|
+
}
|
|
3539
|
+
applyPreset(field, pattern) {
|
|
3540
|
+
this.store.setPatternValidator(this.store.keyOf(field), pattern);
|
|
3541
|
+
}
|
|
3542
|
+
// ─── Dependencies ──────────────────────────────────────────────────────────
|
|
3543
|
+
allDependencies = computed(() => computeRuleDependencies(this.store.config(), this.store.rules(), this.lang()), ...(ngDevMode ? [{ debugName: "allDependencies" }] : /* istanbul ignore next */ []));
|
|
3544
|
+
incomingDependencies = computed(() => {
|
|
3545
|
+
const f = this.field();
|
|
3546
|
+
if (!f)
|
|
3547
|
+
return [];
|
|
3548
|
+
const key = this.store.keyOf(f);
|
|
3549
|
+
const id = f.id;
|
|
3550
|
+
return this.allDependencies().edges.filter(e => e.targetKey === key || e.targetKey === id);
|
|
3551
|
+
}, ...(ngDevMode ? [{ debugName: "incomingDependencies" }] : /* istanbul ignore next */ []));
|
|
3552
|
+
outgoingDependencies = computed(() => {
|
|
3553
|
+
const f = this.field();
|
|
3554
|
+
if (!f)
|
|
3555
|
+
return [];
|
|
3556
|
+
const key = this.store.keyOf(f);
|
|
3557
|
+
const id = f.id;
|
|
3558
|
+
return this.allDependencies().edges.filter(e => e.sourceKey === key || e.sourceKey === id);
|
|
3559
|
+
}, ...(ngDevMode ? [{ debugName: "outgoingDependencies" }] : /* istanbul ignore next */ []));
|
|
3560
|
+
selectField(key) {
|
|
3561
|
+
if (key)
|
|
3562
|
+
this.store.selectField(key);
|
|
3563
|
+
}
|
|
3564
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldInspectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3565
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FieldInspectorComponent, isStandalone: true, selector: "ngx-field-inspector", ngImport: i0, template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n All inspector sections start expanded by default so authors have immediate access to\n validation, automation, visibility conditions, rules, dependencies, and reference settings\n without needing to manually unfold each section. Sections can still be collapsed as desired.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length || supportsPattern(f.type) || f.validators?.pattern) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n\n @if (supportsPattern(f.type) || f.validators?.pattern) {\n <div class=\"deb-pattern-box\" data-testid=\"pattern-playground\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('pattern') }}</mat-label>\n <input\n matInput\n data-testid=\"field-pattern-input\"\n [ngModel]=\"f.validators?.pattern ?? ''\"\n (ngModelChange)=\"store.setPatternValidator(store.keyOf(f), $event)\"\n />\n <mat-hint>{{ ui.text('patternHint') }}</mat-hint>\n </mat-form-field>\n\n <div class=\"deb-pattern-presets\">\n <span class=\"deb-section-title\">{{ ui.text('quickPresets') }}:</span>\n @for (preset of patternPresets; track preset.label) {\n <button\n type=\"button\"\n class=\"deb-preset-btn\"\n [attr.data-testid]=\"'preset-' + preset.label.toLowerCase()\"\n (click)=\"applyPreset(f, preset.pattern)\"\n >\n {{ preset.label }}\n </button>\n }\n </div>\n\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('testPattern') }}</span>\n <div\n class=\"deb-pattern-status\"\n [class.deb-pattern-status--match]=\"regexEvaluation().status === 'match'\"\n [class.deb-pattern-status--no-match]=\"regexEvaluation().status === 'no-match'\"\n [class.deb-pattern-status--invalid]=\"regexEvaluation().status === 'invalid'\"\n [class.deb-pattern-status--idle]=\"regexEvaluation().status === 'idle'\"\n [class.deb-pattern-status--empty]=\"regexEvaluation().status === 'empty'\"\n data-testid=\"regex-test-status\"\n >\n @switch (regexEvaluation().status) {\n @case ('match') {\n <mat-icon>check_circle</mat-icon> {{ ui.text('patternMatch') }}\n }\n @case ('no-match') {\n <mat-icon>cancel</mat-icon> {{ ui.text('patternNoMatch') }}\n }\n @case ('invalid') {\n <mat-icon>error</mat-icon> {{ ui.text('patternInvalid') }}\n }\n @case ('idle') {\n <mat-icon>edit_note</mat-icon> {{ ui.text('testPatternPlaceholder') }}\n }\n @case ('empty') {\n <mat-icon>info</mat-icon> {{ ui.text('patternEmptyPrompt') }}\n }\n }\n </div>\n </div>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <input\n matInput\n data-testid=\"regex-test-input\"\n [placeholder]=\"ui.text('testPatternPlaceholder')\"\n [ngModel]=\"testSample()\"\n (ngModelChange)=\"testSample.set($event)\"\n />\n </mat-form-field>\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\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>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('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>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('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>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Field Dependencies Visualizer -->\n <details\n class=\"deb-inspector__section\"\n open\n data-testid=\"section-dependencies\"\n >\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDependencies') }}\n @if (incomingDependencies().length + outgoingDependencies().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"dependencies-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (incomingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('dependsOn') }} ({{ incomingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"incoming-dependencies\">\n @for (dep of incomingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.sourceLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.sourceLabel })\"\n (click)=\"selectField(dep.sourceKey)\"\n >\n <mat-icon>arrow_back</mat-icon> {{ ui.text('jumpToField', { field: dep.sourceLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (outgoingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('affects') }} ({{ outgoingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"outgoing-dependencies\">\n @for (dep of outgoingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.targetLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.targetLabel })\"\n (click)=\"selectField(dep.targetKey)\"\n >\n <mat-icon>arrow_forward</mat-icon> {{ ui.text('jumpToField', { field: dep.targetLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (!incomingDependencies().length && !outgoingDependencies().length) {\n <p class=\"deb-hint\" data-testid=\"no-field-dependencies\">{{ ui.text('noDependencies') }}</p>\n }\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</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>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.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-pattern-box{display:flex;flex-direction:column;gap:8px;margin-top:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px}.deb-pattern-status{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}.deb-pattern-status--match{background:#dcfce7;color:#166534}.deb-pattern-status--no-match{background:#fee2e2;color:#991b1b}.deb-pattern-status--invalid{background:#fef3c7;color:#92400e}.deb-pattern-status--idle,.deb-pattern-status--empty{background:#f1f5f9;color:#334155}.deb-pattern-presets{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.deb-preset-btn{padding:2px 8px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;font-size:11px;color:#334155;cursor:pointer;transition:background .1s ease}.deb-preset-btn:hover{background:#e2e8f0}.deb-dep-list{display:flex;flex-direction:column;gap:6px;margin-top:4px;margin-bottom:12px}.deb-dep-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px}.deb-dep-info{display:flex;flex-direction:column;min-width:0}.deb-dep-name{font-size:12px;font-weight:600;color:#0f172a}.deb-dep-detail{font-size:11px;color:#64748b}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$2.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "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: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: EntityReferenceConfigComponent, selector: "ngx-entity-reference-config" }, { kind: "component", type: FieldRulesListComponent, selector: "ngx-field-rules-list" }, { kind: "component", type: ReferencedFieldConfigComponent, selector: "ngx-referenced-field-config" }] });
|
|
3295
3566
|
}
|
|
3296
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3567
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldInspectorComponent, decorators: [{
|
|
3297
3568
|
type: Component,
|
|
3298
3569
|
args: [{ selector: 'ngx-field-inspector', standalone: true, imports: [
|
|
3299
3570
|
FormsModule,
|
|
@@ -3309,7 +3580,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
3309
3580
|
EntityReferenceConfigComponent,
|
|
3310
3581
|
FieldRulesListComponent,
|
|
3311
3582
|
ReferencedFieldConfigComponent,
|
|
3312
|
-
], template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n What is open by default is what an author sets when they add a field \u2014 name it, size it,\n mark it required, give a choice field its options. The advanced half starts closed, but\n opens itself as soon as *this* field uses it: a field with two rules is not a field whose\n rules are advanced, and hiding what is already configured is how a setting gets forgotten\n about. A closed section still reports itself through the count in its heading.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\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>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" [attr.open]=\"displayFlagCount(f) ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" [attr.open]=\"showWhenEntries(f).length ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('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>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" [attr.open]=\"meta()?.isEntityRef || (f.patchOnTrue ?? []).length ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('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>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" [attr.open]=\"store.rulesForSelectedField().length ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" [attr.open]=\"f.isReferenced || f.hasDrift ? '' : null\">\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</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>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.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}\n"] }]
|
|
3583
|
+
], template: "@if (field(); as f) {\n <!--\n The inspector, in collapsible sections.\n\n It used to be one flat scroll of fifteen divider-separated blocks, so renaming a field\n meant scrolling past every advanced setting a field has, and finding the rules list meant\n scrolling past all of them.\n\n All inspector sections start expanded by default so authors have immediate access to\n validation, automation, visibility conditions, rules, dependencies, and reference settings\n without needing to manually unfold each section. Sections can still be collapsed as desired.\n\n `<details>` rather than mat-expansion-panel: the inspector already sits inside a card in a\n scrolling rail, and a third level of Material elevation inside that read as noise.\n -->\n <div class=\"deb-inspector\">\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionBasics') }}\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <!-- Identity -->\n <div class=\"deb-row\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('fieldId') }}</mat-label>\n <input matInput data-testid=\"field-id\" [value]=\"f.id\" disabled />\n @if (store.hasManualId(f.id)) {\n <mat-hint>{{ ui.text('fieldIdFixedHint') }}</mat-hint>\n } @else {\n <mat-hint>{{ ui.text('fieldIdDerivedHint') }}</mat-hint>\n }\n </mat-form-field>\n <span class=\"deb-chip\">{{ meta()?.label || f.type }}</span>\n </div>\n\n @if (tabOptions().length > 1) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('tab') }}</mat-label>\n <mat-select data-testid=\"field-tab\" [ngModel]=\"currentTabId(f)\" (ngModelChange)=\"moveToTab(f, $event)\">\n @for (tab of tabOptions(); track tab.id) {\n <mat-option [value]=\"tab.id\">{{ tab.label }}</mat-option>\n }\n </mat-select>\n <mat-hint>{{ ui.text('tabMoveHint') }}</mat-hint>\n </mat-form-field>\n }\n\n <!-- Localised text (active language) -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('labelInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-label\"\n [ngModel]=\"labelValue(f)\"\n (ngModelChange)=\"store.setFieldLabel(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n\n @if (meta()?.supportsPlaceholder) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('placeholderInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [ngModel]=\"placeholderValue(f)\"\n (ngModelChange)=\"store.setFieldPlaceholder(store.keyOf(f), lang(), $event)\"\n />\n </mat-form-field>\n }\n\n <!--\n Help text, offered on every field type \u2014 including the ones with no placeholder,\n which are exactly the ones most likely to need explaining. Its hint says what it is\n *for*, because the box above it is the one authors reach for by habit and a\n placeholder is gone the moment somebody types.\n -->\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('hintInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n data-testid=\"field-hint\"\n [ngModel]=\"hintValue(f)\"\n (ngModelChange)=\"store.setFieldHint(store.keyOf(f), lang(), $event)\"\n />\n <mat-hint>{{ ui.text('hintHint') }}</mat-hint>\n </mat-form-field>\n\n @if (meta()?.supportsDefaultValue) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('defaultValue') }}</mat-label>\n <input\n matInput\n [ngModel]=\"f.defaultValue ?? ''\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { defaultValue: $event })\"\n />\n </mat-form-field>\n }\n </div>\n </details>\n\n <!--\n Width.\n\n `colSpan` has been in the schema and honoured by the renderer since the 12-column grid\n existed, and the builder had no control for it \u2014 so every form authored here came out a\n single-column ladder, and the only way to put two fields on one row was to hand-edit the\n JSON. This is that control.\n -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionLayout') }}\n <span class=\"deb-inspector__count\" data-testid=\"layout-count\">{{ f.colSpan ?? 12 }}/12</span>\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-span-picker\" role=\"group\" [attr.aria-label]=\"ui.text('width')\">\n @for (choice of spanChoices; track choice.span) {\n <button\n type=\"button\"\n class=\"deb-span-picker__btn\"\n [class.deb-span-picker__btn--active]=\"(f.colSpan ?? 12) === choice.span\"\n [attr.data-testid]=\"'field-span-' + choice.span\"\n [attr.aria-pressed]=\"(f.colSpan ?? 12) === choice.span\"\n [matTooltip]=\"ui.text('widthOf', { span: choice.span })\"\n (click)=\"store.updateField(store.keyOf(f), { colSpan: choice.span })\"\n >\n {{ ui.text(choice.textKey) }}\n </button>\n }\n </div>\n <p class=\"deb-hint\">{{ ui.text('widthHint') }}</p>\n </div>\n </details>\n\n <!-- Validators -->\n <!-- Validators -->\n @if (meta(); as m) {\n @if (m.flagValidators.length || m.paramValidators.length || supportsPattern(f.type) || f.validators?.pattern) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionValidation') }}\n @if (validatorCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"validation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (m.flagValidators.length) {\n <div class=\"deb-flags-row\">\n @for (v of m.flagValidators; track v) {\n <mat-checkbox\n color=\"primary\"\n [ngModel]=\"store.hasFlagValidator(f, v)\"\n (ngModelChange)=\"store.toggleFlagValidator(store.keyOf(f), v, $event)\"\n >{{ v }}</mat-checkbox\n >\n }\n </div>\n }\n @if (m.paramValidators.length) {\n <div class=\"deb-param-validators-grid\">\n @for (v of m.paramValidators; track v) {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ v }}</mat-label>\n <input\n matInput\n type=\"number\"\n [ngModel]=\"store.getParamValidator(f, v)\"\n (ngModelChange)=\"store.setParamValidator(store.keyOf(f), v, toNum($event))\"\n />\n </mat-form-field>\n }\n </div>\n }\n\n @if (supportsPattern(f.type) || f.validators?.pattern) {\n <div class=\"deb-pattern-box\" data-testid=\"pattern-playground\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('pattern') }}</mat-label>\n <input\n matInput\n data-testid=\"field-pattern-input\"\n [ngModel]=\"f.validators?.pattern ?? ''\"\n (ngModelChange)=\"store.setPatternValidator(store.keyOf(f), $event)\"\n />\n <mat-hint>{{ ui.text('patternHint') }}</mat-hint>\n </mat-form-field>\n\n <div class=\"deb-pattern-presets\">\n <span class=\"deb-section-title\">{{ ui.text('quickPresets') }}:</span>\n @for (preset of patternPresets; track preset.label) {\n <button\n type=\"button\"\n class=\"deb-preset-btn\"\n [attr.data-testid]=\"'preset-' + preset.label.toLowerCase()\"\n (click)=\"applyPreset(f, preset.pattern)\"\n >\n {{ preset.label }}\n </button>\n }\n </div>\n\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('testPattern') }}</span>\n <div\n class=\"deb-pattern-status\"\n [class.deb-pattern-status--match]=\"regexEvaluation().status === 'match'\"\n [class.deb-pattern-status--no-match]=\"regexEvaluation().status === 'no-match'\"\n [class.deb-pattern-status--invalid]=\"regexEvaluation().status === 'invalid'\"\n [class.deb-pattern-status--idle]=\"regexEvaluation().status === 'idle'\"\n [class.deb-pattern-status--empty]=\"regexEvaluation().status === 'empty'\"\n data-testid=\"regex-test-status\"\n >\n @switch (regexEvaluation().status) {\n @case ('match') {\n <mat-icon>check_circle</mat-icon> {{ ui.text('patternMatch') }}\n }\n @case ('no-match') {\n <mat-icon>cancel</mat-icon> {{ ui.text('patternNoMatch') }}\n }\n @case ('invalid') {\n <mat-icon>error</mat-icon> {{ ui.text('patternInvalid') }}\n }\n @case ('idle') {\n <mat-icon>edit_note</mat-icon> {{ ui.text('testPatternPlaceholder') }}\n }\n @case ('empty') {\n <mat-icon>info</mat-icon> {{ ui.text('patternEmptyPrompt') }}\n }\n }\n </div>\n </div>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <input\n matInput\n data-testid=\"regex-test-input\"\n [placeholder]=\"ui.text('testPatternPlaceholder')\"\n [ngModel]=\"testSample()\"\n (ngModelChange)=\"testSample.set($event)\"\n />\n </mat-form-field>\n </div>\n }\n </div>\n </details>\n }\n }\n\n <!-- Options (dropdown / radio / multiSelect) -->\n @if (meta()?.hasOptions) {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionOptions') }}\n @if ((f.options ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"options-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <span class=\"deb-section-title\">{{ ui.text('dataSource') }}</span>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('optionsComeFrom') }}</mat-label>\n <mat-select\n data-testid=\"data-source\"\n [ngModel]=\"store.fieldDataSource(f)\"\n (ngModelChange)=\"store.setFieldDataSource(store.keyOf(f), $event)\"\n >\n <mat-option value=\"none\">{{ ui.text('optionsSourceNone') }}</mat-option>\n <mat-option value=\"manual\">{{ ui.text('optionsSourceInline') }}</mat-option>\n <mat-option value=\"lookup\">{{ ui.text('optionsSourceList') }}</mat-option>\n </mat-select>\n </mat-form-field>\n\n <!-- Named lookup list -->\n @if (store.fieldDataSource(f) === 'lookup') {\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('listName') }}</mat-label>\n <input\n matInput\n data-testid=\"list-name\"\n [ngModel]=\"f.listName ?? ''\"\n (ngModelChange)=\"store.setListName(store.keyOf(f), $event)\"\n />\n </mat-form-field>\n <p class=\"deb-hint\">\n {{ ui.text('listNameHint') }}\n </p>\n }\n\n <!-- Inline options -->\n @if (store.fieldDataSource(f) !== 'lookup') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('options') }}</span>\n <span class=\"deb-row__actions\">\n @if (store.hasUnkeyedOptions()) {\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"assign-option-keys\"\n (click)=\"store.assignOptionKeys()\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >\n <mat-icon>key</mat-icon> {{ ui.text('assignOptionKeys') }}\n </button>\n }\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-option\"\n (click)=\"store.addOption(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('option') }}\n </button>\n </span>\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>{{ ui.text('optionInLanguage', { language: lang() }) }}</mat-label>\n <input\n matInput\n [attr.data-testid]=\"'option-' + $index\"\n [ngModel]=\"optionLabel(opt)\"\n (ngModelChange)=\"store.setOptionLabel(store.keyOf(f), $index, lang(), $event)\"\n />\n </mat-form-field>\n <!--\n Not a control. A key is the option's identity and every record already\n holding it matches on it, so a text box here \u2014 even a read-only one \u2014\n would be a data migration disguised as a field. It is shown because an\n author needs to be able to read it, and for no other reason.\n -->\n @if (store.optionKey(opt); as key) {\n <code\n class=\"deb-option-key\"\n [attr.data-testid]=\"'option-key-' + $index\"\n [attr.aria-label]=\"ui.text('optionKey')\"\n [matTooltip]=\"ui.text('optionKeyHint')\"\n >{{ key }}</code\n >\n }\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n (click)=\"store.removeOption(store.keyOf(f), $index)\"\n [matTooltip]=\"ui.text('removeOption')\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.options ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('noOptionsYet') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Flags -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDisplay') }}\n @if (displayFlagCount(f); as count) {\n <span class=\"deb-inspector__count\" data-testid=\"display-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-toggles\" role=\"group\" [attr.aria-label]=\"ui.text('display')\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.visibility ?? true\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { visibility: $event })\"\n >{{ ui.text('visible') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.table?.visible ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { table: { visible: $event } })\"\n >{{ ui.text('tableColumn') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.readonly ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { readonly: $event })\"\n >{{ ui.text('readOnly') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.disabled ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { disabled: $event })\"\n >{{ ui.text('disabled') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.maskData ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { maskData: $event })\"\n >{{ ui.text('maskData') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n data-testid=\"toggle-critical\"\n [ngModel]=\"f.criticalField ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { criticalField: $event })\"\n [matTooltip]=\"ui.text('criticalFieldTooltip')\"\n >{{ ui.text('criticalField') }}</mat-slide-toggle\n >\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"f.showOnMinimize ?? false\"\n (ngModelChange)=\"store.updateField(store.keyOf(f), { showOnMinimize: $event })\"\n [matTooltip]=\"ui.text('showInSummaryTooltip')\"\n >{{ ui.text('showInSummary') }}</mat-slide-toggle\n >\n </div>\n </div>\n </details>\n\n <!-- Conditional visibility (static showWhen) -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionVisibility') }}\n @if (showWhenEntries(f).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"visibility-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('showWhen') }}</span>\n <button mat-stroked-button type=\"button\" data-testid=\"add-show-when\" (click)=\"addShowWhen(f)\">\n <mat-icon>add</mat-icon> {{ ui.text('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>{{ ui.text('watchedField') }}</mat-label>\n <mat-select\n data-testid=\"show-when-field\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"renameShowWhen(f, entry.key, $event)\"\n >\n @for (option of optionsWith(entry.key); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('equals') }}</mat-label>\n <input matInput [ngModel]=\"entry.display\" (ngModelChange)=\"setShowWhenValue(f, entry.key, $event)\" />\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeCondition')\"\n (click)=\"removeShowWhen(f, entry.key)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!showWhenEntries(f).length) {\n <p class=\"deb-hint\">{{ ui.text('alwaysVisibleHint') }}</p>\n }\n </div>\n </details>\n\n <!-- Entity reference, cascade, autoPatch, and patchOnTrue -->\n @if (meta()?.isEntityRef || f.type === 'boolean' || f.type === 'checkbox') {\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionAutomation') }}\n @if ((f.patchOnTrue ?? []).length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"automation-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (meta()?.isEntityRef) {\n <ngx-entity-reference-config />\n }\n\n <!-- patchOnTrue (boolean/checkbox only) -->\n @if (f.type === 'boolean' || f.type === 'checkbox') {\n <div class=\"deb-row deb-row--split\">\n <span class=\"deb-section-title\">{{ ui.text('copyOnTrue') }}</span>\n <button\n mat-stroked-button\n type=\"button\"\n data-testid=\"add-patch-on-true\"\n (click)=\"store.addPatchOnTrueMapping(store.keyOf(f))\"\n >\n <mat-icon>add</mat-icon> {{ ui.text('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>{{ ui.text('fromField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-from\"\n [ngModel]=\"mapping.from\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { from: $event })\"\n >\n @for (option of optionsWith(mapping.from); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\">\n <mat-label>{{ ui.text('toField') }}</mat-label>\n <mat-select\n data-testid=\"patch-on-true-to\"\n [ngModel]=\"mapping.to\"\n (ngModelChange)=\"store.updatePatchOnTrueMapping(store.keyOf(f), $index, { to: $event })\"\n >\n @for (option of optionsWith(mapping.to); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }} <span class=\"deb-path-hint\">{{ option.path }}</span>\n </mat-option>\n }\n </mat-select>\n </mat-form-field>\n <button\n mat-icon-button\n type=\"button\"\n color=\"warn\"\n [matTooltip]=\"ui.text('removeMapping')\"\n (click)=\"store.removePatchOnTrueMapping(store.keyOf(f), $index)\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n }\n @if (!(f.patchOnTrue ?? []).length) {\n <p class=\"deb-hint\">{{ ui.text('copyOnTrueHint') }}</p>\n }\n }\n </div>\n </details>\n }\n\n <!-- Rules attached to this field -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionRules') }}\n @if (store.rulesForSelectedField().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"rules-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-field-rules-list />\n </div>\n </details>\n\n <!-- Field Dependencies Visualizer -->\n <details\n class=\"deb-inspector__section\"\n open\n data-testid=\"section-dependencies\"\n >\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionDependencies') }}\n @if (incomingDependencies().length + outgoingDependencies().length; as count) {\n <span class=\"deb-inspector__count\" data-testid=\"dependencies-count\">{{ count }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n @if (incomingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('dependsOn') }} ({{ incomingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"incoming-dependencies\">\n @for (dep of incomingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.sourceLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.sourceLabel })\"\n (click)=\"selectField(dep.sourceKey)\"\n >\n <mat-icon>arrow_back</mat-icon> {{ ui.text('jumpToField', { field: dep.sourceLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (outgoingDependencies().length) {\n <span class=\"deb-section-title\">{{ ui.text('affects') }} ({{ outgoingDependencies().length }})</span>\n <div class=\"deb-dep-list\" data-testid=\"outgoing-dependencies\">\n @for (dep of outgoingDependencies(); track dep.id) {\n <div class=\"deb-dep-row\">\n <div class=\"deb-dep-info\">\n <span class=\"deb-dep-name\">{{ dep.targetLabel }}</span>\n <span class=\"deb-dep-detail\">{{ dep.description }}</span>\n </div>\n <button\n mat-stroked-button\n type=\"button\"\n [matTooltip]=\"ui.text('jumpToField', { field: dep.targetLabel })\"\n (click)=\"selectField(dep.targetKey)\"\n >\n <mat-icon>arrow_forward</mat-icon> {{ ui.text('jumpToField', { field: dep.targetLabel }) }}\n </button>\n </div>\n }\n </div>\n }\n\n @if (!incomingDependencies().length && !outgoingDependencies().length) {\n <p class=\"deb-hint\" data-testid=\"no-field-dependencies\">{{ ui.text('noDependencies') }}</p>\n }\n </div>\n </details>\n\n <!-- Cross-entity field reference & drift management -->\n <details class=\"deb-inspector__section\" open>\n <summary class=\"deb-inspector__summary\">\n {{ ui.text('sectionReference') }}\n @if (f.isReferenced) {\n <span class=\"deb-inspector__count\" data-testid=\"reference-count\">{{ f.referencedEntityKey }}</span>\n }\n <mat-icon class=\"deb-inspector__chevron\">expand_more</mat-icon>\n </summary>\n <div class=\"deb-inspector__body\">\n <ngx-referenced-field-config />\n </div>\n </details>\n </div>\n} @else {\n <div class=\"deb-inspector__empty\">\n <mat-icon>touch_app</mat-icon>\n <p>{{ ui.text('inspectorEmpty') }}</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>*{flex:1}.deb-row>.deb-chip{flex:0 0 auto}.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-pattern-box{display:flex;flex-direction:column;gap:8px;margin-top:8px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px}.deb-pattern-status{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600}.deb-pattern-status--match{background:#dcfce7;color:#166534}.deb-pattern-status--no-match{background:#fee2e2;color:#991b1b}.deb-pattern-status--invalid{background:#fef3c7;color:#92400e}.deb-pattern-status--idle,.deb-pattern-status--empty{background:#f1f5f9;color:#334155}.deb-pattern-presets{display:flex;flex-wrap:wrap;gap:4px;align-items:center}.deb-preset-btn{padding:2px 8px;border-radius:4px;border:1px solid #cbd5e1;background:#fff;font-size:11px;color:#334155;cursor:pointer;transition:background .1s ease}.deb-preset-btn:hover{background:#e2e8f0}.deb-dep-list{display:flex;flex-direction:column;gap:6px;margin-top:4px;margin-bottom:12px}.deb-dep-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px}.deb-dep-info{display:flex;flex-direction:column;min-width:0}.deb-dep-name{font-size:12px;font-weight:600;color:#0f172a}.deb-dep-detail{font-size:11px;color:#64748b}\n"] }]
|
|
3313
3584
|
}] });
|
|
3314
3585
|
|
|
3315
3586
|
/**
|
|
@@ -3356,11 +3627,8 @@ const GROUP_TEXT_KEY = {
|
|
|
3356
3627
|
/**
|
|
3357
3628
|
* FieldPaletteComponent — the list of buildable field types.
|
|
3358
3629
|
*
|
|
3359
|
-
*
|
|
3360
|
-
*
|
|
3361
|
-
* grid forced two equal columns and the button refused to wrap, so a third of the palette
|
|
3362
|
-
* could not be read. And with no order to them, finding "Month & Year" meant scanning the
|
|
3363
|
-
* whole list: the grouping and the filter are what make a palette this size navigable.
|
|
3630
|
+
* Grouped and filterable, because a flat list of twenty-two types is not navigable, and
|
|
3631
|
+
* tiles wrap their labels so names like "Entity Reference" are never clipped.
|
|
3364
3632
|
*/
|
|
3365
3633
|
class FieldPaletteComponent {
|
|
3366
3634
|
/** Builder chrome, overridable via BUILDER_TEXT. */
|
|
@@ -3389,8 +3657,8 @@ class FieldPaletteComponent {
|
|
|
3389
3657
|
})).filter(section => section.items.length > 0);
|
|
3390
3658
|
}, ...(ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []));
|
|
3391
3659
|
pick = new EventEmitter();
|
|
3392
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3393
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
3660
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldPaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3661
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: FieldPaletteComponent, isStandalone: true, selector: "ngx-field-palette", outputs: { pick: "pick" }, ngImport: i0, template: `
|
|
3394
3662
|
<div class="deb-palette">
|
|
3395
3663
|
<div class="deb-palette__search">
|
|
3396
3664
|
<mat-icon aria-hidden="true">search</mat-icon>
|
|
@@ -3438,9 +3706,9 @@ class FieldPaletteComponent {
|
|
|
3438
3706
|
<p class="deb-hint" data-testid="palette-no-match">{{ ui.text('paletteNoMatch', { query: query() }) }}</p>
|
|
3439
3707
|
}
|
|
3440
3708
|
</div>
|
|
3441
|
-
`, isInline: true, styles: [".deb-palette{display:flex;flex-direction:column;gap:14px}.deb-palette__search{display:flex;align-items:center;gap:8px;padding:0 10px;height:36px;border:1px solid var(--deb-border, #e2e8f0);border-radius:8px;background:#f8fafc}.deb-palette__search:focus-within{border-color:var(--deb-accent, #6366f1);background:#fff;box-shadow:0 0 0 3px #6366f129}.deb-palette__search mat-icon{font-size:17px;width:17px;height:17px;color:var(--deb-muted, #64748b);flex:none}.deb-palette__search input{flex:1;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:13px;color:inherit}.deb-palette__search input::-webkit-search-cancel-button{display:none}.deb-palette__clear{appearance:none;border:0;background:none;padding:0;cursor:pointer;display:flex;color:var(--deb-muted, #64748b)}.deb-palette__clear:hover{color:#0f172a}.deb-palette__section{display:flex;flex-direction:column;gap:6px}.deb-palette__grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}.deb-palette__item{justify-content:flex-start!important;height:auto!important;min-height:38px;line-height:1.25!important;border-radius:8px!important;border:1px solid var(--deb-border, #e2e8f0)!important;padding:7px 10px!important;font-family:var(--font-heading, inherit)!important;font-weight:600!important;font-size:12.5px!important;transition:all .18s cubic-bezier(.16,1,.3,1)!important;background:#fff!important;text-align:left!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:#4338ca;margin-right:7px;font-size:17px;width:17px;height:17px;flex:none}.deb-palette__item span{white-space:normal;overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){.deb-palette__item{transition:none!important}.deb-palette__item:hover{transform:none}}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type:
|
|
3709
|
+
`, isInline: true, styles: [".deb-palette{display:flex;flex-direction:column;gap:14px}.deb-palette__search{display:flex;align-items:center;gap:8px;padding:0 10px;height:36px;border:1px solid var(--deb-border, #e2e8f0);border-radius:8px;background:#f8fafc}.deb-palette__search:focus-within{border-color:var(--deb-accent, #6366f1);background:#fff;box-shadow:0 0 0 3px #6366f129}.deb-palette__search mat-icon{font-size:17px;width:17px;height:17px;color:var(--deb-muted, #64748b);flex:none}.deb-palette__search input{flex:1;min-width:0;border:0;outline:0;background:transparent;font:inherit;font-size:13px;color:inherit}.deb-palette__search input::-webkit-search-cancel-button{display:none}.deb-palette__clear{appearance:none;border:0;background:none;padding:0;cursor:pointer;display:flex;color:var(--deb-muted, #64748b)}.deb-palette__clear:hover{color:#0f172a}.deb-palette__section{display:flex;flex-direction:column;gap:6px}.deb-palette__grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}.deb-palette__item{justify-content:flex-start!important;height:auto!important;min-height:38px;line-height:1.25!important;border-radius:8px!important;border:1px solid var(--deb-border, #e2e8f0)!important;padding:7px 10px!important;font-family:var(--font-heading, inherit)!important;font-weight:600!important;font-size:12.5px!important;transition:all .18s cubic-bezier(.16,1,.3,1)!important;background:#fff!important;text-align:left!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:#4338ca;margin-right:7px;font-size:17px;width:17px;height:17px;flex:none}.deb-palette__item span{white-space:normal;overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){.deb-palette__item{transition:none!important}.deb-palette__item:hover{transform:none}}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3442
3710
|
}
|
|
3443
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3711
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: FieldPaletteComponent, decorators: [{
|
|
3444
3712
|
type: Component,
|
|
3445
3713
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-field-palette', standalone: true, imports: [MatButtonModule, MatIconModule, MatTooltipModule], template: `
|
|
3446
3714
|
<div class="deb-palette">
|
|
@@ -3522,8 +3790,8 @@ class TabManagerComponent {
|
|
|
3522
3790
|
}
|
|
3523
3791
|
return true;
|
|
3524
3792
|
}
|
|
3525
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3526
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
3793
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TabManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3794
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: TabManagerComponent, isStandalone: true, selector: "ngx-tab-manager", ngImport: i0, template: `
|
|
3527
3795
|
<div class="deb-tabs">
|
|
3528
3796
|
<div class="deb-tabs__head">
|
|
3529
3797
|
<span class="deb-section-title">{{ ui.text('tabsManager') }}</span>
|
|
@@ -3667,9 +3935,9 @@ class TabManagerComponent {
|
|
|
3667
3935
|
</div>
|
|
3668
3936
|
}
|
|
3669
3937
|
</div>
|
|
3670
|
-
`, 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[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$2.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: 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:
|
|
3938
|
+
`, 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[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i3$2.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: 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: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3671
3939
|
}
|
|
3672
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
3940
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: TabManagerComponent, decorators: [{
|
|
3673
3941
|
type: Component,
|
|
3674
3942
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-tab-manager', standalone: true, imports: [
|
|
3675
3943
|
FormsModule,
|
|
@@ -3844,8 +4112,8 @@ class EntityBuilderTreeNodeComponent {
|
|
|
3844
4112
|
const lang = this.store.activeLanguage();
|
|
3845
4113
|
return resolveLabel(field.label, lang) || field.id;
|
|
3846
4114
|
}
|
|
3847
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
3848
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
4115
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4116
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityBuilderTreeNodeComponent, isStandalone: true, selector: "ngx-entity-builder-tree-node", inputs: { field: "field", index: "index", totalCount: "totalCount" }, ngImport: i0, template: `
|
|
3849
4117
|
<div
|
|
3850
4118
|
cdkDrag
|
|
3851
4119
|
class="deb-field-row"
|
|
@@ -3959,9 +4227,9 @@ class EntityBuilderTreeNodeComponent {
|
|
|
3959
4227
|
}
|
|
3960
4228
|
</div>
|
|
3961
4229
|
}
|
|
3962
|
-
`, isInline: true, dependencies: [{ kind: "component", type: EntityBuilderTreeNodeComponent, selector: "ngx-entity-builder-tree-node", inputs: ["field", "index", "totalCount"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type:
|
|
4230
|
+
`, isInline: true, dependencies: [{ kind: "component", type: EntityBuilderTreeNodeComponent, selector: "ngx-entity-builder-tree-node", inputs: ["field", "index", "totalCount"] }, { kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1$1.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3963
4231
|
}
|
|
3964
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4232
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderTreeNodeComponent, decorators: [{
|
|
3965
4233
|
type: Component,
|
|
3966
4234
|
args: [{
|
|
3967
4235
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -4113,8 +4381,8 @@ class EntityBuilderCanvasComponent {
|
|
|
4113
4381
|
onDrop(tabId, event) {
|
|
4114
4382
|
this.store.reorderField(event.previousIndex, event.currentIndex, tabId);
|
|
4115
4383
|
}
|
|
4116
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4117
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
4384
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4385
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityBuilderCanvasComponent, isStandalone: true, selector: "ngx-entity-builder-canvas", outputs: { collapse: "collapse" }, ngImport: i0, template: `
|
|
4118
4386
|
<mat-card class="deb-card deb-card--canvas">
|
|
4119
4387
|
<mat-card-header>
|
|
4120
4388
|
<mat-card-title class="deb-card__title">
|
|
@@ -4139,10 +4407,9 @@ class EntityBuilderCanvasComponent {
|
|
|
4139
4407
|
@if (store.fields().length === 0) {
|
|
4140
4408
|
<div class="deb-empty">
|
|
4141
4409
|
<!--
|
|
4142
|
-
Decoration, and marked as such
|
|
4143
|
-
says, so
|
|
4144
|
-
|
|
4145
|
-
Hidden from the accessibility tree, it is neither announced nor measured.
|
|
4410
|
+
Decoration, and marked as such: the sentence below says everything this glyph
|
|
4411
|
+
says, so it is hidden from the accessibility tree. axe still measures its
|
|
4412
|
+
contrast, so its colour passes AA like any other text.
|
|
4146
4413
|
-->
|
|
4147
4414
|
<mat-icon aria-hidden="true">widgets</mat-icon>
|
|
4148
4415
|
<p>{{ ui.text('canvasEmpty') }}</p>
|
|
@@ -4173,9 +4440,9 @@ class EntityBuilderCanvasComponent {
|
|
|
4173
4440
|
}
|
|
4174
4441
|
</mat-card-content>
|
|
4175
4442
|
</mat-card>
|
|
4176
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$3.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type:
|
|
4443
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$1.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$3.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { 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"] }, { kind: "component", type: EntityBuilderTreeNodeComponent, selector: "ngx-entity-builder-tree-node", inputs: ["field", "index", "totalCount"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4177
4444
|
}
|
|
4178
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
4445
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderCanvasComponent, decorators: [{
|
|
4179
4446
|
type: Component,
|
|
4180
4447
|
args: [{
|
|
4181
4448
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -4215,10 +4482,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4215
4482
|
@if (store.fields().length === 0) {
|
|
4216
4483
|
<div class="deb-empty">
|
|
4217
4484
|
<!--
|
|
4218
|
-
Decoration, and marked as such
|
|
4219
|
-
says, so
|
|
4220
|
-
|
|
4221
|
-
Hidden from the accessibility tree, it is neither announced nor measured.
|
|
4485
|
+
Decoration, and marked as such: the sentence below says everything this glyph
|
|
4486
|
+
says, so it is hidden from the accessibility tree. axe still measures its
|
|
4487
|
+
contrast, so its colour passes AA like any other text.
|
|
4222
4488
|
-->
|
|
4223
4489
|
<mat-icon aria-hidden="true">widgets</mat-icon>
|
|
4224
4490
|
<p>{{ ui.text('canvasEmpty') }}</p>
|
|
@@ -4255,6 +4521,408 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4255
4521
|
type: Output
|
|
4256
4522
|
}] } });
|
|
4257
4523
|
|
|
4524
|
+
class RuleDependencyGraphComponent {
|
|
4525
|
+
ui = inject(BuilderTextService);
|
|
4526
|
+
store = inject(BuilderStore);
|
|
4527
|
+
dismiss = new EventEmitter();
|
|
4528
|
+
selectField = new EventEmitter();
|
|
4529
|
+
filterQuery = signal('', ...(ngDevMode ? [{ debugName: "filterQuery" }] : /* istanbul ignore next */ []));
|
|
4530
|
+
selectedType = signal('all', ...(ngDevMode ? [{ debugName: "selectedType" }] : /* istanbul ignore next */ []));
|
|
4531
|
+
graphData = computed(() => computeRuleDependencies(this.store.config(), this.store.rules(), this.ui.language()), ...(ngDevMode ? [{ debugName: "graphData" }] : /* istanbul ignore next */ []));
|
|
4532
|
+
filteredEdges = computed(() => {
|
|
4533
|
+
const q = this.filterQuery().trim().toLowerCase();
|
|
4534
|
+
const type = this.selectedType();
|
|
4535
|
+
return this.graphData().edges.filter(edge => {
|
|
4536
|
+
if (type !== 'all' && edge.type !== type)
|
|
4537
|
+
return false;
|
|
4538
|
+
if (!q)
|
|
4539
|
+
return true;
|
|
4540
|
+
return (edge.sourceLabel.toLowerCase().includes(q) ||
|
|
4541
|
+
edge.sourceKey.toLowerCase().includes(q) ||
|
|
4542
|
+
edge.targetLabel.toLowerCase().includes(q) ||
|
|
4543
|
+
edge.targetKey.toLowerCase().includes(q) ||
|
|
4544
|
+
edge.actionSummary.toLowerCase().includes(q) ||
|
|
4545
|
+
edge.description.toLowerCase().includes(q));
|
|
4546
|
+
});
|
|
4547
|
+
}, ...(ngDevMode ? [{ debugName: "filteredEdges" }] : /* istanbul ignore next */ []));
|
|
4548
|
+
typeName(type) {
|
|
4549
|
+
switch (type) {
|
|
4550
|
+
case 'rule':
|
|
4551
|
+
return this.ui.text('dependencyTypeRule');
|
|
4552
|
+
case 'showWhen':
|
|
4553
|
+
return this.ui.text('dependencyTypeShowWhen');
|
|
4554
|
+
case 'cascade':
|
|
4555
|
+
return this.ui.text('dependencyTypeCascade');
|
|
4556
|
+
case 'patchOnTrue':
|
|
4557
|
+
case 'autoPatch':
|
|
4558
|
+
return this.ui.text('dependencyTypePatch');
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
onNodeClick(key) {
|
|
4562
|
+
if (!key)
|
|
4563
|
+
return;
|
|
4564
|
+
this.selectField.emit(key);
|
|
4565
|
+
this.dismiss.emit();
|
|
4566
|
+
}
|
|
4567
|
+
onBackdropClick(event) {
|
|
4568
|
+
if (event.target.classList.contains('deb-graph-backdrop')) {
|
|
4569
|
+
this.dismiss.emit();
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleDependencyGraphComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4573
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: RuleDependencyGraphComponent, isStandalone: true, selector: "ngx-rule-dependency-graph", outputs: { dismiss: "dismiss", selectField: "selectField" }, ngImport: i0, template: `
|
|
4574
|
+
<div class="deb-graph-backdrop" (click)="onBackdropClick($event)">
|
|
4575
|
+
<div class="deb-graph-dialog" role="dialog" aria-modal="true" data-testid="rule-graph-dialog">
|
|
4576
|
+
<div class="deb-graph-header">
|
|
4577
|
+
<div class="deb-graph-header__titles">
|
|
4578
|
+
<div class="deb-graph-header__title-row">
|
|
4579
|
+
<mat-icon class="deb-graph-icon">hub</mat-icon>
|
|
4580
|
+
<h2 class="deb-graph-title">{{ ui.text('ruleGraphTitle') }}</h2>
|
|
4581
|
+
</div>
|
|
4582
|
+
<p class="deb-graph-subtitle">{{ ui.text('ruleGraphSubtitle') }}</p>
|
|
4583
|
+
</div>
|
|
4584
|
+
<button
|
|
4585
|
+
mat-icon-button
|
|
4586
|
+
type="button"
|
|
4587
|
+
data-testid="close-rule-graph"
|
|
4588
|
+
[matTooltip]="ui.text('closeDialog')"
|
|
4589
|
+
(click)="dismiss.emit()"
|
|
4590
|
+
>
|
|
4591
|
+
<mat-icon>close</mat-icon>
|
|
4592
|
+
</button>
|
|
4593
|
+
</div>
|
|
4594
|
+
|
|
4595
|
+
<div class="deb-graph-metrics">
|
|
4596
|
+
<span class="deb-metric-pill deb-metric-pill--rule">
|
|
4597
|
+
<mat-icon>rule</mat-icon>
|
|
4598
|
+
{{ graphData().totalRules }} {{ ui.text('dependencyTypeRule') }}
|
|
4599
|
+
</span>
|
|
4600
|
+
<span class="deb-metric-pill deb-metric-pill--show-when">
|
|
4601
|
+
<mat-icon>visibility</mat-icon>
|
|
4602
|
+
{{ graphData().totalShowWhen }} {{ ui.text('dependencyTypeShowWhen') }}
|
|
4603
|
+
</span>
|
|
4604
|
+
<span class="deb-metric-pill deb-metric-pill--cascade">
|
|
4605
|
+
<mat-icon>account_tree</mat-icon>
|
|
4606
|
+
{{ graphData().totalCascades }} {{ ui.text('dependencyTypeCascade') }}
|
|
4607
|
+
</span>
|
|
4608
|
+
<span class="deb-metric-pill deb-metric-pill--patch">
|
|
4609
|
+
<mat-icon>content_copy</mat-icon>
|
|
4610
|
+
{{ graphData().totalPatches }} {{ ui.text('dependencyTypePatch') }}
|
|
4611
|
+
</span>
|
|
4612
|
+
</div>
|
|
4613
|
+
|
|
4614
|
+
<div class="deb-graph-controls">
|
|
4615
|
+
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-graph-search">
|
|
4616
|
+
<mat-icon matPrefix>search</mat-icon>
|
|
4617
|
+
<input
|
|
4618
|
+
matInput
|
|
4619
|
+
data-testid="filter-dependencies-input"
|
|
4620
|
+
[placeholder]="ui.text('filterDependencies')"
|
|
4621
|
+
[ngModel]="filterQuery()"
|
|
4622
|
+
(ngModelChange)="filterQuery.set($event)"
|
|
4623
|
+
/>
|
|
4624
|
+
@if (filterQuery()) {
|
|
4625
|
+
<button mat-icon-button matSuffix type="button" (click)="filterQuery.set('')">
|
|
4626
|
+
<mat-icon>clear</mat-icon>
|
|
4627
|
+
</button>
|
|
4628
|
+
}
|
|
4629
|
+
</mat-form-field>
|
|
4630
|
+
|
|
4631
|
+
<div class="deb-graph-filter-types">
|
|
4632
|
+
<button
|
|
4633
|
+
type="button"
|
|
4634
|
+
class="deb-filter-btn"
|
|
4635
|
+
[class.deb-filter-btn--active]="selectedType() === 'all'"
|
|
4636
|
+
(click)="selectedType.set('all')"
|
|
4637
|
+
>
|
|
4638
|
+
All ({{ graphData().edges.length }})
|
|
4639
|
+
</button>
|
|
4640
|
+
<button
|
|
4641
|
+
type="button"
|
|
4642
|
+
class="deb-filter-btn"
|
|
4643
|
+
[class.deb-filter-btn--active]="selectedType() === 'rule'"
|
|
4644
|
+
(click)="selectedType.set('rule')"
|
|
4645
|
+
>
|
|
4646
|
+
{{ ui.text('dependencyTypeRule') }}
|
|
4647
|
+
</button>
|
|
4648
|
+
<button
|
|
4649
|
+
type="button"
|
|
4650
|
+
class="deb-filter-btn"
|
|
4651
|
+
[class.deb-filter-btn--active]="selectedType() === 'showWhen'"
|
|
4652
|
+
(click)="selectedType.set('showWhen')"
|
|
4653
|
+
>
|
|
4654
|
+
{{ ui.text('dependencyTypeShowWhen') }}
|
|
4655
|
+
</button>
|
|
4656
|
+
<button
|
|
4657
|
+
type="button"
|
|
4658
|
+
class="deb-filter-btn"
|
|
4659
|
+
[class.deb-filter-btn--active]="selectedType() === 'cascade'"
|
|
4660
|
+
(click)="selectedType.set('cascade')"
|
|
4661
|
+
>
|
|
4662
|
+
{{ ui.text('dependencyTypeCascade') }}
|
|
4663
|
+
</button>
|
|
4664
|
+
<button
|
|
4665
|
+
type="button"
|
|
4666
|
+
class="deb-filter-btn"
|
|
4667
|
+
[class.deb-filter-btn--active]="selectedType() === 'patchOnTrue'"
|
|
4668
|
+
(click)="selectedType.set('patchOnTrue')"
|
|
4669
|
+
>
|
|
4670
|
+
{{ ui.text('dependencyTypePatch') }}
|
|
4671
|
+
</button>
|
|
4672
|
+
</div>
|
|
4673
|
+
</div>
|
|
4674
|
+
|
|
4675
|
+
<div class="deb-graph-body" data-testid="rule-graph-edges">
|
|
4676
|
+
@if (filteredEdges().length === 0) {
|
|
4677
|
+
<div class="deb-graph-empty" data-testid="no-dependencies-found">
|
|
4678
|
+
<mat-icon>search_off</mat-icon>
|
|
4679
|
+
<p>{{ ui.text('noDependenciesFound') }}</p>
|
|
4680
|
+
</div>
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
@for (edge of filteredEdges(); track edge.id) {
|
|
4684
|
+
<div class="deb-edge-card" [attr.data-testid]="'edge-card-' + edge.id">
|
|
4685
|
+
<!-- Source -->
|
|
4686
|
+
<button
|
|
4687
|
+
type="button"
|
|
4688
|
+
class="deb-node-chip deb-node-chip--source"
|
|
4689
|
+
[matTooltip]="ui.text('jumpToField', { field: edge.sourceLabel })"
|
|
4690
|
+
(click)="onNodeClick(edge.sourceKey)"
|
|
4691
|
+
>
|
|
4692
|
+
<mat-icon class="deb-node-chip__icon">tune</mat-icon>
|
|
4693
|
+
<span class="deb-node-chip__label">{{ edge.sourceLabel }}</span>
|
|
4694
|
+
<span class="deb-node-chip__key">{{ edge.sourceKey }}</span>
|
|
4695
|
+
</button>
|
|
4696
|
+
|
|
4697
|
+
<!-- Connection details -->
|
|
4698
|
+
<div class="deb-edge-flow">
|
|
4699
|
+
<span
|
|
4700
|
+
class="deb-edge-badge"
|
|
4701
|
+
[class.deb-edge-badge--rule]="edge.type === 'rule'"
|
|
4702
|
+
[class.deb-edge-badge--show-when]="edge.type === 'showWhen'"
|
|
4703
|
+
[class.deb-edge-badge--cascade]="edge.type === 'cascade'"
|
|
4704
|
+
[class.deb-edge-badge--patch]="edge.type === 'patchOnTrue' || edge.type === 'autoPatch'"
|
|
4705
|
+
>
|
|
4706
|
+
{{ typeName(edge.type) }}
|
|
4707
|
+
</span>
|
|
4708
|
+
<span class="deb-edge-arrow">
|
|
4709
|
+
<span class="deb-edge-summary">{{ edge.actionSummary }}</span>
|
|
4710
|
+
<mat-icon class="deb-edge-arrow-icon">arrow_forward</mat-icon>
|
|
4711
|
+
</span>
|
|
4712
|
+
<span class="deb-edge-desc">{{ edge.description }}</span>
|
|
4713
|
+
</div>
|
|
4714
|
+
|
|
4715
|
+
<!-- Target -->
|
|
4716
|
+
<button
|
|
4717
|
+
type="button"
|
|
4718
|
+
class="deb-node-chip deb-node-chip--target"
|
|
4719
|
+
[matTooltip]="ui.text('jumpToField', { field: edge.targetLabel })"
|
|
4720
|
+
(click)="onNodeClick(edge.targetKey)"
|
|
4721
|
+
>
|
|
4722
|
+
<mat-icon class="deb-node-chip__icon">output</mat-icon>
|
|
4723
|
+
<span class="deb-node-chip__label">{{ edge.targetLabel }}</span>
|
|
4724
|
+
<span class="deb-node-chip__key">{{ edge.targetKey }}</span>
|
|
4725
|
+
</button>
|
|
4726
|
+
</div>
|
|
4727
|
+
}
|
|
4728
|
+
</div>
|
|
4729
|
+
|
|
4730
|
+
<div class="deb-graph-footer">
|
|
4731
|
+
<span class="deb-graph-count">
|
|
4732
|
+
{{ ui.text('totalDependencies', { count: filteredEdges().length }) }}
|
|
4733
|
+
</span>
|
|
4734
|
+
<button mat-flat-button color="primary" type="button" (click)="dismiss.emit()">
|
|
4735
|
+
{{ ui.text('closeDialog') }}
|
|
4736
|
+
</button>
|
|
4737
|
+
</div>
|
|
4738
|
+
</div>
|
|
4739
|
+
</div>
|
|
4740
|
+
`, isInline: true, styles: [".deb-graph-backdrop{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:1000;padding:24px;animation:debFadeIn .15s ease-out}@keyframes debFadeIn{0%{opacity:0}to{opacity:1}}.deb-graph-dialog{background:var(--deb-surface, #ffffff);border:1px solid var(--deb-border, #e2e8f0);border-radius:14px;width:100%;max-width:960px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 25px 50px -12px #00000040;overflow:hidden}.deb-graph-header{display:flex;align-items:flex-start;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--deb-border, #e2e8f0)}.deb-graph-header__title-row{display:flex;align-items:center;gap:10px}.deb-graph-icon{color:var(--deb-accent, #6366f1);font-size:26px;width:26px;height:26px}.deb-graph-title{font-size:18px;font-weight:700;color:#0f172a;margin:0}.deb-graph-subtitle{font-size:13px;color:var(--deb-muted, #64748b);margin:4px 0 0}.deb-graph-metrics{display:flex;gap:12px;padding:12px 24px;background:#f8fafc;border-bottom:1px solid var(--deb-border, #e2e8f0);flex-wrap:wrap}.deb-metric-pill{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:9999px;font-size:12px;font-weight:600;border:1px solid transparent}.deb-metric-pill mat-icon{font-size:16px;width:16px;height:16px}.deb-metric-pill--rule{background:#eef2ff;color:#4f46e5;border-color:#c7d2fe}.deb-metric-pill--show-when{background:#f0fdf4;color:#16a34a;border-color:#bbf7d0}.deb-metric-pill--cascade{background:#fefce8;color:#ca8a04;border-color:#fef08a}.deb-metric-pill--patch{background:#fdf2f8;color:#db2777;border-color:#fbcfe8}.deb-graph-controls{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 24px;border-bottom:1px solid var(--deb-border, #e2e8f0);flex-wrap:wrap}.deb-graph-search{flex:1;min-width:240px}.deb-graph-filter-types{display:flex;gap:6px;flex-wrap:wrap}.deb-filter-btn{padding:5px 12px;border-radius:6px;border:1px solid var(--deb-border, #e2e8f0);background:#fff;font-size:12px;font-weight:500;color:#475569;cursor:pointer;transition:all .15s ease}.deb-filter-btn:hover{background:#f1f5f9;color:#0f172a}.deb-filter-btn--active{background:var(--deb-accent, #6366f1);color:#fff;border-color:var(--deb-accent, #6366f1)}.deb-filter-btn--active:hover{background:var(--deb-accent-hover, #4f46e5)}.deb-graph-body{flex:1;overflow-y:auto;padding:20px 24px;display:flex;flex-direction:column;gap:12px}.deb-graph-empty{text-align:center;padding:48px 16px;color:var(--deb-muted, #64748b)}.deb-graph-empty mat-icon{font-size:40px;width:40px;height:40px;margin-bottom:8px;opacity:.5}.deb-edge-card{display:grid;grid-template-columns:220px 1fr 220px;align-items:center;gap:16px;padding:12px 16px;background:#fff;border:1px solid var(--deb-border, #e2e8f0);border-radius:10px;transition:box-shadow .15s ease,border-color .15s ease}.deb-edge-card:hover{border-color:#cbd5e1;box-shadow:0 4px 6px -1px #0000000d}.deb-node-chip{display:flex;flex-direction:column;align-items:flex-start;padding:8px 12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;cursor:pointer;text-align:left;width:100%;transition:background .15s ease,border-color .15s ease}.deb-node-chip:hover{background:#f1f5f9;border-color:var(--deb-accent, #6366f1)}.deb-node-chip__icon{font-size:16px;width:16px;height:16px;color:var(--deb-accent, #6366f1);margin-bottom:2px}.deb-node-chip__label{font-size:13px;font-weight:600;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px}.deb-node-chip__key{font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;color:var(--deb-muted, #64748b);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px}.deb-edge-flow{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;text-align:center}.deb-edge-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 8px;border-radius:4px}.deb-edge-badge--rule{background:#eef2ff;color:#4f46e5}.deb-edge-badge--show-when{background:#f0fdf4;color:#16a34a}.deb-edge-badge--cascade{background:#fefce8;color:#a16207}.deb-edge-badge--patch{background:#fdf2f8;color:#be185d}.deb-edge-arrow{display:inline-flex;align-items:center;gap:6px;color:#475569}.deb-edge-summary{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;background:#f1f5f9;padding:2px 8px;border-radius:4px}.deb-edge-arrow-icon{font-size:16px;width:16px;height:16px;color:var(--deb-muted, #64748b)}.deb-edge-desc{font-size:11px;color:var(--deb-muted, #64748b)}.deb-graph-footer{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-top:1px solid var(--deb-border, #e2e8f0);background:#f8fafc}.deb-graph-count{font-size:13px;color:var(--deb-muted, #64748b)}@media(max-width:768px){.deb-edge-card{grid-template-columns:1fr;gap:8px}}\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[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatChipsModule }, { 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.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { 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: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4741
|
+
}
|
|
4742
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: RuleDependencyGraphComponent, decorators: [{
|
|
4743
|
+
type: Component,
|
|
4744
|
+
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-rule-dependency-graph', standalone: true, imports: [
|
|
4745
|
+
FormsModule,
|
|
4746
|
+
MatButtonModule,
|
|
4747
|
+
MatChipsModule,
|
|
4748
|
+
MatFormFieldModule,
|
|
4749
|
+
MatIconModule,
|
|
4750
|
+
MatInputModule,
|
|
4751
|
+
MatTooltipModule,
|
|
4752
|
+
], template: `
|
|
4753
|
+
<div class="deb-graph-backdrop" (click)="onBackdropClick($event)">
|
|
4754
|
+
<div class="deb-graph-dialog" role="dialog" aria-modal="true" data-testid="rule-graph-dialog">
|
|
4755
|
+
<div class="deb-graph-header">
|
|
4756
|
+
<div class="deb-graph-header__titles">
|
|
4757
|
+
<div class="deb-graph-header__title-row">
|
|
4758
|
+
<mat-icon class="deb-graph-icon">hub</mat-icon>
|
|
4759
|
+
<h2 class="deb-graph-title">{{ ui.text('ruleGraphTitle') }}</h2>
|
|
4760
|
+
</div>
|
|
4761
|
+
<p class="deb-graph-subtitle">{{ ui.text('ruleGraphSubtitle') }}</p>
|
|
4762
|
+
</div>
|
|
4763
|
+
<button
|
|
4764
|
+
mat-icon-button
|
|
4765
|
+
type="button"
|
|
4766
|
+
data-testid="close-rule-graph"
|
|
4767
|
+
[matTooltip]="ui.text('closeDialog')"
|
|
4768
|
+
(click)="dismiss.emit()"
|
|
4769
|
+
>
|
|
4770
|
+
<mat-icon>close</mat-icon>
|
|
4771
|
+
</button>
|
|
4772
|
+
</div>
|
|
4773
|
+
|
|
4774
|
+
<div class="deb-graph-metrics">
|
|
4775
|
+
<span class="deb-metric-pill deb-metric-pill--rule">
|
|
4776
|
+
<mat-icon>rule</mat-icon>
|
|
4777
|
+
{{ graphData().totalRules }} {{ ui.text('dependencyTypeRule') }}
|
|
4778
|
+
</span>
|
|
4779
|
+
<span class="deb-metric-pill deb-metric-pill--show-when">
|
|
4780
|
+
<mat-icon>visibility</mat-icon>
|
|
4781
|
+
{{ graphData().totalShowWhen }} {{ ui.text('dependencyTypeShowWhen') }}
|
|
4782
|
+
</span>
|
|
4783
|
+
<span class="deb-metric-pill deb-metric-pill--cascade">
|
|
4784
|
+
<mat-icon>account_tree</mat-icon>
|
|
4785
|
+
{{ graphData().totalCascades }} {{ ui.text('dependencyTypeCascade') }}
|
|
4786
|
+
</span>
|
|
4787
|
+
<span class="deb-metric-pill deb-metric-pill--patch">
|
|
4788
|
+
<mat-icon>content_copy</mat-icon>
|
|
4789
|
+
{{ graphData().totalPatches }} {{ ui.text('dependencyTypePatch') }}
|
|
4790
|
+
</span>
|
|
4791
|
+
</div>
|
|
4792
|
+
|
|
4793
|
+
<div class="deb-graph-controls">
|
|
4794
|
+
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="deb-graph-search">
|
|
4795
|
+
<mat-icon matPrefix>search</mat-icon>
|
|
4796
|
+
<input
|
|
4797
|
+
matInput
|
|
4798
|
+
data-testid="filter-dependencies-input"
|
|
4799
|
+
[placeholder]="ui.text('filterDependencies')"
|
|
4800
|
+
[ngModel]="filterQuery()"
|
|
4801
|
+
(ngModelChange)="filterQuery.set($event)"
|
|
4802
|
+
/>
|
|
4803
|
+
@if (filterQuery()) {
|
|
4804
|
+
<button mat-icon-button matSuffix type="button" (click)="filterQuery.set('')">
|
|
4805
|
+
<mat-icon>clear</mat-icon>
|
|
4806
|
+
</button>
|
|
4807
|
+
}
|
|
4808
|
+
</mat-form-field>
|
|
4809
|
+
|
|
4810
|
+
<div class="deb-graph-filter-types">
|
|
4811
|
+
<button
|
|
4812
|
+
type="button"
|
|
4813
|
+
class="deb-filter-btn"
|
|
4814
|
+
[class.deb-filter-btn--active]="selectedType() === 'all'"
|
|
4815
|
+
(click)="selectedType.set('all')"
|
|
4816
|
+
>
|
|
4817
|
+
All ({{ graphData().edges.length }})
|
|
4818
|
+
</button>
|
|
4819
|
+
<button
|
|
4820
|
+
type="button"
|
|
4821
|
+
class="deb-filter-btn"
|
|
4822
|
+
[class.deb-filter-btn--active]="selectedType() === 'rule'"
|
|
4823
|
+
(click)="selectedType.set('rule')"
|
|
4824
|
+
>
|
|
4825
|
+
{{ ui.text('dependencyTypeRule') }}
|
|
4826
|
+
</button>
|
|
4827
|
+
<button
|
|
4828
|
+
type="button"
|
|
4829
|
+
class="deb-filter-btn"
|
|
4830
|
+
[class.deb-filter-btn--active]="selectedType() === 'showWhen'"
|
|
4831
|
+
(click)="selectedType.set('showWhen')"
|
|
4832
|
+
>
|
|
4833
|
+
{{ ui.text('dependencyTypeShowWhen') }}
|
|
4834
|
+
</button>
|
|
4835
|
+
<button
|
|
4836
|
+
type="button"
|
|
4837
|
+
class="deb-filter-btn"
|
|
4838
|
+
[class.deb-filter-btn--active]="selectedType() === 'cascade'"
|
|
4839
|
+
(click)="selectedType.set('cascade')"
|
|
4840
|
+
>
|
|
4841
|
+
{{ ui.text('dependencyTypeCascade') }}
|
|
4842
|
+
</button>
|
|
4843
|
+
<button
|
|
4844
|
+
type="button"
|
|
4845
|
+
class="deb-filter-btn"
|
|
4846
|
+
[class.deb-filter-btn--active]="selectedType() === 'patchOnTrue'"
|
|
4847
|
+
(click)="selectedType.set('patchOnTrue')"
|
|
4848
|
+
>
|
|
4849
|
+
{{ ui.text('dependencyTypePatch') }}
|
|
4850
|
+
</button>
|
|
4851
|
+
</div>
|
|
4852
|
+
</div>
|
|
4853
|
+
|
|
4854
|
+
<div class="deb-graph-body" data-testid="rule-graph-edges">
|
|
4855
|
+
@if (filteredEdges().length === 0) {
|
|
4856
|
+
<div class="deb-graph-empty" data-testid="no-dependencies-found">
|
|
4857
|
+
<mat-icon>search_off</mat-icon>
|
|
4858
|
+
<p>{{ ui.text('noDependenciesFound') }}</p>
|
|
4859
|
+
</div>
|
|
4860
|
+
}
|
|
4861
|
+
|
|
4862
|
+
@for (edge of filteredEdges(); track edge.id) {
|
|
4863
|
+
<div class="deb-edge-card" [attr.data-testid]="'edge-card-' + edge.id">
|
|
4864
|
+
<!-- Source -->
|
|
4865
|
+
<button
|
|
4866
|
+
type="button"
|
|
4867
|
+
class="deb-node-chip deb-node-chip--source"
|
|
4868
|
+
[matTooltip]="ui.text('jumpToField', { field: edge.sourceLabel })"
|
|
4869
|
+
(click)="onNodeClick(edge.sourceKey)"
|
|
4870
|
+
>
|
|
4871
|
+
<mat-icon class="deb-node-chip__icon">tune</mat-icon>
|
|
4872
|
+
<span class="deb-node-chip__label">{{ edge.sourceLabel }}</span>
|
|
4873
|
+
<span class="deb-node-chip__key">{{ edge.sourceKey }}</span>
|
|
4874
|
+
</button>
|
|
4875
|
+
|
|
4876
|
+
<!-- Connection details -->
|
|
4877
|
+
<div class="deb-edge-flow">
|
|
4878
|
+
<span
|
|
4879
|
+
class="deb-edge-badge"
|
|
4880
|
+
[class.deb-edge-badge--rule]="edge.type === 'rule'"
|
|
4881
|
+
[class.deb-edge-badge--show-when]="edge.type === 'showWhen'"
|
|
4882
|
+
[class.deb-edge-badge--cascade]="edge.type === 'cascade'"
|
|
4883
|
+
[class.deb-edge-badge--patch]="edge.type === 'patchOnTrue' || edge.type === 'autoPatch'"
|
|
4884
|
+
>
|
|
4885
|
+
{{ typeName(edge.type) }}
|
|
4886
|
+
</span>
|
|
4887
|
+
<span class="deb-edge-arrow">
|
|
4888
|
+
<span class="deb-edge-summary">{{ edge.actionSummary }}</span>
|
|
4889
|
+
<mat-icon class="deb-edge-arrow-icon">arrow_forward</mat-icon>
|
|
4890
|
+
</span>
|
|
4891
|
+
<span class="deb-edge-desc">{{ edge.description }}</span>
|
|
4892
|
+
</div>
|
|
4893
|
+
|
|
4894
|
+
<!-- Target -->
|
|
4895
|
+
<button
|
|
4896
|
+
type="button"
|
|
4897
|
+
class="deb-node-chip deb-node-chip--target"
|
|
4898
|
+
[matTooltip]="ui.text('jumpToField', { field: edge.targetLabel })"
|
|
4899
|
+
(click)="onNodeClick(edge.targetKey)"
|
|
4900
|
+
>
|
|
4901
|
+
<mat-icon class="deb-node-chip__icon">output</mat-icon>
|
|
4902
|
+
<span class="deb-node-chip__label">{{ edge.targetLabel }}</span>
|
|
4903
|
+
<span class="deb-node-chip__key">{{ edge.targetKey }}</span>
|
|
4904
|
+
</button>
|
|
4905
|
+
</div>
|
|
4906
|
+
}
|
|
4907
|
+
</div>
|
|
4908
|
+
|
|
4909
|
+
<div class="deb-graph-footer">
|
|
4910
|
+
<span class="deb-graph-count">
|
|
4911
|
+
{{ ui.text('totalDependencies', { count: filteredEdges().length }) }}
|
|
4912
|
+
</span>
|
|
4913
|
+
<button mat-flat-button color="primary" type="button" (click)="dismiss.emit()">
|
|
4914
|
+
{{ ui.text('closeDialog') }}
|
|
4915
|
+
</button>
|
|
4916
|
+
</div>
|
|
4917
|
+
</div>
|
|
4918
|
+
</div>
|
|
4919
|
+
`, styles: [".deb-graph-backdrop{position:fixed;inset:0;background:#0f172a99;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:1000;padding:24px;animation:debFadeIn .15s ease-out}@keyframes debFadeIn{0%{opacity:0}to{opacity:1}}.deb-graph-dialog{background:var(--deb-surface, #ffffff);border:1px solid var(--deb-border, #e2e8f0);border-radius:14px;width:100%;max-width:960px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 25px 50px -12px #00000040;overflow:hidden}.deb-graph-header{display:flex;align-items:flex-start;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--deb-border, #e2e8f0)}.deb-graph-header__title-row{display:flex;align-items:center;gap:10px}.deb-graph-icon{color:var(--deb-accent, #6366f1);font-size:26px;width:26px;height:26px}.deb-graph-title{font-size:18px;font-weight:700;color:#0f172a;margin:0}.deb-graph-subtitle{font-size:13px;color:var(--deb-muted, #64748b);margin:4px 0 0}.deb-graph-metrics{display:flex;gap:12px;padding:12px 24px;background:#f8fafc;border-bottom:1px solid var(--deb-border, #e2e8f0);flex-wrap:wrap}.deb-metric-pill{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:9999px;font-size:12px;font-weight:600;border:1px solid transparent}.deb-metric-pill mat-icon{font-size:16px;width:16px;height:16px}.deb-metric-pill--rule{background:#eef2ff;color:#4f46e5;border-color:#c7d2fe}.deb-metric-pill--show-when{background:#f0fdf4;color:#16a34a;border-color:#bbf7d0}.deb-metric-pill--cascade{background:#fefce8;color:#ca8a04;border-color:#fef08a}.deb-metric-pill--patch{background:#fdf2f8;color:#db2777;border-color:#fbcfe8}.deb-graph-controls{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 24px;border-bottom:1px solid var(--deb-border, #e2e8f0);flex-wrap:wrap}.deb-graph-search{flex:1;min-width:240px}.deb-graph-filter-types{display:flex;gap:6px;flex-wrap:wrap}.deb-filter-btn{padding:5px 12px;border-radius:6px;border:1px solid var(--deb-border, #e2e8f0);background:#fff;font-size:12px;font-weight:500;color:#475569;cursor:pointer;transition:all .15s ease}.deb-filter-btn:hover{background:#f1f5f9;color:#0f172a}.deb-filter-btn--active{background:var(--deb-accent, #6366f1);color:#fff;border-color:var(--deb-accent, #6366f1)}.deb-filter-btn--active:hover{background:var(--deb-accent-hover, #4f46e5)}.deb-graph-body{flex:1;overflow-y:auto;padding:20px 24px;display:flex;flex-direction:column;gap:12px}.deb-graph-empty{text-align:center;padding:48px 16px;color:var(--deb-muted, #64748b)}.deb-graph-empty mat-icon{font-size:40px;width:40px;height:40px;margin-bottom:8px;opacity:.5}.deb-edge-card{display:grid;grid-template-columns:220px 1fr 220px;align-items:center;gap:16px;padding:12px 16px;background:#fff;border:1px solid var(--deb-border, #e2e8f0);border-radius:10px;transition:box-shadow .15s ease,border-color .15s ease}.deb-edge-card:hover{border-color:#cbd5e1;box-shadow:0 4px 6px -1px #0000000d}.deb-node-chip{display:flex;flex-direction:column;align-items:flex-start;padding:8px 12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;cursor:pointer;text-align:left;width:100%;transition:background .15s ease,border-color .15s ease}.deb-node-chip:hover{background:#f1f5f9;border-color:var(--deb-accent, #6366f1)}.deb-node-chip__icon{font-size:16px;width:16px;height:16px;color:var(--deb-accent, #6366f1);margin-bottom:2px}.deb-node-chip__label{font-size:13px;font-weight:600;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px}.deb-node-chip__key{font-family:ui-monospace,SFMono-Regular,monospace;font-size:11px;color:var(--deb-muted, #64748b);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px}.deb-edge-flow{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;text-align:center}.deb-edge-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:2px 8px;border-radius:4px}.deb-edge-badge--rule{background:#eef2ff;color:#4f46e5}.deb-edge-badge--show-when{background:#f0fdf4;color:#16a34a}.deb-edge-badge--cascade{background:#fefce8;color:#a16207}.deb-edge-badge--patch{background:#fdf2f8;color:#be185d}.deb-edge-arrow{display:inline-flex;align-items:center;gap:6px;color:#475569}.deb-edge-summary{font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;background:#f1f5f9;padding:2px 8px;border-radius:4px}.deb-edge-arrow-icon{font-size:16px;width:16px;height:16px;color:var(--deb-muted, #64748b)}.deb-edge-desc{font-size:11px;color:var(--deb-muted, #64748b)}.deb-graph-footer{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-top:1px solid var(--deb-border, #e2e8f0);background:#f8fafc}.deb-graph-count{font-size:13px;color:var(--deb-muted, #64748b)}@media(max-width:768px){.deb-edge-card{grid-template-columns:1fr;gap:8px}}\n"] }]
|
|
4920
|
+
}], propDecorators: { dismiss: [{
|
|
4921
|
+
type: Output
|
|
4922
|
+
}], selectField: [{
|
|
4923
|
+
type: Output
|
|
4924
|
+
}] } });
|
|
4925
|
+
|
|
4258
4926
|
/** RBAC actions surfaced in the settings panel. */
|
|
4259
4927
|
const RBAC_ACTIONS = ['view', 'edit', 'delete'];
|
|
4260
4928
|
/** Shared stable empty array — never allocate a fresh [] per change-detection (mat-select loops). */
|
|
@@ -4351,6 +5019,17 @@ class EntityBuilderComponent {
|
|
|
4351
5019
|
togglePreview() {
|
|
4352
5020
|
this.previewOpen.update(open => !open);
|
|
4353
5021
|
}
|
|
5022
|
+
ruleGraphOpen = signal(false, ...(ngDevMode ? [{ debugName: "ruleGraphOpen" }] : /* istanbul ignore next */ []));
|
|
5023
|
+
openRuleGraph() {
|
|
5024
|
+
this.ruleGraphOpen.set(true);
|
|
5025
|
+
}
|
|
5026
|
+
closeRuleGraph() {
|
|
5027
|
+
this.ruleGraphOpen.set(false);
|
|
5028
|
+
}
|
|
5029
|
+
onSelectFieldFromGraph(key) {
|
|
5030
|
+
this.store.selectField(key);
|
|
5031
|
+
this.ruleGraphOpen.set(false);
|
|
5032
|
+
}
|
|
4354
5033
|
/*
|
|
4355
5034
|
* Collapsing from inside a panel destroys the button that was just pressed, and expanding
|
|
4356
5035
|
* from a rail destroys the rail. Either way the browser drops focus onto `<body>`, which
|
|
@@ -4488,10 +5167,10 @@ class EntityBuilderComponent {
|
|
|
4488
5167
|
.filter(Boolean)));
|
|
4489
5168
|
this.store.setPermission(action, roles);
|
|
4490
5169
|
}
|
|
4491
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
4492
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: EntityBuilderComponent, isStandalone: true, selector: "ngx-entity-builder", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: false, isRequired: false, transformFunction: null }, rules: { classPropertyName: "rules", publicName: "rules", isSignal: false, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: false, isRequired: false, transformFunction: null }, uiLanguage: { classPropertyName: "uiLanguage", publicName: "uiLanguage", isSignal: false, isRequired: false, transformFunction: null }, availableRoles: { classPropertyName: "availableRoles", publicName: "availableRoles", isSignal: false, isRequired: false, transformFunction: null }, userRoles: { classPropertyName: "userRoles", publicName: "userRoles", isSignal: false, isRequired: false, transformFunction: null }, commonModules: { classPropertyName: "commonModules", publicName: "commonModules", isSignal: false, isRequired: false, transformFunction: null }, leftSidebarOpen: { classPropertyName: "leftSidebarOpen", publicName: "leftSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, rightSidebarOpen: { classPropertyName: "rightSidebarOpen", publicName: "rightSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, fieldsOpen: { classPropertyName: "fieldsOpen", publicName: "fieldsOpen", isSignal: true, isRequired: false, transformFunction: null }, previewOpen: { classPropertyName: "previewOpen", publicName: "previewOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { configChange: "configChange", rulesChange: "rulesChange", save: "save", leftSidebarOpen: "leftSidebarOpenChange", rightSidebarOpen: "rightSidebarOpenChange", fieldsOpen: "fieldsOpenChange", previewOpen: "previewOpenChange" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, providers: [BuilderStore], usesOnChanges: true, ngImport: i0, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </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 class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\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 <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('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>{{ ui.text('actionRoles', { action }) }}</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>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input matInput [ngModel]=\"rolesFor(action).join(', ')\" (ngModelChange)=\"setRolesFromText(action, $event)\" />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\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, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </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: the exported JSON \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> {{ ui.text('configJson') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n</div>\n", styles: ["ngx-entity-builder{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-ink: #4f46e5;--deb-accent-soft: #eef2ff;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;--deb-rail: 44px;--deb-left: clamp(320px, 26vw, 460px);--deb-right: 380px;--deb-left-wide: calc(var(--deb-left) + var(--deb-right) - var(--deb-rail));--deb-toolbar-h: 56px;--deb-sticky-top: calc(var(--deb-toolbar-h) + 20px);display:block}.deb-toolbar{background:var(--deb-surface);color:#0f172a;border-bottom:1px solid var(--deb-border);gap:4px;height:var(--deb-toolbar-h);position:sticky;top:0;z-index:20;flex-wrap:wrap;padding-block:4px}mat-toolbar.deb-toolbar{height:auto;min-height:var(--deb-toolbar-h)}.deb-toolbar__mark{color:var(--deb-accent-ink)}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:15px;font-weight:700;color:#0f172a}.deb-toolbar__entity{margin-left:10px;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deb-spacer{flex:1 1 auto}.deb-problems{font-weight:600}.deb-problems--error{color:#ef4444;font-weight:600}.deb-problems-menu__text{white-space:normal;line-height:1.35;padding:6px 0}.deb-problems-menu .mat-mdc-menu-item{height:auto;min-height:40px}.deb-problems-menu{max-width:460px}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;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:var(--deb-left) minmax(0,1fr) var(--deb-right);gap:24px;padding:24px;align-items:start;transition:grid-template-columns .25s cubic-bezier(.16,1,.3,1)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-right)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left-wide) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col{display:flex;flex-direction:column;gap:18px;min-width:0}.deb-col[hidden],.deb-section--fields[hidden],.deb-preview-slot[hidden]{display:none!important}.deb-col>.deb-card{flex:0 0 auto}@keyframes debDockFadeIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes debDockSlideInY{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debSectionExpand{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debColLeftExpand{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes debColRightExpand{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}.deb-toggle-pane{display:grid;grid-template-rows:1fr;opacity:1;transform:translateY(0);transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-toggle-pane--collapsed{grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden}.deb-toggle-pane__inner,.deb-dock-pane__inner{overflow:hidden;min-height:0;padding:4px 2px;margin:-4px -2px}.deb-dock-pane{display:grid;grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden;transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-dock-pane--open{grid-template-rows:1fr;opacity:1;transform:translateY(0);pointer-events:auto;visibility:visible}.deb-col--left:not([hidden]){animation:debColLeftExpand .24s cubic-bezier(.16,1,.3,1) forwards}.deb-col--right:not([hidden]){animation:debColRightExpand .24s cubic-bezier(.16,1,.3,1) forwards}@media(prefers-reduced-motion:reduce){.deb-body,.deb-col--left,.deb-col--right,.deb-col--center,.deb-section--fields,.deb-preview-slot,.deb-dock-btn,.deb-card,.deb-sidebar-toggle-btn{transition:none!important;animation:none!important}}.deb-col--left,.deb-col--right{position:sticky;top:var(--deb-sticky-top);max-height:calc(100vh - var(--deb-sticky-top) - 20px);overflow-y:auto;padding-right:4px;transition:opacity .2s ease}.deb-card{border-radius:12px!important;border:1px solid var(--deb-border, #e2e8f0)!important;background:var(--deb-surface, #ffffff)!important;box-shadow:0 1px 3px #00000008,0 1px 2px #00000005!important;transition:box-shadow .2s ease,border-color .2s ease!important;overflow:hidden}.deb-card:hover{border-color:#cbd5e1!important;box-shadow:0 4px 12px #0f172a0d!important}.deb-card mat-card-header{padding:14px 16px 10px!important;display:flex!important;align-items:center!important}.deb-card mat-card-content{padding:0 16px 16px!important}.deb-card__title{display:flex!important;align-items:center!important;gap:8px!important;font-family:var(--font-heading, sans-serif)!important;font-size:13.5px!important;font-weight:700!important;color:#0f172a!important;margin:0!important}.deb-card__icon{color:var(--deb-accent-ink, #4f46e5)!important;font-size:18px!important;width:18px!important;height:18px!important;flex:none!important}.deb-card__collapse-btn{margin-left:auto!important;width:32px!important;height:32px!important;color:var(--deb-muted, #64748b)!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.deb-card__collapse-btn:hover{color:#0f172a!important;background:#f1f5f9!important}.deb-sidebar-toggle-btn{color:var(--deb-muted, #64748b);transition:all .15s ease}.deb-sidebar-toggle-btn:hover{color:#0f172a;background:#f1f5f9}.deb-sidebar-toggle-btn--active{color:var(--deb-accent-ink, #4f46e5)!important;background:var(--deb-accent-soft, #eef2ff)!important}.deb-dock-btn{position:sticky;top:var(--deb-sticky-top);align-self:start;justify-self:center;z-index:10;display:inline-flex;flex-direction:column;align-items:center;gap:10px;width:36px;padding:12px 0;border-radius:9999px;border:1px solid #c7d2fe;background:#fff;color:var(--deb-accent-hover, #4f46e5);font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;box-shadow:0 4px 14px #6366f126;cursor:pointer;animation:debDockFadeIn .22s cubic-bezier(.16,1,.3,1) forwards;transition:background .2s ease,border-color .2s ease,box-shadow .2s ease}.deb-dock-btn:hover{background:var(--deb-accent-soft, #eef2ff);border-color:var(--deb-accent, #6366f1);box-shadow:0 6px 20px #6366f140}.deb-dock-btn mat-icon{flex:none}.deb-dock-btn__icon{font-size:20px;width:20px;height:20px;color:var(--deb-accent-ink, #4f46e5)}.deb-dock-btn__chevron{font-size:16px;width:16px;height:16px;opacity:.75}.deb-dock-btn__label{display:none;letter-spacing:.04em;white-space:nowrap}.deb-dock-btn--horizontal{flex-direction:row;justify-content:center;width:100%;padding:10px 18px;border-radius:12px;gap:12px;position:static;animation:debDockSlideInY .22s cubic-bezier(.16,1,.3,1) forwards}.deb-dock-btn--horizontal .deb-dock-btn__label{display:inline}.deb-card--preview mat-card-content{padding:0 16px 16px!important}.deb-settings-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-bottom:6px}.deb-mask-row{padding:4px 0 8px}.deb-preview-slot:empty{display:none}@media(max-width:1300px){ngx-entity-builder{--deb-left: clamp(280px, 28vw, 340px)}.deb-body{grid-template-columns:var(--deb-left) minmax(0,1fr)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col--right{grid-column:2;position:static;max-height:none;overflow:visible}}@media(max-width:900px){ngx-entity-builder{--deb-rail: 36px}.deb-body{gap:12px;padding:12px;grid-template-columns:minmax(0,1fr)}.deb-body.deb-body--left-collapsed,.deb-body.deb-body--right-collapsed,.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:minmax(0,1fr)}.deb-col--left,.deb-col--right,.deb-dock-btn--left,.deb-dock-btn--right{grid-column:1;position:static;max-height:none;overflow:visible}.deb-dock-btn--left,.deb-dock-btn--right{flex-direction:row;justify-content:center;width:auto;justify-self:stretch;padding:10px 16px}.deb-dock-btn__label{display:inline}}@media(max-width:900px){.deb-toolbar__title,.deb-toolbar__mark{display:none}}@media(max-width:560px){.deb-toolbar__btn-label{display:none}}.deb-canvas__group{margin:14px 0 6px;font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-canvas__group:first-of-type{margin-top:0}.deb-field-list{display:flex;flex-direction:column;gap:8px}.deb-field-list:empty{min-height:44px;border:1px dashed var(--deb-border);border-radius:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:9px 12px;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-field-row__actions{display:flex;align-items:center;margin-left:auto;opacity:0;transition:opacity .15s ease}.deb-field-row:hover .deb-field-row__actions,.deb-field-row:focus-within .deb-field-row__actions,.deb-field-row--active .deb-field-row__actions{opacity:1}@media(hover:none){.deb-field-row__actions{opacity:1}}@media(max-width:560px){.deb-field-row{flex-wrap:wrap;row-gap:4px}.deb-field-row__actions{margin-left:auto}}.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;flex:none}.deb-span-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;background:#f1f5f9;color:#475569;font-family:ui-monospace,SFMono-Regular,monospace;flex:none}.deb-drag-handle{cursor:grab;color:#7d8a9c;transition:color .15s;flex:none}.deb-drag-handle:hover{color:var(--deb-accent-ink)}.deb-field-type{color:var(--deb-accent-ink);flex:none}.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:#dc2626;font-weight:700;flex:none}.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.deb-field-row{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent, #6366f1);background:#fff}.cdk-drag-placeholder.deb-field-row{opacity:.35}.cdk-drag-animating.deb-field-row{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)}@media(prefers-reduced-motion:reduce){.deb-field-row,.deb-field-row__actions,.cdk-drag-animating.deb-field-row,.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:none}.deb-field-row:hover{transform:none}}.deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__section{border:1px solid var(--deb-border);border-radius:10px;overflow:hidden;background:var(--deb-surface)}.deb-inspector__summary{display:flex;align-items:center;gap:8px;width:100%;padding:10px 12px;cursor:pointer;list-style:none;font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted);background:#f8fafc;-webkit-user-select:none;user-select:none}.deb-inspector__summary::-webkit-details-marker{display:none}.deb-inspector__summary:hover{color:#0f172a}.deb-inspector__summary:focus-visible{outline:2px solid var(--deb-accent);outline-offset:-2px}.deb-inspector__chevron{margin-left:auto;transition:transform .18s ease;color:#7d8a9c;font-size:20px;width:20px;height:20px}.deb-inspector__section[open]>.deb-inspector__summary>.deb-inspector__chevron{transform:rotate(180deg)}.deb-inspector__count{padding:1px 7px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:10px;font-weight:700;letter-spacing:0}.deb-inspector__body{display:flex;flex-direction:column;gap:10px;padding:12px;border-top:1px solid var(--deb-border)}@media(prefers-reduced-motion:reduce){.deb-inspector__chevron{transition:none}}.deb-span-picker{display:flex;gap:4px;flex-wrap:wrap}.deb-span-picker__btn{appearance:none;cursor:pointer;flex:1;min-width:46px;padding:7px 4px;border:1px solid var(--deb-border);border-radius:8px;background:var(--deb-surface);color:var(--deb-muted);font:inherit;font-size:12px;font-weight:700;transition:border-color .15s ease,background .15s ease,color .15s ease}.deb-span-picker__btn:hover{border-color:#a5b4fc;color:#0f172a}.deb-span-picker__btn:focus-visible{outline:2px solid var(--deb-accent);outline-offset:2px}.deb-span-picker__btn--active{border-color:var(--deb-accent);background:var(--deb-accent-soft);color:var(--deb-accent-hover)}.deb-option-row{display:flex;align-items:center;gap:8px}.deb-option-key{flex:0 0 8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:4px 8px;border-radius:4px;background:var(--deb-surface-2, rgba(0, 0, 0, .05));color:var(--deb-muted);font-family:var(--font-mono, ui-monospace, monospace);font-size:12px}.deb-row__actions{display:flex;gap:8px;align-items:center}.deb-flags-row{display:flex;gap:16px;flex-wrap:wrap;padding:2px 0 6px}.deb-param-validators-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-toggles{display:flex;flex-direction:column;gap:6px}.deb-toggles mat-slide-toggle{display:flex;justify-content:space-between;width:100%;padding:5px 8px;border-radius:8px;background:#f8fafc;border:1px solid #f1f5f9;transition:all .15s ease}.deb-toggles mat-slide-toggle:hover{background:#f1f5f9;border-color:#e2e8f0}@media(prefers-reduced-motion:reduce){.deb-span-picker__btn{transition:none}}.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}.deb-path-hint{color:#0000008c;font-size:11px;margin-left:6px}\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[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$3.MatCardTitle, selector: "mat-card-title, [mat-card-title], [matCardTitle]" }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4$2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$2.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: i5.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i8.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatToolbarModule }, { kind: "component", type: i11.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i4$1.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", outputs: ["collapse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
5170
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5171
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: EntityBuilderComponent, isStandalone: true, selector: "ngx-entity-builder", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: false, isRequired: false, transformFunction: null }, rules: { classPropertyName: "rules", publicName: "rules", isSignal: false, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: false, isRequired: false, transformFunction: null }, uiLanguage: { classPropertyName: "uiLanguage", publicName: "uiLanguage", isSignal: false, isRequired: false, transformFunction: null }, availableRoles: { classPropertyName: "availableRoles", publicName: "availableRoles", isSignal: false, isRequired: false, transformFunction: null }, userRoles: { classPropertyName: "userRoles", publicName: "userRoles", isSignal: false, isRequired: false, transformFunction: null }, commonModules: { classPropertyName: "commonModules", publicName: "commonModules", isSignal: false, isRequired: false, transformFunction: null }, leftSidebarOpen: { classPropertyName: "leftSidebarOpen", publicName: "leftSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, rightSidebarOpen: { classPropertyName: "rightSidebarOpen", publicName: "rightSidebarOpen", isSignal: true, isRequired: false, transformFunction: null }, fieldsOpen: { classPropertyName: "fieldsOpen", publicName: "fieldsOpen", isSignal: true, isRequired: false, transformFunction: null }, previewOpen: { classPropertyName: "previewOpen", publicName: "previewOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { configChange: "configChange", rulesChange: "rulesChange", save: "save", leftSidebarOpen: "leftSidebarOpenChange", rightSidebarOpen: "rightSidebarOpenChange", fieldsOpen: "fieldsOpenChange", previewOpen: "previewOpenChange" }, host: { listeners: { "document:keydown": "onKeydown($event)" } }, providers: [BuilderStore], usesOnChanges: true, ngImport: i0, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-button\n type=\"button\"\n data-testid=\"builder-rule-graph\"\n (click)=\"openRuleGraph()\"\n [matTooltip]=\"ui.text('ruleGraph')\"\n [attr.aria-label]=\"ui.text('ruleGraph')\"\n >\n <mat-icon>hub</mat-icon>\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('ruleGraph') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </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 class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\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 <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('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>{{ ui.text('actionRoles', { action }) }}</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>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input matInput [ngModel]=\"rolesFor(action).join(', ')\" (ngModelChange)=\"setRolesFromText(action, $event)\" />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\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, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </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: the exported JSON \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> {{ ui.text('configJson') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n</div>\n\n@if (ruleGraphOpen()) {\n <ngx-rule-dependency-graph\n (dismiss)=\"closeRuleGraph()\"\n (selectField)=\"onSelectFieldFromGraph($event)\"\n />\n}\n", styles: ["ngx-entity-builder{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-ink: #4f46e5;--deb-accent-soft: #eef2ff;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;--deb-rail: 44px;--deb-left: clamp(320px, 26vw, 460px);--deb-right: 380px;--deb-left-wide: calc(var(--deb-left) + var(--deb-right) - var(--deb-rail));--deb-toolbar-h: 56px;--deb-sticky-top: calc(var(--deb-toolbar-h) + 20px);display:block}.deb-toolbar{background:var(--deb-surface);color:#0f172a;border-bottom:1px solid var(--deb-border);gap:4px;height:var(--deb-toolbar-h);position:sticky;top:0;z-index:20;flex-wrap:wrap;padding-block:4px}mat-toolbar.deb-toolbar{height:auto;min-height:var(--deb-toolbar-h)}.deb-toolbar__mark{color:var(--deb-accent-ink)}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:15px;font-weight:700;color:#0f172a}.deb-toolbar__entity{margin-left:10px;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deb-spacer{flex:1 1 auto}.deb-problems{font-weight:600}.deb-problems--error{color:#ef4444;font-weight:600}.deb-problems-menu__text{white-space:normal;line-height:1.35;padding:6px 0}.deb-problems-menu .mat-mdc-menu-item{height:auto;min-height:40px}.deb-problems-menu{max-width:460px}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;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:var(--deb-left) minmax(0,1fr) var(--deb-right);gap:24px;padding:24px;align-items:start;transition:grid-template-columns .25s cubic-bezier(.16,1,.3,1)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-right)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left-wide) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col{display:flex;flex-direction:column;gap:18px;min-width:0}.deb-col[hidden],.deb-section--fields[hidden],.deb-preview-slot[hidden]{display:none!important}.deb-col>.deb-card{flex:0 0 auto}@keyframes debDockFadeIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes debDockSlideInY{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debSectionExpand{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debColLeftExpand{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes debColRightExpand{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}.deb-toggle-pane{display:grid;grid-template-rows:1fr;opacity:1;transform:translateY(0);transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-toggle-pane--collapsed{grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden}.deb-toggle-pane__inner,.deb-dock-pane__inner{overflow:hidden;min-height:0;padding:4px 2px;margin:-4px -2px}.deb-dock-pane{display:grid;grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden;transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-dock-pane--open{grid-template-rows:1fr;opacity:1;transform:translateY(0);pointer-events:auto;visibility:visible}.deb-col--left:not([hidden]){animation:debColLeftExpand .24s cubic-bezier(.16,1,.3,1) forwards}.deb-col--right:not([hidden]){animation:debColRightExpand .24s cubic-bezier(.16,1,.3,1) forwards}@media(prefers-reduced-motion:reduce){.deb-body,.deb-col--left,.deb-col--right,.deb-col--center,.deb-section--fields,.deb-preview-slot,.deb-dock-btn,.deb-card,.deb-sidebar-toggle-btn{transition:none!important;animation:none!important}}.deb-col--left,.deb-col--right{position:sticky;top:var(--deb-sticky-top);max-height:calc(100vh - var(--deb-sticky-top) - 20px);overflow-y:auto;padding-right:4px;transition:opacity .2s ease}.deb-card{border-radius:12px!important;border:1px solid var(--deb-border, #e2e8f0)!important;background:var(--deb-surface, #ffffff)!important;box-shadow:0 1px 3px #00000008,0 1px 2px #00000005!important;transition:box-shadow .2s ease,border-color .2s ease!important;overflow:hidden}.deb-card:hover{border-color:#cbd5e1!important;box-shadow:0 4px 12px #0f172a0d!important}.deb-card mat-card-header{padding:14px 16px 10px!important;display:flex!important;align-items:center!important}.deb-card mat-card-content{padding:0 16px 16px!important}.deb-card__title{display:flex!important;align-items:center!important;gap:8px!important;font-family:var(--font-heading, sans-serif)!important;font-size:13.5px!important;font-weight:700!important;color:#0f172a!important;margin:0!important}.deb-card__icon{color:var(--deb-accent-ink, #4f46e5)!important;font-size:18px!important;width:18px!important;height:18px!important;flex:none!important}.deb-card__collapse-btn{margin-left:auto!important;width:32px!important;height:32px!important;color:var(--deb-muted, #64748b)!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.deb-card__collapse-btn:hover{color:#0f172a!important;background:#f1f5f9!important}.deb-sidebar-toggle-btn{color:var(--deb-muted, #64748b);transition:all .15s ease}.deb-sidebar-toggle-btn:hover{color:#0f172a;background:#f1f5f9}.deb-sidebar-toggle-btn--active{color:var(--deb-accent-ink, #4f46e5)!important;background:var(--deb-accent-soft, #eef2ff)!important}.deb-dock-btn{position:sticky;top:var(--deb-sticky-top);align-self:start;justify-self:center;z-index:10;display:inline-flex;flex-direction:column;align-items:center;gap:10px;width:36px;padding:12px 0;border-radius:9999px;border:1px solid #c7d2fe;background:#fff;color:var(--deb-accent-hover, #4f46e5);font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;box-shadow:0 4px 14px #6366f126;cursor:pointer;animation:debDockFadeIn .22s cubic-bezier(.16,1,.3,1) forwards;transition:background .2s ease,border-color .2s ease,box-shadow .2s ease}.deb-dock-btn:hover{background:var(--deb-accent-soft, #eef2ff);border-color:var(--deb-accent, #6366f1);box-shadow:0 6px 20px #6366f140}.deb-dock-btn mat-icon{flex:none}.deb-dock-btn__icon{font-size:20px;width:20px;height:20px;color:var(--deb-accent-ink, #4f46e5)}.deb-dock-btn__chevron{font-size:16px;width:16px;height:16px;opacity:.75}.deb-dock-btn__label{display:none;letter-spacing:.04em;white-space:nowrap}.deb-dock-btn--horizontal{flex-direction:row;justify-content:center;width:100%;padding:10px 18px;border-radius:12px;gap:12px;position:static;animation:debDockSlideInY .22s cubic-bezier(.16,1,.3,1) forwards}.deb-dock-btn--horizontal .deb-dock-btn__label{display:inline}.deb-card--preview mat-card-content{padding:0 16px 16px!important}.deb-settings-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-bottom:6px}.deb-mask-row{padding:4px 0 8px}.deb-preview-slot:empty{display:none}@media(max-width:1300px){ngx-entity-builder{--deb-left: clamp(280px, 28vw, 340px)}.deb-body{grid-template-columns:var(--deb-left) minmax(0,1fr)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col--right{grid-column:2;position:static;max-height:none;overflow:visible}}@media(max-width:900px){ngx-entity-builder{--deb-rail: 36px}.deb-body{gap:12px;padding:12px;grid-template-columns:minmax(0,1fr)}.deb-body.deb-body--left-collapsed,.deb-body.deb-body--right-collapsed,.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:minmax(0,1fr)}.deb-col--left,.deb-col--right,.deb-dock-btn--left,.deb-dock-btn--right{grid-column:1;position:static;max-height:none;overflow:visible}.deb-dock-btn--left,.deb-dock-btn--right{flex-direction:row;justify-content:center;width:auto;justify-self:stretch;padding:10px 16px}.deb-dock-btn__label{display:inline}}@media(max-width:900px){.deb-toolbar__title,.deb-toolbar__mark{display:none}}@media(max-width:560px){.deb-toolbar__btn-label{display:none}}.deb-canvas__group{margin:14px 0 6px;font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-canvas__group:first-of-type{margin-top:0}.deb-field-list{display:flex;flex-direction:column;gap:8px}.deb-field-list:empty{min-height:44px;border:1px dashed var(--deb-border);border-radius:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:9px 12px;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-field-row__actions{display:flex;align-items:center;margin-left:auto;opacity:0;transition:opacity .15s ease}.deb-field-row:hover .deb-field-row__actions,.deb-field-row:focus-within .deb-field-row__actions,.deb-field-row--active .deb-field-row__actions{opacity:1}@media(hover:none){.deb-field-row__actions{opacity:1}}@media(max-width:560px){.deb-field-row{flex-wrap:wrap;row-gap:4px}.deb-field-row__actions{margin-left:auto}}.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;flex:none}.deb-span-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;background:#f1f5f9;color:#475569;font-family:ui-monospace,SFMono-Regular,monospace;flex:none}.deb-drag-handle{cursor:grab;color:#7d8a9c;transition:color .15s;flex:none}.deb-drag-handle:hover{color:var(--deb-accent-ink)}.deb-field-type{color:var(--deb-accent-ink);flex:none}.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:#dc2626;font-weight:700;flex:none}.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:var(--deb-muted)}.cdk-drag-preview.deb-field-row{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent, #6366f1);background:#fff}.cdk-drag-placeholder.deb-field-row{opacity:.35}.cdk-drag-animating.deb-field-row{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)}@media(prefers-reduced-motion:reduce){.deb-field-row,.deb-field-row__actions,.cdk-drag-animating.deb-field-row,.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:none}.deb-field-row:hover{transform:none}}.deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__section{border:1px solid var(--deb-border);border-radius:10px;overflow:hidden;background:var(--deb-surface)}.deb-inspector__summary{display:flex;align-items:center;gap:8px;width:100%;padding:10px 12px;cursor:pointer;list-style:none;font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted);background:#f8fafc;-webkit-user-select:none;user-select:none}.deb-inspector__summary::-webkit-details-marker{display:none}.deb-inspector__summary:hover{color:#0f172a}.deb-inspector__summary:focus-visible{outline:2px solid var(--deb-accent);outline-offset:-2px}.deb-inspector__chevron{margin-left:auto;transition:transform .18s ease;color:#7d8a9c;font-size:20px;width:20px;height:20px}.deb-inspector__section[open]>.deb-inspector__summary>.deb-inspector__chevron{transform:rotate(180deg)}.deb-inspector__count{padding:1px 7px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:10px;font-weight:700;letter-spacing:0}.deb-inspector__body{display:flex;flex-direction:column;gap:10px;padding:12px;border-top:1px solid var(--deb-border)}@media(prefers-reduced-motion:reduce){.deb-inspector__chevron{transition:none}}.deb-span-picker{display:flex;gap:4px;flex-wrap:wrap}.deb-span-picker__btn{appearance:none;cursor:pointer;flex:1;min-width:46px;padding:7px 4px;border:1px solid var(--deb-border);border-radius:8px;background:var(--deb-surface);color:var(--deb-muted);font:inherit;font-size:12px;font-weight:700;transition:border-color .15s ease,background .15s ease,color .15s ease}.deb-span-picker__btn:hover{border-color:#a5b4fc;color:#0f172a}.deb-span-picker__btn:focus-visible{outline:2px solid var(--deb-accent);outline-offset:2px}.deb-span-picker__btn--active{border-color:var(--deb-accent);background:var(--deb-accent-soft);color:var(--deb-accent-hover)}.deb-option-row{display:flex;align-items:center;gap:8px}.deb-option-key{flex:0 0 8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:4px 8px;border-radius:4px;background:var(--deb-surface-2, rgba(0, 0, 0, .05));color:var(--deb-muted);font-family:var(--font-mono, ui-monospace, monospace);font-size:12px}.deb-row__actions{display:flex;gap:8px;align-items:center}.deb-flags-row{display:flex;gap:16px;flex-wrap:wrap;padding:2px 0 6px}.deb-param-validators-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-toggles{display:flex;flex-direction:column;gap:6px}.deb-toggles mat-slide-toggle{display:flex;justify-content:space-between;width:100%;padding:5px 8px;border-radius:8px;background:#f8fafc;border:1px solid #f1f5f9;transition:all .15s ease}.deb-toggles mat-slide-toggle:hover{background:#f1f5f9;border-color:#e2e8f0}@media(prefers-reduced-motion:reduce){.deb-span-picker__btn{transition:none}}.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}.deb-path-hint{color:#0000008c;font-size:11px;margin-left:6px}\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[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$3.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$3.MatCardContent, selector: "mat-card-content" }, { kind: "component", type: i3$3.MatCardHeader, selector: "mat-card-header" }, { kind: "directive", type: i3$3.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: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i8.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i7.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i3$1.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: 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", outputs: ["collapse"] }, { kind: "component", type: RuleDependencyGraphComponent, selector: "ngx-rule-dependency-graph", outputs: ["dismiss", "selectField"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
4493
5172
|
}
|
|
4494
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
5173
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: EntityBuilderComponent, decorators: [{
|
|
4495
5174
|
type: Component,
|
|
4496
5175
|
args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'ngx-entity-builder', standalone: true, providers: [BuilderStore], imports: [
|
|
4497
5176
|
FormsModule,
|
|
@@ -4511,7 +5190,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4511
5190
|
TabManagerComponent,
|
|
4512
5191
|
FieldInspectorComponent,
|
|
4513
5192
|
EntityBuilderCanvasComponent,
|
|
4514
|
-
], encapsulation: ViewEncapsulation.None, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </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 class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\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 <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('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>{{ ui.text('actionRoles', { action }) }}</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>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input matInput [ngModel]=\"rolesFor(action).join(', ')\" (ngModelChange)=\"setRolesFromText(action, $event)\" />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\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, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </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: the exported JSON \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> {{ ui.text('configJson') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n</div>\n", styles: ["ngx-entity-builder{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-ink: #4f46e5;--deb-accent-soft: #eef2ff;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;--deb-rail: 44px;--deb-left: clamp(320px, 26vw, 460px);--deb-right: 380px;--deb-left-wide: calc(var(--deb-left) + var(--deb-right) - var(--deb-rail));--deb-toolbar-h: 56px;--deb-sticky-top: calc(var(--deb-toolbar-h) + 20px);display:block}.deb-toolbar{background:var(--deb-surface);color:#0f172a;border-bottom:1px solid var(--deb-border);gap:4px;height:var(--deb-toolbar-h);position:sticky;top:0;z-index:20;flex-wrap:wrap;padding-block:4px}mat-toolbar.deb-toolbar{height:auto;min-height:var(--deb-toolbar-h)}.deb-toolbar__mark{color:var(--deb-accent-ink)}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:15px;font-weight:700;color:#0f172a}.deb-toolbar__entity{margin-left:10px;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deb-spacer{flex:1 1 auto}.deb-problems{font-weight:600}.deb-problems--error{color:#ef4444;font-weight:600}.deb-problems-menu__text{white-space:normal;line-height:1.35;padding:6px 0}.deb-problems-menu .mat-mdc-menu-item{height:auto;min-height:40px}.deb-problems-menu{max-width:460px}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;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:var(--deb-left) minmax(0,1fr) var(--deb-right);gap:24px;padding:24px;align-items:start;transition:grid-template-columns .25s cubic-bezier(.16,1,.3,1)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-right)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left-wide) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col{display:flex;flex-direction:column;gap:18px;min-width:0}.deb-col[hidden],.deb-section--fields[hidden],.deb-preview-slot[hidden]{display:none!important}.deb-col>.deb-card{flex:0 0 auto}@keyframes debDockFadeIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes debDockSlideInY{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debSectionExpand{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debColLeftExpand{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes debColRightExpand{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}.deb-toggle-pane{display:grid;grid-template-rows:1fr;opacity:1;transform:translateY(0);transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-toggle-pane--collapsed{grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden}.deb-toggle-pane__inner,.deb-dock-pane__inner{overflow:hidden;min-height:0;padding:4px 2px;margin:-4px -2px}.deb-dock-pane{display:grid;grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden;transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-dock-pane--open{grid-template-rows:1fr;opacity:1;transform:translateY(0);pointer-events:auto;visibility:visible}.deb-col--left:not([hidden]){animation:debColLeftExpand .24s cubic-bezier(.16,1,.3,1) forwards}.deb-col--right:not([hidden]){animation:debColRightExpand .24s cubic-bezier(.16,1,.3,1) forwards}@media(prefers-reduced-motion:reduce){.deb-body,.deb-col--left,.deb-col--right,.deb-col--center,.deb-section--fields,.deb-preview-slot,.deb-dock-btn,.deb-card,.deb-sidebar-toggle-btn{transition:none!important;animation:none!important}}.deb-col--left,.deb-col--right{position:sticky;top:var(--deb-sticky-top);max-height:calc(100vh - var(--deb-sticky-top) - 20px);overflow-y:auto;padding-right:4px;transition:opacity .2s ease}.deb-card{border-radius:12px!important;border:1px solid var(--deb-border, #e2e8f0)!important;background:var(--deb-surface, #ffffff)!important;box-shadow:0 1px 3px #00000008,0 1px 2px #00000005!important;transition:box-shadow .2s ease,border-color .2s ease!important;overflow:hidden}.deb-card:hover{border-color:#cbd5e1!important;box-shadow:0 4px 12px #0f172a0d!important}.deb-card mat-card-header{padding:14px 16px 10px!important;display:flex!important;align-items:center!important}.deb-card mat-card-content{padding:0 16px 16px!important}.deb-card__title{display:flex!important;align-items:center!important;gap:8px!important;font-family:var(--font-heading, sans-serif)!important;font-size:13.5px!important;font-weight:700!important;color:#0f172a!important;margin:0!important}.deb-card__icon{color:var(--deb-accent-ink, #4f46e5)!important;font-size:18px!important;width:18px!important;height:18px!important;flex:none!important}.deb-card__collapse-btn{margin-left:auto!important;width:32px!important;height:32px!important;color:var(--deb-muted, #64748b)!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.deb-card__collapse-btn:hover{color:#0f172a!important;background:#f1f5f9!important}.deb-sidebar-toggle-btn{color:var(--deb-muted, #64748b);transition:all .15s ease}.deb-sidebar-toggle-btn:hover{color:#0f172a;background:#f1f5f9}.deb-sidebar-toggle-btn--active{color:var(--deb-accent-ink, #4f46e5)!important;background:var(--deb-accent-soft, #eef2ff)!important}.deb-dock-btn{position:sticky;top:var(--deb-sticky-top);align-self:start;justify-self:center;z-index:10;display:inline-flex;flex-direction:column;align-items:center;gap:10px;width:36px;padding:12px 0;border-radius:9999px;border:1px solid #c7d2fe;background:#fff;color:var(--deb-accent-hover, #4f46e5);font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;box-shadow:0 4px 14px #6366f126;cursor:pointer;animation:debDockFadeIn .22s cubic-bezier(.16,1,.3,1) forwards;transition:background .2s ease,border-color .2s ease,box-shadow .2s ease}.deb-dock-btn:hover{background:var(--deb-accent-soft, #eef2ff);border-color:var(--deb-accent, #6366f1);box-shadow:0 6px 20px #6366f140}.deb-dock-btn mat-icon{flex:none}.deb-dock-btn__icon{font-size:20px;width:20px;height:20px;color:var(--deb-accent-ink, #4f46e5)}.deb-dock-btn__chevron{font-size:16px;width:16px;height:16px;opacity:.75}.deb-dock-btn__label{display:none;letter-spacing:.04em;white-space:nowrap}.deb-dock-btn--horizontal{flex-direction:row;justify-content:center;width:100%;padding:10px 18px;border-radius:12px;gap:12px;position:static;animation:debDockSlideInY .22s cubic-bezier(.16,1,.3,1) forwards}.deb-dock-btn--horizontal .deb-dock-btn__label{display:inline}.deb-card--preview mat-card-content{padding:0 16px 16px!important}.deb-settings-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-bottom:6px}.deb-mask-row{padding:4px 0 8px}.deb-preview-slot:empty{display:none}@media(max-width:1300px){ngx-entity-builder{--deb-left: clamp(280px, 28vw, 340px)}.deb-body{grid-template-columns:var(--deb-left) minmax(0,1fr)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col--right{grid-column:2;position:static;max-height:none;overflow:visible}}@media(max-width:900px){ngx-entity-builder{--deb-rail: 36px}.deb-body{gap:12px;padding:12px;grid-template-columns:minmax(0,1fr)}.deb-body.deb-body--left-collapsed,.deb-body.deb-body--right-collapsed,.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:minmax(0,1fr)}.deb-col--left,.deb-col--right,.deb-dock-btn--left,.deb-dock-btn--right{grid-column:1;position:static;max-height:none;overflow:visible}.deb-dock-btn--left,.deb-dock-btn--right{flex-direction:row;justify-content:center;width:auto;justify-self:stretch;padding:10px 16px}.deb-dock-btn__label{display:inline}}@media(max-width:900px){.deb-toolbar__title,.deb-toolbar__mark{display:none}}@media(max-width:560px){.deb-toolbar__btn-label{display:none}}.deb-canvas__group{margin:14px 0 6px;font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-canvas__group:first-of-type{margin-top:0}.deb-field-list{display:flex;flex-direction:column;gap:8px}.deb-field-list:empty{min-height:44px;border:1px dashed var(--deb-border);border-radius:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:9px 12px;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-field-row__actions{display:flex;align-items:center;margin-left:auto;opacity:0;transition:opacity .15s ease}.deb-field-row:hover .deb-field-row__actions,.deb-field-row:focus-within .deb-field-row__actions,.deb-field-row--active .deb-field-row__actions{opacity:1}@media(hover:none){.deb-field-row__actions{opacity:1}}@media(max-width:560px){.deb-field-row{flex-wrap:wrap;row-gap:4px}.deb-field-row__actions{margin-left:auto}}.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;flex:none}.deb-span-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;background:#f1f5f9;color:#475569;font-family:ui-monospace,SFMono-Regular,monospace;flex:none}.deb-drag-handle{cursor:grab;color:#7d8a9c;transition:color .15s;flex:none}.deb-drag-handle:hover{color:var(--deb-accent-ink)}.deb-field-type{color:var(--deb-accent-ink);flex:none}.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:#dc2626;font-weight:700;flex:none}.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.deb-field-row{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent, #6366f1);background:#fff}.cdk-drag-placeholder.deb-field-row{opacity:.35}.cdk-drag-animating.deb-field-row{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)}@media(prefers-reduced-motion:reduce){.deb-field-row,.deb-field-row__actions,.cdk-drag-animating.deb-field-row,.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:none}.deb-field-row:hover{transform:none}}.deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__section{border:1px solid var(--deb-border);border-radius:10px;overflow:hidden;background:var(--deb-surface)}.deb-inspector__summary{display:flex;align-items:center;gap:8px;width:100%;padding:10px 12px;cursor:pointer;list-style:none;font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted);background:#f8fafc;-webkit-user-select:none;user-select:none}.deb-inspector__summary::-webkit-details-marker{display:none}.deb-inspector__summary:hover{color:#0f172a}.deb-inspector__summary:focus-visible{outline:2px solid var(--deb-accent);outline-offset:-2px}.deb-inspector__chevron{margin-left:auto;transition:transform .18s ease;color:#7d8a9c;font-size:20px;width:20px;height:20px}.deb-inspector__section[open]>.deb-inspector__summary>.deb-inspector__chevron{transform:rotate(180deg)}.deb-inspector__count{padding:1px 7px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:10px;font-weight:700;letter-spacing:0}.deb-inspector__body{display:flex;flex-direction:column;gap:10px;padding:12px;border-top:1px solid var(--deb-border)}@media(prefers-reduced-motion:reduce){.deb-inspector__chevron{transition:none}}.deb-span-picker{display:flex;gap:4px;flex-wrap:wrap}.deb-span-picker__btn{appearance:none;cursor:pointer;flex:1;min-width:46px;padding:7px 4px;border:1px solid var(--deb-border);border-radius:8px;background:var(--deb-surface);color:var(--deb-muted);font:inherit;font-size:12px;font-weight:700;transition:border-color .15s ease,background .15s ease,color .15s ease}.deb-span-picker__btn:hover{border-color:#a5b4fc;color:#0f172a}.deb-span-picker__btn:focus-visible{outline:2px solid var(--deb-accent);outline-offset:2px}.deb-span-picker__btn--active{border-color:var(--deb-accent);background:var(--deb-accent-soft);color:var(--deb-accent-hover)}.deb-option-row{display:flex;align-items:center;gap:8px}.deb-option-key{flex:0 0 8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:4px 8px;border-radius:4px;background:var(--deb-surface-2, rgba(0, 0, 0, .05));color:var(--deb-muted);font-family:var(--font-mono, ui-monospace, monospace);font-size:12px}.deb-row__actions{display:flex;gap:8px;align-items:center}.deb-flags-row{display:flex;gap:16px;flex-wrap:wrap;padding:2px 0 6px}.deb-param-validators-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-toggles{display:flex;flex-direction:column;gap:6px}.deb-toggles mat-slide-toggle{display:flex;justify-content:space-between;width:100%;padding:5px 8px;border-radius:8px;background:#f8fafc;border:1px solid #f1f5f9;transition:all .15s ease}.deb-toggles mat-slide-toggle:hover{background:#f1f5f9;border-color:#e2e8f0}@media(prefers-reduced-motion:reduce){.deb-span-picker__btn{transition:none}}.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}.deb-path-hint{color:#0000008c;font-size:11px;margin-left:6px}\n"] }]
|
|
5193
|
+
RuleDependencyGraphComponent,
|
|
5194
|
+
], encapsulation: ViewEncapsulation.None, template: "<mat-toolbar class=\"deb-toolbar\">\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-left-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"leftSidebarOpen()\"\n [attr.aria-expanded]=\"leftSidebarOpen()\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"toggleLeftSidebar()\"\n [matTooltip]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n [attr.aria-label]=\"leftSidebarOpen() ? ui.text('collapsePalette') : ui.text('expandPalette')\"\n >\n <mat-icon>{{ leftSidebarOpen() ? 'menu_open' : 'menu' }}</mat-icon>\n </button>\n\n <mat-icon class=\"deb-toolbar__mark\">dashboard_customize</mat-icon>\n <span class=\"deb-toolbar__title\">{{ ui.text('builderTitle') }}</span>\n <span class=\"deb-toolbar__entity\" data-testid=\"builder-toolbar-entity\">{{ store.config().entity || '\u2014' }}</span>\n <span class=\"deb-spacer\"></span>\n\n <!--\n The issue count, and the issues.\n It used to be a button labelled \"0 / 1\" whose tooltip said to hover the items in the list\n \u2014 a list that did not exist anywhere in the builder. The count now opens the problems\n themselves, and an entry that names a field selects it, which is the action the person\n reading the message wants to take next.\n -->\n @if (store.problems().length) {\n <button\n mat-button\n type=\"button\"\n class=\"deb-problems\"\n data-testid=\"builder-problems\"\n [class.deb-problems--error]=\"!store.isValid()\"\n [matMenuTriggerFor]=\"problemsMenu\"\n [matTooltip]=\"ui.text('issuesTooltip', { count: store.problems().length })\"\n >\n <mat-icon>{{ store.isValid() ? 'warning' : 'error' }}</mat-icon>\n {{ ui.text('issuesCount', { errors: store.errors().length, total: store.problems().length }) }}\n </button>\n <mat-menu #problemsMenu=\"matMenu\" class=\"deb-problems-menu\">\n @for (problem of store.problems(); track $index) {\n <button\n mat-menu-item\n type=\"button\"\n data-testid=\"builder-problem\"\n [disabled]=\"!problem.fieldId\"\n (click)=\"selectProblemField(problem)\"\n >\n <mat-icon [class.deb-problems--error]=\"problem.level === 'error'\">{{\n problem.level === 'error' ? 'error' : 'warning'\n }}</mat-icon>\n <span class=\"deb-problems-menu__text\">{{ problem.message }}</span>\n </button>\n }\n </mat-menu>\n }\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-undo\"\n [disabled]=\"!store.canUndo()\"\n (click)=\"store.undo()\"\n [matTooltip]=\"ui.text('undoTooltip')\"\n [attr.aria-label]=\"ui.text('undo')\"\n >\n <mat-icon>undo</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"builder-redo\"\n [disabled]=\"!store.canRedo()\"\n (click)=\"store.redo()\"\n [matTooltip]=\"ui.text('redoTooltip')\"\n [attr.aria-label]=\"ui.text('redo')\"\n >\n <mat-icon>redo</mat-icon>\n </button>\n\n <button\n mat-button\n type=\"button\"\n (click)=\"copyJson()\"\n [matTooltip]=\"ui.text('copyJson')\"\n [attr.aria-label]=\"ui.text('copyJson')\"\n >\n <mat-icon>content_copy</mat-icon>\n <!-- The label goes on a narrow screen; the tooltip and aria-label above carry the name. -->\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('copyJson') }}</span>\n </button>\n\n <button\n mat-button\n type=\"button\"\n data-testid=\"builder-rule-graph\"\n (click)=\"openRuleGraph()\"\n [matTooltip]=\"ui.text('ruleGraph')\"\n [attr.aria-label]=\"ui.text('ruleGraph')\"\n >\n <mat-icon>hub</mat-icon>\n <span class=\"deb-toolbar__btn-label\">{{ ui.text('ruleGraph') }}</span>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-fields\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"fieldsOpen()\"\n [attr.aria-expanded]=\"fieldsOpen()\"\n aria-controls=\"deb-fields-section\"\n (click)=\"toggleFields()\"\n [matTooltip]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n [attr.aria-label]=\"fieldsOpen() ? ui.text('collapseFields') : ui.text('expandFields')\"\n >\n <mat-icon>{{ fieldsOpen() ? 'view_agenda' : 'table_rows' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-preview\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"previewOpen()\"\n [attr.aria-expanded]=\"previewOpen()\"\n aria-controls=\"deb-preview-section\"\n (click)=\"togglePreview()\"\n [matTooltip]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n [attr.aria-label]=\"previewOpen() ? ui.text('collapsePreview') : ui.text('expandPreview')\"\n >\n <mat-icon>{{ previewOpen() ? 'visibility' : 'visibility_off' }}</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n data-testid=\"toggle-right-sidebar\"\n class=\"deb-sidebar-toggle-btn\"\n [class.deb-sidebar-toggle-btn--active]=\"rightSidebarOpen()\"\n [attr.aria-expanded]=\"rightSidebarOpen()\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"toggleRightSidebar()\"\n [matTooltip]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n [attr.aria-label]=\"rightSidebarOpen() ? ui.text('collapseInspector') : ui.text('expandInspector')\"\n >\n <mat-icon>{{ rightSidebarOpen() ? 'vertical_split' : 'tune' }}</mat-icon>\n </button>\n\n <!--\n The tooltip sits on a wrapper, not on the button.\n\n A disabled button receives no pointer events, so a tooltip bound to it never fires \u2014 which\n is the one state where the explanation is worth having. Saving stays disabled here (unlike\n the renderer's Save, which refuses instead): an invalid *schema* is not something the\n author can be shown their way out of one field at a time, and the issue list beside this\n button is where that conversation happens.\n -->\n <span [matTooltip]=\"store.isValid() ? '' : ui.text('saveBlockedTooltip')\" [matTooltipDisabled]=\"store.isValid()\">\n <!-- primary, not accent: white on Material's default pink accent is 3.33:1, below AA. -->\n <button mat-flat-button color=\"primary\" type=\"button\" [disabled]=\"!store.isValid()\" (click)=\"doSave()\">\n <mat-icon>save</mat-icon> {{ ui.text('save') }}\n </button>\n </span>\n</mat-toolbar>\n\n<div\n class=\"deb-body\"\n [class.deb-body--left-collapsed]=\"!leftSidebarOpen()\"\n [class.deb-body--right-collapsed]=\"!rightSidebarOpen()\"\n>\n <!-- Collapsed left sidebar: the rail that brings it back. -->\n @if (!leftSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--left\"\n data-testid=\"expand-left-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"expandLeft()\"\n [matTooltip]=\"ui.text('expandPalette')\"\n [attr.aria-label]=\"ui.text('expandPalette')\"\n >\n <!--\n The card's own icon, not its name turned on its side. A 44px rail can only hold a\n label by rotating it, and a rotated word is one you have to tilt your head to read \u2014\n worse in German, where \"Eigenschaften\" is half a screen tall. The tooltip and the\n accessible name carry the words; the label below returns when the rail lies flat.\n -->\n <mat-icon class=\"deb-dock-btn__icon\">add_circle_outline</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('paletteRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_right</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Left: settings, palette, tabs \u2500\u2500\u2500 -->\n <div\n id=\"deb-left-sidebar\"\n class=\"deb-col deb-col--left\"\n data-testid=\"builder-left-sidebar\"\n [hidden]=\"!leftSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--palette\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">add_circle_outline</mat-icon>\n {{ ui.text('addField') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-left-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-left-sidebar\"\n (click)=\"collapseLeft()\"\n [matTooltip]=\"ui.text('collapsePalette')\"\n [attr.aria-label]=\"ui.text('collapsePalette')\"\n >\n <mat-icon>chevron_left</mat-icon>\n </button>\n </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 class=\"deb-card deb-card--settings\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ store.config().entity || ui.text('entityName') }}\n </mat-card-title>\n </mat-card-header>\n <mat-card-content>\n <div class=\"deb-settings-grid\">\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('entityName') }}</mat-label>\n <input\n matInput\n [ngModel]=\"store.config().entity\"\n (ngModelChange)=\"store.setEntityName($event)\"\n [placeholder]=\"ui.text('entityNamePlaceholder')\"\n />\n </mat-form-field>\n\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"deb-full\">\n <mat-label>{{ ui.text('editingLanguage') }}</mat-label>\n <mat-select\n data-testid=\"builder-editing-language\"\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 <div class=\"deb-mask-row\">\n <mat-slide-toggle\n color=\"primary\"\n [ngModel]=\"store.config().maskData ?? false\"\n (ngModelChange)=\"store.setMaskData($event)\"\n >{{ ui.text('maskEntity') }}</mat-slide-toggle\n >\n </div>\n\n <mat-expansion-panel class=\"deb-rbac\">\n <mat-expansion-panel-header>\n <mat-panel-title>{{ ui.text('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>{{ ui.text('actionRoles', { action }) }}</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>{{ ui.text('actionRolesPlaceholder', { action }) }}</mat-label>\n <input matInput [ngModel]=\"rolesFor(action).join(', ')\" (ngModelChange)=\"setRolesFromText(action, $event)\" />\n </mat-form-field>\n }\n }\n <p class=\"deb-hint\">{{ ui.text('permissionsHint') }}</p>\n </mat-expansion-panel>\n </mat-card-content>\n </mat-card>\n\n <mat-card class=\"deb-card deb-card--tabs\">\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, then what it renders as \u2500\u2500\u2500 -->\n <div class=\"deb-col deb-col--center\" data-testid=\"builder-canvas-column\">\n <!-- Collapsed fields canvas dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--fields\"\n [class.deb-dock-pane--open]=\"!fieldsOpen()\"\n [attr.aria-hidden]=\"fieldsOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--fields\"\n data-testid=\"expand-fields\"\n aria-expanded=\"false\"\n aria-controls=\"deb-fields-section\"\n (click)=\"expandFields()\"\n [matTooltip]=\"ui.text('expandFields')\"\n [attr.aria-label]=\"ui.text('expandFields')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">format_list_bulleted</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('fieldsRailLabel') }} ({{ store.fields().length }})</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_down</mat-icon>\n </button>\n </div>\n </div>\n\n <!-- Fields canvas section -->\n <div\n id=\"deb-fields-section\"\n class=\"deb-toggle-pane deb-section--fields\"\n [class.deb-toggle-pane--collapsed]=\"!fieldsOpen()\"\n data-testid=\"builder-fields-section\"\n [attr.aria-hidden]=\"!fieldsOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <ngx-entity-builder-canvas (collapse)=\"collapseFields()\" />\n </div>\n </div>\n\n <!-- Collapsed preview dock bar -->\n <div\n class=\"deb-dock-pane deb-dock-pane--preview\"\n [class.deb-dock-pane--open]=\"!previewOpen()\"\n [attr.aria-hidden]=\"previewOpen()\"\n >\n <div class=\"deb-dock-pane__inner\">\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--horizontal deb-dock-btn--preview\"\n data-testid=\"expand-preview\"\n aria-expanded=\"false\"\n aria-controls=\"deb-preview-section\"\n (click)=\"expandPreview()\"\n [matTooltip]=\"ui.text('expandPreview')\"\n [attr.aria-label]=\"ui.text('expandPreview')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">visibility</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('previewRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">keyboard_arrow_up</mat-icon>\n </button>\n </div>\n </div>\n\n <!--\n The live preview, moved up from the footer.\n Directly under the field list is where a change and its effect can be seen in the same glance.\n -->\n <div\n id=\"deb-preview-section\"\n class=\"deb-toggle-pane deb-preview-slot\"\n [class.deb-toggle-pane--collapsed]=\"!previewOpen()\"\n data-testid=\"builder-preview-section\"\n [attr.aria-hidden]=\"!previewOpen()\"\n >\n <div class=\"deb-toggle-pane__inner\">\n <mat-card class=\"deb-card deb-card--preview\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">visibility</mat-icon>\n {{ ui.text('previewHeading') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-preview\"\n aria-expanded=\"true\"\n aria-controls=\"deb-preview-section\"\n (click)=\"collapsePreview()\"\n [matTooltip]=\"ui.text('collapsePreview')\"\n [attr.aria-label]=\"ui.text('collapsePreview')\"\n >\n <mat-icon>keyboard_arrow_down</mat-icon>\n </button>\n </mat-card-header>\n <mat-card-content>\n <ng-content select=\"[ngxBuilderPreview]\"></ng-content>\n </mat-card-content>\n </mat-card>\n </div>\n </div>\n </div>\n\n <!-- Collapsed right sidebar: the rail that brings it back. -->\n @if (!rightSidebarOpen()) {\n <button\n type=\"button\"\n class=\"deb-dock-btn deb-dock-btn--right\"\n data-testid=\"expand-right-sidebar\"\n aria-expanded=\"false\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"expandRight()\"\n [matTooltip]=\"ui.text('expandInspector')\"\n [attr.aria-label]=\"ui.text('expandInspector')\"\n >\n <mat-icon class=\"deb-dock-btn__icon\">tune</mat-icon>\n <span class=\"deb-dock-btn__label\">{{ ui.text('inspectorRailLabel') }}</span>\n <mat-icon class=\"deb-dock-btn__chevron\">chevron_left</mat-icon>\n </button>\n }\n\n <!-- \u2500\u2500\u2500 Right: inspector \u2500\u2500\u2500 -->\n <div\n id=\"deb-right-sidebar\"\n class=\"deb-col deb-col--right\"\n data-testid=\"builder-right-sidebar\"\n [hidden]=\"!rightSidebarOpen()\"\n >\n <mat-card class=\"deb-card deb-card--inspector\">\n <mat-card-header>\n <mat-card-title class=\"deb-card__title\">\n <mat-icon class=\"deb-card__icon\">tune</mat-icon>\n {{ ui.text('fieldProperties') }}\n </mat-card-title>\n <button\n mat-icon-button\n type=\"button\"\n class=\"deb-card__collapse-btn\"\n data-testid=\"collapse-right-sidebar\"\n aria-expanded=\"true\"\n aria-controls=\"deb-right-sidebar\"\n (click)=\"collapseRight()\"\n [matTooltip]=\"ui.text('collapseInspector')\"\n [attr.aria-label]=\"ui.text('collapseInspector')\"\n >\n <mat-icon>chevron_right</mat-icon>\n </button>\n </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: the exported JSON \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> {{ ui.text('configJson') }}</mat-panel-title>\n </mat-expansion-panel-header>\n <pre class=\"deb-json\">{{ json }}</pre>\n </mat-expansion-panel>\n</div>\n\n@if (ruleGraphOpen()) {\n <ngx-rule-dependency-graph\n (dismiss)=\"closeRuleGraph()\"\n (selectField)=\"onSelectFieldFromGraph($event)\"\n />\n}\n", styles: ["ngx-entity-builder{--deb-accent: #6366f1;--deb-accent-hover: #4f46e5;--deb-accent-ink: #4f46e5;--deb-accent-soft: #eef2ff;--deb-border: #e2e8f0;--deb-muted: #64748b;--deb-surface: #ffffff;--deb-rail: 44px;--deb-left: clamp(320px, 26vw, 460px);--deb-right: 380px;--deb-left-wide: calc(var(--deb-left) + var(--deb-right) - var(--deb-rail));--deb-toolbar-h: 56px;--deb-sticky-top: calc(var(--deb-toolbar-h) + 20px);display:block}.deb-toolbar{background:var(--deb-surface);color:#0f172a;border-bottom:1px solid var(--deb-border);gap:4px;height:var(--deb-toolbar-h);position:sticky;top:0;z-index:20;flex-wrap:wrap;padding-block:4px}mat-toolbar.deb-toolbar{height:auto;min-height:var(--deb-toolbar-h)}.deb-toolbar__mark{color:var(--deb-accent-ink)}.deb-toolbar__title{margin-left:8px;font-family:var(--font-heading, sans-serif);font-size:15px;font-weight:700;color:#0f172a}.deb-toolbar__entity{margin-left:10px;padding:3px 10px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-family:ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:600;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.deb-spacer{flex:1 1 auto}.deb-problems{font-weight:600}.deb-problems--error{color:#ef4444;font-weight:600}.deb-problems-menu__text{white-space:normal;line-height:1.35;padding:6px 0}.deb-problems-menu .mat-mdc-menu-item{height:auto;min-height:40px}.deb-problems-menu{max-width:460px}.deb-full{width:100%}.deb-grow{flex:1}.deb-row{display:flex;gap:8px;align-items:center}.deb-row--split{justify-content:space-between}.deb-section-title{font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;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:var(--deb-left) minmax(0,1fr) var(--deb-right);gap:24px;padding:24px;align-items:start;transition:grid-template-columns .25s cubic-bezier(.16,1,.3,1)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-right)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left-wide) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col{display:flex;flex-direction:column;gap:18px;min-width:0}.deb-col[hidden],.deb-section--fields[hidden],.deb-preview-slot[hidden]{display:none!important}.deb-col>.deb-card{flex:0 0 auto}@keyframes debDockFadeIn{0%{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}@keyframes debDockSlideInY{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debSectionExpand{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@keyframes debColLeftExpand{0%{opacity:0;transform:translate(-12px)}to{opacity:1;transform:translate(0)}}@keyframes debColRightExpand{0%{opacity:0;transform:translate(12px)}to{opacity:1;transform:translate(0)}}.deb-toggle-pane{display:grid;grid-template-rows:1fr;opacity:1;transform:translateY(0);transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-toggle-pane--collapsed{grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden}.deb-toggle-pane__inner,.deb-dock-pane__inner{overflow:hidden;min-height:0;padding:4px 2px;margin:-4px -2px}.deb-dock-pane{display:grid;grid-template-rows:0fr;opacity:0;transform:translateY(-8px);pointer-events:none;visibility:hidden;transition:grid-template-rows .32s cubic-bezier(.16,1,.3,1),opacity .22s ease,transform .28s cubic-bezier(.16,1,.3,1)}.deb-dock-pane--open{grid-template-rows:1fr;opacity:1;transform:translateY(0);pointer-events:auto;visibility:visible}.deb-col--left:not([hidden]){animation:debColLeftExpand .24s cubic-bezier(.16,1,.3,1) forwards}.deb-col--right:not([hidden]){animation:debColRightExpand .24s cubic-bezier(.16,1,.3,1) forwards}@media(prefers-reduced-motion:reduce){.deb-body,.deb-col--left,.deb-col--right,.deb-col--center,.deb-section--fields,.deb-preview-slot,.deb-dock-btn,.deb-card,.deb-sidebar-toggle-btn{transition:none!important;animation:none!important}}.deb-col--left,.deb-col--right{position:sticky;top:var(--deb-sticky-top);max-height:calc(100vh - var(--deb-sticky-top) - 20px);overflow-y:auto;padding-right:4px;transition:opacity .2s ease}.deb-card{border-radius:12px!important;border:1px solid var(--deb-border, #e2e8f0)!important;background:var(--deb-surface, #ffffff)!important;box-shadow:0 1px 3px #00000008,0 1px 2px #00000005!important;transition:box-shadow .2s ease,border-color .2s ease!important;overflow:hidden}.deb-card:hover{border-color:#cbd5e1!important;box-shadow:0 4px 12px #0f172a0d!important}.deb-card mat-card-header{padding:14px 16px 10px!important;display:flex!important;align-items:center!important}.deb-card mat-card-content{padding:0 16px 16px!important}.deb-card__title{display:flex!important;align-items:center!important;gap:8px!important;font-family:var(--font-heading, sans-serif)!important;font-size:13.5px!important;font-weight:700!important;color:#0f172a!important;margin:0!important}.deb-card__icon{color:var(--deb-accent-ink, #4f46e5)!important;font-size:18px!important;width:18px!important;height:18px!important;flex:none!important}.deb-card__collapse-btn{margin-left:auto!important;width:32px!important;height:32px!important;color:var(--deb-muted, #64748b)!important;display:inline-flex!important;align-items:center!important;justify-content:center!important}.deb-card__collapse-btn:hover{color:#0f172a!important;background:#f1f5f9!important}.deb-sidebar-toggle-btn{color:var(--deb-muted, #64748b);transition:all .15s ease}.deb-sidebar-toggle-btn:hover{color:#0f172a;background:#f1f5f9}.deb-sidebar-toggle-btn--active{color:var(--deb-accent-ink, #4f46e5)!important;background:var(--deb-accent-soft, #eef2ff)!important}.deb-dock-btn{position:sticky;top:var(--deb-sticky-top);align-self:start;justify-self:center;z-index:10;display:inline-flex;flex-direction:column;align-items:center;gap:10px;width:36px;padding:12px 0;border-radius:9999px;border:1px solid #c7d2fe;background:#fff;color:var(--deb-accent-hover, #4f46e5);font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;box-shadow:0 4px 14px #6366f126;cursor:pointer;animation:debDockFadeIn .22s cubic-bezier(.16,1,.3,1) forwards;transition:background .2s ease,border-color .2s ease,box-shadow .2s ease}.deb-dock-btn:hover{background:var(--deb-accent-soft, #eef2ff);border-color:var(--deb-accent, #6366f1);box-shadow:0 6px 20px #6366f140}.deb-dock-btn mat-icon{flex:none}.deb-dock-btn__icon{font-size:20px;width:20px;height:20px;color:var(--deb-accent-ink, #4f46e5)}.deb-dock-btn__chevron{font-size:16px;width:16px;height:16px;opacity:.75}.deb-dock-btn__label{display:none;letter-spacing:.04em;white-space:nowrap}.deb-dock-btn--horizontal{flex-direction:row;justify-content:center;width:100%;padding:10px 18px;border-radius:12px;gap:12px;position:static;animation:debDockSlideInY .22s cubic-bezier(.16,1,.3,1) forwards}.deb-dock-btn--horizontal .deb-dock-btn__label{display:inline}.deb-card--preview mat-card-content{padding:0 16px 16px!important}.deb-settings-grid{display:grid;grid-template-columns:1.4fr 1fr;gap:8px;margin-bottom:6px}.deb-mask-row{padding:4px 0 8px}.deb-preview-slot:empty{display:none}@media(max-width:1300px){ngx-entity-builder{--deb-left: clamp(280px, 28vw, 340px)}.deb-body{grid-template-columns:var(--deb-left) minmax(0,1fr)}.deb-body.deb-body--right-collapsed{grid-template-columns:var(--deb-left) minmax(0,1fr) var(--deb-rail)}.deb-body.deb-body--left-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr)}.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:var(--deb-rail) minmax(0,1fr) var(--deb-rail)}.deb-col--right{grid-column:2;position:static;max-height:none;overflow:visible}}@media(max-width:900px){ngx-entity-builder{--deb-rail: 36px}.deb-body{gap:12px;padding:12px;grid-template-columns:minmax(0,1fr)}.deb-body.deb-body--left-collapsed,.deb-body.deb-body--right-collapsed,.deb-body.deb-body--left-collapsed.deb-body--right-collapsed{grid-template-columns:minmax(0,1fr)}.deb-col--left,.deb-col--right,.deb-dock-btn--left,.deb-dock-btn--right{grid-column:1;position:static;max-height:none;overflow:visible}.deb-dock-btn--left,.deb-dock-btn--right{flex-direction:row;justify-content:center;width:auto;justify-self:stretch;padding:10px 16px}.deb-dock-btn__label{display:inline}}@media(max-width:900px){.deb-toolbar__title,.deb-toolbar__mark{display:none}}@media(max-width:560px){.deb-toolbar__btn-label{display:none}}.deb-canvas__group{margin:14px 0 6px;font-family:var(--font-heading, sans-serif);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--deb-muted)}.deb-canvas__group:first-of-type{margin-top:0}.deb-field-list{display:flex;flex-direction:column;gap:8px}.deb-field-list:empty{min-height:44px;border:1px dashed var(--deb-border);border-radius:10px}.deb-field-row{display:flex;align-items:center;gap:10px;padding:9px 12px;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-field-row__actions{display:flex;align-items:center;margin-left:auto;opacity:0;transition:opacity .15s ease}.deb-field-row:hover .deb-field-row__actions,.deb-field-row:focus-within .deb-field-row__actions,.deb-field-row--active .deb-field-row__actions{opacity:1}@media(hover:none){.deb-field-row__actions{opacity:1}}@media(max-width:560px){.deb-field-row{flex-wrap:wrap;row-gap:4px}.deb-field-row__actions{margin-left:auto}}.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;flex:none}.deb-span-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:6px;background:#f1f5f9;color:#475569;font-family:ui-monospace,SFMono-Regular,monospace;flex:none}.deb-drag-handle{cursor:grab;color:#7d8a9c;transition:color .15s;flex:none}.deb-drag-handle:hover{color:var(--deb-accent-ink)}.deb-field-type{color:var(--deb-accent-ink);flex:none}.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:#dc2626;font-weight:700;flex:none}.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:var(--deb-muted)}.cdk-drag-preview.deb-field-row{box-shadow:0 12px 28px #4f46e538;border-radius:10px;border:1px solid var(--deb-accent, #6366f1);background:#fff}.cdk-drag-placeholder.deb-field-row{opacity:.35}.cdk-drag-animating.deb-field-row{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)}@media(prefers-reduced-motion:reduce){.deb-field-row,.deb-field-row__actions,.cdk-drag-animating.deb-field-row,.deb-field-list.cdk-drop-list-dragging .deb-field-row:not(.cdk-drag-placeholder){transition:none}.deb-field-row:hover{transform:none}}.deb-inspector{display:flex;flex-direction:column;gap:12px}.deb-inspector__section{border:1px solid var(--deb-border);border-radius:10px;overflow:hidden;background:var(--deb-surface)}.deb-inspector__summary{display:flex;align-items:center;gap:8px;width:100%;padding:10px 12px;cursor:pointer;list-style:none;font-family:var(--font-heading, sans-serif);font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--deb-muted);background:#f8fafc;-webkit-user-select:none;user-select:none}.deb-inspector__summary::-webkit-details-marker{display:none}.deb-inspector__summary:hover{color:#0f172a}.deb-inspector__summary:focus-visible{outline:2px solid var(--deb-accent);outline-offset:-2px}.deb-inspector__chevron{margin-left:auto;transition:transform .18s ease;color:#7d8a9c;font-size:20px;width:20px;height:20px}.deb-inspector__section[open]>.deb-inspector__summary>.deb-inspector__chevron{transform:rotate(180deg)}.deb-inspector__count{padding:1px 7px;border-radius:9999px;background:var(--deb-accent-soft);color:var(--deb-accent-hover);font-size:10px;font-weight:700;letter-spacing:0}.deb-inspector__body{display:flex;flex-direction:column;gap:10px;padding:12px;border-top:1px solid var(--deb-border)}@media(prefers-reduced-motion:reduce){.deb-inspector__chevron{transition:none}}.deb-span-picker{display:flex;gap:4px;flex-wrap:wrap}.deb-span-picker__btn{appearance:none;cursor:pointer;flex:1;min-width:46px;padding:7px 4px;border:1px solid var(--deb-border);border-radius:8px;background:var(--deb-surface);color:var(--deb-muted);font:inherit;font-size:12px;font-weight:700;transition:border-color .15s ease,background .15s ease,color .15s ease}.deb-span-picker__btn:hover{border-color:#a5b4fc;color:#0f172a}.deb-span-picker__btn:focus-visible{outline:2px solid var(--deb-accent);outline-offset:2px}.deb-span-picker__btn--active{border-color:var(--deb-accent);background:var(--deb-accent-soft);color:var(--deb-accent-hover)}.deb-option-row{display:flex;align-items:center;gap:8px}.deb-option-key{flex:0 0 8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:4px 8px;border-radius:4px;background:var(--deb-surface-2, rgba(0, 0, 0, .05));color:var(--deb-muted);font-family:var(--font-mono, ui-monospace, monospace);font-size:12px}.deb-row__actions{display:flex;gap:8px;align-items:center}.deb-flags-row{display:flex;gap:16px;flex-wrap:wrap;padding:2px 0 6px}.deb-param-validators-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.deb-toggles{display:flex;flex-direction:column;gap:6px}.deb-toggles mat-slide-toggle{display:flex;justify-content:space-between;width:100%;padding:5px 8px;border-radius:8px;background:#f8fafc;border:1px solid #f1f5f9;transition:all .15s ease}.deb-toggles mat-slide-toggle:hover{background:#f1f5f9;border-color:#e2e8f0}@media(prefers-reduced-motion:reduce){.deb-span-picker__btn{transition:none}}.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}.deb-path-hint{color:#0000008c;font-size:11px;margin-left:6px}\n"] }]
|
|
4515
5195
|
}], ctorParameters: () => [], propDecorators: { config: [{
|
|
4516
5196
|
type: Input
|
|
4517
5197
|
}], rules: [{
|
|
@@ -4543,5 +5223,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
|
|
|
4543
5223
|
* Generated bundle index. Do not edit.
|
|
4544
5224
|
*/
|
|
4545
5225
|
|
|
4546
|
-
export { BUILDER_TEXT, BuilderStore, BuilderTextService, DEFAULT_BUILDER_TEXT, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleFormComponent, TabManagerComponent, fieldPathOptions, withExistingOptions };
|
|
5226
|
+
export { BUILDER_TEXT, BuilderStore, BuilderTextService, DEFAULT_BUILDER_TEXT, EntityBuilderCanvasComponent, EntityBuilderComponent, EntityBuilderTreeNodeComponent, EntityReferenceConfigComponent, FieldInspectorComponent, FieldPaletteComponent, FieldRulesListComponent, RuleDependencyGraphComponent, RuleFormComponent, TabManagerComponent, computeRuleDependencies, fieldPathOptions, withExistingOptions };
|
|
4547
5227
|
//# sourceMappingURL=ngx-dynamic-entity-builder.mjs.map
|