@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.
- package/README.md +67 -0
- package/assets/formula-builder.css +2 -0
- package/assets/i18n/ar.json +36 -0
- package/assets/i18n/en.json +36 -0
- package/fesm2022/masterteam-formula-builder.mjs +878 -0
- package/fesm2022/masterteam-formula-builder.mjs.map +1 -0
- package/package.json +40 -0
- package/types/masterteam-formula-builder.d.ts +271 -0
|
@@ -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
|