@yuuvis/client-framework 3.7.1 → 3.8.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.
@@ -0,0 +1,2148 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, signal, computed, Injectable, input, ChangeDetectionStrategy, Component, ElementRef, output, viewChild, effect, NgModule } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import * as i1$1 from '@angular/forms';
5
+ import { FormControl, ReactiveFormsModule } from '@angular/forms';
6
+ import * as i2$1 from '@angular/material/autocomplete';
7
+ import { MatAutocompleteTrigger, MatAutocompleteModule } from '@angular/material/autocomplete';
8
+ import { MatButtonModule } from '@angular/material/button';
9
+ import * as i4 from '@angular/material/chips';
10
+ import { MatChipsModule } from '@angular/material/chips';
11
+ import * as i1 from '@angular/material/icon';
12
+ import { MatIconModule } from '@angular/material/icon';
13
+ import { MatInputModule } from '@angular/material/input';
14
+ import * as i5 from '@angular/material/select';
15
+ import { MatSelectModule } from '@angular/material/select';
16
+ import * as i2 from '@angular/material/tooltip';
17
+ import { MatTooltipModule } from '@angular/material/tooltip';
18
+ import { SystemService, TranslateService, BaseObjectTypeField, TranslatePipe } from '@yuuvis/client-core';
19
+ import { MetadataFormFieldComponent } from '@yuuvis/client-framework/metadata-form';
20
+ import { RendererDirective } from '@yuuvis/client-framework/renderer';
21
+ import { YmtIconButtonDirective } from '@yuuvis/material';
22
+ import { Subscription } from 'rxjs';
23
+ import { debounceTime } from 'rxjs/operators';
24
+ import { NgTemplateOutlet } from '@angular/common';
25
+
26
+ function isConditionGroup(node) {
27
+ return node.kind === 'group';
28
+ }
29
+ function isTableCondition(node) {
30
+ return node.kind === 'table';
31
+ }
32
+ function isFieldCondition(node) {
33
+ return !isConditionGroup(node) && !isTableCondition(node);
34
+ }
35
+
36
+ const DATE_PREFIX_LEN$2 = 5;
37
+ function escapeCmis(value) {
38
+ return value.replace(/'/g, "''").replace(/\\/g, '\\\\');
39
+ }
40
+ /**
41
+ * Escape a value for use inside a CMIS `LIKE` pattern. The wildcard characters
42
+ * `%` and `_` are backslash-escaped so a user-typed `%`/`_` matches literally
43
+ * instead of acting as a wildcard. The backslash itself is escaped first (so an
44
+ * existing `\` can't turn the following character into an escape sequence), then
45
+ * single quotes are doubled as everywhere else.
46
+ */
47
+ function escapeCmisLike(value) {
48
+ return value
49
+ .replace(/\\/g, '\\\\')
50
+ .replace(/%/g, '\\%')
51
+ .replace(/_/g, '\\_')
52
+ .replace(/'/g, "''");
53
+ }
54
+ function isNumericType(internalType) {
55
+ return internalType === 'integer' || internalType === 'decimal';
56
+ }
57
+ function isBooleanType(internalType) {
58
+ return internalType === 'boolean' || internalType.startsWith('boolean:');
59
+ }
60
+ function isFiniteNumberLiteral(value) {
61
+ return value.trim() !== '' && Number.isFinite(Number(value));
62
+ }
63
+ /**
64
+ * Render a scalar value as a CMIS literal, driven by the field's internal type.
65
+ * Strings and datetimes are quoted and escaped. Numbers and booleans are emitted
66
+ * bare — but ONLY when the value really is a finite number / a `true|false`
67
+ * boolean; any other value for a non-string field is quoted-and-escaped as a
68
+ * fallback so a malformed or hostile value (e.g. `1 OR 1=1` on an integer field)
69
+ * can never break out of the literal.
70
+ */
71
+ function renderScalarLiteral(value, internalType) {
72
+ if (internalType.startsWith('string') || internalType === 'datetime') {
73
+ return `'${escapeCmis(value)}'`;
74
+ }
75
+ if (isNumericType(internalType) && isFiniteNumberLiteral(value)) {
76
+ return value;
77
+ }
78
+ if (isBooleanType(internalType) && (value === 'true' || value === 'false')) {
79
+ return value;
80
+ }
81
+ // Unknown type, or a value that doesn't match its declared type: never emit it
82
+ // bare (injection / malformed-SQL guard).
83
+ return `'${escapeCmis(value)}'`;
84
+ }
85
+ /**
86
+ * Serialize a single raw form-control value to its CMIS string form.
87
+ * `Date` → ISO 8601; everything else → trimmed string.
88
+ */
89
+ function serializeScalar(value) {
90
+ if (value === null || value === undefined)
91
+ return '';
92
+ if (typeof value === 'string')
93
+ return value.trim();
94
+ if (value instanceof Date)
95
+ return value.toISOString();
96
+ return String(value);
97
+ }
98
+ /**
99
+ * Type-aware "the user actually entered something" check. The metadata value
100
+ * editor emits strings, numbers, dates and arrays depending on the field type,
101
+ * so a plain truthiness/`.trim()` test is wrong (`0` and `false` are valid).
102
+ */
103
+ function isValuePresent(value) {
104
+ if (value === null || value === undefined)
105
+ return false;
106
+ if (typeof value === 'string')
107
+ return value.trim().length > 0;
108
+ if (typeof value === 'number')
109
+ return !Number.isNaN(value);
110
+ if (typeof value === 'boolean')
111
+ return true;
112
+ if (value instanceof Date)
113
+ return !Number.isNaN(value.getTime());
114
+ if (Array.isArray(value))
115
+ return value.some(isValuePresent);
116
+ return true; // objects (e.g. RangeValue) — present if non-null
117
+ }
118
+ /**
119
+ * Normalize a raw form-control value into what we store in `FieldCondition.value`.
120
+ * Arrays are kept as a cleaned `string[]` (drives IN-clauses); everything else
121
+ * collapses to a single string.
122
+ */
123
+ function normalizeConditionValue(value) {
124
+ if (Array.isArray(value)) {
125
+ return value.map((entry) => serializeScalar(entry)).filter((entry) => entry.length > 0);
126
+ }
127
+ return serializeScalar(value);
128
+ }
129
+ /* eslint-disable id-length */
130
+ const CMIS_OPERATOR = {
131
+ eq: '=',
132
+ neq: '<>',
133
+ gt: '>',
134
+ gte: '>=',
135
+ lt: '<',
136
+ lte: '<='
137
+ };
138
+ /* eslint-enable id-length */
139
+ /**
140
+ * Operators that carry their own meaning and need no value step: date presets,
141
+ * the boolean `= true/false` shortcuts, and the null checks (`empty`/`not_empty`).
142
+ * The build flow commits these immediately instead of advancing to the value editor.
143
+ */
144
+ function isValuelessOperator(operator) {
145
+ return (operator.startsWith('date:') ||
146
+ operator === 'eq_true' ||
147
+ operator === 'eq_false' ||
148
+ operator === 'empty' ||
149
+ operator === 'not_empty');
150
+ }
151
+ /**
152
+ * Whether a condition is an *unset placeholder*: it uses a value-requiring operator
153
+ * (eq / like / gt / …) but carries no value yet. Such conditions are kept in the tree
154
+ * — so a saved query can act as a fill-in template — but contribute nothing to the
155
+ * emitted CMIS query. An empty value is unambiguously "unset" here because null-checks
156
+ * have their own `empty` / `not_empty` operators (offered on every queryable type).
157
+ */
158
+ function isConditionUnset(cond) {
159
+ return !isValuelessOperator(cond.operator) && !isValuePresent(cond.value);
160
+ }
161
+ /**
162
+ * Produce a copy of `blocks` with the values of the conditions in `overrides` replaced.
163
+ * Used by form mode to overlay the user's fill-out values onto the (untouched) template
164
+ * before building the query, so the saved template stays reusable. Conditions are matched
165
+ * by reference — the keys are the dynamic-condition references captured when form mode was
166
+ * entered. Returns the original array unchanged when there is nothing to overlay.
167
+ */
168
+ function overlayConditionValues(blocks, overrides) {
169
+ if (overrides.size === 0)
170
+ return blocks;
171
+ const mapNode = (node) => {
172
+ if (isConditionGroup(node) || isTableCondition(node)) {
173
+ return { ...node, conditions: node.conditions.map(mapNode) };
174
+ }
175
+ return overrides.has(node) ? { ...node, value: overrides.get(node) } : node;
176
+ };
177
+ return blocks.map((block) => ({ ...block, conditions: block.conditions.map(mapNode) }));
178
+ }
179
+ /* eslint-disable id-length */
180
+ /**
181
+ * Calendar-day arithmetic. Using `new Date(y, m, d ± n)` (rather than
182
+ * adding milliseconds) keeps boundaries aligned to local midnight across
183
+ * DST transitions where a day is not exactly 24h.
184
+ *
185
+ * Week starts on Monday (ISO 8601 / European default).
186
+ */
187
+ function datePresetToRange(preset) {
188
+ const now = new Date();
189
+ const year = now.getFullYear();
190
+ const month = now.getMonth();
191
+ const date = now.getDate();
192
+ let from;
193
+ let to;
194
+ switch (preset) {
195
+ case 'today':
196
+ from = new Date(year, month, date);
197
+ to = new Date(year, month, date + 1);
198
+ break;
199
+ case 'thisWeek': {
200
+ // getDay(): 0=Sun, 1=Mon … 6=Sat. Shift so Mon=0 … Sun=6.
201
+ const dayFromMonday = (now.getDay() + 6) % 7;
202
+ from = new Date(year, month, date - dayFromMonday);
203
+ to = new Date(year, month, date - dayFromMonday + 7);
204
+ break;
205
+ }
206
+ case 'thisMonth':
207
+ from = new Date(year, month, 1);
208
+ to = new Date(year, month + 1, 1);
209
+ break;
210
+ case 'thisYear':
211
+ default:
212
+ from = new Date(year, 0, 1);
213
+ to = new Date(year + 1, 0, 1);
214
+ break;
215
+ }
216
+ return { from: from.toISOString(), to: to.toISOString() };
217
+ }
218
+ /* eslint-enable id-length */
219
+ /**
220
+ * Render a single {@link FieldCondition} as a CMIS predicate. Handles the null
221
+ * checks (`IS NULL` / `IS NOT NULL`), multi-value `IN` / `NOT IN`, `LIKE`
222
+ * (with wildcard-escaped, `%`-wrapped pattern), date-preset ranges
223
+ * (`>= from AND < to`), and the comparison operators (`=`, `<>`, `>`, …).
224
+ */
225
+ function buildConditionClause(cond) {
226
+ const operator = cond.operator;
227
+ // Unset placeholders (value-requiring operator, no value yet) contribute nothing —
228
+ // this also guards against `LIKE '%%'`, `= ''` and `IN ()` for empty input.
229
+ if (isConditionUnset(cond))
230
+ return '';
231
+ // Valueless null checks short-circuit before any value handling.
232
+ if (operator === 'empty')
233
+ return `${cond.fieldId} IS NULL`;
234
+ if (operator === 'not_empty')
235
+ return `${cond.fieldId} IS NOT NULL`;
236
+ // Multi-value (e.g. multi-select catalog): `eq` → IN (...), `neq` → NOT IN (...).
237
+ if (Array.isArray(cond.value)) {
238
+ const list = cond.value.map((entry) => renderScalarLiteral(entry, cond.internalType)).join(', ');
239
+ const inOperator = operator === 'neq' ? 'NOT IN' : 'IN';
240
+ return `${cond.fieldId} ${inOperator} (${list})`;
241
+ }
242
+ const value = serializeScalar(cond.value);
243
+ if (operator === 'like') {
244
+ return `${cond.fieldId} LIKE '%${escapeCmisLike(value)}%'`;
245
+ }
246
+ if (operator.startsWith('date:')) {
247
+ // eslint-disable-next-line id-length
248
+ const { from, to } = datePresetToRange(operator.slice(DATE_PREFIX_LEN$2));
249
+ return `${cond.fieldId} >= '${from}' AND ${cond.fieldId} < '${to}'`;
250
+ }
251
+ const cmisOp = CMIS_OPERATOR[operator] ?? '=';
252
+ return `${cond.fieldId} ${cmisOp} ${renderScalarLiteral(value, cond.internalType)}`;
253
+ }
254
+ /**
255
+ * Render a queryable-table condition as `tableField[*].(col op val AND/OR …)`.
256
+ * `[*]` matches any row; the parenthesized column predicates join with the
257
+ * table's combinator. Empty tables contribute nothing.
258
+ *
259
+ * Column predicates reuse {@link buildConditionClause}: a column condition's
260
+ * `fieldId` is the bare column id (no table prefix), which is exactly what
261
+ * belongs inside the parentheses.
262
+ */
263
+ function buildTableClause(table) {
264
+ const cols = table.conditions.map(buildNodeClause).filter((part) => part !== '');
265
+ if (cols.length === 0)
266
+ return '';
267
+ return `${table.fieldId}[*].(${cols.join(` ${table.combinator} `)})`;
268
+ }
269
+ /**
270
+ * Recursively render a condition node. Groups produce parenthesized
271
+ * sub-expressions; empty groups contribute nothing; single-child groups
272
+ * collapse their redundant parens. Table conditions delegate to
273
+ * {@link buildTableClause}.
274
+ */
275
+ function buildNodeClause(node) {
276
+ if (isTableCondition(node)) {
277
+ return buildTableClause(node);
278
+ }
279
+ if (isConditionGroup(node)) {
280
+ const parts = node.conditions.map(buildNodeClause).filter((part) => part !== '');
281
+ if (parts.length === 0)
282
+ return '';
283
+ if (parts.length === 1)
284
+ return parts[0];
285
+ return `(${parts.join(` ${node.combinator} `)})`;
286
+ }
287
+ return buildConditionClause(node);
288
+ }
289
+ /**
290
+ * Build the type-matching clause for a block. Primary object types collapse to
291
+ * `objectTypeId = 'x'` (single) or `objectTypeId IN (...)` (multiple); secondary
292
+ * object types use `system:secondaryObjectTypeIds IN (...)`. A block mixing both
293
+ * flavors OR-combines the two clauses inside parentheses.
294
+ */
295
+ function buildTypeClause(types) {
296
+ const quote = (id) => `'${escapeCmis(id)}'`;
297
+ const primaries = types.filter((type) => !type.isSot).map((type) => type.id);
298
+ const sots = types.filter((type) => type.isSot).map((type) => type.id);
299
+ const parts = [];
300
+ if (primaries.length > 0) {
301
+ parts.push(primaries.length === 1
302
+ ? `objectTypeId = ${quote(primaries[0])}`
303
+ : `objectTypeId IN (${primaries.map(quote).join(', ')})`);
304
+ }
305
+ if (sots.length > 0) {
306
+ parts.push(`system:secondaryObjectTypeIds IN (${sots.map(quote).join(', ')})`);
307
+ }
308
+ if (parts.length === 0)
309
+ return '';
310
+ return parts.length === 1 ? parts[0] : `(${parts.join(' OR ')})`;
311
+ }
312
+ /**
313
+ * The column qualifier that scopes a full-text CONTAINS. `all` searches metadata + content (no
314
+ * qualifier); `content` / `metadata` restrict to that virtual full-text column.
315
+ */
316
+ const FULLTEXT_SCOPE_COLUMN = {
317
+ all: '',
318
+ content: 'system:content',
319
+ metadata: 'system:metadata'
320
+ };
321
+ /**
322
+ * Build the whole-object full-text clause: a single `CONTAINS('term')` predicate, optionally scoped
323
+ * to a column and AND-ed with a type restriction. Returns `''` when the term is blank.
324
+ */
325
+ function buildFulltextClause(fulltext) {
326
+ const term = fulltext.term.trim();
327
+ if (!term)
328
+ return '';
329
+ const column = FULLTEXT_SCOPE_COLUMN[fulltext.scope];
330
+ const contains = column ? `${column} CONTAINS('${escapeCmis(term)}')` : `CONTAINS('${escapeCmis(term)}')`;
331
+ const typeCond = buildTypeClause(fulltext.types);
332
+ return typeCond ? `(${typeCond} AND ${contains})` : contains;
333
+ }
334
+ /**
335
+ * Assemble the full CMIS statement from the search state. Each unit — the
336
+ * full-text clause plus every type block (type restriction `AND`-ed with its
337
+ * conditions) — is parenthesized and joined by `combinator`. Returns `''` when
338
+ * nothing contributes a clause, so an empty search yields no query.
339
+ *
340
+ * @param blocks The type blocks with their conditions.
341
+ * @param combinator How the top-level units combine (`AND` / `OR`).
342
+ * @param fulltext Optional whole-object full-text unit.
343
+ * @returns A `SELECT * FROM system:object WHERE …` statement, or `''`.
344
+ */
345
+ function buildCmisQuery(blocks, combinator, fulltext) {
346
+ const units = [];
347
+ const fulltextClause = fulltext ? buildFulltextClause(fulltext) : '';
348
+ if (fulltextClause !== '')
349
+ units.push(fulltextClause);
350
+ for (const block of blocks) {
351
+ const condJoin = ` ${block.conditionCombinator} `;
352
+ const typeCond = buildTypeClause(block.types);
353
+ const condParts = [];
354
+ for (const node of block.conditions) {
355
+ const clause = buildNodeClause(node);
356
+ if (clause !== '')
357
+ condParts.push(clause);
358
+ }
359
+ // Conditions combine with the user-chosen combinator; the type restriction
360
+ // is always AND-ed with them so a block matches the chosen type(s) *and*
361
+ // satisfies the condition expression.
362
+ const condsClause = condParts.length === 0 ? '' : condParts.length === 1 ? condParts[0] : `(${condParts.join(condJoin)})`;
363
+ const blockParts = [];
364
+ if (typeCond !== '')
365
+ blockParts.push(typeCond);
366
+ if (condsClause !== '')
367
+ blockParts.push(condsClause);
368
+ if (blockParts.length === 0)
369
+ continue;
370
+ units.push(blockParts.length === 1 ? blockParts[0] : `(${blockParts.join(' AND ')})`);
371
+ }
372
+ if (units.length === 0)
373
+ return '';
374
+ const joined = units.length === 1 ? units[0] : units.join(` ${combinator} `);
375
+ return `SELECT * FROM system:object WHERE ${joined}`;
376
+ }
377
+
378
+ /**
379
+ * Walk the block tree and return a new `blocks` array where `target` has been
380
+ * replaced by `updater(target)`. Identity is by reference equality.
381
+ *
382
+ * Returns the original array reference if the target is not found, so callers
383
+ * can detect a no-op via reference equality.
384
+ */
385
+ /**
386
+ * Rebuild a container with a new `conditions` array while preserving its
387
+ * discriminated kind (`SearchBlock` / `ConditionGroup` / `TableCondition`).
388
+ *
389
+ * The cast is sound: every caller only ever produces conditions that are valid
390
+ * for the container's kind (e.g. a table only receives row-groups). TypeScript
391
+ * cannot verify that across a union spread — `{ ...target, conditions }` widens
392
+ * a `TableCondition`'s `conditions` from `ConditionGroup[]` to `ConditionNode[]`
393
+ * and so fails to narrow back to the input type.
394
+ */
395
+ function withConditions(container, conditions) {
396
+ return { ...container, conditions };
397
+ }
398
+ function updateContainer(blocks, target, updater) {
399
+ let changed = false;
400
+ const walk = (node) => {
401
+ if (node === target) {
402
+ changed = true;
403
+ return updater(node);
404
+ }
405
+ if (isConditionGroup(node) || isTableCondition(node)) {
406
+ const next = node.conditions.map(walk);
407
+ if (next.some((child, idx) => child !== node.conditions[idx])) {
408
+ return withConditions(node, next);
409
+ }
410
+ }
411
+ return node;
412
+ };
413
+ const nextBlocks = blocks.map((block) => {
414
+ if (block === target) {
415
+ changed = true;
416
+ return updater(block);
417
+ }
418
+ const nextConditions = block.conditions.map(walk);
419
+ if (nextConditions.some((child, idx) => child !== block.conditions[idx])) {
420
+ return { ...block, conditions: nextConditions };
421
+ }
422
+ return block;
423
+ });
424
+ return changed ? nextBlocks : blocks;
425
+ }
426
+ /**
427
+ * Remove `target` from its parent container. If the parent is a
428
+ * `ConditionGroup` and becomes empty as a result, the parent is also removed
429
+ * (recursively up the chain). Top-level `SearchBlock`s are never auto-pruned.
430
+ *
431
+ * Returns the original array reference if the target is not found.
432
+ */
433
+ function removeNode(blocks, target) {
434
+ let changed = false;
435
+ // A group or table that loses its last child is pruned, cascading up the chain.
436
+ const pruneContainer = (container) => {
437
+ const kids = container.conditions;
438
+ const next = kids.map(walk).filter((node) => node !== null);
439
+ if (next.length === 0)
440
+ return null;
441
+ if (next.length === kids.length && next.every((child, idx) => child === kids[idx])) {
442
+ return container;
443
+ }
444
+ return withConditions(container, next);
445
+ };
446
+ const walk = (node) => {
447
+ if (node === target) {
448
+ changed = true;
449
+ return null;
450
+ }
451
+ if (isConditionGroup(node) || isTableCondition(node)) {
452
+ return pruneContainer(node);
453
+ }
454
+ return node;
455
+ };
456
+ const nextBlocks = blocks
457
+ .map((block) => {
458
+ if (block === target) {
459
+ changed = true;
460
+ return null;
461
+ }
462
+ const nextConditions = block.conditions.map(walk).filter((node) => node !== null);
463
+ if (nextConditions.length === block.conditions.length &&
464
+ nextConditions.every((child, idx) => child === block.conditions[idx])) {
465
+ return block;
466
+ }
467
+ return { ...block, conditions: nextConditions };
468
+ })
469
+ .filter((block) => block !== null);
470
+ return changed ? nextBlocks : blocks;
471
+ }
472
+ /**
473
+ * Find the top-level `SearchBlock` that contains `container` (directly or
474
+ * transitively). Returns `container` itself when it already is a `SearchBlock`.
475
+ * Returns `null` if not found in the tree.
476
+ */
477
+ function findOwningBlock(blocks, container) {
478
+ // A SearchBlock is its own owner; only groups and tables need a tree walk.
479
+ if (!isConditionGroup(container) && !isTableCondition(container)) {
480
+ return blocks.includes(container) ? container : null;
481
+ }
482
+ const contains = (node, needle) => {
483
+ if (node === needle)
484
+ return true;
485
+ if (isConditionGroup(node) || isTableCondition(node)) {
486
+ return node.conditions.some((child) => contains(child, needle));
487
+ }
488
+ return false;
489
+ };
490
+ for (const block of blocks) {
491
+ if (block.conditions.some((child) => contains(child, container)))
492
+ return block;
493
+ }
494
+ return null;
495
+ }
496
+
497
+ const DATE_PREFIX_LEN$1 = 5;
498
+ /* eslint-disable id-length */
499
+ const OPERATOR_LABEL_KEYS = {
500
+ like: 'yuv.smart-search.operator.like',
501
+ empty: 'yuv.smart-search.operator.empty',
502
+ not_empty: 'yuv.smart-search.operator.not-empty'
503
+ };
504
+ const OPERATOR_SYMBOLS = {
505
+ eq: '=',
506
+ neq: '≠',
507
+ gt: '>',
508
+ gte: '>=',
509
+ lt: '<',
510
+ lte: '<='
511
+ };
512
+ /* eslint-enable id-length */
513
+ const DATE_PRESETS = [
514
+ { id: 'today', labelKey: 'yuv.smart-search.date-preset.today' },
515
+ { id: 'thisWeek', labelKey: 'yuv.smart-search.date-preset.this-week' },
516
+ { id: 'thisMonth', labelKey: 'yuv.smart-search.date-preset.this-month' },
517
+ { id: 'thisYear', labelKey: 'yuv.smart-search.date-preset.this-year' }
518
+ ];
519
+ /**
520
+ * State + mutators for SmartSearch, shared between the host component and the
521
+ * recursive `SmartSearchGroupComponent`. Provided at the host component level
522
+ * so each `<yuv-smart-search>` instance gets its own controller.
523
+ *
524
+ * UI concerns (focus management, autocomplete plumbing, blur suppression) stay
525
+ * in the host component; this controller is purely about data.
526
+ */
527
+ class SmartSearchEditController {
528
+ constructor() {
529
+ this.#system = inject(SystemService);
530
+ this.#translate = inject(TranslateService);
531
+ /** The set of object type IDs that can be used as search blocks. */
532
+ this.allowedTypes = signal([], ...(ngDevMode ? [{ debugName: "allowedTypes" }] : /* istanbul ignore next */ []));
533
+ /** ObjectTypeField IDs to exclude from the field-step autocomplete suggestions. */
534
+ this.skipProperties = signal([], ...(ngDevMode ? [{ debugName: "skipProperties" }] : /* istanbul ignore next */ []));
535
+ // ── Persistent form controls (one per step) ──────────────────────────────
536
+ this.fieldCtrl = new FormControl('');
537
+ this.operatorCtrl = new FormControl('');
538
+ // Untyped value: the metadata renderer for the picked field may write a
539
+ // string, number, Date or string[] (see normalizeConditionValue).
540
+ this.valueCtrl = new FormControl('');
541
+ // ── State signals ─────────────────────────────────────────────────────────
542
+ /** Current input step */
543
+ this.step = signal('type', ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
544
+ /** The owning top-level block (drives field suggestions for the active edit) */
545
+ this.activeBlock = signal(null, ...(ngDevMode ? [{ debugName: "activeBlock" }] : /* istanbul ignore next */ []));
546
+ /** The immediate parent container the new condition will land in */
547
+ this.activeContainer = signal(null, ...(ngDevMode ? [{ debugName: "activeContainer" }] : /* istanbul ignore next */ []));
548
+ /** Field selected waiting for operator/value */
549
+ this.pendingField = signal(null, ...(ngDevMode ? [{ debugName: "pendingField" }] : /* istanbul ignore next */ []));
550
+ /** Operator label for the pending operator (displayed as a pill) */
551
+ this.pendingOperatorLabel = signal('', ...(ngDevMode ? [{ debugName: "pendingOperatorLabel" }] : /* istanbul ignore next */ []));
552
+ /** The accumulated search blocks */
553
+ this.blocks = signal([], ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
554
+ /** Types staged in the multi-select before the block is committed (type step). */
555
+ this.draftTypes = signal([], ...(ngDevMode ? [{ debugName: "draftTypes" }] : /* istanbul ignore next */ []));
556
+ /** Index of the condition being edited (-1 = new condition) */
557
+ this.editingConditionIndex = signal(-1, ...(ngDevMode ? [{ debugName: "editingConditionIndex" }] : /* istanbul ignore next */ []));
558
+ /** Snapshot of the condition under edit, used to restore on cancel */
559
+ this.#editingSnapshot = null;
560
+ /** Monotonic counter for generating stable block ids. */
561
+ this.#blockSeq = 0;
562
+ /**
563
+ * How to combine the top-level units (full-text unit + type blocks). These are
564
+ * type-scoped, so they always join with OR ("this as well as that"); the UI no
565
+ * longer exposes a toggle. Kept as a signal so saved states still round-trip.
566
+ */
567
+ this.combinator = signal('OR', ...(ngDevMode ? [{ debugName: "combinator" }] : /* istanbul ignore next */ []));
568
+ /** The current filter term for autocomplete suggestions */
569
+ this.inputTerm = signal('', ...(ngDevMode ? [{ debugName: "inputTerm" }] : /* istanbul ignore next */ []));
570
+ /** The whole-object full-text search unit (independent of the condition blocks). */
571
+ this.fulltext = signal({ term: '', scope: 'all', types: [] }, ...(ngDevMode ? [{ debugName: "fulltext" }] : /* istanbul ignore next */ []));
572
+ /** Whether the dynamic-conditions feature is enabled (mirrors the host `supportDynamicConditions` input). */
573
+ this.supportDynamic = signal(false, ...(ngDevMode ? [{ debugName: "supportDynamic" }] : /* istanbul ignore next */ []));
574
+ /**
575
+ * Form mode: replace the builder with a generated form of the user-marked dynamic
576
+ * conditions. View state only — the template ({@link blocks}) is never mutated; the
577
+ * form's values are overlaid onto the dynamic conditions to drive {@link cmisQuery},
578
+ * so the template stays reusable.
579
+ */
580
+ this.formMode = signal(false, ...(ngDevMode ? [{ debugName: "formMode" }] : /* istanbul ignore next */ []));
581
+ /** The dynamic conditions surfaced as form rows; rebuilt on each {@link enterFormMode}. */
582
+ this.formFields = signal([], ...(ngDevMode ? [{ debugName: "formFields" }] : /* istanbul ignore next */ []));
583
+ /**
584
+ * The dynamic form rows grouped by their owning block (each carrying a muted header of
585
+ * target types); rebuilt on each {@link enterFormMode}. Blocks combine with `OR`
586
+ * ("as well as"), matching the builder.
587
+ */
588
+ this.formBlocks = signal([], ...(ngDevMode ? [{ debugName: "formBlocks" }] : /* istanbul ignore next */ []));
589
+ /** Overlay values entered in the form, keyed by the dynamic condition's reference (refs are stable while form mode is active). */
590
+ this.#formValues = signal(new Map(), ...(ngDevMode ? [{ debugName: "#formValues" }] : /* istanbul ignore next */ []));
591
+ /** Whether any condition is marked dynamic (gates the form-mode toggle). */
592
+ this.hasDynamicConditions = computed(() => this.blocks().some((block) => this.#anyDynamic(block.conditions)), ...(ngDevMode ? [{ debugName: "hasDynamicConditions" }] : /* istanbul ignore next */ []));
593
+ this.objectTypes = computed(() => this.#system.getObjectTypes(true, 'search').filter((type) => this.allowedTypes().includes(type.id)), ...(ngDevMode ? [{ debugName: "objectTypes" }] : /* istanbul ignore next */ []));
594
+ /** Fields for the active top-level block, used by the field-step suggestions. */
595
+ this.activeBlockFields = computed(() => this.#sharedFields(this.activeBlock()?.types ?? []), ...(ngDevMode ? [{ debugName: "activeBlockFields" }] : /* istanbul ignore next */ []));
596
+ /**
597
+ * When the active edit is scoped inside a table, the resolved column definitions
598
+ * of that table (as full `ObjectTypeField`s with `_internalType`). `null` when
599
+ * not editing inside a table.
600
+ */
601
+ this.activeTableColumns = computed(() => {
602
+ const container = this.activeContainer();
603
+ return container && isTableCondition(container) ? this.#tableColumns(container) : null;
604
+ }, ...(ngDevMode ? [{ debugName: "activeTableColumns" }] : /* istanbul ignore next */ []));
605
+ /**
606
+ * The fields offered at the field step: a table's **columns** when editing
607
+ * inside a table, otherwise the active block's shared fields.
608
+ */
609
+ this.activeFields = computed(() => {
610
+ const columns = this.activeTableColumns();
611
+ if (columns)
612
+ return this.#columnSuggestions(columns);
613
+ return this.activeBlockFields();
614
+ }, ...(ngDevMode ? [{ debugName: "activeFields" }] : /* istanbul ignore next */ []));
615
+ this.suggestions = computed(() => {
616
+ const step = this.step();
617
+ const term = this.inputTerm().toLowerCase();
618
+ if (step === 'type') {
619
+ // Only hide types already staged in the *current* draft block. Types used by
620
+ // other blocks stay available — a new block may target the same type again.
621
+ const stagedIds = new Set(this.draftTypes().map((type) => type.id));
622
+ const all = this.objectTypes()
623
+ .filter((type) => !stagedIds.has(type.id))
624
+ .map((type) => ({ kind: 'type', id: type.id, label: type.label ?? type.id }))
625
+ .sort((a, b) => a.label.localeCompare(b.label));
626
+ return term ? all.filter((item) => item.label.toLowerCase().includes(term)) : all;
627
+ }
628
+ if (step === 'field') {
629
+ const all = [...this.activeFields()].sort((a, b) => a.label.localeCompare(b.label));
630
+ return term
631
+ ? all.filter((item) => item.label.toLowerCase().includes(term) || item.id.toLowerCase().includes(term))
632
+ : all;
633
+ }
634
+ if (step === 'operator') {
635
+ const field = this.pendingField();
636
+ if (!field)
637
+ return [];
638
+ const all = this.#operatorsForInternalType(field.internalType ?? 'string');
639
+ return term ? all.filter((item) => item.label.toLowerCase().includes(term)) : all;
640
+ }
641
+ return [];
642
+ }, ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
643
+ /** Number of top-level query units: the full-text unit (when it has a term) plus each block. */
644
+ this.unitCount = computed(() => (this.fulltext().term.trim() ? 1 : 0) + this.blocks().length, ...(ngDevMode ? [{ debugName: "unitCount" }] : /* istanbul ignore next */ []));
645
+ this.showCombinator = computed(() => this.unitCount() >= 2, ...(ngDevMode ? [{ debugName: "showCombinator" }] : /* istanbul ignore next */ []));
646
+ this.cmisQuery = computed(() => {
647
+ // In form mode, overlay the form's entered values onto the (untouched) template so
648
+ // the emitted query reflects the user's fill-out without mutating the saved blocks.
649
+ const blocks = this.formMode() ? overlayConditionValues(this.blocks(), this.#formValues()) : this.blocks();
650
+ return buildCmisQuery(blocks, this.combinator(), this.fulltext());
651
+ }, ...(ngDevMode ? [{ debugName: "cmisQuery" }] : /* istanbul ignore next */ []));
652
+ /** The active input control for the current step. */
653
+ this.activeCtrl = computed(() => {
654
+ switch (this.step()) {
655
+ case 'field':
656
+ return this.fieldCtrl;
657
+ case 'operator':
658
+ return this.operatorCtrl;
659
+ case 'value':
660
+ return this.valueCtrl;
661
+ default:
662
+ return this.fieldCtrl;
663
+ }
664
+ }, ...(ngDevMode ? [{ debugName: "activeCtrl" }] : /* istanbul ignore next */ []));
665
+ }
666
+ #system;
667
+ #translate;
668
+ /** Snapshot of the condition under edit, used to restore on cancel */
669
+ #editingSnapshot;
670
+ /** Monotonic counter for generating stable block ids. */
671
+ #blockSeq;
672
+ /** Overlay values entered in the form, keyed by the dynamic condition's reference (refs are stable while form mode is active). */
673
+ #formValues;
674
+ // ── State save / restore ──────────────────────────────────────────────────
675
+ /** Deep-clone the current blocks, combinator and full-text unit into a serializable {@link SmartSearchState}. */
676
+ getState() {
677
+ return {
678
+ blocks: JSON.parse(JSON.stringify(this.blocks())),
679
+ combinator: this.combinator(),
680
+ fulltext: JSON.parse(JSON.stringify(this.fulltext()))
681
+ };
682
+ }
683
+ /** Replace the current state with a saved one, normalizing legacy blocks and cancelling any in-progress edit. */
684
+ loadState(state) {
685
+ this.cancelPending();
686
+ // Form-mode overlay keys reference the old tree; drop form mode on reload.
687
+ this.exitFormMode();
688
+ this.blocks.set(state.blocks.map((block) => this.#normalizeBlock(block)));
689
+ this.combinator.set(state.combinator);
690
+ this.fulltext.set(state.fulltext ?? { term: '', scope: 'all', types: [] });
691
+ }
692
+ /** Reset all search state back to its initial empty values. */
693
+ reset() {
694
+ this.cancelPending();
695
+ this.blocks.set([]);
696
+ this.combinator.set('OR');
697
+ this.fulltext.set({ term: '', scope: 'all', types: [] });
698
+ this.exitFormMode();
699
+ }
700
+ // ── Dynamic conditions + form mode ────────────────────────────────────────
701
+ /** Mark or unmark a condition as dynamic (immutably replaces the node in its container). */
702
+ setConditionDynamic(container, condition, dynamic) {
703
+ this.blocks.update((blocks) => updateContainer(blocks, container, (target) => withConditions(target, target.conditions.map((node) => (node === condition ? { ...condition, dynamic } : node)))));
704
+ }
705
+ /**
706
+ * Enter form mode: collect every dynamic condition, resolve its value editor and seed a
707
+ * `FormControl` with its current value. The blocks themselves are left untouched — the
708
+ * form's edits are overlaid via {@link setFormValue} / {@link cmisQuery}.
709
+ */
710
+ enterFormMode() {
711
+ if (!this.supportDynamic())
712
+ return;
713
+ const allFields = [];
714
+ const formBlocks = [];
715
+ for (const block of this.blocks()) {
716
+ const fields = [];
717
+ const walk = (nodes, table) => {
718
+ for (const node of nodes) {
719
+ if (isTableCondition(node))
720
+ walk(node.conditions, node);
721
+ else if (isConditionGroup(node))
722
+ walk(node.conditions, table);
723
+ else if (node.dynamic && !isValuelessOperator(node.operator)) {
724
+ const field = {
725
+ condition: node,
726
+ def: this.#resolveDynamicFieldDef(block, table, node.fieldId),
727
+ control: new FormControl(node.value)
728
+ };
729
+ fields.push(field);
730
+ allFields.push(field);
731
+ }
732
+ }
733
+ };
734
+ walk(block.conditions, null);
735
+ // Only blocks that actually contribute a fillable row get a header in the form.
736
+ if (fields.length)
737
+ formBlocks.push({ block, fields });
738
+ }
739
+ this.formFields.set(allFields);
740
+ this.formBlocks.set(formBlocks);
741
+ this.#formValues.set(new Map());
742
+ this.formMode.set(true);
743
+ }
744
+ /** Leave form mode and discard the transient form state (the template is untouched). */
745
+ exitFormMode() {
746
+ this.formMode.set(false);
747
+ this.formFields.set([]);
748
+ this.formBlocks.set([]);
749
+ this.#formValues.set(new Map());
750
+ }
751
+ /** Toggle form mode (rebuilds the form rows on each entry). */
752
+ toggleFormMode() {
753
+ if (this.formMode())
754
+ this.exitFormMode();
755
+ else
756
+ this.enterFormMode();
757
+ }
758
+ /** Record a value entered in the form for a dynamic condition (drives the overlaid {@link cmisQuery}). */
759
+ setFormValue(condition, raw) {
760
+ this.#formValues.update((current) => new Map(current).set(condition, normalizeConditionValue(raw)));
761
+ }
762
+ /** Whether any condition in `nodes` (recursively) is marked dynamic and has a fillable value. */
763
+ #anyDynamic(nodes) {
764
+ return nodes.some((node) => isConditionGroup(node) || isTableCondition(node)
765
+ ? this.#anyDynamic(node.conditions)
766
+ : !!node.dynamic && !isValuelessOperator(node.operator));
767
+ }
768
+ /** Resolve the value-editor field definition for a dynamic condition, honouring its table context. */
769
+ #resolveDynamicFieldDef(block, table, fieldId) {
770
+ if (table) {
771
+ return this.#tableColumns(table, block).find((col) => col.id === fieldId) ?? null;
772
+ }
773
+ return this.resolveFieldDefinition(block.types, fieldId);
774
+ }
775
+ // ── Full-text unit ────────────────────────────────────────────────────────
776
+ /** Set the full-text search term. */
777
+ setFulltextTerm(term) {
778
+ this.fulltext.update((current) => ({ ...current, term }));
779
+ }
780
+ /** Set the full-text search scope (`all` / `metadata` / `content`). */
781
+ setFulltextScope(scope) {
782
+ this.fulltext.update((current) => ({ ...current, scope }));
783
+ }
784
+ /** Add a target type to the full-text unit (no-op if already present). */
785
+ addFulltextType(type, label) {
786
+ this.fulltext.update((current) => current.types.some((picked) => picked.id === type.id)
787
+ ? current
788
+ : { ...current, types: [...current.types, { id: type.id, label, isSot: type.isSot }] });
789
+ }
790
+ /** Remove a target type from the full-text unit by id. */
791
+ removeFulltextType(id) {
792
+ this.fulltext.update((current) => ({ ...current, types: current.types.filter((type) => type.id !== id) }));
793
+ }
794
+ /** Replace the full-text target types from a list of object-type ids (unknown ids are dropped). */
795
+ setFulltextTypes(ids) {
796
+ const types = ids
797
+ .map((id) => this.objectTypes().find((objectType) => objectType.id === id))
798
+ .filter((objectType) => !!objectType)
799
+ .map((objectType) => ({ id: objectType.id, label: objectType.label ?? objectType.id, isSot: objectType.isSot }));
800
+ this.fulltext.update((current) => ({ ...current, types }));
801
+ }
802
+ // ── Type draft (multi-select) ─────────────────────────────────────────────
803
+ /** Stage a type in the draft multi-select (no-op if already staged). */
804
+ addDraftType(type, label) {
805
+ if (this.draftTypes().some((draft) => draft.id === type.id))
806
+ return;
807
+ this.draftTypes.update((draft) => [...draft, { id: type.id, label, isSot: type.isSot }]);
808
+ }
809
+ /** Remove a staged type from the draft multi-select. */
810
+ removeDraftType(id) {
811
+ this.draftTypes.update((draft) => draft.filter((type) => type.id !== id));
812
+ }
813
+ /**
814
+ * Commit the staged draft types into a new block and clear the draft.
815
+ * Returns the created block, or `null` when the draft is empty.
816
+ */
817
+ confirmDraftTypes() {
818
+ const types = this.draftTypes();
819
+ if (types.length === 0)
820
+ return null;
821
+ const newBlock = {
822
+ id: this.#nextId(),
823
+ types: [...types],
824
+ conditions: [],
825
+ conditionCombinator: 'AND'
826
+ };
827
+ this.blocks.update((blocks) => [...blocks, newBlock]);
828
+ this.draftTypes.set([]);
829
+ return newBlock;
830
+ }
831
+ // ── Block + group + condition mutators ───────────────────────────────────
832
+ /** Remove a whole block; cancels the in-progress edit if it belonged to that block. */
833
+ removeBlock(block) {
834
+ this.blocks.update((blocks) => blocks.filter((blk) => blk !== block));
835
+ if (this.activeBlock() === block) {
836
+ this.cancelPending();
837
+ }
838
+ }
839
+ /** Append an empty `ConditionGroup` to a container and start adding inside it. */
840
+ addGroup(parent) {
841
+ const newGroup = { kind: 'group', conditions: [], combinator: 'AND' };
842
+ this.blocks.update((blocks) => updateContainer(blocks, parent, (container) => withConditions(container, [...container.conditions, newGroup])));
843
+ // The new group's reference is stable across the update because we appended
844
+ // its literal — safe to use directly as the active container.
845
+ this.startAddCondition(newGroup);
846
+ return newGroup;
847
+ }
848
+ /**
849
+ * Remove a group from its parent container. If the parent group becomes
850
+ * empty, it is also removed (cascading up). Top-level blocks are preserved.
851
+ */
852
+ removeGroup(group) {
853
+ this.blocks.update((blocks) => removeNode(blocks, group));
854
+ if (this.activeContainer() === group) {
855
+ this.cancelPending();
856
+ }
857
+ }
858
+ /** Remove a condition from the tree; empty parent groups/tables are pruned by the cascade. */
859
+ removeCondition(_container, condition) {
860
+ // While an *existing* condition is being edited it is temporarily pulled out
861
+ // of the tree (see editCondition). Removing the last remaining sibling would
862
+ // empty — and thus prune — the shared container, losing the in-flight edit.
863
+ // Re-home the snapshot first so it survives, then end the edit cleanly.
864
+ if (this.editingConditionIndex() >= 0 && this.#editingSnapshot) {
865
+ this.restoreEditingSnapshot();
866
+ this.cancelPending();
867
+ }
868
+ this.blocks.update((blocks) => removeNode(blocks, condition));
869
+ // If the active container itself was pruned by the cascade, drop the edit.
870
+ const active = this.activeContainer();
871
+ if (active && !this.#containerExists(active)) {
872
+ this.cancelPending();
873
+ }
874
+ }
875
+ /** Set the top-level combinator joining the query units (full-text unit + blocks). */
876
+ setCombinator(value) {
877
+ this.combinator.set(value);
878
+ }
879
+ /** Set the AND/OR combinator of a single container (block, group or table). */
880
+ setContainerCombinator(container, value) {
881
+ this.blocks.update((blocks) => updateContainer(blocks, container, (target) => isConditionGroup(target) || isTableCondition(target)
882
+ ? { ...target, combinator: value }
883
+ : { ...target, conditionCombinator: value }));
884
+ }
885
+ /**
886
+ * The picked field resolves to a queryable `table` type. A table has no operator
887
+ * of its own; instead it holds column conditions directly. Insert an empty
888
+ * {@link TableCondition} and scope the edit into it so the user picks a column next.
889
+ */
890
+ addTableCondition(field) {
891
+ const container = this.activeContainer();
892
+ if (!container)
893
+ return;
894
+ const table = {
895
+ kind: 'table',
896
+ fieldId: field.id,
897
+ fieldLabel: field.label,
898
+ conditions: [],
899
+ // Columns are row-scoped and always join with OR ("a row that has colA as
900
+ // well as colB"); the UI shows a static "as well as" label, not a toggle.
901
+ combinator: 'OR'
902
+ };
903
+ this.blocks.update((blocks) => updateContainer(blocks, container, (target) => withConditions(target, [...target.conditions, table])));
904
+ // `table`'s reference is stable across the update (we appended its literal).
905
+ this.startAddCondition(table);
906
+ }
907
+ // ── Inline-edit flow ──────────────────────────────────────────────────────
908
+ /** Begin adding a condition inside `container`. */
909
+ startAddCondition(container) {
910
+ const owningBlock = findOwningBlock(this.blocks(), container);
911
+ if (!owningBlock)
912
+ return;
913
+ this.activeBlock.set(owningBlock);
914
+ this.activeContainer.set(container);
915
+ this.pendingField.set(null);
916
+ this.pendingOperatorLabel.set('');
917
+ this.editingConditionIndex.set(-1);
918
+ this.#editingSnapshot = null;
919
+ this.fieldCtrl.setValue('', { emitEvent: false });
920
+ this.operatorCtrl.setValue('', { emitEvent: false });
921
+ this.valueCtrl.setValue('', { emitEvent: false });
922
+ this.step.set('field');
923
+ }
924
+ /** Begin editing an existing condition inside `container`. */
925
+ editCondition(container, condition, index) {
926
+ this.#editingSnapshot = { ...condition };
927
+ // Remove the condition from the tree so the inline editor takes its slot,
928
+ // and capture the new container reference produced by the immutable update.
929
+ let refreshed = container;
930
+ this.blocks.update((blocks) => updateContainer(blocks, container, (target) => {
931
+ const next = withConditions(target, target.conditions.filter((node) => node !== condition));
932
+ refreshed = next;
933
+ return next;
934
+ }));
935
+ const owningBlock = findOwningBlock(this.blocks(), refreshed);
936
+ if (!owningBlock)
937
+ return;
938
+ this.activeBlock.set(owningBlock);
939
+ this.activeContainer.set(refreshed);
940
+ this.editingConditionIndex.set(index);
941
+ const fieldItem = {
942
+ kind: 'field',
943
+ id: condition.fieldId,
944
+ label: condition.fieldLabel,
945
+ internalType: condition.internalType,
946
+ operator: condition.operator
947
+ };
948
+ this.pendingField.set(fieldItem);
949
+ this.pendingOperatorLabel.set(this.operatorLabel(condition.operator));
950
+ this.fieldCtrl.setValue(condition.fieldLabel, { emitEvent: false });
951
+ this.operatorCtrl.setValue(this.operatorLabel(condition.operator), { emitEvent: false });
952
+ this.step.set(isValuelessOperator(condition.operator) ? 'operator' : 'value');
953
+ }
954
+ /** Jump back to the field step and clear the input so the user can re-type. */
955
+ editField() {
956
+ this.fieldCtrl.setValue('', { emitEvent: false });
957
+ this.step.set('field');
958
+ this.inputTerm.set('');
959
+ }
960
+ /** Jump back to the operator step and clear the input so the user can re-type. */
961
+ editOperator() {
962
+ this.operatorCtrl.setValue('', { emitEvent: false });
963
+ this.step.set('operator');
964
+ this.inputTerm.set('');
965
+ }
966
+ /** Abandon the in-progress edit, restoring state and pruning any empty container created for it. */
967
+ cancelPending() {
968
+ // Prune empty containers created via addGroup / addTableCondition and never
969
+ // committed into. removeNode cascades up, so an empty parent is removed too.
970
+ const container = this.activeContainer();
971
+ if (container &&
972
+ (isConditionGroup(container) || isTableCondition(container)) &&
973
+ container.conditions.length === 0) {
974
+ this.blocks.update((blocks) => removeNode(blocks, container));
975
+ }
976
+ this.activeBlock.set(null);
977
+ this.activeContainer.set(null);
978
+ this.pendingField.set(null);
979
+ this.pendingOperatorLabel.set('');
980
+ this.editingConditionIndex.set(-1);
981
+ this.#editingSnapshot = null;
982
+ this.draftTypes.set([]);
983
+ this.step.set('type');
984
+ this.fieldCtrl.setValue('', { emitEvent: false });
985
+ this.operatorCtrl.setValue('', { emitEvent: false });
986
+ this.valueCtrl.setValue('', { emitEvent: false });
987
+ this.inputTerm.set('');
988
+ }
989
+ /** Insert (or, when editing, re-insert at its original index) the finished condition into the active container. */
990
+ commitCondition(condition) {
991
+ const container = this.activeContainer();
992
+ if (!container)
993
+ return;
994
+ const idx = this.editingConditionIndex();
995
+ this.blocks.update((blocks) => updateContainer(blocks, container, (target) => {
996
+ const conditions = [...target.conditions];
997
+ if (idx >= 0 && idx <= conditions.length) {
998
+ conditions.splice(idx, 0, condition);
999
+ }
1000
+ else {
1001
+ conditions.push(condition);
1002
+ }
1003
+ return withConditions(target, conditions);
1004
+ }));
1005
+ this.activeBlock.set(null);
1006
+ this.activeContainer.set(null);
1007
+ this.pendingField.set(null);
1008
+ this.pendingOperatorLabel.set('');
1009
+ this.editingConditionIndex.set(-1);
1010
+ this.#editingSnapshot = null;
1011
+ this.step.set('type');
1012
+ this.fieldCtrl.setValue('', { emitEvent: false });
1013
+ this.operatorCtrl.setValue('', { emitEvent: false });
1014
+ this.valueCtrl.setValue('', { emitEvent: false });
1015
+ this.inputTerm.set('');
1016
+ }
1017
+ /**
1018
+ * On blur with an incomplete edit of an existing condition, reinsert the
1019
+ * original verbatim at its original index.
1020
+ */
1021
+ restoreEditingSnapshot() {
1022
+ const idx = this.editingConditionIndex();
1023
+ const container = this.activeContainer();
1024
+ const snapshot = this.#editingSnapshot;
1025
+ if (idx < 0 || !container || !snapshot)
1026
+ return;
1027
+ this.blocks.update((blocks) => updateContainer(blocks, container, (target) => {
1028
+ const conditions = [...target.conditions];
1029
+ conditions.splice(idx, 0, snapshot);
1030
+ return withConditions(target, conditions);
1031
+ }));
1032
+ }
1033
+ /**
1034
+ * Whether the in-progress condition has all parts needed to be committed. A value
1035
+ * is *not* required: a field + operator with a blank value commits as an unset
1036
+ * placeholder (see {@link isConditionUnset}).
1037
+ */
1038
+ isConditionComplete() {
1039
+ const field = this.pendingField();
1040
+ if (!field)
1041
+ return false;
1042
+ return (field.operator ?? '') !== '';
1043
+ }
1044
+ /** The operators available for a field's internal type, as autocomplete items. */
1045
+ operatorsForField(field) {
1046
+ return this.#operatorsForInternalType(field.internalType ?? 'string');
1047
+ }
1048
+ /** Human-readable label for an operator id: a math symbol (`=`, `≠`, …), a translated key, or the id itself. */
1049
+ operatorLabel(operator) {
1050
+ const symbol = OPERATOR_SYMBOLS[operator];
1051
+ if (symbol)
1052
+ return symbol;
1053
+ const key = OPERATOR_LABEL_KEYS[operator];
1054
+ return key ? this.#translate.instant(key) : operator;
1055
+ }
1056
+ /** Build a condition record from a date preset / boolean operator / value. */
1057
+ buildCommitCondition(field, operatorId, operatorLabelText, value, internalTypeOverride) {
1058
+ if (operatorId.startsWith('date:')) {
1059
+ return {
1060
+ fieldId: field.id,
1061
+ internalType: internalTypeOverride ?? field.internalType ?? 'datetime',
1062
+ fieldLabel: field.label,
1063
+ operator: operatorId,
1064
+ operatorLabel: operatorLabelText,
1065
+ value: operatorId.slice(DATE_PREFIX_LEN$1),
1066
+ conditionLabel: `${field.label} ${operatorLabelText}`
1067
+ };
1068
+ }
1069
+ if (operatorId === 'empty' || operatorId === 'not_empty') {
1070
+ return {
1071
+ fieldId: field.id,
1072
+ internalType: internalTypeOverride ?? field.internalType ?? 'string',
1073
+ fieldLabel: field.label,
1074
+ operator: operatorId,
1075
+ operatorLabel: operatorLabelText,
1076
+ value: '',
1077
+ conditionLabel: `${field.label} ${operatorLabelText}`
1078
+ };
1079
+ }
1080
+ if (operatorId === 'eq_true' || operatorId === 'eq_false') {
1081
+ const val = operatorId === 'eq_true' ? 'true' : 'false';
1082
+ return {
1083
+ fieldId: field.id,
1084
+ internalType: internalTypeOverride ?? field.internalType ?? 'boolean',
1085
+ fieldLabel: field.label,
1086
+ operator: 'eq',
1087
+ operatorLabel: '=',
1088
+ value: val,
1089
+ conditionLabel: `${field.label} = ${val}`
1090
+ };
1091
+ }
1092
+ const valueLabel = Array.isArray(value) ? value.join(', ') : value;
1093
+ return {
1094
+ fieldId: field.id,
1095
+ internalType: internalTypeOverride ?? field.internalType ?? 'string',
1096
+ fieldLabel: field.label,
1097
+ operator: operatorId,
1098
+ operatorLabel: operatorLabelText,
1099
+ value,
1100
+ // Omit the value segment for an unset placeholder so the chip/aria label reads
1101
+ // cleanly ("Name like") instead of carrying a trailing space.
1102
+ conditionLabel: valueLabel ? `${field.label} ${operatorLabelText} ${valueLabel}` : `${field.label} ${operatorLabelText}`
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Resolve a field definition by id across the given block types, falling back to
1107
+ * the universal base fields. Used by the host to render the value editor for a
1108
+ * picked field (including inherited base fields not present on any type).
1109
+ */
1110
+ resolveFieldDefinition(types, fieldId) {
1111
+ // When editing inside a table row, the field is one of the table's columns.
1112
+ const column = this.activeTableColumns()?.find((col) => col.id === fieldId);
1113
+ if (column)
1114
+ return column;
1115
+ return this.#resolveTypeField(types, fieldId);
1116
+ }
1117
+ /** Resolve a field by id from the block types, falling back to base/system fields. */
1118
+ #resolveTypeField(types, fieldId) {
1119
+ for (const type of types) {
1120
+ const objectType = this.objectTypes().find((candidate) => candidate.id === type.id);
1121
+ const field = objectType?.fields.find((typeField) => typeField.id === fieldId);
1122
+ if (field)
1123
+ return this.#withInternalType(field);
1124
+ }
1125
+ const baseField = this.#baseFields().find((field) => field.id === fieldId);
1126
+ return baseField ? this.#withInternalType(baseField) : null;
1127
+ }
1128
+ /**
1129
+ * Re-derive `_internalType` from the field's full classification — including its
1130
+ * `catalog` reference. The schema-baked `_internalType` omits `catalog`, so a
1131
+ * dynamic catalog would otherwise be misrendered as a static (empty) catalog.
1132
+ * Mirrors how object-form / renderer.service resolve the element type.
1133
+ */
1134
+ #withInternalType(field) {
1135
+ return {
1136
+ ...field,
1137
+ _internalType: this.#system.getInternalFormElementType(field.propertyType, field.classifications, field.catalog)
1138
+ };
1139
+ }
1140
+ // ── Internals ─────────────────────────────────────────────────────────────
1141
+ #nextId() {
1142
+ this.#blockSeq += 1;
1143
+ return `blk-${this.#blockSeq}`;
1144
+ }
1145
+ /**
1146
+ * Keep the id sequence ahead of any id already in the tree (e.g. ids carried in
1147
+ * by a loaded state). Without this, `#blockSeq` stays at 0 after `loadState` and
1148
+ * the next generated id (`blk-1`) collides with a restored `blk-1`, breaking the
1149
+ * `@for` track-by on block id.
1150
+ */
1151
+ #trackSeq(id) {
1152
+ const match = /^blk-(\d+)$/.exec(id);
1153
+ if (match)
1154
+ this.#blockSeq = Math.max(this.#blockSeq, Number(match[1]));
1155
+ }
1156
+ /**
1157
+ * Normalize a block coming from a loaded state. Ensures an `id`, and migrates
1158
+ * legacy single-type blocks (`typeId`/`typeLabel`/`isSot`) into the `types[]`
1159
+ * shape so older saved states remain loadable.
1160
+ */
1161
+ #normalizeBlock(block) {
1162
+ const legacy = block;
1163
+ const types = Array.isArray(legacy.types)
1164
+ ? legacy.types
1165
+ : [{ id: legacy.typeId ?? '', label: legacy.typeLabel ?? legacy.typeId ?? '', isSot: legacy.isSot }];
1166
+ const id = legacy.id ?? this.#nextId();
1167
+ this.#trackSeq(id);
1168
+ return {
1169
+ id,
1170
+ types,
1171
+ conditions: legacy.conditions ?? [],
1172
+ conditionCombinator: legacy.conditionCombinator ?? 'AND'
1173
+ };
1174
+ }
1175
+ /**
1176
+ * Fields shared by *all* given types (the intersection by field id), shaped as
1177
+ * field suggestions. A field must be `queryable`, not in `skipProperties`, and
1178
+ * present on every type. Field metadata (label, internalType) is taken from the
1179
+ * first type that declares it.
1180
+ *
1181
+ * Each type's own fields are unioned with the universal base/system fields so
1182
+ * inherited properties (e.g. `system:creationDate`) become selectable too.
1183
+ */
1184
+ #sharedFields(types) {
1185
+ if (types.length === 0)
1186
+ return [];
1187
+ const objectTypes = types
1188
+ .map((type) => this.objectTypes().find((objectType) => objectType.id === type.id))
1189
+ .filter((objectType) => !!objectType);
1190
+ if (objectTypes.length !== types.length)
1191
+ return [];
1192
+ const skip = new Set(this.skipProperties());
1193
+ const [first, ...rest] = objectTypes;
1194
+ const restFieldIds = rest.map((objectType) => new Set(this.#resolveFields(objectType).map((field) => field.id)));
1195
+ return this.#resolveFields(first)
1196
+ .filter((field) => field.queryable !== false && !skip.has(field.id))
1197
+ .filter((field) => restFieldIds.every((ids) => ids.has(field.id)))
1198
+ .map((field) => ({
1199
+ kind: 'field',
1200
+ id: field.id,
1201
+ label: this.#system.getLocalizedLabel(field.id) || field.label || field.name || field.id,
1202
+ internalType: this.#system.getInternalFormElementType(field.propertyType, field.classifications, field.catalog)
1203
+ }));
1204
+ }
1205
+ /**
1206
+ * The universal base/system fields (`system:creationDate`, `system:createdBy`, …)
1207
+ * resolved from the global property definitions. These are inherited by every
1208
+ * object type but are not part of any concrete type's resolved `fields`.
1209
+ */
1210
+ #baseFields() {
1211
+ return Object.values(BaseObjectTypeField)
1212
+ .map((id) => this.#system.getObjectTypeField(id))
1213
+ .filter((field) => !!field);
1214
+ }
1215
+ /** A type's own fields unioned with the base fields, de-duplicated by id (own wins). */
1216
+ #resolveFields(objectType) {
1217
+ return this.#dedupeById([...objectType.fields, ...this.#baseFields()]);
1218
+ }
1219
+ #dedupeById(fields) {
1220
+ const seen = new Set();
1221
+ return fields.filter((field) => (seen.has(field.id) ? false : seen.add(field.id) && true));
1222
+ }
1223
+ // ── Table columns ──────────────────────────────────────────────────────────
1224
+ /**
1225
+ * Resolve a table field's `columnDefinitions` into full `ObjectTypeField`s. Defaults to
1226
+ * the active block (the inline-edit case); form mode passes the owning block explicitly.
1227
+ */
1228
+ #tableColumns(table, block = this.activeBlock()) {
1229
+ if (!block)
1230
+ return [];
1231
+ const tableField = this.#resolveTypeField(block.types, table.fieldId);
1232
+ return (tableField?.columnDefinitions ?? []).map((col) => this.#columnToField(col));
1233
+ }
1234
+ /**
1235
+ * Map a table column definition to an `ObjectTypeField` with a re-derived
1236
+ * `_internalType` (so the value editor renders the right widget). Column defs may
1237
+ * carry `classification` (schema) or `classifications` (base shape) — accept both.
1238
+ */
1239
+ #columnToField(col) {
1240
+ const classifications = col.classifications ?? col.classification;
1241
+ return {
1242
+ ...col,
1243
+ name: col.name ?? col.id,
1244
+ label: this.#system.getLocalizedLabel(col.id) || col.id,
1245
+ classifications,
1246
+ _internalType: this.#system.getInternalFormElementType(col.propertyType, classifications, col.catalog)
1247
+ };
1248
+ }
1249
+ /** Shape resolved table columns as field-step suggestions. */
1250
+ #columnSuggestions(columns) {
1251
+ const skip = new Set(this.skipProperties());
1252
+ return columns
1253
+ .filter((col) => col.queryable !== false && !skip.has(col.id))
1254
+ .map((col) => ({
1255
+ kind: 'field',
1256
+ id: col.id,
1257
+ label: this.#system.getLocalizedLabel(col.id) || col.label || col.name || col.id,
1258
+ internalType: col._internalType
1259
+ }));
1260
+ }
1261
+ /** Check whether a container (block, group or table) still exists in the current tree by reference. */
1262
+ #containerExists(container) {
1263
+ if (!isConditionGroup(container) && !isTableCondition(container)) {
1264
+ return this.blocks().includes(container);
1265
+ }
1266
+ return this.#containsRef(this.blocks().flatMap((block) => block.conditions), container);
1267
+ }
1268
+ #containsRef(nodes, needle) {
1269
+ for (const node of nodes) {
1270
+ if (node === needle)
1271
+ return true;
1272
+ if ((isConditionGroup(node) || isTableCondition(node)) && this.#containsRef(node.conditions, needle))
1273
+ return true;
1274
+ }
1275
+ return false;
1276
+ }
1277
+ #operatorsForInternalType(internalType) {
1278
+ const base = (ids) => ids.map((id) => ({ kind: 'operator', id, label: this.operatorLabel(id) }));
1279
+ // Null checks are offered on every queryable type.
1280
+ const empty = () => base(['empty', 'not_empty']);
1281
+ switch (internalType) {
1282
+ case 'integer':
1283
+ case 'decimal':
1284
+ return [...base(['eq', 'neq', 'gt', 'gte', 'lt', 'lte']), ...empty()];
1285
+ case 'datetime':
1286
+ return [
1287
+ ...base(['eq', 'neq', 'gt', 'gte', 'lt', 'lte']),
1288
+ ...DATE_PRESETS.map((preset) => ({
1289
+ kind: 'date-preset',
1290
+ id: `date:${preset.id}`,
1291
+ label: this.#translate.instant(preset.labelKey)
1292
+ })),
1293
+ ...empty()
1294
+ ];
1295
+ case 'boolean':
1296
+ case 'boolean:switch':
1297
+ return [
1298
+ { kind: 'operator', id: 'eq_true', label: this.#translate.instant('yuv.smart-search.operator.eq-true') },
1299
+ { kind: 'operator', id: 'eq_false', label: this.#translate.instant('yuv.smart-search.operator.eq-false') },
1300
+ ...empty()
1301
+ ];
1302
+ case 'string:catalog':
1303
+ case 'string:catalog:i18n':
1304
+ case 'string:catalog:dynamic':
1305
+ case 'string:organization':
1306
+ case 'string:organization:set':
1307
+ case 'string:reference':
1308
+ return [...base(['eq', 'neq']), ...empty()];
1309
+ case 'table':
1310
+ return [];
1311
+ default:
1312
+ return [...base(['like', 'eq', 'neq']), ...empty()];
1313
+ }
1314
+ }
1315
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SmartSearchEditController, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1316
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SmartSearchEditController }); }
1317
+ }
1318
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SmartSearchEditController, decorators: [{
1319
+ type: Injectable
1320
+ }] });
1321
+
1322
+ /**
1323
+ * Renders a single `ConditionContainer` (block body, nested group, or table).
1324
+ *
1325
+ * - When `bare=true`, the panel wrapper (combinator header + remove button) is
1326
+ * omitted; used by the host for the top-level `SearchBlock` body where the
1327
+ * block already has its own type header.
1328
+ * - When `bare=false` (default), renders the full nested-group panel with a
1329
+ * group-scoped combinator toggle and an X to remove the group.
1330
+ * - A `TableCondition` renders as a labelled panel whose children are column
1331
+ * conditions; like a block/group it offers "Add condition", but no "Add group".
1332
+ *
1333
+ * Chip and inline-editor markup is supplied by the host as `TemplateRef`s so
1334
+ * Material plumbing (FormControls, autocomplete) stays in one place.
1335
+ *
1336
+ * This is an internal building block of {@link SmartSearchComponent} and is
1337
+ * rendered recursively for nested groups and tables — it is not meant to be used
1338
+ * standalone. The host shares its {@link SmartSearchEditController} instance with it.
1339
+ *
1340
+ * @example
1341
+ * ```html
1342
+ * <yuv-smart-search-group
1343
+ * [group]="block"
1344
+ * [bare]="true"
1345
+ * [chipTpl]="chipTpl"
1346
+ * [editorTpl]="editorTpl"
1347
+ * />
1348
+ * ```
1349
+ */
1350
+ class SmartSearchGroupComponent {
1351
+ constructor() {
1352
+ /** The container to render: a top-level block body, a nested group, or a table condition. */
1353
+ this.group = input.required(...(ngDevMode ? [{ debugName: "group" }] : /* istanbul ignore next */ []));
1354
+ /**
1355
+ * When `true`, omit the panel wrapper (combinator header + remove button) and
1356
+ * render only the children. Used for the top-level block body, which already
1357
+ * carries its own type header. Defaults to `false` (full nested-group panel).
1358
+ */
1359
+ this.bare = input(false, ...(ngDevMode ? [{ debugName: "bare" }] : /* istanbul ignore next */ []));
1360
+ /** Host-supplied template for a committed condition chip. */
1361
+ this.chipTpl = input.required(...(ngDevMode ? [{ debugName: "chipTpl" }] : /* istanbul ignore next */ []));
1362
+ /** Host-supplied template for the inline condition editor. */
1363
+ this.editorTpl = input.required(...(ngDevMode ? [{ debugName: "editorTpl" }] : /* istanbul ignore next */ []));
1364
+ this.ctrl = inject(SmartSearchEditController);
1365
+ this.isGroup = isConditionGroup;
1366
+ this.isTable = isTableCondition;
1367
+ /** True when this container is a `TableCondition` (its children are column conditions). */
1368
+ this.isTableContainer = computed(() => isTableCondition(this.group()), ...(ngDevMode ? [{ debugName: "isTableContainer" }] : /* istanbul ignore next */ []));
1369
+ /** Label shown in the table panel header. */
1370
+ this.tableLabel = computed(() => {
1371
+ const container = this.group();
1372
+ return isTableCondition(container) ? container.fieldLabel : '';
1373
+ }, ...(ngDevMode ? [{ debugName: "tableLabel" }] : /* istanbul ignore next */ []));
1374
+ /** Combinator value irrespective of whether this is a block, group or table. */
1375
+ this.containerCombinator = computed(() => {
1376
+ const container = this.group();
1377
+ if (isConditionGroup(container) || isTableCondition(container))
1378
+ return container.combinator;
1379
+ return container.conditionCombinator;
1380
+ }, ...(ngDevMode ? [{ debugName: "containerCombinator" }] : /* istanbul ignore next */ []));
1381
+ this.isActive = computed(() => this.ctrl.activeContainer() === this.group(), ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
1382
+ }
1383
+ /** Set this container's AND/OR combinator. */
1384
+ setCombinator(value) {
1385
+ this.ctrl.setContainerCombinator(this.group(), value);
1386
+ }
1387
+ /** Remove this container when it is a nested group (no-op for blocks and tables). */
1388
+ removeGroup() {
1389
+ const container = this.group();
1390
+ if (isConditionGroup(container)) {
1391
+ this.ctrl.removeGroup(container);
1392
+ }
1393
+ }
1394
+ /** Begin adding a condition inside this container. */
1395
+ startAddCondition() {
1396
+ this.ctrl.startAddCondition(this.group());
1397
+ }
1398
+ /** Add a nested condition group inside this container. */
1399
+ addGroup() {
1400
+ this.ctrl.addGroup(this.group());
1401
+ }
1402
+ /** Narrow a child node to a {@link ConditionGroup} for the template (guarded by `isGroup`). */
1403
+ asGroup(node) {
1404
+ return node;
1405
+ }
1406
+ /** Narrow a child node to a {@link TableCondition} for the template (guarded by `isTable`). */
1407
+ asTable(node) {
1408
+ return node;
1409
+ }
1410
+ /** Narrow a child node to a {@link FieldCondition} for the template (the remaining case). */
1411
+ asCondition(node) {
1412
+ return node;
1413
+ }
1414
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SmartSearchGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1415
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: SmartSearchGroupComponent, isStandalone: true, selector: "yuv-smart-search-group", inputs: { group: { classPropertyName: "group", publicName: "group", isSignal: true, isRequired: true, transformFunction: null }, bare: { classPropertyName: "bare", publicName: "bare", isSignal: true, isRequired: false, transformFunction: null }, chipTpl: { classPropertyName: "chipTpl", publicName: "chipTpl", isSignal: true, isRequired: true, transformFunction: null }, editorTpl: { classPropertyName: "editorTpl", publicName: "editorTpl", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@if (bare()) {\n <ng-container *ngTemplateOutlet=\"body\" />\n} @else {\n <div class=\"group\" [class.group--active]=\"isActive()\" [class.group--table]=\"isTableContainer()\">\n @if (isTableContainer()) {\n <div class=\"group__header\">\n <span class=\"group__table-label\">{{ tableLabel() }}</span>\n <!-- Pseudo-operator: a table reads like a normal condition \u2014 \"Agent has \u2026\".\n It is the only operator a table offers, so it is applied implicitly. -->\n <span class=\"group__table-op\">{{ 'yuv.smart-search.operator.has' | translate }}</span>\n </div>\n }\n <div class=\"group__body\">\n <ng-container *ngTemplateOutlet=\"body\" />\n </div>\n </div>\n}\n\n<ng-template #body>\n @for (node of group().conditions; track node; let i = $index) {\n <div class=\"condition-row\">\n @if (isTable(node)) {\n <yuv-smart-search-group [group]=\"asTable(node)\" [chipTpl]=\"chipTpl()\" [editorTpl]=\"editorTpl()\" />\n } @else if (isGroup(node)) {\n <yuv-smart-search-group [group]=\"asGroup(node)\" [chipTpl]=\"chipTpl()\" [editorTpl]=\"editorTpl()\" />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"chipTpl(); context: { $implicit: asCondition(node), container: group(), index: i }\"\n />\n }\n @if (!$last) {\n <div class=\"condition-combinator\">\n <!-- Table columns are row-scoped and always combine with OR (\"a row that\n has this as well as that\"), so they show a static label instead of a\n toggle. Groups and blocks keep their AND/OR toggle. -->\n @if (isTableContainer()) {\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n } @else {\n <button\n class=\"combinator__btn\"\n [class.combinator__btn--active]=\"containerCombinator() === 'AND'\"\n (click)=\"setCombinator('AND')\"\n >\n {{ 'yuv.smart-search.combinator.and' | translate }}\n </button>\n <button\n class=\"combinator__btn\"\n [class.combinator__btn--active]=\"containerCombinator() === 'OR'\"\n (click)=\"setCombinator('OR')\"\n >\n {{ 'yuv.smart-search.combinator.or' | translate }}\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (isActive()) {\n <ng-container *ngTemplateOutlet=\"editorTpl(); context: { $implicit: group() }\" />\n } @else {\n <div class=\"add-btns\">\n <button\n ymtIconButton\n icon-button-size=\"small\"\n class=\"add-condition-btn\"\n [matTooltip]=\"'yuv.smart-search.add-condition' | translate\"\n (click)=\"startAddCondition()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n <!-- Tables hold column conditions directly \u2014 no grouping inside them. -->\n @if (!isTableContainer()) {\n <button\n ymtIconButton\n icon-button-size=\"small\"\n class=\"add-condition-btn\"\n [matTooltip]=\"'yuv.smart-search.add-group' | translate\"\n (click)=\"addGroup()\"\n >\n <mat-icon>data_array</mat-icon>\n </button>\n }\n </div>\n }\n</ng-template>\n", styles: [":host{display:contents}.group{flex:1;display:flex;flex-direction:column;gap:var(--ymt-spacing-2xs);padding:var(--ymt-spacing-2xs);margin:var(--ymt-spacing-2xs) 0;border-radius:var(--ymt-corner-xs);background:rgb(from var(--ymt-outline) r g b/.06);border:1px dashed var(--ymt-outline)}.group--active{border-color:var(--ymt-primary)}.group--table{border-style:solid}.group__header{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.group__table-label{font-weight:600;font-size:.85em;opacity:.85}.group__table-op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle)}.group__remove{margin-left:auto;opacity:.6}.group__remove:hover{opacity:1}.group__body{display:flex;flex-direction:column;align-items:stretch;gap:var(--ymt-spacing-2xs)}.condition-row{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.condition-row>:first-child{flex:1;min-width:0}.add-btns{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.add-condition-btn{color:var(--ymt-text-color-subtle)}.combinator{display:flex;justify-content:center;gap:0}.combinator__label{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs)}.combinator__btn{--bg: transparent;--fg: var(--ymt-text-color);--mdc-shape-small: var(--ymt-corner-xs);font-size:.82em;background-color:var(--bg);color:var(--fg);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs);border:1px solid var(--ymt-inverse-surface)}.combinator__btn:focus-visible{box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.combinator__btn:first-child{border-radius:var(--mdc-shape-small) 0 0 var(--mdc-shape-small)}.combinator__btn:last-child{border-radius:0 var(--mdc-shape-small) var(--mdc-shape-small) 0}.combinator__btn--active{--bg: var(--ymt-inverse-surface);--fg: var(--ymt-on-inverse-surface)}\n"], dependencies: [{ kind: "component", type: SmartSearchGroupComponent, selector: "yuv-smart-search-group", inputs: ["group", "bare", "chipTpl", "editorTpl"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "directive", type: YmtIconButtonDirective, selector: "button[ymtIconButton],button[ymt-icon-button],a[ymtIconButton],a[ymt-icon-button]", inputs: ["disabled", "disableRipple", "aria-disabled", "disabledInteractive", "icon-button-size"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1416
+ }
1417
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SmartSearchGroupComponent, decorators: [{
1418
+ type: Component,
1419
+ args: [{ selector: 'yuv-smart-search-group', imports: [
1420
+ NgTemplateOutlet,
1421
+ MatButtonModule,
1422
+ YmtIconButtonDirective,
1423
+ MatIconModule,
1424
+ MatTooltipModule,
1425
+ YmtIconButtonDirective,
1426
+ TranslatePipe
1427
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (bare()) {\n <ng-container *ngTemplateOutlet=\"body\" />\n} @else {\n <div class=\"group\" [class.group--active]=\"isActive()\" [class.group--table]=\"isTableContainer()\">\n @if (isTableContainer()) {\n <div class=\"group__header\">\n <span class=\"group__table-label\">{{ tableLabel() }}</span>\n <!-- Pseudo-operator: a table reads like a normal condition \u2014 \"Agent has \u2026\".\n It is the only operator a table offers, so it is applied implicitly. -->\n <span class=\"group__table-op\">{{ 'yuv.smart-search.operator.has' | translate }}</span>\n </div>\n }\n <div class=\"group__body\">\n <ng-container *ngTemplateOutlet=\"body\" />\n </div>\n </div>\n}\n\n<ng-template #body>\n @for (node of group().conditions; track node; let i = $index) {\n <div class=\"condition-row\">\n @if (isTable(node)) {\n <yuv-smart-search-group [group]=\"asTable(node)\" [chipTpl]=\"chipTpl()\" [editorTpl]=\"editorTpl()\" />\n } @else if (isGroup(node)) {\n <yuv-smart-search-group [group]=\"asGroup(node)\" [chipTpl]=\"chipTpl()\" [editorTpl]=\"editorTpl()\" />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"chipTpl(); context: { $implicit: asCondition(node), container: group(), index: i }\"\n />\n }\n @if (!$last) {\n <div class=\"condition-combinator\">\n <!-- Table columns are row-scoped and always combine with OR (\"a row that\n has this as well as that\"), so they show a static label instead of a\n toggle. Groups and blocks keep their AND/OR toggle. -->\n @if (isTableContainer()) {\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n } @else {\n <button\n class=\"combinator__btn\"\n [class.combinator__btn--active]=\"containerCombinator() === 'AND'\"\n (click)=\"setCombinator('AND')\"\n >\n {{ 'yuv.smart-search.combinator.and' | translate }}\n </button>\n <button\n class=\"combinator__btn\"\n [class.combinator__btn--active]=\"containerCombinator() === 'OR'\"\n (click)=\"setCombinator('OR')\"\n >\n {{ 'yuv.smart-search.combinator.or' | translate }}\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (isActive()) {\n <ng-container *ngTemplateOutlet=\"editorTpl(); context: { $implicit: group() }\" />\n } @else {\n <div class=\"add-btns\">\n <button\n ymtIconButton\n icon-button-size=\"small\"\n class=\"add-condition-btn\"\n [matTooltip]=\"'yuv.smart-search.add-condition' | translate\"\n (click)=\"startAddCondition()\"\n >\n <mat-icon>add</mat-icon>\n </button>\n <!-- Tables hold column conditions directly \u2014 no grouping inside them. -->\n @if (!isTableContainer()) {\n <button\n ymtIconButton\n icon-button-size=\"small\"\n class=\"add-condition-btn\"\n [matTooltip]=\"'yuv.smart-search.add-group' | translate\"\n (click)=\"addGroup()\"\n >\n <mat-icon>data_array</mat-icon>\n </button>\n }\n </div>\n }\n</ng-template>\n", styles: [":host{display:contents}.group{flex:1;display:flex;flex-direction:column;gap:var(--ymt-spacing-2xs);padding:var(--ymt-spacing-2xs);margin:var(--ymt-spacing-2xs) 0;border-radius:var(--ymt-corner-xs);background:rgb(from var(--ymt-outline) r g b/.06);border:1px dashed var(--ymt-outline)}.group--active{border-color:var(--ymt-primary)}.group--table{border-style:solid}.group__header{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.group__table-label{font-weight:600;font-size:.85em;opacity:.85}.group__table-op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle)}.group__remove{margin-left:auto;opacity:.6}.group__remove:hover{opacity:1}.group__body{display:flex;flex-direction:column;align-items:stretch;gap:var(--ymt-spacing-2xs)}.condition-row{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.condition-row>:first-child{flex:1;min-width:0}.add-btns{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.add-condition-btn{color:var(--ymt-text-color-subtle)}.combinator{display:flex;justify-content:center;gap:0}.combinator__label{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs)}.combinator__btn{--bg: transparent;--fg: var(--ymt-text-color);--mdc-shape-small: var(--ymt-corner-xs);font-size:.82em;background-color:var(--bg);color:var(--fg);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs);border:1px solid var(--ymt-inverse-surface)}.combinator__btn:focus-visible{box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.combinator__btn:first-child{border-radius:var(--mdc-shape-small) 0 0 var(--mdc-shape-small)}.combinator__btn:last-child{border-radius:0 var(--mdc-shape-small) var(--mdc-shape-small) 0}.combinator__btn--active{--bg: var(--ymt-inverse-surface);--fg: var(--ymt-on-inverse-surface)}\n"] }]
1428
+ }], propDecorators: { group: [{ type: i0.Input, args: [{ isSignal: true, alias: "group", required: true }] }], bare: [{ type: i0.Input, args: [{ isSignal: true, alias: "bare", required: false }] }], chipTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "chipTpl", required: true }] }], editorTpl: [{ type: i0.Input, args: [{ isSignal: true, alias: "editorTpl", required: true }] }] } });
1429
+
1430
+ const DEBOUNCE_MS = 150;
1431
+ const DATE_PREFIX_LEN = 5;
1432
+ /**
1433
+ * Visual query builder that turns guided, chip-based user input into a CMIS query.
1434
+ *
1435
+ * The user composes a search in two independent parts:
1436
+ * - a **full-text bar** (`CONTAINS`) with a scope (all / metadata / content) and an
1437
+ * optional object-type restriction, and
1438
+ * - one or more **type blocks**, each targeting one or more object types and holding
1439
+ * field conditions, nested groups and table-column conditions.
1440
+ *
1441
+ * Conditions are built step by step (type → field → operator → value) with an inline
1442
+ * autocomplete editor; the value step renders the field's real metadata widget
1443
+ * (datepicker, catalog select, organization picker, …). The resulting CMIS query is
1444
+ * emitted through {@link queryChange} on every change and can be saved/restored as a
1445
+ * plain-data {@link SmartSearchState} via {@link getState} / {@link loadState}.
1446
+ *
1447
+ * State and mutators live in {@link SmartSearchEditController} (provided per instance);
1448
+ * this component owns only the UI concerns (focus, autocomplete plumbing, blur handling).
1449
+ *
1450
+ * @example
1451
+ * ```html
1452
+ * <!-- Restrict the picker to two object types and skip a noisy property -->
1453
+ * <yuv-smart-search
1454
+ * [types]="['document', 'invoice']"
1455
+ * [skipProperties]="['system:traceId']"
1456
+ * (queryChange)="onQuery($event)"
1457
+ * />
1458
+ * ```
1459
+ *
1460
+ * @example
1461
+ * ```ts
1462
+ * // Save and restore the builder state (e.g. a stored search)
1463
+ * const search = viewChild.required(SmartSearchComponent);
1464
+ *
1465
+ * onQuery(cmisQuery: string) {
1466
+ * // empty string means "no query" — treat it as a cleared search
1467
+ * this.results.set(cmisQuery ? this.backend.search(cmisQuery) : []);
1468
+ * }
1469
+ *
1470
+ * persist() {
1471
+ * localStorage.setItem('search', JSON.stringify(this.search().getState()));
1472
+ * }
1473
+ *
1474
+ * restore() {
1475
+ * const state = localStorage.getItem('search');
1476
+ * if (state) this.search().loadState(JSON.parse(state));
1477
+ * }
1478
+ * ```
1479
+ */
1480
+ class SmartSearchComponent {
1481
+ #host;
1482
+ /** Previous activeContainer value — used to detect transitions for focus management. */
1483
+ #prevActiveContainer;
1484
+ constructor() {
1485
+ this.#host = inject((ElementRef));
1486
+ this.ctrl = inject(SmartSearchEditController);
1487
+ /**
1488
+ * Object-type ids that may be searched. Restricts the type picker (both the
1489
+ * full-text type filter and the type-block multi-select) to these types. When
1490
+ * empty, no type is offered — set at least one id to enable building blocks.
1491
+ */
1492
+ this.types = input([], ...(ngDevMode ? [{ debugName: "types" }] : /* istanbul ignore next */ []));
1493
+ /**
1494
+ * Field ids to hide from the field-step autocomplete (e.g. internal/system
1495
+ * properties that should not be user-queryable). Applies to block fields and
1496
+ * table columns alike.
1497
+ */
1498
+ this.skipProperties = input([], ...(ngDevMode ? [{ debugName: "skipProperties" }] : /* istanbul ignore next */ []));
1499
+ /**
1500
+ * Enables the **dynamic conditions** feature. When `true`, each committed condition can
1501
+ * be marked dynamic and a form-mode toggle appears that swaps the builder for a generated
1502
+ * fill-out form of those conditions. Off by default — the builder then behaves exactly as
1503
+ * it does without the feature.
1504
+ */
1505
+ this.supportDynamicConditions = input(false, ...(ngDevMode ? [{ debugName: "supportDynamicConditions" }] : /* istanbul ignore next */ []));
1506
+ /**
1507
+ * Emits the current CMIS query string whenever the search changes. An empty
1508
+ * string is emitted for an empty search (including the initial seed), so
1509
+ * consumers should treat `''` as "no query" rather than expecting only
1510
+ * non-empty values.
1511
+ */
1512
+ this.queryChange = output();
1513
+ this.auto = viewChild.required('auto');
1514
+ /** Trigger of the currently-focused autocomplete input — used to re-open the
1515
+ * panel after a type pick so the multi-select stays open. */
1516
+ this.trigger = viewChild(MatAutocompleteTrigger, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
1517
+ /** Term control for the full-text bar (independent of the build-step controls). */
1518
+ this.fulltextTermCtrl = new FormControl('');
1519
+ /** Sentinel option value representing "no type restriction" in the type multi-select. */
1520
+ this.ALL_TYPES = '__all__';
1521
+ /**
1522
+ * Guard that prevents `onInlineBlur` from cancelling the pending condition
1523
+ * when we programmatically open the inline editor.
1524
+ */
1525
+ this._suppressNextBlur = false;
1526
+ /** Prevents the valueChanges subscriber from overwriting inputTerm during programmatic setValue */
1527
+ this._suppressInputTerm = false;
1528
+ /**
1529
+ * Set when a type is staged via the autocomplete so the ENTER that triggered the
1530
+ * selection does not also fall through to `confirmTypes()`. Cleared on the next
1531
+ * tick. Needed because the chip-input directive changes the keydown listener order,
1532
+ * so `onTypeEnter` can run *after* the autocomplete has already closed its panel.
1533
+ */
1534
+ this._suppressTypeConfirm = false;
1535
+ /**
1536
+ * Set while an autocomplete option is being applied so the panel's `closed`
1537
+ * event (which Material emits synchronously right after `optionSelected`) is not
1538
+ * mistaken for the user abandoning an empty property picker. Consumed
1539
+ * synchronously in `onPickerClosed`.
1540
+ */
1541
+ this._pickerJustSelected = false;
1542
+ /** Previous activeContainer value — used to detect transitions for focus management. */
1543
+ this.#prevActiveContainer = null;
1544
+ // ── Public API (proxies onto the controller) ─────────────────────────────
1545
+ this.blocks = this.ctrl.blocks;
1546
+ this.draftTypes = this.ctrl.draftTypes;
1547
+ this.combinator = this.ctrl.combinator;
1548
+ this.step = this.ctrl.step;
1549
+ this.activeBlock = this.ctrl.activeBlock;
1550
+ this.activeContainer = this.ctrl.activeContainer;
1551
+ this.pendingField = this.ctrl.pendingField;
1552
+ this.pendingOperatorLabel = this.ctrl.pendingOperatorLabel;
1553
+ this.editingConditionIndex = this.ctrl.editingConditionIndex;
1554
+ this.inputTerm = this.ctrl.inputTerm;
1555
+ this.cmisQuery = this.ctrl.cmisQuery;
1556
+ this.showCombinator = this.ctrl.showCombinator;
1557
+ this.suggestions = this.ctrl.suggestions;
1558
+ this.fulltext = this.ctrl.fulltext;
1559
+ this.objectTypes = this.ctrl.objectTypes;
1560
+ this.activeBlockFields = this.ctrl.activeBlockFields;
1561
+ this.formMode = this.ctrl.formMode;
1562
+ this.formFields = this.ctrl.formFields;
1563
+ this.formBlocks = this.ctrl.formBlocks;
1564
+ this.hasDynamicConditions = this.ctrl.hasDynamicConditions;
1565
+ /** Whether a committed condition is an unset placeholder (drives the chip's "fill me" affordance). */
1566
+ this.isUnset = isConditionUnset;
1567
+ /**
1568
+ * Whether an operator carries its own meaning and has no value input (`is empty`,
1569
+ * `is not empty`, date presets). Such conditions can't be made dynamic — there is
1570
+ * nothing to fill out in the form.
1571
+ */
1572
+ this.isValueless = isValuelessOperator;
1573
+ /**
1574
+ * Selected ids for the type multi-select. Falls back to the `ALL_TYPES`
1575
+ * sentinel when no concrete type is picked (empty selection = no restriction).
1576
+ */
1577
+ this.fulltextTypeSelection = computed(() => {
1578
+ const ids = this.fulltext().types.map((type) => type.id);
1579
+ return ids.length ? ids : [this.ALL_TYPES];
1580
+ }, ...(ngDevMode ? [{ debugName: "fulltextTypeSelection" }] : /* istanbul ignore next */ []));
1581
+ this.fieldCtrl = this.ctrl.fieldCtrl;
1582
+ this.operatorCtrl = this.ctrl.operatorCtrl;
1583
+ this.valueCtrl = this.ctrl.valueCtrl;
1584
+ /**
1585
+ * Field definition driving the value-step editor. Memoized so the `[field]`
1586
+ * reference stays stable across change-detection cycles — re-resolving on every
1587
+ * CD (via a template method call) would re-create the editor and reset widgets
1588
+ * like the catalog select before their options render.
1589
+ */
1590
+ this.valueFieldDef = computed(() => this.getObjectTypeField(this.ctrl.pendingField()), ...(ngDevMode ? [{ debugName: "valueFieldDef" }] : /* istanbul ignore next */ []));
1591
+ /**
1592
+ * `displayWith` for the shared autocomplete. Suggestions are objects but the
1593
+ * input text is driven by the form controls, so a picked option must not write
1594
+ * a label back into the field — return the raw string, or empty otherwise.
1595
+ */
1596
+ this.displayFn = (item) => {
1597
+ if (typeof item === 'string')
1598
+ return item;
1599
+ return '';
1600
+ };
1601
+ // Mirror the `types` input into the controller signal so it can compute
1602
+ // available object types and filter the type suggestions.
1603
+ effect(() => {
1604
+ this.ctrl.allowedTypes.set(this.types());
1605
+ });
1606
+ effect(() => {
1607
+ this.ctrl.skipProperties.set(this.skipProperties());
1608
+ });
1609
+ // Mirror the dynamic-conditions opt-in into the controller; leaving the feature
1610
+ // disabled also forces form mode off.
1611
+ effect(() => {
1612
+ const enabled = this.supportDynamicConditions();
1613
+ this.ctrl.supportDynamic.set(enabled);
1614
+ if (!enabled)
1615
+ this.ctrl.exitFormMode();
1616
+ });
1617
+ // Bind each generated form row's control to the controller's overlay values. The
1618
+ // form-field set is rebuilt on every enterFormMode, so re-subscribe on change and
1619
+ // tear the previous subscriptions down via the effect's cleanup.
1620
+ effect((onCleanup) => {
1621
+ const fields = this.ctrl.formFields();
1622
+ const sub = new Subscription();
1623
+ for (const field of fields) {
1624
+ sub.add(field.control.valueChanges
1625
+ .pipe(debounceTime(DEBOUNCE_MS))
1626
+ .subscribe((val) => this.ctrl.setFormValue(field.condition, val)));
1627
+ }
1628
+ onCleanup(() => sub.unsubscribe());
1629
+ });
1630
+ // Drive inputTerm from whichever control is active for the current step
1631
+ this.ctrl.fieldCtrl.valueChanges.pipe(debounceTime(DEBOUNCE_MS), takeUntilDestroyed()).subscribe((val) => {
1632
+ const step = this.ctrl.step();
1633
+ if (step === 'field' || step === 'type') {
1634
+ this.ctrl.inputTerm.set(typeof val === 'string' ? val : '');
1635
+ }
1636
+ });
1637
+ this.ctrl.operatorCtrl.valueChanges.pipe(debounceTime(DEBOUNCE_MS), takeUntilDestroyed()).subscribe((val) => {
1638
+ if (this.ctrl.step() === 'operator')
1639
+ this.ctrl.inputTerm.set(typeof val === 'string' ? val : '');
1640
+ });
1641
+ this.ctrl.valueCtrl.valueChanges.pipe(debounceTime(DEBOUNCE_MS), takeUntilDestroyed()).subscribe((val) => {
1642
+ if (this._suppressInputTerm)
1643
+ return;
1644
+ if (this.ctrl.step() === 'value')
1645
+ this.ctrl.inputTerm.set(typeof val === 'string' ? val : '');
1646
+ });
1647
+ // Drive the full-text term (independent of the build steps).
1648
+ this.fulltextTermCtrl.valueChanges.pipe(debounceTime(DEBOUNCE_MS), takeUntilDestroyed()).subscribe((val) => {
1649
+ this.ctrl.setFulltextTerm(typeof val === 'string' ? val : '');
1650
+ });
1651
+ // Emits the seed empty query on first run too — consumers should treat
1652
+ // an empty string as "no query" rather than expecting only non-empty values.
1653
+ effect(() => {
1654
+ this.queryChange.emit(this.ctrl.cmisQuery());
1655
+ });
1656
+ // Focus the inline editor whenever a new container becomes active. Covers
1657
+ // "Add condition" / "Add group" clicks coming from any nesting depth —
1658
+ // those go straight through the controller without touching the host.
1659
+ effect(() => {
1660
+ const container = this.ctrl.activeContainer();
1661
+ if (!container) {
1662
+ this.#prevActiveContainer = null;
1663
+ return;
1664
+ }
1665
+ if (container === this.#prevActiveContainer)
1666
+ return;
1667
+ this.#prevActiveContainer = container;
1668
+ this._suppressNextBlur = true;
1669
+ setTimeout(() => {
1670
+ this.#focusInput();
1671
+ this._suppressNextBlur = false;
1672
+ });
1673
+ });
1674
+ }
1675
+ /** Set the AND/OR combinator that joins the conditions within a single container (block, group or table). */
1676
+ setConditionCombinator(container, value) {
1677
+ this.ctrl.setContainerCombinator(container, value);
1678
+ }
1679
+ /**
1680
+ * Capture the current search as a serializable snapshot. Safe for
1681
+ * `JSON.stringify`/`JSON.parse` roundtrips — use it to persist a search and
1682
+ * later restore it with {@link loadState}.
1683
+ */
1684
+ getState() {
1685
+ return this.ctrl.getState();
1686
+ }
1687
+ /** Restore a previously {@link getState saved} search, replacing the current one. */
1688
+ loadState(state) {
1689
+ this.ctrl.loadState(state);
1690
+ // The full-text term input is a local control, not bound to the signal (scope and
1691
+ // types are). Sync it from the restored state so the term box reflects the load.
1692
+ this.fulltextTermCtrl.setValue(this.ctrl.fulltext().term, { emitEvent: false });
1693
+ }
1694
+ /** Clear the whole search: discard all blocks, conditions and the full-text term. */
1695
+ clear() {
1696
+ this.fulltextTermCtrl.setValue('', { emitEvent: false });
1697
+ this.trigger()?.closePanel();
1698
+ this.ctrl.reset();
1699
+ }
1700
+ /**
1701
+ * Toggle form mode: swap the builder for a generated fill-out form of the user-marked
1702
+ * dynamic conditions. The template is left intact — the form's values are overlaid onto
1703
+ * the query. No-op unless {@link supportDynamicConditions} is set.
1704
+ */
1705
+ toggleFormMode() {
1706
+ this.ctrl.toggleFormMode();
1707
+ }
1708
+ /** Enter form mode ("Essentials") — generated fill-out form of the dynamic conditions. */
1709
+ enterFormMode() {
1710
+ this.ctrl.enterFormMode();
1711
+ }
1712
+ /** Leave form mode ("Full Form") — back to the full builder search. */
1713
+ exitFormMode() {
1714
+ this.ctrl.exitFormMode();
1715
+ }
1716
+ /** Mark/unmark a committed condition as dynamic (surfaced in the fill-out form). */
1717
+ toggleDynamic(container, condition) {
1718
+ this.ctrl.setConditionDynamic(container, condition, !condition.dynamic);
1719
+ }
1720
+ /**
1721
+ * Set the top-level combinator joining the query units (the full-text unit and
1722
+ * the type blocks). Kept for state round-tripping; the UI currently always
1723
+ * joins units with `OR`.
1724
+ */
1725
+ setCombinator(value) {
1726
+ this.ctrl.setCombinator(value);
1727
+ }
1728
+ /** Remove an entire type block and all of its conditions. */
1729
+ removeBlock(block) {
1730
+ this.ctrl.removeBlock(block);
1731
+ }
1732
+ /** Commit the staged draft types into a new block and reset the type input. */
1733
+ confirmTypes() {
1734
+ const block = this.ctrl.confirmDraftTypes();
1735
+ if (!block)
1736
+ return;
1737
+ this.trigger()?.closePanel();
1738
+ this.ctrl.fieldCtrl.setValue('', { emitEvent: false });
1739
+ this.ctrl.inputTerm.set('');
1740
+ setTimeout(() => this.#focusAddCondition(block.id));
1741
+ }
1742
+ /** Start adding a new condition inside the given container and focus the inline editor. */
1743
+ startAddCondition(block) {
1744
+ this.ctrl.startAddCondition(block);
1745
+ this._suppressNextBlur = true;
1746
+ setTimeout(() => {
1747
+ this.#focusInput();
1748
+ this._suppressNextBlur = false;
1749
+ });
1750
+ }
1751
+ /** Add a nested condition group inside the given container and focus the inline editor. */
1752
+ addGroup(block) {
1753
+ this.ctrl.addGroup(block);
1754
+ this._suppressNextBlur = true;
1755
+ setTimeout(() => {
1756
+ this.#focusInput();
1757
+ this._suppressNextBlur = false;
1758
+ });
1759
+ }
1760
+ // ── Full-text bar ───────────────────────────────────────────────────────────
1761
+ /** Set where the full-text search looks: `all` (metadata + content), `metadata`, or `content`. */
1762
+ setFulltextScope(scope) {
1763
+ this.ctrl.setFulltextScope(scope);
1764
+ }
1765
+ /**
1766
+ * Reconcile the type multi-select with the "All types" sentinel. Picking
1767
+ * "All types" while concrete types are selected clears the restriction;
1768
+ * picking any concrete type drops the sentinel.
1769
+ */
1770
+ onFulltextTypesChange(ids) {
1771
+ const hadAll = this.fulltext().types.length === 0;
1772
+ // User just clicked "All types" while specific types were selected → clear restriction.
1773
+ if (ids.includes(this.ALL_TYPES) && !hadAll) {
1774
+ this.ctrl.setFulltextTypes([]);
1775
+ return;
1776
+ }
1777
+ // Otherwise keep only the concrete ids (an empty array also means "all").
1778
+ this.ctrl.setFulltextTypes(ids.filter((id) => id !== this.ALL_TYPES));
1779
+ }
1780
+ // ── Inline-editor coordination ─────────────────────────────────────────────
1781
+ /**
1782
+ * Handle a pick from the shared autocomplete panel. Branches on the current
1783
+ * build step: stages a type (keeping the panel open for more), advances a
1784
+ * field to its operator (or opens a table container), or applies a chosen
1785
+ * operator. Single-operator fields and valueless operators commit immediately.
1786
+ */
1787
+ onSuggestionSelected(event) {
1788
+ const item = event.option.value;
1789
+ // The panel closes (and emits `closed`) as a side effect of this selection.
1790
+ // Flag it so `onPickerClosed` doesn't treat that close as an abandoned edit —
1791
+ // matters for the table branch below, which leaves `step` at 'field'.
1792
+ this._pickerJustSelected = true;
1793
+ setTimeout(() => (this._pickerJustSelected = false));
1794
+ switch (this.ctrl.step()) {
1795
+ case 'type': {
1796
+ const objectType = this.ctrl.objectTypes().find((type) => type.id === item.id);
1797
+ if (!objectType)
1798
+ break;
1799
+ // Stage the type and keep the panel open so more types can be picked.
1800
+ this.ctrl.addDraftType(objectType, item.label);
1801
+ this.ctrl.fieldCtrl.setValue('', { emitEvent: false });
1802
+ this.ctrl.inputTerm.set('');
1803
+ this._suppressNextBlur = true;
1804
+ this._suppressTypeConfirm = true;
1805
+ setTimeout(() => {
1806
+ this.#focusInput();
1807
+ this.trigger()?.openPanel();
1808
+ this._suppressNextBlur = false;
1809
+ this._suppressTypeConfirm = false;
1810
+ });
1811
+ break;
1812
+ }
1813
+ case 'field': {
1814
+ // A table field has no operator of its own: open a table container with a
1815
+ // first row and scope the edit into that row so the user picks a column next.
1816
+ if ((item.internalType ?? '') === 'table') {
1817
+ this.ctrl.addTableCondition(item);
1818
+ this._suppressNextBlur = true;
1819
+ setTimeout(() => {
1820
+ this.#focusInput();
1821
+ this._suppressNextBlur = false;
1822
+ });
1823
+ break;
1824
+ }
1825
+ this.ctrl.pendingField.set(item);
1826
+ this.ctrl.fieldCtrl.setValue(item.label, { emitEvent: false });
1827
+ this.ctrl.operatorCtrl.setValue('', { emitEvent: false });
1828
+ // Clear the field-step filter term so the operator suggestions aren't
1829
+ // filtered by the text typed to find the field — otherwise the operator
1830
+ // panel has no matching options and never opens.
1831
+ this.ctrl.inputTerm.set('');
1832
+ // If the field's type allows exactly one operator, skip the operator
1833
+ // picker and apply it immediately.
1834
+ const operators = this.ctrl.operatorsForField(item);
1835
+ if (operators.length === 1) {
1836
+ this.#applyOperator(item, operators[0]);
1837
+ break;
1838
+ }
1839
+ this.ctrl.step.set('operator');
1840
+ this._suppressNextBlur = true;
1841
+ setTimeout(() => {
1842
+ this.#focusInput();
1843
+ this._suppressNextBlur = false;
1844
+ });
1845
+ break;
1846
+ }
1847
+ case 'operator': {
1848
+ const field = this.ctrl.pendingField();
1849
+ if (!field)
1850
+ break;
1851
+ this.#applyOperator(field, item);
1852
+ break;
1853
+ }
1854
+ }
1855
+ }
1856
+ /**
1857
+ * Enter in the add-type input. When the input is empty there's no option to
1858
+ * pick, so Enter commits the staged types — even while the autocomplete panel
1859
+ * is open. While the user is typing a filter term, Enter is left to the
1860
+ * autocomplete so it can select the highlighted option.
1861
+ */
1862
+ onTypeEnter() {
1863
+ // The ENTER that selected an autocomplete option must not also confirm the
1864
+ // block — regardless of whether this handler runs before or after the
1865
+ // autocomplete closes its panel.
1866
+ if (this._suppressTypeConfirm)
1867
+ return;
1868
+ const term = this.ctrl.fieldCtrl.value;
1869
+ if (typeof term === 'string' && term.trim())
1870
+ return;
1871
+ if (this.ctrl.draftTypes().length === 0)
1872
+ return;
1873
+ this.confirmTypes();
1874
+ }
1875
+ /**
1876
+ * Enter / confirm-button handler for the value step. Commits the in-progress
1877
+ * condition when a value is present. No-op while the autocomplete panel is open
1878
+ * (Enter selects the highlighted option there) or before the value step.
1879
+ */
1880
+ onEnter() {
1881
+ if (this.auto().isOpen)
1882
+ return;
1883
+ if (this.ctrl.step() !== 'value')
1884
+ return;
1885
+ // No value guard: a blank value commits an unset placeholder (fill-in template).
1886
+ const raw = this.ctrl.valueCtrl.value;
1887
+ const field = this.ctrl.pendingField();
1888
+ const fieldOp = field?.operator ?? '';
1889
+ if (!field || !fieldOp)
1890
+ return;
1891
+ this.ctrl.commitCondition(this.ctrl.buildCommitCondition(field, fieldOp, this.ctrl.operatorLabel(fieldOp), normalizeConditionValue(raw)));
1892
+ }
1893
+ /**
1894
+ * Called when the inline-input wrapper loses focus.
1895
+ * Commits the condition if complete, discards it if incomplete.
1896
+ *
1897
+ * Deferred via setTimeout(0): raw value renderers (datepicker dialog,
1898
+ * mat-select panel, mat-autocomplete panel) mount their UI in the
1899
+ * .cdk-overlay-container, which is OUTSIDE the wrapper. Reading
1900
+ * document.activeElement on the next task lets us detect that focus
1901
+ * is still in our logical scope.
1902
+ */
1903
+ onInlineBlur(event) {
1904
+ if (this._suppressNextBlur)
1905
+ return;
1906
+ const wrapper = event.currentTarget;
1907
+ setTimeout(() => {
1908
+ if (this._suppressNextBlur)
1909
+ return;
1910
+ if (!this.ctrl.activeContainer())
1911
+ return;
1912
+ if (this.auto().isOpen)
1913
+ return;
1914
+ const active = document.activeElement;
1915
+ if (active && wrapper.contains(active))
1916
+ return;
1917
+ if (active?.closest('.cdk-overlay-container'))
1918
+ return;
1919
+ // Backdrop dismissal: focus is briefly on <body> while an overlay pane is still mounted.
1920
+ if (document.querySelector('.cdk-overlay-container .cdk-overlay-pane'))
1921
+ return;
1922
+ if (this.ctrl.isConditionComplete()) {
1923
+ this.#commitFromBlur();
1924
+ }
1925
+ else {
1926
+ this.ctrl.restoreEditingSnapshot();
1927
+ this.ctrl.cancelPending();
1928
+ }
1929
+ });
1930
+ }
1931
+ /**
1932
+ * Fired when the shared autocomplete panel closes. Starting a condition opens
1933
+ * the property picker; if the user dismisses it (outside click, Escape) without
1934
+ * choosing a field, there's an empty editor with nothing to commit — drop it.
1935
+ *
1936
+ * Only the field step is handled here: operator/value abandonment is already
1937
+ * covered by `onInlineBlur` (those steps don't auto-open a picker the user can
1938
+ * dismiss while keeping focus). Restores the original when editing an existing
1939
+ * condition, mirroring the blur path.
1940
+ */
1941
+ onPickerClosed() {
1942
+ // `closed` fires synchronously right after `optionSelected`, so consume the
1943
+ // flag here (not in a deferred task) to stay ahead of the focus timers.
1944
+ const justSelected = this._pickerJustSelected;
1945
+ this._pickerJustSelected = false;
1946
+ if (justSelected)
1947
+ return;
1948
+ setTimeout(() => {
1949
+ if (this._suppressNextBlur)
1950
+ return;
1951
+ if (this.auto().isOpen)
1952
+ return;
1953
+ if (this.ctrl.step() !== 'field')
1954
+ return;
1955
+ if (!this.ctrl.activeContainer())
1956
+ return;
1957
+ // A non-empty filter term means the user is still picking a field.
1958
+ const term = this.ctrl.fieldCtrl.value;
1959
+ if (typeof term === 'string' && term.trim())
1960
+ return;
1961
+ this.ctrl.restoreEditingSnapshot();
1962
+ this.ctrl.cancelPending();
1963
+ });
1964
+ }
1965
+ /** Abandon the in-progress condition edit, discarding any partial input. */
1966
+ cancelPending() {
1967
+ this.ctrl.cancelPending();
1968
+ }
1969
+ /** Jump the inline editor back to the field step so the user can re-pick the field. */
1970
+ editField() {
1971
+ this.ctrl.editField();
1972
+ setTimeout(() => this.#focusInput());
1973
+ }
1974
+ /** Jump the inline editor back to the operator step so the user can re-pick the operator. */
1975
+ editOperator() {
1976
+ this.ctrl.editOperator();
1977
+ setTimeout(() => this.#focusInput());
1978
+ }
1979
+ /**
1980
+ * Open an already-committed condition for editing in place. The condition is
1981
+ * pulled out of the tree into the inline editor at its original index, pre-set
1982
+ * to the appropriate step (value, or operator for valueless operators).
1983
+ */
1984
+ editCondition(container, condition, index) {
1985
+ this.ctrl.editCondition(container, condition, index);
1986
+ this._suppressNextBlur = true;
1987
+ if (this.ctrl.step() === 'value') {
1988
+ this._suppressInputTerm = true;
1989
+ setTimeout(() => {
1990
+ this.ctrl.valueCtrl.setValue(condition.value);
1991
+ this._suppressInputTerm = false;
1992
+ this.#focusInput();
1993
+ this._suppressNextBlur = false;
1994
+ });
1995
+ }
1996
+ else {
1997
+ setTimeout(() => {
1998
+ this.#focusInput();
1999
+ this._suppressNextBlur = false;
2000
+ });
2001
+ }
2002
+ }
2003
+ /** Remove a committed condition from its container. */
2004
+ removeCondition(container, condition) {
2005
+ this.ctrl.removeCondition(container, condition);
2006
+ }
2007
+ /**
2008
+ * Renderer input for a committed condition's value, or `null` when the value
2009
+ * should render as plain text. id-based fields (e.g. organization) store an
2010
+ * opaque id as the query value; routing it through the property renderer
2011
+ * resolves it to a human-readable label for the chip. Valueless operators
2012
+ * (empty/not_empty, date presets) and booleans keep their plain text: their
2013
+ * stored value is synthetic/serialized, not the shape those renderers expect.
2014
+ */
2015
+ valueRendererInput(condition) {
2016
+ if (isValuelessOperator(condition.operator))
2017
+ return null;
2018
+ const internalType = condition.internalType ?? '';
2019
+ if (internalType.startsWith('boolean'))
2020
+ return null;
2021
+ if (!isValuePresent(condition.value))
2022
+ return null;
2023
+ return { propertyName: condition.fieldId, rendererType: internalType, value: condition.value };
2024
+ }
2025
+ /**
2026
+ * Resolve the full {@link ObjectTypeField} definition for a suggestion item in
2027
+ * the active block, or `null` when none applies. Drives the value-step metadata
2028
+ * widget so the right editor (datepicker, catalog, …) is rendered.
2029
+ */
2030
+ getObjectTypeField(item) {
2031
+ const block = this.ctrl.activeBlock();
2032
+ if (!item || !block)
2033
+ return null;
2034
+ // The field is shared across all of the block's types — take the definition
2035
+ // from the first type that declares it, falling back to the universal base fields.
2036
+ return this.ctrl.resolveFieldDefinition(block.types, item.id);
2037
+ }
2038
+ // ── Internals ─────────────────────────────────────────────────────────────
2039
+ /**
2040
+ * Apply a chosen operator to the pending field: commit straight away for
2041
+ * date-preset / boolean operators (which carry their own value), otherwise
2042
+ * advance to the value step.
2043
+ */
2044
+ #applyOperator(field, item) {
2045
+ if (item.kind === 'date-preset') {
2046
+ this.ctrl.commitCondition({
2047
+ fieldId: field.id,
2048
+ internalType: field.internalType ?? 'datetime',
2049
+ fieldLabel: field.label,
2050
+ operator: item.id,
2051
+ operatorLabel: item.label,
2052
+ value: item.id.slice(DATE_PREFIX_LEN),
2053
+ conditionLabel: `${field.label} ${item.label}`
2054
+ });
2055
+ }
2056
+ else if (item.id === 'eq_true' || item.id === 'eq_false') {
2057
+ const val = item.id === 'eq_true' ? 'true' : 'false';
2058
+ this.ctrl.commitCondition({
2059
+ fieldId: field.id,
2060
+ internalType: field.internalType ?? 'boolean',
2061
+ fieldLabel: field.label,
2062
+ operator: 'eq',
2063
+ operatorLabel: '=',
2064
+ value: val,
2065
+ conditionLabel: `${field.label} = ${val}`
2066
+ });
2067
+ }
2068
+ else if (item.id === 'empty' || item.id === 'not_empty') {
2069
+ this.ctrl.commitCondition(this.ctrl.buildCommitCondition(field, item.id, item.label, ''));
2070
+ }
2071
+ else {
2072
+ this.ctrl.pendingField.set({ ...field, operator: item.id });
2073
+ this.ctrl.pendingOperatorLabel.set(item.label);
2074
+ this.ctrl.operatorCtrl.setValue(item.label, { emitEvent: false });
2075
+ this.ctrl.inputTerm.set('');
2076
+ this.ctrl.step.set('value');
2077
+ this._suppressNextBlur = true;
2078
+ setTimeout(() => {
2079
+ this.#focusInput();
2080
+ this._suppressNextBlur = false;
2081
+ });
2082
+ }
2083
+ }
2084
+ #commitFromBlur() {
2085
+ const field = this.ctrl.pendingField();
2086
+ if (!field)
2087
+ return;
2088
+ const operator = field.operator ?? '';
2089
+ if (!operator)
2090
+ return;
2091
+ const raw = this.ctrl.valueCtrl.value;
2092
+ this.ctrl.commitCondition(this.ctrl.buildCommitCondition(field, operator, this.ctrl.operatorLabel(operator), normalizeConditionValue(raw)));
2093
+ }
2094
+ #focusInput() {
2095
+ this.#host.nativeElement.querySelector('.inline-input input')?.focus();
2096
+ }
2097
+ /** Focus the "Add condition" (+) button of a block — used after confirming its types. */
2098
+ #focusAddCondition(blockId) {
2099
+ this.#host.nativeElement.querySelector(`.block[data-block-id="${blockId}"] .add-condition-btn`)?.focus();
2100
+ }
2101
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SmartSearchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2102
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: SmartSearchComponent, isStandalone: true, selector: "yuv-smart-search", inputs: { types: { classPropertyName: "types", publicName: "types", isSignal: true, isRequired: false, transformFunction: null }, skipProperties: { classPropertyName: "skipProperties", publicName: "skipProperties", isSignal: true, isRequired: false, transformFunction: null }, supportDynamicConditions: { classPropertyName: "supportDynamicConditions", publicName: "supportDynamicConditions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { queryChange: "queryChange" }, providers: [SmartSearchEditController], viewQueries: [{ propertyName: "auto", first: true, predicate: ["auto"], descendants: true, isSignal: true }, { propertyName: "trigger", first: true, predicate: MatAutocompleteTrigger, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"smart-search\" halo-container halo-container-skip=\"true\">\n <!-- Condition chip template \u2014 passed down into the recursive group component. -->\n <ng-template #chipTpl let-condition let-container=\"container\" let-i=\"index\">\n <div\n class=\"condition-chip\"\n [class.condition-chip--unset]=\"isUnset(condition)\"\n [class.condition-chip--dynamic]=\"condition.dynamic\"\n tabindex=\"0\"\n role=\"button\"\n [attr.aria-label]=\"'yuv.smart-search.condition.edit-aria' | translate: { label: condition.conditionLabel }\"\n (click)=\"editCondition(container, condition, i)\"\n (keydown.space)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n (keydown.enter)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n >\n @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n }\n\n <span class=\"condition-chip__part condition-chip__field\">\n <!-- <mat-icon>tune</mat-icon> -->\n {{ condition.fieldLabel }}\n </span>\n <span class=\"condition-chip__part condition-chip__op\">{{ condition.operatorLabel }}</span>\n <span class=\"condition-chip__part condition-chip__value\">\n @if (isUnset(condition)) {\n <span class=\"condition-chip__placeholder\">{{ 'yuv.smart-search.condition.unset-value' | translate }}</span>\n } @else if (valueRendererInput(condition); as rendererInput) {\n <ng-container *yuvRenderer=\"rendererInput\" />\n } @else {\n {{ condition.value }}\n }\n </span>\n <!-- @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n } -->\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__remove\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'yuv.smart-search.condition.remove' | translate\"\n (click)=\"$event.stopPropagation(); removeCondition(container, condition)\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- Inline editor template \u2014 rendered inside whichever container is active. -->\n <ng-template #editorTpl>\n <div class=\"inline-input\" tabindex=\"-1\" (focusout)=\"onInlineBlur($event)\" (keydown.escape)=\"cancelPending()\">\n @if (ctrl.step() !== 'field' && ctrl.pendingField()) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'field'\"\n (click)=\"editField()\"\n [matTooltip]=\"'yuv.smart-search.field.change' | translate\"\n >\n {{ ctrl.pendingField()?.label }}\n </button>\n }\n\n @if (ctrl.step() === 'value' && ctrl.operatorCtrl.value) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'operator'\"\n (click)=\"editOperator()\"\n [matTooltip]=\"'yuv.smart-search.operator.change' | translate\"\n >\n {{ ctrl.operatorCtrl.value }}\n </button>\n }\n\n @if (ctrl.step() === 'field') {\n <input\n [formControl]=\"ctrl.fieldCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.field.pick' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n } @else if (ctrl.step() === 'operator') {\n <input\n [formControl]=\"ctrl.operatorCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.operator.select' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n } @else if (ctrl.step() === 'value') {\n @let otf = valueFieldDef();\n @if (otf) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"otf\" situation=\"EDIT\" [formControl]=\"ctrl.valueCtrl\" />\n } @else {\n <input\n [formControl]=\"ctrl.valueCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.value.enter' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n }\n }\n\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"confirm-btn\"\n [disabled]=\"!ctrl.isConditionComplete()\"\n (click)=\"onEnter()\"\n [matTooltip]=\"'yuv.smart-search.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"cancel-btn\"\n (click)=\"cancelPending()\"\n [matTooltip]=\"'yuv.smart-search.cancel' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- \u2500\u2500 Toolbar: Essentials \u21C4 Full Form mode switch (only when the dynamic feature is enabled) \u2500\u2500 -->\n @if (supportDynamicConditions() && (hasDynamicConditions() || formMode())) {\n <div class=\"toolbar\">\n <div class=\"toolbar__mode\" role=\"group\" [attr.aria-label]=\"'yuv.smart-search.mode.label' | translate\">\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"formMode()\"\n [attr.aria-pressed]=\"formMode()\"\n (click)=\"enterFormMode()\"\n >\n {{ 'yuv.smart-search.mode.essentials' | translate }}\n </button>\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"!formMode()\"\n [attr.aria-pressed]=\"!formMode()\"\n (click)=\"exitFormMode()\"\n >\n {{ 'yuv.smart-search.mode.full' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Generated fill-out form of the dynamic conditions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (formMode()) {\n <div class=\"dynamic-form\">\n @for (grp of formBlocks(); track grp.block.id; let last = $last) {\n <div class=\"dynamic-form__block\">\n <!-- Muted header listing the block's target types (a block matches any of them). -->\n <div class=\"dynamic-form__block-header\">\n @for (t of grp.block.types; track t.id) {\n @if (!$first) {\n <span class=\"dynamic-form__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"dynamic-form__type\">{{ t.label }}</span>\n }\n </div>\n\n <div class=\"dynamic-form__rows\">\n @for (f of grp.fields; track f.condition) {\n <div class=\"dynamic-form__row\">\n <span class=\"dynamic-form__label\">{{ f.condition.fieldLabel }}</span>\n <span class=\"dynamic-form__op\">{{ f.condition.operatorLabel }}</span>\n <div class=\"dynamic-form__value\">\n @if (f.def) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"f.def\" situation=\"EDIT\" [formControl]=\"f.control\" />\n } @else {\n <input [formControl]=\"f.control\" [placeholder]=\"'yuv.smart-search.value.enter' | translate\" />\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Blocks are type-scoped and always combine with OR (\"this type as well as that type\"). -->\n @if (!last) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n } @empty {\n <span class=\"dynamic-form__empty\">{{ 'yuv.smart-search.mode.empty' | translate }}</span>\n }\n </div>\n } @else {\n <!-- \u2500\u2500 Full-text search bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"fulltext\" [class.muted]=\"!fulltext().term.trim() && ctrl.blocks().length\">\n <!-- Row 1: full-width term -->\n <div class=\"fulltext__term\">\n <mat-icon class=\"fulltext__icon\">search</mat-icon>\n <input\n class=\"fulltext__input\"\n [formControl]=\"fulltextTermCtrl\"\n [placeholder]=\"'yuv.smart-search.fulltext.placeholder' | translate\"\n />\n </div>\n\n <!-- Row 2: scope (single) + types (multiple) -->\n <div class=\"fulltext__filters\">\n <mat-select\n class=\"fulltext__scope\"\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltext().scope\"\n (selectionChange)=\"setFulltextScope($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.scope.label' | translate\"\n >\n <mat-option value=\"all\">{{ 'yuv.smart-search.fulltext.scope.all' | translate }}</mat-option>\n <mat-option value=\"metadata\">{{ 'yuv.smart-search.fulltext.scope.metadata' | translate }}</mat-option>\n <mat-option value=\"content\">{{ 'yuv.smart-search.fulltext.scope.content' | translate }}</mat-option>\n </mat-select>\n\n <mat-select\n class=\"fulltext__types\"\n multiple\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltextTypeSelection()\"\n (selectionChange)=\"onFulltextTypesChange($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.types.label' | translate\"\n >\n <mat-option [value]=\"ALL_TYPES\">{{ 'yuv.smart-search.fulltext.types.all' | translate }}</mat-option>\n @for (t of objectTypes(); track t.id) {\n <mat-option [value]=\"t.id\">{{ t.label ?? t.id }}</mat-option>\n }\n </mat-select>\n </div>\n </div>\n\n <!-- Joiner between the full-text unit and the condition blocks. Top-level units\n always combine with OR (\"looking for this as well as that\"), so this is a\n static label rather than a toggle. -->\n @if (fulltext().term.trim() && ctrl.blocks().length) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n\n <!-- \u2500\u2500 Step 1: Type blocks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (block of ctrl.blocks(); track block.id) {\n <div class=\"block\" [attr.data-block-id]=\"block.id\" [class.block--active]=\"ctrl.activeBlock() === block\">\n <div class=\"block__header\">\n <div class=\"block__types\">\n @for (t of block.types; track t.id) {\n @if (!$first) {\n <span class=\"block__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"block__type\">\n <span class=\"block__type-label\">{{ t.label }}</span>\n </span>\n }\n </div>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"block__remove\"\n (click)=\"removeBlock(block)\"\n [matTooltip]=\"'yuv.smart-search.type.remove' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"block__conditions\">\n <yuv-smart-search-group [group]=\"block\" [bare]=\"true\" [chipTpl]=\"chipTpl\" [editorTpl]=\"editorTpl\" />\n </div>\n </div>\n\n <!-- Static joiner between blocks \u2014 blocks are type-scoped, so they always\n combine with OR (\"this type as well as that type\"). -->\n @if (!$last && ctrl.showCombinator()) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n }\n\n <!-- \u2500\u2500 Step 1 input: add a type block (multi-select) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (ctrl.step() === 'type') {\n <div class=\"add-type-row\">\n <button\n type=\"button\"\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__icon\"\n (click)=\"typeInput.focus()\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n\n <!-- Staged draft types as removable Material chips -->\n <mat-chip-grid\n #typeChipGrid\n class=\"add-type-row__chips\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n @for (t of ctrl.draftTypes(); track t.id) {\n <mat-chip-row (removed)=\"ctrl.removeDraftType(t.id)\">\n {{ t.label }}\n <button matChipRemove [attr.aria-label]=\"'yuv.smart-search.type.remove-draft' | translate\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-chip-row>\n }\n <input\n #typeInput\n [formControl]=\"ctrl.fieldCtrl\"\n [placeholder]=\"'yuv.smart-search.add-type' | translate\"\n [matAutocomplete]=\"auto\"\n [matChipInputFor]=\"typeChipGrid\"\n (keydown.enter)=\"onTypeEnter()\"\n (keydown.escape)=\"cancelPending()\"\n />\n </mat-chip-grid>\n\n @if (ctrl.draftTypes().length) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__confirm\"\n (click)=\"confirmTypes()\"\n [matTooltip]=\"'yuv.smart-search.type.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n }\n </div>\n }\n }\n\n <!-- \u2500\u2500 Shared autocomplete panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <mat-autocomplete\n #auto\n panelWidth=\"auto\"\n [displayWith]=\"displayFn\"\n (optionSelected)=\"onSuggestionSelected($event)\"\n (closed)=\"onPickerClosed()\"\n >\n @for (s of ctrl.suggestions(); track s.id) {\n <mat-option [value]=\"s\">\n <div class=\"suggestion\">\n @if (s.kind !== 'type') {\n <mat-icon class=\"suggestion__icon\">\n @switch (s.kind) {\n @case ('field') {\n tune\n }\n @case ('date-preset') {\n calendar_today\n }\n @default {\n manage_search\n }\n }\n </mat-icon>\n }\n <span class=\"suggestion__label\">{{ s.label }}</span>\n </div>\n </mat-option>\n }\n </mat-autocomplete>\n</div>\n", styles: [":host{display:block;--outline: rgb(from var(--ymt-text-color) r g b / .5);--focus-visible-border-color: var(--ymt-primary);--focus-visible-border-shadow-color: rgb(from var(--ymt-primary) r g b / .3)}::ng-deep .smart-search-select-panel.mat-mdc-select-panel{min-width:max-content}::ng-deep .smart-search-select-panel.mat-mdc-select-panel .mat-mdc-option .mdc-list-item__primary-text{white-space:nowrap}.smart-search{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.fulltext{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px solid var(--outline);border-radius:var(--ymt-corner-s);transition:opacity .15s}.fulltext.muted{opacity:.7}.fulltext.muted:hover,.fulltext.muted:focus-within{opacity:1}.fulltext__term{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.fulltext__icon{color:var(--ymt-text-color-subtle)}.fulltext__input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.fulltext__input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.fulltext__filters{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--ymt-spacing-s)}.fulltext__scope,.fulltext__types{width:auto;font-size:.9em;color:var(--ymt-text-color-subtle);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding-inline:var(--ymt-spacing-xs)}.fulltext__scope:focus-visible,.fulltext__types:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.block{border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden;transition:border-color .15s;padding:var(--ymt-spacing-2xs)}.block:focus-within{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 3px var(--focus-visible-border-shadow-color)}.block__header{border-radius:var(--ymt-corner-xs);display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);background:var(--ymt-surface-container-high)}.block__types{flex:1;display:flex;align-items:center;flex-wrap:wrap}.block__type{display:inline-flex;align-items:center;gap:var(--ymt-spacing-2xs)}.block__type-sep{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-xs)}.block__remove{margin-inline-start:auto;opacity:.7}.block__conditions{display:flex;flex-direction:column;align-items:stretch;gap:var(--ymt-spacing-2xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);min-height:44px}.condition-chip{display:flex;flex:1;align-items:center;gap:0;border-radius:var(--ymt-corner-xs);border:1px solid var(--outline);cursor:pointer;overflow:hidden;outline:none;gap:var(--ymt-spacing-2xs)}.condition-chip:hover{border-color:var(--ymt-outline)}.condition-chip:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__part{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);padding:0 var(--ymt-spacing-xs);height:100%;white-space:nowrap;color:var(--ymt-text-color)}.condition-chip__part:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);padding:0}.condition-chip__value{flex:1;overflow:hidden;text-overflow:ellipsis;min-height:24px;--tile-slot-padding: 0;--yuv-renderer-display: inline-flex}.condition-chip__remove{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__remove:hover{color:var(--ymt-text-color)}.condition-chip__placeholder{font:var(--ymt-font-body-subtle);font-style:italic;color:var(--ymt-text-color-subtle);opacity:.8}.condition-chip__dynamic{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__dynamic:hover{color:var(--ymt-text-color)}.condition-chip--dynamic{outline:2px solid var(--ymt-inverse-surface);outline-offset:1px}.condition-chip--dynamic .condition-chip__dynamic{border-radius:0;background-color:var(--ymt-inverse-surface);color:var(--ymt-on-inverse-surface)}.toolbar{display:flex;justify-content:flex-end;align-items:center;background:var(--ymt-surface-container-high);border-radius:var(--ymt-corner-xs);border:1px solid var(--ymt-outline);padding:var(--ymt-spacing-3xs)}.toolbar__mode{display:inline-flex;gap:0}.toolbar__mode-btn{--bg: transparent;--fg: var(--ymt-text-color);--mdc-shape-small: var(--ymt-corner-xs);font-size:.82em;background-color:var(--bg);color:var(--fg);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs);border:1px solid var(--ymt-inverse-surface);cursor:pointer}.toolbar__mode-btn:focus-visible{box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.toolbar__mode-btn:first-child{border-radius:var(--mdc-shape-small) 0 0 var(--mdc-shape-small)}.toolbar__mode-btn:last-child{border-radius:0 var(--mdc-shape-small) var(--mdc-shape-small) 0}.toolbar__mode-btn--active{--bg: var(--ymt-inverse-surface);--fg: var(--ymt-on-inverse-surface)}.dynamic-form{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.dynamic-form__block{container-type:inline-size;border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden}.dynamic-form__block-header{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-s);font:var(--ymt-font-body-subtle)}.dynamic-form__type-sep{text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-2xs)}.dynamic-form__rows{display:grid;grid-template-columns:max-content max-content 1fr;row-gap:var(--ymt-spacing-s);column-gap:var(--ymt-spacing-xs);overflow:auto;padding:var(--ymt-spacing-s)}.dynamic-form__row{display:grid;grid-template-columns:subgrid;grid-column:1/-1;align-items:center}.dynamic-form__label{color:var(--ymt-text-color)}.dynamic-form__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);border:1px solid var(--outline);padding-inline:var(--ymt-spacing-2xs);border-radius:var(--ymt-corner-xs)}.dynamic-form__value{flex:1;padding:var(--ymt-spacing-xs);border:1px solid var(--outline);border-radius:var(--ymt-corner-xs)}.dynamic-form__value input{width:100%;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.dynamic-form__empty{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle)}@container (max-width: 499px){.dynamic-form__rows{grid-template-columns:1fr auto}.dynamic-form__row{grid-template-columns:subgrid;grid-column:1/-1;row-gap:var(--ymt-spacing-2xs)}.dynamic-form__value{grid-column:1/-1}}.inline-input{display:flex;align-items:center;gap:var(--ymt-spacing-2xs);flex:1;border:1px dashed var(--outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-2xs);background:var(--ymt-surface)}.inline-input yuv-metadata-form-field{border-radius:var(--ymt-corner-xs)}.inline-input yuv-metadata-form-field:focus-within{outline:1px solid var(--focus-visible-border-color)}.inline-input .part-pill{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);white-space:nowrap;font:inherit;font-size:var(--ymt-font-body-subtle-size);color:var(--ymt-text-color);background:var(--ymt-surface-container);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-4xs) var(--ymt-spacing-xs);cursor:pointer;transition:border-color .1s,background .1s}.inline-input .part-pill:hover{background:var(--ymt-surface-container-high)}.inline-input .part-pill:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill--active{border-color:var(--ymt-primary);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill mat-icon{font-size:var(--ymt-sizing-xs);width:var(--ymt-sizing-xs);height:var(--ymt-sizing-xs)}.inline-input yuv-metadata-form-field{flex:1;min-width:150px}.inline-input input{flex:1;border:none;outline:none;background:transparent}.inline-input .confirm-btn:not(:disabled){opacity:1;color:var(--ymt-color-accent)}.combinator{display:flex;justify-content:center;gap:0}.combinator__label{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs)}.add-type-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px dashed var(--ymt-outline);border-radius:var(--ymt-corner-s);color:var(--ymt-text-color-subtle)}.add-type-row__icon{display:inline-flex;align-items:center;padding:0;border:0;background:transparent;color:inherit;cursor:pointer}.add-type-row__icon mat-icon{font-size:var(--ymt-sizing-m);width:var(--ymt-sizing-m);height:var(--ymt-sizing-m);opacity:.5}.add-type-row__icon:hover mat-icon{opacity:.8}.add-type-row__confirm{color:var(--ymt-primary)}.add-type-row__chips{flex:1;min-width:120px}.add-type-row input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;font-size:.9em;color:inherit}.add-type-row input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.condition-combinator{display:flex;align-self:center}.suggestion{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.suggestion__icon{font-size:var(--ymt-sizing-s);width:var(--ymt-sizing-s);height:var(--ymt-sizing-s);--icon-size: var(--ymt-sizing-s);opacity:.6}.suggestion__label{flex:1}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatAutocompleteModule }, { kind: "component", type: i2$1.MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "component", type: i2$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: i2$1.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatChipsModule }, { kind: "component", type: i4.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: i4.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled", "readonly", "matChipInputDisabledInteractive"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: i4.MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: i4.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i5.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: "directive", type: YmtIconButtonDirective, selector: "button[ymtIconButton],button[ymt-icon-button],a[ymtIconButton],a[ymt-icon-button]", inputs: ["disabled", "disableRipple", "aria-disabled", "disabledInteractive", "icon-button-size"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: MetadataFormFieldComponent, selector: "yuv-metadata-form-field", inputs: ["formChangedSubject", "field", "variant", "situation"] }, { kind: "directive", type: RendererDirective, selector: "[yuvRenderer]", inputs: ["yuvRenderer"] }, { kind: "component", type: SmartSearchGroupComponent, selector: "yuv-smart-search-group", inputs: ["group", "bare", "chipTpl", "editorTpl"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2103
+ }
2104
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SmartSearchComponent, decorators: [{
2105
+ type: Component,
2106
+ args: [{ selector: 'yuv-smart-search', imports: [
2107
+ ReactiveFormsModule,
2108
+ MatAutocompleteModule,
2109
+ MatInputModule,
2110
+ MatIconModule,
2111
+ MatButtonModule,
2112
+ MatChipsModule,
2113
+ MatSelectModule,
2114
+ YmtIconButtonDirective,
2115
+ MatTooltipModule,
2116
+ MetadataFormFieldComponent,
2117
+ RendererDirective,
2118
+ SmartSearchGroupComponent,
2119
+ TranslatePipe
2120
+ ], providers: [SmartSearchEditController], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"smart-search\" halo-container halo-container-skip=\"true\">\n <!-- Condition chip template \u2014 passed down into the recursive group component. -->\n <ng-template #chipTpl let-condition let-container=\"container\" let-i=\"index\">\n <div\n class=\"condition-chip\"\n [class.condition-chip--unset]=\"isUnset(condition)\"\n [class.condition-chip--dynamic]=\"condition.dynamic\"\n tabindex=\"0\"\n role=\"button\"\n [attr.aria-label]=\"'yuv.smart-search.condition.edit-aria' | translate: { label: condition.conditionLabel }\"\n (click)=\"editCondition(container, condition, i)\"\n (keydown.space)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n (keydown.enter)=\"$event.preventDefault(); editCondition(container, condition, i)\"\n >\n @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n }\n\n <span class=\"condition-chip__part condition-chip__field\">\n <!-- <mat-icon>tune</mat-icon> -->\n {{ condition.fieldLabel }}\n </span>\n <span class=\"condition-chip__part condition-chip__op\">{{ condition.operatorLabel }}</span>\n <span class=\"condition-chip__part condition-chip__value\">\n @if (isUnset(condition)) {\n <span class=\"condition-chip__placeholder\">{{ 'yuv.smart-search.condition.unset-value' | translate }}</span>\n } @else if (valueRendererInput(condition); as rendererInput) {\n <ng-container *yuvRenderer=\"rendererInput\" />\n } @else {\n {{ condition.value }}\n }\n </span>\n <!-- @if (supportDynamicConditions() && !isValueless(condition.operator)) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__dynamic\"\n tabindex=\"-1\"\n [attr.aria-pressed]=\"!!condition.dynamic\"\n [matTooltip]=\"\n (condition.dynamic\n ? 'yuv.smart-search.condition.unmark-dynamic'\n : 'yuv.smart-search.condition.mark-dynamic'\n ) | translate\n \"\n (click)=\"$event.stopPropagation(); toggleDynamic(container, condition)\"\n >\n <mat-icon>bolt</mat-icon>\n </button>\n } -->\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"condition-chip__remove\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'yuv.smart-search.condition.remove' | translate\"\n (click)=\"$event.stopPropagation(); removeCondition(container, condition)\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- Inline editor template \u2014 rendered inside whichever container is active. -->\n <ng-template #editorTpl>\n <div class=\"inline-input\" tabindex=\"-1\" (focusout)=\"onInlineBlur($event)\" (keydown.escape)=\"cancelPending()\">\n @if (ctrl.step() !== 'field' && ctrl.pendingField()) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'field'\"\n (click)=\"editField()\"\n [matTooltip]=\"'yuv.smart-search.field.change' | translate\"\n >\n {{ ctrl.pendingField()?.label }}\n </button>\n }\n\n @if (ctrl.step() === 'value' && ctrl.operatorCtrl.value) {\n <button\n class=\"part-pill\"\n [class.part-pill--active]=\"ctrl.step() === 'operator'\"\n (click)=\"editOperator()\"\n [matTooltip]=\"'yuv.smart-search.operator.change' | translate\"\n >\n {{ ctrl.operatorCtrl.value }}\n </button>\n }\n\n @if (ctrl.step() === 'field') {\n <input\n [formControl]=\"ctrl.fieldCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.field.pick' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n } @else if (ctrl.step() === 'operator') {\n <input\n [formControl]=\"ctrl.operatorCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.operator.select' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n } @else if (ctrl.step() === 'value') {\n @let otf = valueFieldDef();\n @if (otf) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"otf\" situation=\"EDIT\" [formControl]=\"ctrl.valueCtrl\" />\n } @else {\n <input\n [formControl]=\"ctrl.valueCtrl\"\n [matAutocomplete]=\"auto\"\n [placeholder]=\"'yuv.smart-search.value.enter' | translate\"\n (keydown.enter)=\"onEnter()\"\n />\n }\n }\n\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"confirm-btn\"\n [disabled]=\"!ctrl.isConditionComplete()\"\n (click)=\"onEnter()\"\n [matTooltip]=\"'yuv.smart-search.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"cancel-btn\"\n (click)=\"cancelPending()\"\n [matTooltip]=\"'yuv.smart-search.cancel' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </ng-template>\n\n <!-- \u2500\u2500 Toolbar: Essentials \u21C4 Full Form mode switch (only when the dynamic feature is enabled) \u2500\u2500 -->\n @if (supportDynamicConditions() && (hasDynamicConditions() || formMode())) {\n <div class=\"toolbar\">\n <div class=\"toolbar__mode\" role=\"group\" [attr.aria-label]=\"'yuv.smart-search.mode.label' | translate\">\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"formMode()\"\n [attr.aria-pressed]=\"formMode()\"\n (click)=\"enterFormMode()\"\n >\n {{ 'yuv.smart-search.mode.essentials' | translate }}\n </button>\n <button\n type=\"button\"\n class=\"toolbar__mode-btn\"\n [class.toolbar__mode-btn--active]=\"!formMode()\"\n [attr.aria-pressed]=\"!formMode()\"\n (click)=\"exitFormMode()\"\n >\n {{ 'yuv.smart-search.mode.full' | translate }}\n </button>\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 Generated fill-out form of the dynamic conditions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (formMode()) {\n <div class=\"dynamic-form\">\n @for (grp of formBlocks(); track grp.block.id; let last = $last) {\n <div class=\"dynamic-form__block\">\n <!-- Muted header listing the block's target types (a block matches any of them). -->\n <div class=\"dynamic-form__block-header\">\n @for (t of grp.block.types; track t.id) {\n @if (!$first) {\n <span class=\"dynamic-form__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"dynamic-form__type\">{{ t.label }}</span>\n }\n </div>\n\n <div class=\"dynamic-form__rows\">\n @for (f of grp.fields; track f.condition) {\n <div class=\"dynamic-form__row\">\n <span class=\"dynamic-form__label\">{{ f.condition.fieldLabel }}</span>\n <span class=\"dynamic-form__op\">{{ f.condition.operatorLabel }}</span>\n <div class=\"dynamic-form__value\">\n @if (f.def) {\n <yuv-metadata-form-field variant=\"raw\" [field]=\"f.def\" situation=\"EDIT\" [formControl]=\"f.control\" />\n } @else {\n <input [formControl]=\"f.control\" [placeholder]=\"'yuv.smart-search.value.enter' | translate\" />\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Blocks are type-scoped and always combine with OR (\"this type as well as that type\"). -->\n @if (!last) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n } @empty {\n <span class=\"dynamic-form__empty\">{{ 'yuv.smart-search.mode.empty' | translate }}</span>\n }\n </div>\n } @else {\n <!-- \u2500\u2500 Full-text search bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"fulltext\" [class.muted]=\"!fulltext().term.trim() && ctrl.blocks().length\">\n <!-- Row 1: full-width term -->\n <div class=\"fulltext__term\">\n <mat-icon class=\"fulltext__icon\">search</mat-icon>\n <input\n class=\"fulltext__input\"\n [formControl]=\"fulltextTermCtrl\"\n [placeholder]=\"'yuv.smart-search.fulltext.placeholder' | translate\"\n />\n </div>\n\n <!-- Row 2: scope (single) + types (multiple) -->\n <div class=\"fulltext__filters\">\n <mat-select\n class=\"fulltext__scope\"\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltext().scope\"\n (selectionChange)=\"setFulltextScope($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.scope.label' | translate\"\n >\n <mat-option value=\"all\">{{ 'yuv.smart-search.fulltext.scope.all' | translate }}</mat-option>\n <mat-option value=\"metadata\">{{ 'yuv.smart-search.fulltext.scope.metadata' | translate }}</mat-option>\n <mat-option value=\"content\">{{ 'yuv.smart-search.fulltext.scope.content' | translate }}</mat-option>\n </mat-select>\n\n <mat-select\n class=\"fulltext__types\"\n multiple\n [panelWidth]=\"null\"\n panelClass=\"smart-search-select-panel\"\n [value]=\"fulltextTypeSelection()\"\n (selectionChange)=\"onFulltextTypesChange($event.value)\"\n [aria-label]=\"'yuv.smart-search.fulltext.types.label' | translate\"\n >\n <mat-option [value]=\"ALL_TYPES\">{{ 'yuv.smart-search.fulltext.types.all' | translate }}</mat-option>\n @for (t of objectTypes(); track t.id) {\n <mat-option [value]=\"t.id\">{{ t.label ?? t.id }}</mat-option>\n }\n </mat-select>\n </div>\n </div>\n\n <!-- Joiner between the full-text unit and the condition blocks. Top-level units\n always combine with OR (\"looking for this as well as that\"), so this is a\n static label rather than a toggle. -->\n @if (fulltext().term.trim() && ctrl.blocks().length) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n\n <!-- \u2500\u2500 Step 1: Type blocks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (block of ctrl.blocks(); track block.id) {\n <div class=\"block\" [attr.data-block-id]=\"block.id\" [class.block--active]=\"ctrl.activeBlock() === block\">\n <div class=\"block__header\">\n <div class=\"block__types\">\n @for (t of block.types; track t.id) {\n @if (!$first) {\n <span class=\"block__type-sep\">{{ 'yuv.smart-search.combinator.or' | translate }}</span>\n }\n <span class=\"block__type\">\n <span class=\"block__type-label\">{{ t.label }}</span>\n </span>\n }\n </div>\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"block__remove\"\n (click)=\"removeBlock(block)\"\n [matTooltip]=\"'yuv.smart-search.type.remove' | translate\"\n >\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <div class=\"block__conditions\">\n <yuv-smart-search-group [group]=\"block\" [bare]=\"true\" [chipTpl]=\"chipTpl\" [editorTpl]=\"editorTpl\" />\n </div>\n </div>\n\n <!-- Static joiner between blocks \u2014 blocks are type-scoped, so they always\n combine with OR (\"this type as well as that type\"). -->\n @if (!$last && ctrl.showCombinator()) {\n <div class=\"combinator\">\n <span class=\"combinator__label\">{{ 'yuv.smart-search.combinator.as-well-as' | translate }}</span>\n </div>\n }\n }\n\n <!-- \u2500\u2500 Step 1 input: add a type block (multi-select) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (ctrl.step() === 'type') {\n <div class=\"add-type-row\">\n <button\n type=\"button\"\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__icon\"\n (click)=\"typeInput.focus()\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n <mat-icon>add_circle_outline</mat-icon>\n </button>\n\n <!-- Staged draft types as removable Material chips -->\n <mat-chip-grid\n #typeChipGrid\n class=\"add-type-row__chips\"\n [attr.aria-label]=\"'yuv.smart-search.add-type' | translate\"\n >\n @for (t of ctrl.draftTypes(); track t.id) {\n <mat-chip-row (removed)=\"ctrl.removeDraftType(t.id)\">\n {{ t.label }}\n <button matChipRemove [attr.aria-label]=\"'yuv.smart-search.type.remove-draft' | translate\">\n <mat-icon>close</mat-icon>\n </button>\n </mat-chip-row>\n }\n <input\n #typeInput\n [formControl]=\"ctrl.fieldCtrl\"\n [placeholder]=\"'yuv.smart-search.add-type' | translate\"\n [matAutocomplete]=\"auto\"\n [matChipInputFor]=\"typeChipGrid\"\n (keydown.enter)=\"onTypeEnter()\"\n (keydown.escape)=\"cancelPending()\"\n />\n </mat-chip-grid>\n\n @if (ctrl.draftTypes().length) {\n <button\n ymtIconButton\n icon-button-size=\"extra-small\"\n class=\"add-type-row__confirm\"\n (click)=\"confirmTypes()\"\n [matTooltip]=\"'yuv.smart-search.type.confirm' | translate\"\n >\n <mat-icon>check</mat-icon>\n </button>\n }\n </div>\n }\n }\n\n <!-- \u2500\u2500 Shared autocomplete panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <mat-autocomplete\n #auto\n panelWidth=\"auto\"\n [displayWith]=\"displayFn\"\n (optionSelected)=\"onSuggestionSelected($event)\"\n (closed)=\"onPickerClosed()\"\n >\n @for (s of ctrl.suggestions(); track s.id) {\n <mat-option [value]=\"s\">\n <div class=\"suggestion\">\n @if (s.kind !== 'type') {\n <mat-icon class=\"suggestion__icon\">\n @switch (s.kind) {\n @case ('field') {\n tune\n }\n @case ('date-preset') {\n calendar_today\n }\n @default {\n manage_search\n }\n }\n </mat-icon>\n }\n <span class=\"suggestion__label\">{{ s.label }}</span>\n </div>\n </mat-option>\n }\n </mat-autocomplete>\n</div>\n", styles: [":host{display:block;--outline: rgb(from var(--ymt-text-color) r g b / .5);--focus-visible-border-color: var(--ymt-primary);--focus-visible-border-shadow-color: rgb(from var(--ymt-primary) r g b / .3)}::ng-deep .smart-search-select-panel.mat-mdc-select-panel{min-width:max-content}::ng-deep .smart-search-select-panel.mat-mdc-select-panel .mat-mdc-option .mdc-list-item__primary-text{white-space:nowrap}.smart-search{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.fulltext{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px solid var(--outline);border-radius:var(--ymt-corner-s);transition:opacity .15s}.fulltext.muted{opacity:.7}.fulltext.muted:hover,.fulltext.muted:focus-within{opacity:1}.fulltext__term{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.fulltext__icon{color:var(--ymt-text-color-subtle)}.fulltext__input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.fulltext__input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.fulltext__filters{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--ymt-spacing-s)}.fulltext__scope,.fulltext__types{width:auto;font-size:.9em;color:var(--ymt-text-color-subtle);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding-inline:var(--ymt-spacing-xs)}.fulltext__scope:focus-visible,.fulltext__types:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.block{border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden;transition:border-color .15s;padding:var(--ymt-spacing-2xs)}.block:focus-within{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 3px var(--focus-visible-border-shadow-color)}.block__header{border-radius:var(--ymt-corner-xs);display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);background:var(--ymt-surface-container-high)}.block__types{flex:1;display:flex;align-items:center;flex-wrap:wrap}.block__type{display:inline-flex;align-items:center;gap:var(--ymt-spacing-2xs)}.block__type-sep{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-xs)}.block__remove{margin-inline-start:auto;opacity:.7}.block__conditions{display:flex;flex-direction:column;align-items:stretch;gap:var(--ymt-spacing-2xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);min-height:44px}.condition-chip{display:flex;flex:1;align-items:center;gap:0;border-radius:var(--ymt-corner-xs);border:1px solid var(--outline);cursor:pointer;overflow:hidden;outline:none;gap:var(--ymt-spacing-2xs)}.condition-chip:hover{border-color:var(--ymt-outline)}.condition-chip:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__part{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);padding:0 var(--ymt-spacing-xs);height:100%;white-space:nowrap;color:var(--ymt-text-color)}.condition-chip__part:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.condition-chip__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);padding:0}.condition-chip__value{flex:1;overflow:hidden;text-overflow:ellipsis;min-height:24px;--tile-slot-padding: 0;--yuv-renderer-display: inline-flex}.condition-chip__remove{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__remove:hover{color:var(--ymt-text-color)}.condition-chip__placeholder{font:var(--ymt-font-body-subtle);font-style:italic;color:var(--ymt-text-color-subtle);opacity:.8}.condition-chip__dynamic{--mat-icon-button-container-shape: 0;color:var(--ymt-text-color-subtle)}.condition-chip__dynamic:hover{color:var(--ymt-text-color)}.condition-chip--dynamic{outline:2px solid var(--ymt-inverse-surface);outline-offset:1px}.condition-chip--dynamic .condition-chip__dynamic{border-radius:0;background-color:var(--ymt-inverse-surface);color:var(--ymt-on-inverse-surface)}.toolbar{display:flex;justify-content:flex-end;align-items:center;background:var(--ymt-surface-container-high);border-radius:var(--ymt-corner-xs);border:1px solid var(--ymt-outline);padding:var(--ymt-spacing-3xs)}.toolbar__mode{display:inline-flex;gap:0}.toolbar__mode-btn{--bg: transparent;--fg: var(--ymt-text-color);--mdc-shape-small: var(--ymt-corner-xs);font-size:.82em;background-color:var(--bg);color:var(--fg);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs);border:1px solid var(--ymt-inverse-surface);cursor:pointer}.toolbar__mode-btn:focus-visible{box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.toolbar__mode-btn:first-child{border-radius:var(--mdc-shape-small) 0 0 var(--mdc-shape-small)}.toolbar__mode-btn:last-child{border-radius:0 var(--mdc-shape-small) var(--mdc-shape-small) 0}.toolbar__mode-btn--active{--bg: var(--ymt-inverse-surface);--fg: var(--ymt-on-inverse-surface)}.dynamic-form{display:flex;flex-direction:column;gap:var(--ymt-spacing-xs)}.dynamic-form__block{container-type:inline-size;border:1px solid var(--outline);border-radius:var(--ymt-corner-s);overflow:hidden}.dynamic-form__block-header{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-s);font:var(--ymt-font-body-subtle)}.dynamic-form__type-sep{text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:0 var(--ymt-spacing-2xs)}.dynamic-form__rows{display:grid;grid-template-columns:max-content max-content 1fr;row-gap:var(--ymt-spacing-s);column-gap:var(--ymt-spacing-xs);overflow:auto;padding:var(--ymt-spacing-s)}.dynamic-form__row{display:grid;grid-template-columns:subgrid;grid-column:1/-1;align-items:center}.dynamic-form__label{color:var(--ymt-text-color)}.dynamic-form__op{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle);border:1px solid var(--outline);padding-inline:var(--ymt-spacing-2xs);border-radius:var(--ymt-corner-xs)}.dynamic-form__value{flex:1;padding:var(--ymt-spacing-xs);border:1px solid var(--outline);border-radius:var(--ymt-corner-xs)}.dynamic-form__value input{width:100%;border:none;outline:none;background:transparent;font:inherit;color:var(--ymt-text-color)}.dynamic-form__empty{font:var(--ymt-font-body-subtle);color:var(--ymt-text-color-subtle)}@container (max-width: 499px){.dynamic-form__rows{grid-template-columns:1fr auto}.dynamic-form__row{grid-template-columns:subgrid;grid-column:1/-1;row-gap:var(--ymt-spacing-2xs)}.dynamic-form__value{grid-column:1/-1}}.inline-input{display:flex;align-items:center;gap:var(--ymt-spacing-2xs);flex:1;border:1px dashed var(--outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-2xs);background:var(--ymt-surface)}.inline-input yuv-metadata-form-field{border-radius:var(--ymt-corner-xs)}.inline-input yuv-metadata-form-field:focus-within{outline:1px solid var(--focus-visible-border-color)}.inline-input .part-pill{display:inline-flex;align-items:center;gap:var(--ymt-spacing-4xs);white-space:nowrap;font:inherit;font-size:var(--ymt-font-body-subtle-size);color:var(--ymt-text-color);background:var(--ymt-surface-container);border:1px solid var(--ymt-outline);border-radius:var(--ymt-corner-xs);padding:var(--ymt-spacing-4xs) var(--ymt-spacing-xs);cursor:pointer;transition:border-color .1s,background .1s}.inline-input .part-pill:hover{background:var(--ymt-surface-container-high)}.inline-input .part-pill:focus-visible{border-color:var(--focus-visible-border-color);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill--active{border-color:var(--ymt-primary);box-shadow:0 0 0 2px var(--focus-visible-border-shadow-color)}.inline-input .part-pill mat-icon{font-size:var(--ymt-sizing-xs);width:var(--ymt-sizing-xs);height:var(--ymt-sizing-xs)}.inline-input yuv-metadata-form-field{flex:1;min-width:150px}.inline-input input{flex:1;border:none;outline:none;background:transparent}.inline-input .confirm-btn:not(:disabled){opacity:1;color:var(--ymt-color-accent)}.combinator{display:flex;justify-content:center;gap:0}.combinator__label{font:var(--ymt-font-body-subtle);text-transform:lowercase;color:var(--ymt-text-color-subtle);padding:var(--ymt-spacing-2xs) var(--ymt-spacing-xs)}.add-type-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--ymt-spacing-xs);padding:var(--ymt-spacing-xs) var(--ymt-spacing-s);border:1px dashed var(--ymt-outline);border-radius:var(--ymt-corner-s);color:var(--ymt-text-color-subtle)}.add-type-row__icon{display:inline-flex;align-items:center;padding:0;border:0;background:transparent;color:inherit;cursor:pointer}.add-type-row__icon mat-icon{font-size:var(--ymt-sizing-m);width:var(--ymt-sizing-m);height:var(--ymt-sizing-m);opacity:.5}.add-type-row__icon:hover mat-icon{opacity:.8}.add-type-row__confirm{color:var(--ymt-primary)}.add-type-row__chips{flex:1;min-width:120px}.add-type-row input{flex:1;min-width:120px;border:none;outline:none;background:transparent;font:inherit;font-size:.9em;color:inherit}.add-type-row input::placeholder{color:var(--ymt-text-color-subtle);opacity:.7}.condition-combinator{display:flex;align-self:center}.suggestion{display:flex;align-items:center;gap:var(--ymt-spacing-xs)}.suggestion__icon{font-size:var(--ymt-sizing-s);width:var(--ymt-sizing-s);height:var(--ymt-sizing-s);--icon-size: var(--ymt-sizing-s);opacity:.6}.suggestion__label{flex:1}\n"] }]
2121
+ }], ctorParameters: () => [], propDecorators: { types: [{ type: i0.Input, args: [{ isSignal: true, alias: "types", required: false }] }], skipProperties: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipProperties", required: false }] }], supportDynamicConditions: [{ type: i0.Input, args: [{ isSignal: true, alias: "supportDynamicConditions", required: false }] }], queryChange: [{ type: i0.Output, args: ["queryChange"] }], auto: [{ type: i0.ViewChild, args: ['auto', { isSignal: true }] }], trigger: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatAutocompleteTrigger), { isSignal: true }] }] } });
2122
+
2123
+ /**
2124
+ * Convenience NgModule that imports and re-exports {@link SmartSearchComponent}.
2125
+ *
2126
+ * The component is standalone — prefer importing `SmartSearchComponent` directly.
2127
+ * This module exists only for consumers still organized around NgModules.
2128
+ */
2129
+ const cmp = [SmartSearchComponent];
2130
+ class YuvSmartSearchModule {
2131
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvSmartSearchModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
2132
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.12", ngImport: i0, type: YuvSmartSearchModule, imports: [SmartSearchComponent], exports: [SmartSearchComponent] }); }
2133
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvSmartSearchModule, imports: [cmp] }); }
2134
+ }
2135
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: YuvSmartSearchModule, decorators: [{
2136
+ type: NgModule,
2137
+ args: [{
2138
+ imports: cmp,
2139
+ exports: cmp
2140
+ }]
2141
+ }] });
2142
+
2143
+ /**
2144
+ * Generated bundle index. Do not edit.
2145
+ */
2146
+
2147
+ export { SmartSearchComponent, YuvSmartSearchModule, buildCmisQuery, buildFulltextClause, buildNodeClause, isConditionGroup, isFieldCondition, isTableCondition };
2148
+ //# sourceMappingURL=yuuvis-client-framework-smart-search.mjs.map