@praxisui/visual-builder
Version:
Visual rule and expression builder for Praxis UI with mini-DSL support, validation and context variables.
1,060 lines (1,030 loc) • 1.03 MB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, Output, Input, ChangeDetectionStrategy, Component, ViewChild, Injectable, Inject } from '@angular/core';
import * as i2 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i1 from '@angular/forms';
import { Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
import * as i6$1 from '@angular/material/card';
import { MatCardModule } from '@angular/material/card';
import * as i6 from '@angular/material/button';
import { MatButtonModule } from '@angular/material/button';
import * as i7 from '@angular/material/icon';
import { MatIconModule } from '@angular/material/icon';
import * as i9$1 from '@angular/material/tabs';
import { MatTabsModule } from '@angular/material/tabs';
import * as i6$2 from '@angular/material/toolbar';
import { MatToolbarModule } from '@angular/material/toolbar';
import * as i11$2 from '@angular/material/sidenav';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import * as i8$1 from '@angular/material/divider';
import { MatDividerModule } from '@angular/material/divider';
import * as i9$2 from '@angular/material/progress-bar';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import * as i1$1 from '@angular/material/snack-bar';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import * as i7$2 from '@angular/material/button-toggle';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import * as i5$1 from '@angular/material/tooltip';
import { MatTooltipModule } from '@angular/material/tooltip';
import * as i1$2 from '@angular/material/dialog';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import * as i13 from '@angular/cdk/drag-drop';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { Subject, Observable, BehaviorSubject, from, debounceTime as debounceTime$1, distinctUntilChanged as distinctUntilChanged$1, takeUntil as takeUntil$1, combineLatest, interval, of, throwError } from 'rxjs';
import * as i7$3 from '@angular/material/menu';
import { MatMenuModule } from '@angular/material/menu';
import * as i4 from '@angular/material/select';
import { MatSelectModule } from '@angular/material/select';
import * as i5 from '@angular/material/input';
import { MatInputModule } from '@angular/material/input';
import * as i3 from '@angular/material/form-field';
import { MatFormFieldModule } from '@angular/material/form-field';
import * as i12 from '@angular/material/chips';
import { MatChipsModule } from '@angular/material/chips';
import * as i11 from '@angular/material/badge';
import { MatBadgeModule } from '@angular/material/badge';
import * as i7$1 from '@angular/material/checkbox';
import { MatCheckboxModule } from '@angular/material/checkbox';
import * as i8 from '@angular/material/datepicker';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatNativeDateModule } from '@angular/material/core';
import * as i12$1 from '@angular/material/autocomplete';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { debounceTime, distinctUntilChanged, takeUntil, startWith, map, switchMap, catchError } from 'rxjs/operators';
import * as i9 from '@angular/material/slider';
import { MatSliderModule } from '@angular/material/slider';
import { PraxisIconDirective } from '@praxisui/core';
import * as i10 from '@angular/material/expansion';
import { MatExpansionModule } from '@angular/material/expansion';
import * as i11$1 from '@angular/material/slide-toggle';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { SpecificationFactory, ComparisonOperator, DslExporter, DslParser, DslValidator, ContextualSpecification, ValidationSeverity as ValidationSeverity$1, ValidationIssueType } from '@praxisui/specification';
import { v4 } from 'uuid';
import * as i12$2 from '@angular/material/progress-spinner';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import * as i13$1 from '@angular/material/stepper';
import { MatStepperModule } from '@angular/material/stepper';
import { ENTER, COMMA } from '@angular/cdk/keycodes';
/**
* Models for the Visual Rule Builder
*/
var RuleNodeType;
(function (RuleNodeType) {
// Basic field comparisons
RuleNodeType["FIELD_CONDITION"] = "fieldCondition";
// Boolean compositions
RuleNodeType["AND_GROUP"] = "andGroup";
RuleNodeType["OR_GROUP"] = "orGroup";
RuleNodeType["NOT_GROUP"] = "notGroup";
RuleNodeType["XOR_GROUP"] = "xorGroup";
RuleNodeType["IMPLIES_GROUP"] = "impliesGroup";
// Conditional validators (Phase 1 Implementation)
RuleNodeType["REQUIRED_IF"] = "requiredIf";
RuleNodeType["VISIBLE_IF"] = "visibleIf";
RuleNodeType["DISABLED_IF"] = "disabledIf";
RuleNodeType["READONLY_IF"] = "readonlyIf";
// Collection validations
RuleNodeType["FOR_EACH"] = "forEach";
RuleNodeType["UNIQUE_BY"] = "uniqueBy";
RuleNodeType["MIN_LENGTH"] = "minLength";
RuleNodeType["MAX_LENGTH"] = "maxLength";
// Optional field handling
RuleNodeType["IF_DEFINED"] = "ifDefined";
RuleNodeType["IF_NOT_NULL"] = "ifNotNull";
RuleNodeType["IF_EXISTS"] = "ifExists";
RuleNodeType["WITH_DEFAULT"] = "withDefault";
// Advanced types
RuleNodeType["FUNCTION_CALL"] = "functionCall";
RuleNodeType["FIELD_TO_FIELD"] = "fieldToField";
RuleNodeType["CONTEXTUAL"] = "contextual";
RuleNodeType["AT_LEAST"] = "atLeast";
RuleNodeType["EXACTLY"] = "exactly";
// Phase 4: Expression and Contextual Support
RuleNodeType["EXPRESSION"] = "expression";
RuleNodeType["CONTEXTUAL_TEMPLATE"] = "contextualTemplate";
// Custom/extensible
RuleNodeType["CUSTOM"] = "custom";
})(RuleNodeType || (RuleNodeType = {}));
/**
* Validator types for conditional validation
*/
var ConditionalValidatorType;
(function (ConditionalValidatorType) {
ConditionalValidatorType["REQUIRED_IF"] = "requiredIf";
ConditionalValidatorType["VISIBLE_IF"] = "visibleIf";
ConditionalValidatorType["DISABLED_IF"] = "disabledIf";
ConditionalValidatorType["READONLY_IF"] = "readonlyIf";
})(ConditionalValidatorType || (ConditionalValidatorType = {}));
/**
* Field schema model for dynamic field configuration in the Visual Builder
*/
var FieldType;
(function (FieldType) {
FieldType["STRING"] = "string";
FieldType["NUMBER"] = "number";
FieldType["INTEGER"] = "integer";
FieldType["BOOLEAN"] = "boolean";
FieldType["DATE"] = "date";
FieldType["DATETIME"] = "datetime";
FieldType["TIME"] = "time";
FieldType["EMAIL"] = "email";
FieldType["URL"] = "url";
FieldType["PHONE"] = "phone";
FieldType["ARRAY"] = "array";
FieldType["OBJECT"] = "object";
FieldType["ENUM"] = "enum";
FieldType["UUID"] = "uuid";
FieldType["JSON"] = "json";
})(FieldType || (FieldType = {}));
/**
* Available comparison operators for each field type
*/
const FIELD_TYPE_OPERATORS = {
[FieldType.STRING]: ['equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'endsWith', 'matches', 'isEmpty', 'isNotEmpty', 'in', 'notIn'],
[FieldType.NUMBER]: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'in', 'notIn', 'between'],
[FieldType.INTEGER]: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'in', 'notIn', 'between'],
[FieldType.BOOLEAN]: ['equals', 'notEquals', 'isTrue', 'isFalse'],
[FieldType.DATE]: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between', 'isNull', 'isNotNull'],
[FieldType.DATETIME]: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between', 'isNull', 'isNotNull'],
[FieldType.TIME]: ['equals', 'notEquals', 'greaterThan', 'greaterThanOrEqual', 'lessThan', 'lessThanOrEqual', 'between'],
[FieldType.EMAIL]: ['equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'endsWith', 'matches', 'isEmpty', 'isNotEmpty'],
[FieldType.URL]: ['equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'endsWith', 'matches', 'isEmpty', 'isNotEmpty'],
[FieldType.PHONE]: ['equals', 'notEquals', 'contains', 'notContains', 'startsWith', 'matches', 'isEmpty', 'isNotEmpty'],
[FieldType.ARRAY]: ['isEmpty', 'isNotEmpty', 'minLength', 'maxLength', 'contains', 'notContains'],
[FieldType.OBJECT]: ['isNull', 'isNotNull', 'hasProperty', 'notHasProperty'],
[FieldType.ENUM]: ['equals', 'notEquals', 'in', 'notIn'],
[FieldType.UUID]: ['equals', 'notEquals', 'matches', 'isEmpty', 'isNotEmpty'],
[FieldType.JSON]: ['isNull', 'isNotNull', 'isEmpty', 'isNotEmpty', 'hasProperty', 'notHasProperty']
};
/**
* Operator display labels for UI
*/
const OPERATOR_LABELS = {
equals: 'equals',
notEquals: 'not equals',
greaterThan: 'greater than',
greaterThanOrEqual: 'greater than or equal',
lessThan: 'less than',
lessThanOrEqual: 'less than or equal',
contains: 'contains',
notContains: 'does not contain',
startsWith: 'starts with',
endsWith: 'ends with',
matches: 'matches pattern',
isEmpty: 'is empty',
isNotEmpty: 'is not empty',
isNull: 'is null',
isNotNull: 'is not null',
isTrue: 'is true',
isFalse: 'is false',
in: 'is in',
notIn: 'is not in',
between: 'is between',
minLength: 'minimum length',
maxLength: 'maximum length',
hasProperty: 'has property',
notHasProperty: 'does not have property'
};
class FieldConditionEditorComponent {
fb;
config = null;
fieldSchemas = {};
configChanged = new EventEmitter();
destroy$ = new Subject();
conditionForm;
fieldCategories = [];
contextVariables = [];
customFunctions = [];
selectedField = null;
selectedOperator = null;
valueType = 'literal';
availableOperators = [];
constructor(fb) {
this.fb = fb;
this.conditionForm = this.createForm();
}
ngOnInit() {
this.setupFieldCategories();
this.setupFormSubscriptions();
this.loadInitialConfig();
}
ngOnChanges(changes) {
if (changes['config'] && !changes['config'].firstChange) {
this.loadInitialConfig();
}
if (changes['fieldSchemas'] && !changes['fieldSchemas'].firstChange) {
this.setupFieldCategories();
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
createForm() {
return this.fb.group({
fieldName: ['', Validators.required],
operator: ['', Validators.required],
value: [''],
valueType: ['literal'],
compareToField: [''],
contextVariable: [''],
functionName: [''],
});
}
setupFormSubscriptions() {
this.conditionForm.valueChanges
.pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$))
.subscribe(() => {
this.emitConfigChange();
});
}
setupFieldCategories() {
const fieldsByCategory = {};
Object.values(this.fieldSchemas).forEach((field) => {
const category = field.uiConfig?.category || 'Other';
if (!fieldsByCategory[category]) {
fieldsByCategory[category] = [];
}
fieldsByCategory[category].push(field);
});
this.fieldCategories = Object.entries(fieldsByCategory)
.map(([name, fields]) => ({
name,
fields: fields.sort((a, b) => a.label.localeCompare(b.label)),
}))
.sort((a, b) => a.name.localeCompare(b.name));
}
loadInitialConfig() {
if (!this.config)
return;
this.conditionForm.patchValue({
fieldName: this.config.fieldName || '',
operator: this.config.operator || '',
value: this.config.value || '',
valueType: this.config.valueType || 'literal',
compareToField: this.config.compareToField || '',
contextVariable: this.config.contextVariable || '',
});
if (this.config.fieldName) {
this.onFieldChanged(this.config.fieldName);
}
const opCtrl = this.conditionForm.get('operator');
if (opCtrl) {
this.selectedOperator = opCtrl.value;
}
}
emitConfigChange() {
if (!this.conditionForm.valid)
return;
const formValue = this.conditionForm.value;
const config = {
type: 'fieldCondition',
fieldName: formValue.fieldName,
operator: formValue.operator,
value: this.processValue(formValue.value),
valueType: formValue.valueType,
compareToField: formValue.compareToField,
contextVariable: formValue.contextVariable,
};
this.configChanged.emit(config);
}
processValue(value) {
if (!value)
return null;
// Handle array operators (in, notIn)
if (this.isArrayOperator() && typeof value === 'string') {
return value
.split(',')
.map((v) => v.trim())
.filter((v) => v.length > 0);
}
// Handle number conversion
if (this.isNumberField() && typeof value === 'string') {
const num = parseFloat(value);
return isNaN(num) ? null : num;
}
return value;
}
// Template methods
getFieldIcon(type) {
const icons = {
string: 'text_fields',
number: 'pin',
integer: 'pin',
boolean: 'toggle_on',
date: 'event',
datetime: 'schedule',
time: 'access_time',
email: 'email',
url: 'link',
phone: 'phone',
array: 'list',
object: 'data_object',
enum: 'list',
uuid: 'fingerprint',
json: 'data_object',
};
return icons[type] || 'text_fields';
}
getOperatorLabel(operator) {
return OPERATOR_LABELS[operator] || operator;
}
getValuePlaceholder() {
if (!this.selectedField)
return 'Enter value';
switch (this.selectedField.type) {
case FieldType.STRING:
case FieldType.EMAIL:
case FieldType.URL:
return 'Enter text value';
case FieldType.NUMBER:
case FieldType.INTEGER:
return 'Enter number';
case FieldType.PHONE:
return '+1234567890';
default:
return 'Enter value';
}
}
getValueHint() {
if (!this.selectedField || !this.selectedOperator)
return '';
if (this.selectedOperator === 'matches') {
return 'Enter a regular expression pattern';
}
if (this.selectedField.format) {
if (this.selectedField.format.minimum !== undefined) {
return `Minimum: ${this.selectedField.format.minimum}`;
}
}
return '';
}
getBooleanLabel() {
return this.selectedOperator === 'isTrue' ? 'True' : 'Value';
}
getCompatibleFields() {
if (!this.selectedField)
return [];
return Object.values(this.fieldSchemas).filter((field) => field.type === this.selectedField?.type &&
field.name !== this.selectedField?.name);
}
getPreviewText() {
const formValue = this.conditionForm.value;
if (!formValue.fieldName || !formValue.operator) {
return 'Incomplete condition';
}
let valueText = '';
if (this.needsValue()) {
switch (formValue.valueType) {
case 'literal':
valueText = this.formatValueForPreview(formValue.value);
break;
case 'field':
valueText = formValue.compareToField || '<field>';
break;
case 'context':
valueText = `\$${formValue.contextVariable || '<variable>'}`;
break;
case 'function':
valueText = `${formValue.functionName || '<function>'}()`;
break;
}
}
const operatorText = this.getOperatorLabel(formValue.operator);
if (valueText) {
return `${formValue.fieldName} ${operatorText} ${valueText}`;
}
else {
return `${formValue.fieldName} ${operatorText}`;
}
}
formatValueForPreview(value) {
if (value === null || value === undefined)
return '<value>';
if (Array.isArray(value))
return `[${value.join(', ')}]`;
if (typeof value === 'string')
return `"${value}"`;
return String(value);
}
// Type checking methods
isStringField() {
return (this.selectedField?.type === FieldType.STRING ||
this.selectedField?.type === FieldType.EMAIL ||
this.selectedField?.type === FieldType.URL ||
this.selectedField?.type === FieldType.PHONE ||
this.selectedField?.type === FieldType.UUID);
}
isNumberField() {
return (this.selectedField?.type === FieldType.NUMBER ||
this.selectedField?.type === FieldType.INTEGER);
}
isBooleanField() {
return this.selectedField?.type === FieldType.BOOLEAN;
}
isDateField() {
return (this.selectedField?.type === FieldType.DATE ||
this.selectedField?.type === FieldType.DATETIME ||
this.selectedField?.type === FieldType.TIME);
}
isEnumField() {
return (this.selectedField?.type === FieldType.ENUM ||
(this.selectedField?.allowedValues?.length ?? 0) > 0);
}
isArrayOperator() {
return this.selectedOperator === 'in' || this.selectedOperator === 'notIn';
}
needsValue() {
const noValueOperators = [
'isEmpty',
'isNotEmpty',
'isNull',
'isNotNull',
'isTrue',
'isFalse',
];
return this.selectedOperator
? !noValueOperators.includes(this.selectedOperator)
: true;
}
// Event handlers
onFieldChanged(event) {
const fieldName = typeof event === 'string' ? event : event.value;
this.selectedField = this.fieldSchemas[fieldName] || null;
if (this.selectedField) {
this.availableOperators =
FIELD_TYPE_OPERATORS[this.selectedField.type] || [];
// Reset operator if not compatible
const currentOperator = this.conditionForm.get('operator')?.value;
if (currentOperator &&
!this.availableOperators.includes(currentOperator)) {
this.conditionForm.patchValue({ operator: '', value: '' });
this.selectedOperator = null;
}
}
else {
this.availableOperators = [];
}
}
onOperatorChanged(event) {
this.selectedOperator = event.value;
// Reset value when operator changes
this.conditionForm.patchValue({ value: '' });
}
onValueTypeChanged(event) {
this.valueType = event.value;
// Reset related fields
this.conditionForm.patchValue({
value: '',
compareToField: '',
contextVariable: '',
functionName: '',
});
}
// Validation methods
hasValidationErrors() {
return this.getValidationErrors().length > 0;
}
getValidationErrors() {
const errors = [];
if (!this.conditionForm.get('fieldName')?.value) {
errors.push('Field is required');
}
if (!this.conditionForm.get('operator')?.value) {
errors.push('Operator is required');
}
if (this.needsValue()) {
const valueType = this.conditionForm.get('valueType')?.value;
switch (valueType) {
case 'literal':
if (!this.conditionForm.get('value')?.value) {
errors.push('Value is required');
}
break;
case 'field':
if (!this.conditionForm.get('compareToField')?.value) {
errors.push('Comparison field is required');
}
break;
case 'context':
if (!this.conditionForm.get('contextVariable')?.value) {
errors.push('Context variable is required');
}
break;
case 'function':
if (!this.conditionForm.get('functionName')?.value) {
errors.push('Function is required');
}
break;
}
}
return errors;
}
isValid() {
return this.conditionForm.valid && !this.hasValidationErrors();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FieldConditionEditorComponent, deps: [{ token: i1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: FieldConditionEditorComponent, isStandalone: true, selector: "praxis-field-condition-editor", inputs: { config: "config", fieldSchemas: "fieldSchemas" }, outputs: { configChanged: "configChanged" }, usesOnChanges: true, ngImport: i0, template: `
<form [formGroup]="conditionForm" class="field-condition-form">
<!-- Field Selection -->
<div class="form-row">
<mat-form-field appearance="outline" class="field-select">
<mat-label>Field</mat-label>
<mat-select
formControlName="fieldName"
(selectionChange)="onFieldChanged($event)"
>
<mat-optgroup
*ngFor="let category of fieldCategories"
[label]="category.name"
>
<mat-option
*ngFor="let field of category.fields"
[value]="field.name"
>
<div class="field-option">
<mat-icon class="field-icon">{{
getFieldIcon(field.type)
}}</mat-icon>
<span class="field-label">{{ field.label }}</span>
<span class="field-type">{{ field.type }}</span>
</div>
</mat-option>
</mat-optgroup>
</mat-select>
<mat-hint *ngIf="selectedField?.description">
{{ selectedField?.description }}
</mat-hint>
</mat-form-field>
</div>
<!-- Operator Selection -->
<div class="form-row" *ngIf="selectedField">
<mat-form-field appearance="outline" class="operator-select">
<mat-label>Operator</mat-label>
<mat-select
formControlName="operator"
(selectionChange)="onOperatorChanged($event)"
>
<mat-option *ngFor="let op of availableOperators" [value]="op">
{{ getOperatorLabel(op) }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Value Input -->
<div
class="form-row"
*ngIf="selectedField && selectedOperator && needsValue()"
>
<div class="value-input-container">
<!-- Value Type Selector -->
<mat-form-field appearance="outline" class="value-type-select">
<mat-label>Value Type</mat-label>
<mat-select
formControlName="valueType"
(selectionChange)="onValueTypeChanged($event)"
>
<mat-option value="literal">Literal Value</mat-option>
<mat-option value="field">Field Reference</mat-option>
<mat-option value="context">Context Variable</mat-option>
<mat-option value="function">Function Call</mat-option>
</mat-select>
</mat-form-field>
<!-- Literal Value Input -->
<div *ngIf="valueType === 'literal'" class="literal-value-input">
<!-- String Input -->
<mat-form-field
*ngIf="isStringField()"
appearance="outline"
class="value-input"
>
<mat-label>Value</mat-label>
<input
matInput
formControlName="value"
[placeholder]="getValuePlaceholder()"
/>
<mat-hint>{{ getValueHint() }}</mat-hint>
</mat-form-field>
<!-- Number Input -->
<mat-form-field
*ngIf="isNumberField()"
appearance="outline"
class="value-input"
>
<mat-label>Value</mat-label>
<input
matInput
type="number"
formControlName="value"
[placeholder]="getValuePlaceholder()"
/>
<mat-hint>{{ getValueHint() }}</mat-hint>
</mat-form-field>
<!-- Boolean Input -->
<div *ngIf="isBooleanField()" class="boolean-input">
<mat-checkbox formControlName="value">
{{ getBooleanLabel() }}
</mat-checkbox>
</div>
<!-- Date Input -->
<mat-form-field
*ngIf="isDateField()"
appearance="outline"
class="value-input"
>
<mat-label>Date</mat-label>
<input
matInput
[matDatepicker]="datePicker"
formControlName="value"
/>
<mat-datepicker-toggle
matIconSuffix
[for]="datePicker"
></mat-datepicker-toggle>
<mat-datepicker #datePicker></mat-datepicker>
</mat-form-field>
<!-- Enum/Select Input -->
<mat-form-field
*ngIf="isEnumField()"
appearance="outline"
class="value-input"
>
<mat-label>Value</mat-label>
<mat-select formControlName="value">
<mat-option
*ngFor="let option of selectedField.allowedValues"
[value]="option.value"
>
{{ option.label }}
</mat-option>
</mat-select>
</mat-form-field>
<!-- Array Input (for 'in' operators) -->
<div *ngIf="isArrayOperator()" class="array-input">
<mat-form-field appearance="outline" class="value-input">
<mat-label>Values (comma separated)</mat-label>
<input
matInput
formControlName="value"
placeholder="value1, value2, value3"
/>
<mat-hint>Enter multiple values separated by commas</mat-hint>
</mat-form-field>
</div>
</div>
<!-- Field Reference Input -->
<mat-form-field
*ngIf="valueType === 'field'"
appearance="outline"
class="value-input"
>
<mat-label>Compare to Field</mat-label>
<mat-select formControlName="compareToField">
<mat-option
*ngFor="let field of getCompatibleFields()"
[value]="field.name"
>
<div class="field-option">
<mat-icon class="field-icon">{{
getFieldIcon(field.type)
}}</mat-icon>
<span class="field-label">{{ field.label }}</span>
</div>
</mat-option>
</mat-select>
</mat-form-field>
<!-- Context Variable Input -->
<mat-form-field
*ngIf="valueType === 'context'"
appearance="outline"
class="value-input"
>
<mat-label>Context Variable</mat-label>
<mat-select formControlName="contextVariable">
<mat-option
*ngFor="let variable of contextVariables"
[value]="variable.name"
>
<div class="context-option">
<span class="variable-name">\${{ variable.name }}</span>
<span class="variable-type">{{ variable.type }}</span>
</div>
</mat-option>
</mat-select>
<mat-hint>Select a dynamic context variable</mat-hint>
</mat-form-field>
<!-- Function Call Input -->
<div *ngIf="valueType === 'function'" class="function-input">
<mat-form-field appearance="outline" class="function-select">
<mat-label>Function</mat-label>
<mat-select formControlName="functionName">
<mat-option
*ngFor="let func of customFunctions"
[value]="func.name"
>
<div class="function-option">
<span class="function-name">{{ func.label }}</span>
<span class="function-desc">{{ func.description }}</span>
</div>
</mat-option>
</mat-select>
</mat-form-field>
<!-- Function parameters would be added here -->
</div>
</div>
</div>
<!-- Validation Messages -->
<div class="validation-messages" *ngIf="hasValidationErrors()">
<div
*ngFor="let error of getValidationErrors()"
class="validation-error"
>
<mat-icon>error</mat-icon>
<span>{{ error }}</span>
</div>
</div>
<!-- Preview -->
<div class="condition-preview" *ngIf="isValid()">
<div class="preview-label">Preview:</div>
<div class="preview-text">{{ getPreviewText() }}</div>
</div>
</form>
`, isInline: true, styles: [".field-condition-form{display:flex;flex-direction:column;gap:16px;min-width:300px}.form-row{display:flex;gap:12px;align-items:flex-start}.field-select,.operator-select,.value-type-select{flex:1;min-width:150px}.value-input-container{display:flex;flex-direction:column;gap:12px;flex:2}.value-input{width:100%}.field-option,.context-option,.function-option{display:flex;align-items:center;gap:8px;width:100%}.field-icon{font-size:16px;width:16px;height:16px;color:var(--md-sys-color-primary)}.field-label{flex:1;font-weight:500}.field-type{font-size:11px;color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container);padding:2px 6px;border-radius:4px}.variable-name{font-family:monospace;font-weight:500;color:var(--md-sys-color-secondary)}.variable-type{font-size:11px;color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container);padding:2px 6px;border-radius:4px}.function-name{font-weight:500;color:var(--md-sys-color-tertiary)}.function-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant);font-style:italic}.boolean-input{display:flex;align-items:center;padding:12px 0}.array-input{width:100%}.function-input{display:flex;flex-direction:column;gap:8px}.function-select{width:100%}.validation-messages{background:var(--md-sys-color-error-container);border-radius:4px;padding:8px 12px}.validation-error{display:flex;align-items:center;gap:6px;color:var(--md-sys-color-on-error-container);font-size:12px;margin-bottom:4px}.validation-error:last-child{margin-bottom:0}.validation-error mat-icon{font-size:14px;width:14px;height:14px}.condition-preview{background:var(--md-sys-color-surface-container);border-radius:8px;padding:12px;border-left:4px solid var(--md-sys-color-primary)}.preview-label{font-size:12px;font-weight:500;color:var(--md-sys-color-on-surface-variant);margin-bottom:4px}.preview-text{font-family:monospace;font-size:14px;color:var(--md-sys-color-on-surface);background:var(--md-sys-color-surface);padding:8px;border-radius:4px;border:1px solid var(--md-sys-color-outline-variant)}@media (max-width: 768px){.form-row{flex-direction:column}.field-select,.operator-select,.value-type-select{width:100%}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i4.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i4.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: i4.MatOptgroup, selector: "mat-optgroup", inputs: ["label", "disabled"], exportAs: ["matOptgroup"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i7.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i7$1.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatDatepickerModule }, { kind: "component", type: i8.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i8.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "component", type: i8.MatDatepickerToggle, selector: "mat-datepicker-toggle", inputs: ["for", "tabIndex", "aria-label", "disabled", "disableRipple"], exportAs: ["matDatepickerToggle"] }, { kind: "ngmodule", type: MatNativeDateModule }, { kind: "ngmodule", type: MatChipsModule }, { kind: "ngmodule", type: MatAutocompleteModule }, { kind: "ngmodule", type: MatTooltipModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FieldConditionEditorComponent, decorators: [{
type: Component,
args: [{ selector: 'praxis-field-condition-editor', standalone: true, imports: [
CommonModule,
ReactiveFormsModule,
MatFormFieldModule,
MatSelectModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatCheckboxModule,
MatDatepickerModule,
MatNativeDateModule,
MatChipsModule,
MatAutocompleteModule,
MatTooltipModule,
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
<form [formGroup]="conditionForm" class="field-condition-form">
<!-- Field Selection -->
<div class="form-row">
<mat-form-field appearance="outline" class="field-select">
<mat-label>Field</mat-label>
<mat-select
formControlName="fieldName"
(selectionChange)="onFieldChanged($event)"
>
<mat-optgroup
*ngFor="let category of fieldCategories"
[label]="category.name"
>
<mat-option
*ngFor="let field of category.fields"
[value]="field.name"
>
<div class="field-option">
<mat-icon class="field-icon">{{
getFieldIcon(field.type)
}}</mat-icon>
<span class="field-label">{{ field.label }}</span>
<span class="field-type">{{ field.type }}</span>
</div>
</mat-option>
</mat-optgroup>
</mat-select>
<mat-hint *ngIf="selectedField?.description">
{{ selectedField?.description }}
</mat-hint>
</mat-form-field>
</div>
<!-- Operator Selection -->
<div class="form-row" *ngIf="selectedField">
<mat-form-field appearance="outline" class="operator-select">
<mat-label>Operator</mat-label>
<mat-select
formControlName="operator"
(selectionChange)="onOperatorChanged($event)"
>
<mat-option *ngFor="let op of availableOperators" [value]="op">
{{ getOperatorLabel(op) }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Value Input -->
<div
class="form-row"
*ngIf="selectedField && selectedOperator && needsValue()"
>
<div class="value-input-container">
<!-- Value Type Selector -->
<mat-form-field appearance="outline" class="value-type-select">
<mat-label>Value Type</mat-label>
<mat-select
formControlName="valueType"
(selectionChange)="onValueTypeChanged($event)"
>
<mat-option value="literal">Literal Value</mat-option>
<mat-option value="field">Field Reference</mat-option>
<mat-option value="context">Context Variable</mat-option>
<mat-option value="function">Function Call</mat-option>
</mat-select>
</mat-form-field>
<!-- Literal Value Input -->
<div *ngIf="valueType === 'literal'" class="literal-value-input">
<!-- String Input -->
<mat-form-field
*ngIf="isStringField()"
appearance="outline"
class="value-input"
>
<mat-label>Value</mat-label>
<input
matInput
formControlName="value"
[placeholder]="getValuePlaceholder()"
/>
<mat-hint>{{ getValueHint() }}</mat-hint>
</mat-form-field>
<!-- Number Input -->
<mat-form-field
*ngIf="isNumberField()"
appearance="outline"
class="value-input"
>
<mat-label>Value</mat-label>
<input
matInput
type="number"
formControlName="value"
[placeholder]="getValuePlaceholder()"
/>
<mat-hint>{{ getValueHint() }}</mat-hint>
</mat-form-field>
<!-- Boolean Input -->
<div *ngIf="isBooleanField()" class="boolean-input">
<mat-checkbox formControlName="value">
{{ getBooleanLabel() }}
</mat-checkbox>
</div>
<!-- Date Input -->
<mat-form-field
*ngIf="isDateField()"
appearance="outline"
class="value-input"
>
<mat-label>Date</mat-label>
<input
matInput
[matDatepicker]="datePicker"
formControlName="value"
/>
<mat-datepicker-toggle
matIconSuffix
[for]="datePicker"
></mat-datepicker-toggle>
<mat-datepicker #datePicker></mat-datepicker>
</mat-form-field>
<!-- Enum/Select Input -->
<mat-form-field
*ngIf="isEnumField()"
appearance="outline"
class="value-input"
>
<mat-label>Value</mat-label>
<mat-select formControlName="value">
<mat-option
*ngFor="let option of selectedField.allowedValues"
[value]="option.value"
>
{{ option.label }}
</mat-option>
</mat-select>
</mat-form-field>
<!-- Array Input (for 'in' operators) -->
<div *ngIf="isArrayOperator()" class="array-input">
<mat-form-field appearance="outline" class="value-input">
<mat-label>Values (comma separated)</mat-label>
<input
matInput
formControlName="value"
placeholder="value1, value2, value3"
/>
<mat-hint>Enter multiple values separated by commas</mat-hint>
</mat-form-field>
</div>
</div>
<!-- Field Reference Input -->
<mat-form-field
*ngIf="valueType === 'field'"
appearance="outline"
class="value-input"
>
<mat-label>Compare to Field</mat-label>
<mat-select formControlName="compareToField">
<mat-option
*ngFor="let field of getCompatibleFields()"
[value]="field.name"
>
<div class="field-option">
<mat-icon class="field-icon">{{
getFieldIcon(field.type)
}}</mat-icon>
<span class="field-label">{{ field.label }}</span>
</div>
</mat-option>
</mat-select>
</mat-form-field>
<!-- Context Variable Input -->
<mat-form-field
*ngIf="valueType === 'context'"
appearance="outline"
class="value-input"
>
<mat-label>Context Variable</mat-label>
<mat-select formControlName="contextVariable">
<mat-option
*ngFor="let variable of contextVariables"
[value]="variable.name"
>
<div class="context-option">
<span class="variable-name">\${{ variable.name }}</span>
<span class="variable-type">{{ variable.type }}</span>
</div>
</mat-option>
</mat-select>
<mat-hint>Select a dynamic context variable</mat-hint>
</mat-form-field>
<!-- Function Call Input -->
<div *ngIf="valueType === 'function'" class="function-input">
<mat-form-field appearance="outline" class="function-select">
<mat-label>Function</mat-label>
<mat-select formControlName="functionName">
<mat-option
*ngFor="let func of customFunctions"
[value]="func.name"
>
<div class="function-option">
<span class="function-name">{{ func.label }}</span>
<span class="function-desc">{{ func.description }}</span>
</div>
</mat-option>
</mat-select>
</mat-form-field>
<!-- Function parameters would be added here -->
</div>
</div>
</div>
<!-- Validation Messages -->
<div class="validation-messages" *ngIf="hasValidationErrors()">
<div
*ngFor="let error of getValidationErrors()"
class="validation-error"
>
<mat-icon>error</mat-icon>
<span>{{ error }}</span>
</div>
</div>
<!-- Preview -->
<div class="condition-preview" *ngIf="isValid()">
<div class="preview-label">Preview:</div>
<div class="preview-text">{{ getPreviewText() }}</div>
</div>
</form>
`, styles: [".field-condition-form{display:flex;flex-direction:column;gap:16px;min-width:300px}.form-row{display:flex;gap:12px;align-items:flex-start}.field-select,.operator-select,.value-type-select{flex:1;min-width:150px}.value-input-container{display:flex;flex-direction:column;gap:12px;flex:2}.value-input{width:100%}.field-option,.context-option,.function-option{display:flex;align-items:center;gap:8px;width:100%}.field-icon{font-size:16px;width:16px;height:16px;color:var(--md-sys-color-primary)}.field-label{flex:1;font-weight:500}.field-type{font-size:11px;color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container);padding:2px 6px;border-radius:4px}.variable-name{font-family:monospace;font-weight:500;color:var(--md-sys-color-secondary)}.variable-type{font-size:11px;color:var(--md-sys-color-on-surface-variant);background:var(--md-sys-color-surface-container);padding:2px 6px;border-radius:4px}.function-name{font-weight:500;color:var(--md-sys-color-tertiary)}.function-desc{font-size:12px;color:var(--md-sys-color-on-surface-variant);font-style:italic}.boolean-input{display:flex;align-items:center;padding:12px 0}.array-input{width:100%}.function-input{display:flex;flex-direction:column;gap:8px}.function-select{width:100%}.validation-messages{background:var(--md-sys-color-error-container);border-radius:4px;padding:8px 12px}.validation-error{display:flex;align-items:center;gap:6px;color:var(--md-sys-color-on-error-container);font-size:12px;margin-bottom:4px}.validation-error:last-child{margin-bottom:0}.validation-error mat-icon{font-size:14px;width:14px;height:14px}.condition-preview{background:var(--md-sys-color-surface-container);border-radius:8px;padding:12px;border-left:4px solid var(--md-sys-color-primary)}.preview-label{font-size:12px;font-weight:500;color:var(--md-sys-color-on-surface-variant);margin-bottom:4px}.preview-text{font-family:monospace;font-size:14px;color:var(--md-sys-color-on-surface);background:var(--md-sys-color-surface);padding:8px;border-radius:4px;border:1px solid var(--md-sys-color-outline-variant)}@media (max-width: 768px){.form-row{flex-direction:column}.field-select,.operator-select,.value-type-select{width:100%}}\n"] }]
}], ctorParameters: () => [{ type: i1.FormBuilder }], propDecorators: { config: [{
type: Input
}], fieldSchemas: [{
type: Input
}], configChanged: [{
type: Output
}] } });
class ConditionalValidatorEditorComponent {
fb;
config = null;
fieldSchemas = {};
configChanged = new EventEmitter();
destroy$ = new Subject();
validatorForm;
fieldCategories = [];
advancedConditions = [];
validatorType = '';
targetField = '';
conditionMode = 'simple';
get showDisabledMessage() {
return this.validatorForm.get('showDisabledMessage')?.value || false;
}
constructor(fb) {
this.fb = fb;
this.validatorForm = this.createForm();
}
ngOnInit() {
this.setupFieldCategories();
this.setupFormSubscriptions();
this.loadInitialConfig()