UNPKG

@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
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()