@masterteam/formula-builder 0.0.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,878 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, signal, Injectable, viewChild, input, output, computed, effect, forwardRef, Component } from '@angular/core';
3
+ import { CommonModule } from '@angular/common';
4
+ import { TranslocoDirective } from '@jsverse/transloco';
5
+ import * as i1 from '@angular/forms';
6
+ import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
7
+ import { Card } from '@masterteam/components/card';
8
+ import { Button } from '@masterteam/components/button';
9
+ import { SelectField } from '@masterteam/components/select-field';
10
+ import { Tooltip } from '@masterteam/components/tooltip';
11
+ import { RadioCards } from '@masterteam/components/radio-cards';
12
+ import * as i2 from 'primeng/popover';
13
+ import { PopoverModule } from 'primeng/popover';
14
+ import { Skeleton } from 'primeng/skeleton';
15
+ import { createPropertyBlock, FormulaToolbar, FormulaStatusBar, FormulaEditor } from '@masterteam/components/formula';
16
+ export { createFunctionBlock, createLiteralBlock, createOperatorBlock, createPropertyBlock } from '@masterteam/components/formula';
17
+ import { HttpClient } from '@angular/common/http';
18
+ import { Subject, debounceTime, distinctUntilChanged, tap, switchMap, of, catchError } from 'rxjs';
19
+
20
+ /**
21
+ * Formula Validator Service
22
+ * Handles real-time validation via API
23
+ */
24
+ /** Default validation result for empty formulas */
25
+ const EMPTY_VALIDATION = {
26
+ isValid: true,
27
+ errors: [],
28
+ warnings: [],
29
+ dependencies: [],
30
+ complexity: 0,
31
+ };
32
+ /** Error validation result */
33
+ const ERROR_VALIDATION = {
34
+ isValid: false,
35
+ errors: [
36
+ { message: 'Validation failed', line: 1, column: 1, severity: 'Error' },
37
+ ],
38
+ warnings: [],
39
+ dependencies: [],
40
+ complexity: 0,
41
+ };
42
+ class FormulaValidatorService {
43
+ http = inject(HttpClient);
44
+ apiUrl = '/formulas';
45
+ /** Debounce time in ms */
46
+ DEBOUNCE_MS = 300;
47
+ /** Current validation result */
48
+ _validation = signal(EMPTY_VALIDATION, ...(ngDevMode ? [{ debugName: "_validation" }] : []));
49
+ /** Loading state */
50
+ _isValidating = signal(false, ...(ngDevMode ? [{ debugName: "_isValidating" }] : []));
51
+ /** Validation subject for debouncing */
52
+ validateSubject = new Subject();
53
+ /** Public signals */
54
+ validation = this._validation.asReadonly();
55
+ isValidating = this._isValidating.asReadonly();
56
+ constructor() {
57
+ this.setupValidationStream();
58
+ }
59
+ /** Setup debounced validation stream */
60
+ setupValidationStream() {
61
+ this.validateSubject
62
+ .pipe(debounceTime(this.DEBOUNCE_MS), distinctUntilChanged((a, b) => a.formula === b.formula &&
63
+ JSON.stringify(a.knownProperties) ===
64
+ JSON.stringify(b.knownProperties)), tap(() => this._isValidating.set(true)), switchMap((request) => this.callValidateApi(request)))
65
+ .subscribe((result) => {
66
+ this._validation.set(result);
67
+ this._isValidating.set(false);
68
+ });
69
+ }
70
+ /** Validate formula (debounced) */
71
+ validate(formula, knownProperties, levelSchemaId, templateId) {
72
+ // Handle empty formula
73
+ if (!formula || formula.trim() === '') {
74
+ this._validation.set(EMPTY_VALIDATION);
75
+ return;
76
+ }
77
+ // Push to debounced stream
78
+ this.validateSubject.next({
79
+ formula,
80
+ knownProperties,
81
+ levelSchemaId,
82
+ templateId,
83
+ });
84
+ }
85
+ /** Validate formula immediately (no debounce) */
86
+ validateImmediate(formula, knownProperties, levelSchemaId, templateId) {
87
+ if (!formula || formula.trim() === '') {
88
+ return of(EMPTY_VALIDATION);
89
+ }
90
+ this._isValidating.set(true);
91
+ return this.callValidateApi({
92
+ formula,
93
+ knownProperties,
94
+ levelSchemaId,
95
+ templateId,
96
+ }).pipe(tap((result) => {
97
+ this._validation.set(result);
98
+ this._isValidating.set(false);
99
+ }));
100
+ }
101
+ /** Call validation API */
102
+ callValidateApi(request) {
103
+ return this.http
104
+ .post(`${this.apiUrl}/validate`, request)
105
+ .pipe(catchError((error) => {
106
+ console.error('Validation API error:', error);
107
+ // Try to extract error message
108
+ const apiError = error.error;
109
+ const message = apiError?.message ?? apiError?.error ?? 'Validation failed';
110
+ return of({
111
+ ...ERROR_VALIDATION,
112
+ errors: [
113
+ { message, line: 1, column: 1, severity: 'Error' },
114
+ ],
115
+ });
116
+ }));
117
+ }
118
+ /** Reset validation state */
119
+ reset() {
120
+ this._validation.set(EMPTY_VALIDATION);
121
+ this._isValidating.set(false);
122
+ }
123
+ /** Get current validation result */
124
+ getCurrentValidation() {
125
+ return this._validation();
126
+ }
127
+ /** Check if formula is valid */
128
+ isValid() {
129
+ return this._validation().isValid;
130
+ }
131
+ /** Get validation errors */
132
+ getErrors() {
133
+ return this._validation().errors;
134
+ }
135
+ /** Get validation warnings */
136
+ getWarnings() {
137
+ return this._validation().warnings;
138
+ }
139
+ /** Get formula dependencies */
140
+ getDependencies() {
141
+ return this._validation().dependencies;
142
+ }
143
+ /** Get formula complexity */
144
+ getComplexity() {
145
+ return this._validation().complexity;
146
+ }
147
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaValidatorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
148
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaValidatorService, providedIn: 'root' });
149
+ }
150
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaValidatorService, decorators: [{
151
+ type: Injectable,
152
+ args: [{ providedIn: 'root' }]
153
+ }], ctorParameters: () => [] });
154
+
155
+ class FormulaContextService {
156
+ http = inject(HttpClient);
157
+ apiUrl = '/formulas';
158
+ contextCache = new Map();
159
+ propertiesCache = new Map();
160
+ nextTokensCache = new Map();
161
+ loadContext(schemaId, contextEntityTypeKey) {
162
+ if (!schemaId)
163
+ return of(null);
164
+ const cacheKey = `${schemaId}:${contextEntityTypeKey ?? ''}`;
165
+ const cached = this.contextCache.get(cacheKey);
166
+ if (cached) {
167
+ return of(cached);
168
+ }
169
+ const query = contextEntityTypeKey
170
+ ? `?contextEntityTypeKey=${encodeURIComponent(contextEntityTypeKey)}`
171
+ : '';
172
+ return this.http
173
+ .get(`${this.apiUrl}/builder/context/${schemaId}${query}`)
174
+ .pipe(tap((response) => {
175
+ if (response) {
176
+ this.contextCache.set(cacheKey, response);
177
+ }
178
+ }), catchError((error) => {
179
+ console.warn('Failed to load formula builder context:', error);
180
+ return of(null);
181
+ }));
182
+ }
183
+ getScopeProperties(schemaId, component, tablePath) {
184
+ if (!schemaId || !component || !tablePath)
185
+ return of([]);
186
+ const cacheKey = `${schemaId}:${component}:${tablePath}`;
187
+ const cached = this.propertiesCache.get(cacheKey);
188
+ if (cached) {
189
+ return of(cached);
190
+ }
191
+ const params = new URLSearchParams({
192
+ schemaId: schemaId.toString(),
193
+ component,
194
+ tablePath,
195
+ });
196
+ return this.http
197
+ .get(`${this.apiUrl}/builder/properties?${params.toString()}`)
198
+ .pipe(tap((response) => {
199
+ this.propertiesCache.set(cacheKey, response ?? []);
200
+ }), catchError((error) => {
201
+ console.warn('Failed to load formula properties:', error);
202
+ return of([]);
203
+ }));
204
+ }
205
+ getNextTokens(schemaId, component, tablePath) {
206
+ if (!schemaId || !component || !tablePath)
207
+ return of(null);
208
+ const cacheKey = `${schemaId}:${component}:${tablePath}`;
209
+ const cached = this.nextTokensCache.get(cacheKey);
210
+ if (cached) {
211
+ return of(cached);
212
+ }
213
+ const params = new URLSearchParams({
214
+ schemaId: schemaId.toString(),
215
+ component,
216
+ tablePath,
217
+ });
218
+ return this.http
219
+ .get(`${this.apiUrl}/builder/next-tokens?${params.toString()}`)
220
+ .pipe(tap((response) => {
221
+ if (response) {
222
+ this.nextTokensCache.set(cacheKey, response);
223
+ }
224
+ }), catchError((error) => {
225
+ console.warn('Failed to load formula next tokens:', error);
226
+ return of(null);
227
+ }));
228
+ }
229
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
230
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaContextService, providedIn: 'root' });
231
+ }
232
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaContextService, decorators: [{
233
+ type: Injectable,
234
+ args: [{ providedIn: 'root' }]
235
+ }] });
236
+
237
+ function resolveSegmentsMeta({ scope, segments, operators, baseRule, rulesByPath, }) {
238
+ if (!scope || segments.length === 0) {
239
+ return [];
240
+ }
241
+ let pathBefore = scope;
242
+ let blocked = false;
243
+ return segments.map((segment, index) => {
244
+ const isBase = index === 0 && segment.operatorToken === scope;
245
+ const operator = operators.find((op) => op.token === segment.operatorToken);
246
+ const rule = blocked
247
+ ? undefined
248
+ : isBase
249
+ ? baseRule
250
+ : rulesByPath[pathBefore]?.[segment.operatorToken];
251
+ const parameterKind = rule?.parameterKind ?? operator?.parameterKind ?? null;
252
+ const requiresParameter = operator?.requiresParameter ?? Boolean(parameterKind);
253
+ const allowedValues = getAllowedValues(rule);
254
+ const normalizedValue = segment.value;
255
+ const isComplete = !requiresParameter || Boolean(normalizedValue);
256
+ if (!blocked && isComplete) {
257
+ pathBefore = appendToPath(pathBefore, segment.operatorToken, normalizedValue, isBase);
258
+ }
259
+ else {
260
+ blocked = true;
261
+ }
262
+ return {
263
+ operatorToken: segment.operatorToken,
264
+ value: normalizedValue,
265
+ parameterKind,
266
+ requiresParameter,
267
+ allowedValues,
268
+ optional: !requiresParameter,
269
+ canRemove: !isBase,
270
+ };
271
+ });
272
+ }
273
+ function buildInitialSegments(scope, operator, rule) {
274
+ if (!operator || !scope)
275
+ return [];
276
+ const parameterKind = operator.parameterKind ?? rule?.parameterKind ?? null;
277
+ const requiresParameter = operator.requiresParameter ?? Boolean(parameterKind);
278
+ const shouldHaveSegment = Boolean(parameterKind) || requiresParameter;
279
+ if (!shouldHaveSegment)
280
+ return [];
281
+ return [{ operatorToken: scope, value: '' }];
282
+ }
283
+ function buildTablePath(scope, segments, baseOperator, stopOnIncomplete) {
284
+ if (!scope)
285
+ return '';
286
+ if (stopOnIncomplete &&
287
+ segments.length === 0 &&
288
+ (baseOperator?.requiresParameter || baseOperator?.parameterKind)) {
289
+ return '';
290
+ }
291
+ let path = scope;
292
+ for (let index = 0; index < segments.length; index += 1) {
293
+ const segment = segments[index];
294
+ const isBase = index === 0 && segment.operatorToken === scope;
295
+ if (!isBase) {
296
+ path += `.${segment.operatorToken}`;
297
+ }
298
+ if (segment.value) {
299
+ path += `.${segment.value}`;
300
+ }
301
+ else if (segment.requiresParameter && stopOnIncomplete) {
302
+ return '';
303
+ }
304
+ }
305
+ return path;
306
+ }
307
+ function getAllowedValues(rule) {
308
+ return rule?.allowedValues ?? [];
309
+ }
310
+ function appendToPath(basePath, operatorToken, value, isBase) {
311
+ if (isBase) {
312
+ if (!value)
313
+ return basePath;
314
+ if (!basePath)
315
+ return value;
316
+ return `${basePath}.${value}`;
317
+ }
318
+ if (!basePath) {
319
+ return value ? `${operatorToken}.${value}` : operatorToken;
320
+ }
321
+ const withOperator = `${basePath}.${operatorToken}`;
322
+ return value ? `${withOperator}.${value}` : withOperator;
323
+ }
324
+
325
+ const DEFAULT_SCOPE_ICON = 'general.placeholder';
326
+ const SCOPE_ICON_MAP = {
327
+ Current: 'map.marker-pin-01',
328
+ Level: 'map.marker-pin-01',
329
+ Parent: 'arrow.arrow-up',
330
+ Children: 'user.users-01',
331
+ Descendants: 'custom.hierarchy-structure',
332
+ Modules: 'file.folder-closed',
333
+ };
334
+ class FormulaBuilder {
335
+ validatorService = inject(FormulaValidatorService);
336
+ contextService = inject(FormulaContextService);
337
+ /** Reference to the formula editor */
338
+ editor = viewChild('formulaEditor', ...(ngDevMode ? [{ debugName: "editor" }] : []));
339
+ // ===== INPUTS =====
340
+ /** Properties per table path (e.g., "Current", "Children.Project") */
341
+ propertiesByPath = input({}, ...(ngDevMode ? [{ debugName: "propertiesByPath" }] : []));
342
+ /** Level schema ID for validation context */
343
+ levelSchemaId = input(...(ngDevMode ? [undefined, { debugName: "levelSchemaId" }] : []));
344
+ /** Template ID for validation context */
345
+ templateId = input(...(ngDevMode ? [undefined, { debugName: "templateId" }] : []));
346
+ /** Placeholder text */
347
+ placeholder = input('Enter formula...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
348
+ /** Hide the top toolbar */
349
+ hideToolbar = input(false, ...(ngDevMode ? [{ debugName: "hideToolbar" }] : []));
350
+ /** Hide the status bar */
351
+ hideStatusBar = input(false, ...(ngDevMode ? [{ debugName: "hideStatusBar" }] : []));
352
+ // ===== OUTPUTS =====
353
+ /** Emits validation result on change */
354
+ validationChange = output();
355
+ /** Emits when tokens change */
356
+ tokensChange = output();
357
+ // ===== STATE =====
358
+ /** Editor focus state */
359
+ hasFocus = signal(false, ...(ngDevMode ? [{ debugName: "hasFocus" }] : []));
360
+ /** Form control value */
361
+ expressionValue = signal('', ...(ngDevMode ? [{ debugName: "expressionValue" }] : []));
362
+ builderValue = signal([], ...(ngDevMode ? [{ debugName: "builderValue" }] : []));
363
+ /** Tokens for the editor */
364
+ tokens = signal([], ...(ngDevMode ? [{ debugName: "tokens" }] : []));
365
+ /** Track last synced builder JSON to avoid loops */
366
+ lastBuilderJson = '';
367
+ /** ControlValueAccessor callbacks */
368
+ onChange = () => { };
369
+ onTouched = () => { };
370
+ /** Disabled state */
371
+ isDisabled = signal(false, ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
372
+ /** Validation result from service */
373
+ validation = this.validatorService.validation;
374
+ /** Is validation in progress */
375
+ isValidating = this.validatorService.isValidating;
376
+ /** Builder context from API */
377
+ builderContext = signal(null, ...(ngDevMode ? [{ debugName: "builderContext" }] : []));
378
+ /** Function categories from context */
379
+ functionCategories = computed(() => this.builderContext()?.functions?.categories ?? [], ...(ngDevMode ? [{ debugName: "functionCategories" }] : []));
380
+ /** Is builder context loading */
381
+ isContextLoading = signal(false, ...(ngDevMode ? [{ debugName: "isContextLoading" }] : []));
382
+ contextRequestId = 0;
383
+ /** Properties loaded from API */
384
+ propertiesByPathApi = signal({}, ...(ngDevMode ? [{ debugName: "propertiesByPathApi" }] : []));
385
+ /** Current property list loading path */
386
+ propertyLoadingPath = signal(null, ...(ngDevMode ? [{ debugName: "propertyLoadingPath" }] : []));
387
+ constructor() {
388
+ // Validate when formula changes
389
+ effect(() => {
390
+ const formula = this.expressionValue();
391
+ if (formula && formula.trim().length > 0) {
392
+ this.validatorService.validate(formula, undefined, this.levelSchemaId(), this.templateId());
393
+ }
394
+ });
395
+ // Emit validation changes
396
+ effect(() => {
397
+ const validation = this.validation();
398
+ this.validationChange.emit(validation);
399
+ });
400
+ // Load builder context when schema changes
401
+ effect(() => {
402
+ const schemaId = this.levelSchemaId();
403
+ if (!schemaId) {
404
+ this.builderContext.set(null);
405
+ this.propertiesByPathApi.set({});
406
+ this.nextTokenRulesByPath.set({});
407
+ this.isContextLoading.set(false);
408
+ return;
409
+ }
410
+ const requestId = ++this.contextRequestId;
411
+ this.isContextLoading.set(true);
412
+ this.nextTokenRulesByPath.set({});
413
+ this.contextService.loadContext(schemaId).subscribe((context) => {
414
+ if (requestId !== this.contextRequestId) {
415
+ return;
416
+ }
417
+ this.builderContext.set(context);
418
+ this.isContextLoading.set(false);
419
+ if (!context) {
420
+ return;
421
+ }
422
+ if (context.currentProperties?.length) {
423
+ const scalarOperators = context.schemaDescriptor?.operators?.filter((operator) => operator.cardinality === 'scalar' &&
424
+ !operator.requiresParameter &&
425
+ !operator.parameterKind) ?? [];
426
+ const scopedProperties = scalarOperators.reduce((acc, operator) => ({
427
+ ...acc,
428
+ [operator.token]: context.currentProperties,
429
+ }), {});
430
+ this.propertiesByPathApi.update((current) => ({
431
+ ...current,
432
+ ...scopedProperties,
433
+ }));
434
+ }
435
+ });
436
+ });
437
+ // Ensure selected scope exists in available options
438
+ effect(() => {
439
+ const scopes = this.availableScopes();
440
+ if (scopes.length === 0)
441
+ return;
442
+ const current = this.propertyScope();
443
+ if (!scopes.some((scope) => scope.key === current)) {
444
+ this.setPropertyScope(scopes[0].key);
445
+ }
446
+ });
447
+ // Fetch properties for the selected table path
448
+ effect(() => {
449
+ const schemaId = this.levelSchemaId();
450
+ const tablePath = this.resolvedTablePath();
451
+ const component = this.propertiesComponent();
452
+ const context = this.builderContext();
453
+ if (!schemaId || !tablePath || !component || !context) {
454
+ this.propertyLoadingPath.set(null);
455
+ return;
456
+ }
457
+ const existing = this.propertiesByPathApi()[tablePath];
458
+ if (existing !== undefined) {
459
+ this.propertyLoadingPath.set(null);
460
+ return;
461
+ }
462
+ this.propertyLoadingPath.set(tablePath);
463
+ this.contextService
464
+ .getScopeProperties(schemaId, component, tablePath)
465
+ .subscribe((props) => {
466
+ this.propertiesByPathApi.update((current) => ({
467
+ ...current,
468
+ [tablePath]: props ?? [],
469
+ }));
470
+ if (this.propertyLoadingPath() === tablePath) {
471
+ this.propertyLoadingPath.set(null);
472
+ }
473
+ });
474
+ });
475
+ // Fetch next-token rules for the current table path
476
+ effect(() => {
477
+ const schemaId = this.levelSchemaId();
478
+ const tablePath = this.resolvedTablePath();
479
+ const component = this.propertiesComponent();
480
+ if (!schemaId || !tablePath || !component)
481
+ return;
482
+ if (this.nextTokenRulesByPath()[tablePath])
483
+ return;
484
+ this.contextService
485
+ .getNextTokens(schemaId, component, tablePath)
486
+ .subscribe((response) => {
487
+ if (!response)
488
+ return;
489
+ this.nextTokenRulesByPath.update((current) => ({
490
+ ...current,
491
+ [tablePath]: response.nextTokenRules ?? {},
492
+ }));
493
+ });
494
+ });
495
+ // Keep base segment in sync with scope metadata
496
+ effect(() => {
497
+ const scope = this.propertyScope();
498
+ const operator = this.baseOperator();
499
+ const shouldHaveSegment = Boolean(operator?.parameterKind) ||
500
+ Boolean(operator?.requiresParameter);
501
+ const segments = this.propertyPathSegments();
502
+ const hasBase = segments[0]?.operatorToken === scope;
503
+ if (shouldHaveSegment && !hasBase) {
504
+ this.propertyPathSegments.set(buildInitialSegments(scope, operator, this.baseRule()));
505
+ return;
506
+ }
507
+ if (!shouldHaveSegment && hasBase) {
508
+ this.propertyPathSegments.set(segments.slice(1));
509
+ }
510
+ });
511
+ // Normalize segment values when allowed values change
512
+ effect(() => {
513
+ const meta = this.pathSegments();
514
+ const segments = this.propertyPathSegments();
515
+ if (meta.length !== segments.length)
516
+ return;
517
+ let changed = false;
518
+ const next = segments.map((segment, index) => {
519
+ const normalized = meta[index]?.value ?? segment.value;
520
+ if (segment.value !== normalized) {
521
+ changed = true;
522
+ return { ...segment, value: normalized };
523
+ }
524
+ return segment;
525
+ });
526
+ if (changed) {
527
+ this.propertyPathSegments.set(next);
528
+ }
529
+ });
530
+ // Ensure selected field exists in current options
531
+ effect(() => {
532
+ const options = this.propertyOptions();
533
+ const current = this.propertyFieldKey();
534
+ if (options.length === 0) {
535
+ if (current)
536
+ this.propertyFieldKey.set('');
537
+ return;
538
+ }
539
+ if (current && !options.some((prop) => prop.key === current)) {
540
+ this.propertyFieldKey.set('');
541
+ }
542
+ });
543
+ }
544
+ ngOnDestroy() {
545
+ this.validatorService.reset();
546
+ }
547
+ // ===== ControlValueAccessor =====
548
+ writeValue(value) {
549
+ if (!value) {
550
+ this.expressionValue.set('');
551
+ this.builderValue.set([]);
552
+ this.tokens.set([]);
553
+ this.lastBuilderJson = '';
554
+ const editor = this.editor();
555
+ if (editor) {
556
+ editor.writeValue([]);
557
+ }
558
+ this.validatorService.reset();
559
+ return;
560
+ }
561
+ this.expressionValue.set(value.expression ?? '');
562
+ const builder = Array.isArray(value.builder) ? value.builder : [];
563
+ this.builderValue.set(builder);
564
+ if (builder.length === 0) {
565
+ this.tokens.set([]);
566
+ this.lastBuilderJson = '';
567
+ const editor = this.editor();
568
+ if (editor) {
569
+ editor.writeValue([]);
570
+ }
571
+ return;
572
+ }
573
+ const serialized = JSON.stringify(builder);
574
+ if (serialized === this.lastBuilderJson) {
575
+ return;
576
+ }
577
+ this.tokens.set(builder);
578
+ const editor = this.editor();
579
+ if (editor) {
580
+ editor.writeValue(builder);
581
+ }
582
+ this.lastBuilderJson = serialized;
583
+ }
584
+ registerOnChange(fn) {
585
+ this.onChange = fn;
586
+ }
587
+ registerOnTouched(fn) {
588
+ this.onTouched = fn;
589
+ }
590
+ setDisabledState(isDisabled) {
591
+ this.isDisabled.set(isDisabled);
592
+ }
593
+ /** Handle formula change from editor */
594
+ onFormulaChange(newFormula) {
595
+ this.expressionValue.set(newFormula);
596
+ this.emitValueChange();
597
+ }
598
+ /** Handle block insert from toolbar */
599
+ onBlockInsert(block) {
600
+ const editor = this.editor();
601
+ if (editor) {
602
+ editor.addBlock(block);
603
+ }
604
+ }
605
+ /** Handle tokens change */
606
+ onTokensChange(newTokens) {
607
+ this.tokens.set(newTokens);
608
+ const json = JSON.stringify(newTokens);
609
+ this.lastBuilderJson = json;
610
+ this.builderValue.set(newTokens);
611
+ this.tokensChange.emit(newTokens);
612
+ this.emitValueChange();
613
+ }
614
+ onEditorFocus() {
615
+ this.hasFocus.set(true);
616
+ }
617
+ onEditorBlur() {
618
+ this.hasFocus.set(false);
619
+ this.onTouched();
620
+ }
621
+ emitValueChange() {
622
+ this.onChange({
623
+ expression: this.expressionValue(),
624
+ builder: this.builderValue(),
625
+ });
626
+ }
627
+ // ===== PUBLIC METHODS =====
628
+ /** Clear the formula */
629
+ clear() {
630
+ this.expressionValue.set('');
631
+ this.tokens.set([]);
632
+ this.builderValue.set([]);
633
+ this.lastBuilderJson = '';
634
+ const editor = this.editor();
635
+ if (editor) {
636
+ editor.writeValue([]);
637
+ }
638
+ this.validatorService.reset();
639
+ this.emitValueChange();
640
+ }
641
+ // ===== PROPERTY COMPOSER =====
642
+ propertyScope = signal('', ...(ngDevMode ? [{ debugName: "propertyScope" }] : []));
643
+ propertyPathSegments = signal([], ...(ngDevMode ? [{ debugName: "propertyPathSegments" }] : []));
644
+ propertyFieldKey = signal('', ...(ngDevMode ? [{ debugName: "propertyFieldKey" }] : []));
645
+ nextTokenRulesByPath = signal({}, ...(ngDevMode ? [{ debugName: "nextTokenRulesByPath" }] : []));
646
+ contextOperators = computed(() => {
647
+ return this.builderContext()?.schemaDescriptor?.operators ?? [];
648
+ }, ...(ngDevMode ? [{ debugName: "contextOperators" }] : []));
649
+ baseOperator = computed(() => this.contextOperators().find((op) => op.token === this.propertyScope()), ...(ngDevMode ? [{ debugName: "baseOperator" }] : []));
650
+ baseRule = computed(() => {
651
+ const scope = this.propertyScope();
652
+ return this.builderContext()?.schemaDescriptor?.nextTokenRules?.[scope];
653
+ }, ...(ngDevMode ? [{ debugName: "baseRule" }] : []));
654
+ availableScopes = computed(() => {
655
+ const operators = this.contextOperators();
656
+ if (operators.length === 0)
657
+ return [];
658
+ return operators.map((operator) => ({
659
+ key: operator.token,
660
+ label: `@${operator.token}`,
661
+ requiresParam: operator.requiresParameter ?? false,
662
+ optional: !operator.requiresParameter,
663
+ }));
664
+ }, ...(ngDevMode ? [{ debugName: "availableScopes" }] : []));
665
+ /** Radio card options for scope selection */
666
+ scopeOptions = computed(() => this.availableScopes().map((scope) => ({
667
+ id: scope.key,
668
+ name: scope.key,
669
+ icon: SCOPE_ICON_MAP[scope.key] ?? DEFAULT_SCOPE_ICON,
670
+ })), ...(ngDevMode ? [{ debugName: "scopeOptions" }] : []));
671
+ /** Handle scope selection from radio cards */
672
+ onScopeChange(item) {
673
+ this.setPropertyScope(String(item.id));
674
+ }
675
+ pathSegments = computed(() => {
676
+ const scope = this.propertyScope();
677
+ return resolveSegmentsMeta({
678
+ scope,
679
+ segments: this.propertyPathSegments(),
680
+ operators: this.contextOperators(),
681
+ baseRule: this.baseRule(),
682
+ rulesByPath: this.nextTokenRulesByPath(),
683
+ });
684
+ }, ...(ngDevMode ? [{ debugName: "pathSegments" }] : []));
685
+ isDirectAccess = computed(() => {
686
+ const operator = this.baseOperator();
687
+ const needsParameter = Boolean(operator?.parameterKind) || Boolean(operator?.requiresParameter);
688
+ return !needsParameter && this.pathSegments().length === 0;
689
+ }, ...(ngDevMode ? [{ debugName: "isDirectAccess" }] : []));
690
+ propertyTablePath = computed(() => buildTablePath(this.propertyScope(), this.pathSegments(), this.baseOperator(), false), ...(ngDevMode ? [{ debugName: "propertyTablePath" }] : []));
691
+ resolvedTablePath = computed(() => buildTablePath(this.propertyScope(), this.pathSegments(), this.baseOperator(), true), ...(ngDevMode ? [{ debugName: "resolvedTablePath" }] : []));
692
+ currentNextTokenRules = computed(() => {
693
+ const path = this.resolvedTablePath();
694
+ if (!path)
695
+ return {};
696
+ return this.nextTokenRulesByPath()[path] ?? {};
697
+ }, ...(ngDevMode ? [{ debugName: "currentNextTokenRules" }] : []));
698
+ nextOperatorOptions = computed(() => {
699
+ const rules = this.currentNextTokenRules();
700
+ const operators = this.contextOperators();
701
+ return Object.keys(rules)
702
+ .map((token) => {
703
+ const rule = rules[token];
704
+ const operator = operators.find((item) => item.token === token);
705
+ const parameterKind = rule?.parameterKind ?? operator?.parameterKind ?? null;
706
+ const requiresParameter = operator?.requiresParameter ?? Boolean(parameterKind);
707
+ const allowedValues = getAllowedValues(rule);
708
+ return {
709
+ token,
710
+ rule,
711
+ requiresParameter,
712
+ allowedValues,
713
+ };
714
+ })
715
+ .filter((option) => option.allowedValues.length > 0 || !option.requiresParameter);
716
+ }, ...(ngDevMode ? [{ debugName: "nextOperatorOptions" }] : []));
717
+ canAddNextSegment = computed(() => {
718
+ if (this.nextOperatorOptions().length === 0)
719
+ return false;
720
+ const segments = this.pathSegments();
721
+ if (segments.length === 0 || !segments[0].value) {
722
+ return false;
723
+ }
724
+ return segments.every((segment) => !segment.requiresParameter || Boolean(segment.value));
725
+ }, ...(ngDevMode ? [{ debugName: "canAddNextSegment" }] : []));
726
+ propertiesComponent = computed(() => {
727
+ const contextKey = this.builderContext()?.contextEntityTypeKey;
728
+ return contextKey;
729
+ }, ...(ngDevMode ? [{ debugName: "propertiesComponent" }] : []));
730
+ mergedPropertiesByPath = computed(() => {
731
+ const merged = {
732
+ ...this.propertiesByPath(),
733
+ };
734
+ const apiProps = this.propertiesByPathApi();
735
+ Object.entries(apiProps).forEach(([path, props]) => {
736
+ if (props && props.length > 0) {
737
+ merged[path] = props;
738
+ }
739
+ });
740
+ return merged;
741
+ }, ...(ngDevMode ? [{ debugName: "mergedPropertiesByPath" }] : []));
742
+ isPropertyLoading = computed(() => {
743
+ const path = this.resolvedTablePath();
744
+ return Boolean(path) && this.propertyLoadingPath() === path;
745
+ }, ...(ngDevMode ? [{ debugName: "isPropertyLoading" }] : []));
746
+ propertyOptions = computed(() => {
747
+ const path = this.resolvedTablePath();
748
+ if (!path)
749
+ return [];
750
+ const byPath = this.mergedPropertiesByPath()[path];
751
+ const list = byPath && byPath.length > 0 ? byPath : [];
752
+ return list.map((prop) => ({
753
+ key: prop.key,
754
+ name: prop.name ?? prop.key,
755
+ }));
756
+ }, ...(ngDevMode ? [{ debugName: "propertyOptions" }] : []));
757
+ selectedProperty = computed(() => {
758
+ const key = this.propertyFieldKey();
759
+ return this.propertyOptions().find((prop) => prop.key === key) ?? null;
760
+ }, ...(ngDevMode ? [{ debugName: "selectedProperty" }] : []));
761
+ canInsertProperty = computed(() => {
762
+ if (!this.selectedProperty())
763
+ return false;
764
+ return Boolean(this.resolvedTablePath());
765
+ }, ...(ngDevMode ? [{ debugName: "canInsertProperty" }] : []));
766
+ setPropertyScope(scope) {
767
+ if (this.propertyScope() === scope) {
768
+ return;
769
+ }
770
+ this.propertyScope.set(scope);
771
+ this.propertyPathSegments.set(buildInitialSegments(scope, this.contextOperators().find((op) => op.token === scope), this.builderContext()?.schemaDescriptor?.nextTokenRules?.[scope]));
772
+ this.propertyFieldKey.set('');
773
+ }
774
+ setPathSegmentValue(index, value) {
775
+ this.propertyPathSegments.update((segments) => {
776
+ // Update the segment at the given index and remove all segments after it
777
+ // because subsequent segments depend on previous values
778
+ const updated = segments.slice(0, index + 1);
779
+ updated[index] = { ...updated[index], value: value ?? '' };
780
+ return updated;
781
+ });
782
+ }
783
+ addNextSegment(operatorToken) {
784
+ const value = '';
785
+ this.propertyPathSegments.update((segments) => [
786
+ ...segments,
787
+ { operatorToken, value },
788
+ ]);
789
+ }
790
+ onAddSegmentSelection(operatorToken, popover) {
791
+ popover.hide();
792
+ this.addNextSegment(operatorToken);
793
+ }
794
+ removePathSegment(index) {
795
+ this.propertyPathSegments.update((segments) => segments.filter((_, i) => i !== index));
796
+ }
797
+ segmentOptions(index) {
798
+ const segment = this.pathSegments()[index];
799
+ if (!segment)
800
+ return [];
801
+ return segment.allowedValues.map((value) => ({
802
+ key: value,
803
+ name: value,
804
+ }));
805
+ }
806
+ insertSelectedProperty(insertBlock) {
807
+ const prop = this.selectedProperty();
808
+ if (!prop)
809
+ return;
810
+ if (!this.canInsertProperty())
811
+ return;
812
+ const tablePath = this.resolvedTablePath();
813
+ if (!tablePath)
814
+ return;
815
+ const display = prop.name ?? prop.key;
816
+ const block = createPropertyBlock(`@${tablePath}::${prop.key}`, 'current', display);
817
+ insertBlock(block);
818
+ }
819
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
820
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: FormulaBuilder, isStandalone: true, selector: "mt-formula-builder", inputs: { propertiesByPath: { classPropertyName: "propertiesByPath", publicName: "propertiesByPath", isSignal: true, isRequired: false, transformFunction: null }, levelSchemaId: { classPropertyName: "levelSchemaId", publicName: "levelSchemaId", isSignal: true, isRequired: false, transformFunction: null }, templateId: { classPropertyName: "templateId", publicName: "templateId", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hideToolbar: { classPropertyName: "hideToolbar", publicName: "hideToolbar", isSignal: true, isRequired: false, transformFunction: null }, hideStatusBar: { classPropertyName: "hideStatusBar", publicName: "hideStatusBar", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { validationChange: "validationChange", tokensChange: "tokensChange" }, host: { classAttribute: "block" }, providers: [
821
+ {
822
+ provide: NG_VALUE_ACCESSOR,
823
+ useExisting: forwardRef(() => FormulaBuilder),
824
+ multi: true,
825
+ },
826
+ ], viewQueries: [{ propertyName: "editor", first: true, predicate: ["formulaEditor"], descendants: true, isSignal: true }], ngImport: i0, template: "<mt-card\r\n headless\r\n [class.ring-2]=\"hasFocus()\"\r\n [class.ring-primary]=\"hasFocus()\"\r\n [paddingless]=\"true\"\r\n>\r\n <div\r\n *transloco=\"let t; prefix: 'formulaBuilder'\"\r\n class=\"flex flex-col overflow-hidden\"\r\n >\r\n <!-- Toolbar - Pass data via inputs (pure component) -->\r\n @if (!hideToolbar()) {\r\n <mt-formula-toolbar\r\n [functionCategories]=\"functionCategories()\"\r\n (onBlockInsert)=\"onBlockInsert($event)\"\r\n >\r\n <ng-template #properties let-insertBlock=\"insertBlock\">\r\n @if (isContextLoading()) {\r\n <div class=\"flex flex-col gap-4 p-4\">\r\n <div class=\"flex items-center justify-between gap-4\">\r\n <p-skeleton\r\n width=\"18rem\"\r\n height=\"2.5rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n <p-skeleton\r\n width=\"7rem\"\r\n height=\"2.5rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n </div>\r\n <div class=\"flex items-center gap-4\">\r\n <div\r\n class=\"flex min-h-11 min-w-0 flex-1 flex-col gap-2 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/50\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <p-skeleton\r\n width=\"3rem\"\r\n height=\"1rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n <p-skeleton\r\n class=\"flex-1\"\r\n height=\"2rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n </div>\r\n <div\r\n class=\"flex items-center gap-2 border-t border-slate-200 pt-2 dark:border-slate-700\"\r\n >\r\n <p-skeleton\r\n width=\"3rem\"\r\n height=\"1rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n <p-skeleton\r\n class=\"flex-1\"\r\n height=\"2.5rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n </div>\r\n <p-skeleton height=\"2rem\" styleClass=\"rounded-md\" />\r\n </div>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"flex flex-col gap-4 p-4\">\r\n <!-- Scope Selection - Full width, centered -->\r\n <div class=\"flex items-center justify-between gap-4\">\r\n <mt-radio-cards\r\n [options]=\"scopeOptions()\"\r\n [activeId]=\"propertyScope()\"\r\n (selectionChange)=\"onScopeChange($event)\"\r\n size=\"small\"\r\n />\r\n </div>\r\n\r\n <!-- Path + Field + Preview Row -->\r\n <div class=\"flex items-center gap-4\">\r\n <!-- Path Container - Flexible -->\r\n <div\r\n class=\"flex min-h-11 min-w-0 flex-1 flex-col gap-2 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/50\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <span\r\n class=\"shrink-0 text-xs font-semibold uppercase tracking-wider text-slate-400\"\r\n >Path</span\r\n >\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n <div class=\"flex min-w-0 flex-1 items-center gap-2\">\r\n @if (isDirectAccess()) {\r\n <span\r\n class=\"rounded-md bg-emerald-100 px-2.5 py-1 text-xs font-medium text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400\"\r\n >\r\n \u2713 Direct Access\r\n </span>\r\n } @else if (pathSegments().length === 0) {\r\n <span class=\"text-xs italic text-slate-400\"\r\n >No path configured</span\r\n >\r\n } @else {\r\n <div\r\n class=\"flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto\"\r\n >\r\n @for (\r\n segment of pathSegments();\r\n track $index;\r\n let segmentIndex = $index\r\n ) {\r\n @if (segmentIndex > 0) {\r\n <span\r\n class=\"shrink-0 text-slate-300 dark:text-slate-600\"\r\n >\u203A</span\r\n >\r\n }\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"\r\n segment.value ||\r\n 'Select ' + segment.operatorToken\r\n \"\r\n icon=\"arrow.chevron-down\"\r\n [outlined]=\"true\"\r\n size=\"small\"\r\n [severity]=\"\r\n segment.value ? 'primary' : 'secondary'\r\n \"\r\n [iconPos]=\"'end'\"\r\n (onClick)=\"pathPopover.toggle($event)\"\r\n ></mt-button>\r\n\r\n <p-popover\r\n #pathPopover\r\n [style]=\"{ width: 'max-content' }\"\r\n appendTo=\"body\"\r\n >\r\n <div class=\"p-2\">\r\n <div\r\n class=\"mb-2 text-xs font-semibold uppercase text-slate-400\"\r\n >\r\n Select {{ segment.operatorToken }}\r\n </div>\r\n <div class=\"flex flex-col gap-1\">\r\n @for (\r\n option of segmentOptions(segmentIndex);\r\n track option.key\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"option.name\"\r\n [icon]=\"\r\n segment.value === option.key\r\n ? 'general.check'\r\n : 'general.minus'\r\n \"\r\n [iconPos]=\"'end'\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n [styleClass]=\"\r\n 'w-full justify-start rounded-md px-2.5 py-1.5 text-left text-xs font-medium transition-colors ' +\r\n (segment.value === option.key\r\n ? 'bg-primary text-white'\r\n : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700')\r\n \"\r\n (onClick)=\"\r\n setPathSegmentValue(\r\n segmentIndex,\r\n option.key\r\n );\r\n pathPopover.hide()\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n @if (segment.optional) {\r\n <mt-button\r\n type=\"button\"\r\n label=\"Clear\"\r\n [outlined]=\"true\"\r\n icon=\"general.x-close\"\r\n [iconPos]=\"'end'\"\r\n size=\"small\"\r\n severity=\"danger\"\r\n styleClass=\"mt-2 w-full rounded-md border border-slate-200 py-1 text-xs text-slate-400 hover:bg-slate-50 dark:border-slate-700\"\r\n (onClick)=\"\r\n setPathSegmentValue(segmentIndex, null);\r\n pathPopover.hide()\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n </p-popover>\r\n\r\n @if (\r\n segment.canRemove &&\r\n segmentIndex === pathSegments().length - 1\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n icon=\"general.x-close\"\r\n size=\"small\"\r\n severity=\"danger\"\r\n styleClass=\"flex size-5 shrink-0 items-center justify-center rounded-full bg-red-500 text-white hover:bg-red-600\"\r\n (onClick)=\"removePathSegment(segmentIndex)\"\r\n ></mt-button>\r\n }\r\n }\r\n <mt-button\r\n type=\"button\"\r\n icon=\"general.plus\"\r\n size=\"small\"\r\n severity=\"primary\"\r\n [disabled]=\"!canAddNextSegment()\"\r\n [class.hidden]=\"!canAddNextSegment()\"\r\n styleClass=\"flex size-6 shrink-0 items-center justify-center rounded-md bg-primary text-xs font-bold text-white hover:opacity-90 disabled:opacity-50\"\r\n (onClick)=\"nextOperatorPopover.toggle($event)\"\r\n ></mt-button>\r\n </div>\r\n }\r\n <p-popover\r\n #nextOperatorPopover\r\n [style]=\"{ width: 'max-content' }\"\r\n appendTo=\"body\"\r\n >\r\n <div class=\"p-2\">\r\n <div\r\n class=\"mb-2 text-xs font-semibold uppercase text-slate-400\"\r\n >\r\n Add Segment\r\n </div>\r\n <div class=\"flex flex-col gap-1\">\r\n @for (\r\n option of nextOperatorOptions();\r\n track option.token\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"option.token\"\r\n icon=\"general.plus\"\r\n [iconPos]=\"'end'\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n styleClass=\"w-full justify-start rounded-md px-2.5 py-1.5 text-left text-xs font-medium transition-colors text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700\"\r\n (onClick)=\"\r\n onAddSegmentSelection(\r\n option.token,\r\n nextOperatorPopover\r\n )\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n </div>\r\n </p-popover>\r\n </div>\r\n </div>\r\n\r\n <!-- Field Selection - Under Path -->\r\n <div\r\n class=\"flex items-center gap-2 border-t border-slate-200 pt-2 dark:border-slate-700\"\r\n >\r\n <span\r\n class=\"shrink-0 text-xs font-semibold uppercase tracking-wider text-slate-400\"\r\n >Field</span\r\n >\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n @if (isPropertyLoading()) {\r\n <p-skeleton\r\n class=\"flex-1\"\r\n height=\"2.5rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n } @else {\r\n <mt-select-field\r\n class=\"flex-1\"\r\n [label]=\"''\"\r\n [filter]=\"true\"\r\n [hasPlaceholderPrefix]=\"false\"\r\n placeholder=\"Select...\"\r\n [(ngModel)]=\"propertyFieldKey\"\r\n [options]=\"propertyOptions()\"\r\n optionLabel=\"name\"\r\n optionValue=\"key\"\r\n [size]=\"'small'\"\r\n />\r\n }\r\n </div>\r\n <div\r\n class=\"w-full truncate rounded-lg bg-amber-100 px-3 py-2 text-sm font-semibold text-amber-700 shadow-sm hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-300 dark:hover:bg-amber-900/50\"\r\n [mtTooltip]=\"\r\n '@' +\r\n propertyTablePath() +\r\n '::' +\r\n (selectedProperty()?.key ?? '...')\r\n \"\r\n tooltipPosition=\"top\"\r\n >\r\n @{{ propertyTablePath() }}::{{\r\n selectedProperty()?.key ?? \"...\"\r\n }}\r\n </div>\r\n <div class=\"flex justify-end\">\r\n <mt-button\r\n type=\"button\"\r\n label=\"Insert\"\r\n icon=\"general.plus\"\r\n severity=\"primary\"\r\n [disabled]=\"!canInsertProperty()\"\r\n (onClick)=\"insertSelectedProperty(insertBlock)\"\r\n ></mt-button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </ng-template>\r\n </mt-formula-toolbar>\r\n }\r\n\r\n <!-- Editor Area -->\r\n <div class=\"p-3\">\r\n <mt-formula-editor\r\n #formulaEditor\r\n [placeholder]=\"placeholder()\"\r\n [initialTokens]=\"tokens()\"\r\n (formulaChange)=\"onFormulaChange($event)\"\r\n (tokensChange)=\"onTokensChange($event)\"\r\n (onFocus)=\"onEditorFocus()\"\r\n (onBlur)=\"onEditorBlur()\"\r\n />\r\n </div>\r\n\r\n <!-- Status Bar - Pass data via inputs (pure component) -->\r\n @if (!hideStatusBar()) {\r\n <mt-formula-status-bar [validation]=\"validation()\" />\r\n }\r\n </div>\r\n</mt-card>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading"], outputs: ["onChange"] }, { kind: "directive", type: Tooltip, selector: "[mtTooltip]" }, { kind: "component", type: RadioCards, selector: "mt-radio-cards", inputs: ["circle", "color", "size", "columns", "options", "activeId", "itemTemplate"], outputs: ["optionsChange", "activeIdChange", "selectionChange"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i2.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "component", type: Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "component", type: FormulaToolbar, selector: "mt-formula-toolbar", inputs: ["knownProperties", "propertiesTemplate", "functionCategories", "operators", "initialTab", "searchPlaceholder", "labels"], outputs: ["onBlockInsert", "onTabChange"] }, { kind: "component", type: FormulaStatusBar, selector: "mt-formula-status-bar", inputs: ["validation", "labels"] }, { kind: "component", type: FormulaEditor, selector: "mt-formula-editor", inputs: ["placeholder", "initialTokens", "disabled"], outputs: ["formulaChange", "tokensChange", "onBlur", "onFocus"] }] });
827
+ }
828
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormulaBuilder, decorators: [{
829
+ type: Component,
830
+ args: [{ selector: 'mt-formula-builder', standalone: true, imports: [
831
+ CommonModule,
832
+ TranslocoDirective,
833
+ FormsModule,
834
+ Card,
835
+ Button,
836
+ SelectField,
837
+ Tooltip,
838
+ RadioCards,
839
+ PopoverModule,
840
+ Skeleton,
841
+ FormulaToolbar,
842
+ FormulaStatusBar,
843
+ FormulaEditor,
844
+ ], host: {
845
+ class: 'block',
846
+ }, providers: [
847
+ {
848
+ provide: NG_VALUE_ACCESSOR,
849
+ useExisting: forwardRef(() => FormulaBuilder),
850
+ multi: true,
851
+ },
852
+ ], template: "<mt-card\r\n headless\r\n [class.ring-2]=\"hasFocus()\"\r\n [class.ring-primary]=\"hasFocus()\"\r\n [paddingless]=\"true\"\r\n>\r\n <div\r\n *transloco=\"let t; prefix: 'formulaBuilder'\"\r\n class=\"flex flex-col overflow-hidden\"\r\n >\r\n <!-- Toolbar - Pass data via inputs (pure component) -->\r\n @if (!hideToolbar()) {\r\n <mt-formula-toolbar\r\n [functionCategories]=\"functionCategories()\"\r\n (onBlockInsert)=\"onBlockInsert($event)\"\r\n >\r\n <ng-template #properties let-insertBlock=\"insertBlock\">\r\n @if (isContextLoading()) {\r\n <div class=\"flex flex-col gap-4 p-4\">\r\n <div class=\"flex items-center justify-between gap-4\">\r\n <p-skeleton\r\n width=\"18rem\"\r\n height=\"2.5rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n <p-skeleton\r\n width=\"7rem\"\r\n height=\"2.5rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n </div>\r\n <div class=\"flex items-center gap-4\">\r\n <div\r\n class=\"flex min-h-11 min-w-0 flex-1 flex-col gap-2 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/50\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <p-skeleton\r\n width=\"3rem\"\r\n height=\"1rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n <p-skeleton\r\n class=\"flex-1\"\r\n height=\"2rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n </div>\r\n <div\r\n class=\"flex items-center gap-2 border-t border-slate-200 pt-2 dark:border-slate-700\"\r\n >\r\n <p-skeleton\r\n width=\"3rem\"\r\n height=\"1rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n <p-skeleton\r\n class=\"flex-1\"\r\n height=\"2.5rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n </div>\r\n <p-skeleton height=\"2rem\" styleClass=\"rounded-md\" />\r\n </div>\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"flex flex-col gap-4 p-4\">\r\n <!-- Scope Selection - Full width, centered -->\r\n <div class=\"flex items-center justify-between gap-4\">\r\n <mt-radio-cards\r\n [options]=\"scopeOptions()\"\r\n [activeId]=\"propertyScope()\"\r\n (selectionChange)=\"onScopeChange($event)\"\r\n size=\"small\"\r\n />\r\n </div>\r\n\r\n <!-- Path + Field + Preview Row -->\r\n <div class=\"flex items-center gap-4\">\r\n <!-- Path Container - Flexible -->\r\n <div\r\n class=\"flex min-h-11 min-w-0 flex-1 flex-col gap-2 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/50\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <span\r\n class=\"shrink-0 text-xs font-semibold uppercase tracking-wider text-slate-400\"\r\n >Path</span\r\n >\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n <div class=\"flex min-w-0 flex-1 items-center gap-2\">\r\n @if (isDirectAccess()) {\r\n <span\r\n class=\"rounded-md bg-emerald-100 px-2.5 py-1 text-xs font-medium text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400\"\r\n >\r\n \u2713 Direct Access\r\n </span>\r\n } @else if (pathSegments().length === 0) {\r\n <span class=\"text-xs italic text-slate-400\"\r\n >No path configured</span\r\n >\r\n } @else {\r\n <div\r\n class=\"flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto\"\r\n >\r\n @for (\r\n segment of pathSegments();\r\n track $index;\r\n let segmentIndex = $index\r\n ) {\r\n @if (segmentIndex > 0) {\r\n <span\r\n class=\"shrink-0 text-slate-300 dark:text-slate-600\"\r\n >\u203A</span\r\n >\r\n }\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"\r\n segment.value ||\r\n 'Select ' + segment.operatorToken\r\n \"\r\n icon=\"arrow.chevron-down\"\r\n [outlined]=\"true\"\r\n size=\"small\"\r\n [severity]=\"\r\n segment.value ? 'primary' : 'secondary'\r\n \"\r\n [iconPos]=\"'end'\"\r\n (onClick)=\"pathPopover.toggle($event)\"\r\n ></mt-button>\r\n\r\n <p-popover\r\n #pathPopover\r\n [style]=\"{ width: 'max-content' }\"\r\n appendTo=\"body\"\r\n >\r\n <div class=\"p-2\">\r\n <div\r\n class=\"mb-2 text-xs font-semibold uppercase text-slate-400\"\r\n >\r\n Select {{ segment.operatorToken }}\r\n </div>\r\n <div class=\"flex flex-col gap-1\">\r\n @for (\r\n option of segmentOptions(segmentIndex);\r\n track option.key\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"option.name\"\r\n [icon]=\"\r\n segment.value === option.key\r\n ? 'general.check'\r\n : 'general.minus'\r\n \"\r\n [iconPos]=\"'end'\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n [styleClass]=\"\r\n 'w-full justify-start rounded-md px-2.5 py-1.5 text-left text-xs font-medium transition-colors ' +\r\n (segment.value === option.key\r\n ? 'bg-primary text-white'\r\n : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700')\r\n \"\r\n (onClick)=\"\r\n setPathSegmentValue(\r\n segmentIndex,\r\n option.key\r\n );\r\n pathPopover.hide()\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n @if (segment.optional) {\r\n <mt-button\r\n type=\"button\"\r\n label=\"Clear\"\r\n [outlined]=\"true\"\r\n icon=\"general.x-close\"\r\n [iconPos]=\"'end'\"\r\n size=\"small\"\r\n severity=\"danger\"\r\n styleClass=\"mt-2 w-full rounded-md border border-slate-200 py-1 text-xs text-slate-400 hover:bg-slate-50 dark:border-slate-700\"\r\n (onClick)=\"\r\n setPathSegmentValue(segmentIndex, null);\r\n pathPopover.hide()\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n </p-popover>\r\n\r\n @if (\r\n segment.canRemove &&\r\n segmentIndex === pathSegments().length - 1\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n icon=\"general.x-close\"\r\n size=\"small\"\r\n severity=\"danger\"\r\n styleClass=\"flex size-5 shrink-0 items-center justify-center rounded-full bg-red-500 text-white hover:bg-red-600\"\r\n (onClick)=\"removePathSegment(segmentIndex)\"\r\n ></mt-button>\r\n }\r\n }\r\n <mt-button\r\n type=\"button\"\r\n icon=\"general.plus\"\r\n size=\"small\"\r\n severity=\"primary\"\r\n [disabled]=\"!canAddNextSegment()\"\r\n [class.hidden]=\"!canAddNextSegment()\"\r\n styleClass=\"flex size-6 shrink-0 items-center justify-center rounded-md bg-primary text-xs font-bold text-white hover:opacity-90 disabled:opacity-50\"\r\n (onClick)=\"nextOperatorPopover.toggle($event)\"\r\n ></mt-button>\r\n </div>\r\n }\r\n <p-popover\r\n #nextOperatorPopover\r\n [style]=\"{ width: 'max-content' }\"\r\n appendTo=\"body\"\r\n >\r\n <div class=\"p-2\">\r\n <div\r\n class=\"mb-2 text-xs font-semibold uppercase text-slate-400\"\r\n >\r\n Add Segment\r\n </div>\r\n <div class=\"flex flex-col gap-1\">\r\n @for (\r\n option of nextOperatorOptions();\r\n track option.token\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"option.token\"\r\n icon=\"general.plus\"\r\n [iconPos]=\"'end'\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n styleClass=\"w-full justify-start rounded-md px-2.5 py-1.5 text-left text-xs font-medium transition-colors text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700\"\r\n (onClick)=\"\r\n onAddSegmentSelection(\r\n option.token,\r\n nextOperatorPopover\r\n )\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n </div>\r\n </p-popover>\r\n </div>\r\n </div>\r\n\r\n <!-- Field Selection - Under Path -->\r\n <div\r\n class=\"flex items-center gap-2 border-t border-slate-200 pt-2 dark:border-slate-700\"\r\n >\r\n <span\r\n class=\"shrink-0 text-xs font-semibold uppercase tracking-wider text-slate-400\"\r\n >Field</span\r\n >\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n @if (isPropertyLoading()) {\r\n <p-skeleton\r\n class=\"flex-1\"\r\n height=\"2.5rem\"\r\n styleClass=\"rounded-md\"\r\n />\r\n } @else {\r\n <mt-select-field\r\n class=\"flex-1\"\r\n [label]=\"''\"\r\n [filter]=\"true\"\r\n [hasPlaceholderPrefix]=\"false\"\r\n placeholder=\"Select...\"\r\n [(ngModel)]=\"propertyFieldKey\"\r\n [options]=\"propertyOptions()\"\r\n optionLabel=\"name\"\r\n optionValue=\"key\"\r\n [size]=\"'small'\"\r\n />\r\n }\r\n </div>\r\n <div\r\n class=\"w-full truncate rounded-lg bg-amber-100 px-3 py-2 text-sm font-semibold text-amber-700 shadow-sm hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-300 dark:hover:bg-amber-900/50\"\r\n [mtTooltip]=\"\r\n '@' +\r\n propertyTablePath() +\r\n '::' +\r\n (selectedProperty()?.key ?? '...')\r\n \"\r\n tooltipPosition=\"top\"\r\n >\r\n @{{ propertyTablePath() }}::{{\r\n selectedProperty()?.key ?? \"...\"\r\n }}\r\n </div>\r\n <div class=\"flex justify-end\">\r\n <mt-button\r\n type=\"button\"\r\n label=\"Insert\"\r\n icon=\"general.plus\"\r\n severity=\"primary\"\r\n [disabled]=\"!canInsertProperty()\"\r\n (onClick)=\"insertSelectedProperty(insertBlock)\"\r\n ></mt-button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </ng-template>\r\n </mt-formula-toolbar>\r\n }\r\n\r\n <!-- Editor Area -->\r\n <div class=\"p-3\">\r\n <mt-formula-editor\r\n #formulaEditor\r\n [placeholder]=\"placeholder()\"\r\n [initialTokens]=\"tokens()\"\r\n (formulaChange)=\"onFormulaChange($event)\"\r\n (tokensChange)=\"onTokensChange($event)\"\r\n (onFocus)=\"onEditorFocus()\"\r\n (onBlur)=\"onEditorBlur()\"\r\n />\r\n </div>\r\n\r\n <!-- Status Bar - Pass data via inputs (pure component) -->\r\n @if (!hideStatusBar()) {\r\n <mt-formula-status-bar [validation]=\"validation()\" />\r\n }\r\n </div>\r\n</mt-card>\r\n" }]
853
+ }], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.ViewChild, args: ['formulaEditor', { isSignal: true }] }], propertiesByPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "propertiesByPath", required: false }] }], levelSchemaId: [{ type: i0.Input, args: [{ isSignal: true, alias: "levelSchemaId", required: false }] }], templateId: [{ type: i0.Input, args: [{ isSignal: true, alias: "templateId", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hideToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideToolbar", required: false }] }], hideStatusBar: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideStatusBar", required: false }] }], validationChange: [{ type: i0.Output, args: ["validationChange"] }], tokensChange: [{ type: i0.Output, args: ["tokensChange"] }] } });
854
+
855
+ /**
856
+ * Services barrel export
857
+ */
858
+
859
+ /**
860
+ * Public API Surface of @masterteam/formula-builder
861
+ *
862
+ * This package provides the FormulaBuilder component which wraps
863
+ * pure UI components from @masterteam/components/formula with
864
+ * backend services for validation and context loading.
865
+ *
866
+ * Architecture:
867
+ * - UI Components: imported from @masterteam/components/formula (pure, no services)
868
+ * - Services: FormulaContextService, FormulaValidatorService (backend logic)
869
+ * - FormulaBuilder: Main wrapper that connects UI with services
870
+ */
871
+ // Main component
872
+
873
+ /**
874
+ * Generated bundle index. Do not edit.
875
+ */
876
+
877
+ export { FormulaBuilder, FormulaContextService, FormulaValidatorService };
878
+ //# sourceMappingURL=masterteam-formula-builder.mjs.map